Audit — la regle des quatre portes, sur tous les systemes
Sortie de inbox/0085, decoupe 6/6 de inbox/0073. Lecture seule sur le code. Ce document, scripts/four-doors.mjs et les items de backlog qu'il ouvre sont la sortie complete. Etat du depot : branche…
Sortie de
inbox/0085, decoupe 6/6 deinbox/0073.Lecture seule sur le code. Ce document,
scripts/four-doors.mjset les items de backlog qu'il ouvre sont la sortie complete.Etat du depot : branche
claude/notion-hub-access-9kqa19, 28 aout 2026.
La regle mesuree
Tout ce qui vit dans la codebase doit etre pilotable a la fois via le MCP, via l'API, via le Chat cote utilisateur, et via le panel Admin/Team cote agence, et rien ne doit pourrir dans un coin sans etre mis en avant, utile, ou relie.
0073 l'avait mesuree sur deux systemes. Celui-ci la mesure sur
vingt-quatre.
Le tableau
Genere par node scripts/four-doors.mjs. Il n'est pas recopie a la main :
si un chiffre ci-dessous ne correspond plus, c'est la commande qui fait foi.
system panel cron api chat mcp
────────────────────────────────────────────────────
studio 9 1 1 ✓ ·
radar-bulletin 2 4 5 · ·
growth 1 2 · · ·
intelligence 8 19 33 ✓ ·
marketplace 28 2 45 · ·
tracking 3 1 5 ag ·
vitals 1 4 6 ✓ ·
aeo 1 2 · ✓ ·
cro 1 · · ✓ ·
aov · 1 · ✓ ·
commerce · 3 · ✓ ·
action-plan · · · ✓ ·
status 1 2 3 · ·
knowledge · · 2 ✓ ·
theme · · 1 ag 1
connectors 4 2 28 ✓ ·
store-core 19 1 40 ✓ 6
organizations 30 · 21 ✓ ·
billing 9 7 9 · ·
agents 4 · 4 · ·
workflow 2 1 4 · ·
memory 3 1 5 · ·
browser · 1 7 ag ·
chat-runtime 3 · 15 · ·
────────────────────────────────────────────────────
counted: 141 panel pages, 58 crons, 306 api routes (crons excluded),
19 registered tool factories, 7 mcp scopes
· case vide. ag porte chat existante uniquement dans la couche
agent : une boutique connectee sans agent complet ne l'a pas.
Ce bloc est le releve du jour de l'audit, laisse tel quel : c'est la
preuve des neuf items ci-dessous, et la reecrire au fil des livraisons
effacerait la raison pour laquelle ils existent. Pour l'etat courant,
relancer la commande — les cases livrees depuis (memory, agents,
workflow, marketplace, growth et radar-bulletin en chat,
knowledge et aov en panel) y apparaissent pleines.
Trois cases de ce releve etaient fausses le jour meme, et pas parce
que le compte etait mal fait : aov, commerce et action-plan
portaient panel: [] dans SYSTEMS, une affirmation ecrite a la main.
commerce avait [orgSlug]/[storeSlug]/insights et action-plan
[orgSlug]/[storeSlug]/intelligence depuis le debut. Le second garde-fou
decrit plus bas ne pouvait pas le voir : store-core reclame le prefixe
[orgSlug]/[storeSlug], donc aucune page de boutique n'est jamais « non
reclamee ». Les declarations sont corrigees ; l'angle mort qui les a
laissees fausses est ouvert en
0110.
La methode, et pourquoi elle n'est pas un grep
0085 posait la contrainte principale : deriver, ne pas grepper un mot,
parce que intelligence-tools.ts dit « radar » deux fois au sens
metaphorique. Chaque colonne est donc comptee sur une structure, jamais sur
un nom :
| Colonne | Comptee sur |
|---|---|
| panel | les page.tsx sous src/app/(dashboard)/ |
| cron | les entrees crons de vercel.json |
| api | les route.ts sous src/app/api/, hors cron/ |
| chat | les fabriques que createAtlasTools appelle, dans src/features/ai/tools/index.ts |
| mcp | les ids de MCP_SCOPES |
Une seule chose est ecrite a la main : le tableau SYSTEMS, qui dit ce que
chaque systeme est — ses prefixes de routes, ses prefixes de cron, ses
fabriques d'outils, ses pages de panel, ses scopes. C'est une affirmation
sur l'architecture, aucun script ne peut la deriver.
Trois choix de mesure ont change le resultat, et meritent d'etre lus :
La porte chat est le registre, pas le repertoire. Un fichier dans
src/features/ai/tools/ est un module d'outils le jour ou quelqu'un
l'ecrit ; il est une porte le jour ou createAtlasTools l'etale. Compter
les fichiers aurait donne types.ts, fence.ts, permissions.ts et
studio-tools.test.ts pour des portes — et aurait continue a compter un
module qu'on aurait debranche, ce qui est exactement l'etat que cet
audit existe pour trouver. Le compte passe ainsi de 21 fichiers a 19
fabriques reellement enregistrees.
Un cron appartient a un systeme par prefixe de chemin complet. La
premiere version coupait le dernier segment sur -, ce qui rangeait
intelligence/refresh-hot dans une famille « refresh ». Treize crons
d'intelligence n'appartenaient alors a personne : la colonne affichait 4 au
lieu de 19.
La colonne MCP a d'abord produit un faux positif sur son propre auteur.
Le premier jet appariait un scope a un systeme par sous-chaine : theme
attrapait boostecom:themes.read. C'est litteralement le piege que 0085
decrit, reproduit dans le script cense le mesurer. Verification faite,
l'appariement etait juste par accident — boostecom:themes.read ouvre
listThemes, getTheme, runAudit, qui sont bien les outils de
theme-tools.ts — mais un resultat juste par accident n'est pas un
resultat. Les scopes sont desormais declares systeme par systeme.
Deux garde-fous, qui sont le fond du script
Un script d'audit ment de deux facons, et les deux sont silencieuses.
Une affirmation qui ne correspond a rien est signalee, jamais comptee zero. Un prefixe devenu obsolete afficherait une case vide, une case vide se lit comme un manque, et l'audit fabriquerait du travail a partir de sa propre faute de frappe. Le script sort en 1 sur ce cas.
Ce que personne ne reclame est affiche. Sans quoi le tableau est un
sous-ensemble flatteur : on obtient toujours un bon score sur les systemes
qu'on a pense a lister. C'est ce garde-fou qui a trouve les quatre derniers
systemes du tableau — agents, workflow, memory, browser —, absents
de ma premiere redaction a la main.
Ce qui reste non reclame aujourd'hui est de la plomberie assumee :
weekly-digest, prune-audit-log, cleanup-drafts, weekly-audits cote
cron, et une cinquantaine de routes d'infrastructure (auth/**,
webhooks/**, health/**, upload, logs, security/csp-report…). Une
route d'authentification n'a pas a etre un systeme pilotable ; elle est
listee pour que le tableau ne devienne pas un sous-ensemble sans qu'on le
voie.
Verifie par mutation, et deliberement hors CI
Un script d'audit dont personne n'a essaye de casser les garde-fous est un script d'audit qui affirme. Cinq mutations, cinq observees :
| Mutation | Ce qui doit arriver | Observe |
|---|---|---|
| un prefixe d'API devenu obsolete | BROKEN, sortie 1 | sortie 1 |
| une fabrique d'outils jamais enregistree | BROKEN, sortie 1 | sortie 1 |
le prefixe de cron intelligence retire | la colonne tombe et les 14 crons orphelins apparaissent | 19 -> 5, les 14 listes |
l'exclusion de createAtlasTools retiree | la fonction se compte elle-meme | 19 -> 20, listee non reclamee |
le systeme browser retire du tableau | son cron et sa fabrique apparaissent non reclames | les deux listes |
Les trois dernieres sont celles qui comptent : elles prouvent que le rapport « non reclame » voit reellement ce que le tableau cacherait. La troisieme est exactement le bug qu'avait la premiere version du script.
Ce script n'est pas branche sur la CI, volontairement. Les autres
documents de docs/audits/ sont des instantanes dates, pas des contrats :
c'est pnpm docs:claims qui tient les affirmations vivantes de
CLAUDE.md. Brancher celui-ci ferait d'une route supprimee un echec de
build au nom d'une photo prise le 28 aout. La commande se relance a la
demande, et elle sort en 1 quand sa carte ment — ce qui est la garantie
utile.
Les cases vides legitimes
0085 etait explicite : « une case vide n'est pas automatiquement un
manque […] sinon il fabrique douze items sans valeur. » Voici celles qui
n'en sont pas.
Toute la colonne MCP — un seul fait, pas quinze items
L'en-tete de src/lib/security/mcp-scopes.ts le dit sans ambiguite : le
catalogue est « un nom lisible pour un groupe d'outils qui partagent deja
une exigence de scope Shopify », avec la Custom App du marchand pour
plafond. Les sept scopes gouvernent les douze outils du relais Shopify.
Aucun systeme dont BoostEcom est proprietaire n'est joignable en MCP : ni Studio, ni Radar, ni Marketplace, ni Intelligence, ni Tracking, ni Vitals, ni AEO, CRO, AOV, Commerce, Knowledge, Memory, Workflow.
C'est un fait d'architecture, pas quinze manques. Il a deja son item —
integrations/0083, que
growth-web/0091
nomme comme son bloqueur. Quinze items « ajouter les scopes MCP de X »
seraient exactement les douze items sans valeur contre lesquels 0085
previent, avec un chapeau different.
La colonne cron n'est pas une porte
Elle vient de 0073 et elle est utile pour situer un systeme, mais la
regle enonce quatre portes : MCP, API, Chat, Panel. « Pas de cron » veut
dire « rien de recurrent a faire », ce qui est une propriete du systeme,
pas un manque. Aucun item n'est ouvert pour une case cron vide.
Trois portes chat volontairement absentes
- status — une page d'etat publique et sa console d'incidents. Qu'un marchand demande a son assistant comment va notre plateforme n'est pas un besoin produit, et la reponse est deja a une page non authentifiee.
- billing — depenser de l'argent dans un tour de chat est la seule action que les garde-fous de credits existent pour garder deliberee : l'estimation pre-stream et le plafond dur mid-stream supposent tous deux que le tour n'est pas ce qui achete les credits.
- chat-runtime — c'est la porte. Un outil qui pilote le chat depuis le chat est une boucle, pas une surface.
Le panel absent de browser
Les outils navigateur sont une primitive du cockpit, pas une destination :
les routes preview/browserbase/** sont consommees par le workspace du
store, qui est leur ecran. Un ecran « sessions de navigateur » n'aurait
d'audience que nous.
Les cases vides reelles
Neuf items, en groupant les cases dont la decision est la meme decision plutot qu'en en recopiant une par cellule.
| # | Case vide | Pilier | Item |
|---|---|---|---|
| 1 | chat — radar + growth | ai-platform | 0093 la primitive, puis 0103 les portes |
| 2 | chat — marketplace | ai-platform | 0094 |
| 3 | chat — memory | ai-platform | 0095 |
| 4 | chat — agents + workflow | ai-platform | 0096 |
| 5 | panel — knowledge | ai-platform | 0097 |
| 6 | api — growth | growth-web | 0098 |
| 7 | api — aeo, cro, aov, commerce | commerce-systems | 0099 |
| 8 | panel — aov seul | commerce-systems | 0100, et 0110 pour la cause |
| 9 | panel — theme | app-shell | 0101 |
Deux observations que le tableau rend visibles et qu'aucune lecture ne donnait :
Cinq cases chat vides, une seule cause pour deux d'entre elles.
0093 a livre cette cause le jour meme ; les deux cases restent vides
jusqu'a 0103, qui ecrit les outils. Une ligne de ce tableau nomme donc
deux items la ou les autres en nomment un : la primitive et la porte ne
sont pas le meme travail, et les confondre ferait disparaitre le second.
AtlasToolContext.userRole vaut owner | admin | member | viewer : c'est
le role dans l'organisation, pas User.role === "ADMIN". La couche
outils n'a aucune notion d'admin plateforme. Radar et Growth sont globaux,
donc leur porte chat n'est pas un travail d'ecriture d'outils — elle attend
cette primitive. Marketplace, Memory, Agents et Workflow sont scopes par
organisation et ne l'attendent pas : c'est pour cela qu'ils sont des items
distincts et non des dependances du meme.
Quatre systemes de boutique n'ont aucune surface HTTP. AEO, CRO, AOV et
Commerce ont une porte chat et, pour deux d'entre eux, une page. Leurs
freres immediats en ont une : vitals a six routes, tracking cinq. Cette
asymetrie entre systemes de meme nature est la preuve, pas le raisonnement.
Trois systemes ont une porte chat et rien a regarder. AOV, Commerce et Action Plan : le marchand peut demander a Atlas son panier moyen, sa retention par cohorte, sa liste priorisee d'actions — et n'a aucun ecran ou la revoir. Action Plan est le cas le plus net : « quoi faire ensuite » est la synthese que la plateforme vend, et elle n'existe que dans un tour de chat qui defile.
Ce que cet audit ne dit pas
Il mesure l'existence d'une porte, jamais sa qualite. Une porte API qui
rend un objet pauvre compte comme une porte. C'est la meme frontiere
assumee que src/test/api-authorization-coverage.test.ts : prouver qu'une
route a un garde n'est pas prouver que le garde refuse.
0097 en est la demonstration. La case panel — knowledge etait vide et
elle avait raison de l'etre : la page de route que le composant decrivait
dans son propre docstring n'avait jamais ete commitee, et le seul acces aux
sources de connaissance etait un tiroir ouvert depuis un noeud de canvas.
Mais le fait interessant etait invisible pour un tableau qui compte des
portes — ce tiroir inventait ses reponses. Ajouter une source ne
touchait jamais l'API ; « synchroniser » ecrivait indexed: true a cote
d'un handler dont le commentaire disait « for v0.0.1: mark as indexed » et
qui n'embarquait rien ; supprimer retirait la ligne en laissant ses chunks
interrogeables par @Atlas, deleteKnowledgeSource n'ayant aucun appelant
malgre un docstring affirmant le contraire. Une porte qui ment compte comme
une porte.
Il ne mesure pas non plus la seconde moitie de la regle — « rien ne doit pourrir dans un coin sans etre mis en avant, utile, ou relie ». Un systeme peut avoir ses quatre portes et rester introuvable dans la navigation.