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¶
validateFileExistsavec path interpolé sortant deprojectRoot→status === 'warn'validateFrontmatterHasidem →status === 'warn'validateFileContainsidem →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¶
- Porte avec un check hors projet → verdict global
NEEDS-REVIEW(pas NO-GO)
Points de vigilance¶
- Rétro-compat API :
interpolate,VALIDATORS,registerValidatorinchangés.safeResolvechange de signature (object au lieu de string) — vérifier que rien ne le consomme ailleurs que dansvalidators.js. - Tests existants : aucun chemin hors projet dans le
schema.jsactuel → 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¶
- Refondre
safeResolveet les 6 validateurs (§1) - Ajouter les 3 tests validators (§1 — tests)
- Lancer
node --test sdlc/__tests__/→ vert - Ajouter les 4 blocs checks/invariants dans
schema.js(§2) - Ajouter les 9 tests evaluator (§3)
- Lancer à nouveau la suite → vert (55+ tests attendus)
- Commit unique ou deux commits séparés :
feat(sdlc): safeResolve en mode NEEDS-REVIEW (chemin hors projet = warn)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.