Instrument : les gardes nommées, comme candidats — le plus petit lecteur qui prouve la thèse
gardes du contrat / conformite (pull_request) Successful in 17s
gardes du contrat / conformite (pull_request) Successful in 17s
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 le demande (gardes_nommees, adr), le journal rend les candidats qu'aucun parcours ne cite, avec leur strate et ce qui les cite. Rien ne devient un constat, aucun état ne change de sens, la section n'existe pas sans les clés : additif, ignorable. Ce qu'un dessin de trois lecteurs (Java annoté, SQL, réconciliation) voulait démontrer, abattu par l'objecteur mandaté, ces quarante lignes le démontrent : huit exceptions de domaine chez openathle, zéro déclarée. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
887d9a90c3
commit
ecf13fe1da
@@ -46,6 +46,22 @@ jamais. La grossièreté se dit dans le réglage (en commentaire) : un rang
|
|||||||
absent de la correspondance est un rang que la juridiction ne sait pas
|
absent de la correspondance est un rang que la juridiction ne sait pas
|
||||||
instrumenter, pas un rang qui va de soi.
|
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
|
||||||
|
constat, aucun état ne change de sens ; sans les clés, la section n'existe pas.
|
||||||
|
|
||||||
|
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
|
||||||
|
écriture — huit exceptions de domaine, zéro citée par un parcours.
|
||||||
|
|
||||||
## Ce qu'il écrit — chez la juridiction, jamais ailleurs
|
## Ce qu'il écrit — chez la juridiction, jamais ailleurs
|
||||||
|
|
||||||
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
|
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
|
||||||
|
|||||||
+55
-1
@@ -177,6 +177,42 @@ function scanCitations(repo, files, ids) {
|
|||||||
return hits;
|
return hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les GARDES NOMMÉES — le plus petit instrument qui prouve que le code tient
|
||||||
|
* des règles que nul n'a déclarées. Une exception de domaine a un nom, un
|
||||||
|
* `throw`, presque toujours une épreuve — et souvent aucune déclaration.
|
||||||
|
* L'instrument la rend comme CANDIDAT, jamais comme constat : il mesure une
|
||||||
|
* existence, il ne déclare rien, il ne renomme aucun état. Déclarer ou non
|
||||||
|
* reste à la juridiction (P4 : l'humain écrit ce que la règle signifie).
|
||||||
|
* Absente du réglage (`gardes_nommees`), la section n'existe pas — rien ne
|
||||||
|
* change pour qui ne l'a pas demandée.
|
||||||
|
*/
|
||||||
|
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 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));
|
||||||
|
const out = [];
|
||||||
|
for (const f of files) {
|
||||||
|
if (f.isTest || !res.some((re) => re.test(f.path))) continue;
|
||||||
|
const nom = f.path.split('/').pop().replace(/\.[^.]+$/, '');
|
||||||
|
const re = new RegExp(`\\b${escapeRe(nom)}\\b`);
|
||||||
|
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)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out.sort((a, b) => a.nom.localeCompare(b.nom));
|
||||||
|
}
|
||||||
|
|
||||||
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))]
|
||||||
@@ -297,7 +333,8 @@ function main() {
|
|||||||
if (last.get(inv.id) === key) continue;
|
if (last.get(inv.id) === key) continue;
|
||||||
fresh.push(constatItem(cfg, inv, verdict, hits.get(inv.id), (counts.get(inv.id) ?? 0) + 1, meta));
|
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 });
|
const gardes = namedGuards(args.repo, cfg, files);
|
||||||
|
emit(args, cfg, { journeys, invariants: invariants.size, tally, fresh, meta, sortiePath, gardes });
|
||||||
}
|
}
|
||||||
|
|
||||||
function emit(args, cfg, r) {
|
function emit(args, cfg, r) {
|
||||||
@@ -313,6 +350,19 @@ function emit(args, cfg, r) {
|
|||||||
: {}),
|
: {}),
|
||||||
etats: r.tally,
|
etats: r.tally,
|
||||||
constats_nouveaux: r.fresh.length,
|
constats_nouveaux: r.fresh.length,
|
||||||
|
// Les candidats vont au journal, réécrit à chaque course : ce n'est pas un
|
||||||
|
// constat (rien n'est mesuré sur un invariant), c'est un inventaire du jour.
|
||||||
|
// Seuls ceux qu'aucun parcours ne cite sont listés — lisibilité (P9) ; le
|
||||||
|
// total dit combien de gardes nommées le code porte.
|
||||||
|
...(r.gardes === null
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
gardes_nommees: {
|
||||||
|
total: r.gardes.length,
|
||||||
|
sans_declaration: r.gardes.filter((g) => !g.dans_parcours).length,
|
||||||
|
candidats: r.gardes.filter((g) => !g.dans_parcours),
|
||||||
|
},
|
||||||
|
}),
|
||||||
reglage_empreinte: r.meta.reglageHash,
|
reglage_empreinte: r.meta.reglageHash,
|
||||||
};
|
};
|
||||||
if (args.dryRun) {
|
if (args.dryRun) {
|
||||||
@@ -331,6 +381,10 @@ function emit(args, cfg, r) {
|
|||||||
console.log(` ${r.invariants} invariant(s) déclaré(s) par ${r.journeys} parcours`);
|
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(` à 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) {
|
||||||
|
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(', ') : ''}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -47,6 +47,21 @@ 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.
|
||||||
|
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-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');
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# ADR-001 — essai
|
||||||
|
|
||||||
|
Le domaine lève `RuleViolatedException` quand la règle est violée.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 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
|
||||||
|
rang: 4
|
||||||
|
chemins: ["src/main/java/**/Domain/**", "src/test/java/**/Domain/**"]
|
||||||
|
- nom: application
|
||||||
|
rang: 3
|
||||||
|
chemins: ["src/main/java/**/Application/**", "src/test/java/**/Application/**"]
|
||||||
|
- nom: interface
|
||||||
|
rang: 1
|
||||||
|
chemins: ["app/src/**"]
|
||||||
|
epreuves: ["src/test/**", "app/src/**/*.essai.*"]
|
||||||
|
sortie: docs/parcours/_mesures/coupe.yaml
|
||||||
|
journal_execution: docs/parcours/_mesures/derniere-execution.yaml
|
||||||
|
|
||||||
@@ -16,3 +16,7 @@ strates:
|
|||||||
epreuves: ["src/test/**", "app/src/**/*.essai.*"]
|
epreuves: ["src/test/**", "app/src/**/*.essai.*"]
|
||||||
sortie: docs/parcours/_mesures/coupe.yaml
|
sortie: docs/parcours/_mesures/coupe.yaml
|
||||||
journal_execution: docs/parcours/_mesures/derniere-execution.yaml
|
journal_execution: docs/parcours/_mesures/derniere-execution.yaml
|
||||||
|
|
||||||
|
gardes_nommees:
|
||||||
|
- "src/main/java/**/Domain/**/*Exception.java"
|
||||||
|
adr: docs/adr
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package x.Domain;
|
||||||
|
|
||||||
|
/** A named domain guard: a rule with a name and a throw — declared nowhere. */
|
||||||
|
public class RuleViolatedException extends RuntimeException {
|
||||||
|
public RuleViolatedException(String why) { super(why); }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user