ADR-0016 — Une seule porte d'audit dans le chat, et le focus est le levier de cout
Le chat exposait trois outils qui disaient tous auditer une boutique : Rien dans les deux premieres phrases ne dit au modele laquelle prendre. Et le plus couteux n'etait pas l'ambiguite : auditStore etait un passthrough…
Statut
Accepté · 2026-09-13
Piliers : ai-platform, commerce-systems
Contexte
Le chat exposait trois outils qui disaient tous auditer une boutique :
| Outil | Description annoncee au modele |
|---|---|
runFullStoreAudit | « Run a COMPLETE storefront audit on the given HTTPS URL… » |
auditStore | « Run a comprehensive audit of the Shopify store: SEO, performance, conversion, tracking, accessibility. » |
runTrackingScan | « Run a tracking & conversion scan… 41 checks » |
Rien dans les deux premieres phrases ne dit au modele laquelle prendre. Et le
plus couteux n'etait pas l'ambiguite : auditStore etait un passthrough brut
non score de bridge.runAudit("full"), appel que runFullStoreAudit fait
deja dans son pilier Admin
(store-audit/engines/admin-pillar.ts).
Un tirage au sort decidait donc si l'operateur recevait un rapport score sur
quatorze piliers ou un blob JSON du meme appel.
Deuxieme contrainte, celle qui a decide de la forme : runFullStoreAudit
n'avait aucun parametre de portee. Il lancait cinq moteurs, dont le seul qui
coute de l'argent reel (une session Browserbase, une minute du quota mensuel
de l'org, plusieurs secondes de temps de mur). Une question etroite — « est-ce
que mon SEO va ? » — payait donc un audit complet. auditStore, lui, avait un
focus. Supprimer l'outil sans deplacer son focus aurait rendu la question
etroite plus chere qu'avant.
Décision
Une seule porte composite dans le chat : runFullStoreAudit. Elle porte un
focus (full | seo | conversion | catalog) qui selectionne les
moteurs a lancer, pas un filtre applique au rapport une fois payé.
auditStore est supprime du chat. Il reste sur le serveur MCP, sous le nom
runAudit : des clients externes l'ont installe comme primitive, et le probleme
de choix d'outil du chat n'est pas le leur.
runTrackingScan reste, comme primitive que le pilier tracking du composite
appelle. Composite appelle primitive ; la primitive n'est pas re-exposee en
second composite.
Alternatives écartées
Garder les deux outils et durcir les descriptions. C'est ce qui avait deja ete tente : les deux descriptions etaient deja explicites, et ne se distinguaient pas pour autant. Un choix d'outil fiable ne se gagne pas en reecrivant deux phrases qui decrivent le meme travail.
Ajouter un focus tracking. Ecrit, puis retire avant d'etre livre. Il
aurait rendu le meme scan replie dans deux piliers sur quatorze, en concurrence
avec runTrackingScan qui rend le rapport brut par categorie. C'est exactement
le defaut qu'on venait de supprimer, recree un etage plus bas. La description du
composite route desormais les questions tracking vers la primitive, et
src/test/one-audit-door-in-the-chat.test.ts
echoue si le focus revient.
Supprimer runTrackingScan et tout faire passer par le composite. Perte
seche : le scan brut porte les scores par categorie, les checks en echec et la
stack detectee, que le repliage en piliers jette.
Supprimer runAudit du serveur MCP aussi. Le probleme etait le choix
d'outil dans notre chat. Un client MCP demande un outil par son nom : il n'a
pas d'ambiguite a resoudre. Le retirer casserait des integrations installees
pour une raison qui n'est pas la leur.
Un focus qui filtre le rapport. C'est la version qui compile, passe les
tests, et ne change pas la facture. Le garde epingle donc le gating dans
l'orchestrateur (engines.has("...")), pas seulement la table.
Conséquences
Un audit etroit ne touche plus Browserbase : seo et conversion font
scrape + sondes publiques, catalog fait scrape + Admin. Seul full
ouvre une session.
Un rapport partiel doit le dire, dans les deux canaux : limitations (ce
que le modele lit) et cardHint.title (ce que l'operateur lit). Un 92 sur un
run SEO seul et un 92 sur un audit complet ne sont pas le meme nombre, et rien
d'autre dans la charge utile ne les distingue.
Ce que cette decision ne regle pas : la page (minimal)/scan/tracking
(7504 lignes, 45 composants) reste une seconde surface d'audit hors du chat, et
les deux chips du composer (Audit, Scan) restent deux intentions. Suivi dans
backlog/ai-platform/2645 et backlog/commerce-systems/2650.
Item
backlog/_archive/ai-platform/2644.
Ce qui aurait laisse passer la regression
Le garde a ete ecrit deux fois. La premiere version comptait les declarations
name: tool({ dans les modules et rapportait une porte. Elle avait tort :
runTrackingScan est declare en return tool({…}) nu dans une fabrique et
nomme au registre (runTrackingScan: createTrackingScanTool(ctx)), donc
elle ne le voyait pas du tout. Mesure par mutation : ajouter un import de moteur
dans tracking-scan.ts laissait la suite verte.
Les noms sont donc resolus depuis tools/index.ts, dans les deux formes
d'enregistrement. Un garde qui ne sait lire qu'une forme sur deux rapporte le
chiffre qu'il sait compter, pas celui qui existe.