ADRADR-0016 · Une seule porte d'audit dans le chat, et le focus est le levier de cout

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 :

OutilDescription 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.