Files
claude/docs/superpowers/specs/2026-07-09-gsc-crux-data-layer-design.md
T
Bastien Chanot 10f20438b1 docs(seo-data): relocate engine test out of gated lib/tests/
config-protection.sh gates lib/tests/* as a guardrail dir; all 8 tasks
edit the engine test. Move it to lib/seo-data/seo-data.test.sh (co-located,
ungated) + extend the make test glob to discover lib/seo-data/*.test.sh.
Root-cause fix, no guardrail weakened. Chosen by user over per-edit bypass.
2026-07-10 00:56:55 +02:00

20 KiB

Spec — Couche data GSC + CrUX pour /seo (+/geo) FULL

  • Date : 2026-07-09
  • Statut : Design validé — prêt pour /writing-plans
  • Auteur : Bastien Chanot (design assisté)
  • Repo : claude-config (~/Documents/claude, symlinké dans ~/.claude via link.sh)
  • Cycle de vie : document de travail transitoire — à supprimer une fois la feature livrée, documentée (/document-release ou /doc) et capitalisée (decisions.md). Ne pas conserver à long terme.

1. Contexte & objectif

L'audit comparatif entre les skills perso /seo + /geo et l'outil marketplace agricidaniel/claude-seo a isolé un seul gap structurel : les skills perso ne peuvent pas lire la donnée Google réelle d'un site (requêtes/positions/impressions de la Search Console, statut d'indexation, Core Web Vitals terrain). Ils se limitent à l'API PageSpeed anonyme (données labo) et à WebSearch.

Objectif : combler ce gap sans installer l'outil tiers (860 KB de Python, mainteneur unique, surface supply-chain + credentials OAuth à confier). On ajoute une couche data minimale, isolée, sous contrôle, branchée sur les analyzers existants.

Ce que ça débloque concrètement :

  • CWV terrain (CrUX, 75e percentile, mobile + desktop, historique) au lieu du seul labo.
  • Requêtes GSC : le pattern « positions 4-10 à fort volume d'impressions » = quick wins que les skills ne pouvaient pas voir.
  • Indexation réelle par URL (GSC URL Inspection) au lieu d'une déduction.

2. Principes directeurs (non négociables)

  1. Sécurité avant tout. Secrets hors git, permissions 0600, scope OAuth lecture seule, redaction systématique, aucun secret dans un rapport/log. Toute surface secret nouvelle sous ~/.claude est explicitement allowlistée dans .gitleaks.toml.
  2. Dégradation gracieuse (fail-open audit). Creds absents / token révoqué / quota 429 → l'audit FULL continue en retombant sur PageSpeed anonyme + une action utilisateur. Jamais de crash.
  3. Consentement OAuth one-shot, runs silencieux ensuite. Le consentement navigateur ne se fait qu'au setup (ou à l'ajout d'un compte). Les audits suivants sont non-interactifs.
  4. Multi-compte, zéro conflit. Plusieurs comptes Google connectables ; deux audits de deux sites en simultané sont isolés par construction (compte + propriété = paramètres explicites par appel, jamais un état global mutable).
  5. Isolation. Le moteur ne connaît rien du SEO (rend du JSON) ; les analyzers ne connaissent rien d'OAuth (consomment du JSON). Deps Python isolées dans un venv dédié.

3. Décisions actées

# Décision Choix
Auth GSC OAuth2 installed-app, consentement one-time, refresh token stocké OAuth2
Scope data v1 GSC Search Analytics + URL Inspection + CrUX field oui (GA4/Ads/Indexing hors v1)
Surface Fold dans /seo (+/geo) FULL ; pas de nouveau skill d'audit oui (setup = make seo-connect, pas un skill d'audit)
Langage Helper Python + google-auth, venv isolé oui
Multi-compte Store keyé par compte ; sélection à chaque audit FULL oui
Persistance token Auto-écriture idempotente dans le store (write-temp→rename) oui
Déclencheur consentement install.sh (proposé) et make seo-connect (toujours dispo) oui, les deux
Gitleaks Allowlister le token store (comme ~/.claude/.env) oui

4. Architecture

4.1 Vue d'ensemble

lib/seo-data/                        ← LE MOTEUR (nouveau, isolé, sans logique SEO)
├── fetch.sh          entrypoint bash : source ~/.claude/.env → active venv → dispatch
│                     sous-commandes → JSON sur stdout → dégrade proprement si creds absents
├── google_seo.py     appels GSC (Search Analytics, URL Inspection, sites.list) + CrUX,
│                     refresh OAuth via google-auth, normalisation JSON
├── connect.py        consentement OAuth one-time (InstalledAppFlow) + écriture store
├── tokenstore.py     lecture/écriture atomique du store keyé (partagé par connect+fetch)
├── requirements.txt  deps épinglées : google-auth, google-auth-oauthlib, requests
└── README.md         contrat d'usage + sous-commandes

~/.claude/.venv-seo-data/            ← venv isolé (deps hors système, reproductible)
~/.claude/seo-data/tokens.json       ← store keyé par compte (0600, hors git)

CONSOMMATEURS (existants, patchés) :
agents/seo-analyzer.md   STEP 4 (CWV) → data terrain CrUX quand dispo ; nouvelle
                         sous-section « Performance GSC » ; STEP 9 axe Technical nourri au réel.
skills/seo/SKILL.md      STEP 0 → sélection compte + propriété (main loop, interactif).

4.2 Composants & frontières

Unité Rôle unique Utilisée comment Dépend de
fetch.sh orchestre env→venv→python, dégrade, redige bash fetch.sh <cmd> --account … --property … → JSON ~/.claude/.env, venv, tokenstore
google_seo.py appelle GSC + CrUX, normalise en JSON appelé par fetch.sh google-auth, requests
connect.py consentement OAuth one-time + découverte propriétés make seo-connect / STEP 0 google-auth-oauthlib, tokenstore
tokenstore.py I/O atomique du store keyé importé par connect + google_seo stdlib (json, os, fcntl)
seo-analyzer (patch) consomme le JSON, score, rapporte inchangé pour l'utilisateur fetch.sh (optionnel)
seo/SKILL.md (patch) sélectionne compte+propriété en STEP 0 interactif, main loop fetch.sh accounts

5. Authentification & secrets (multi-compte)

5.1 Modèle OAuth

  • Type : OAuth2 « installed app » (client Desktop créé dans la console GCP par l'utilisateur).
  • Scope unique : https://www.googleapis.com/auth/webmasters.readonly (GSC lecture seule). Least privilege strict : le token ne peut rien modifier sur GSC, révocable côté Google.
  • Flow : connect.py construit la config client en mémoire (InstalledAppFlow.from_client_config) à partir des vars d'env — aucun client_secret.json sur disque. Le consentement ouvre un serveur local + navigateur ; au retour, on obtient un refresh_token (durable) écrit dans le store.
  • Runs d'audit : google_seo.py échange le refresh_token contre un access_token éphémère en mémoire (jamais persisté). Donc aucune écriture disque pendant un audit.

5.2 Vault ~/.claude/.env (app partagée + CrUX)

Ne contient que ce qui est commun à tous les comptes :

# ── Google SEO data layer (lib/seo-data) ──
# App OAuth Desktop partagée (console GCP → APIs & Services → Identifiants).
# Scope demandé : webmasters.readonly. Setup : make seo-connect
GOOGLE_OAUTH_CLIENT_ID=<votre-client-id.apps.googleusercontent.com>
GOOGLE_OAUTH_CLIENT_SECRET=<votre-client-secret>
# Clé API CrUX + PageSpeed (console GCP → clé API restreinte à ces deux APIs).
# Get it: https://developer.chrome.com/docs/crux/api  (bouton "Get a key")
CRUX_API_KEY=<votre-cle-crux>

Les refresh tokens ne sont PAS ici (multi-compte → store dédié).

5.3 Token store keyé

~/.claude/seo-data/tokens.json, permissions 0600, dossier 0700 :

{
  "version": 1,
  "accounts": {
    "client-a": {
      "refresh_token": "<opaque>",
      "scopes": ["https://www.googleapis.com/auth/webmasters.readonly"],
      "granted_at": "2026-07-09T…",
      "properties": ["sc-domain:site-a.com", "https://www.site-a.com/"]
    },
    "client-b": { "…": "…" }
  }
}
  • Clé = label choisi par l'utilisateur au moment du connect (ex. client-a), pas l'email. Raison sécurité : keyer par email obligerait à élargir le scope OAuth (userinfo.email) juste pour l'identification. On reste à webmasters.readonly strict ; le label suffit à distinguer les comptes. Collision de label → connect demande confirmation (écraser / renommer).
  • properties = propriétés GSC accessibles (via sites.list, déjà dans le scope webmasters.readonly), pré-remplies au connect pour la sélection en STEP 0.
  • Écriture uniquement au connect (jamais pendant un audit), atomique : write vers tokens.json.tmp → fsync → rename ; verrou fcntl exclusif le temps de l'échange read-modify-write pour couvrir deux connect simultanés.

5.4 Gitleaks allowlist + gitignore

  • ~/.claude/seo-data/tokens.json vit hors du repo (le repo ne symlink que hooks agents skills lib templates rules). Il n'entre donc jamais en git directement.
  • Mais make scan-secrets (gitleaks) balaie ~/.claude à la recherche de copies de secrets. On ajoute une entrée d'allowlist dans .gitleaks.toml [allowlist].paths, exactement comme ~/.claude/.env l'est déjà :
    # Token store OAuth de la couche seo-data — secret local légitime (BDR-026 pattern),
    # hors git, 0600. On l'allowliste pour ne pas noyer scan-secrets de faux positifs.
    '''(^|/)\.claude/seo-data/tokens\.json$''',
    
  • .gitignore : ajouter .venv-seo-data/ et seo-data/tokens.json par prudence (au cas où un chemin relatif les ferait apparaître sous le repo), en complément de l'exclusion .env* existante.

6. Sélection de compte & propriété (STEP 0, main loop)

Interactif → se déroule dans le dispatcher /seo (main loop), jamais dans le subagent (qui ne peut pas interagir). Uniquement en FULL (LOCAL n'a pas de donnée live).

  1. Lister les comptes connectés : bash lib/seo-data/fetch.sh accounts → JSON {accounts:[…]}.
  2. Présenter à l'utilisateur (label + propriétés découvertes) :
    COMPTE GOOGLE pour cet audit FULL :
      1) client-a   (sc-domain:site-a.com, https://www.site-a.com/)
      2) client-b   (sc-domain:site-b.com)
      N) Connecter un nouveau compte (choisir un label)
      S) Ignorer (audit sans donnée GSC — CWV terrain via CrUX seulement)
    
  3. « Connecter un nouveau compte » → demande un label, lance connect.py dans le main loop (consentement navigateur), puis auto-découverte des propriétés (sites.list) → re-liste.
  4. Compte choisi → si plusieurs propriétés, demander laquelle correspond au site audité.
  5. Le couple (account, property) retenu est passé explicitement dans le contexte de l'analyzer (bloc BUSINESS CONTEXT du dispatch, STEP 1), qui appellera fetch.sh queries|inspect --account <a> --property <p>.

CrUX ne demande pas de compte (clé API publique) → toujours tenté si CRUX_API_KEY présent, indépendamment du choix de compte.


7. Sûreté concurrentielle (2 sites en parallèle)

Garantie par construction, pas par verrou global :

  • Pas d'état « compte courant ». Le compte + la propriété sont des arguments explicites de chaque fetch.sh. Deux audits (2 sessions Claude, ou 2 sites) ne partagent aucune variable mutable de sélection.
  • Audits = lecture seule du store. Les access tokens sont éphémères en mémoire, jamais écrits. Donc deux audits concurrents ne s'écrivent jamais dessus.
  • Écriture = seulement au connect, atomique (tmp→fsync→rename) sous verrou fcntl, pour couvrir le cas rare de deux consentements simultanés.
  • Le venv est en lecture seule à l'exécution (créé/maj uniquement par make seo-connect).

8. Périmètre data & mapping dans le rapport

Donnée Source Sous-commande Atterrit dans SEO.md
CWV terrain (LCP/INP/CLS 75e pct, mobile+desktop, historique) CrUX API fetch.sh crux §2 Audit technique — note primaire ; PageSpeed labo gardé en secondaire diagnostic
Requêtes (impressions, clics, CTR, position) GSC Search Analytics fetch.sh queries §2/§8 — sous-section « Performance GSC » + quick wins position 4-10
Pages (perf par URL) GSC Search Analytics fetch.sh queries --dim page idem — top pages
Indexation par URL GSC URL Inspection fetch.sh inspect §2 indexabilité — fait vs déduction
  • Scoring : l'axe Technical (STEP 9 de seo-analyzer) se calcule sur le terrain quand dispo ; sinon labo (dégradation).
  • Nouveau contenu de rapport : une sous-section « Performance GSC (90 j) » dans §2, listant top requêtes + les quick wins position 4-10. Reste en français, cohérent avec l'existant.

9. Interface du moteur (fetch.sh)

Contrat stable que les analyzers consomment (JSON sur stdout, exit 0 même en dégradé) :

fetch.sh accounts
  → {"status":"ok","accounts":[{"label":"…","properties":[…],"granted_at":"…"}]}   # [] si aucun compte

fetch.sh crux    --url https://ex.com [--strategy mobile|desktop]
  → {"status":"ok","source":"crux","lcp_p75_ms":…,"inp_p75_ms":…,"cls_p75":…}      # métrique absente = clé omise
  → {"status":"degraded","reason":"no_crux_key"|"no_field_data"|"rate_limited"}
  # 404 page-level → retry automatique origin-level avant de dégrader

fetch.sh queries --account client-a --property sc-domain:ex.com [--days 90] [--dim query|page]
  → {"status":"ok","source":"gsc","rows":[{"key":"…","clicks":…,"impressions":…,"ctr":…,"position":…}]}
  → {"status":"degraded","reason":"no_credentials"|"token_revoked"|"network_error"|"rate_limited"}

fetch.sh inspect --account client-a --property … --url https://ex.com/page
  → {"status":"ok","source":"gsc","indexed":true,"coverage":"…","last_crawl":"…"}
  → {"status":"degraded","reason":"…"}

Règles : jamais de secret dans la sortie ; messages d'erreur génériques ; exit 0 en dégradé (l'analyzer décide de la suite), exit ≠ 0 uniquement sur mauvais usage (args invalides, exit 2). Toute erreur imprévue (HTTP 403/5xx, timeout, DNS) → {"status":"degraded","reason":"unexpected_error"}

  • exit 0 — jamais de traceback, jamais de stdout vide. Tests : SEO_DATA_ENV_FILE permet de substituer le vault (les tests pointent /dev/null — jamais le vrai ~/.claude/.env) ; SEO_DATA_DEBUG=1 réactive stderr pour diagnostiquer.

10. Dégradation gracieuse & posture sécurité

  • Fail-open audit / fail-closed data : creds manquants, refresh échoué, 429 → {"status":"degraded"}, exit 0. L'analyzer bascule sur PageSpeed anonyme et émet en §11 « Connecter GSC : make seo-connect » (réutilise la formulation automation-catalog.md).
  • Redaction : fetch.sh ne logge jamais les variables d'env ni le token ; stdout = JSON de données uniquement ; stderr = messages génériques.
  • Least privilege : scope webmasters.readonly seul ; clé CrUX restreinte à CrUX + PageSpeed.
  • Supply-chain maîtrisée : 3 libs Google officielles, épinglées dans requirements.txt, isolées dans un venv — surface auditablement listée, sans commune mesure avec l'outil tiers.
  • Reprise sur token révoqué : doctor.sh signale, message pointe vers make seo-connect pour re-consentir.

11. Install & déploiement (touch-list)

Séquence respectant l'ordre critique link.sh → vault joignable → consentement (évite le blocker connu : le symlink ~/.claude/.env est créé par link.sh, absent sur machine fraîche ; tout lecteur de creds vise le canonical ~/.claude/.env, pas $REPO/.env).

Fichier Modification
.env.example Ajouter les 3 vars (client id/secret, CrUX key) au format existant (# Used by: / # Get it: + placeholder). Seul fichier versionné touché côté secrets.
install.sh Après link.sh (§5) et claude login (§3) : étape optionnelle idempotente (moule « Press Enter to connect… ») → si aucun compte dans le store, propose make seo-connect ; skip sinon.
Makefile Cible user-facing seo-connect (venv + pip install -r lib/seo-data/requirements.txt + connect.py, rejouable) et extension de la cible test pour découvrir lib/seo-data/*.test.sh (le test vit hors lib/tests/, gaté par config-protection.sh).
doctor.sh Nouveau check (lit ~/.claude/.env canonical) : venv + deps présents ? au moins un compte dans le store ? CRUX_API_KEY présent ? → PASS / WARN, jamais fatal.
.gitleaks.toml Allowlist du token store (cf. §5.4).
.gitignore Ajouter .venv-seo-data/ et seo-data/tokens.json (ceinture + bretelles).

12. Tests

lib/seo-data/seo-data.test.sh, convention du repo (tf/tr_/tn + compteurs PASS/FAIL), sans appel réseau. Placé sous lib/seo-data/ (co-localisé, hors lib/tests/ que hooks/config-protection.sh protège comme dossier-garde) ; make test le découvre via le glob ajouté en Task 6. En TDD : bash lib/seo-data/seo-data.test.sh.

  • Parsing des sous-commandes/args de fetch.sh (bons/mauvais usages, exit codes).
  • Parsing de forme JSON sur fixtures commitées (réponses GSC/CrUX mockées) → shape attendue.
  • Redaction : erreur simulée (token bidon) → aucune valeur secrète dans stdout/stderr.
  • Dégradation : creds absents → {"status":"degraded"} + exit 0 (pas 1).
  • Isolement : deux invocations --account différentes → sélections indépendantes (pas d'état partagé).

Fixtures sous lib/seo-data/fixtures/ (réponses synthétiques, aucun vrai secret/PII).


13. Hors périmètre (YAGNI v1)

  • GA4 (trafic organique), Google Ads / Keyword Planner, Indexing API.
  • Modification de geo-analyzer (le GSC pourrait plus tard éclairer quelles requêtes déclenchent des AI Overviews — v2).
  • Audit multi-propriétés en un seul run (une propriété par audit en v1).
  • Monitoring/drift dans le temps (SQLite) — l'outil tiers le fait ; hors scope v1.
  • CrUX History API (tendance 25 semaines) — v1 = snapshot p75 seulement.
  • Routage de l'appel PageSpeed via fetch.sh avec CRUX_API_KEY (dé-quota) — rejeté en v1 pour raison sécurité : passer la clé à l'analyzer l'exposerait dans le contexte du subagent (ligne de commande curl → risque de fuite dans rapport/log). v2 : sous-commande fetch.sh pagespeed où la clé reste confinée au moteur.
  • Nouveau skill d'audit dédié /gsc (le setup passe par make seo-connect, l'usage par /seo FULL).

14. Liste des fichiers touchés

Créés

  • lib/seo-data/fetch.sh
  • lib/seo-data/google_seo.py
  • lib/seo-data/connect.py
  • lib/seo-data/tokenstore.py
  • lib/seo-data/requirements.txt
  • lib/seo-data/README.md
  • lib/seo-data/seo-data.test.sh
  • lib/seo-data/fixtures/*.json

Modifiés

  • .env.example (3 vars)
  • install.sh (étape consentement optionnelle post-link)
  • Makefile (cible seo-connect)
  • doctor.sh (check creds non-fatal)
  • .gitleaks.toml (allowlist token store)
  • .gitignore (venv + token store)
  • agents/seo-analyzer.md (STEP 4 CWV terrain + sous-section Performance GSC ; STEP 9 axe Technical)
  • skills/seo/SKILL.md (STEP 0 sélection compte + propriété en FULL)
  • agents/resources/automation-catalog.md (section « Google Search Console — connexion OAuth » réutilisable en §11)

Hors repo (générés au setup, jamais commités)

  • ~/.claude/.venv-seo-data/
  • ~/.claude/seo-data/tokens.json

15. Risques & mitigations

Risque Mitigation
Symlink ~/.claude/.env absent sur machine fraîche Lecture du canonical ~/.claude/.env ; consentement après link.sh
Deux connect simultanés corrompent le store Écriture atomique tmp→rename + verrou fcntl
Deux audits sur 2 sites se marchent dessus Compte+propriété explicites par appel ; audits en lecture seule
Secret loggé par erreur Redaction imposée + test dédié
Token store flaggé par scan-secrets Allowlist gitleaks explicite (§5.4)
python3/venv absent doctor.sh warn ; make seo-connect crée le venv ; dégradation si absent
Quota GSC/CrUX (429) Traité comme degraded → audit continue

16. Questions ouvertes résolues

  • Scopes → webmasters.readonly seul.
  • CrUX vs PageSpeed → les deux : CrUX terrain primaire, PageSpeed labo secondaire/fallback.
  • Forme data en §2 → terrain 75e pct primaire, labo en secondaire.
  • GSC obligatoire ? → non, optionnel, dégradation gracieuse ; proposé à chaque FULL.
  • Token expiré → degraded + doctor.sh pointe make seo-connect.
  • Locale → rapports en français, cohérent avec l'existant.
  • Multi-compte → store keyé par label utilisateur (scope inchangé) ; sélection par audit ; isolation par arguments explicites.