feat(published-language): l'instrument émet la correspondance de rangs déclarée

Percursus lit les strates en rangs (1–5, échelle fermée par ses ADR-003/004) ;
openathle les nomme (domaine, application, infrastructure, interface). La
clause loci de Percursus, déclarée à la main, est devenue inférable depuis que
la coupe existe — et l'humain ne saisit pas l'inférable (P4).

Le réglage de la juridiction peut désormais porter rang: N par strate. Le
constat émet rangs_constates — seulement si TOUTES les strates constatées en
déclarent un : une correspondance partielle laisserait deviner, et deviner
n'est pas constater. Le journal publie la table complète
(correspondance_rangs) pour joindre les constats anciens, qui ne se
réécrivent jamais. Champ optionnel, additif : aucun réglage existant ne
change de comportement.

Auto-épreuve : dix-huit vérifications, dont l'ordre des rangs, l'absence de
rangs sur l'invariant sans locus, et la table du journal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-10 13:20:11 +02:00
co-authored by Claude Opus 5
parent 92d402444e
commit 7f03f37ef7
4 changed files with 39 additions and 1 deletions
+13
View File
@@ -33,6 +33,19 @@ complète : `juridiction`, `cadence_heures`, `parcours`, `strates` (la première
— ou celle marquée `due: true` — est l'étage dû), `epreuves`, `sortie`, — ou celle marquée `due: true` — est l'étage dû), `epreuves`, `sortie`,
`journal_execution`. `journal_execution`.
## La correspondance vers une échelle tierce (`rang`, optionnel)
Une juridiction peut faire correspondre chacune de ses strates à une échelle
tierce (p. ex. les rangs 1–5 de Percursus) en déclarant `rang: N` sur la
strate, dans son réglage. L'instrument émet alors `rangs_constates` dans
chaque constat — **seulement si toutes les strates constatées portent un
rang** : une correspondance partielle laisserait deviner, et deviner n'est
pas constater — et publie la table complète (`correspondance_rangs`) dans le
journal d'exécution, pour joindre les constats anciens, qui ne se réécrivent
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.
## 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
+20 -1
View File
@@ -71,9 +71,12 @@ function loadConfig(repo, path) {
if (cfg?.[key] === undefined) refuse(`le réglage « ${path} » ne déclare pas « ${key} »`); if (cfg?.[key] === undefined) refuse(`le réglage « ${path} » ne déclare pas « ${key} »`);
if (!Array.isArray(cfg.strates) || cfg.strates.length === 0) if (!Array.isArray(cfg.strates) || cfg.strates.length === 0)
refuse(`le réglage « ${path} » doit déclarer au moins une strate`); refuse(`le réglage « ${path} » doit déclarer au moins une strate`);
for (const s of cfg.strates) for (const s of cfg.strates) {
if (typeof s?.nom !== 'string' || !Array.isArray(s?.chemins)) if (typeof s?.nom !== 'string' || !Array.isArray(s?.chemins))
refuse(`chaque strate du réglage porte « nom » et « chemins » — l'une des deux manque`); refuse(`chaque strate du réglage porte « nom » et « chemins » — l'une des deux manque`);
if (s.rang !== undefined && typeof s.rang !== 'number')
refuse(`la strate « ${s.nom} » déclare un rang qui n'est pas un nombre — le rang est une correspondance vers une échelle tierce, ou rien`);
}
const due = cfg.strates.find((s) => s.due === true) ?? cfg.strates[0]; const due = cfg.strates.find((s) => s.due === true) ?? cfg.strates[0];
return { ...cfg, due: due.nom, raw: readFileSync(full, 'utf8'), path }; return { ...cfg, due: due.nom, raw: readFileSync(full, 'utf8'), path };
} }
@@ -232,6 +235,7 @@ function constatItem(cfg, inv, verdict, hits, seq, meta) {
etat: verdict.etat, etat: verdict.etat,
constat: sentence(inv.id, verdict, cfg.due), constat: sentence(inv.id, verdict, cfg.due),
strates_constatees: verdict.strates, strates_constatees: verdict.strates,
...rangsConstates(cfg, verdict.strates),
pieces: kept, pieces: kept,
...(omitted > 0 ? { pieces_omises: omitted } : {}), ...(omitted > 0 ? { pieces_omises: omitted } : {}),
citations_hors_epreuve: hits.filter((h) => !h.isTest).length, citations_hors_epreuve: hits.filter((h) => !h.isTest).length,
@@ -247,6 +251,18 @@ function constatItem(cfg, inv, verdict, hits, seq, meta) {
return item; return item;
} }
/**
* La correspondance vers une échelle tierce (p. ex. les rangs de Percursus),
* si la juridiction la déclare dans son réglage. Émise SEULEMENT quand toutes
* les strates constatées portent un rang : une correspondance partielle
* laisserait le consommateur deviner — deviner n'est pas constater.
*/
function rangsConstates(cfg, strates) {
const rang = new Map(cfg.strates.filter((s) => typeof s.rang === 'number').map((s) => [s.nom, s.rang]));
if (strates.length === 0 || !strates.every((n) => rang.has(n))) return {};
return { rangs_constates: strates.map((n) => rang.get(n)) };
}
function gitRevision(repo) { function gitRevision(repo) {
try { try {
return execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); return execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -292,6 +308,9 @@ function emit(args, cfg, r) {
...(typeof cfg.cadence_heures === 'number' ? { cadence_heures: cfg.cadence_heures } : {}), ...(typeof cfg.cadence_heures === 'number' ? { cadence_heures: cfg.cadence_heures } : {}),
parcours_lus: r.journeys, parcours_lus: r.journeys,
invariants: r.invariants, invariants: r.invariants,
...(cfg.strates.some((s) => typeof s.rang === 'number')
? { correspondance_rangs: Object.fromEntries(cfg.strates.filter((s) => typeof s.rang === 'number').map((s) => [s.nom, s.rang])) }
: {}),
etats: r.tally, etats: r.tally,
constats_nouveaux: r.fresh.length, constats_nouveaux: r.fresh.length,
reglage_empreinte: r.meta.reglageHash, reglage_empreinte: r.meta.reglageHash,
+3
View File
@@ -44,6 +44,9 @@ try {
assert((byInv(first, 'INV-001')?.strates_constatees ?? []).join(',') === 'domaine,application', 'INV-001 strata ordered as the tuning declares them'); assert((byInv(first, 'INV-001')?.strates_constatees ?? []).join(',') === 'domaine,application', 'INV-001 strata ordered as the tuning declares them');
assert(Array.isArray(byInv(first, 'INV-001')?.homonymie), 'INV-001 carries its homonymy — two statements, one id, exposed not resolved'); assert(Array.isArray(byInv(first, 'INV-001')?.homonymie), 'INV-001 carries its homonymy — two statements, one id, exposed not resolved');
assert(byInv(first, 'INV-001')?.citations_hors_epreuve === 1, 'INV-001 counts its non-test citation without treating it as proof'); assert(byInv(first, 'INV-001')?.citations_hors_epreuve === 1, 'INV-001 counts its non-test citation without treating it as proof');
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');
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
@@ -5,10 +5,13 @@ parcours: docs/parcours
strates: strates:
- nom: domaine - nom: domaine
due: true due: true
rang: 4
chemins: ["src/main/java/**/Domain/**", "src/test/java/**/Domain/**"] chemins: ["src/main/java/**/Domain/**", "src/test/java/**/Domain/**"]
- nom: application - nom: application
rang: 3
chemins: ["src/main/java/**/Application/**", "src/test/java/**/Application/**"] chemins: ["src/main/java/**/Application/**", "src/test/java/**/Application/**"]
- nom: interface - nom: interface
rang: 1
chemins: ["app/src/**"] chemins: ["app/src/**"]
epreuves: ["src/test/**", "app/src/**/*.essai.*"] epreuves: ["src/test/**", "app/src/**/*.essai.*"]
sortie: docs/parcours/_mesures/coupe.yaml sortie: docs/parcours/_mesures/coupe.yaml