#!/usr/bin/env node /** * Self-test of the instrument, on a throwaway fixture jurisdiction. * * The fixture is synthetic BY NECESSITY: the real jurisdictions this package * serves cannot ship inside it. The instrument's first real run — on the * observed jurisdiction's own CI — is the measure that counts; this file only * proves the mechanics: the four states, the homonymy carried, the * append-on-change discipline, and the run journal. * * Exit codes: 0 sound, 1 broken. A refusal names its cause. */ import { cpSync, mkdtempSync, readFileSync, rmSync, unlinkSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { parse as parseYaml } from 'yaml'; const here = dirname(fileURLToPath(import.meta.url)); let failures = 0; const ok = (m) => console.log(` ✓ ${m}`); const fail = (m) => { failures += 1; console.error(` ✗ ${m}`); }; const assert = (cond, m) => (cond ? ok(m) : fail(m)); function run(repo, extra = []) { return execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'reglage.yaml', '--repo', repo, ...extra], { encoding: 'utf8', }); } const repo = mkdtempSync(join(tmpdir(), 'instrument-selftest-')); cpSync(join(here, 'selftest/fixture'), repo, { recursive: true }); const coupe = () => parseYaml(readFileSync(join(repo, 'docs/parcours/_mesures/coupe.yaml'), 'utf8')); const journal = () => parseYaml(readFileSync(join(repo, 'docs/parcours/_mesures/derniere-execution.yaml'), 'utf8')); const byInv = (items, id) => items.filter((c) => c.invariant === id).at(-1); try { console.log('First run — the four states, observed:'); run(repo); const first = coupe(); assert(first.length === 3, `three findings appended (got ${first.length})`); assert(byInv(first, 'INV-001')?.etat === 'renforce', 'INV-001 renforce — due stratum and above'); assert((byInv(first, 'INV-001')?.strates_constatees ?? []).join(',') === 'domaine,application', 'INV-001 strata ordered as the tuning declares them'); assert(Array.isArray(byInv(first, 'INV-001')?.homonymie), 'INV-001 carries its homonymy — two statements, one id, exposed not resolved'); assert(byInv(first, 'INV-001')?.citations_hors_epreuve === 1, 'INV-001 counts its non-test citation without treating it as proof'); assert((byInv(first, 'INV-001')?.rangs_constates ?? []).join(',') === '4,3', 'INV-001 carries the third-party ranks its tuning declares, in stratum order'); assert(byInv(first, 'INV-003')?.rangs_constates === undefined, 'INV-003 (no locus) carries no ranks — nothing observed maps to nothing'); assert(JSON.stringify(journal().correspondance_rangs) === JSON.stringify({ domaine: 4, application: 3, interface: 1 }), 'journal publishes the full rank correspondence for consumers joining old findings'); // Named guards — candidates, never findings: a domain exception nobody declared. const g = journal().gardes_nommees; assert(g && g.total === 1 && g.sans_declaration === 1, 'the fixture has one named domain guard, and no journey declares it'); assert(g.candidats[0].nom === 'RuleViolatedException' && g.candidats[0].strate === 'domaine', 'the candidate carries its name and its stratum'); assert(g.candidats[0].dans_parcours === false && g.candidats[0].dans_adr === true && g.candidats[0].eprouve === false, 'the candidate says where it is cited — ADR yes, journey no, test no'); assert(!('candidats' in journal()) , 'candidates live under gardes_nommees, not at the journal root'); // Backward compatibility: a tuning without the key gets no section at all. // On its OWN copy of the fixture: a second run on `repo` would rewrite the // journal and falsify the assertions that follow (caught by the self-test // itself on 2026-09-21 — a guard that shares state with what it guards). const repo2 = mkdtempSync(join(tmpdir(), 'instrument-selftest-compat-')); cpSync(join(here, 'selftest/fixture'), repo2, { recursive: true }); execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'reglage-sans-gardes.yaml', '--repo', repo2], { encoding: 'utf8', stdio: 'pipe' }); const journal2 = parseYaml(readFileSync(join(repo2, 'docs/parcours/_mesures/derniere-execution.yaml'), 'utf8')); assert(!('gardes_nommees' in journal2), 'a tuning that does not ask for named guards changes nothing'); assert(byInv(first, 'INV-002')?.etat === 'mal_domicilie', 'INV-002 mal_domicilie — proven only away from home'); assert(byInv(first, 'INV-003')?.etat === 'aucun_locus', 'INV-003 aucun_locus — named by no test'); assert(journal().constats_nouveaux === 3 && journal().invariants === 3, 'journal counts the run'); console.log('Second run — an unchanged finding produces no event:'); run(repo); assert(coupe().length === 3, 'nothing appended on an unchanged repository'); assert(journal().constats_nouveaux === 0, 'journal still rewritten — freshness is witnessed even when nothing changed'); console.log('Third run — a guard disappears, the finding changes:'); unlinkSync(join(repo, 'src/test/java/x/Domain/RuleTest.java')); run(repo); const third = coupe(); assert(third.length === 4, 'exactly one new finding appended'); assert(byInv(third, 'INV-001')?.etat === 'mal_domicilie', 'INV-001 fell to mal_domicilie when its due-stratum test vanished'); assert(byInv(third, 'INV-001')?.id === 'essai/MES-INV-001-2', 'the new finding takes the next sequence number, the old one stands'); console.log('Dry run — states without writing:'); const before = readFileSync(join(repo, 'docs/parcours/_mesures/coupe.yaml'), 'utf8'); run(repo, ['--dry-run']); assert(readFileSync(join(repo, 'docs/parcours/_mesures/coupe.yaml'), 'utf8') === before, '--dry-run writes nothing'); console.log('Loud failure — a broken prerequisite refuses, it never half-runs:'); let refused = false; try { execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'absent.yaml', '--repo', repo], { encoding: 'utf8', stdio: 'pipe' }); } catch (e) { refused = String(e.stderr).includes('REFUS'); } assert(refused, 'a missing tuning file is refused with its remedy, not defaulted'); } finally { rmSync(repo, { recursive: true, force: true }); } if (failures > 0) { console.error(`Instrument self-test: ${failures} failure(s).`); process.exit(1); } console.log('Instrument self-test: sound.');