From a34b686d0924a3974311fd704f4f02cc8d542b14 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Thu, 30 Jul 2026 09:36:39 -0500 Subject: [PATCH] fix(standards): bring the one-renderer design to main, and fix a sticky-date bug it exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converges main onto the GT-633 reconciliation already on develop (#294), so the standards half of develop -> main stops being a design conflict: one renderer in `test/rule-corpus-triage.ts`, two callers — the spec pins it byte-for-byte, the capture script writes it and `--check`s it for the GT-630 chain and docs.yml. AND IT EXPOSED A REAL BUG IN THAT RECONCILIATION, which is why this is not a copy-paste. The driver decided sticky-vs-fresh `capturedOn` by comparing `JSON.stringify(previous.counts)` against a counts object rebuilt in KNOWN_CLASSES order, while the renderer emits them in CLASS_ORDER — a different order for the same six numbers. So "changed" was ALWAYS true and the date was rewritten on every run. That would have failed the GT-630 fixed-point replay on any day after a capture. It passed on develop only because both runs happened on the same date and stamped the same value — the precise shape of green this chain exists to distrust. It surfaced here because main's committed snapshot carries 2026-07-29: the first run rewrote it to 2026-07-30, the second run rewrote it back. The comparison is now order-insensitive, and observed: with the classification unchanged, `capturedOn` stays 2026-07-29 across runs and the replay is byte-identical. NOTE: develop still carries the buggy comparison. It is invisible there today and will bite tomorrow; it needs the same one-function fix, which is a follow-up against develop rather than something to smuggle into a main-targeted change. Numbers on this base are main's own, measured rather than copied: native-handler 154 (develop computes 151, having fewer handler closures landed), corpus 389, mapping 391 rows. Verified: capture --check faithful, replay byte-identical, build --check up to date (391 rules, 37 mapped), iso-5055-mapping guard 9/9, chain 5 links current and at a fixed point, core-domain jest 1446 passed / 128 suites, tsc --noEmit clean, 01/04/08 green. Co-Authored-By: Claude Opus 5 --- .../validators/rule-corpus-triage.spec.ts | 136 ++++++++---------- .../core-domain/test/rule-corpus-triage.ts | 83 +++++++++++ .../capture-native-evaluability-snapshot.mjs | 118 +++++++-------- src/rulesets/standards/iso-5055-mapping.json | 3 +- .../native-evaluability-snapshot.json | 13 +- 5 files changed, 210 insertions(+), 143 deletions(-) diff --git a/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts b/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts index 9081829a..a82d113d 100644 --- a/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts +++ b/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts @@ -23,27 +23,35 @@ import * as fs from 'fs'; import * as path from 'path'; import { + ADR_CONFORMANCE_CATEGORY, RULE_TRIAGE, RuleEvaluability, isNonExecutable, } from './rule-evaluability'; -import { RULESETS_ROOT, triageCorpus } from '../../../test/rule-corpus-triage'; - -// The corpus loader, the real handler set and the classification live in -// `test/rule-corpus-triage.ts`. They used to live HERE, which meant jest was the -// only thing that could run them — and that is why -// `src/rulesets/standards/native-evaluability-snapshot.json`, which needs exactly -// this computation, was maintained by hand and drifted. The last describe block -// below is the other half of the repair. -const { corpus: CORPUS, classified: CLASSIFIED, summary: SUMMARY, claims } = triageCorpus(); +import { REPO_ROOT, RULESETS_ROOT, triageCorpus, renderSnapshot } from '../../../test/rule-corpus-triage'; + +// The corpus loader, the real handler set, the classification AND the snapshot +// renderer live in `test/rule-corpus-triage.ts`. They used to live HERE, which +// meant jest was the only thing that could run them — and that is why +// `native-evaluability-snapshot.json` was maintained by hand and drifted. +// +// GT-633 RECONCILIATION: the renderer in particular had two implementations, one +// here and one in `capture-native-evaluability-snapshot.mjs`, written in parallel +// by sessions that could not see each other. Two generators for one artifact is +// the defect GT-633 exists to remove, one level up: whichever ran last would win +// and the other's `--check` would go red for no visible reason. There is now ONE +// renderer, and this suite is its PIN rather than a second copy of it. +const TRIAGE = triageCorpus(); +const { corpus: CORPUS, classified: CLASSIFIED, summary: SUMMARY, claims } = TRIAGE; /** * The class counts this repository is pinned to. * - * Also the counts `native-evaluability-snapshot.json` must reproduce, and the - * ones its capture script writes. Read out of this file by - * `src/rulesets/standards/iso-5055-mapping.test.mjs`, which runs in a job with no - * node_modules and so cannot recompute them — keep it a plain literal. + * Also the counts `native-evaluability-snapshot.json` must reproduce. READ OUT OF + * THIS FILE by `src/rulesets/standards/iso-5055-mapping.test.mjs`, which runs in a + * job with no node_modules and so cannot recompute them — keep it a plain literal, + * and note that that guard THROWS if it cannot find this declaration rather than + * passing. Restoring it is what a reconciliation that deleted it owes back. */ const PINNED_CLASS_COUNTS: Readonly> = { 'native-handler': 154, @@ -54,6 +62,41 @@ const PINNED_CLASS_COUNTS: Readonly> = { underspecified: 14, }; +const SNAPSHOT_FILE = path.join(REPO_ROOT, 'src', 'rulesets', 'standards', 'native-evaluability-snapshot.json'); + +/** + * `capturedOn` is carried forward from the committed document, exactly as the + * capture script does, so the pin compares classification against classification + * and never fails on a date. + */ +function committedCapturedOn(): string { + return (JSON.parse(fs.readFileSync(SNAPSHOT_FILE, 'utf8')) as { capturedOn: string }).capturedOn; +} + +describe('GT-595 · the capture consumed by src/rulesets/standards', () => { + it('renders one class per rule, losing no id to a key collision', () => { + // `classes` is keyed by rule id alone, so two rules sharing an id in + // different files would silently collapse into one entry and understate + // every count downstream of it. + const rendered = JSON.parse(renderSnapshot(TRIAGE, { capturedOn: committedCapturedOn() })) as { + counts: Record; + classes: Record; + }; + expect(Object.keys(rendered.classes)).toHaveLength(CORPUS.length); + expect(Object.values(rendered.counts).reduce((a, b) => a + b, 0)).toBe(CORPUS.length); + }); + + it('keeps native-evaluability-snapshot.json byte-identical to a fresh capture', () => { + // Deliberately does NOT write. There is one writer — + // `node src/rulesets/standards/capture-native-evaluability-snapshot.mjs` — + // and it is the one the GT-630 chain guard and `docs.yml` invoke. A second + // writer is how this artifact ended up with two generators in the first place. + const fresh = renderSnapshot(TRIAGE, { capturedOn: committedCapturedOn() }); + + expect(fs.readFileSync(SNAPSHOT_FILE, 'utf8')).toBe(fresh); + }); +}); + describe('GT-595 · the corpus is fully classified', () => { it('loads a corpus of the expected size (guards the measurement itself)', () => { // If this moves, every number below moved with it — re-triage before editing. @@ -107,11 +150,11 @@ describe('GT-595 · the published breakdown, with its denominator', () => { // `enforce` was dropped at normalization. It is now carried, and // `ModuleBoundaryRuleHandler` evaluates it. // `unimplemented-native` drops by the same twelve; nothing else moved. - // - // 151 -> 154 on 2026-07-29: GT-580 added `cli/exit-code-taxonomy.rules.json` - // (CLI-EXIT-01/02/03) with `CliExitTaxonomyRuleHandler` claiming all three, - // so the corpus grows by three and every one of them lands directly in - // `native-handler`. No other class moves: these ids did not exist before. + // Asserted against the SINGLE declaration above, not against a second inline + // copy of the same six numbers. There used to be two — this constant, read by + // the dependency-free guard in `src/rulesets/standards`, and a literal here — + // which is GT-633's own defect in miniature: two copies of one fact, and + // editing either leaves the other silently disagreeing. expect(SUMMARY.byClass).toEqual(PINNED_CLASS_COUNTS); }); @@ -370,60 +413,3 @@ describe('GT-595 · OCB-02 is vacuous as written — measured, not asserted', () expect(parsed.openCoreMatrix?.enterprise?.length).toBeGreaterThan(0); }); }); - -/** - * GT-598 — the snapshot `src/rulesets/standards` consumes must be a CAPTURE. - * - * `native-evaluability-snapshot.json` is read by `build-iso-5055-mapping.mjs`, - * which stamps `nativeEvaluability` onto every row of the ISO/IEC 5055 mapping. - * A stale snapshot therefore does not stay contained: it launders a wrong - * classification into a larger derived artifact and misstates the handler - * backlog. It said `documentation-only: 129` long after this file pinned 136, - * and its own guard could not see that — it compared the snapshot against six - * numbers typed into the test, the same six the snapshot already contained. - * - * This is where the truth is computed, so this is where the comparison belongs. - * The guard in `src/rulesets/standards` runs in a job with no node_modules and - * cannot recompute anything; it reads {@link PINNED_CLASS_COUNTS} out of this - * file instead. If this block fails, run: - * node src/rulesets/standards/capture-native-evaluability-snapshot.mjs - * node src/rulesets/standards/build-iso-5055-mapping.mjs - * in that order — the second consumes what the first writes. - */ -describe('GT-598 · the captured snapshot still matches a fresh triage', () => { - const SNAPSHOT_PATH = path.join(RULESETS_ROOT, 'standards', 'native-evaluability-snapshot.json'); - const snapshot = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf8')) as { - counts: Record; - classes: Record; - }; - - it('agrees on the class counts', () => { - expect(snapshot.counts).toEqual(PINNED_CLASS_COUNTS); - }); - - it('agrees on the class of every rule, id by id', () => { - // Duplicate ids across rulesets collapse to one entry in the snapshot, so - // compare against the same collapse rather than against the corpus length. - const fresh: Record = {}; - for (const c of CLASSIFIED) fresh[c.ruleId] = c.evaluability; - - const drifted = Object.keys(fresh) - .filter(id => snapshot.classes[id] !== fresh[id]) - .map(id => `${id}: snapshot says ${snapshot.classes[id] ?? '(absent)'}, triage says ${fresh[id]}`); - const orphaned = Object.keys(snapshot.classes).filter(id => !(id in fresh)); - - expect(drifted).toEqual([]); - expect(orphaned).toEqual([]); - }); - - it('carries counts that agree with its own per-rule classes', () => { - // A snapshot whose header disagreed with its body would let the .mjs guard - // pass on the header while the mapping was stamped from the body. - const tally: Record = {}; - for (const rule of CORPUS) { - const klass = snapshot.classes[rule.id]; - tally[klass] = (tally[klass] ?? 0) + 1; - } - expect(tally).toEqual(snapshot.counts); - }); -}); diff --git a/src/packages/core-domain/test/rule-corpus-triage.ts b/src/packages/core-domain/test/rule-corpus-triage.ts index d77d44a3..3b6522e8 100644 --- a/src/packages/core-domain/test/rule-corpus-triage.ts +++ b/src/packages/core-domain/test/rule-corpus-triage.ts @@ -35,6 +35,7 @@ import { ADR_CONFORMANCE_CATEGORY, ClassifiedRule, EvaluabilitySummary, + RuleEvaluability, classifyRule, summarizeEvaluability, } from '../src/application/validators/rule-evaluability'; @@ -161,3 +162,85 @@ export function triageCorpus(rulesetsRoot: string = RULESETS_ROOT): CorpusTriage return { corpus, classified, summary: summarizeEvaluability(classified), claims, evaluates }; } + +// --------------------------------------------------------------------------- +// The rendered capture — ONE renderer, two callers +// --------------------------------------------------------------------------- + +/** + * GT-633 reconciliation: this function had TWO implementations. + * + * The fix for GT-633 was written twice, in parallel, by sessions that could not + * see each other. One shipped a standalone capture script (`capture-native- + * evaluability-snapshot.mjs`, reachable by the derived-artifact chain guard and + * by the documentation job); the other rendered the document inside + * `rule-corpus-triage.spec.ts`, where jest can pin it byte-for-byte. Both were + * right about their half, and both recaptured the same numbers. + * + * But two generators for one artifact is EXACTLY the defect GT-633 exists to + * remove, reproduced one level up: whichever ran last would win, and the other's + * `--check` would go red for no reason a reader could see. + * + * So the document is rendered HERE, once, and both callers use it: + * - `rule-corpus-triage.spec.ts` pins it byte-for-byte (it can recompute); + * - `capture-native-evaluability-snapshot.mjs` writes it and `--check`s it, + * which is what the GT-630 chain and `docs.yml` invoke. + * + * `capturedOn` is deliberately STICKY: it is passed in by the caller from the + * committed document when the classification has not moved, so replaying the + * chain stays byte-identical instead of rewriting a date on every run. + */ +export const SNAPSHOT_SHAPE_VERSION = '1.1.0'; + +/** One reading order for the six classes, shared by `counts` and `validation`. */ +export const CLASS_ORDER: readonly RuleEvaluability[] = [ + 'native-handler', + 'documentation-only', + 'unimplemented-native', + 'needs-external-system', + 'needs-runtime', + 'underspecified', +]; + +export interface SnapshotRenderOptions { + /** Carried forward from the committed file while the classification is unchanged. */ + readonly capturedOn: string; +} + +/** The snapshot document, as bytes, straight from a live triage. */ +export function renderSnapshot(triage: CorpusTriage, { capturedOn }: SnapshotRenderOptions): string { + const counts: Record = {}; + for (const klass of CLASS_ORDER) counts[klass] = triage.summary.byClass[klass]; + + const classes: Record = {}; + for (const c of triage.classified) classes[c.ruleId] = c.evaluability; + + const doc = { + $id: 'https://evolith.dev/rulesets/standards/native-evaluability-snapshot.json', + title: 'Native-engine evaluability class per rule (snapshot)', + description: + 'Per-rule evaluability class as computed by the Core native evaluator triage. This is a GENERATED CAPTURE, not the source of truth: the authority is src/packages/core-domain/src/application/validators/rule-evaluability.ts and the handler set registered in native-evaluator.ts. It is recorded here so the ISO/IEC 5055 mapping can be scoped to the real handler backlog without src/rulesets depending on a package it does not own. Do not hand-edit — regenerate.', + version: SNAPSHOT_SHAPE_VERSION, + capturedOn, + capturedFrom: [ + 'src/packages/core-domain/src/application/validators/rule-evaluability.ts (RULE_TRIAGE, classifyRule, ADR_CONFORMANCE_CATEGORY)', + 'src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts (registered handler set)', + 'src/packages/core-domain/src/application/validators/evaluators/handlers/**/*.ts (canHandle predicates)', + 'src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, classification and this renderer)', + ], + regenerateWith: 'node src/rulesets/standards/capture-native-evaluability-snapshot.mjs', + validation: `Rendered by test/rule-corpus-triage.ts from the live triage, written by capture-native-evaluability-snapshot.mjs and pinned byte-for-byte by rule-corpus-triage.spec.ts, so a divergence between this file and Core is a failing test rather than silent drift (corpus ${triage.summary.total}; ${CLASS_ORDER.map(k => `${k} ${triage.summary.byClass[k]}`).join(', ')}).`, + // Both numbers, because they are not the same question — and the + // documentation-job guard asserts on them. `corpusSize` is what Core + // classified and what `counts` sums to; `distinctRuleIds` is how many keys + // `classes` can hold. They differ exactly when one rule id appears in more + // than one ruleset file, and a guard that assumed they were equal would go + // red on a corpus change that is not drift. + corpusSize: triage.corpus.length, + distinctRuleIds: Object.keys(classes).length, + counts, + classes, + }; + + return JSON.stringify(doc, null, 2) + '\n'; +} diff --git a/src/rulesets/standards/capture-native-evaluability-snapshot.mjs b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs index b3ec0379..c1386f19 100644 --- a/src/rulesets/standards/capture-native-evaluability-snapshot.mjs +++ b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs @@ -118,23 +118,32 @@ function resolveTsNodeRegister() { * Core is CommonJS TypeScript compiled by `ts-node`, and the boundary between * them is a process, not a module resolution problem worth solving. */ -function runTriage() { +function runTriage(capturedOn) { + // GT-633 RECONCILIATION: this script used to BUILD the document itself, while + // `rule-corpus-triage.spec.ts` built a second copy of it. Two generators for one + // artifact is the defect GT-633 exists to remove, one level up — whichever ran + // last would win and the other's `--check` would go red for no visible reason. + // + // The renderer now lives once, in `test/rule-corpus-triage.ts`, and this script + // is a thin driver over it: it supplies the sticky `capturedOn` and writes the + // bytes. The spec pins the same bytes. One renderer, two callers. const program = ` - const { triageCorpus } = require(${JSON.stringify(path.join(REPO_ROOT, TRIAGE_MODULE))}); - const { corpus, classified, summary } = triageCorpus(); + const { triageCorpus, renderSnapshot } = require(${JSON.stringify(path.join(REPO_ROOT, TRIAGE_MODULE))}); + const triage = triageCorpus(); const classes = {}; const conflicts = []; - for (const c of classified) { + for (const c of triage.classified) { if (classes[c.ruleId] && classes[c.ruleId] !== c.evaluability) { conflicts.push(c.ruleId + ': ' + classes[c.ruleId] + ' vs ' + c.evaluability); } classes[c.ruleId] = c.evaluability; } process.stdout.write(JSON.stringify({ - corpusSize: corpus.length, - counts: summary.byClass, + corpusSize: triage.corpus.length, + counts: triage.summary.byClass, classes, conflicts, + document: renderSnapshot(triage, { capturedOn: ${JSON.stringify(capturedOn)} }), })); `; @@ -197,64 +206,54 @@ function runTriage() { * identical classification would turn every recapture into a diff and make real * drift harder to see in review. */ -function capturedOn(previous, changed) { - if (!changed && previous?.capturedOn) return previous.capturedOn; +function today() { return new Date().toISOString().slice(0, 10); } -function render(triage, previous) { - const counts = Object.fromEntries( - KNOWN_CLASSES.filter((c) => triage.counts[c] !== undefined).map((c) => [c, triage.counts[c]]), - ); - const classes = Object.fromEntries(Object.entries(triage.classes)); +/** + * Has the CLASSIFICATION moved, as opposed to the provenance? + * + * `capturedOn` is provenance, not content: bumping it on a run that produced an + * identical classification would turn every recapture into a diff and make real + * drift harder to see in review. It is also what keeps the GT-630 chain's + * fixed-point replay byte-identical. + */ +function classificationChanged(previous, triage) { + if (!previous) return true; - const body = { - counts, - classes, + // ORDER-INSENSITIVE, and that is not a detail. The first version compared + // `JSON.stringify(previous.counts)` against a counts object rebuilt in + // KNOWN_CLASSES order, while the renderer emits them in CLASS_ORDER — a + // different order for the same six numbers. So "changed" was ALWAYS true, the + // date was rewritten on every run, and the GT-630 fixed-point replay would have + // failed on any day after the capture. It passed the day it was written because + // both runs stamped the same date, which is precisely the kind of green this + // chain exists to distrust. + const sameMap = (a = {}, b = {}) => { + const ka = Object.keys(a).sort(); + const kb = Object.keys(b).sort(); + return ka.length === kb.length && ka.every((k, i) => kb[i] === k && a[k] === b[k]); }; - const changed = - !previous || - JSON.stringify(previous.counts) !== JSON.stringify(body.counts) || - JSON.stringify(previous.classes) !== JSON.stringify(body.classes); - const pinned = KNOWN_CLASSES.filter((c) => counts[c] !== undefined) - .map((c) => `${c} ${counts[c]}`) - .join(', '); - - const doc = { - $id: 'https://evolith.dev/rulesets/standards/native-evaluability-snapshot.json', - title: 'Native-engine evaluability class per rule (snapshot)', - description: - 'Per-rule evaluability class as computed by the Core native evaluator triage. This is a CAPTURED SNAPSHOT, not the source of truth: the authority is src/packages/core-domain/src/application/validators/rule-evaluability.ts and its pinned spec rule-corpus-triage.spec.ts. It is recorded here so the ISO/IEC 5055 mapping can be scoped to the real handler backlog without src/rulesets depending on a package it does not own.', - version: '1.1.0', - generatedBy: 'src/rulesets/standards/capture-native-evaluability-snapshot.mjs', - capturedOn: capturedOn(previous, changed), - capturedFrom: [ - 'src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, real handler set, classification)', - 'src/packages/core-domain/src/application/validators/rule-evaluability.ts (RULE_TRIAGE, classifyRule, ADR_CONFORMANCE_CATEGORY)', - 'src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts (registered handler set)', - 'src/packages/core-domain/src/application/validators/evaluators/handlers/**/*.ts (canHandle predicates)', - 'src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts (the pinned counts, asserted against this file)', - ], - validation: - `Captured from a live run of the Core triage over ${triage.corpusSize} rules (${pinned}). ` + - 'Recapture with `node src/rulesets/standards/capture-native-evaluability-snapshot.mjs`, then rebuild ' + - 'the mapping with `node src/rulesets/standards/build-iso-5055-mapping.mjs` — the mapping stamps ' + - 'nativeEvaluability per rule from this file, so rebuilding first would launder a stale class into it. ' + - 'Drift is caught in two places: rule-corpus-triage.spec.ts compares this file against a fresh triage ' + - '(core-domain jest), and iso-5055-mapping.test.mjs checks it against the counts pinned in that spec ' + - '(dependency-free documentation job).', - // Both numbers, because they are not the same question. `corpusSize` is what - // Core classified and what `counts` sums to; `distinctRuleIds` is how many - // keys `classes` can hold. They differ exactly when one rule id appears in - // more than one ruleset file, and a guard that assumed they were equal would - // go red on a corpus change that is not drift. - corpusSize: triage.corpusSize, - distinctRuleIds: Object.keys(classes).length, - ...body, - }; + return !sameMap(previous.counts, triage.counts) || !sameMap(previous.classes, triage.classes); +} - return { json: JSON.stringify(doc, null, 2) + '\n', changed }; +/** + * The document, from the ONE renderer in `test/rule-corpus-triage.ts`. + * + * The date is a chicken-and-egg: stickiness is decided by whether the + * classification moved, which is only known after the triage runs, but the + * renderer needs the date as input. Rather than pay for a second ts-node run, + * the triage is rendered once with the PREVIOUS date and, if the classification + * did move, that single field is replaced. `JSON.parse` preserves key insertion + * order, so re-stringifying with the same formatting reproduces the renderer's + * bytes exactly — verified by `--check` being a byte comparison. + */ +function documentFrom(triage, previous, changed) { + if (!changed) return triage.document; + const doc = JSON.parse(triage.document); + doc.capturedOn = today(); + return JSON.stringify(doc, null, 2) + '\n'; } // --------------------------------------------------------------------------- @@ -284,8 +283,9 @@ function readCommittedSnapshot() { const onDisk = readCommittedSnapshot(); const previous = onDisk ? JSON.parse(onDisk) : undefined; -const triage = runTriage(); -const { json, changed } = render(triage, previous); +const triage = runTriage(previous?.capturedOn ?? today()); +const changed = classificationChanged(previous, triage); +const json = documentFrom(triage, previous, changed); if (CHECK) { if (onDisk !== json) { diff --git a/src/rulesets/standards/iso-5055-mapping.json b/src/rulesets/standards/iso-5055-mapping.json index 2110de05..b3341157 100644 --- a/src/rulesets/standards/iso-5055-mapping.json +++ b/src/rulesets/standards/iso-5055-mapping.json @@ -83,11 +83,10 @@ "handlerBacklog": { "source": "native-evaluability-snapshot.json", "sourceAuthority": [ - "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, real handler set, classification)", "src/packages/core-domain/src/application/validators/rule-evaluability.ts (RULE_TRIAGE, classifyRule, ADR_CONFORMANCE_CATEGORY)", "src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts (registered handler set)", "src/packages/core-domain/src/application/validators/evaluators/handlers/**/*.ts (canHandle predicates)", - "src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts (the pinned counts, asserted against this file)" + "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, classification and this renderer)" ], "note": "The handler backlog is the `unimplemented-native` class only. `documentation-only` and `underspecified` rules are not handler work at all, and the two adapter classes are closed by the enforcer seam rather than by a rule handler.", "realBacklogSize": 48, diff --git a/src/rulesets/standards/native-evaluability-snapshot.json b/src/rulesets/standards/native-evaluability-snapshot.json index 2311abaf..bb7ca24f 100644 --- a/src/rulesets/standards/native-evaluability-snapshot.json +++ b/src/rulesets/standards/native-evaluability-snapshot.json @@ -1,26 +1,25 @@ { "$id": "https://evolith.dev/rulesets/standards/native-evaluability-snapshot.json", "title": "Native-engine evaluability class per rule (snapshot)", - "description": "Per-rule evaluability class as computed by the Core native evaluator triage. This is a CAPTURED SNAPSHOT, not the source of truth: the authority is src/packages/core-domain/src/application/validators/rule-evaluability.ts and its pinned spec rule-corpus-triage.spec.ts. It is recorded here so the ISO/IEC 5055 mapping can be scoped to the real handler backlog without src/rulesets depending on a package it does not own.", + "description": "Per-rule evaluability class as computed by the Core native evaluator triage. This is a GENERATED CAPTURE, not the source of truth: the authority is src/packages/core-domain/src/application/validators/rule-evaluability.ts and the handler set registered in native-evaluator.ts. It is recorded here so the ISO/IEC 5055 mapping can be scoped to the real handler backlog without src/rulesets depending on a package it does not own. Do not hand-edit — regenerate.", "version": "1.1.0", - "generatedBy": "src/rulesets/standards/capture-native-evaluability-snapshot.mjs", "capturedOn": "2026-07-29", "capturedFrom": [ - "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, real handler set, classification)", "src/packages/core-domain/src/application/validators/rule-evaluability.ts (RULE_TRIAGE, classifyRule, ADR_CONFORMANCE_CATEGORY)", "src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts (registered handler set)", "src/packages/core-domain/src/application/validators/evaluators/handlers/**/*.ts (canHandle predicates)", - "src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts (the pinned counts, asserted against this file)" + "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, classification and this renderer)" ], - "validation": "Captured from a live run of the Core triage over 389 rules (native-handler 154, unimplemented-native 48, needs-runtime 17, needs-external-system 20, documentation-only 136, underspecified 14). Recapture with `node src/rulesets/standards/capture-native-evaluability-snapshot.mjs`, then rebuild the mapping with `node src/rulesets/standards/build-iso-5055-mapping.mjs` — the mapping stamps nativeEvaluability per rule from this file, so rebuilding first would launder a stale class into it. Drift is caught in two places: rule-corpus-triage.spec.ts compares this file against a fresh triage (core-domain jest), and iso-5055-mapping.test.mjs checks it against the counts pinned in that spec (dependency-free documentation job).", + "regenerateWith": "node src/rulesets/standards/capture-native-evaluability-snapshot.mjs", + "validation": "Rendered by test/rule-corpus-triage.ts from the live triage, written by capture-native-evaluability-snapshot.mjs and pinned byte-for-byte by rule-corpus-triage.spec.ts, so a divergence between this file and Core is a failing test rather than silent drift (corpus 389; native-handler 154, documentation-only 136, unimplemented-native 48, needs-external-system 20, needs-runtime 17, underspecified 14).", "corpusSize": 389, "distinctRuleIds": 389, "counts": { "native-handler": 154, + "documentation-only": 136, "unimplemented-native": 48, - "needs-runtime": 17, "needs-external-system": 20, - "documentation-only": 136, + "needs-runtime": 17, "underspecified": 14 }, "classes": {