Author SHA1 Message Date
temoin-agentandClaude Fable 5.1 654042d0fa Gardes nommées, reprise de revue : « nommée » jamais « déclarée », exclusions, registre du refus
gardes du contrat / conformite (pull_request) Successful in 17s
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 <noreply@anthropic.com>
2026-09-21 21:13:02 +02:00
5 changed files with 88 additions and 22 deletions
+21 -8
View File
@@ -48,15 +48,28 @@ instrumenter, pas un rang qui va de soi.
## Les gardes nommées — des candidats, jamais des constats (optionnel) ## 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 Une exception de domaine est une règle qui a un nom et un `throw` — et souvent
toujours une épreuve — et souvent aucune déclaration. Si le réglage porte ni épreuve ni déclaration. Si le réglage porte `gardes_nommees` (motifs de
`gardes_nommees` (motifs de fichiers) et `adr` (où chercher les citations), fichiers) et `adr` (où chercher les citations), le journal rend
le journal d'exécution rend `gardes_nommees: { total, sans_declaration, `gardes_nommees: { motifs, total, non_nommees_par_un_parcours, sans_epreuve,
candidats }` — seuls ceux qu'aucun parcours ne cite sont listés, avec leur ecartees, differees, candidats }`. Trois précautions, chacune répondant à une
strate et ce qui les cite (ADR, épreuve). L'instrument **mesure une existence, faute possible :
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 - **« 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. 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 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 des règles que nul n'a déclarées. Mesuré chez openathle le jour de son
+39 -8
View File
@@ -190,29 +190,51 @@ function scanCitations(repo, files, ids) {
function namedGuards(repo, cfg, files) { function namedGuards(repo, cfg, files) {
if (!Array.isArray(cfg.gardes_nommees) || cfg.gardes_nommees.length === 0) return null; if (!Array.isArray(cfg.gardes_nommees) || cfg.gardes_nommees.length === 0) return null;
const res = cfg.gardes_nommees.map(globToRegExp); 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 read = (p) => readFileSync(join(repo, p), 'utf8');
const mdUnder = (dir) => const mdUnder = (dir) =>
typeof dir === 'string' && existsSync(join(repo, dir)) ? walk(repo, join(repo, dir), []).filter((p) => p.endsWith('.md')) : []; 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 parcoursText = mdUnder(cfg.parcours).map(read).join('\n');
const adrText = mdUnder(cfg.adr).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)); 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 = []; const out = [];
for (const f of files) { 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 nom = f.path.split('/').pop().replace(/\.[^.]+$/, '');
const re = new RegExp(`\\b${escapeRe(nom)}\\b`); 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({ out.push({
nom, nom,
chemin: f.path, chemin: f.path,
strate: f.stratum, strate: f.stratum,
dans_parcours: re.test(parcoursText), // NOMMÉE, jamais « déclarée » : un parcours peut porter la règle en
dans_adr: re.test(adrText), // langue naturelle sans nommer la classe. L'instrument observe la
eprouve: tests.some((x) => re.test(x)), // 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)); 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) { function judge(hits, cfg) {
const order = cfg.strates.map((s) => s.nom); const order = cfg.strates.map((s) => s.nom);
const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))] const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))]
@@ -358,9 +380,18 @@ function emit(args, cfg, r) {
? {} ? {}
: { : {
gardes_nommees: { 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, total: r.gardes.length,
sans_declaration: r.gardes.filter((g) => !g.dans_parcours).length, non_nommees_par_un_parcours: r.gardes.filter((g) => !g.nommee_par_parcours).length,
candidats: r.gardes.filter((g) => !g.dans_parcours), 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, 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(` à 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}`}`); console.log(` constat(s) nouveau(x) : ${r.fresh.length}${args.dryRun ? '' : ` → ${cfg.sortie}`}`);
if (r.gardes !== null) { if (r.gardes !== null) {
const sans = r.gardes.filter((g) => !g.dans_parcours); const cand = r.gardes.filter((g) => !g.nommee_par_parcours && g.decision === undefined);
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(', ') : ''}`); 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(', ') : ''}`);
} }
} }
+21 -6
View File
@@ -10,7 +10,7 @@
* *
* Exit codes: 0 sound, 1 broken. A refusal names its cause. * 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 { join, dirname } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url'; 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-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(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'); 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; 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'); // Excluded by pattern = not observed at all: the NotFound one is neither counted nor listed.
assert(g.candidats[0].nom === 'RuleViolatedException' && g.candidats[0].strate === 'domaine', 'the candidate carries its name and its stratum'); 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(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(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(!('candidats' in journal()) , 'candidates live under gardes_nommees, not at the journal root'); 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. // 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 // 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 // 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' }); 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')); 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(!('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-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(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'); assert(journal().constats_nouveaux === 3 && journal().invariants === 3, 'journal counts the run');
+3
View File
@@ -19,4 +19,7 @@ journal_execution: docs/parcours/_mesures/derniere-execution.yaml
gardes_nommees: gardes_nommees:
- "src/main/java/**/Domain/**/*Exception.java" - "src/main/java/**/Domain/**/*Exception.java"
gardes_exclues:
- "**/*NotFoundException.java"
gardes_ecartees: docs/parcours/_meta/gardes-ecartees.yaml
adr: docs/adr adr: docs/adr
@@ -0,0 +1,4 @@
package x.Domain;
/** An absence is nobody's business rule: excluded by pattern. */
public class ThingNotFoundException extends RuntimeException {}