ArchitectureSignaux des piliers : les mesures planifiees nourrissent le hub

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. Item backlog/commerce-systems/3081. Contrat : src/lib/pillar-signals.ts (tag SOT pillar-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 parChecks
aeo-auditaeo-audit-store (hebdomadaire)aeo/crawler_blocked, schema_errors, score_drop, agents_md_missing, agents_md_stale
psifetch-psivitals/psi_{lcp,cls,tbt}_poor/<template>/<device>
cruxfetch-cruxvitals/crux_{lcp,inp,cls}/<origin|hash url>/<device>
vitals-regressionvitals-detect-regressionsvitals/regression_{lcp,inp,cls}/<pageType>/<device>
geo-citationsaeo-citation-rungeo/citation_rate_drop
ai-referralsaggregate-pixel-events (chemin quotidien)geo/ai_referrals_drop
tracking-scanrecordScan (scans deja lances)tracking/{pixel_missing,capi_missing,consent_mode_missing,score_drop}
gscseo-gsc-syncseo/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), resolvedAt intact ;
  • 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

VariableDefautRegistre
VITALS_SHOPIFY_RUM_ENABLEDoffverification S8a
GOOGLE_SEARCH_CONSOLE_ENABLEDoff (D1 oui, bloquee : verification Google + confidentialite)D1
TRACKING_SCHEDULED_RESCAN_ENABLEDon (0 l'eteint)D2
GEO_CITATION_RUNS_PER_PROMPT3 payant, 1 sinonD3
PIXEL_REFERRER_CAPTURE_ENABLEDoff (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 filtre ActivityQuery.pillar (kinds par prefixe ALERT_PREFIX_PILLAR, taches par colonne pillar).
  • src/services/pillar-signals/reads.ts : loadGscSummary, loadAiReferralSummary, loadGeoCitationSummary, loadAgentDiscovery, chacun re-verifie workspace.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 » est baseline.current, le niveau mesure quand la tache est nee.
  • Table TaskVerification, une ligne par (tache, cycle). task_done et action_shipped partagent le cycle ; un nouveau cycle seulement apres une reouverture (un status_change posterieur a la cloture).
  • Armement : src/services/pillar-signals/verification/arm.ts, appele apres l'ecriture par updateTaskStatus, decideProposal et emitAgentAction ; annulation au depart de DONE.
  • Mesure : job task-verify (tenant lu dans la ligne), PSI 10 minutes apres, le reste par le balayage quotidien de outcome-attribution : latence jusqu'a un jour pour les methodes par sondage. Delai depasse ou 60 tentatives : expired, inconclusive no_new_measurement.
MethodeMetriquesFenetre
psi_labvitals.*_labun test labo apres le changement (rapport reutilise)
crux_fieldvitals.*_p75 source cruxpremier enregistrement 28 jours apres
shopify_rumvitals.*_p75 source shopify7 j contre 7 j, moyenne ponderee des p75 journaliers (approchee)
rum_windowvitals.*_p75 source rum7 j contre 7 j, ponderee par echantillons
next_auditaeo.*premier audit apres
next_citation_rungeo.citation_ratepremiere serie apres, Wilson
referral_windowgeo.ai_referral_share14 j contre 14 j, Wilson
next_scantracking.*premier scan apres (sans D2, la ligne peut expirer)
gsc_windowseo.*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.