diff --git a/instrument/README.md b/instrument/README.md new file mode 100644 index 0000000..a2e9636 --- /dev/null +++ b/instrument/README.md @@ -0,0 +1,69 @@ +# L'instrument — où chaque garde vit réellement + +Chaque garde vit à son étage ; l'instrument regarde où elle est réellement +posée. Il court **dans la CI de la juridiction observée** — la juridiction se +mesure elle-même ; le registre publie l'instrument et reçoit les constats +(`temoin/ADR-018`, `prismagram-corpus/ADR-061` §2.6 et §2.8). + +## Ce qu'il constate + +Pour chaque invariant que la juridiction déclare dans le front-matter de ses +parcours (`invariants:`), l'instrument cherche chaque fichier qui le **nomme** +et rend un état : + +| État | Sens | +|---|---| +| `domicilie` | nommé par une épreuve à son étage dû, nulle part au-dessus | +| `renforce` | nommé par des épreuves à son étage dû **et** au-dessus — défense en profondeur, signalée positivement, jamais blâmée | +| `mal_domicilie` | nommé par des épreuves **uniquement hors** de son étage dû — le seul écart | +| `aucun_locus` | nommé par aucune épreuve | + +L'instrument constate la **nomination**, pas l'exécution : c'est la CI de la +juridiction qui fait passer ses épreuves ; l'instrument dit seulement où elles +habitent. Un même identifiant déclaré avec deux énoncés différents est une +homonymie : elle est portée dans le constat, jamais résolue ici. + +## Le réglage — il appartient à la juridiction + +L'instrument ne connaît aucun chemin d'aucun projet : toute convention vit +dans un fichier de réglage que la juridiction tient dans son dépôt, et que +chaque constat épingle par son empreinte. Voir +[`selftest/fixture/reglage.yaml`](selftest/fixture/reglage.yaml) pour la forme +complète : `juridiction`, `cadence_heures`, `parcours`, `strates` (la première +— ou celle marquée `due: true` — est l'étage dû), `epreuves`, `sortie`, +`journal_execution`. + +## Ce qu'il écrit — chez la juridiction, jamais ailleurs + +- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état + change** — un constat inchangé ne produit aucun événement. Chaque constat + porte ses pièces (chemin, ligne, strate), sa révision, l'empreinte du + réglage : il est recalculable par quiconque clone le dépôt. +- **`journal_execution`** : réécrit à **chaque** exécution — le témoin de + fraîcheur. Un journal plus vieux que la cadence déclarée est un écart que le + lecteur du dépôt constate ; jamais un silence. + +## Exécution + +```sh +node instrument/run.mjs --config docs/parcours/_meta/strates.yaml +# --repo DIR : racine du dépôt mesuré (défaut : répertoire courant) +# --dry-run : affiche les constats sans rien écrire +``` + +Sort en erreur **uniquement** sur prérequis cassé (réglage absent ou +illisible, aucun parcours, aucun invariant) — jamais sur un écart constaté : +l'écart est le produit, pas une panne. `npm run instrument:selftest` joue +l'auto-épreuve sur une juridiction d'essai jetable. + +## Limites dites (v0) + +- Seuls les invariants déclarés dans les parcours sont mesurés ; un invariant + qui ne vit que dans une ADR n'est pas encore vu. +- La citation est textuelle (mot entier) ; une épreuve qui vérifie sans nommer + n'est pas constatée — nommer ses invariants dans ses épreuves est le geste + que l'instrument récompense. +- Une citation est attribuée à l'identifiant que les parcours déclarent ; si + une ADR déclare un homonyme, l'attribution peut se tromper. Unifier son + espace de noms d'invariants est un geste de la juridiction, en amont de + toute mesure. diff --git a/instrument/run.mjs b/instrument/run.mjs new file mode 100644 index 0000000..d09ab1c --- /dev/null +++ b/instrument/run.mjs @@ -0,0 +1,317 @@ +#!/usr/bin/env node +/** + * Instrument — where each guard actually lives (temoin/ADR-018, + * prismagram-corpus/ADR-061 §2.6 and §2.8). + * + * Runs INSIDE the observed jurisdiction's CI: the jurisdiction measures + * itself; the registry publishes the instrument and receives the findings. + * The instrument reads the invariants the jurisdiction declares in its + * journey documents, finds every file that names them, and states, per + * invariant, at which stratum its verification is observed: + * + * domicilie named by a test at its due stratum, nowhere above + * renforce named by tests at its due stratum AND above — defence in + * depth, reported positively, never blamed + * mal_domicilie named by tests only OUTSIDE its due stratum — the one gap + * aucun_locus named by no test at all + * + * The instrument states where guards live; it does not run them, and it does + * not fail on gaps: the gap IS the product. It exits non-zero only when a + * prerequisite is broken — loud failure, never a silent success. + * + * Findings are appended (never rewritten) to the jurisdiction's own + * repository, and only when they change: an unchanged finding produces no + * event. The run journal is rewritten at every run, so that staleness can be + * judged against the declared cadence by whoever reads the repository. + * + * Usage: + * node run.mjs --config docs/parcours/_meta/strates.yaml [--repo DIR] [--dry-run] + */ +import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; +import { dirname, isAbsolute, join, relative, sep } from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +const JOURNEY_FILE = /^P[MU]-\d{3}.*\.md$/; +const IGNORED_DIRS = new Set(['node_modules', 'target', 'dist', 'build', 'coverage', 'out']); +const MAX_FILE_BYTES = 2_000_000; +const MAX_PIECES_PER_STRATUM = 5; + +function refuse(message) { + console.error(`Instrument — REFUS : ${message}`); + process.exit(1); +} + +function parseArgs(argv) { + const args = { repo: process.cwd(), dryRun: false, config: null }; + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === '--config') args.config = argv[i + 1] ?? null, i += 1; + else if (argv[i] === '--repo') args.repo = argv[i + 1] ?? args.repo, i += 1; + else if (argv[i] === '--dry-run') args.dryRun = true; + else refuse(`argument inconnu « ${argv[i]} » — attendus : --config , --repo , --dry-run`); + } + if (args.config === null) + refuse('--config est requis. Le réglage appartient à la juridiction : voir instrument/README.md pour sa forme.'); + return args; +} + +/** The tuning file belongs to the jurisdiction; the instrument only knows its shape. */ +function loadConfig(repo, path) { + const full = isAbsolute(path) ? path : join(repo, path); + if (!existsSync(full)) + refuse(`le réglage « ${path} » n'existe pas dans ${repo}. Rien n'est créé à la volée : la juridiction pose son réglage (voir instrument/README.md), puis relance.`); + let cfg; + try { + cfg = parseYaml(readFileSync(full, 'utf8')); + } catch (e) { + refuse(`le réglage « ${path} » n'est pas un YAML lisible : ${e instanceof Error ? e.message.split('\n')[0] : e}`); + } + for (const key of ['juridiction', 'parcours', 'strates', 'epreuves', 'sortie', 'journal_execution']) + if (cfg?.[key] === undefined) refuse(`le réglage « ${path} » ne déclare pas « ${key} »`); + if (!Array.isArray(cfg.strates) || cfg.strates.length === 0) + refuse(`le réglage « ${path} » doit déclarer au moins une strate`); + for (const s of cfg.strates) + if (typeof s?.nom !== 'string' || !Array.isArray(s?.chemins)) + refuse(`chaque strate du réglage porte « nom » et « chemins » — l'une des deux manque`); + const due = cfg.strates.find((s) => s.due === true) ?? cfg.strates[0]; + return { ...cfg, due: due.nom, raw: readFileSync(full, 'utf8'), path }; +} + +/** Glob → RegExp. Supports **, * and ? — nothing else, on purpose. */ +function globToRegExp(glob) { + let out = '^'; + for (let i = 0; i < glob.length; i += 1) { + const c = glob[i]; + if (c === '*' && glob[i + 1] === '*') { + out += glob[i + 2] === '/' ? '(?:.*/)?' : '.*'; + i += glob[i + 2] === '/' ? 2 : 1; + } else if (c === '*') out += '[^/]*'; + else if (c === '?') out += '[^/]'; + else out += /[A-Za-z0-9_\-/]/.test(c) ? c : `\\${c}`; + } + return new RegExp(`${out}$`); +} + +function walk(root, dir, acc) { + for (const name of readdirSync(dir)) { + if (name.startsWith('.')) continue; + const full = join(dir, name); + const st = statSync(full, { throwIfNoEntry: false }); + if (st === undefined) continue; + if (st.isDirectory() && !IGNORED_DIRS.has(name)) walk(root, full, acc); + else if (st.isFile() && st.size <= MAX_FILE_BYTES) acc.push(relative(root, full).split(sep).join('/')); + } + return acc; +} + +function frontMatter(text) { + const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(text); + if (m === null) return null; + try { return parseYaml(m[1]); } catch { return null; } +} + +/** + * Invariants as the jurisdiction declares them, in its journeys' front matter. + * The same id declared with two different statements is an homonymy: it is + * carried into the finding, never resolved here — the instrument exposes, it + * does not reconcile. + */ +function collectInvariants(repo, dir) { + let files; + try { + files = readdirSync(join(repo, dir)).filter((f) => JOURNEY_FILE.test(f)).sort(); + } catch { + refuse(`le répertoire des parcours « ${dir} » est introuvable — rien à mesurer n'est pas un succès`); + } + const byId = new Map(); + for (const f of files) { + const fm = frontMatter(readFileSync(join(repo, dir, f), 'utf8')); + const journey = typeof fm?.id === 'string' ? fm.id : (f.match(/^(P[MU]-\d{3})/)?.[1] ?? f); + const declared = Array.isArray(fm?.invariants) ? fm.invariants : []; + for (const inv of declared) { + if (typeof inv?.id !== 'string') continue; + const cur = byId.get(inv.id) ?? { id: inv.id, statements: new Map(), declaredBy: [] }; + cur.declaredBy.push(journey); + const s = typeof inv.enonce === 'string' ? inv.enonce.trim() : ''; + if (s !== '' && !cur.statements.has(s)) cur.statements.set(s, journey); + byId.set(inv.id, cur); + } + } + if (byId.size === 0) + refuse(`aucun invariant déclaré dans « ${dir} » — rien à mesurer n'est pas un succès`); + return { invariants: byId, journeys: files.length }; +} + +const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** Every file covered by a stratum, with its stratum and its test-ness. */ +function classifyFiles(repo, cfg) { + const strata = cfg.strates.map((s) => ({ nom: s.nom, res: s.chemins.map(globToRegExp) })); + const testRes = cfg.epreuves.map(globToRegExp); + const out = []; + for (const path of walk(repo, repo, [])) { + const stratum = strata.find((s) => s.res.some((re) => re.test(path))); + if (stratum === undefined) continue; + out.push({ path, stratum: stratum.nom, isTest: testRes.some((re) => re.test(path)) }); + } + return out; +} + +/** Where each invariant is NAMED — the instrument observes naming, not execution. */ +function scanCitations(repo, files, ids) { + const pattern = new RegExp(`\\b(${ids.map(escapeRe).join('|')})\\b`); + const hits = new Map(ids.map((id) => [id, []])); + for (const f of files) { + const text = readFileSync(join(repo, f.path), 'utf8'); + if (!pattern.test(text)) continue; + const each = new RegExp(pattern.source, 'g'); + text.split('\n').forEach((line, i) => { + for (const m of line.matchAll(each)) + hits.get(m[1]).push({ path: f.path, line: i + 1, stratum: f.stratum, isTest: f.isTest }); + }); + } + return hits; +} + +function judge(hits, cfg) { + const order = cfg.strates.map((s) => s.nom); + const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))] + .sort((a, b) => order.indexOf(a) - order.indexOf(b)); + if (strata.length === 0) return { etat: 'aucun_locus', strates: [] }; + if (!strata.includes(cfg.due)) return { etat: 'mal_domicilie', strates: strata }; + return { etat: strata.length === 1 ? 'domicilie' : 'renforce', strates: strata }; +} + +/** One sentence a reader understands without the doctrine — layer 1 wording. */ +function sentence(id, verdict, due) { + const above = verdict.strates.filter((s) => s !== due); + if (verdict.etat === 'aucun_locus') return `${id} : aucune épreuve ne le nomme — aucun endroit constaté.`; + if (verdict.etat === 'mal_domicilie') + return `${id} : vérifié seulement hors de son étage (${verdict.strates.join(', ')}, jamais ${due}) — la garde n'est pas à sa place.`; + if (verdict.etat === 'renforce') + return `${id} : vérifié à son étage (${due}) et au-dessus (${above.join(', ')}) — défense en profondeur.`; + return `${id} : vérifié à son étage (${due}).`; +} + +/** Last recorded finding per invariant, and how many findings each has. */ +function readPrevious(sortiePath) { + const last = new Map(); + const counts = new Map(); + if (!existsSync(sortiePath)) return { last, counts }; + const items = parseYaml(readFileSync(sortiePath, 'utf8')); + for (const it of Array.isArray(items) ? items : []) { + if (typeof it?.invariant !== 'string') continue; + counts.set(it.invariant, (counts.get(it.invariant) ?? 0) + 1); + last.set(it.invariant, `${it.etat}|${(it.strates_constatees ?? []).join(',')}`); + } + return { last, counts }; +} + +function pieces(hits) { + const kept = []; + const omitted = { n: 0 }; + const perStratum = new Map(); + for (const h of hits.filter((x) => x.isTest)) { + const n = (perStratum.get(h.stratum) ?? 0) + 1; + perStratum.set(h.stratum, n); + if (n <= MAX_PIECES_PER_STRATUM) kept.push({ chemin: h.path, ligne: h.line, strate: h.stratum }); + else omitted.n += 1; + } + return { kept, omitted: omitted.n }; +} + +function constatItem(cfg, inv, verdict, hits, seq, meta) { + const { kept, omitted } = pieces(hits); + const item = { + id: `${cfg.juridiction}/MES-${inv.id}-${seq}`, + classe: 'measure', + invariant: inv.id, + declare_par: inv.declaredBy, + enonce: inv.statements.size > 0 ? [...inv.statements.keys()][0] : null, + etat: verdict.etat, + constat: sentence(inv.id, verdict, cfg.due), + strates_constatees: verdict.strates, + pieces: kept, + ...(omitted > 0 ? { pieces_omises: omitted } : {}), + citations_hors_epreuve: hits.filter((h) => !h.isTest).length, + ...(inv.statements.size > 1 + ? { homonymie: [...inv.statements.entries()].map(([enonce, par]) => ({ enonce, declare_par: par })) } + : {}), + provenance: 'measured', + confiance: 1.0, + mesure_le: meta.now, + revision: meta.revision, + reglage_empreinte: meta.reglageHash, + }; + return item; +} + +function gitRevision(repo) { + try { + return execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + } catch { + return null; + } +} + +const FILE_HEADER = `# Constats de l'instrument — en ajout seul, un constat ne se réécrit jamais. +# Un état qui change s'écrit comme un constat nouveau (temoin/ADR-018). +`; + +function main() { + const args = parseArgs(process.argv.slice(2)); + const cfg = loadConfig(args.repo, args.config); + const { invariants, journeys } = collectInvariants(args.repo, cfg.parcours); + const files = classifyFiles(args.repo, cfg); + const hits = scanCitations(args.repo, files, [...invariants.keys()]); + const meta = { + now: new Date().toISOString(), + revision: gitRevision(args.repo), + reglageHash: `sha256:${createHash('sha256').update(cfg.raw).digest('hex')}`, + }; + const sortiePath = join(args.repo, cfg.sortie); + const { last, counts } = readPrevious(sortiePath); + const tally = { domicilie: 0, renforce: 0, mal_domicilie: 0, aucun_locus: 0 }; + const fresh = []; + for (const inv of [...invariants.values()].sort((a, b) => a.id.localeCompare(b.id))) { + const verdict = judge(hits.get(inv.id), cfg); + tally[verdict.etat] += 1; + const key = `${verdict.etat}|${verdict.strates.join(',')}`; + if (last.get(inv.id) === key) continue; + fresh.push(constatItem(cfg, inv, verdict, hits.get(inv.id), (counts.get(inv.id) ?? 0) + 1, meta)); + } + emit(args, cfg, { journeys, invariants: invariants.size, tally, fresh, meta, sortiePath }); +} + +function emit(args, cfg, r) { + const journal = { + juridiction: cfg.juridiction, + executee_le: r.meta.now, + revision: r.meta.revision, + ...(typeof cfg.cadence_heures === 'number' ? { cadence_heures: cfg.cadence_heures } : {}), + parcours_lus: r.journeys, + invariants: r.invariants, + etats: r.tally, + constats_nouveaux: r.fresh.length, + reglage_empreinte: r.meta.reglageHash, + }; + if (args.dryRun) { + if (r.fresh.length > 0) process.stdout.write(stringifyYaml(r.fresh)); + console.log('--- (--dry-run : rien n\'est écrit)'); + } else { + if (r.fresh.length > 0) { + mkdirSync(dirname(r.sortiePath), { recursive: true }); + const prefix = existsSync(r.sortiePath) ? '' : FILE_HEADER; + appendFileSync(r.sortiePath, prefix + stringifyYaml(r.fresh)); + } + mkdirSync(dirname(join(args.repo, cfg.journal_execution)), { recursive: true }); + writeFileSync(join(args.repo, cfg.journal_execution), stringifyYaml(journal)); + } + console.log(`Instrument — ${cfg.juridiction}${r.meta.revision === null ? '' : ` @ ${r.meta.revision.slice(0, 10)}`}`); + console.log(` ${r.invariants} invariant(s) déclaré(s) par ${r.journeys} parcours`); + console.log(` à son étage : ${r.tally.domicilie} · renforcé : ${r.tally.renforce} · hors de son étage : ${r.tally.mal_domicilie} · jamais éprouvé : ${r.tally.aucun_locus}`); + console.log(` constat(s) nouveau(x) : ${r.fresh.length}${args.dryRun ? '' : ` → ${cfg.sortie}`}`); +} + +main(); diff --git a/instrument/selftest.mjs b/instrument/selftest.mjs new file mode 100644 index 0000000..cf0f7ff --- /dev/null +++ b/instrument/selftest.mjs @@ -0,0 +1,85 @@ +#!/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-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.'); diff --git a/instrument/selftest/fixture/app/src/flow.essai.ts b/instrument/selftest/fixture/app/src/flow.essai.ts new file mode 100644 index 0000000..1aa9e90 --- /dev/null +++ b/instrument/selftest/fixture/app/src/flow.essai.ts @@ -0,0 +1,2 @@ +// INV-002 proven at the interface stratum. +export {} diff --git a/instrument/selftest/fixture/docs/parcours/PM-001-essai.md b/instrument/selftest/fixture/docs/parcours/PM-001-essai.md new file mode 100644 index 0000000..3a71f42 --- /dev/null +++ b/instrument/selftest/fixture/docs/parcours/PM-001-essai.md @@ -0,0 +1,8 @@ +--- +id: PM-001 +titre: Processus d'essai +invariants: + - id: INV-001 + enonce: Un essai vaut toujours, constaté ou non. +--- +# PM-001 — le même identifiant, un autre énoncé : homonymie à exposer diff --git a/instrument/selftest/fixture/docs/parcours/PU-001-essai.md b/instrument/selftest/fixture/docs/parcours/PU-001-essai.md new file mode 100644 index 0000000..c52ea00 --- /dev/null +++ b/instrument/selftest/fixture/docs/parcours/PU-001-essai.md @@ -0,0 +1,12 @@ +--- +id: PU-001 +titre: Parcours d'essai +invariants: + - id: INV-001 + enonce: Un essai ne vaut que constaté. + - id: INV-002 + enonce: Nul ne franchit sans être nommé. + - id: INV-003 + enonce: Ce que personne n'éprouve n'existe pas. +--- +# PU-001 — corps sans importance pour l'instrument diff --git a/instrument/selftest/fixture/reglage.yaml b/instrument/selftest/fixture/reglage.yaml new file mode 100644 index 0000000..5b81cd2 --- /dev/null +++ b/instrument/selftest/fixture/reglage.yaml @@ -0,0 +1,15 @@ +# Réglage de la juridiction d'essai — la forme que toute juridiction fournit. +juridiction: essai +cadence_heures: 24 +parcours: docs/parcours +strates: + - nom: domaine + due: true + chemins: ["src/main/java/**/Domain/**", "src/test/java/**/Domain/**"] + - nom: application + chemins: ["src/main/java/**/Application/**", "src/test/java/**/Application/**"] + - nom: interface + chemins: ["app/src/**"] +epreuves: ["src/test/**", "app/src/**/*.essai.*"] +sortie: docs/parcours/_mesures/coupe.yaml +journal_execution: docs/parcours/_mesures/derniere-execution.yaml diff --git a/instrument/selftest/fixture/src/main/java/x/Domain/Rule.java b/instrument/selftest/fixture/src/main/java/x/Domain/Rule.java new file mode 100644 index 0000000..5a66ffc --- /dev/null +++ b/instrument/selftest/fixture/src/main/java/x/Domain/Rule.java @@ -0,0 +1,2 @@ +// Guard for INV-001 — names it, proves nothing. +class Rule {} diff --git a/instrument/selftest/fixture/src/test/java/x/Application/API/FlowTest.java b/instrument/selftest/fixture/src/test/java/x/Application/API/FlowTest.java new file mode 100644 index 0000000..76f51f8 --- /dev/null +++ b/instrument/selftest/fixture/src/test/java/x/Application/API/FlowTest.java @@ -0,0 +1,2 @@ +// Proves INV-001 and INV-002 above the due stratum. +class FlowTest {} diff --git a/instrument/selftest/fixture/src/test/java/x/Domain/RuleTest.java b/instrument/selftest/fixture/src/test/java/x/Domain/RuleTest.java new file mode 100644 index 0000000..2b8afd6 --- /dev/null +++ b/instrument/selftest/fixture/src/test/java/x/Domain/RuleTest.java @@ -0,0 +1,2 @@ +// Proves INV-001 at its due stratum. +class RuleTest {} diff --git a/package.json b/package.json index 40c365d..d21e3f8 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,15 @@ "description": "Contrat d'échange inter-juridictions : grammaire source (grammar/) et enveloppe d'assertion (envelope/), sous un seul numéro de version (ADR-061/A1.2 §2.6, décision 6). Privé jusqu'à l'extraction publique décrite dans OPENING.md.", "license": "Apache-2.0", "scripts": { - "conformance": "node envelope/conformance/run.mjs" + "conformance": "node envelope/conformance/run.mjs", + "instrument": "node instrument/run.mjs", + "instrument:selftest": "node instrument/selftest.mjs" }, "devDependencies": { "ajv": "^8.17.0", "ajv-formats": "^3.0.0" + }, + "dependencies": { + "yaml": "2.9.0" } }