Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0808286bf7 | ||
|
|
654042d0fa | ||
|
|
8670f57b16 | ||
|
|
ecf13fe1da | ||
|
|
887d9a90c3 |
@@ -46,6 +46,35 @@ 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 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
|
||||||
|
é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
|
||||||
|
|||||||
+86
-1
@@ -177,6 +177,64 @@ 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 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)) || 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,
|
||||||
|
// 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) {
|
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 +355,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 +372,28 @@ 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: {
|
||||||
|
// 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,
|
||||||
|
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,
|
reglage_empreinte: r.meta.reglageHash,
|
||||||
};
|
};
|
||||||
if (args.dryRun) {
|
if (args.dryRun) {
|
||||||
@@ -331,6 +412,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 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(', ') : ''}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
+31
-1
@@ -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,6 +47,36 @@ 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 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;
|
||||||
|
// 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
|
||||||
|
// 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');
|
||||||
|
// 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');
|
||||||
|
|||||||
@@ -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,10 @@ 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"
|
||||||
|
gardes_exclues:
|
||||||
|
- "**/*NotFoundException.java"
|
||||||
|
gardes_ecartees: docs/parcours/_meta/gardes-ecartees.yaml
|
||||||
|
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); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package x.Domain;
|
||||||
|
|
||||||
|
/** An absence is nobody's business rule: excluded by pattern. */
|
||||||
|
public class ThingNotFoundException extends RuntimeException {}
|
||||||
Reference in New Issue
Block a user