Compare commits
1
Commits
e27cbbbeff
...
ecf13fe1da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
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
|
||||
|
||||
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
|
||||
|
||||
+55
-1
@@ -177,6 +177,42 @@ function scanCitations(repo, files, ids) {
|
||||
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) {
|
||||
const order = cfg.strates.map((s) => s.nom);
|
||||
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;
|
||||
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) {
|
||||
@@ -313,6 +350,19 @@ function emit(args, cfg, r) {
|
||||
: {}),
|
||||
etats: r.tally,
|
||||
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,
|
||||
};
|
||||
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(` à 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(', ') : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
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-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');
|
||||
|
||||
@@ -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.*"]
|
||||
sortie: docs/parcours/_mesures/coupe.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