From 654042d0fab5f7f5d6d70774dfaadea133b6a4c6 Mon Sep 17 00:00:00 2001 From: temoin-agent Date: Mon, 21 Sep 2026 21:13:02 +0200 Subject: [PATCH] =?UTF-8?q?Gardes=20nomm=C3=A9es,=20reprise=20de=20revue?= =?UTF-8?q?=20:=20=C2=AB=20nomm=C3=A9e=20=C2=BB=20jamais=20=C2=AB=20d?= =?UTF-8?q?=C3=A9clar=C3=A9e=20=C2=BB,=20exclusions,=20registre=20du=20ref?= =?UTF-8?q?us?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les trois corrections retenues de la revue extérieure d'openathle#275 — que j'avais amendées sur la branche d'une PR déjà fusionnée, donc jamais livrées : les champs disent ce qu'ils mesurent (nommee_par_parcours, _par_adr, _par_epreuve — un parcours peut porter la règle en prose sans nommer la classe) ; une absence n'est pas une règle (gardes_exclues, par motif — exclue = non observée) ; décider que non a un réceptacle (gardes_ecartees, registre de la juridiction, lu jamais écrit). Le journal porte ses motifs : la limite est dite là où la mesure s'écrit. La triade est mesurée : sans_epreuve. Co-Authored-By: Claude Fable 5.1 --- instrument/README.md | 29 ++++++++---- instrument/run.mjs | 47 +++++++++++++++---- instrument/selftest.mjs | 27 ++++++++--- instrument/selftest/fixture/reglage.yaml | 3 ++ .../java/x/Domain/ThingNotFoundException.java | 4 ++ 5 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 instrument/selftest/fixture/src/main/java/x/Domain/ThingNotFoundException.java diff --git a/instrument/README.md b/instrument/README.md index 594737b..08c19c8 100644 --- a/instrument/README.md +++ b/instrument/README.md @@ -48,15 +48,28 @@ instrumenter, pas un rang qui va de soi. ## Les gardes nommées — des candidats, jamais des constats (optionnel) -Une exception de domaine est une règle qui a un nom, un `throw`, presque -toujours une épreuve — et souvent aucune déclaration. Si le réglage porte -`gardes_nommees` (motifs de fichiers) et `adr` (où chercher les citations), -le journal d'exécution rend `gardes_nommees: { total, sans_declaration, -candidats }` — seuls ceux qu'aucun parcours ne cite sont listés, avec leur -strate et ce qui les cite (ADR, épreuve). L'instrument **mesure une existence, -il ne déclare rien** : déclarer un candidat (un invariant dans le parcours qui -le porte) ou décider que non reste à la juridiction. Rien ne devient un +Une exception de domaine est une règle qui a un nom et un `throw` — et souvent +ni épreuve ni déclaration. Si le réglage porte `gardes_nommees` (motifs de +fichiers) et `adr` (où chercher les citations), le journal rend +`gardes_nommees: { motifs, total, non_nommees_par_un_parcours, sans_epreuve, +ecartees, differees, candidats }`. Trois précautions, chacune répondant à une +faute possible : + +- **« Nommée », jamais « déclarée ».** Un parcours peut porter la règle en + langue naturelle sans nommer la classe ; l'instrument n'observe que la + nomination, et les champs le disent (`nommee_par_parcours`, `_par_adr`, + `_par_epreuve`). Absent ≠ non déclaré. +- **Une exception n'est pas un invariant.** Une absence (`NotFound`) n'est une + règle pour personne : la juridiction l'exclut par motif (`gardes_exclues`). +- **Décider que non a un réceptacle.** Un registre de la juridiction + (`gardes_ecartees` : `nom`, `decision: ecarte | differe`, `motif`, `jusqu_au`), + que l'instrument **lit et n'écrit jamais** : l'écarté sort des candidats et + reste dans le compte ; le différé revient à sa date. + +L'instrument **mesure une existence, il ne déclare rien** ; rien ne devient un constat, aucun état ne change de sens ; sans les clés, la section n'existe pas. +Sa limite est dans le journal (`motifs`) : ce qui ne porte pas le motif — +`require`, objets-valeurs, contraintes de schéma — lui est invisible. Pourquoi si peu : c'est le plus petit instrument qui prouve que le code tient des règles que nul n'a déclarées. Mesuré chez openathle le jour de son diff --git a/instrument/run.mjs b/instrument/run.mjs index 9d04ae9..db0a4f7 100644 --- a/instrument/run.mjs +++ b/instrument/run.mjs @@ -190,29 +190,51 @@ function scanCitations(repo, files, ids) { function namedGuards(repo, cfg, files) { if (!Array.isArray(cfg.gardes_nommees) || cfg.gardes_nommees.length === 0) return null; const res = cfg.gardes_nommees.map(globToRegExp); + const exclus = (Array.isArray(cfg.gardes_exclues) ? cfg.gardes_exclues : []).map(globToRegExp); const read = (p) => readFileSync(join(repo, p), 'utf8'); const mdUnder = (dir) => typeof dir === 'string' && existsSync(join(repo, dir)) ? walk(repo, join(repo, dir), []).filter((p) => p.endsWith('.md')) : []; const parcoursText = mdUnder(cfg.parcours).map(read).join('\n'); const adrText = mdUnder(cfg.adr).map(read).join('\n'); const tests = files.filter((f) => f.isTest).map((f) => read(f.path)); + // Le réceptacle du refus : ce que la juridiction a DÉCIDÉ d'écarter ou de + // différer ne reparaît pas chaque nuit. Sans lui, la liste n'est jamais un + // delta et le bruit devient un stresseur (ADR-061 §2.3). Le registre est un + // fichier de la juridiction ; l'instrument le lit, il ne l'écrit jamais. + const decisions = new Map(); + if (typeof cfg.gardes_ecartees === 'string' && existsFileAt(repo, cfg.gardes_ecartees)) { + const reg = parseYaml(read(cfg.gardes_ecartees)); + for (const d of Array.isArray(reg) ? reg : []) if (d && typeof d.nom === 'string') decisions.set(d.nom, d); + } const out = []; for (const f of files) { - if (f.isTest || !res.some((re) => re.test(f.path))) continue; + if (f.isTest || !res.some((re) => re.test(f.path)) || exclus.some((re) => re.test(f.path))) continue; const nom = f.path.split('/').pop().replace(/\.[^.]+$/, ''); const re = new RegExp(`\\b${escapeRe(nom)}\\b`); + const d = decisions.get(nom); + const differee = d?.decision === 'differe' && typeof d.jusqu_au === 'string' && d.jusqu_au >= new Date().toISOString().slice(0, 10); out.push({ nom, chemin: f.path, strate: f.stratum, - dans_parcours: re.test(parcoursText), - dans_adr: re.test(adrText), - eprouve: tests.some((x) => re.test(x)), + // NOMMÉE, jamais « déclarée » : un parcours peut porter la règle en + // langue naturelle sans nommer la classe. L'instrument observe la + // nomination — c'est sa limite, dite dans le nom des champs. + nommee_par_parcours: re.test(parcoursText), + nommee_par_adr: re.test(adrText), + nommee_par_epreuve: tests.some((x) => re.test(x)), + ...(d?.decision === 'ecarte' ? { decision: 'ecarte', motif: d.motif ?? null } : {}), + ...(differee ? { decision: 'differe', jusqu_au: d.jusqu_au } : {}), }); } return out.sort((a, b) => a.nom.localeCompare(b.nom)); } +function existsFileAt(repo, p) { + const st = statSync(join(repo, p), { throwIfNoEntry: false }); + return st !== undefined && st.isFile(); +} + 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))] @@ -358,9 +380,18 @@ function emit(args, cfg, r) { ? {} : { gardes_nommees: { + // Ce que la mesure regarde, dit tel quel : les motifs. Ce qui n'y + // correspond pas (require, objets-valeurs, contraintes SQL) est + // invisible — la limite est dans le journal, pas seulement au README. + motifs: cfg.gardes_nommees, + ...(Array.isArray(cfg.gardes_exclues) && cfg.gardes_exclues.length ? { exclus: cfg.gardes_exclues } : {}), total: r.gardes.length, - sans_declaration: r.gardes.filter((g) => !g.dans_parcours).length, - candidats: r.gardes.filter((g) => !g.dans_parcours), + non_nommees_par_un_parcours: r.gardes.filter((g) => !g.nommee_par_parcours).length, + sans_epreuve: r.gardes.filter((g) => !g.nommee_par_epreuve).length, + ecartees: r.gardes.filter((g) => g.decision === 'ecarte').length, + differees: r.gardes.filter((g) => g.decision === 'differe').length, + // Les candidats du jour : non nommés par un parcours, ni écartés, ni différés. + candidats: r.gardes.filter((g) => !g.nommee_par_parcours && g.decision === undefined), }, }), reglage_empreinte: r.meta.reglageHash, @@ -382,8 +413,8 @@ function emit(args, cfg, r) { 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}`}`); if (r.gardes !== null) { - const sans = r.gardes.filter((g) => !g.dans_parcours); - console.log(` gardes nommées : ${r.gardes.length}, dont ${sans.length} qu'aucun parcours ne déclare${sans.length ? ' — ' + sans.map((g) => g.nom).join(', ') : ''}`); + const cand = r.gardes.filter((g) => !g.nommee_par_parcours && g.decision === undefined); + console.log(` gardes nommées : ${r.gardes.length} · candidates (non nommées par un parcours, ni écartées) : ${cand.length}${cand.length ? ' — ' + cand.map((g) => g.nom).join(', ') : ''}`); } } diff --git a/instrument/selftest.mjs b/instrument/selftest.mjs index 105f48f..fc00798 100644 --- a/instrument/selftest.mjs +++ b/instrument/selftest.mjs @@ -10,7 +10,7 @@ * * Exit codes: 0 sound, 1 broken. A refusal names its cause. */ -import { cpSync, mkdtempSync, readFileSync, rmSync, unlinkSync, existsSync } from 'node:fs'; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, existsSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; @@ -47,12 +47,18 @@ try { 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. + // Named guards — candidates, never findings: a domain exception nobody NAMES + // in a journey. Fields say « named », not « declared » : a journey may hold + // the rule in prose without naming the class — that is the instrument's limit. 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'); + // Excluded by pattern = not observed at all: the NotFound one is neither counted nor listed. + assert(g && g.total === 1 && g.non_nommees_par_un_parcours === 1, 'the fixture has one observed named guard (the NotFound one is excluded), and no journey names it'); + assert(JSON.stringify(g.motifs) === JSON.stringify(['src/main/java/**/Domain/**/*Exception.java']), 'the journal says which patterns it looked at — the lamppost is written down'); + assert(g.candidats.length === 1 && g.candidats[0].nom === 'RuleViolatedException', 'the NotFound one is excluded by pattern; the other is a candidate'); + const c = g.candidats[0]; + assert(c.strate === 'domaine' && c.nommee_par_parcours === false && c.nommee_par_adr === true && c.nommee_par_epreuve === false, 'the candidate says who names it — ADR yes, journey no, test no'); + assert(g.sans_epreuve === 1, 'the triad is measured, not asserted: the observed guard lacks a test'); + assert(g.ecartees === 0 && g.differees === 0, 'no decision register yet: nothing set aside'); // 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 @@ -62,6 +68,15 @@ try { 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'); + // The refusal register: what the jurisdiction set aside does not come back + // every night. Read by the instrument, never written by it. + const repo3 = mkdtempSync(join(tmpdir(), 'instrument-selftest-registre-')); + cpSync(join(here, 'selftest/fixture'), repo3, { recursive: true }); + mkdirSync(join(repo3, 'docs/parcours/_meta'), { recursive: true }); + writeFileSync(join(repo3, 'docs/parcours/_meta/gardes-ecartees.yaml'), "- nom: RuleViolatedException\n decision: ecarte\n motif: une plomberie, pas une règle\n"); + execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'reglage.yaml', '--repo', repo3], { encoding: 'utf8', stdio: 'pipe' }); + const j3 = parseYaml(readFileSync(join(repo3, 'docs/parcours/_mesures/derniere-execution.yaml'), 'utf8')).gardes_nommees; + assert(j3.total === 1 && j3.ecartees === 1 && j3.candidats.length === 0, 'a guard set aside leaves the candidates and stays in the count'); 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'); diff --git a/instrument/selftest/fixture/reglage.yaml b/instrument/selftest/fixture/reglage.yaml index cd4baf5..c2a04ae 100644 --- a/instrument/selftest/fixture/reglage.yaml +++ b/instrument/selftest/fixture/reglage.yaml @@ -19,4 +19,7 @@ journal_execution: docs/parcours/_mesures/derniere-execution.yaml gardes_nommees: - "src/main/java/**/Domain/**/*Exception.java" +gardes_exclues: + - "**/*NotFoundException.java" +gardes_ecartees: docs/parcours/_meta/gardes-ecartees.yaml adr: docs/adr diff --git a/instrument/selftest/fixture/src/main/java/x/Domain/ThingNotFoundException.java b/instrument/selftest/fixture/src/main/java/x/Domain/ThingNotFoundException.java new file mode 100644 index 0000000..2f0f947 --- /dev/null +++ b/instrument/selftest/fixture/src/main/java/x/Domain/ThingNotFoundException.java @@ -0,0 +1,4 @@ +package x.Domain; + +/** An absence is nobody's business rule: excluded by pattern. */ +public class ThingNotFoundException extends RuntimeException {} -- 2.54.0