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>
This commit is contained in:
temoin-agent
2026-09-21 21:13:02 +02:00
co-authored by Claude Fable 5.1
parent 8670f57b16
commit 654042d0fa
5 changed files with 88 additions and 22 deletions
+39 -8
View File
@@ -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(', ') : ''}`);
}
}