ArchitectureLe Radar — de la source au lecteur

Le Radar — de la source au lecteur

Comment une information entre dans la memoire editoriale, comment elle en sort, et par ou on pilote tout ca. Ce fichier est la carte du systeme Radar / Bulletin. Le pendant Studio est studio-agency-os.md ; les…

Comment une information entre dans la memoire editoriale, comment elle en sort, et par ou on pilote tout ca.

Ce fichier est la carte du systeme Radar / Bulletin. Le pendant Studio est studio-agency-os.md ; les composants growth qui s'appuient dessus vivent dans src/features/growth/.


1. Le principe, en une phrase

Trois responsabilites, jamais confondues : l'un se souvient, l'autre decide quoi dire, le troisieme decide si ca peut partir. Quatre crons les portent (cf. §2) : la composition en compte deux, une par edition. Cette ligne a annonce « trois crons » pendant que la section suivante en listait quatre.

Aucun ne peut faire le travail d'un autre, et c'est deliberе : une heure de collecte ne doit jamais pouvoir atteindre un lecteur toute seule.

sources           bulletin-radar        bulletin-weekly      bulletin-dispatch
(12 flux)  ─────→  collecte      ─────→  compose      ─────→  envoie
                   → etat NEW            → Demande             → apres
                     (non publiable)       a approuver           approbation
                   horaire               lundi                 toutes les 15 min

La barriere est un etat, pas une convention : tout ce que la collecte ecrit entre en NEW, et isPublishable refuse cet etat. Un flux qui s'emballe ne peut donc pas produire un envoi.


2. Les quatre crons

Derives de vercel.json — si ce tableau et le fichier divergent, le fichier a raison.

CronCadenceCe qu'il faitCe qu'il ne fait PAS
bulletin-radarhoraireRemplit la memoire Ecosysteme depuis les flux de sources primairesNe compose jamais, n'envoie jamais
bulletin-weeklylundiCompose l'edition Ecosysteme depuis les SignauxN'envoie pas — produit une Demande
bulletin-store-weeklylundiCompose l'edition par boutique suivie depuis le graphe d'intelligence. Calcul une fois par store, personnalise par abonnementN'envoie pas
bulletin-dispatch15 minEnvoie les Demandes approuveesNe compose pas, ne s'auto-approuve pas

Un cinquieme, growth-attribution, reconcilie l'attribution observee (inscriptions confirmees, envois reels) : quotidien.

Une source injoignable est signalee, pas avalee. Le compte atterrit dans l'issue du run, donc CronExecution le porte : c'est ce qui fait qu'un flux mort devient visible au lieu de se lire comme une semaine calme.


3. Les sources

12 sources declarees (6 verifiees, 6 jamais vues repondre : unverifiedSources()) dans src/features/bulletin/radar-sources.ts. Ce paragraphe a annonce « 14 » pendant plusieurs livraisons : le registre en a toujours contenu 12, et ce 14 se confondait avec les 14 familles d'angles nommees juste en dessous.

Chaque source porte un beat : le domaine que l'editeur couvre, dans le vocabulaire de la doctrine. Ce n'est pas une etiquette libre : declarer une source, c'est declarer ce qu'elle publie reellement, et la garde de couverture editoriale compare les beats declares aux 14 familles d'angles.

Deux tiers, avec des poids distincts :

TierScoreConfianceNature de la preuve
vendor6292declared — l'editeur parle de son propre produit
press5568reported — un tiers rapporte

Un vendeur qui annonce son changelog est une preuve declaree : fiable sur le fait, nulle sur le jugement. La presse est moins fiable sur le fait et plus utile sur le contexte. Les deux poids encodent ca.

Limite connue, et elle est humaine. Une source peut repondre 200 et publier autre chose que son beat declare. Aucune machine de ce depot ne peut l'observer : seul un lecteur le peut. C'est l'objet de backlog/growth-web/0048, et c'est pour ca qu'il est bloque sur une verification humaine et pas sur du code.


4. Le modele de donnees

TableRole
BulletinRequestUne demande d'envoi. type et scope sont des String, pas des enums : la doctrine gagne des types plus vite qu'un enum Postgres ne veut migrer, et les valeurs sont validees au bord
BulletinContactLe destinataire
BulletinSubscriptionL'abonnement — c'est lui qui personnalise une edition store deja calculee
GrowthUnitUne verite verifiee, distribuable. Les rendus selectionnent dedans, ils n'y ajoutent jamais rien
ContentRenderUn rendu d'une unite sur un canal. Publie exige une URL publique observee plus une preuve
AttributionEventAttribution observee uniquement. Il n'y a pas de type impression : rien ici ne sait en observer une

Les six canaux de rendu : email, article, linkedin, x, instagram, facebook (src/features/growth/growth-kinds.ts).

Un seul a une sortie cablee : email, via le Bulletin. Les cinq autres sortent dans le presse-papier d'une personne, c'est le handoff manuel, et c'est la route de production supportee, pas un contournement.

4bis. Le plan de contenu entre par un import, pas par la saisie

Le plan Q4 (47 unites, 238 brouillons Postiz) est importe depuis /admin/content/growth par importContentPlan, qui appelle importPlanUnits (src/services/growth/plan-import.ts) sur le manifeste src/services/growth/plan-2026-q4-units.json, genere par $HOME/.claude/skills/boostecom-content/scripts/plan-src/build.mjs.

  • La cle est dedupeKey, deja unique : plan:<campagne>:<unite>. Aucune colonne ajoutee. Relance, l'import ne cree rien et rend « creees / deja presentes ».
  • Il n'ecrit que ce que le plan porte : les faits et leurs sources, resolus depuis references/facts.md. Le reste garde le defaut de colonne (preuve moderee, scores a 50, originalite a 0, pas d'audience, pas de date observee). Avec ces defauts le score vaut environ 45, donc « hold » : deriveRenders ne propose aucun canal, et c'est voulu tant que personne n'a juge ces unites.
  • Une unite sans these est sautee et nommee, jamais remplie. Les posts du plan dont l'unite n'existe pas apres l'import sont listes.

Les ids rendus remplissent references/growth-unit-ids.json, que build.mjs injecte dans utm_content : c'est ce qui permet a unitFromToken() de crediter une inscription a l'unite. Procedure complete : $HOME/.claude/skills/boostecom-content/references/postiz.md §4.


5. Par ou on pilote: l'etat reel

C'est la question que ce document existe pour repondre, et la reponse est plus etroite qu'on ne l'imagine.

SurfaceEtatCe qui existe
Panel admin✅/admin/content/bulletin et /admin/content/bulletin/health — composition, approbation, sante des flux
Cron✅Les quatre ci-dessus, authentifies par withCronAuth
API✅GET /api/admin/bulletin/[section], GET /api/admin/growth/[section], plus les trois routes publiques du double opt-in (subscribe, confirm, unsubscribe) — voir 5bis pour la forme exacte des deux dernieres
Chat / Atlas✅src/features/ai/tools/radar-tools.ts, enregistre par createAtlasTools
MCP❌Absent du catalogue de lib/security/mcp-scopes.ts

Ce tableau a menti pendant deux livraisons. Il annonçait encore « aucun outil IA n'expose le Radar » et « une seule route API » apres que ai-platform/0103 eut ecrit radar-tools.ts et growth-web/0098 la route de lecture Growth ; il nommait un panel /admin/operations/bulletin qui n'existe pas, le repertoire est admin/content/. Un tableau d'etat qu'on ne rejoue pas devient une liste de travaux deja faits, et c'est la forme de derive la plus chere : elle fabrique des items. Voir commerce-systems/0100, ouvert sur trois ecrans dont deux existaient.

L'etat courant se derive : node scripts/four-doors.mjs.

5bis. Le clic de confirmation est un clic humain

confirm et unsubscribe ont deux methodes, et la difference est la propriete que le double opt-in achete :

MethodeCe qu'elle fait
GETrend une page avec un bouton. Ne change rien.
POSTdepense le token : confirme, ou retire le consentement

La raison est operationnelle, pas theorique. Les suites de securite mail d'entreprise (Microsoft Safe Links, et toute fonction « previsualiser le lien ») recuperent les URL d'un message avant qu'un humain les voie. Avec la mutation sur le GET, c'est une MACHINE qui confirmait, ce qui vide la seconde etape de son sens ; et sur un radar de boutique, confirm() declenche triggerStoreIndex, donc le scanner declenchait aussi un scan facture. Le commentaire de confirm() nomme ce risque depuis le debut : « a scan costs real money ». Le GET etait la porte qu'il laissait ouverte.

Le POST reste le point d'entree RFC 8058 one-click (List-Unsubscribe-Post: List-Unsubscribe=One-Click) : le client mail du lecteur appelle ce meme handler, et lui demander une confirmation ferait de nous un expediteur casse.

Les deux pages parlent la langue du lecteur. La locale se resout dans cet ordre : l'adresse du token via resolveEmailLocale (donc la meme que celle de l'email qui amene ici), puis le cookie NEXT_LOCALE, puis en. La copie vit dans email.landing.* des six catalogues, un namespace que server-only garde hors de tout bundle navigateur. Elles etaient en francais tutoye et lang="fr" pour tout le monde jusqu'a growth-web/0305.

Un token inconnu, un token deja depense et un clic repete rendent la MEME page, a l'octet pres : sinon l'endpoint devient un moyen de demander si une adresse est abonnee.


Un piege de lecture, et il m'a failli m'avoir. intelligence-tools.ts contient deux fois le mot « radar » : dans des descriptions d'outils, au sens metaphorique (« the radar that answers "what's about to pop?" »). Ce sont des outils d'Intelligence, sans rapport avec ce systeme. Chercher le mot donne une reponse fausse ; chercher la structure donne la bonne.


6. L'ecart avec la cible

La cible enoncee : tout ce qui vit dans la codebase doit etre pilotable depuis le MCP, l'API, le Chat cote utilisateur, et le panel cote agence, et rien ne doit pourrir dans un coin sans etre relie.

Le Radar en couvre trois sur quatre. Il est operable par une personne devant le panel admin, par l'horloge, par un agent depuis un tour de chat, et par un programme tiers via les routes de lecture. Il ne l'est pas depuis MCP.

Ce trou-la a passe un moment sans proprietaire, et c'est le point a retenir de cette section. growth-web/0091 a ete archive avec sa case « les scopes MCP couvrent le Radar » non cochee, bloquee par integrations/0083 ; 0083 a livre le Studio et a ecrit que « le precedent qu'il pose vaudra pour le Radar (growth-web/0091 l'attend), l'Intelligence, le Marketplace ». Personne ne l'a repris pendant deux livraisons. Une case non cochee sur un item archive est invisible : elle ne figure dans aucune file.

Il en a un depuis : integrations/0112, et il est blocked a dessein. Ajouter boostecom:radar.read au catalogue par analogie avec 0083 serait la mauvaise correction : le Radar est gate par requirePlatformAdmin, et les trois surfaces MCP du depot sont toutes tenant (api/mcp/[storeId] par store, api/mcp/intelligence par organisation) ou publiques. Le scope apparaitrait sur l'ecran de consentement de chaque marchand sans qu'aucun puisse le satisfaire, et poserait une surface admin-plateforme derriere un grant par boutique. Ce qui manque n'est pas une entree de catalogue, c'est une decision : ou vit une surface MCP authentifiee non-tenant, ou l'ecrit-on qu'il n'y en aura pas.

Ce document decrit ce qui tourne, pas ce qui est vise. Le reste de l'ecart est suivi ailleurs : voir §7.


7. Fichiers

CheminCe qu'il porte
src/features/bulletin/radar-sources.tsLe registre des 12 sources, leurs beats et leurs tiers
src/features/bulletin/store-radar.tsLe radar par boutique
src/services/bulletin/radar-global.tsLa collecte, et le signalement d'une source injoignable
src/services/bulletin/compose-weekly.tsLa composition Ecosysteme
src/services/bulletin/compose-store-weekly.tsLa composition par boutique
src/services/bulletin/dispatch.tsL'envoi des Demandes approuvees
src/services/bulletin/health.tsLa sante — ce qui aurait du partir et n'est pas parti
src/features/growth/growth-kinds.tsCanaux, objectifs de CTA, libelles
src/features/growth/handoff.tsCe qu'un operateur emporte pour publier a la main
src/features/growth/authorship.tsLe contrat d'auteur — il refuse, il ne remplit jamais
src/services/growth/plan-import.tsL'import idempotent du plan de contenu en unites (§4bis)

Chacun de ces modules a ses tests a cote. La sante du systeme se lit sur /admin/content/bulletin/health, jamais dans ce fichier : un document ne sait pas si un flux est mort.