Aller au contenu

Spec — Vague 3 d'automatisation des portes + refonte safeResolve

Document d'entrée pour une session d'implémentation. Périmètre : 4 nouvelles portes exécutables + bascule de safeResolve vers la sémantique NEEDS-REVIEW.


1. Refonte safeResolve — mode NEEDS-REVIEW

Contexte

Aujourd'hui, safeResolve (dans sdlc/validators.js) throw une erreur si le chemin interpolé sort du projectRoot. Cette erreur est catchée par chaque validateur fichier et convertie en fail → verdict NO-GO.

Dans un environnement dédié et maîtrisé, cette contrainte est trop stricte : un chemin hors projet peut être intentionnel (artefact partagé entre projets, montage externe, etc.). L'objectif est de transformer cette détection en signal plutôt qu'en refus, pour que l'opérateur humain décide.

Changement de comportement

Cas Avant Après
Chemin dans projet, fichier présent pass → GO pass → GO
Chemin dans projet, fichier absent fail → NO-GO fail → NO-GO
Chemin hors projet fail → NO-GO warn → NEEDS-REVIEW
Erreur parse frontmatter fail → NO-GO fail → NO-GO

L'opérateur garde la main via sdlc move --force (déjà implémenté, déjà tracé dans l'historique).

Diffs à appliquer — sdlc/validators.js

1.1. safeResolve : retourner un résultat discriminé au lieu de throw

// AVANT
function safeResolve(projectRoot, relPath) {
  const abs = path.resolve(projectRoot, relPath);
  const rootAbs = path.resolve(projectRoot);
  if (!abs.startsWith(rootAbs + path.sep) && abs !== rootAbs) {
    throw new Error(`Chemin hors projet refusé : ${relPath}`);
  }
  return abs;
}

// APRÈS
function safeResolve(projectRoot, relPath) {
  const abs = path.resolve(projectRoot, relPath);
  const rootAbs = path.resolve(projectRoot);
  const inside = abs.startsWith(rootAbs + path.sep) || abs === rootAbs;
  return { abs, inside, relPath };
}

1.2. Adapter les 3 validateurs fichiers

Pour validateFileExists, validateFileNotEmpty, validateFileContains, remplacer :

try {
  const abs = safeResolve(ctx.projectRoot, relPath);
  // ... logique existante
} catch (e) {
  return fail(id, desc, e.message);
}

par :

const { abs, inside } = safeResolve(ctx.projectRoot, relPath);
if (!inside) {
  return warn(id, desc,
    `Chemin hors projet (${relPath}) — validation humaine requise`,
    relPath);
}
// ... logique existante (sans try/catch sur safeResolve)

Le try/catch autour des appels fs.* peut être conservé si on veut capturer les erreurs d'I/O (permissions), mais ce n'est pas l'objet de ce changement.

1.3. Adapter readFrontmatter (helper partagé)

// AVANT
function readFrontmatter(projectRoot, relPath) {
  const abs = safeResolve(projectRoot, relPath);
  if (!fs.existsSync(abs)) return { error: `Fichier absent : ${relPath}` };
  try {
    return { fm: parseFrontmatter(fs.readFileSync(abs, 'utf8')) };
  } catch (e) {
    return { error: `Parse frontmatter : ${e.message}` };
  }
}

// APRÈS
function readFrontmatter(projectRoot, relPath) {
  const { abs, inside } = safeResolve(projectRoot, relPath);
  if (!inside) return { outside: true };
  if (!fs.existsSync(abs)) return { error: `Fichier absent : ${relPath}` };
  try {
    return { fm: parseFrontmatter(fs.readFileSync(abs, 'utf8')) };
  } catch (e) {
    return { error: `Parse frontmatter : ${e.message}` };
  }
}

1.4. Adapter les 3 validateurs frontmatter

Pour validateFrontmatterHas, validateFrontmatterIn, validateFrontmatterRegex, remplacer :

const { fm, error } = readFrontmatter(ctx.projectRoot, relPath);
if (error) return fail(id, desc, error, relPath);

par :

const result = readFrontmatter(ctx.projectRoot, relPath);
if (result.outside) {
  return warn(id, desc,
    `Chemin hors projet (${relPath}) — validation humaine requise`,
    relPath);
}
if (result.error) return fail(id, desc, result.error, relPath);
const fm = result.fm;

Tests à ajouter — sdlc/__tests__/validators.test.js

  1. validateFileExists avec path interpolé sortant de projectRoot → status === 'warn'
  2. validateFrontmatterHas idem → status === 'warn'
  3. validateFileContains idem → status === 'warn'

Pattern de test : utiliser un path comme ../outside/{id}.md et un projectRoot dans un tmpdir, vérifier que le status est warn et que la raison contient "hors projet".

Tests à ajouter — sdlc/__tests__/evaluator.test.js

  1. Porte avec un check hors projet → verdict global NEEDS-REVIEW (pas NO-GO)

Points de vigilance

  • Rétro-compat API : interpolate, VALIDATORS, registerValidator inchangés. safeResolve change de signature (object au lieu de string) — vérifier que rien ne le consomme ailleurs que dans validators.js.
  • Tests existants : aucun chemin hors projet dans le schema.js actuel → zéro régression attendue.

2. Vague 3 — 4 nouvelles portes automatisées

2.1. be.P2 — GO Cadrage (COMEX)

Transition : be.REVUE_BUSINESS → CADRAGE_EN_COURS

Ajouter à la définition de la transition dans sdlc/schema.js :

checks: [
  { kind: 'fileExists', id: 'eo-exists',
    path: '.product/.epics/{id}/eo.md' },
  { kind: 'fileExists', id: 'ehs-draft',
    path: '.product/.epics/{id}/ehs.md' },
  { kind: 'frontmatterHas', id: 'comex-decision',
    path: '.product/.epics/{id}/eo.md',
    keys: ['comex_decision', 'comex_date', 'sponsor'] },
  { kind: 'frontmatterIn', id: 'comex-go',
    path: '.product/.epics/{id}/eo.md',
    key: 'comex_decision', allowed: ['go', 'conditional-go'] },
],

Rationale : verrouille la trace COMEX avant d'engager le cadrage. ehs.md en draft est attendu dès P2 (sera figé à P3).

2.2. us.P5-DOR — DOR Récit

Transition : us.AFFINAGE → PRET_DOR

invariants: [
  { kind: 'parentStatusIn', id: 'parent-fe-ready',
    allowed: ['PRET_FABRICATION', 'FABRICATION'] },
],
checks: [
  { kind: 'fileExists', id: 'story-file',
    path: '.product/.stories/{id}/story.md' },
  { kind: 'frontmatterHas', id: 'dor-meta',
    path: '.product/.stories/{id}/story.md',
    keys: ['acceptance_criteria', 'estimate', 'story_points'] },
  { kind: 'frontmatterRegex', id: 'story-points-numeric',
    path: '.product/.stories/{id}/story.md',
    key: 'story_points', pattern: '^\\d+$' },
],

Rationale : le DOR récit exige un artefact story.md avec AC/estimate/SP, et le parent FE doit être au moins en PRET_FABRICATION (cohérence du flux de sprint).

2.3. fe.P9-init — Déploiement initié

Transition : fe.PRET_LIVRAISON → DEPLOIEMENT

checks: [
  { kind: 'fileExists', id: 'deployment-plan',
    path: '.product/.features/{id}/deployment-plan.md' },
  { kind: 'frontmatterHas', id: 'deploy-meta',
    path: '.product/.features/{id}/deployment-plan.md',
    keys: ['deploy_window', 'rollback_plan', 'observability_url'] },
],

Rationale : pas de déploiement sans plan documenté (fenêtre, rollback, observabilité).

2.4. fe.P9-term — Feature livrée

Transition : fe.DEPLOIEMENT → LIVRE

invariants: [
  { kind: 'allChildrenStatusIn', id: 'us-all-valide',
    allowed: ['VALIDE', 'ARCHIVED'] },
],
checks: [
  { kind: 'fileExists', id: 'deployment-report',
    path: '.product/.features/{id}/deployment-report.md' },
  { kind: 'frontmatterHas', id: 'deploy-evidence',
    path: '.product/.features/{id}/deployment-report.md',
    keys: ['deployed_at', 'deployed_by', 'smoke_tests_status'] },
  { kind: 'frontmatterIn', id: 'smoke-tests-green',
    path: '.product/.features/{id}/deployment-report.md',
    key: 'smoke_tests_status', allowed: ['green', 'passed', 'success'] },
],

Rationale : verrouille la livraison sur 3 piliers — toutes US livrées/archivées, rapport de déploiement présent, smoke tests verts.


3. Tests à ajouter — sdlc/__tests__/evaluator.test.js

Un test par porte (structure alignée sur les tests existants P4-DOR, P6-PR, P6-DoD, P8) :

3.1. be.P2 GO Cadrage — NO-GO si EO absente

Setup : BE en REVUE_BUSINESS, pas de eo.md. Attendu : verdict NO-GO, raison contient "eo.md".

3.2. be.P2 GO Cadrage — NO-GO si comex_decision = 'no-go'

Setup : EO + EHS présents, comex_decision: no-go dans frontmatter. Attendu : verdict NO-GO, comex-go en fail.

3.3. be.P2 GO Cadrage — GO sur conditional-go

Setup : EO + EHS + frontmatter complet, comex_decision: conditional-go. Attendu : verdict GO.

3.4. us.P5-DOR — NO-GO si parent FE en ANALYSE

Setup : US en AFFINAGE, FE parent en ANALYSE (pas encore PRET_FABRICATION). Attendu : verdict NO-GO, parent-fe-ready en fail.

3.5. us.P5-DOR — NO-GO si story_points non numérique

Setup : story.md avec story_points: "TBD". Attendu : verdict NO-GO, story-points-numeric en fail.

3.6. fe.P9-init — NO-GO sans deployment-plan.md

Setup : FE en PRET_LIVRAISON, aucun plan. Attendu : verdict NO-GO.

3.7. fe.P9-term — NO-GO si une US en DEV

Setup : FE en DEPLOIEMENT, report présent et vert, mais 1 US enfant en DEV. Attendu : verdict NO-GO, us-all-valide en fail.

3.8. fe.P9-term — NO-GO si smoke_tests_status = 'red'

Setup : tout en place sauf smoke_tests_status: red. Attendu : verdict NO-GO, smoke-tests-green en fail.

3.9. (lié au point 1) porte avec chemin hors projet → NEEDS-REVIEW

Setup : une porte quelconque avec un check dont le path interpolé sort du projectRoot. Attendu : verdict NEEDS-REVIEW, au moins un résultat en warn.


4. Séquence d'implémentation suggérée

  1. Refondre safeResolve et les 6 validateurs (§1)
  2. Ajouter les 3 tests validators (§1 — tests)
  3. Lancer node --test sdlc/__tests__/ → vert
  4. Ajouter les 4 blocs checks/invariants dans schema.js (§2)
  5. Ajouter les 9 tests evaluator (§3)
  6. Lancer à nouveau la suite → vert (55+ tests attendus)
  7. Commit unique ou deux commits séparés :
  8. feat(sdlc): safeResolve en mode NEEDS-REVIEW (chemin hors projet = warn)
  9. feat(sdlc): vague 3 — be.P2, us.P5-DOR, fe.P9-init, fe.P9-term automatisées

5. État de couverture après cette vague

Entité Portes automatisées Portes descriptives restantes
BE P2, P3, P10 (3) P1, P10-term/persévère/pivot, blocages, archives (15)
FE P4, P4-DOR, P7, P8, P9-init, P9-term (6) Sprint-start, archives (3)
US P5-start, P5-DOR, Sprint-dev, P6-PR, P6-DoD (5) PO-demo, Defect, archives (4)

Total : 14 portes automatisées / 42 (vs 10 avant cette vague).

Les portes restantes sont majoritairement des décisions humaines pures (COMEX terminal, archives, déblocages) pour lesquelles l'automatisation n'apporterait qu'une documentation de façade.