diff --git a/instrument/README.md b/instrument/README.md index a2e9636..8e97d05 100644 --- a/instrument/README.md +++ b/instrument/README.md @@ -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`, `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 - **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état diff --git a/instrument/run.mjs b/instrument/run.mjs index d09ab1c..93796d3 100644 --- a/instrument/run.mjs +++ b/instrument/run.mjs @@ -71,9 +71,12 @@ function loadConfig(repo, path) { if (cfg?.[key] === undefined) refuse(`le réglage « ${path} » ne déclare pas « ${key} »`); if (!Array.isArray(cfg.strates) || cfg.strates.length === 0) 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)) 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]; 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, constat: sentence(inv.id, verdict, cfg.due), strates_constatees: verdict.strates, + ...rangsConstates(cfg, verdict.strates), pieces: kept, ...(omitted > 0 ? { pieces_omises: omitted } : {}), citations_hors_epreuve: hits.filter((h) => !h.isTest).length, @@ -247,6 +251,18 @@ function constatItem(cfg, inv, verdict, hits, seq, meta) { 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) { try { 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 } : {}), parcours_lus: r.journeys, 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, constats_nouveaux: r.fresh.length, reglage_empreinte: r.meta.reglageHash, diff --git a/instrument/selftest.mjs b/instrument/selftest.mjs index cf0f7ff..bbed6fc 100644 --- a/instrument/selftest.mjs +++ b/instrument/selftest.mjs @@ -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(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')?.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-003')?.etat === 'aucun_locus', 'INV-003 aucun_locus — named by no test'); assert(journal().constats_nouveaux === 3 && journal().invariants === 3, 'journal counts the run'); diff --git a/instrument/selftest/fixture/reglage.yaml b/instrument/selftest/fixture/reglage.yaml index 5b81cd2..5de656d 100644 --- a/instrument/selftest/fixture/reglage.yaml +++ b/instrument/selftest/fixture/reglage.yaml @@ -5,10 +5,13 @@ 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