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 danssrc/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.
| Cron | Cadence | Ce qu'il fait | Ce qu'il ne fait PAS |
|---|---|---|---|
bulletin-radar | horaire | Remplit la memoire Ecosysteme depuis les flux de sources primaires | Ne compose jamais, n'envoie jamais |
bulletin-weekly | lundi | Compose l'edition Ecosysteme depuis les Signaux | N'envoie pas — produit une Demande |
bulletin-store-weekly | lundi | Compose l'edition par boutique suivie depuis le graphe d'intelligence. Calcul une fois par store, personnalise par abonnement | N'envoie pas |
bulletin-dispatch | 15 min | Envoie les Demandes approuvees | Ne 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 :
| Tier | Score | Confiance | Nature de la preuve |
|---|---|---|---|
vendor | 62 | 92 | declared — l'editeur parle de son propre produit |
press | 55 | 68 | reported — 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
200et 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 debacklog/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
| Table | Role |
|---|---|
BulletinRequest | Une 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 |
BulletinContact | Le destinataire |
BulletinSubscription | L'abonnement — c'est lui qui personnalise une edition store deja calculee |
GrowthUnit | Une verite verifiee, distribuable. Les rendus selectionnent dedans, ils n'y ajoutent jamais rien |
ContentRender | Un rendu d'une unite sur un canal. Publie exige une URL publique observee plus une preuve |
AttributionEvent | Attribution 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 » :deriveRendersne 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.
| Surface | Etat | Ce 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/0103eut ecritradar-tools.tsetgrowth-web/0098la route de lecture Growth ; il nommait un panel/admin/operations/bulletinqui n'existe pas, le repertoire estadmin/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. Voircommerce-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 :
| Methode | Ce qu'elle fait |
|---|---|
GET | rend une page avec un bouton. Ne change rien. |
POST | depense 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.tscontient 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
| Chemin | Ce qu'il porte |
|---|---|
src/features/bulletin/radar-sources.ts | Le registre des 12 sources, leurs beats et leurs tiers |
src/features/bulletin/store-radar.ts | Le radar par boutique |
src/services/bulletin/radar-global.ts | La collecte, et le signalement d'une source injoignable |
src/services/bulletin/compose-weekly.ts | La composition Ecosysteme |
src/services/bulletin/compose-store-weekly.ts | La composition par boutique |
src/services/bulletin/dispatch.ts | L'envoi des Demandes approuvees |
src/services/bulletin/health.ts | La sante — ce qui aurait du partir et n'est pas parti |
src/features/growth/growth-kinds.ts | Canaux, objectifs de CTA, libelles |
src/features/growth/handoff.ts | Ce qu'un operateur emporte pour publier a la main |
src/features/growth/authorship.ts | Le contrat d'auteur — il refuse, il ne remplit jamais |
src/services/growth/plan-import.ts | L'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.