Signaux des piliers : les mesures planifiees nourrissent le hub
Pilier commerce-systems. Item backlog/commerce-systems/3081. Contrat : src/lib/pillar-signals.ts (tag SOT pillar-signal-kinds, ai-referrer-hosts, wilson-interval).
Pilier
commerce-systems. Itembacklog/commerce-systems/3081. Contrat :src/lib/pillar-signals.ts(tag SOTpillar-signal-kinds, ai-referrer-hosts, wilson-interval).
Le principe
Une mesure planifiee qui se degrade devient une ligne StoreAlert du hub
d'activite (docs/architecture/store-activity.md), avec son pilier, son
metrique et sa ligne de base. Le cycle proactif en tire une proposition
de tache, jamais un travail lance : ADR 0034, un cron detecte, propose et
verifie, un clic humain demarre le travail. Aucun appel de modele dans
cette chaine.
job planifie ──> detecteur pur ──> PillarSignalDraft[] ──> emitPillarSignals
│
StoreAlert (source cron:signal:<source>)
│
atlas-proactive ──> planPillarProposals ──> alertToTask(forceProposal)
│
Task WAITING_REVIEW, assignee par pilier
Kinds et sources
Un kind est <pilier>/<check>[/<scope>...], construit uniquement par
pillarSignalKind(). Les checks sont listes par PILLAR_CHECKS.
Source (SignalSource) | Ecrit par | Checks |
|---|---|---|
aeo-audit | aeo-audit-store (hebdomadaire) | aeo/crawler_blocked, schema_errors, score_drop, agents_md_missing, agents_md_stale |
psi | fetch-psi | vitals/psi_{lcp,cls,tbt}_poor/<template>/<device> |
crux | fetch-crux | vitals/crux_{lcp,inp,cls}/<origin|hash url>/<device> |
vitals-regression | vitals-detect-regressions | vitals/regression_{lcp,inp,cls}/<pageType>/<device> |
geo-citations | aeo-citation-run | geo/citation_rate_drop |
ai-referrals | aggregate-pixel-events (chemin quotidien) | geo/ai_referrals_drop |
tracking-scan | recordScan (scans deja lances) | tracking/{pixel_missing,capi_missing,consent_mode_missing,score_drop} |
gsc | seo-gsc-sync | seo/clicks_drop, seo/ctr_low/<hash page>, seo/position_drop/<hash requete> |
StoreAlert.source = cron:signal:<source> : resolveInsights ne ferme
donc que les kinds du meme detecteur. PSI ne resout jamais un kind CrUX.
Un job qui n'evalue qu'une partie des kinds (un PSI pour une URL et un
appareil) passe evaluatedKinds a l'emetteur, qui ne resout que ceux-la.
Hachage du texte libre
Une requete Search Console ou un chemin de page n'entre jamais dans un
kind ni dans un titre : scopeHash() (src/services/pillar-signals/scope-hash.ts,
10 hex de sha256, serveur seulement). Le texte vit dans
metadata.signal.scope, que le panneau du pilier montre aux membres
workspace.read. Les titres stockes sont generiques et en anglais ; le
lecteur recoit dashboard.store.alerts.pillar.<pilier>.<check> dans sa
langue (src/lib/alerts/localize.ts).
Transition seule, respect de l'humain
upsertSignalInsight (src/services/store-activity/mutations.ts) :
- kind ouvert : mis a jour sur place (metadata, corps),
resolvedAtintact ; - kind qu'un humain a acquitte ou resolu depuis moins de
SIGNAL_REOPEN_COOLDOWN_DAYS(7) : ignore ; - kind en snooze : ignore ;
- sinon cree, ou rouvert.
L'auto-resolution ne ferme que les lignes sans acknowledgedBy : ce
qu'un humain a touche reste comme il l'a laisse. La severite est info
ou warn, jamais critical (le dispatcher enverrait un e-mail).
Les detecteurs vitals ouvrent sur un franchissement de seuil, pas sur un niveau : tant que la condition tient, un kind deja ouvert est re-emis (ni rouvert ni auto-resolu).
Vers la tache
alertToTask(actor, storeId, alertId, { ownerAgent, forceProposal }) lit
metadata.signal : pillar du signal, metricKey = le kind (scope,
unique), baseline = { ...signal.baseline, metricId, scope }.
PILLAR_OWNER_AGENT : seo, aeo, geo -> @Maya ; vitals, cro, tracking,
aov -> @Marco ; commerce -> @Otis. Le cycle propose au plus 3 taches par
boutique et par passage, toujours WAITING_REVIEW.
GEO : des taux, jamais un verdict
src/features/aeo/citation-stats.ts : chaque passage porte un runId
(BrandCitation.runId, les lignes anciennes sont groupees par jour
legacy-YYYY-MM-DD). Un passage donne un taux avec un intervalle de
Wilson ; le taux groupe couvre les 3 derniers passages. Le signal de
baisse exige des intervalles disjoints et n >= 10 des deux cotes.
Dormant par construction a la depense par defaut : ~10 reponses par
passage separent rarement deux intervalles. dropSignalDormant le dit
a l'ecran. D3 (tranchee le 2026-09-27) : 3 passages par prompt sur un
plan payant, 1 sinon, GEO_CITATION_RUNS_PER_PROMPT surcharge.
Referrals IA
Le pixel ne stocke que aiReferrerHost(referrer) : un hote d'assistant
connu (AI_REFERRER_HOSTS) ou null, jamais un autre hote ni un chemin,
avec PIXEL_REFERRER_CAPTURE_ENABLED (D4) et le consentement. Pas
d'index sur la table chaude : le generateur du schema guard ne sait pas
ecrire d'index partiel, et la lecture est bornee par
(storeId, capturedAt) sur 28 jours.
Vitals Shopify (RUM)
ShopifyQL web_performance via runShopifyQL avec le scope deja accorde
read_reports et SHOPIFY_API_VERSION. Colonnes lues par nom. Les
requetes principales ne demandent que les colonnes documentees
(lcp_p75_ms, p75_cls, page_loads) ; inp_p75_ms, non verifiee, part
dans des requetes a part, car ShopifyQL rejette toute la requete sur une
colonne inconnue : leur echec laisse les lignes LCP/CLS intactes et
inscrit inp_p75_ms dans ShopifyWebPerfSync.missingColumns.
Cadence quotidienne voulue (vitals-shopify-pull, 06:37 UTC) et non
horaire : la requete lit des jours complets (TIMESERIES day), un tirage
horaire relirait les memes jours. Le RUM Shopify n'a pas de source
SignalSource propre : il atteint le hub par vitals-regression (job
source shopify, candidat 24 h, un job par boutique et par jour), repris
en tourniquet via regressionResumeFrom au-dela de 100 par tick. Meme
tourniquet pour seo-gsc-sync (nextCursor, 1000 par tick). Rang de fusion : RUM 6,
SHOPIFY_RUM 5, CRUX 4, PSI_FIELD 3, PSI_LAB 2, BROWSERBASE 1, et une ligne
Shopify sous MIN_SHOPIFY_PAGE_LOADS (50) chargements est ignoree.
Eteint (VITALS_SHOPIFY_RUM_ENABLED) jusqu'a la verification S8a.
Bascules
| Variable | Defaut | Registre |
|---|---|---|
VITALS_SHOPIFY_RUM_ENABLED | off | verification S8a |
GOOGLE_SEARCH_CONSOLE_ENABLED | off (D1 oui, bloquee : verification Google + confidentialite) | D1 |
TRACKING_SCHEDULED_RESCAN_ENABLED | on (0 l'eteint) | D2 |
GEO_CITATION_RUNS_PER_PROMPT | 3 payant, 1 sinon | D3 |
PIXEL_REFERRER_CAPTURE_ENABLED | off (D4 oui, bloquee : extension pixel) | D4 |
D2 : le cron tracking-rescan (le 2 du mois) enfile un job
tracking-rescan-store par boutique payante deja scannee et pas encore ce
mois (curseur keyset, dedoublonnage boutique + mois). Le job re-verifie
l'entitlement et le plafond Browserbase, lance runTrackingScan sans
identifiants OAuth et ecrit la ligne Scan avec son rapport : les
detecteurs tracking prennent le relais.
Lues par src/services/pillar-signals/flags.ts. Les decisions sont dans
docs/ops/operator-decisions.md.
Lecture
loadPillarPanel(storeId, pillar)(src/services/store-activity/actions.ts) : le filtreActivityQuery.pillar(kinds par prefixeALERT_PREFIX_PILLAR, taches par colonnepillar).src/services/pillar-signals/reads.ts:loadGscSummary,loadAiReferralSummary,loadGeoCitationSummary,loadAgentDiscovery, chacun re-verifieworkspace.read.- Outils @Atlas (
src/features/ai/tools/pillar-signal-tools.ts) :getPillarSignals,getSearchConsoleSummary,getAiReferrals,getCitationRates,getTaskVerification, lecture seule, boutique lue dans le contexte.
Verification : re-mesure apres changement (3082)
Une tache pilier (pillar, metricKey, baseline) qui passe a DONE, ou
dont une action agent mutante liee reussit, arme une re-mesure
deterministe. Avant/apres, jamais causal : le resultat le dit, la
carte le dit, l'outil le dit.
- Contrat :
src/lib/pillar-signals-verification.ts(resolveVerificationPlan,verdictForValues,verdictForRates, Newcombe). Le « avant » estbaseline.current, le niveau mesure quand la tache est nee. - Table
TaskVerification, une ligne par (tache, cycle).task_doneetaction_shippedpartagent le cycle ; un nouveau cycle seulement apres une reouverture (unstatus_changeposterieur a la cloture). - Armement :
src/services/pillar-signals/verification/arm.ts, appele apres l'ecriture parupdateTaskStatus,decideProposaletemitAgentAction; annulation au depart de DONE. - Mesure : job
task-verify(tenant lu dans la ligne), PSI 10 minutes apres, le reste par le balayage quotidien deoutcome-attribution: latence jusqu'a un jour pour les methodes par sondage. Delai depasse ou 60 tentatives :expired,inconclusive no_new_measurement.
| Methode | Metriques | Fenetre |
|---|---|---|
psi_lab | vitals.*_lab | un test labo apres le changement (rapport reutilise) |
crux_field | vitals.*_p75 source crux | premier enregistrement 28 jours apres |
shopify_rum | vitals.*_p75 source shopify | 7 j contre 7 j, moyenne ponderee des p75 journaliers (approchee) |
rum_window | vitals.*_p75 source rum | 7 j contre 7 j, ponderee par echantillons |
next_audit | aeo.* | premier audit apres |
next_citation_run | geo.citation_rate | premiere serie apres, Wilson |
referral_window | geo.ai_referral_share | 14 j contre 14 j, Wilson |
next_scan | tracking.* | premier scan apres (sans D2, la ligne peut expirer) |
gsc_window | seo.* | 28 j contre 28 j, derriere GOOGLE_SEARCH_CONSOLE_ENABLED |
funnel_window et revenue_window existent sans plan jusqu'a 3083.
Issues (verification/outcomes.ts) : une regression ouvre un insight
cron:verification (kind <pilier>/verify_regressed/<scope>, source
verification) et une tache de suivi WAITING_REVIEW liee au parent ; la
tache regressee n'est jamais rouverte (ADR 0034). Une amelioration ecrit
le fait store_win:<metricId> (persistFacts, sans embedding, 90
jours) ; le cycle proactif ne repropose pas une metrique gagnee depuis
14 jours, sauf alerte plus recente. Le feed porte la facette
verification et le filtre serveur verified ; l'outil
getTaskVerification la lit en texte.