Audits · septembre 2026Audit — pilier ai-platform

Audit — pilier ai-platform

Semaine du 8 au 14 septembre 2026, tour ai-platform de la rotation docs/team/roster.md. Le pilier precedemment audite a ce titre l'a ete le 25 aout (2026-08-25-ai-platform.md), soit 17 jours. Lecture seule sur le code…

Semaine du 8 au 14 septembre 2026, tour ai-platform de la rotation docs/team/roster.md. Le pilier precedemment audite a ce titre l'a ete le 25 aout (2026-08-25-ai-platform.md), soit 17 jours.

Lecture seule sur le code. La sortie complete de ce tour est ce fichier plus huit items de backlog. Aucune ligne de src/ n'a ete modifiee.

Périmètre

Commit audite : 906b716c (origin/main, 2026-09-11 03:5x UTC). Les huit constats ont ete re-verifies sur ce sha apres que origin/main a avance pendant la passe.

Perimetre lu (fiche @ai-platform-engineer, roster.md + les 26 chemins de ownership.json) :

CheminFichiersLignes TS/TSX
src/features/ai/**52192 594
src/app/api/chat|agents|workflow|sidekick|tasks|branches|evi|registry/**446 368
src/services/agents|agent-autonomy|agent-ledger|ai-models|skills|knowledge/**233 742
src/features/sidekick/**, src/lib/workflows|presence/**171 616
evals/** (+ vitest.evals.config.ts)5 (+1)665
docs/architecture/agentic-stack|memory-layer|atlas-channels.md, docs/canvas/**13—

Soit 104 985 lignes, mesurees sur 906b716c — un chiffre qui aura bouge avant que vous ne lisiez ceci, et c'est le sujet : store-history-tool.ts est arrive dans ce perimetre pendant la passe (cf. P2-5). Une fraction a ete lue, et la section « Ce qui n'a pas ete couvert » dit laquelle.

Ce qui a ete passe en revue exhaustivement

SondeResultat
maxSteps dans le perimetre0
toDataStreamResponse()0
generateObject / streamObject (hors commentaires et tests)0
useChat({ api })0
framer-motion0
Imports directs @ai-sdk/anthropic|openai|google3, tous provider.tools.* — voir « Non-constats »
message.content sur un UIMessage0 (5 occurrences, aucune sur un UIMessage — voir « Non-constats »)
Route handlers du perimetre sans porte d'authentification0
Entrees d'outil acceptant un storeId du modele1, re-scopee — voir « Non-constats »
Copies de requireStoreId dans src/features/ai/tools/7 → P2-5
pnpm deadcode (knip)0 fichier inutilise, 1 125 exports inutilises repo-large (dette connue, deja portee par 0277)
pnpm fleet:scope sur ce diffvert

Ce qui n'a PAS ete couvert

  • src/features/ai/workflow/** (1,2 Mo) : le canvas est deja porte par 0366, 0391, 0496 ; non relu.
  • src/features/ai/chat/runtime/** : audite le 2026-09-08 (items 0565 a 0570) ; non relu pour ne pas dupliquer.
  • src/features/ai/sdk/** et src/features/ai/skills/** : parcourus, pas audites.
  • Le cout par tour, que la DoD du pilier exige de ne pas laisser regresser. Toujours aucune mesure, le constat de l'audit du 25 aout tient mot pour mot. Je ne rouvre pas d'item : 0279 porte deja les tests manquants du pilier.
  • La memoire au-dela de docs/architecture/memory-layer.md et de son README vivant.

P0

Aucun. Rien dans ce perimetre ne casse la production maintenant, et rien n'ouvre un chemin cross-tenant. Les deux constats P1 ci-dessous degradent silencieusement, ce qui est different et se traite cette semaine plutot que cette nuit.


P1

P1-1 — Le cron cleanup-drafts n'archive rien, et le dira SUCCESS tous les jours

→ backlog/ai-platform/0596

archiveBranch est une server action gatee sur une session navigateur :

Son seul appelant est un cron :

  • src/app/api/cron/cleanup-drafts/route.ts:23 — withCronAuth("cleanup-drafts", …), donc un Bearer CRON_SECRET, aucun cookie de session
  • :37 — await archiveBranch(branch.id)
  • :39-41 — le catch pousse { ok: false, error } dans results et continue

Consequence : chaque ligne leve Unauthorized, la boucle en avale jusqu'a 100 par run, et le handler rend { checked: N, archived: 0, results: [...] }.

Le second etage est ce qui rend le premier invisible. src/services/cron/auth.ts:197 definit FAILURE_KEYS = ["failed", "failedCount", "errors", "errorCount"] ; ce handler n'expose aucune de ces clefs, donc verdictFor (:217-218) retourne SUCCESS sans meme regarder les succes. La ligne CronExecution est SUCCESS, le log est cron.cleanup-drafts.completed au niveau info, et /admin/platform/crons affiche un job vert. Le seul temoin est le contenu de results, dans le payload d'un log info.

Ce que ca coute reellement : le cron existe pour tenir la limite Shopify de 20 themes par boutique (en-tete du fichier, ligne 5). Les brouillons de theme crees par le chat-as-branch ne sont jamais supprimes cote Shopify ; passe le 20e, themeDuplicate echoue et une boutique ne peut plus ouvrir de conversation de theme. Le cron tourne 15 3 * * * (vercel.json:204-205).

Ce constat avait deja ete vu et n'a jamais ete transforme en item. backlog/_archive/ai-platform/0593, lignes 101-107, le decrit exactement, sous « Bogue latent sans rapport releve au passage … A traiter separement ». backlog/inbox/ est vide et aucun item ouvert ne cite cleanup-drafts. C'est la regle §6 du protocole (« Un travail revele une suite → un nouvel item dans backlog/inbox/ ») non appliquee, et la raison pour laquelle cet audit existe.

P1-2 — Quatre portes du pilier lisent OrganizationMember a la main, la faute que cinq routes soeurs documentent comme corrigee

→ backlog/ai-platform/0597

Organization.ownerId est une colonne. Une organisation heritee, creee avant que l'inscription n'insere la ligne de son proprietaire, n'a aucune ligne OrganizationMember pour son owner. src/lib/security/tenant-auth.ts:54-58 traite ce cas en premier, et src/app/api/admin/data-integrity/backfill-members/route.ts:22-35 existe uniquement pour les rattraper (WHERE NOT EXISTS (SELECT 1 FROM "OrganizationMember" …)), sur declenchement manuel d'un operateur. La classe est donc reelle, et son eradication n'est pas garantie.

Quatre routes du pilier posent la question a la main :

Fichier:ligneEffet pour le proprietaire d'une org heritee
src/app/api/chat/voice/route.ts:32orgId silencieusement abandonne
src/app/api/branches/[id]/diff/route.ts:31403 sur le diff de son propre theme
src/app/api/branches/[id]/stream/route.ts:44403 sur la timeline de versions
src/app/api/tasks/[id]/stream/route.ts:45403 sur la console de sa propre tache

La plus couteuse est la voix, et elle merite d'etre deroulee. Le client transmet l'orgId que le shell a resolu (src/features/ai/chat/runtime/use-chat-setup.ts:61-63). La route ne trouve pas de ligne membre, laisse orgId a undefined, donc storeId aussi (:41-47), puis signe quand meme un custom_session_id vide (:49). Cote CLM, src/app/api/evi/chat/completions/route.ts:396-399 est explicite : « Unauthenticated turns (no orgId) have no balance to charge: the IP rate limit above is their only bound ». Un proprietaire qui paie obtient donc des tours vocaux @Atlas gratuits et non factures, sans contexte boutique, plafonnes par 20 tours/min/IP — et rien dans l'UI ne le dit.

Le commentaire :28-29 de cette route affirme la premisse inverse : « the session user must be a member (owner rows are member rows too) ». Cinq routes soeurs, hors pilier, ont corrige le meme defaut et documentent pourquoi : stores/[storeId]/alerts/route.ts:26-35 (supprime le 2026-09-26, app-shell/3033), alerts/count/route.ts:46-52 (idem), alerts/[alertId], probe/page-insights, reports/latest, runtime-snapshot. ai-platform est le pilier qui a garde quatre copies de la faute que tout le monde a reparee.

P1-3 — Le prompt systeme du pilier tait 9 de ses 26 chemins et envoie la regle « model names » sur le mauvais fichier

→ backlog/ai-platform/0598

.claude/agents/ai-platform-engineer.md est ce qu'un agent @ai-platform-engineer recoit au boot. Deux derives, le meme fichier.

a) Le ## Scope (lignes 17-23) declare 15 chemins ; ownership.json en attribue 26. Neuf manquent, en comptant les deux docs d'architecture citees en prose dans ## Boot :

src/app/api/branches/**          src/app/api/evi/**
src/app/api/registry/**          src/app/api/wizard/skill/**
src/app/(minimal)/embed/**       docs/architecture/atlas-channels.md
docs/canvas/**                   evals/**
vitest.evals.config.ts

Un agent qui lit ce fichier croit que les branches, EVI, le registre de skills, l'embed public, docs/canvas/ et toute la suite d'evals ne sont pas a lui — et pnpm fleet:scope lui dira le contraire au moment de la PR. C'est mot pour mot le defaut que roster.md a corrige pour ce pilier le 2026-09-05 : « cette ligne … taisait neuf chemins que .claude/fleet/ownership.json attribue au pilier ». Le roster a ete repare, le prompt systeme ne l'a pas ete, alors que roster.md:9-10 pose la regle : « si tu changes un perimetre ici, change-le aussi dans ownership.json et dans .claude/agents/ ».

Rien ne l'attrape, et c'est verifiable : src/test/roster-perimeter.test.ts:25-27 couvre bien .claude/agents/* mais dit explicitement « Exhaustiveness is NOT checked » ; src/test/pillar-scope-matches-ownership.test.ts, qui verifie les deux sens, ne lit que docs/team/roster.md. Le trou est exactement a l'intersection.

b) Ligne 44 : « Model names and versions come from config (src/config/atlas-version.ts), never inline ». Ce fichier ne contient aucun identifiant de modele : il exporte ATLAS_SURFACE_PATHS, ATLAS_INTELLIGENCE_VERSION, ATLAS_MATURITY. Le catalogue est src/config/ai-models.ts (MODELS, modelId(), tierModelId()), tenu par pnpm models:check — ce que CLAUDE.md et AGENTS.md disent tous les deux. Le chemin existe sur disque, donc doc-paths-exist.test.ts passe : la derive est semantique, et sa consequence est celle que ce test decrit lui-meme, « the agent looks, finds nothing, and either gives up or RECREATES it ».

Les deux tiennent dans un seul item parce qu'ils se corrigent dans le meme fichier : deux items se disputeraient le meme verrou de branche.


P2

P2-4 — Le composeur du prompt @Atlas appartient au pilier intelligence

→ backlog/ai-platform/0599

src/services/algorithms/prompts/layer-system.ts porte composeAtlasPrompt (:67) et, en dur, les six regles de la Layer 1 en FR et EN — veracite, citations, propriete de l'action, locale, concision, confidentialite (:39-61). C'est la constitution que le modele lit a chaque tour.

Ses deux seuls consommateurs runtime sont dans le pilier : handler.ts:55,519 et handler-system-layers.ts:34. Mais ownership.json attribue src/services/algorithms/** a intelligence, dont la mission est « Store Intelligence & Prediction ».

Donc : changer une regle de base du prompt @Atlas est une PR cross-pilier ou une violation de perimetre, et un pilier dont ce n'est pas le sujet possede 60 lignes de garde-fous LLM. Pire, docs/architecture/memory-layer.md:19 — une doc d'ai-platform — presente ce fichier comme une surface du pilier. La carte et le territoire se contredisent sans que rien ne le signale : roster-perimeter.test.ts verifie qu'un chemin CITE est bien au pilier, pas qu'un fichier soit au bon endroit.

P2-5 — Sept copies de requireStoreId, sept de notConnected, cinq de errMsg

→ backlog/ai-platform/0600

Dans src/features/ai/tools/, un repertoire qui possede deja permissions.ts et types.ts partages :

HelperFichiers
requireStoreIdstore-history-tool.ts:25, aov-tools.ts:26, cro-tools.ts:19, action-plan-tools.ts:17, commerce-tools.ts:35, vitals-tools.ts:49, aeo-tools.ts:20
notConnectedmemes fichiers, :29 :30 :23 :21 :39 :53 :24
errMsgstore-history-tool.ts:36, aov-tools.ts:37, cro-tools.ts:30, action-plan-tools.ts:28, aeo-tools.ts:31

requireStoreId est return ctx.storeId ?? null dans les sept. Les notConnected different d'un mot (« AOV tools » / « CRO tools » / …) et d'un type : vitals-tools.ts:53 rend { ok: false; error: string } la ou les six autres rendent { ok: false as const, … }, donc le narrowing du retour n'est pas le meme selon l'outil.

Ce n'est pas de la dette figee : store-history-tool.ts est la septieme copie et n'existait pas il y a deux heures — elle est arrivee sur origin/main pendant cette passe. Le motif se reproduit a chaque nouveau domaine d'outils.

Meme famille, degre moindre, note sans item separe : requireOrgMembership existe deux fois dans le pilier (branches/shared.ts:39, tasks/actions.ts:38). Le commentaire :45-55 du premier explique justement que quatre homonymes ont empeche toute sonde d'autorisation de les separer. Deux survivent.

P2-6 — Soixante lignes identiques entre deux wizard-skills

→ backlog/ai-platform/0601

src/features/ai/wizard-skills/structured-data-wiring.ts:60-119 et src/features/ai/wizard-skills/crawl-surface-builder.ts:73-132 portent chacun readThemeFile + upsertThemeFiles. diff sur les deux blocs : deux lignes differentes sur soixante, et les deux sont le nom de l'operation GraphQL (wireJsonLd vs wireRobots), qui n'a aucun effet. Toute correction faite sur l'un (retry 429, gestion des userErrors, pagination) manquera l'autre en silence.

P2-7 — Le harnais d'evals cite un ADR qui n'existe pas

→ backlog/ai-platform/0602

evals/support/harness.ts:6 : « The choice is argued in docs/decisions/2026-09-05-eval-harness.md ». Ce fichier n'existe pas ; l'ADR est docs/decisions/0008-le-harnais-d-eval-est-vitest.md, que CLAUDE.md cite correctement. C'est la seule occurrence du chemin fantome dans le depot. doc-paths-exist.test.ts ne lit que les documents de boot en .md, pas les commentaires .ts, donc rien ne l'attrape.

P2-8 — memory-layer.md appelle « iRen kernel » ce que le code appelle « @Atlas kernel »

→ backlog/ai-platform/0603

docs/architecture/memory-layer.md:19 : « Layer 1 (base) + Layer 2 (iRen kernel) + Layer 3 ». Le code dit « Layer 2 : @Atlas kernel » (layer-system.ts:5). AGENTS.md a deja retire cette marque de son propre texte en notant qu'elle « survives nowhere in src/ but two CSS comments ».

La ligne compte parce qu'elle est dans le tableau « What already exists (DO NOT duplicate) », c'est-a-dire la partie de la page explicitement tenue a jour — deux autres cellules du meme tableau ont ete corrigees en place les 5 et 6 septembre. Un lecteur la lit donc comme du present. 0278 (hygiene-naming-organization-36) couvre @iRen dans hub.css et identity-registry.ts ; sa preuve dit « rg iRen src → ces 2 lignes seulement », scopee a src. docs/ n'a jamais ete regarde.


Ce que ce tour renvoie hors pilier

→ backlog/inbox/0604

Le trou de garde derriere P1-3 n'est pas propre a ai-platform. En comparant chaque .claude/agents/<agent>.md a ownership.json :

PilierPossedeDeclareTait
growth-web391727
app-shell311421
ai-platform261511
platform-ops281811
data-platform1376
commerce-systems21183
billing15132
intelligence / marketplace / design-system——1 chacun
security-identity / integrations——0

L'item va dans backlog/inbox/ et pas dans un pilier : les fichiers concernes (.claude/agents/**) appartiennent a @conductor, et le garde (src/test/**) est une surface partagee. C'est @conductor qui tranche.


Non-constats

Ce que j'ai pris pour un defaut et qui n'en est pas. Cette section existe pour qu'un prochain tour ne les rouvre pas.

Les trois imports @ai-sdk/anthropic sont corrects. orchestrator/runtime/handler-tools-build.ts:27, bot/bot-handlers.ts:42, bot/whatsapp-per-store.ts:22. Les trois usages sont anthropic.tools.webSearch_20250305({ maxUses: 5 }) — un provider-defined tool, pas un modele. Les modeles passent par modelId("sonnet") et gatewayProvider: "anthropic". src/test/ai-gateway-routing.test.ts tient la distinction, roster.md et .claude/agents/ai-platform-engineer.md la formulent correctement depuis 0057. Deja ecrit comme « non-constat » le 25 aout ; c'est le deuxieme tour ou je dois le re-verifier. Si une sonde automatique le resignale un jour, c'est la sonde qui a tort.

Les cinq message.content ne sont pas l'anti-pattern. Aucun ne porte sur un UIMessage du SDK : voice/messages.tsx:41 lit un msg.message.content de @humeai/voice-react, widget/widget.tsx:197 un message du widget maison, context/level.ts:131 un objet { content: string } local, use-chat-persistence.ts:118 et conversations/[id]/messages/route.ts:308 la colonne Message.content de Prisma.

maxSteps : 0. toDataStreamResponse : 0. generateObject / streamObject en usage : 0. Les seules occurrences des deux derniers sont dans memory/extract.ts:173 et son test, qui les nomment pour dire qu'ils sont interdits.

/api/branches/[id]/fork et /publish n'ont pas de porte dans le route handler, et c'est correct. forkBranch / publishBranch appellent requireOrgMembership(orgId, "store.update") en premiere ligne (actions/publish.ts:32,81), et restoreVersion de meme. Le defaut oppose — le void params qui ignorait l'id de branche de l'URL — a ete corrige et documente dans l'en-tete de restore/route.ts.

publishStudioDrop accepte un storeId du modele, et c'est scope. studio-write-tools.ts:158 : l'entree est re-resolue par studioActorFor(caller, storeId, "studio.drop.publish") puis comparee a ctx.orgId via outsideOrg(...) avant toute ecriture. C'est le seul outil du registre qui prend un identifiant de tenant en entree.

docs/canvas/** n'est pas de la derive. Les dix pages portent le meme bandeau : « archive de conception, mai 2026 … Ce qu'elles ne sont pas : une description du code d'aujourd'hui », plus un tableau « Ce que le plan de mai laissait ouvert, et ou ca en est » verifie chemin par chemin le 2026-08-28. Les iRen qu'on y trouve sont dans le perimetre de cette declaration. Nommer la chose morte et dire qu'elle est morte est exactement la regle du depot.

docs/architecture/agentic-stack.md a ete reparee. L'item 0058 issu de l'audit du 25 aout est archive, le doc porte desormais un en-tete « Ce que ce document est / n'est pas », une section « Les surfaces du pilier » dont j'ai verifie les 15 chemins (tous presents), et les deux sections de mai sont encadrees et datees.

src/features/ai/rules/ ne contient qu'un README, et c'est assume. Le README dit « For now: empty placeholder » et nomme ou vit l'implementation reelle. C'est la distinction que l'item 0056 a posee entre un placeholder et un vestige.

src/features/ai/connectors/ (deux .gitkeep, zero code) est deja suivi. 0277 (ai-platform-tools-35) et 0278 (hygiene-naming-organization-28) le portent tous les deux. Non resignale.

Les 1 125 exports inutilises de knip ne sont pas un constat de ce tour. 229 sont dans le perimetre, mais knip.json classe exports, types et duplicates en "warn" : c'est une dette assumee au niveau du depot, pas une regression du pilier. 0277 en porte deja les morceaux nommes (sops.ts, MODEL_MAP, clearPresence).

Le rate-limit de /api/chat est maintenant par utilisateur. 0277 (ai-platform-runtime-33) decrit une cle par IP+chemin qui throttlait une agence derriere un NAT. session-auth.ts:100-104 utilise desormais l'identite authentifiee (security-identity/0364). Ce sous-constat de 0277 est perime ; je ne le corrige pas, c'est au pilier de le rayer en livrant l'item.


Prochain pilier de la rotation : integrations (audit du 26 aout, 16 jours, et quatre items blocked — le plus gros stock de blocage de la flotte apres ai-platform).