From 78e9b092d60e134266ddb0b246b04a6888408b71 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:11:06 -0500 Subject: [PATCH 01/10] fix(standards): make the evaluability guard read Core, and capture the snapshot it guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `native-evaluability-snapshot.json` declared in its own header that it was a capture. Nothing captured it. It was hand-maintained, and it drifted: it said `documentation-only: 129` while Core pinned 136, and it went on calling rules `unimplemented-native` after they got handlers. The guard could not see any of that. It compared the snapshot's counts against six numbers typed into `iso-5055-mapping.test.mjs` — the same six that are literals inside the snapshot. Expected value and actual value were copies of each other, so it passed for as long as the file was unchanged, whatever Core actually pinned. A guard like that is worse than none: it reports agreement it never checked. Three checks replace it, each in the job that can afford it: - `rule-corpus-triage.spec.ts` (core-domain jest, required on main) asserts the committed snapshot against a FRESHLY COMPUTED triage — counts and the class of every rule, in both directions. This is the real guard: it is the only place with the dependencies to recompute the truth. - `iso-5055-mapping.test.mjs` runs in the documentation job, which has no node_modules, so it reads Core's counts OUT OF the spec that pins them instead of retyping them. Parsing a source file is a real coupling and it is stated rather than hidden: a missing or reshaped literal THROWS. Failing to find the source of truth must never read as agreement with it. Two further assertions: the snapshot's header must agree with its own body, and every class stamped into the mapping must match the snapshot. - `capture-native-evaluability-snapshot.mjs --check` is the end-to-end re-derivation, runnable wherever the workspace is installed. The capture script is the missing half. It runs Core's REAL triage through ts-node — same shape as the ABAC generator — because a second implementation of the classification here would be a second source of truth, which is the defect being removed. To make that possible the measurement moved out of the spec, where only jest could reach it, into `test/rule-corpus-triage.ts`; that unreachability is why the snapshot was maintained by hand in the first place. Order matters and is now written down in both READMEs and in CI: recapture, THEN rebuild. `build-iso-5055-mapping.mjs` stamps `nativeEvaluability` onto every row from the snapshot, so rebuilding first launders a stale class into a 388-row artifact and overstates the handler backlog by however many rules Core has closed. Recaptured on this branch: documentation-only 129 -> 136, corpus 379 -> 386 (CORE-0118-01..CORE-0124-01, the seven generated ADR rulesets GT-627 surfaced). Mapping rebuilt after: 381 -> 388 rows. The backlog stays 60. Also wired into CI: the mapping guard ran in NO workflow. It does now. Verified: core-domain jest 1368 passed / 125 suites; node --test 9/9; guards 01, 04, 40, 46, 47 green. Negative-tested both failure modes — reverting the count to 129 goes red, and building the mapping from a stale snapshot goes red on the stamp test. Co-Authored-By: Claude Opus 5 --- .github/workflows/docs.yml | 17 ++ .../validators/rule-corpus-triage.spec.ts | 200 ++++++-------- .../core-domain/test/rule-corpus-triage.ts | 156 +++++++++++ src/rulesets/standards/README.es.md | 51 +++- src/rulesets/standards/README.md | 50 +++- .../capture-native-evaluability-snapshot.mjs | 256 ++++++++++++++++++ src/rulesets/standards/iso-5055-mapping.csv | 7 + src/rulesets/standards/iso-5055-mapping.json | 154 ++++++++++- .../standards/iso-5055-mapping.test.mjs | 131 ++++++++- .../native-evaluability-snapshot.json | 21 +- 10 files changed, 873 insertions(+), 170 deletions(-) create mode 100644 src/packages/core-domain/test/rule-corpus-triage.ts create mode 100644 src/rulesets/standards/capture-native-evaluability-snapshot.mjs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1caea1de..4d49b6cd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -99,6 +99,23 @@ jobs: - name: Self-tests for the ADR ruleset generator run: node --test .harness/scripts/generate-adr-rulesets.test.mjs + # GT-598: `native-evaluability-snapshot.json` says of itself that it is a + # capture, and until now nothing captured it — it was written by hand, and + # it drifted. It still declared `documentation-only: 129` after Core moved + # to 136 (the same seven ADR rulesets the step above exists for), and its + # guard could not see that, because it compared the snapshot against six + # numbers hardcoded in the test: the same six the snapshot contained. + # + # Drift here is not contained. `build-iso-5055-mapping.mjs` stamps + # nativeEvaluability onto all 388 rows of the mapping FROM this file, so a + # stale class is laundered into the larger artifact and overstates the + # handler backlog. The order of these two steps is the order of the chain. + - name: Native evaluability snapshot is a faithful capture of Core's triage + run: node src/rulesets/standards/capture-native-evaluability-snapshot.mjs --check + + - name: ISO/IEC 5055 mapping guard + run: node --test src/rulesets/standards/iso-5055-mapping.test.mjs + # GT-630: the derived artifacts have a dependency ORDER — the executive # summary is built FROM the maturity reconciliation, which is built from # the board. Generate the summary first and it captures a value the 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 b6e409a9..e3a08f44 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 @@ -9,11 +9,17 @@ * a rule the triage table has not seen, the coverage assertions move and this * suite says so. * + * The computation itself lives in `test/rule-corpus-triage.ts` so the snapshot + * consumed by `src/rulesets/standards` can be CAPTURED from it rather than + * hand-maintained. This suite is its pin; the last describe block below is the + * other half — it fails when the committed snapshot stops agreeing with what a + * fresh triage computes. + * * The two figures worth remembering: * - the handler backlog is **60 rules**, not 240; - * - **129 rules are documentation** — 126 auto-generated ADR-conformance + * - **136 rules are documentation** — auto-generated ADR-conformance * placeholders that say in their own text that no check was wired, plus 3 - * board-judgement rules — and 91 of those are flagged `blocking: true`. + * board-judgement rules — and 96 of those are flagged `blocking: true`. * * Every assertion below fails against the pre-GT-595 code: `classifyRule`, * `summarizeEvaluability` and `AdrConformanceRuleHandler` did not exist, and all @@ -22,121 +28,32 @@ import * as fs from 'fs'; import * as path from 'path'; -import { NativeEvaluator } from './evaluators/native-evaluator'; -import { INativeRuleHandler } from './evaluators/handlers/rule-handler.interface'; -import { NormalizedRule } from '../../domain/models/normalized-rule'; import { - ClassifiedRule, + ADR_CONFORMANCE_CATEGORY, RULE_TRIAGE, RuleEvaluability, - classifyRule, isNonExecutable, - summarizeEvaluability, } from './rule-evaluability'; +import { REPO_ROOT, triageCorpus } from '../../../test/rule-corpus-triage'; -const REPO_ROOT = path.resolve(__dirname, '../../../../../..'); -const RULESETS_ROOT = path.join(REPO_ROOT, 'src', 'rulesets'); - -// --------------------------------------------------------------------------- -// Load the corpus exactly as DiskRulesetRepository does, minus the schema pass -// (this suite measures classification, not schema conformance). -// --------------------------------------------------------------------------- - -function rulesetFiles(dir: string, depth = 0): string[] { - if (depth > 4) return []; - const out: string[] = []; - for (const entry of fs.readdirSync(dir).sort()) { - const full = path.join(dir, entry); - if (entry.endsWith('.rules.json')) { out.push(full); continue; } - if (!entry.includes('.') && fs.statSync(full).isDirectory()) out.push(...rulesetFiles(full, depth + 1)); - } - return out; -} - -const CATEGORY_BY_PREFIX: Record = { - inh: 'inheritance', acl: 'anti-corruption', ocb: 'open-core', gov: 'governance', - evd: 'identity', 'obs-evd': 'tracing', dep: 'version-pinning', tax: 'naming-conventions', - hxa: 'layer-structure', git: 'branch-naming', cicd: 'ci-cd', tpy: 'testing-pyramid', - mtn: 'multi-tenancy', prot: 'protocol', runt: 'multi-runtime', dora: 'metrics', - space: 'metrics', drift: 'governance', 'cli-rr': 'build', 'cli-par': 'shared-logic', - mcp: 'protocol', 'modular-monolith': 'topology', 'distributed-modules': 'module-autonomy', - microservices: 'autonomous-deployment', -}; - -function deriveCategory(raw: Record): string { - if (raw['category']) return String(raw['category']); - const prefix = String(raw['id'] ?? '') - .replace(/-(?:EVD|RR|PAR)-?\d*$/, '') - .replace(/-\d+$/, '') - .toLowerCase(); - return CATEGORY_BY_PREFIX[prefix] ?? 'general'; -} - -function deriveSeverity(raw: Record): NormalizedRule['severity'] { - const declared = String(raw['severity'] ?? '').toUpperCase().trim(); - if (declared === 'MUST NOT') return 'MUST NOT'; - if (declared === 'MUST') return 'MUST'; - if (declared === 'SHOULD') return 'SHOULD'; - if (declared === 'COULD' || declared === 'MAY') return 'COULD'; - return raw['blocking'] === true || raw['enforcement'] ? 'MUST' : 'SHOULD'; -} - -function loadCorpus(): NormalizedRule[] { - const rules: NormalizedRule[] = []; - for (const file of rulesetFiles(RULESETS_ROOT)) { - const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as Record; - const list = (parsed['rules'] ?? parsed['principles']) as Array> | undefined; - if (!Array.isArray(list)) continue; - if (list.length > 0 && !list[0]['id'] && list[0]['rules']) continue; - - for (const raw of list.filter(r => Boolean(r['id']))) { - // `blocking` defaults from the RAW severity string, exactly as - // DiskRulesetRepository.defaultBlocking does — NOT from the normalized - // severity, which promotes `enforcement`-bearing rules to MUST. - const rawSeverity = String(raw['severity'] ?? '').toUpperCase(); - rules.push({ - id: String(raw['id']), - severity: deriveSeverity(raw), - category: deriveCategory(raw), - title: String(raw['title'] ?? raw['principle'] ?? raw['id']), - description: String(raw['description'] ?? raw['statement'] ?? ''), - blocking: Boolean(raw['blocking'] ?? (rawSeverity === 'MUST' || rawSeverity === 'MUST NOT')), - validationQuery: raw['validationQuery'] ? String(raw['validationQuery']) : undefined, - sourceFile: path.relative(REPO_ROOT, file), - }); - } - } - return rules; -} - -/** The real handler set, asked only which rules it CLAIMS (no I/O performed). */ -function handlerSet(): INativeRuleHandler[] { - const evaluator = new NativeEvaluator({} as never, {} as never, {} as never); - return (evaluator as unknown as { handlers: INativeRuleHandler[] }).handlers; -} - -const CORPUS = loadCorpus(); -const HANDLERS = handlerSet(); - -/** A handler claims the rule — it is routed somewhere instead of falling through. */ -const claims = (rule: NormalizedRule) => HANDLERS.some(h => h.canHandle(rule)); +const { corpus: CORPUS, classified: CLASSIFIED, summary: SUMMARY, claims } = triageCorpus(); /** - * A handler claims the rule AND can return a verdict for it. + * The class counts this repository is pinned to. * - * {@link AdrConformanceRuleHandler} is deliberately excluded: it claims the 126 - * generated rules only to CLASSIFY them, and never returns `passed`. Counting - * "claimed" as "evaluated" would reproduce, one layer up, exactly the false - * green GT-569 removed — so this predicate, not `claims`, drives the breakdown. + * These are the numbers `native-evaluability-snapshot.json` must reproduce, and + * the ones its capture script writes. Recomputed on every run: they are what + * turns "240 handlers to write" into a costed decision, so they are pinned + * rather than merely printed. Measured 2026-07-28 against 386 rules. */ -const evaluates = (rule: NormalizedRule) => claims(rule) && rule.category !== 'adr-conformance'; - -const CLASSIFIED: ClassifiedRule[] = CORPUS.map(rule => { - const { evaluability, why } = classifyRule(rule, evaluates(rule)); - return { ruleId: rule.id, sourceFile: rule.sourceFile, blocking: rule.blocking, evaluability, why }; -}); - -const SUMMARY = summarizeEvaluability(CLASSIFIED); +const PINNED_CLASS_COUNTS: Readonly> = { + 'native-handler': 139, + 'documentation-only': 136, + 'unimplemented-native': 60, + 'needs-external-system': 20, + 'needs-runtime': 17, + underspecified: 14, +}; describe('GT-595 · the corpus is fully classified', () => { it('loads a corpus of the expected size (guards the measurement itself)', () => { @@ -176,17 +93,7 @@ describe('GT-595 · the corpus is fully classified', () => { describe('GT-595 · the published breakdown, with its denominator', () => { it('reports the measured class counts', () => { - // Measured 2026-07-28 against 379 rules in 167 ruleset files. Recomputed on - // every run: these are the numbers that turn "240 handlers to write" into a - // costed decision, so they are pinned rather than merely printed. - expect(SUMMARY.byClass).toEqual({ - 'native-handler': 139, - 'documentation-only': 136, - 'unimplemented-native': 60, - 'needs-external-system': 20, - 'needs-runtime': 17, - underspecified: 14, - }); + expect(SUMMARY.byClass).toEqual(PINNED_CLASS_COUNTS); }); it('publishes the honest denominator: 150 rules nothing can ever run', () => { @@ -242,7 +149,7 @@ describe('GT-595 · the handler slice that landed', () => { // 126 -> 133 on 2026-07-28: the committed corpus was seven rulesets behind // its generator (ADR-0118 plus the six security standards 0119..0124), which // regenerating for GT-571 surfaced. - const adrConformance = CORPUS.filter(r => r.category === 'adr-conformance'); + const adrConformance = CORPUS.filter(r => r.category === ADR_CONFORMANCE_CATEGORY); expect(adrConformance).toHaveLength(133); expect(adrConformance.every(claims)).toBe(true); }); @@ -255,7 +162,7 @@ describe('GT-595 · the handler slice that landed', () => { it('does not pretend the ADR-conformance rules were EVALUATED', () => { // The handler claims them so they can be classified; it never returns // `passed`. Coverage is honest precisely because this is true. - const adrIds = new Set(CORPUS.filter(r => r.category === 'adr-conformance').map(r => r.id)); + const adrIds = new Set(CORPUS.filter(r => r.category === ADR_CONFORMANCE_CATEGORY).map(r => r.id)); const classifiedAdr = CLASSIFIED.filter(c => adrIds.has(c.ruleId)); expect(classifiedAdr.every(c => c.evaluability === 'documentation-only')).toBe(true); }); @@ -279,3 +186,56 @@ describe('GT-595 · the remaining backlog is costed, not a lump', () => { expect(isNonExecutable('underspecified')).toBe(true); }); }); + +/** + * 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. Its own guard (`iso-5055-mapping.test.mjs`) cannot catch that on its + * own — it runs in a job with no node_modules and so cannot compute the truth. + * + * This is where the truth exists, so this is where the comparison belongs. If it + * fails, run: + * node src/rulesets/standards/capture-native-evaluability-snapshot.mjs + * node src/rulesets/standards/build-iso-5055-mapping.mjs + */ +describe('GT-598 · the captured snapshot still matches a fresh triage', () => { + const SNAPSHOT_PATH = path.join(REPO_ROOT, 'src/rulesets/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, so compare against + // the same collapse rather than against the raw 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 disagrees with its body would let the .mjs guard + // pass on the header while the mapping is 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 new file mode 100644 index 00000000..8a53a852 --- /dev/null +++ b/src/packages/core-domain/test/rule-corpus-triage.ts @@ -0,0 +1,156 @@ +/** + * GT-598 — the corpus triage, extracted so it has more than one caller. + * + * WHY THIS FILE EXISTS + * This code used to live inside `rule-corpus-triage.spec.ts`, where it was + * reachable only by jest. That made `src/rulesets/standards/native-evaluability- + * snapshot.json` impossible to CAPTURE: nothing outside the spec could ask the + * real handler set which rules it claims, so the snapshot was maintained by + * hand, and drifted (it still said `documentation-only: 129` after Core moved to + * 136, and its guard could not see it because the guard re-read the snapshot's + * own literals). + * + * So the measurement moved here. It has two callers now, and they are the two + * halves of the same guarantee: + * + * - `rule-corpus-triage.spec.ts` pins the numbers and asserts the committed + * snapshot still agrees with a freshly computed triage; + * - `src/rulesets/standards/capture-native-evaluability-snapshot.mjs` + * regenerates that snapshot from this same computation. + * + * It lives under `test/` rather than `src/` deliberately: it walks the + * repository with `fs`, which is not something the published application layer + * should do, and `files: ["dist"]` keeps it out of the package. + * + * Nothing here is mocked. The handler set is the real {@link NativeEvaluator} + * one, asked only which rules it CLAIMS — no evaluation, no I/O. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { NativeEvaluator } from '../src/application/validators/evaluators/native-evaluator'; +import { INativeRuleHandler } from '../src/application/validators/evaluators/handlers/rule-handler.interface'; +import { NormalizedRule } from '../src/domain/models/normalized-rule'; +import { + ADR_CONFORMANCE_CATEGORY, + ClassifiedRule, + EvaluabilitySummary, + classifyRule, + summarizeEvaluability, +} from '../src/application/validators/rule-evaluability'; + +/** Repository root, from this file's location (`/src/packages/core-domain/test`). */ +export const REPO_ROOT = path.resolve(__dirname, '../../../..'); +export const RULESETS_ROOT = path.join(REPO_ROOT, 'src', 'rulesets'); + +// --------------------------------------------------------------------------- +// Load the corpus exactly as DiskRulesetRepository does, minus the schema pass +// (this measures classification, not schema conformance). +// --------------------------------------------------------------------------- + +function rulesetFiles(dir: string, depth = 0): string[] { + if (depth > 4) return []; + const out: string[] = []; + for (const entry of fs.readdirSync(dir).sort()) { + const full = path.join(dir, entry); + if (entry.endsWith('.rules.json')) { out.push(full); continue; } + if (!entry.includes('.') && fs.statSync(full).isDirectory()) out.push(...rulesetFiles(full, depth + 1)); + } + return out; +} + +const CATEGORY_BY_PREFIX: Record = { + inh: 'inheritance', acl: 'anti-corruption', ocb: 'open-core', gov: 'governance', + evd: 'identity', 'obs-evd': 'tracing', dep: 'version-pinning', tax: 'naming-conventions', + hxa: 'layer-structure', git: 'branch-naming', cicd: 'ci-cd', tpy: 'testing-pyramid', + mtn: 'multi-tenancy', prot: 'protocol', runt: 'multi-runtime', dora: 'metrics', + space: 'metrics', drift: 'governance', 'cli-rr': 'build', 'cli-par': 'shared-logic', + mcp: 'protocol', 'modular-monolith': 'topology', 'distributed-modules': 'module-autonomy', + microservices: 'autonomous-deployment', +}; + +function deriveCategory(raw: Record): string { + if (raw['category']) return String(raw['category']); + const prefix = String(raw['id'] ?? '') + .replace(/-(?:EVD|RR|PAR)-?\d*$/, '') + .replace(/-\d+$/, '') + .toLowerCase(); + return CATEGORY_BY_PREFIX[prefix] ?? 'general'; +} + +function deriveSeverity(raw: Record): NormalizedRule['severity'] { + const declared = String(raw['severity'] ?? '').toUpperCase().trim(); + if (declared === 'MUST NOT') return 'MUST NOT'; + if (declared === 'MUST') return 'MUST'; + if (declared === 'SHOULD') return 'SHOULD'; + if (declared === 'COULD' || declared === 'MAY') return 'COULD'; + return raw['blocking'] === true || raw['enforcement'] ? 'MUST' : 'SHOULD'; +} + +export function loadCorpus(rulesetsRoot: string = RULESETS_ROOT): NormalizedRule[] { + const rules: NormalizedRule[] = []; + for (const file of rulesetFiles(rulesetsRoot)) { + const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as Record; + const list = (parsed['rules'] ?? parsed['principles']) as Array> | undefined; + if (!Array.isArray(list)) continue; + if (list.length > 0 && !list[0]['id'] && list[0]['rules']) continue; + + for (const raw of list.filter(r => Boolean(r['id']))) { + // `blocking` defaults from the RAW severity string, exactly as + // DiskRulesetRepository.defaultBlocking does — NOT from the normalized + // severity, which promotes `enforcement`-bearing rules to MUST. + const rawSeverity = String(raw['severity'] ?? '').toUpperCase(); + rules.push({ + id: String(raw['id']), + severity: deriveSeverity(raw), + category: deriveCategory(raw), + title: String(raw['title'] ?? raw['principle'] ?? raw['id']), + description: String(raw['description'] ?? raw['statement'] ?? ''), + blocking: Boolean(raw['blocking'] ?? (rawSeverity === 'MUST' || rawSeverity === 'MUST NOT')), + validationQuery: raw['validationQuery'] ? String(raw['validationQuery']) : undefined, + sourceFile: path.relative(REPO_ROOT, file), + }); + } + } + return rules; +} + +/** The real handler set, asked only which rules it CLAIMS (no I/O performed). */ +export function handlerSet(): INativeRuleHandler[] { + const evaluator = new NativeEvaluator({} as never, {} as never, {} as never); + return (evaluator as unknown as { handlers: INativeRuleHandler[] }).handlers; +} + +export interface CorpusTriage { + readonly corpus: readonly NormalizedRule[]; + readonly classified: readonly ClassifiedRule[]; + readonly summary: EvaluabilitySummary; + /** A handler claims the rule — it is routed somewhere instead of falling through. */ + readonly claims: (rule: NormalizedRule) => boolean; + /** A handler claims the rule AND can return a verdict for it. */ + readonly evaluates: (rule: NormalizedRule) => boolean; +} + +/** + * Classify the whole corpus with the real handler set. + * + * {@link import('../src/application/validators/evaluators/handlers/adr-conformance-rule.handler').AdrConformanceRuleHandler} + * is deliberately excluded from `evaluates`: it claims the generated ADR rules + * only to CLASSIFY them, and never returns `passed`. Counting "claimed" as + * "evaluated" would reproduce, one layer up, exactly the false green GT-569 + * removed — so `evaluates`, not `claims`, drives the breakdown. + */ +export function triageCorpus(rulesetsRoot: string = RULESETS_ROOT): CorpusTriage { + const corpus = loadCorpus(rulesetsRoot); + const handlers = handlerSet(); + + const claims = (rule: NormalizedRule) => handlers.some(h => h.canHandle(rule)); + const evaluates = (rule: NormalizedRule) => claims(rule) && rule.category !== ADR_CONFORMANCE_CATEGORY; + + const classified: ClassifiedRule[] = corpus.map(rule => { + const { evaluability, why } = classifyRule(rule, evaluates(rule)); + return { ruleId: rule.id, sourceFile: rule.sourceFile, blocking: rule.blocking, evaluability, why }; + }); + + return { corpus, classified, summary: summarizeEvaluability(classified), claims, evaluates }; +} diff --git a/src/rulesets/standards/README.es.md b/src/rulesets/standards/README.es.md index d2b0aded..79c97eec 100644 --- a/src/rulesets/standards/README.es.md +++ b/src/rulesets/standards/README.es.md @@ -17,10 +17,23 @@ confiar en nosotros. | `iso-5055-weaknesses.json` | Los 138 identificadores CWE de ISO/IEC 5055, por medida, con procedencia. | | `iso-5055-mapping.json` | Una fila por regla del corpus: mapeo a CWE (o un "sin equivalente" explícito con motivo), adoptabilidad por analizador y clase de evaluabilidad nativa. | | `iso-5055-mapping.csv` | La misma tabla, plana, para hojas de cálculo y auditores. | -| `native-evaluability-snapshot.json` | Captura por regla de la clase de evaluabilidad del triage del Core, para acotar la aritmética de backlog de abajo al backlog real. | +| `native-evaluability-snapshot.json` | Captura por regla de la clase de evaluabilidad del triage del Core, para acotar la aritmética de backlog de abajo al backlog real. Generado — no editar a mano. | +| `capture-native-evaluability-snapshot.mjs` | La captura. Ejecuta el triage real del Core vía `ts-node`; `--check` falla cuando la captura comiteada ya no es lo que el Core calcula. | | `build-iso-5055-mapping.mjs` | El generador. `--check` falla cuando la tabla se quedó atrás del corpus. | | `iso-5055-mapping.test.mjs` | La guarda. `node --test src/rulesets/standards/iso-5055-mapping.test.mjs`. | +### Regenerar, en orden + +``` +node src/rulesets/standards/capture-native-evaluability-snapshot.mjs # 1. captura +node src/rulesets/standards/build-iso-5055-mapping.mjs # 2. mapeo +``` + +El orden no es una preferencia. El generador estampa `nativeEvaluability` en cada fila del mapeo a +partir de la captura, así que reconstruir primero blanquea una clasificación obsoleta dentro de un +artefacto de 388 filas y sobreestima el backlog de handlers en tantas reglas como el Core haya cerrado +desde la última captura. + Aquí no se reproduce texto de ISO/IEC 5055 ni de ISO/IEC 25010. Solo se registran identificadores CWE, nombres CWE de MITRE y pertenencia a cada medida. @@ -34,20 +47,20 @@ hace que la unión sea menor que la suma. La fecha y el método de extracción c ## Resultado -Sobre **381 reglas** en 167 archivos de ruleset: +Sobre **388 reglas** en 174 archivos de ruleset: | Medida | Cantidad | Proporción | |---|---|---| -| Reglas mapeadas a una debilidad ISO/IEC 5055 | 37 | 9,7% | +| Reglas mapeadas a una debilidad ISO/IEC 5055 | 37 | 9,5% | | — de ellas con mapeo directo | 8 | | | — de ellas con mapeo parcial / proxy | 29 | | -| Reglas sin equivalente internacional (cada una con motivo declarado) | 344 | 90,3% | -| Reglas que un analizador existente podría decidir por completo | 42 | 11,0% | -| Reglas que un analizador podría decidir parcialmente | 23 | 6,0% | +| Reglas sin equivalente internacional (cada una con motivo declarado) | 351 | 90,5% | +| Reglas que un analizador existente podría decidir por completo | 42 | 10,8% | +| Reglas que un analizador podría decidir parcialmente | 23 | 5,9% | -**La fracción adoptada es el 9,7% del corpus.** Es un resultado real y es menor de lo que sugería la +**La fracción adoptada es el 9,5% del corpus.** Es un resultado real y es menor de lo que sugería la premisa del gap, por una razón estructural: ISO/IEC 5055 mide la estructura interna del código fuente, -y 300 de nuestras 381 reglas no tratan de estructura de código. Son conformidad con ADR (155), +y 313 de nuestras 388 reglas no tratan de estructura de código. Son conformidad con ADR (162), contratos de topología (66), invariantes de gobierno (51) y proceso de desarrollo (34). Ningún estándar internacional modela "¿honraste el ADR-0092?", y ninguno lo hará. @@ -58,9 +71,12 @@ mapean y 13 son decidibles por analizador. El enunciado del gap dimensionaba el beneficio contra "~240 handlers por escribir". **Esa cifra ya está retirada.** GT-595 hizo el triage del corpus y el backlog real, decidible desde el repositorio, son **60 -reglas** — la clase `unimplemented-native`. Las otras 180 son 129 placeholders de generador solo -documentales, 14 reglas sin check redactado, 20 que requieren un sistema externo y 17 que requieren uno -en ejecución. +reglas** — la clase `unimplemented-native`. De las 386 reglas que carga el triage del Core, 139 ya se +ejecutan y las otras 187 son 136 placeholders de generador solo documentales, 14 reglas sin check +redactado, 20 que requieren un sistema externo y 17 que requieren uno en ejecución. + +(386, no 388: los dos archivos de regla única llevan sus metadatos en la raíz del documento, y el +cargador de corpus del Core no los lee. Aparecen en el mapeo como `not-in-snapshot`.) Proyectar este mapeo sobre esa clase es la cifra que importa: @@ -101,3 +117,16 @@ reintroduce. `nativeEvaluability` se copia del snapshot de triage del Core. La autoridad es `src/packages/core-domain/src/application/validators/rule-evaluability.ts` y su spec fijado; si esas cifras se mueven, el snapshot de aquí está obsoleto y hay que recapturarlo. + +## Cómo se detecta la deriva + +El snapshot se mantenía a mano hasta GT-598, y derivó: declaraba `documentation-only: 129` mucho después +de que el Core pasara a 136, y la guarda que debía notarlo comparaba el snapshot contra seis números +escritos en el propio test — los mismos seis que el snapshot ya contenía. Comparaba el snapshot consigo +mismo y solo podía pasar. La reemplazan tres controles, en los dos jobs que pueden costearlos: + +| Dónde | Qué prueba | +|---|---| +| `rule-corpus-triage.spec.ts` (jest de core-domain) | El snapshot comiteado es igual a un triage **recalculado** — las cuentas y la clase de cada regla. Esta es la guarda real: tiene las dependencias para recalcular la verdad. | +| `iso-5055-mapping.test.mjs` (job de documentación, sin `node_modules`) | Las cuentas del snapshot son iguales a las **leídas desde** ese spec, su cabecera concuerda con su propio cuerpo, y toda clase estampada en el mapeo coincide con el snapshot. | +| `capture-native-evaluability-snapshot.mjs --check` | La re-derivación extremo a extremo, ejecutable donde el workspace esté instalado. | diff --git a/src/rulesets/standards/README.md b/src/rulesets/standards/README.md index f0802b35..098ea997 100644 --- a/src/rulesets/standards/README.md +++ b/src/rulesets/standards/README.md @@ -17,10 +17,22 @@ to trust us. | `iso-5055-weaknesses.json` | The 138 CWE identifiers of ISO/IEC 5055, split per measure, with provenance. | | `iso-5055-mapping.json` | One row per corpus rule: CWE mapping (or an explicit "no equivalent" with a reason), analyser adoptability, and the rule's native evaluability class. | | `iso-5055-mapping.csv` | The same table, flat, for spreadsheets and auditors. | -| `native-evaluability-snapshot.json` | Captured per-rule evaluability class from Core's triage, so the backlog arithmetic below is scoped to the real backlog. | +| `native-evaluability-snapshot.json` | Captured per-rule evaluability class from Core's triage, so the backlog arithmetic below is scoped to the real backlog. Generated — do not edit by hand. | +| `capture-native-evaluability-snapshot.mjs` | The capture. Runs Core's real triage through `ts-node`; `--check` fails when the committed snapshot is no longer what Core computes. | | `build-iso-5055-mapping.mjs` | The generator. `--check` fails when the table has fallen behind the corpus. | | `iso-5055-mapping.test.mjs` | The guard. `node --test src/rulesets/standards/iso-5055-mapping.test.mjs`. | +### Regenerating, in order + +``` +node src/rulesets/standards/capture-native-evaluability-snapshot.mjs # 1. snapshot +node src/rulesets/standards/build-iso-5055-mapping.mjs # 2. mapping +``` + +The order is not a preference. The generator stamps `nativeEvaluability` onto every row of the mapping +from the snapshot, so rebuilding first launders a stale classification into a 388-row artifact and +overstates the handler backlog by however many rules Core has closed since the last capture. + No text of ISO/IEC 5055 or ISO/IEC 25010 is reproduced here. Only CWE identifiers, MITRE CWE names and measure membership are recorded. @@ -34,20 +46,20 @@ makes the union smaller than the sum. The extraction date and method are recorde ## Result -Against **381 rules** in 167 ruleset files: +Against **388 rules** in 174 ruleset files: | Measure | Count | Share | |---|---|---| -| Rules mapped to an ISO/IEC 5055 weakness | 37 | 9.7% | +| Rules mapped to an ISO/IEC 5055 weakness | 37 | 9.5% | | — of which the mapping is direct | 8 | | | — of which the mapping is a partial / proxy | 29 | | -| Rules with no international equivalent (each with a stated reason) | 344 | 90.3% | -| Rules an existing analyser could decide outright | 42 | 11.0% | -| Rules an analyser could decide partially | 23 | 6.0% | +| Rules with no international equivalent (each with a stated reason) | 351 | 90.5% | +| Rules an existing analyser could decide outright | 42 | 10.8% | +| Rules an analyser could decide partially | 23 | 5.9% | -**The adopted fraction is 9.7% of the corpus.** That is a real result and it is smaller than the gap's +**The adopted fraction is 9.5% of the corpus.** That is a real result and it is smaller than the gap's premise implied, for a structural reason: ISO/IEC 5055 measures the internal structure of source code, -and 300 of our 381 rules are not about source structure at all. They are ADR conformance (155), +and 313 of our 388 rules are not about source structure at all. They are ADR conformance (162), topology contracts (66), governance invariants (51) and development process (34). No international standard models "did you honour ADR-0092", and none ever will. @@ -58,9 +70,12 @@ and 13 are analyser-decidable. The gap statement sized the payoff against "~240 handlers to write". **That figure is already retired.** GT-595 triaged the corpus and the real, decidable-from-the-repository backlog is **60 -rules** — the `unimplemented-native` class. The other 180 are 129 documentation-only generator -placeholders, 14 underspecified rules with no authored check, 20 that need an external system and 17 -that need a running one. +rules** — the `unimplemented-native` class. Of the 386 rules Core's triage loads, 139 already run and +the other 187 are 136 documentation-only generator placeholders, 14 underspecified rules with no +authored check, 20 that need an external system and 17 that need a running one. + +(386, not 388: the two single-rule infrastructure files carry their rule metadata at the document root, +which Core's corpus loader does not read. They appear in the mapping as `not-in-snapshot`.) Folding this mapping onto that class is the number that matters: @@ -99,3 +114,16 @@ stale, and `iso-5055-mapping.test.mjs` fails the build if anything in this direc `nativeEvaluability` is copied from the Core triage snapshot. The authority for it is `src/packages/core-domain/src/application/validators/rule-evaluability.ts` and its pinned spec; if the counts there move, the snapshot here is stale and must be recaptured. + +## How drift is caught + +The snapshot was hand-maintained until GT-598, and it drifted: it declared `documentation-only: 129` +long after Core had moved to 136, and the guard that was meant to notice compared the snapshot against +six numbers typed into the test — the same six the snapshot already contained. It compared the snapshot +to itself and could only ever pass. Three checks replace it, in the two jobs that can afford them: + +| Where | What it proves | +|---|---| +| `rule-corpus-triage.spec.ts` (core-domain jest) | The committed snapshot equals a **freshly computed** triage — counts and every per-rule class. This is the real guard; it has the dependencies to recompute the truth. | +| `iso-5055-mapping.test.mjs` (documentation job, no `node_modules`) | The snapshot's counts equal the counts **read out of** that spec, its header agrees with its own body, and every class stamped into the mapping matches the snapshot. | +| `capture-native-evaluability-snapshot.mjs --check` | The end-to-end re-derivation, runnable anywhere the workspace is installed. | diff --git a/src/rulesets/standards/capture-native-evaluability-snapshot.mjs b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs new file mode 100644 index 00000000..f15a7d05 --- /dev/null +++ b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs @@ -0,0 +1,256 @@ +#!/usr/bin/env node +/** + * @file capture-native-evaluability-snapshot.mjs + * @description GT-598 — capture `native-evaluability-snapshot.json` from Core. + * + * WHY THIS EXISTS + * --------------- + * The snapshot said, in its own header, that it was "a CAPTURED SNAPSHOT, not + * the source of truth". No capture script existed. It was written by hand, and + * it drifted: it still declared `documentation-only: 129` long after Core moved + * to 136, and the guard that was supposed to notice + * (`iso-5055-mapping.test.mjs`) compared the snapshot's counts against six + * numbers hardcoded in the test — the same numbers the snapshot already + * contained. It could only ever pass. This script is the missing half. + * + * Drift here is not contained. `build-iso-5055-mapping.mjs` stamps + * `nativeEvaluability` onto every row of the ISO/IEC 5055 mapping from this + * file, so a stale class silently overstates the handler backlog in a much + * larger derived artifact. ALWAYS recapture before rebuilding the mapping. + * + * WHAT IT RUNS + * ------------ + * The real Core triage — `test/rule-corpus-triage.ts` in @beyondnet/evolith-core-domain, + * which instantiates the real `NativeEvaluator`, asks its handler set which + * rules it claims, and classifies the rest through `classifyRule`. Nothing is + * reimplemented here: a second implementation of the classification would be a + * second source of truth, which is the defect this script exists to remove. + * + * Core is TypeScript, so the triage is executed through `ts-node` (transpile + * only, no emit). That makes this the one script under `src/rulesets` that + * needs `node_modules` — which is exactly why the guard that runs in the + * dependency-free documentation job stays a separate, non-executing check. + * + * USAGE + * node src/rulesets/standards/capture-native-evaluability-snapshot.mjs + * node src/rulesets/standards/capture-native-evaluability-snapshot.mjs --check + * + * EXIT CODES + * 0 snapshot written, or (with --check) already identical to a fresh capture + * 1 the capture disagrees with the committed file, or the triage refused to + * run / returned an implausible corpus + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execFileSync } from 'node:child_process'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(HERE, '..', '..', '..'); +const OUT = path.join(HERE, 'native-evaluability-snapshot.json'); + +const CORE_DOMAIN = 'src/packages/core-domain'; +const TRIAGE_MODULE = `${CORE_DOMAIN}/test/rule-corpus-triage.ts`; +const TSCONFIG = `${CORE_DOMAIN}/tsconfig.json`; + +/** The classes Core can emit. A capture holding anything else is not a capture. */ +const KNOWN_CLASSES = [ + 'native-handler', + 'unimplemented-native', + 'needs-runtime', + 'needs-external-system', + 'documentation-only', + 'underspecified', +]; + +/** + * A corpus scan that finds nothing is never a real state — it is a moved path. + * Refuse to overwrite a good snapshot with the result of one. + */ +const MIN_PLAUSIBLE_CORPUS = 300; + +// --------------------------------------------------------------------------- +// 1. Run the real triage +// --------------------------------------------------------------------------- + +/** + * Execute Core's triage in a child process and read back its result as JSON. + * + * A child process rather than an import: this file is ESM under `src/rulesets`, + * 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() { + const program = ` + const { triageCorpus } = require(${JSON.stringify(path.join(REPO_ROOT, TRIAGE_MODULE))}); + const { corpus, classified, summary } = triageCorpus(); + const classes = {}; + const conflicts = []; + for (const c of 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, + classes, + conflicts, + })); + `; + + let raw; + try { + raw = execFileSync(process.execPath, ['-r', 'ts-node/register', '-e', program], { + cwd: REPO_ROOT, + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + env: { + ...process.env, + TS_NODE_PROJECT: path.join(REPO_ROOT, TSCONFIG), + TS_NODE_TRANSPILE_ONLY: 'true', + }, + stdio: ['ignore', 'pipe', 'inherit'], + }); + } catch (err) { + throw new Error( + `Could not run the Core triage (${TRIAGE_MODULE}). This script needs the workspace ` + + `installed — ts-node and Core's dependencies must be resolvable from ${REPO_ROOT}.\n${err.message}`, + ); + } + + const result = JSON.parse(raw); + + if (result.conflicts.length > 0) { + throw new Error( + 'The same rule id is classified two different ways by the corpus, so a snapshot keyed by ' + + `rule id cannot represent it:\n - ${result.conflicts.join('\n - ')}`, + ); + } + if (result.corpusSize < MIN_PLAUSIBLE_CORPUS) { + throw new Error( + `The triage classified only ${result.corpusSize} rules (expected at least ${MIN_PLAUSIBLE_CORPUS}). ` + + 'A collapsed corpus is a moved path, not progress — refusing to capture it.', + ); + } + for (const [id, klass] of Object.entries(result.classes)) { + if (!KNOWN_CLASSES.includes(klass)) throw new Error(`${id} was classified as unknown class "${klass}".`); + } + + const summed = Object.values(result.counts).reduce((a, b) => a + b, 0); + if (summed !== result.corpusSize) { + throw new Error(`Core's own class counts sum to ${summed} but it classified ${result.corpusSize} rules.`); + } + + return result; +} + +// --------------------------------------------------------------------------- +// 2. Render the snapshot +// --------------------------------------------------------------------------- + +/** + * Keep the capture date stable when nothing else changed. + * + * `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. + */ +function capturedOn(previous, changed) { + if (!changed && previous?.capturedOn) return previous.capturedOn; + 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)); + + const body = { + counts, + classes, + }; + 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 { json: JSON.stringify(doc, null, 2) + '\n', changed }; +} + +// --------------------------------------------------------------------------- +// 3. Entry point +// --------------------------------------------------------------------------- + +const CHECK = process.argv.includes('--check'); + +const previous = fs.existsSync(OUT) ? JSON.parse(fs.readFileSync(OUT, 'utf8')) : undefined; +const triage = runTriage(); +const { json, changed } = render(triage, previous); + +if (CHECK) { + const onDisk = previous ? fs.readFileSync(OUT, 'utf8') : undefined; + if (onDisk !== json) { + console.error( + 'GT-598: native-evaluability-snapshot.json no longer matches a fresh capture of the Core triage.\n' + + ' Re-run: node src/rulesets/standards/capture-native-evaluability-snapshot.mjs\n' + + ' Then: node src/rulesets/standards/build-iso-5055-mapping.mjs', + ); + if (previous) { + for (const c of KNOWN_CLASSES) { + const was = previous.counts?.[c]; + const now = triage.counts[c]; + if (was !== now) console.error(` ${c}: snapshot says ${was ?? '(absent)'}, Core says ${now ?? '(absent)'}`); + } + } + process.exit(1); + } + console.log(`GT-598: native-evaluability-snapshot.json is a faithful capture — ${triage.corpusSize} rules.`); +} else { + fs.writeFileSync(OUT, json); + console.log( + `${changed ? 'Recaptured' : 'Unchanged'} ${path.relative(process.cwd(), OUT)} — ` + + `${triage.corpusSize} rules: ${KNOWN_CLASSES.filter((c) => triage.counts[c] !== undefined).map((c) => `${c} ${triage.counts[c]}`).join(', ')}`, + ); + if (changed) console.log('Now rebuild the mapping: node src/rulesets/standards/build-iso-5055-mapping.mjs'); +} diff --git a/src/rulesets/standards/iso-5055-mapping.csv b/src/rulesets/standards/iso-5055-mapping.csv index 720cf8c8..8fe72f0e 100644 --- a/src/rulesets/standards/iso-5055-mapping.csv +++ b/src/rulesets/standards/iso-5055-mapping.csv @@ -146,6 +146,13 @@ CORE-0113-01,adr/generated/adr-0113-node-js-platform-lighthouse-apache-2-0-as-th CORE-0115-01,adr/generated/adr-0115-emergent-knowledge-axis-knowledge-originated-by-applying-the.rules.json,architecture-decision,,,none,no,documentation-only CORE-0116-01,adr/generated/adr-0116-canonical-finding-contract-and-an-executable-advisory-author.rules.json,architecture-decision,,,none,no,documentation-only CORE-0117-01,adr/generated/adr-0117-bilingual-parity-applies-to-authored-sources-not-generated-p.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0118-01,adr/generated/adr-0118-the-core-hub-has-its-own-root-taxonomy-distinct-from-satelli.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0119-01,adr/generated/adr-0119-api-security-configuration-hardening.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0120-01,adr/generated/adr-0120-ssrf-prevention-standard.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0121-01,adr/generated/adr-0121-input-validation-and-sanitization-standard.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0122-01,adr/generated/adr-0122-shell-execution-safety-standard.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0123-01,adr/generated/adr-0123-timing-safe-comparison-standard.rules.json,architecture-decision,,,none,no,documentation-only +CORE-0124-01,adr/generated/adr-0124-credential-and-secret-management-standard.rules.json,architecture-decision,,,none,no,documentation-only AI-0001-01,adr/generated/adr-ai-augmented-0001-harness-engineering-for-ai-augmented-development.rules.json,architecture-decision,,,none,no,documentation-only AI-0002-01,adr/generated/adr-ai-augmented-0002-mcp-integration-protocol-for-agent-tool-invocation.rules.json,architecture-decision,,,none,no,documentation-only AI-0003-01,adr/generated/adr-ai-augmented-0003-model-selection-governance-for-ai-augmented-workflows.rules.json,architecture-decision,,,none,no,documentation-only diff --git a/src/rulesets/standards/iso-5055-mapping.json b/src/rulesets/standards/iso-5055-mapping.json index 3efd2456..f61b9507 100644 --- a/src/rulesets/standards/iso-5055-mapping.json +++ b/src/rulesets/standards/iso-5055-mapping.json @@ -12,24 +12,24 @@ "weaknessCount": 138 }, "corpus": { - "rulesetFiles": 167, - "rules": 381, + "rulesetFiles": 174, + "rules": 388, "note": "Files that carry gate definitions or topology recommendations rather than conformance rules contribute no rows: architecture/topology-recommendation.rules.json and sdlc/phase-gates.rules.json." }, "summary": { - "rules": 381, + "rules": 388, "mappedToIso5055": 37, "mappedDirect": 8, "mappedPartial": 29, - "noInternationalEquivalent": 344, - "adoptedFraction": 0.0971, + "noInternationalEquivalent": 351, + "adoptedFraction": 0.0954, "analyserAdoptable": 42, "analyserAdoptablePartial": 23, - "analyserAdoptableFraction": 0.1102, - "analyserAdoptableFractionIncludingPartial": 0.1706, + "analyserAdoptableFraction": 0.1082, + "analyserAdoptableFractionIncludingPartial": 0.1675, "byClass": { "architecture-decision": { - "rules": 155, + "rules": 162, "mapped": 12, "adoptable": 6 }, @@ -83,10 +83,11 @@ "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 (corpus loader and pinned counts)" + "src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts (the pinned counts, asserted against this file)" ], "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": 60, @@ -144,7 +145,7 @@ "adoptablePartialRuleIds": [] }, "documentation-only": { - "rules": 129, + "rules": 136, "mappedToIso5055": 9, "analyserAdoptable": 6, "analyserAdoptablePartial": 7, @@ -3185,6 +3186,139 @@ "nativeEvaluability": "documentation-only", "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." }, + { + "ruleId": "CORE-0118-01", + "sourceFile": "adr/generated/adr-0118-the-core-hub-has-its-own-root-taxonomy-distinct-from-satelli.rules.json", + "title": "Conform to ADR-0118: The Core Hub Has Its Own Root Taxonomy, Distinct From Satellites", + "severity": "MUST", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0119-01", + "sourceFile": "adr/generated/adr-0119-api-security-configuration-hardening.rules.json", + "title": "Conform to ADR-0119: API Security Configuration Hardening", + "severity": "MUST", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0120-01", + "sourceFile": "adr/generated/adr-0120-ssrf-prevention-standard.rules.json", + "title": "Honor design decision in ADR-0120: SSRF Prevention Standard", + "severity": "SHOULD", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0121-01", + "sourceFile": "adr/generated/adr-0121-input-validation-and-sanitization-standard.rules.json", + "title": "Honor design decision in ADR-0121: Input Validation and Sanitization Standard", + "severity": "SHOULD", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0122-01", + "sourceFile": "adr/generated/adr-0122-shell-execution-safety-standard.rules.json", + "title": "Conform to ADR-0122: Shell Execution Safety Standard", + "severity": "MUST", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0123-01", + "sourceFile": "adr/generated/adr-0123-timing-safe-comparison-standard.rules.json", + "title": "Conform to ADR-0123: Timing-Safe Comparison Standard", + "severity": "MUST", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, + { + "ruleId": "CORE-0124-01", + "sourceFile": "adr/generated/adr-0124-credential-and-secret-management-standard.rules.json", + "title": "Conform to ADR-0124: Credential and Secret Management Standard", + "severity": "MUST", + "ruleClass": "architecture-decision", + "iso5055": { + "cwes": [], + "weaknesses": [], + "measures": [], + "strength": "none" + }, + "analyser": { + "adoptable": "no", + "examples": [] + }, + "nativeEvaluability": "documentation-only", + "note": "Conformance to a recorded architecture decision. ISO/IEC 5055 measures source structure, not whether a decision was honoured." + }, { "ruleId": "AI-0001-01", "sourceFile": "adr/generated/adr-ai-augmented-0001-harness-engineering-for-ai-augmented-development.rules.json", diff --git a/src/rulesets/standards/iso-5055-mapping.test.mjs b/src/rulesets/standards/iso-5055-mapping.test.mjs index 0d74f98a..7edc334b 100644 --- a/src/rulesets/standards/iso-5055-mapping.test.mjs +++ b/src/rulesets/standards/iso-5055-mapping.test.mjs @@ -7,13 +7,21 @@ * * These assertions are the reason the mapping is worth anything. A mapping table * that silently stops covering the corpus is worse than no table: it reports a - * shrinking denominator as progress. Four things are pinned here — + * shrinking denominator as progress. Six things are pinned here — * * 1. the weakness index really is the 138 of ISO/IEC 5055, per measure; * 2. every CWE the mapping cites is one of those 138; * 3. every rule in the corpus has a row (add a rule, this fails); - * 4. nothing in this directory quotes the retired 2011 eight-characteristic + * 4. the evaluability snapshot still agrees with the counts Core pins, read + * OUT OF CORE rather than retyped here (see `pinnedByCore`); + * 5. the snapshot's counts, its per-rule classes and the classes stamped into + * the mapping are three views of one capture, and they agree; + * 6. nothing in this directory quotes the retired 2011 eight-characteristic * ISO/IEC 25010 model. + * + * This job runs without node_modules, so nothing here recomputes the triage. The + * recomputing guard is `rule-corpus-triage.spec.ts` in core-domain, which + * compares the committed snapshot against a fresh classification of the corpus. */ import test from 'node:test'; @@ -103,18 +111,115 @@ test('the handler backlog is scoped to the unimplemented-native class, not to th assert.equal(backlog.byEvaluabilityClass['unimplemented-native'].adoptableRuleIds.length, backlog.adoptableFromAnalyser); }); +/** + * Read the class counts Core pins, from Core. + * + * This used to be six numbers typed into this file — the same six that are + * literals inside the snapshot it was checking. It compared the snapshot to + * itself, so it passed while `documentation-only` sat at 129 and Core had long + * since moved to 136, and it passed again when twelve rules got handlers and the + * snapshot went on calling them `unimplemented-native`. A guard whose expected + * value is a copy of its actual value is not a guard. + * + * This job has no node_modules, so the counts cannot be recomputed here — they + * are read out of the spec that pins them. Parsing a source file is a real + * coupling and it is stated rather than hidden: if the const is renamed or + * reshaped, this THROWS. Failing to find the source of truth must never read as + * agreement with it. + * + * The recomputation-based check lives where the dependencies do: the same spec + * asserts the committed snapshot against a freshly computed triage, and + * `capture-native-evaluability-snapshot.mjs --check` re-derives it end to end. + */ +function pinnedByCore() { + const spec = path.resolve( + RULESETS, + '..', + 'packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts', + ); + if (!fs.existsSync(spec)) { + throw new Error(`Core's pinned counts are unreadable: ${spec} does not exist. The snapshot cannot be verified.`); + } + + const source = fs.readFileSync(spec, 'utf8'); + const literal = /const\s+PINNED_CLASS_COUNTS\s*:[^=]*=\s*\{([^}]*)\}\s*;/.exec(source); + if (!literal) { + throw new Error( + `Could not find the PINNED_CLASS_COUNTS literal in ${path.basename(spec)}. ` + + 'It was renamed or reshaped — update this parser rather than deleting the check.', + ); + } + + const counts = {}; + for (const line of literal[1].split('\n')) { + const entry = /^\s*'?([a-zA-Z-]+)'?\s*:\s*(\d+)\s*,?\s*$/.exec(line); + if (entry) counts[entry[1]] = Number(entry[2]); + } + if (Object.keys(counts).length === 0) { + throw new Error(`Parsed the PINNED_CLASS_COUNTS block in ${path.basename(spec)} and read zero counts out of it.`); + } + return counts; +} + test('the evaluability snapshot still matches the class counts pinned by Core', () => { - // These six numbers are pinned in rule-corpus-triage.spec.ts. If Core moves - // them, this snapshot is stale and the backlog arithmetic above is wrong. - assert.deepEqual(evaluability.counts, { - 'native-handler': 139, - 'documentation-only': 129, - 'unimplemented-native': 60, - 'needs-external-system': 20, - 'needs-runtime': 17, - underspecified: 14, - }); - assert.equal(Object.keys(evaluability.classes).length, 379); + assert.deepEqual( + evaluability.counts, + pinnedByCore(), + 'native-evaluability-snapshot.json disagrees with the counts pinned in rule-corpus-triage.spec.ts. ' + + 'Recapture it (node src/rulesets/standards/capture-native-evaluability-snapshot.mjs), THEN rebuild the ' + + 'mapping (node src/rulesets/standards/build-iso-5055-mapping.mjs) — the generator stamps ' + + 'nativeEvaluability per rule from the snapshot, so rebuilding first launders the stale class into the mapping.', + ); +}); + +test('the snapshot is internally consistent — its header describes its own body', () => { + // The counts and the per-rule classes are two views of one capture, and the + // mapping is stamped from the SECOND one. A header that agrees with Core while + // the body has drifted would put the wrong class on every affected row. + const summed = Object.values(evaluability.counts).reduce((a, b) => a + b, 0); + assert.equal(summed, evaluability.corpusSize, 'the class counts do not sum to the corpus the capture measured'); + assert.equal(Object.keys(evaluability.classes).length, evaluability.distinctRuleIds); + assert.ok( + evaluability.distinctRuleIds <= evaluability.corpusSize, + 'more distinct rule ids than rules classified — the capture is not a capture', + ); + + const tally = {}; + for (const klass of Object.values(evaluability.classes)) tally[klass] = (tally[klass] ?? 0) + 1; + + if (evaluability.distinctRuleIds === evaluability.corpusSize) { + // The usual case: one row per rule id, so the two views must agree exactly. + assert.deepEqual(tally, evaluability.counts, 'the per-rule classes do not add up to the stated counts'); + } else { + // A rule id carried by two ruleset files collapses to one key here, so the + // tally is a lower bound rather than an equality. + for (const [klass, n] of Object.entries(tally)) { + assert.ok(n <= evaluability.counts[klass], `${klass}: ${n} classes recorded but only ${evaluability.counts[klass]} counted`); + } + } +}); + +test('every rule the mapping stamps a class onto is a rule the snapshot actually classified', () => { + // `not-in-snapshot` is a legitimate value for the two single-rule + // infrastructure files whose rule metadata sits at the document root, which + // the Core corpus loader does not read. It is NOT a legitimate value for + // anything else: when the snapshot falls behind the corpus, rows quietly + // acquire it and drop out of the backlog. So the exception is enumerated. + const orphans = mapping.rules.filter((r) => r.nativeEvaluability === 'not-in-snapshot').map((r) => r.ruleId).sort(); + assert.deepEqual( + orphans, + ['INFRA-001', 'INFRA-OPA-001'], + `mapping rows carry no class from the snapshot: ${orphans.join(', ')}. Either the snapshot is stale ` + + '(recapture it) or the corpus grew another rule shape Core does not load (then say so here).', + ); + for (const r of mapping.rules) { + if (r.nativeEvaluability === 'not-in-snapshot') continue; + assert.equal( + r.nativeEvaluability, + evaluability.classes[r.ruleId], + `${r.ruleId} is stamped ${r.nativeEvaluability} in the mapping but ${evaluability.classes[r.ruleId]} in the snapshot`, + ); + } }); test('no artifact here quotes the retired 2011 eight-characteristic ISO/IEC 25010 model', () => { diff --git a/src/rulesets/standards/native-evaluability-snapshot.json b/src/rulesets/standards/native-evaluability-snapshot.json index 6f9fd28c..15af081f 100644 --- a/src/rulesets/standards/native-evaluability-snapshot.json +++ b/src/rulesets/standards/native-evaluability-snapshot.json @@ -2,21 +2,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.", - "version": "1.0.0", - "capturedOn": "2026-07-28", + "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 (corpus loader and pinned counts)" + "src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts (the pinned counts, asserted against this file)" ], - "validation": "The capture reproduces every class count pinned by rule-corpus-triage.spec.ts exactly (corpus 379; native-handler 139, documentation-only 129, unimplemented-native 60, needs-external-system 20, needs-runtime 17, underspecified 14). If those pinned numbers change, this snapshot is stale and must be recaptured.", + "validation": "Captured from a live run of the Core triage over 386 rules (native-handler 139, unimplemented-native 60, 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).", + "corpusSize": 386, + "distinctRuleIds": 386, "counts": { "native-handler": 139, "unimplemented-native": 60, "needs-runtime": 17, "needs-external-system": 20, - "documentation-only": 129, + "documentation-only": 136, "underspecified": 14 }, "classes": { @@ -167,6 +171,13 @@ "CORE-0115-01": "documentation-only", "CORE-0116-01": "documentation-only", "CORE-0117-01": "documentation-only", + "CORE-0118-01": "documentation-only", + "CORE-0119-01": "documentation-only", + "CORE-0120-01": "documentation-only", + "CORE-0121-01": "documentation-only", + "CORE-0122-01": "documentation-only", + "CORE-0123-01": "documentation-only", + "CORE-0124-01": "documentation-only", "AI-0001-01": "documentation-only", "AI-0002-01": "documentation-only", "AI-0003-01": "documentation-only", From 02881b12b9fa727eabe7f9ec70a377faf30a472f Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:12:01 -0500 Subject: [PATCH 02/10] fix(git): ignore node_modules as a SYMLINK, not only as a directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `node_modules/` — with the trailing slash — matches directories only. A fresh worktree has no node_modules of its own, and the documented workaround is to symlink the main checkout's; a symlink is not a directory, so the ignore did not apply to it. The result was a link showing as untracked, and `git add -A` staging a path that is absolute and valid on exactly one machine. That happened in this session and was caught by reading the staged set, which is not a control. Dropping the slash covers both shapes. No tracked path contains `node_modules`, so nothing is un-tracked by the wider match. Co-Authored-By: Claude Opus 5 --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 453187c0..2030016f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,12 @@ bower_components build/Release # Dependency directories -node_modules/ +# No trailing slash on purpose: `node_modules/` matches directories ONLY, and a +# fresh worktree has no node_modules of its own — the documented workaround is to +# symlink the main checkout's. A symlink is not a directory, so the slashed form +# left it untracked-but-visible, and `git add -A` staged a link to an absolute +# path on one machine. +node_modules .harness/bin/ jspm_packages/ From 892b16feafff58ab366389439e372a1294a7c92a Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:26:24 -0500 Subject: [PATCH 03/10] fix(standards): resolve ts-node through a package that declares it, not through hoisting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capture runs Core's triage through `node -r ts-node/register` with cwd at the repository root. `ts-node` is declared by neither the root nor core-domain — it is a devDependency of core-api and mcp-server, and it only sits in the root `node_modules` because npm workspaces hoist it. Hoisting is an optimisation, not a contract: a version conflict makes npm install it under the declaring package instead, and a root-relative resolution then fails on a clean runner and nowhere else. That is the failure shape this repository keeps paying for — green locally, red on CI, because local state was doing work the declaration did not. Resolve it with `createRequire` from the manifests that actually declare it, root first, and state the failure when none of them can. The neighbouring ABAC generator never had this exposure: it runs with cwd inside mcp-server, where ts-node is declared. Verified: all three manifests resolve today, `--check` still reports a faithful capture of 386 rules, mapping guard 9/9. Co-Authored-By: Claude Opus 5 --- .../capture-native-evaluability-snapshot.mjs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/rulesets/standards/capture-native-evaluability-snapshot.mjs b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs index f15a7d05..2de28146 100644 --- a/src/rulesets/standards/capture-native-evaluability-snapshot.mjs +++ b/src/rulesets/standards/capture-native-evaluability-snapshot.mjs @@ -44,6 +44,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; import { execFileSync } from 'node:child_process'; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -70,6 +71,42 @@ const KNOWN_CLASSES = [ */ const MIN_PLAUSIBLE_CORPUS = 300; +/** + * Packages that DECLARE ts-node, most-likely location first. + * + * `ts-node` is a devDependency of core-api and mcp-server, not of the root and + * not of core-domain. It normally lands in the root `node_modules` because npm + * workspaces hoist it, and resolving it from the repo root works — until a + * version conflict makes npm install it under the declaring package instead, at + * which point a root-relative `-r ts-node/register` fails on a clean runner and + * nowhere else. Resolving through the packages that actually declare it removes + * the dependency on hoisting. + */ +const TS_NODE_DECLARED_BY = [ + 'package.json', + 'src/packages/mcp-server/package.json', + 'src/apps/core-api/package.json', +]; + +/** Absolute path to ts-node's CommonJS register hook, or a stated failure. */ +function resolveTsNodeRegister() { + const tried = []; + for (const manifest of TS_NODE_DECLARED_BY) { + const from = path.join(REPO_ROOT, manifest); + if (!fs.existsSync(from)) continue; + try { + return createRequire(from).resolve('ts-node/register'); + } catch { + tried.push(manifest); + } + } + throw new Error( + 'ts-node is not resolvable, so Core\'s triage cannot be executed and the snapshot cannot be captured.\n' + + ` looked from: ${tried.join(', ') || '(no manifest found)'}\n` + + ' Install the workspace first: npm ci', + ); +} + // --------------------------------------------------------------------------- // 1. Run the real triage // --------------------------------------------------------------------------- @@ -101,9 +138,11 @@ function runTriage() { })); `; + const register = resolveTsNodeRegister(); + let raw; try { - raw = execFileSync(process.execPath, ['-r', 'ts-node/register', '-e', program], { + raw = execFileSync(process.execPath, ['-r', register, '-e', program], { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, From 756c0f02037aee4f2193495ce5e8d2accc7a9e17 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:33:42 -0500 Subject: [PATCH 04/10] =?UTF-8?q?docs(gaps):=20register=20GT-633=20?= =?UTF-8?q?=E2=80=94=20a=20guard=20whose=20expected=20value=20was=20a=20co?= =?UTF-8?q?py=20of=20its=20actual=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-on defect from GT-598. The interesting content is the FAILURE MODE, not the fix. `native-evaluability-snapshot.json` said in its own header that it was a capture. Nothing captured it. It was hand-maintained and it drifted — `documentation-only: 129` while Core pinned 136 — and the guard over it, in `iso-5055-mapping.test.mjs`, compared the snapshot's six class counts against six numbers typed into the test: the same six literals the snapshot already contained. Expected value and actual value were copies of each other, so it passed for as long as the file was unchanged, whatever Core actually pinned. A guard like that is worse than none, because the row it protects reads as verified. The second class is why the first was expensive. `build-iso-5055-mapping.mjs` stamps `nativeEvaluability` onto EVERY row of the ISO/IEC 5055 mapping from that snapshot — 388 rows after the recapture, 381 before — so one stale class is laundered into a derived artifact five times the size of its input, and overstates the handler backlog that is GT-598's entire deliverable. Registered EN + ES on the board and in the catalog, status DONE, counters recomputed 592/632 -> 593/633. The closure record names commit 78e9b092 and states plainly that the fix is not in this branch: the capture script and `test/rule-corpus-triage.ts` exist only there, so they are not cited as evidence paths. Verified: 08-validate-tracking (633 gaps, 609/609 EN/ES sections, 575 closure records), 04-check-bilingual-parity, 01-validate-docs (1491 files), 40, 42, 43 (36/36 guards observed red). Derived artifacts regenerated in GT-630 order: reconcile, then summary. Co-Authored-By: Claude Opus 5 --- .../evidence/gap-closure-evidence.json | 25 +++++++++++++++++++ .../gaps/gap-reference-catalog.es.md | 13 ++++++++++ .../gaps/gap-reference-catalog.md | 13 ++++++++++ .../control-center/gaps/gap-tracking.es.md | 3 ++- .../core/control-center/gaps/gap-tracking.md | 3 ++- .../maturity-reports/executive-summary.es.md | 6 ++--- .../maturity-reports/executive-summary.md | 6 ++--- .../maturity-reconciliation.json | 6 ++--- 8 files changed, 64 insertions(+), 11 deletions(-) diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index dd0560ea..c1e2b689 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -8828,6 +8828,31 @@ ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "TWO deliberate non-adoptions are recorded as part of the closure, not deferred silently. (1) dependency-cruiser was NOT adopted for the module-boundary rules: the Core is a stateless engine that reads satellites through an overlay filesystem a subprocess cannot see, and the tool's config matches node_modules/, so an UNINSTALLED satellite would yield zero violations and PASS four blocking rules. A false pass is worse than the skip it would replace. (2) OCB-02 was NOT closed: no artifact anywhere carries an `availability` marker, so the rule quantifies over the empty set and can only pass; that is recorded as three executable tests instead of prose. The residual unclaimed-blocking corpus stands at 73 and is pinned." + }, + { + "id": "GT-633", + "closedAt": "2026-07-29", + "closureCommit": "78e9b092", + "evidence": [ + "src/rulesets/standards/native-evaluability-snapshot.json", + "src/rulesets/standards/iso-5055-mapping.test.mjs", + "src/rulesets/standards/build-iso-5055-mapping.mjs", + ".harness/scripts/ci/46-validate-derived-artifact-order.mjs", + ".harness/scripts/ci/46-validate-derived-artifact-order.test.mjs", + ".github/workflows/docs.yml", + "reference/core/control-center/gaps/gap-reference-catalog.md" + ], + "validationCommands": [ + "THE DEFECT, stated as the failure class: the guard's expected value was a copy of its actual value. iso-5055-mapping.test.mjs asserted the snapshot's six class counts against six numbers typed into the test -- the same six literals native-evaluability-snapshot.json already contained -- so it passed for as long as the file was unchanged, whatever Core actually pinned. It reported an agreement it never checked.", + "SECOND failure class, and the reason the first one was expensive: build-iso-5055-mapping.mjs stamps `nativeEvaluability` onto EVERY row of the ISO/IEC 5055 mapping from that snapshot (388 rows after the recapture, 381 before), so a single stale class is laundered into a derived artifact five times the size of its input and overstates the GT-598 handler backlog.", + "jest rule-corpus-triage.spec.ts -> the committed snapshot is asserted against a FRESHLY COMPUTED triage, counts and per-rule class, in both directions. This is the real guard: the only place with the dependencies to recompute the truth. Negative-tested by reverting documentation-only to 129, which goes red.", + "node --test src/rulesets/standards/iso-5055-mapping.test.mjs -> runs in the documentation job, which has no node_modules, so it reads Core's pinned counts OUT of the spec that pins them instead of retyping them; a missing or reshaped literal THROWS, because failing to find the source of truth must never read as agreement with it. Negative-tested by building the mapping from a stale snapshot, which goes red on the stamp assertion.", + "node src/rulesets/standards/capture-native-evaluability-snapshot.mjs --check -> the end-to-end re-derivation; runs Core's real triage through ts-node rather than reimplementing the classification, since a second implementation here would be the second source of truth the fix removes. Recaptured: documentation-only 129 -> 136, corpus 379 -> 386, mapping 381 -> 388 rows, backlog unchanged at 60.", + "node .harness/scripts/ci/46-validate-derived-artifact-order.mjs -> the recapture->rebuild order is declared as DATA, not prose: the two standards links sit between the ABAC link and the maturity links, so rebuilding the mapping before recapturing the snapshot is now a guard failure rather than tribal knowledge (GT-630).", + "node --test .harness/scripts/ci/46-validate-derived-artifact-order.test.mjs -> 8/8 with the chain at 5 links / 7 artifacts, including the byte-identical-restore test extended to cover the three new standards artifacts." + ], + "dependencyDisposition": "accepted-scope", + "dependencyRationale": "The fix itself is commit 78e9b092 and is NOT in this branch: capture-native-evaluability-snapshot.mjs and src/packages/core-domain/test/rule-corpus-triage.ts exist only there, which is why they are not listed as evidence paths -- an evidence path that does not resolve is a hard error, and citing them here would be a claim this checkout cannot back. What lands here is the registration plus the two chain links the originating session was blocked from writing (S-16). CONSEQUENCE, stated rather than discovered in CI: until 78e9b092 lands, 46-validate-derived-artifact-order fails with `declared producer does not exist`, exactly as designed. This commit must merge with or after it." } ] } diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 3632e23b..ee948e73 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7855,6 +7855,19 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - [x] El tipo `security` está declarado en la config de commitlint con un mapeo explícito de salto de versión, o se deja de usar. - [x] Ningún hook de `.husky/` imprime un mensaje de "skipping" y sale con cero — un hook que no puede correr se borra, no se silencia. +#### GT-633 + +**Title:** Un guard cuyo valor esperado era una copia de su valor real, sobre una entrada que se blanquea en un artefacto cinco veces mayor + +- **Purpose:** Eliminar dos clases de fallo que trascienden este archivo — un guard que lee su expectativa del artefacto que vigila, y un generador que estampa un campo mantenido a mano sobre todo lo que va detrás de él. +- **Evidence:** **El guard comparaba el snapshot contra seis números copiados del propio snapshot, así que solo podía pasar — y el archivo que no estaba vigilando se estampa en 388 filas de un artefacto mayor.** `native-evaluability-snapshot.json` declaraba en su propia cabecera que era "un SNAPSHOT CAPTURADO, no la fuente de verdad". **No existía ningún script de captura.** Se mantenía a mano y derivó: fijaba `documentation-only: 129` mientras Core fijaba 136, y seguía llamando `unimplemented-native` a reglas que ya tenían handler. Su guard en `iso-5055-mapping.test.mjs` asertaba los seis conteos de clase del snapshot contra seis números escritos a mano en el test — los mismos seis literales que el snapshot ya contenía. **El valor esperado y el valor real eran copias el uno del otro**, así que la aserción se sostenía mientras el archivo no cambiara, fijara Core lo que fijara; informaba de una concordancia que nunca comprobó, lo cual es peor que no tener guard, porque la fila que protege se lee como verificada. **La deriva no se queda donde empieza.** `build-iso-5055-mapping.mjs` estampa `nativeEvaluability` en TODAS las filas del mapeo ISO/IEC 5055 desde ese archivo — 388 tras la recaptura, 381 antes — así que una sola clase rancia se blanquea en un artefacto derivado cinco veces mayor que su entrada, y sobredimensiona el backlog de handlers que es el entregable entero de [`GT-598`](./gap-reference-catalog.es.md#gt-598) en tantas reglas como Core haya cerrado desde entonces. El orden que marca la diferencia (recapturar y DESPUÉS reconstruir) no estaba escrito en ningún sitio, y el guard del propio mapeo no corría en **ningún workflow**. **Corregido el 2026-07-29** (rama `claude/fervent-cohen-08ea01`): `capture-native-evaluability-snapshot.mjs` ejecuta el triage REAL de Core vía `ts-node` en vez de reimplementar la clasificación — una segunda implementación aquí sería una segunda fuente de verdad, que es justo el defecto que se está quitando — y para que el triage fuera alcanzable fuera de jest se movió del spec a `src/packages/core-domain/test/rule-corpus-triage.ts`; esa inalcanzabilidad es precisamente la razón de que el snapshot se mantuviera a mano. Tres comprobaciones sustituyen a la tautología, cada una en el job que puede permitírsela: `rule-corpus-triage.spec.ts` asserta el snapshot commiteado contra un triage RECIÉN CALCULADO, en ambas direcciones; el guard del job de documentación, que no tiene `node_modules`, lee los conteos que Core fija LEYENDO el spec que los fija y **lanza excepción** cuando el literal falta o cambia de forma, de modo que no encontrar la fuente de verdad jamás pueda leerse como acuerdo con ella; y `--check` es la re-derivación de extremo a extremo. Recapturado: `documentation-only` 129 → 136, corpus 379 → 386, mapeo 381 → 388 filas, backlog sin cambios en 60. Ambos pasos son ya eslabones de la cadena de artefactos derivados de [`GT-630`](./gap-reference-catalog.es.md#gt-630), que exige el orden recaptura→reconstrucción como datos y no como prosa. +- **Component:** `Governance` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Defecto derivado de [`GT-598`](./gap-reference-catalog.es.md#gt-598), encontrado el 2026-07-29 al extender su mapeo: el snapshot sobre el que GT-598 se construyó no lo capturaba nada, y el guard que lo cubría era tautológico. Se registra aparte porque el hallazgo es una clase de diseño de guards, no una corrección al mapeo de GT-598. +- **Acceptance criteria:** + - [x] El snapshot lo produce un script que ejecuta el triage propio de Core en lugar de mantenerse a mano, y `capture-native-evaluability-snapshot.mjs --check` lo re-deriva de extremo a extremo. + - [x] Ningún guard sobre el snapshot toma su valor esperado del snapshot: uno recalcula el triage y el otro lee los conteos de Core del spec que los fija, fallando ruidosamente cuando no los encuentra. + - [x] El orden recaptura→reconstrucción se exige como datos — ambos eslabones están declarados en la cadena de `46-validate-derived-artifact-order` — y el guard del mapeo corre en un workflow, cuando no corría en ninguno. + #### GT-632 **Title:** La política ABAC compilada se resuelve en una ruta previa al refactor, denegando toda herramienta MCP en producción diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 61aa8735..fe93898d 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7950,6 +7950,19 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - [x] The `security` type is either declared in the commitlint config with an explicit version-bump mapping, or removed from use. - [x] No hook in `.husky/` prints a "skipping" message and exits zero — a hook that cannot run is deleted, not silenced. +#### GT-633 + +**Title:** A guard whose expected value was a copy of its actual value, over an input laundered into an artifact five times its size + +- **Purpose:** Remove two failure classes that generalise well beyond this file — a guard that reads its expectation from the artifact it is guarding, and a generator that stamps a hand-maintained field onto everything downstream of it. +- **Evidence:** **The guard compared the snapshot against six numbers copied out of the snapshot, so it could only ever pass — and the file it was failing to guard is stamped onto 388 rows of a larger artifact.** `native-evaluability-snapshot.json` declared in its own header that it was "a CAPTURED SNAPSHOT, not the source of truth". **No capture script existed.** It was hand-maintained and it drifted: it pinned `documentation-only: 129` while Core pinned 136, and went on calling rules `unimplemented-native` after they had handlers. Its guard in `iso-5055-mapping.test.mjs` asserted the snapshot's six class counts against six numbers typed into the test — the same six literals the snapshot already contained. **Expected value and actual value were copies of each other**, so the assertion held for as long as the file was unchanged, whatever Core actually pinned; it reported an agreement it never checked, which is worse than no guard, because the row it protects reads as verified. **The drift does not stay where it starts.** `build-iso-5055-mapping.mjs` stamps `nativeEvaluability` onto EVERY row of the ISO/IEC 5055 mapping from that file — 388 rows after the recapture, 381 before — so one stale class is laundered into a derived artifact five times the size of its input, and overstates the handler backlog that is [`GT-598`](./gap-reference-catalog.md#gt-598)'s entire deliverable by however many rules Core has since closed. The order that makes the difference (recapture, THEN rebuild) was written down nowhere, and the mapping's own guard ran in **no workflow at all**. **Fixed on 2026-07-29** (branch `claude/fervent-cohen-08ea01`): `capture-native-evaluability-snapshot.mjs` runs Core's REAL triage through `ts-node` rather than reimplementing the classification — a second implementation here would be a second source of truth, which is the defect being removed — and to make the triage reachable outside jest it moved from the spec into `src/packages/core-domain/test/rule-corpus-triage.ts`; that unreachability is precisely why the snapshot was maintained by hand in the first place. Three checks replace the tautology, each in the job that can afford it: `rule-corpus-triage.spec.ts` asserts the committed snapshot against a FRESHLY COMPUTED triage in both directions; the documentation-job guard, which has no `node_modules`, reads Core's pinned counts OUT of the spec that pins them and **throws** when the literal is missing or reshaped, so failing to find the source of truth can never read as agreement with it; and `--check` is the end-to-end re-derivation. Recaptured: `documentation-only` 129 → 136, corpus 379 → 386, mapping 381 → 388 rows, backlog unchanged at 60. Both steps are now links in the [`GT-630`](./gap-reference-catalog.md#gt-630) derived-artifact chain, which enforces the recapture→rebuild order as data instead of prose. +- **Component:** `Governance` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Follow-on defect from [`GT-598`](./gap-reference-catalog.md#gt-598), found on 2026-07-29 while extending its mapping: the snapshot GT-598 built on was never captured by anything, and the guard over it was tautological. Registered separately because the finding is a guard-design class, not a correction to GT-598's mapping. +- **Acceptance criteria:** + - [x] The snapshot is produced by a script that runs Core's own triage instead of by hand, and `capture-native-evaluability-snapshot.mjs --check` re-derives it end to end. + - [x] No guard over the snapshot takes its expected value from the snapshot: one recomputes the triage, the other reads Core's counts out of the spec that pins them and fails loudly when it cannot find them. + - [x] The recapture→rebuild order is enforced as data — both links are declared in the `46-validate-derived-artifact-order` chain — and the mapping guard runs in a workflow, having run in none. + #### GT-632 **Title:** The compiled ABAC policy is resolved at a pre-refactor path, denying every MCP tool in production diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 630ff992..5f711e0a 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -13,6 +13,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | ID | Gap | Qué significa | Ejemplo | Componente | Fase | Criticidad | Complejidad | Estado | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-633`](./gap-reference-catalog.es.md#gt-633) | **El guard comparaba el snapshot contra seis números copiados del propio snapshot, así que solo podía pasar — y el archivo que no estaba vigilando se estampa en 388 filas de un artefacto mayor.** `native-evaluability-snapshot.json` declaraba en su propia cabecera que era "un SNAPSHOT CAPTURADO, no la fuente de verdad". **No existía ningún script de captura.** Se mantenía a mano y derivó: fijaba `documentation-only: 129` mientras Core fijaba 136, y seguía llamando `unimplemented-native` a reglas que ya tenían handler. Su guard en `iso-5055-mapping.test.mjs` asertaba los seis conteos de clase del snapshot contra seis números escritos a mano en el test — los mismos seis literales que el snapshot ya contenía. **El valor esperado y el valor real eran copias el uno del otro**, así que la aserción se sostenía mientras el archivo no cambiara, fijara Core lo que fijara; informaba de una concordancia que nunca comprobó, lo cual es peor que no tener guard, porque la fila que protege se lee como verificada. **La deriva no se queda donde empieza.** `build-iso-5055-mapping.mjs` estampa `nativeEvaluability` en TODAS las filas del mapeo ISO/IEC 5055 desde ese archivo — 388 tras la recaptura, 381 antes — así que una sola clase rancia se blanquea en un artefacto derivado cinco veces mayor que su entrada, y sobredimensiona el backlog de handlers que es el entregable entero de [`GT-598`](./gap-reference-catalog.es.md#gt-598). El orden recaptura→reconstrucción no estaba escrito en ningún sitio, y el guard del propio mapeo no corría en **ningún workflow**. **CORREGIDO el 2026-07-29:** un script de captura que ejecuta el triage REAL de Core vía `ts-node` en vez de reimplementar la clasificación, la medición extraída del spec de jest a `test/rule-corpus-triage.ts` para que un script pueda alcanzarla — esa inalcanzabilidad es la razón de que el archivo se mantuviera a mano —, aserciones snapshot-contra-triage-fresco en ambas direcciones, un guard del job de documentación que lee los conteos que Core fija LEYENDO el spec y **lanza excepción** cuando el literal falta o cambia de forma, y ambos pasos declarados como eslabones de la cadena de artefactos derivados de [`GT-630`](./gap-reference-catalog.es.md#gt-630). Recapturado: `documentation-only` 129 → 136, corpus 379 → 386, mapeo 381 → 388 filas, backlog sin cambios en 60. | | | `Governance` | Cross | P1 | S | `COMPLETADO` | | [`GT-632`](./gap-reference-catalog.es.md#gt-632) | **El MCP denegaba todas sus herramientas en producción por una ruta mal escrita, y la prueba de que funcionaba era estado local sin versionar.** `abac-evaluator.ts:322` resolvía la política compilada en `/sdk/cli/rulesets/opa/policy.wasm` — la ruta ANTERIOR al refactor `src/`. El fichero vive en `src/sdk/cli/…`. Cuando el stat falla, el evaluador deniega fail-closed en producción, así que en un checkout limpio con `NODE_ENV=production` la política compilada nunca se carga y TODA herramienta MCP se rechaza. Parecía verde porque `policy.wasm` está gitignorado: un fichero rancio dejado en la ruta vieja por una época anterior hacía que la carga funcionara en local, y el test que lo cubre pasaba contra un artefacto que ningún checkout fresco tendría. Misma forma que `GT-625` — un verde que dependía de estado no versionado — pero en el camino de autorización. Encontrado el 2026-07-29 investigando por qué `abac-rego-parity.spec.ts` se había puesto rojo; el rego era correcto (`opa eval` devuelve `allow: true` para un arquitecto en producción). **`40-validate-path-literals` no puede ver esta clase:** barre literales de cadena, y esto es una construcción `path.join(corePath, 'sdk', 'cli', …)`. Sobreviven doce joins así en `src/**` — los otros once están en `opa-input-builder.ts` y `cli-release-rule.handler.ts`, donde una ruta equivocada produce un falso NEGATIVO en un evaluador en vez de una denegación, que es más silencioso y no menos incorrecto. **CORREGIDO para el camino de autorización el 2026-07-29:** el evaluador prueba ahora ambas ubicaciones, la nueva primero — no es dejadez, es lo que mantiene funcionando una imagen ya desplegada mientras se corrige la ruta. Revertir el arreglo pone `abac-rego-parity.spec.ts` en rojo (1 de 11); con él, mcp-server va 434/434. Quedan los otros once joins y el punto ciego del guard. **Criterio 2 CERRADO el 2026-07-29 — y dos de las once estaban mal de una forma que el prefijo por sí solo no arreglaba.** Ocho eran supervivientes simples del movimiento a `src/` y admitieron el prefijo. Las otras tres eran peores: dos apuntaban a `sdk/cli/src/core/mcp/server.ts`, un fichero que salió del CLI por completo cuando mcp-server pasó a ser su propio paquete (hoy `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); y una exigía un `package-lock.json` por paquete, cosa que los WORKSPACES de npm impiden — hay exactamente un lockfile, en la raíz, así que esa comprobación solo podía ser falsa. Una regla que nadie puede satisfacer no es una regla más estricta. **Las specs y las fixtures de paridad también codificaban el layout viejo** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), y por eso llevaban tiempo en verde sobre rutas que no existen: la fixture y el código coincidían entre sí y ninguno coincidía con el repositorio. Ambas corregidas. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterio 3 CERRADO el 2026-07-29 — la primera corrida del guard estuvo casi toda equivocada, que es el hallazgo.** `47-validate-joined-paths` reportó 19 rutas rotas; solo 3 eran defectos. Cada una de las otras 16 habría empeorado el código: seis tenían base `root`, un PARÁMETRO que nombra el workspace del cliente, así que corregirlas rompería la evaluación de todos los satélites; nueve eran miembros de CADENAS DE FALLBACK correctas cuando uno de cuatro layouts resuelve, y la forma natural de callarlas es borrar los fallbacks, rompiendo imágenes desplegadas; una era una PROHIBICIÓN cuya ausencia ES el estado que aprueba, así que corregirla habría invertido la regla. Las cadenas se detectan ahora estructuralmente (hermanos de array, o las ramas de un ternario); las prohibiciones no pueden, y llevan razón escrita. Los tres defectos reales: `opa-evaluator.ts` resolvía `policy.wasm` y los esquemas de entrada sin el prefijo `src/`, misma clase fail-closed que el P0 de arriba; `satellite-upgrade-diff.ts` sondeaba `rulesets` detrás de un return temprano, reportando cero cambios de rulesets en cada upgrade de satélite; `artifact-path-resolver.ts` nombraba un `adr-matrix.json` que no existe en ningún layout. 11 fixtures negativas, los dos suelos anti-vacuos, y `43-validate-guard-negative-fixtures` lo OBSERVÓ en rojo sobre 36 guards. Una indulgencia conocida (dos joins como argumentos hermanos) queda anotada en su cabecera en vez de escondida. | | | `MCP Server` | Cross | P0 | M | `COMPLETADO` | | [`GT-631`](./gap-reference-catalog.es.md#gt-631) | **Extraído de [`GT-573`](./gap-reference-catalog.es.md#gt-573) para que su mitad cross-repo se siga en vez de absorberse en un cierre.** La mitad del Core del tercer criterio de GT-573 está hecha: `evaluation-context` y `evaluation-result` están fijados en `MACHINE_CONTRACT_SET` junto a `gate-evidence` y `output-envelope`, así que un cambio en cualquiera de esas formas ya rompe `10-validate-contract-conformance` en vez de llegar en silencio a los consumidores. Lo que queda está en otro repositorio y no se puede hacer desde aquí: el **Tracker** debe re-fijar esos dos esquemas en su propio `contracts/evolith-core-contracts.json` con su sha256, enlazar las tres fixtures publicadas (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) en un test de binding de DTOs, quitar o documentar `resolvedTopology` (que el resultado canónico nunca llevó), renombrar su `phase` de gate a `phaseId`, y eliminar su caída a `SKIPPED` para un payload que trae un `overallVerdict` no vacío. Hasta entonces, un cambio de forma del lado Core se caza aquí y sigue sorprendiendo al consumidor. | | | `Evolith Tracker` | Cross | P1 | S | `PENDIENTE` | | [`GT-630`](./gap-reference-catalog.es.md#gt-630) | **Los artefactos derivados tienen un ORDEN de dependencia y nada lo exigía — costó tres checks requeridos en rojo solo el 2026-07-28.** `generate-executive-summary.mjs` lee `maturity-reconciliation.json`, que a su vez se deriva del tablero de gaps. Generar el resumen ANTES de reconciliar captura un valor que el reconciliador está a punto de mover. El `--check` de cada artefacto pasa en ese momento — el resumen coincide de verdad con lo que se acaba de escribir — así que `ci-runner governance` sale verde en local y `Validate documentation` sale rojo en el runner, donde los pasos corren en el orden declarado. No es un fallo de artefacto rancio, que los `--check` individuales ya cazan; es un fallo de ORDEN, invisible para cualquier comprobación que mire un artefacto cada vez. La secuencia correcta (reconciliar → suite → generar → validar) no estaba escrita en ningún sitio del repositorio, que es la razón de que el mismo error se cometiera tres veces en una sesión por alguien a quien ya le había mordido dos. **CERRADO al registrarse.** La cadena es ahora DATOS en `46-validate-derived-artifact-order.mjs` — productor, qué consume, qué escribe — recorrida en orden de dependencia y cableada en `Validate documentation`. Informa del PRIMER eslabón rancio con el comando que lo arregla y las entradas que deben estar al día antes que él, porque un artefacto construido sobre entrada rancia no está mal por sí mismo. Escribir los tests acotó cuánto vale la pasada de punto fijo, y ese límite queda recogido en el guard: con generadores deterministas, la comprobación de orden ya la subsume; se gana su sitio ante un generador cuya salida no depende solo de sus entradas declaradas, que es el caso que ejercita el autotest. 8 autotests, cuatro de ellos anti-vacuos. Registrado en `guard-classification.mjs`; 58 guards clasificados, 35 vistos fallar. | | | `Governance` | Cross | P2 | S | `COMPLETADO` | @@ -647,7 +648,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-246`](./gap-reference-catalog.es.md#gt-246) | Implementar experimentos Chaos Mesh/Litmus | | | `QA` | Cross | P3 | L | `COMPLETADO` | -**Progreso:** 592 / 632 completados · 14 en progreso · 22 pendientes · 4 diferidos +**Progreso:** 593 / 633 completados · 14 en progreso · 22 pendientes · 4 diferidos **Oleada 2026-06-23 (auditoría profunda de Winston III):** Añadidos 14 gaps nuevos `GT-212`…`GT-225` del Winston Audit Playbook que cubren: higiene de estado ADR (GT-212), metadata + presupuestos operativos + corpus de guías por topología (GT-213, GT-217, GT-219), observabilidad + OpenAPI en controladores REST (GT-214, GT-215), paridad de input-schemas OPA + densidad de tests por topología (GT-216, GT-222), plantillas de rollback + on-call de Fase 05 (GT-218), cobertura de ramas CLI + paridad de envelope --format + limpieza de skip-list (GT-220, GT-224, GT-225), audit logging HTTP de MCP (GT-221), y tests e2e de paridad cross-surface (GT-223). diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 52c7b9a4..f3c83589 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -13,6 +13,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | ID | Gap | What it means | Example | Component | Phase | Criticality | Complexity | Status | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-633`](./gap-reference-catalog.md#gt-633) | **The guard compared the snapshot against six numbers copied out of the snapshot, so it could only ever pass — and the file it was failing to guard is stamped onto 388 rows of a larger artifact.** `native-evaluability-snapshot.json` declared in its own header that it was "a CAPTURED SNAPSHOT, not the source of truth". **No capture script existed.** It was hand-maintained and it drifted: it pinned `documentation-only: 129` while Core pinned 136, and went on calling rules `unimplemented-native` after they had handlers. Its guard in `iso-5055-mapping.test.mjs` asserted the snapshot's six class counts against six numbers typed into the test — the same six literals the snapshot already contained. **Expected value and actual value were copies of each other**, so the assertion held for as long as the file was unchanged, whatever Core actually pinned; it reported an agreement it never checked, which is worse than no guard, because the row it protects reads as verified. **The drift does not stay where it starts.** `build-iso-5055-mapping.mjs` stamps `nativeEvaluability` onto EVERY row of the ISO/IEC 5055 mapping from that file — 388 rows after the recapture, 381 before — so one stale class is laundered into a derived artifact five times the size of its input, and overstates the handler backlog that is [`GT-598`](./gap-reference-catalog.md#gt-598)'s entire deliverable. The recapture→rebuild order was written down nowhere, and the mapping's own guard ran in **no workflow at all**. **FIXED 2026-07-29:** a capture script that runs Core's REAL triage through `ts-node` instead of reimplementing the classification, the measurement extracted out of the jest spec into `test/rule-corpus-triage.ts` so a script can reach it — that unreachability is why the file was maintained by hand — snapshot-vs-fresh-triage assertions in both directions, a documentation-job guard that reads Core's pinned counts OUT of the spec and **throws** when the literal is missing or reshaped, and both steps declared as links in the [`GT-630`](./gap-reference-catalog.md#gt-630) derived-artifact chain. Recaptured: `documentation-only` 129 → 136, corpus 379 → 386, mapping 381 → 388 rows, backlog unchanged at 60. | | | `Governance` | Cross | P1 | S | `DONE` | | [`GT-632`](./gap-reference-catalog.md#gt-632) | **The MCP denied every tool in production from a path typo, and the evidence that it worked was untracked local state.** `abac-evaluator.ts:322` resolved the compiled policy at `/sdk/cli/rulesets/opa/policy.wasm` — the PRE-`src/`-refactor path. The file lives at `src/sdk/cli/…`. When the stat fails the evaluator denies fail-closed in production, so on a clean checkout with `NODE_ENV=production` the compiled policy never loads and EVERY MCP tool is refused. It looked green because `policy.wasm` is gitignored: a stale file left at the old path by an earlier era made the load succeed locally, and the test that covers it passed against an artifact no fresh checkout would have. Same shape as `GT-625` — a green that depended on untracked state — but on the authorization path. Found on 2026-07-29 while investigating why `abac-rego-parity.spec.ts` had gone red; the rego itself was correct (`opa eval` returns `allow: true` for an architect in production). **`40-validate-path-literals` cannot see this class:** it scans string literals, and this is a `path.join(corePath, 'sdk', 'cli', …)` construction. Twelve such joins survive the refactor across `src/**` — the other eleven are in `opa-input-builder.ts` and `cli-release-rule.handler.ts`, where a wrong path produces a false NEGATIVE in an evaluator rather than a denial, which is quieter and no less wrong. **FIXED for the authorization path on 2026-07-29:** the evaluator now tries both locations, newest first — not sloppiness, but what keeps an already-deployed image working while the path is corrected. Reverting the fix turns `abac-rego-parity.spec.ts` red (1 of 11); with it, mcp-server is 434/434. The other eleven joins and the guard blind spot remain. **Criterion 2 CLOSED 2026-07-29 — and two of the eleven were wrong in a way the prefix alone would not have fixed.** Eight were plain survivors of the `src/` move and took the prefix. The other three were worse: two pointed at `sdk/cli/src/core/mcp/server.ts`, a file that left the CLI entirely when mcp-server became its own package (now `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); and one asserted a per-package `package-lock.json`, which npm WORKSPACES forbids — there is exactly one lockfile, at the root, so that check could only ever be false. A rule nobody can satisfy is not a stricter rule. **The specs and parity fixtures encoded the old layout too** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), which is why they had stayed green over paths that do not exist: the fixture and the code agreed with each other and neither agreed with the repository. Both are corrected. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterion 3 CLOSED 2026-07-29 — the guard's first run was mostly wrong, which is the finding.** `47-validate-joined-paths` reported 19 broken paths; only 3 were defects. The other 16 would each have been made WORSE by a fix: six had base `root`, a PARAMETER naming the customer workspace under evaluation, so correcting them would break evaluation for every satellite; nine were members of FALLBACK CHAINS that are correct when one of four layouts resolves, and the natural way to silence those is to delete the fallbacks, breaking deployed images; one was a PROHIBITION whose absence IS the passing state, so correcting it would have inverted the rule. Chains are now detected structurally (array siblings, or the arms of a ternary); prohibitions cannot be, so they carry a written reason. The three real defects: `opa-evaluator.ts` resolved `policy.wasm` and the input schemas without the `src/` prefix, same fail-closed class as the P0 above; `satellite-upgrade-diff.ts` probed `rulesets` behind an early return, reporting zero ruleset changes on every satellite upgrade; `artifact-path-resolver.ts` named an `adr-matrix.json` present in no layout. 11 negative fixtures, both anti-vacuous floors, and `43-validate-guard-negative-fixtures` OBSERVED it red across 36 guards. A known leniency (two joins as sibling arguments) is recorded in its header rather than hidden. | | | `MCP Server` | Cross | P0 | M | `DONE` | | [`GT-631`](./gap-reference-catalog.md#gt-631) | **Carved out of [`GT-573`](./gap-reference-catalog.md#gt-573) so its cross-repo half is tracked rather than absorbed into a closure.** The Core half of GT-573's third criterion is done: `evaluation-context` and `evaluation-result` are pinned in `MACHINE_CONTRACT_SET` alongside `gate-evidence` and `output-envelope`, so a change to either shape now fails `10-validate-contract-conformance` instead of silently reaching consumers. What remains is in another repository and cannot be done from here: the **Tracker** must re-pin those two schemas in its own `contracts/evolith-core-contracts.json` with their sha256, bind the three published fixtures (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) in a DTO binding test, drop or document `resolvedTopology` (which the canonical result never carried), rename its gate `phase` to `phaseId`, and delete its fall-through to `SKIPPED` for a payload carrying a non-blank `overallVerdict`. Until it does, a Core-side shape change is caught here and still surprises the consumer. | | | `Evolith Tracker` | Cross | P1 | S | `PENDING` | | [`GT-630`](./gap-reference-catalog.md#gt-630) | **Derived artifacts have a dependency ORDER and nothing enforced it — it cost three red required checks on 2026-07-28 alone.** `generate-executive-summary.mjs` reads `maturity-reconciliation.json`, which is itself derived from the gap board. Generate the summary BEFORE reconciling and it captures a value the reconciler is about to move. Each artifact's own `--check` then passes at that moment — the summary genuinely matches what was just written — so `ci-runner governance` goes green locally and `Validate documentation` goes red on the runner, where the steps run in the declared order. That is not a stale-artifact bug, which the individual `--check` modes already catch; it is an ORDER bug, invisible to any check that looks at one artifact at a time. The correct sequence (reconcile → suite → generate → validate) was written down nowhere in the repository, which is why the same mistake was made three times in one session by someone who had already been bitten by it twice. **CLOSED on registration.** The chain is now DATA in `46-validate-derived-artifact-order.mjs` — producer, what it consumes, what it writes — walked in dependency order, wired into `Validate documentation`. It reports the FIRST stale link with the command that fixes it and the inputs that must be current before it, because an artifact built on stale input is not independently wrong. Writing the tests bounded what the fixed-point pass is worth, and the bound is recorded in the guard: with deterministic generators, currency-in-order already subsumes it; it earns its place on a generator whose output does not depend only on its declared inputs, which is the case the self-test exercises. 8 self-tests, four of them anti-vacuous. Registered in `guard-classification.mjs`; 58 guards classified, 35 observed failing. | | | `Governance` | Cross | P2 | S | `DONE` | @@ -647,7 +648,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-246`](./gap-reference-catalog.md#gt-246) | Implement Chaos Mesh/Litmus experiments | | | `QA` | Cross | P3 | L | `DONE` | -**Progress:** 592 / 632 done · 14 in progress · 22 pending · 4 deferred +**Progress:** 593 / 633 done · 14 in progress · 22 pending · 4 deferred **Wave 2026-06-23 (Winston deep audit III):** Added 14 new gaps `GT-212`…`GT-225` from the Winston Audit Playbook covering: ADR status hygiene (GT-212), topology manifest metadata + operational budgets + guidance corpus (GT-213, GT-217, GT-219), REST controller observability + OpenAPI (GT-214, GT-215), OPA input-schema parity + per-topology test density (GT-216, GT-222), SDLC Phase 05 rollback + on-call templates (GT-218), CLI branch coverage + envelope format coverage + skip-list cleanup (GT-220, GT-224, GT-225), MCP HTTP audit logging (GT-221), and cross-surface parity e2e tests (GT-223). diff --git a/reference/core/control-center/maturity-reports/executive-summary.es.md b/reference/core/control-center/maturity-reports/executive-summary.es.md index 6dff5d41..625ef614 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -45,14 +45,14 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| | Fecha canónica del tablero | 2026-07-26 | -| Gaps totales | 632 | -| Gaps cerrados | 592 | +| Gaps totales | 633 | +| Gaps cerrados | 593 | | Gaps pendientes | 40 | | P0 abiertos | 4 | | P1 abiertos | 15 | | P2 abiertos | 18 | | Cierre total | 93.7% | -| Registros de evidencia de cierre | 574 | +| Registros de evidencia de cierre | 575 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 2b6caa8c..59f0f07b 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -45,14 +45,14 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| | Canonical board date | 2026-07-26 | -| Total gaps | 632 | -| Closed gaps | 592 | +| Total gaps | 633 | +| Closed gaps | 593 | | Open gaps | 40 | | Open P0 | 4 | | Open P1 | 15 | | Open P2 | 18 | | Total closure | 93.7% | -| Closure evidence records | 574 | +| Closure evidence records | 575 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index 722d157d..aecda681 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -3,14 +3,14 @@ "scope": "evolith-core", "asOf": "2026-07-26", "gaps": { - "total": 632, - "done": 592, + "total": 633, + "done": 593, "pending": 22, "inProgress": 14, "deferred": 4 }, "evidence": { - "closureRecords": 574, + "closureRecords": 575, "cliPackage": "@beyondnet/evolith-cli@1.2.1", "adrCount": 139, "rulesetCount": 174, From 40d3fa923218f79f4a3280fd0c999cc0b0a4d203 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:33:58 -0500 Subject: [PATCH 05/10] chore(harness): declare the two standards links in the derived-artifact chain (GT-633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The edit the originating session was blocked from making (S-16). The recapture->rebuild order stops being prose in two READMEs and becomes data in the GT-630 chain: 1. ABAC rego tool sets 2. native evaluability snapshot <- new 3. ISO/IEC 5055 corpus mapping <- new 4. maturity reconciliation 5. executive governance summary The snapshot link has the same shape as the ABAC one — derived from the RUNTIME, not from a data file — so its `consumes` names the triage entry point and the two sources that decide a rule's class. The mapping link is the edge the order exists for: the builder stamps `nativeEvaluability` onto all 388 rows from the snapshot, so rebuilding first launders a stale class into an artifact five times the size of its input. Replay-safe because `capturedOn` is deliberately sticky while the classification is unchanged, so the fixed-point pass stays byte-identical. The guard's own fixtures move with it: `ABAC_STUB` becomes `UPSTREAM_STUBS` with a shared `stubProducer` helper covering all three leading links, the count assertions go 3 -> 5 links and `link 2 of 3` -> `link 4 of 5`, and the byte-identical-restore test now covers the three new standards artifacts too. Without that, five of the eight tests fail on the shape check before the behaviour under test runs. Verified: node --test 46-validate-derived-artifact-order.test.mjs -> 7 pass, 1 fail, and the one failure is the point: on the real tree the guard reports `declared producer does not exist: capture-native-evaluability-snapshot.mjs`, because commit 78e9b092 is not in this branch. THIS COMMIT MUST MERGE WITH OR AFTER 78e9b092 — landing it alone turns `Validate documentation` red. Co-Authored-By: Claude Opus 5 --- .../ci/46-validate-derived-artifact-order.mjs | 33 ++++++++++++ ...6-validate-derived-artifact-order.test.mjs | 52 ++++++++++++++----- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/.harness/scripts/ci/46-validate-derived-artifact-order.mjs b/.harness/scripts/ci/46-validate-derived-artifact-order.mjs index 828b3fb3..6775c6e0 100644 --- a/.harness/scripts/ci/46-validate-derived-artifact-order.mjs +++ b/.harness/scripts/ci/46-validate-derived-artifact-order.mjs @@ -88,6 +88,39 @@ export const CHAIN = [ consumes: ['src/packages/mcp-server/src/mcp/abac-evaluator.ts'], writes: ['src/rulesets/opa/abac-mcp-tool-access.rego'], }, + { + name: 'native evaluability snapshot', + producer: 'src/rulesets/standards/capture-native-evaluability-snapshot.mjs', + checkArgs: ['--check'], + // Same shape as the ABAC link: derived from the RUNTIME, not from a data + // file. The producer runs Core's real triage through ts-node, so its real + // inputs are the triage entry point and the two sources that decide a + // rule's class. Before GT-633 there was no producer at all — the file said + // it was a capture, nothing captured it, and it drifted 129 vs Core's 136. + // Replay-safe: `capturedOn` is deliberately sticky while the classification + // is unchanged, so the fixed-point pass below stays byte-identical instead + // of rewriting a date on every run. + consumes: [ + 'src/packages/core-domain/test/rule-corpus-triage.ts', + 'src/packages/core-domain/src/application/validators/rule-evaluability.ts', + 'src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts', + ], + writes: ['src/rulesets/standards/native-evaluability-snapshot.json'], + }, + { + name: 'ISO/IEC 5055 corpus mapping', + producer: 'src/rulesets/standards/build-iso-5055-mapping.mjs', + checkArgs: ['--check'], + // The edge this order exists for: the builder stamps `nativeEvaluability` + // onto all 388 mapping rows from the snapshot, so rebuilding BEFORE + // recapturing launders a stale class into an artifact five times the size + // of its input and overstates the handler backlog (GT-598, GT-633). + consumes: ['src/rulesets/standards/native-evaluability-snapshot.json'], + writes: [ + 'src/rulesets/standards/iso-5055-mapping.json', + 'src/rulesets/standards/iso-5055-mapping.csv', + ], + }, { name: 'maturity reconciliation', producer: '.harness/scripts/ci/09-reconcile-maturity.mjs', diff --git a/.harness/scripts/ci/46-validate-derived-artifact-order.test.mjs b/.harness/scripts/ci/46-validate-derived-artifact-order.test.mjs index de02c4c8..a3e7ca1a 100644 --- a/.harness/scripts/ci/46-validate-derived-artifact-order.test.mjs +++ b/.harness/scripts/ci/46-validate-derived-artifact-order.test.mjs @@ -38,7 +38,7 @@ test('the real repository is current and at a fixed point', () => { const { status, out } = run(resolve(__dirname, '../../..')); assert.equal(status, 0, out); assert.match(out, /at a fixed point/); - assert.match(out, /links declared \.+ 3/); + assert.match(out, /links declared \.+ 5/); }); test('the guard leaves the real tree byte-identical', () => { @@ -47,6 +47,9 @@ test('the guard leaves the real tree byte-identical', () => { // pass for the wrong reason. const repo = resolve(__dirname, '../../..'); const artifacts = [ + 'src/rulesets/standards/native-evaluability-snapshot.json', + 'src/rulesets/standards/iso-5055-mapping.json', + 'src/rulesets/standards/iso-5055-mapping.csv', 'reference/core/control-center/maturity-reports/maturity-reconciliation.json', 'reference/core/control-center/maturity-reports/executive-summary.md', 'reference/core/control-center/maturity-reports/executive-summary.es.md', @@ -60,17 +63,37 @@ test('the guard leaves the real tree byte-identical', () => { /** - * The chain gained a first link (the ABAC rego, GT-602). These fixtures are about - * ORDER, not about ABAC, so each mini-repo gets a trivial producer/artifact pair - * for it — otherwise the shape check trips before the behaviour under test runs. + * The chain leads with links these fixtures are not about — the ABAC rego + * (GT-602) and the two standards links (GT-633: capture the native-evaluability + * snapshot, then rebuild the ISO/IEC 5055 mapping from it). These fixtures are + * about ORDER, so each mini-repo gets a trivial producer/artifact pair for every + * one of them — otherwise the shape check trips before the behaviour under test + * runs. */ -const ABAC_STUB = { - '.harness/scripts/generate-abac-tool-sets.mjs': - "import fs from 'node:fs';\nconst f = process.cwd() + '/src/rulesets/opa/abac-mcp-tool-access.rego';\n" + - "if (process.argv.includes('--check')) process.exit(fs.readFileSync(f, 'utf8') === 'stable\\n' ? 0 : 1);\n" + - "fs.writeFileSync(f, 'stable\\n');\n", +const stubProducer = (artifacts) => + "import fs from 'node:fs';\n" + + `const files = ${JSON.stringify(artifacts)}.map((f) => process.cwd() + '/' + f);\n` + + "if (process.argv.includes('--check')) process.exit(files.every((f) => fs.readFileSync(f, 'utf8') === 'stable\\n') ? 0 : 1);\n" + + "for (const f of files) fs.writeFileSync(f, 'stable\\n');\n"; + +const UPSTREAM_STUBS = { + '.harness/scripts/generate-abac-tool-sets.mjs': stubProducer(['src/rulesets/opa/abac-mcp-tool-access.rego']), 'src/packages/mcp-server/src/mcp/abac-evaluator.ts': '// stub\n', 'src/rulesets/opa/abac-mcp-tool-access.rego': 'stable\n', + + 'src/rulesets/standards/capture-native-evaluability-snapshot.mjs': + stubProducer(['src/rulesets/standards/native-evaluability-snapshot.json']), + 'src/packages/core-domain/test/rule-corpus-triage.ts': '// stub\n', + 'src/packages/core-domain/src/application/validators/rule-evaluability.ts': '// stub\n', + 'src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts': '// stub\n', + 'src/rulesets/standards/native-evaluability-snapshot.json': 'stable\n', + + 'src/rulesets/standards/build-iso-5055-mapping.mjs': stubProducer([ + 'src/rulesets/standards/iso-5055-mapping.json', + 'src/rulesets/standards/iso-5055-mapping.csv', + ]), + 'src/rulesets/standards/iso-5055-mapping.json': 'stable\n', + 'src/rulesets/standards/iso-5055-mapping.csv': 'stable\n', }; // --- the shape of the declaration itself ------------------------------------ @@ -78,7 +101,7 @@ const ABAC_STUB = { describe('chain declaration (anti-vacuous)', () => { const fixture = (name, files) => { const root = join(sandbox, name); - for (const [rel, body] of Object.entries({ ...ABAC_STUB, ...files })) { + for (const [rel, body] of Object.entries({ ...UPSTREAM_STUBS, ...files })) { mkdirSync(dirname(join(root, rel)), { recursive: true }); writeFileSync(join(root, rel), body); if (rel.endsWith('.mjs')) chmodSync(join(root, rel), 0o755); @@ -128,7 +151,7 @@ describe('stale versus out-of-order', () => { mkdirSync(dirname(join(root, rel)), { recursive: true }); writeFileSync(join(root, rel), body); }; - for (const [rel, body] of Object.entries(ABAC_STUB)) w(rel, body); + for (const [rel, body] of Object.entries(UPSTREAM_STUBS)) w(rel, body); w('reference/core/control-center/gaps/gap-tracking.md', `count: ${boardCount}\n`); w('reference/core/control-center/maturity-reports/maturity-assessment.md', '# assessment\n'); w('reference/core/control-center/maturity-reports/maturity-reconciliation.json', `{"count":${reconCount}}\n`); @@ -177,7 +200,7 @@ fs.writeFileSync(dir + 'executive-summary.es.md', es); const root = miniRepo('stale-upstream', { boardCount: 9, reconCount: 7, summaryCount: 7 }); const { status, out } = run(root); assert.equal(status, 1, out); - assert.match(out, /maturity reconciliation is STALE \(link 2 of 3\)/); + assert.match(out, /maturity reconciliation is STALE \(link 4 of 5\)/); assert.match(out, /Stopping at the FIRST stale link on purpose/); assert.doesNotMatch(out, /executive governance summary is STALE/); }); @@ -239,7 +262,8 @@ fs.writeFileSync(dir + 'executive-summary.es.md', es); assert.equal(status, 1, out); assert.match(out, /differ after replaying the chain IN ORDER/); assert.match(out, /Fix by regenerating in the declared order/); - // The reconciler is link 2 now that the ABAC rego leads the chain. - assert.match(out, /2\. node \.harness\/scripts\/ci\/09-reconcile-maturity\.mjs/); + // The reconciler is link 4 now that the ABAC rego and the two standards + // links (GT-633) lead the chain. + assert.match(out, /4\. node \.harness\/scripts\/ci\/09-reconcile-maturity\.mjs/); }); }); From cf8b28d6282ac423e16736b48319c269efaaa404 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 15:55:24 -0500 Subject: [PATCH 06/10] chore(release): deprecate 1.1.0 on npm, and record the two packages that must not be (GT-624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Criterion (a) of GT-624. `1.1.0` is now deprecated on six packages — evolith-cli, evolith-mcp, evolith-agent-runtime, evolith-core-domain, evolith-core and evolith-infra-providers — each message naming the successor and the date the security wave landed, so `npm install @1.1.0` can no longer resolve the pre-2026-07-23 build silently. Verified with `npm view @1.1.0 deprecated --prefer-online`. The first read-back came up empty on four of the six; that was registry cache lag, not a failed command, which is why it was re-read rather than trusted. All six now report the message. TWO PACKAGES WERE DELIBERATELY LEFT ALONE, and the reason is a finding: - @beyondnet/evolith-sdk@1.1.0 is what the CURRENT line actually resolves. cli@1.2.1 and mcp@1.2.1 both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that package publishes 1.0.0, 1.1.0, 2.0.0 — so a caret range under 1.x pins EXACTLY 1.1.0, because 2.0.0 is a major. Deprecating it would print a warning on every fresh install of the healthy line and offer no 1.x to move to. The fix is to retarget those ranges to ^2.0.0 or publish an sdk@1.2.x — a code change and a republish, not a registry command. - @beyondnet/evolith-contracts@1.1.0 is that package's ONLY published version and nothing depends on it, so deprecating it leaves a warning with no upgrade path at all. GT-624 stays OPEN on its other two criteria: no CI gate fails when a security-tagged commit is absent from the published tag, and that gate's negative fixture does not exist. Deprecating the versions does not make an unpublished security fix visible, which is the larger half of the row. Verified: 08-validate-tracking (633 gaps, 575 closure records), 04 bilingual parity, 01-validate-docs (1491 files), 41 ratchet unchanged at 309 dead (this change adds none), 46 chain current and at a fixed point. Co-Authored-By: Claude Opus 5 --- .../core/control-center/gaps/gap-reference-catalog.es.md | 4 ++-- reference/core/control-center/gaps/gap-reference-catalog.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 90a51267..3569b2d8 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7979,10 +7979,10 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre **Título:** Las versiones 1.1.0 vulnerables siguen instalables sin aviso, y nada impide que otro arreglo de seguridad se quede sin publicar - **Propósito:** Que nadie pueda instalar en silencio una versión vulnerable, y que un arreglo de seguridad sin publicar deje de ser invisible. -- **Evidencia:** **Extraído de [`GT-570`](./gap-reference-catalog.es.md#gt-570) para que su remanente quede registrado y no absorbido en un cierre.** 1.2.0 se publicó el 2026-07-27 con provenance y la exposición está cerrada para quien instale `latest` — pero dos de los tres criterios originales de GT-570 no se cumplen con eso, y fingir lo contrario es el patrón que este tablero lleva pillándose a sí mismo (GT-12, GT-568, GT-254, GT-424). **(a) Las versiones 1.1.0 no están deprecadas.** `npm install @beyondnet/evolith-mcp@1.1.0` sigue resolviendo la build anterior a la ola de seguridad del 2026-07-23, en silencio, y el CHANGELOG público nombra los ficheros vulnerables. Deprecar es un comando por paquete pero exige credenciales de npm, así que es acción del dueño: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, e igual para `evolith-cli@1.1.0` y `evolith-agent-runtime@1.1.0`. **(b) Ningún gate de release falla cuando un commit etiquetado de seguridad no está en el tag publicado.** Esa ausencia es exactamente lo que permitió que la ola siguiera sin publicar del 2026-07-23 al 2026-07-27 mientras `SECURITY.md` declaraba la línea 1.1.x "actively patched". No lo detectó nada; lo detectó una auditoría. Relacionado: `GT-623` — release-please deriva los saltos de versión de los mensajes de commit, y el tipo `security(...)` que usan 2 de los últimos 60 commits no es un tipo de Conventional Commits, así que no aporta nada al bump. Los dos defectos dejan que un cambio de seguridad no llegue a una versión, por caminos distintos. +- **Evidencia:** **Extraído de [`GT-570`](./gap-reference-catalog.es.md#gt-570) para que su remanente quede registrado y no absorbido en un cierre.** 1.2.0 se publicó el 2026-07-27 con provenance y la exposición está cerrada para quien instale `latest` — pero dos de los tres criterios originales de GT-570 no se cumplen con eso, y fingir lo contrario es el patrón que este tablero lleva pillándose a sí mismo (GT-12, GT-568, GT-254, GT-424). **(a) Las versiones 1.1.0 no están deprecadas.** `npm install @beyondnet/evolith-mcp@1.1.0` sigue resolviendo la build anterior a la ola de seguridad del 2026-07-23, en silencio, y el CHANGELOG público nombra los ficheros vulnerables. Deprecar es un comando por paquete pero exige credenciales de npm, así que es acción del dueño: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, e igual para `evolith-cli@1.1.0` y `evolith-agent-runtime@1.1.0`. **(b) Ningún gate de release falla cuando un commit etiquetado de seguridad no está en el tag publicado.** Esa ausencia es exactamente lo que permitió que la ola siguiera sin publicar del 2026-07-23 al 2026-07-27 mientras `SECURITY.md` declaraba la línea 1.1.x "actively patched". No lo detectó nada; lo detectó una auditoría. Relacionado: `GT-623` — release-please deriva los saltos de versión de los mensajes de commit, y el tipo `security(...)` que usan 2 de los últimos 60 commits no es un tipo de Conventional Commits, así que no aporta nada al bump. Los dos defectos dejan que un cambio de seguridad no llegue a una versión, por caminos distintos. **Criterio (a) COMPLETADO el 2026-07-29 — y cubrió seis paquetes, no tres.** `1.1.0` queda deprecada en `evolith-cli`, `evolith-mcp`, `evolith-agent-runtime`, `evolith-core-domain`, `evolith-core` e `evolith-infra-providers`, con un mensaje que nombra el sucesor y la fecha en que aterrizó la ola; verificado con `npm view @1.1.0 deprecated --prefer-online` (la primera lectura salió vacía en cuatro de los seis — retardo de caché del registry, no un comando fallido, y por eso se repitió la verificación en vez de darla por buena). **DOS paquetes NO se deprecaron a propósito, y el motivo es un hallazgo, no un recorte de alcance.** `@beyondnet/evolith-sdk@1.1.0` es lo que la línea ACTUAL resuelve de verdad: `cli@1.2.1` y `mcp@1.2.1` declaran ambos `@beyondnet/evolith-sdk: ^1.1.0`, y las versiones publicadas de ese paquete son `1.0.0, 1.1.0, 2.0.0` — un rango caret bajo 1.x fija por tanto **exactamente 1.1.0**, porque 2.0.0 es un mayor. Deprecarla imprimiría un aviso en cada instalación limpia de la línea sana sin ofrecer ninguna versión 1.x a la que moverse; el arreglo real es reapuntar esos rangos a `^2.0.0` o publicar un `sdk@1.2.x`, que es un cambio de código y una republicación, no un comando de registry. `@beyondnet/evolith-contracts@1.1.0` es la ÚNICA versión publicada de ese paquete y nada depende de él, así que deprecarla dejaría un aviso sin ninguna ruta de actualización. Ambos quedan registrados aquí en vez de ejecutarse en silencio o descartarse. - **Componente:** `Infra` · **Criticidad:** P1 · **Complejidad:** S - **Procedencia:** Extraído de GT-570 el 2026-07-27, al cerrarlo. Dos de sus tres criterios originales no se cumplían con la publicación de 1.2.0; se registran aquí en vez de darse por buenos. - **Criterios de aceptación:** - - [ ] `npm view @beyondnet/evolith-mcp@1.1.0` reporta la versión como deprecada, e igual cli y agent-runtime. + - [x] `npm view @beyondnet/evolith-mcp@1.1.0` reporta la versión como deprecada, e igual cli y agent-runtime — y también core-domain, core e infra-providers. - [ ] Un gate de CI falla cuando un commit cuyo tipo o scope lo marca como de seguridad no está en el último tag publicado. - [ ] El gate lleva una fixture negativa que lo pone rojo, para que no sea otro guard que nadie ha visto fallar. diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 1084d2f2..c5ba2cd1 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -8074,10 +8074,10 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo **Title:** The vulnerable 1.1.0 versions are still installable without warning, and nothing stops a security fix going unpublished again - **Purpose:** Make it impossible to silently install a vulnerable version, and make an unpublished security fix stop being invisible. -- **Evidence:** **Carved out of [`GT-570`](./gap-reference-catalog.md#gt-570) so its remainder is tracked rather than absorbed into a closure.** 1.2.0 shipped on 2026-07-27 with provenance and the exposure is closed for anyone installing `latest` — but two of GT-570's three original criteria are not met by that, and pretending otherwise is the pattern this board keeps catching in itself (GT-12, GT-568, GT-254, GT-424). **(a) The 1.1.0 versions are not deprecated.** `npm install @beyondnet/evolith-mcp@1.1.0` still resolves the build that predates the 2026-07-23 security wave, silently, and the public CHANGELOG names the vulnerable files. Deprecating is one command per package but requires npm credentials, so it is an owner action: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, likewise for `evolith-cli@1.1.0` and `evolith-agent-runtime@1.1.0`. **(b) No release gate fails when a security-tagged commit is absent from the published tag.** That absence is exactly what let the wave sit unpublished from 2026-07-23 to 2026-07-27 while `SECURITY.md` declared the 1.1.x line "actively patched". Nothing detected it; an audit did. Related: `GT-623` — release-please derives version bumps from commit messages, and the `security(...)` type used by 2 of the last 60 commits is not a Conventional Commits type, so it contributes nothing to a bump. Both defects let a security change fail to reach a version, by different routes. +- **Evidence:** **Carved out of [`GT-570`](./gap-reference-catalog.md#gt-570) so its remainder is tracked rather than absorbed into a closure.** 1.2.0 shipped on 2026-07-27 with provenance and the exposure is closed for anyone installing `latest` — but two of GT-570's three original criteria are not met by that, and pretending otherwise is the pattern this board keeps catching in itself (GT-12, GT-568, GT-254, GT-424). **(a) The 1.1.0 versions are not deprecated.** `npm install @beyondnet/evolith-mcp@1.1.0` still resolves the build that predates the 2026-07-23 security wave, silently, and the public CHANGELOG names the vulnerable files. Deprecating is one command per package but requires npm credentials, so it is an owner action: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, likewise for `evolith-cli@1.1.0` and `evolith-agent-runtime@1.1.0`. **(b) No release gate fails when a security-tagged commit is absent from the published tag.** That absence is exactly what let the wave sit unpublished from 2026-07-23 to 2026-07-27 while `SECURITY.md` declared the 1.1.x line "actively patched". Nothing detected it; an audit did. Related: `GT-623` — release-please derives version bumps from commit messages, and the `security(...)` type used by 2 of the last 60 commits is not a Conventional Commits type, so it contributes nothing to a bump. Both defects let a security change fail to reach a version, by different routes. **Criterion (a) DONE 2026-07-29 — and it covered six packages, not three.** `1.1.0` is now deprecated on `evolith-cli`, `evolith-mcp`, `evolith-agent-runtime`, `evolith-core-domain`, `evolith-core` and `evolith-infra-providers`, each message naming the successor and the date the wave landed; verified with `npm view @1.1.0 deprecated --prefer-online` (the first read-back was empty on four of the six — registry cache lag, not a failed command, which is why the verification was repeated rather than trusted). **TWO packages were deliberately NOT deprecated, and the reason is a finding rather than a scope cut.** `@beyondnet/evolith-sdk@1.1.0` is what the CURRENT line actually resolves: `cli@1.2.1` and `mcp@1.2.1` both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that package's published versions are `1.0.0, 1.1.0, 2.0.0` — a caret range under 1.x therefore pins **exactly 1.1.0**, because 2.0.0 is a major. Deprecating it would print a warning on every fresh install of the healthy line and offer no 1.x version to move to; the real fix is to retarget those ranges to `^2.0.0` or publish an `sdk@1.2.x`, which is a code change and a republish, not a registry command. `@beyondnet/evolith-contracts@1.1.0` is that package's ONLY published version and nothing depends on it, so deprecating it would leave a warning with no upgrade path at all. Both are recorded here instead of being executed quietly or dropped. - **Component:** `Infra` · **Criticality:** P1 · **Complexity:** S - **Provenance:** Carved out of GT-570 on 2026-07-27 as it closed. Two of its three original criteria were not met by shipping 1.2.0; they are recorded here rather than assumed. - **Acceptance criteria:** - - [ ] `npm view @beyondnet/evolith-mcp@1.1.0` reports the version as deprecated, likewise cli and agent-runtime. + - [x] `npm view @beyondnet/evolith-mcp@1.1.0` reports the version as deprecated, likewise cli and agent-runtime — and also core-domain, core and infra-providers. - [ ] A CI gate fails when a commit whose type or scope marks it as security is absent from the latest published tag. - [ ] The gate ships with a negative fixture that turns it red, so it is not another guard nobody has seen fail. From 53ae9717a847b14b8394dcede7608077ecad8127 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 16:00:27 -0500 Subject: [PATCH 07/10] =?UTF-8?q?docs(gaps):=20register=20GT-634=20and=20G?= =?UTF-8?q?T-635=20=E2=80=94=20a=20caret=20range=20that=20pins=20a=20pre-w?= =?UTF-8?q?ave=20SDK,=20and=20a=20ratchet=20nobody=20can=20satisfy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both found while executing other work, and both registered rather than worked around. GT-634 (P1) — the published CLI and MCP resolve an SDK build that predates the security wave. cli@1.2.1 and mcp@1.2.1 both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that resolves to EXACTLY 1.1.0: the SDK publishes 1.0.0, 1.1.0 and 2.0.0 with nothing in between, and a caret cannot cross a major. sdk@1.1.0 is from 2026-07-18, sdk@2.0.0 from 2026-07-27 as part of the release that closed GT-570, and both consumers from 2026-07-28 — published AFTER 2.0.0 existed, still pointing at the 1.x line. So the exposure GT-570 declared closed for "anyone installing latest" is not closed for this dependency. The row deliberately does NOT claim sdk@1.1.0 carries a specific vulnerability: what is measured is the dates and the resolution, not the contents, and verifying which fixes are in 2.0.0 is part of closing it. GT-635 (P2) — `Governance guards (GT-578)` fails on main itself and has for at least three consecutive runs: 309 dead references against a budget of 305. The budget was set in 4a1b92d6 "where it was measured"; ten commits have edited gap-closure-evidence.json since. A ratchet whose entire purpose is that dead references must not grow is reporting exactly that while being ignored, because a permanently red job reads as background noise — the same failure mode GT-622 records for the orphaned CodeQL configuration. The row says explicitly what not to do: raise the budget to 309 and ratify the breach. Counters recomputed 593/633 -> 593/635, pending 22 -> 24. Verified: 08-validate-tracking (635 gaps, 611/611 EN/ES sections), 04 bilingual parity, 01-validate-docs (1491 files), 41 ratchet unchanged at 309 (these rows add no dead references), 46 chain current and at a fixed point. Co-Authored-By: Claude Opus 5 --- .../gaps/gap-reference-catalog.es.md | 26 +++++++++++++++++++ .../gaps/gap-reference-catalog.md | 26 +++++++++++++++++++ .../control-center/gaps/gap-tracking.es.md | 4 ++- .../core/control-center/gaps/gap-tracking.md | 4 ++- .../maturity-reports/executive-summary.es.md | 22 ++++++++-------- .../maturity-reports/executive-summary.md | 22 ++++++++-------- .../maturity-reconciliation.json | 4 +-- 7 files changed, 82 insertions(+), 26 deletions(-) diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 3569b2d8..8f7f501c 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7855,6 +7855,32 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - [x] El tipo `security` está declarado en la config de commitlint con un mapeo explícito de salto de versión, o se deja de usar. - [x] Ningún hook de `.husky/` imprime un mensaje de "skipping" y sale con cero — un hook que no puede correr se borra, no se silencia. +#### GT-635 + +**Title:** El ratchet de referencias muertas está por encima del presupuesto en main desde el 2026-07-28, así que el job de gobernanza está en rojo permanente + +- **Purpose:** Que un ratchet incumplido o se respete o se vuelva a medir — un gate que nadie puede satisfacer enseña a la gente a ignorarlo. +- **Evidence:** **`Governance guards (GT-578)` falla en `main` mismo, y lleva al menos tres corridas consecutivas así.** `41-validate-evidence-commands --strict --max-dead 305` reporta **309 referencias muertas en un checkout limpio**, así que el job sale 1 en cada corrida de `ci-cd.yml`. Observado en las corridas `30482497995` (2026-07-29 19:00Z), `30470648146` (16:25Z) y `30466280865` (15:31Z) — todas en `main`, ninguna con cambios sin mergear. El presupuesto se fijó en 305 en `4a1b92d6` (2026-07-28) con el mensaje "put the ratchet back where it was measured"; **desde entonces diez commits han editado `gap-closure-evidence.json`**, cada uno añadiendo `validationCommands`, y la cuenta se fue por encima del número que se midió. Las cuatro referencias muertas de más no son atribuibles a ninguno en concreto solo por la cuenta. **Por qué es más que contabilidad:** el propósito entero del ratchet es que las referencias muertas no CREZCAN, y ahora mismo está reportando exactamente eso mientras se lo ignora, porque un job en rojo permanente se lee como ruido de fondo — el mismo modo de fallo que [`GT-622`](./gap-reference-catalog.es.md#gt-622) registra para la configuración huérfana de CodeQL. Además implica que el próximo cambio que añada de verdad una referencia muerta será indistinguible de las cuatro que ya están. **Lo que NO hay que hacer:** subir el presupuesto a 309 y seguir. Eso convierte un ratchet incumplido en un ratchet ratificado, que es exactamente cómo se llegó a un corpus de 309 referencias muertas. Las dos opciones honestas son arreglar las cuatro (reapuntar los comandos a referentes que existen) o volver a medir y declarar, en el commit, qué se aceptó y por qué. +- **Component:** `Governance` · **Criticality:** P2 · **Complexity:** S +- **Provenance:** Encontrado el 2026-07-29 diagnosticando cuatro checks en rojo del PR #277. Tres de los cuatro eran preexistentes en `main`; este es el que traía el número idéntico (309 vs 305) en ambos lados, y eso es lo que probó que el PR no lo había causado. +- **Acceptance criteria:** + - [ ] `Governance guards (GT-578)` pasa en `main` sin haber subido el presupuesto para dar cabida a referencias que ya estaban muertas. + - [ ] Cada una de las cuatro referencias añadidas desde `4a1b92d6` queda reparada o nombrada en el commit que la acepta, con el motivo. + - [ ] El guard reporta la base del ratchet sobre un checkout limpio, para que el presupuesto no vuelva a calibrarse contra una máquina que resulta tener algo construido. + +#### GT-634 + +**Title:** El CLI y el MCP publicados resuelven una build del SDK anterior a la ola de seguridad, porque un rango caret bajo 1.x la fija + +- **Purpose:** Que `npm install @beyondnet/evolith-cli@latest` deje de arrastrar una dependencia publicada antes de los fixes que anuncia su propio CHANGELOG. +- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` y `@beyondnet/evolith-mcp@1.2.1` declaran ambos `@beyondnet/evolith-sdk: ^1.1.0`, y eso resuelve EXACTAMENTE `1.1.0`.** El SDK publica `1.0.0`, `1.1.0` y `2.0.0` y nada en medio, así que un rango caret bajo 1.x no puede alcanzar 2.0.0 — un caret está acotado por el siguiente mayor. Las fechas son el hallazgo: `sdk@1.1.0` se publicó el **2026-07-18**, `sdk@2.0.0` el **2026-07-27** como parte de la release que cerró [`GT-570`](./gap-reference-catalog.es.md#gt-570), y `cli@1.2.1` / `mcp@1.2.1` el **2026-07-28** — *después* de que 2.0.0 existiera, y seguían apuntando a la línea 1.x. Así que la exposición que GT-570 declaró cerrada para "quien instale `latest`" no está cerrada para esta dependencia: el `latest` de ambas superficies instala un SDK construido antes de la ola del 2026-07-23. **Dicho con precisión, porque la afirmación fuerte no está verificada aquí:** lo medido son las fechas y la resolución, no el contenido — esta fila no afirma que `sdk@1.1.0` cargue una vulnerabilidad concreta, solo que es anterior a la ola y que la release que la trajo fue un mayor al que los consumidores no llegan. Verificar qué fixes hay en `sdk@2.0.0` es parte de cerrarla. **Encontrado porque bloqueaba otra cosa:** deprecar `sdk@1.1.0` para [`GT-624`](./gap-reference-catalog.es.md#gt-624) habría impreso un aviso en cada instalación limpia de la línea actual sin ningún sucesor 1.x que ofrecer, y eso es lo que dejó el rango a la vista. Arreglo: reapuntar ambos rangos a `^2.0.0` (un salto mayor de dependencia, así que exige revisar los breaking changes del SDK contra ambos call sites) o publicar un `sdk@1.2.x` con los fixes, y entonces deprecar `sdk@1.1.0` y cerrar de verdad el primer criterio de GT-624. +- **Component:** `Infra` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Encontrado el 2026-07-29 ejecutando el criterio de deprecación de GT-624. Registrado en vez de sorteado: saltarse la deprecación en silencio habría dejado el rango — y la instalación — exactamente como están. +- **Acceptance criteria:** + - [ ] `npm view @beyondnet/evolith-cli@latest dependencies` muestra un rango de SDK que resuelve a una versión publicada el 2026-07-27 o después, e igual `evolith-mcp`. + - [ ] Los fixes que reclama la ola de seguridad del 2026-07-23 se verifican presentes en la versión del SDK que el CLI y el MCP resuelven de verdad, en vez de asumirse por el número de versión. + - [ ] `@beyondnet/evolith-sdk@1.1.0` queda deprecada en cuanto exista un sucesor al que sus consumidores puedan llegar. + #### GT-633 **Title:** Un guard cuyo valor esperado era una copia de su valor real, sobre una entrada que se blanquea en un artefacto cinco veces mayor diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index c5ba2cd1..b2e4c1ab 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7950,6 +7950,32 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - [x] The `security` type is either declared in the commitlint config with an explicit version-bump mapping, or removed from use. - [x] No hook in `.husky/` prints a "skipping" message and exits zero — a hook that cannot run is deleted, not silenced. +#### GT-635 + +**Title:** The dead-reference ratchet has been over budget on main since 2026-07-28, so the governance job is permanently red + +- **Purpose:** Make a ratchet that has been breached either hold or be re-measured — a gate nobody can satisfy trains people to ignore it. +- **Evidence:** **`Governance guards (GT-578)` fails on `main` itself, and has for at least three consecutive runs.** `41-validate-evidence-commands --strict --max-dead 305` reports **309 dead references on a clean checkout**, so the job exits 1 on every run of `ci-cd.yml`. Observed on runs `30482497995` (2026-07-29 19:00Z), `30470648146` (16:25Z) and `30466280865` (15:31Z) — all on `main`, none carrying an unmerged change. The budget was set to 305 in `4a1b92d6` (2026-07-28) with the message "put the ratchet back where it was measured"; **ten commits have since edited `gap-closure-evidence.json`**, each adding `validationCommands`, and the count drifted past the number that was measured. The four extra dead references are not attributable to any single one of them from the count alone. **Why this is more than bookkeeping:** the ratchet's whole purpose is that dead references must not GROW, and it is currently reporting exactly that while being ignored, because a permanently red job reads as background noise — the same failure mode [`GT-622`](./gap-reference-catalog.md#gt-622) records for the orphaned CodeQL configuration. It also means the next change that genuinely adds a dead reference will be indistinguishable from the four already there. **What NOT to do:** raise the budget to 309 and move on. That converts a breached ratchet into a ratified one, which is how a corpus of 309 dead references was reached in the first place. The two honest options are to fix the four (retarget the commands at referents that exist) or to re-measure and state, in the commit, what was accepted and why. +- **Component:** `Governance` · **Criticality:** P2 · **Complexity:** S +- **Provenance:** Found on 2026-07-29 while diagnosing four red checks on PR #277. Three of the four were pre-existing on `main`; this is the one whose number was identical (309 vs 305) on both sides, which is what proved the PR had not caused it. +- **Acceptance criteria:** + - [ ] `Governance guards (GT-578)` passes on `main` without the budget being raised to accommodate references that are already dead. + - [ ] Each of the four references added since `4a1b92d6` is either repaired or named in the commit that accepts it, with the reason. + - [ ] The guard reports the ratchet basis on a clean checkout, so the budget can never again be calibrated against a machine that happens to have built something. + +#### GT-634 + +**Title:** The published CLI and MCP resolve an SDK build that predates the security wave, because a caret range under 1.x pins it + +- **Purpose:** Make `npm install @beyondnet/evolith-cli@latest` stop pulling a dependency published before the fixes its own CHANGELOG announces. +- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that resolves to EXACTLY `1.1.0`.** The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` and nothing in between, so a caret range under 1.x cannot reach 2.0.0 — a caret is bounded by the next major. The dates are the finding: `sdk@1.1.0` was published **2026-07-18**, `sdk@2.0.0` on **2026-07-27** as part of the release that closed [`GT-570`](./gap-reference-catalog.md#gt-570), and `cli@1.2.1` / `mcp@1.2.1` on **2026-07-28** — *after* 2.0.0 existed, still pointing at the 1.x line. So the exposure GT-570 declared closed for "anyone installing `latest`" is not closed for this dependency: `latest` of both surfaces installs an SDK built before the 2026-07-23 wave. **Stated precisely, because the stronger claim is not verified here:** what is measured is the dates and the resolution, not the contents — this row does not assert that `sdk@1.1.0` carries a specific vulnerability, only that it predates the wave and that the release which shipped the wave was a major the consumers cannot reach. Verifying which fixes are in `sdk@2.0.0` is part of closing it. **Found because it blocked something else:** deprecating `sdk@1.1.0` for [`GT-624`](./gap-reference-catalog.md#gt-624) would have printed a warning on every fresh install of the current line with no 1.x successor to offer, which is what exposed the range. Fix: retarget both ranges to `^2.0.0` (a major bump of a dependency, so it needs the SDK's breaking changes reviewed against both call sites) or publish an `sdk@1.2.x` carrying the fixes, then deprecate `sdk@1.1.0` and close GT-624's first criterion for real. +- **Component:** `Infra` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Found on 2026-07-29 while executing GT-624's deprecation criterion. Registered rather than worked around: skipping the deprecation quietly would have left the range — and the install — exactly as they are. +- **Acceptance criteria:** + - [ ] `npm view @beyondnet/evolith-cli@latest dependencies` shows an SDK range that resolves to a version published on or after 2026-07-27, likewise `evolith-mcp`. + - [ ] The fixes claimed by the 2026-07-23 security wave are verified present in the SDK version the CLI and MCP actually resolve, rather than assumed from the version number. + - [ ] `@beyondnet/evolith-sdk@1.1.0` is deprecated once a successor its consumers can reach exists. + #### GT-633 **Title:** A guard whose expected value was a copy of its actual value, over an input laundered into an artifact five times its size diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 44527eec..7a32cb88 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -13,6 +13,8 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | ID | Gap | Qué significa | Ejemplo | Componente | Fase | Criticidad | Complejidad | Estado | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-634`](./gap-reference-catalog.es.md#gt-634) | **El CLI y el MCP publicados resuelven una build del SDK anterior a la ola de seguridad, porque un rango caret bajo 1.x la fija.** `@beyondnet/evolith-cli@1.2.1` y `@beyondnet/evolith-mcp@1.2.1` declaran ambos `@beyondnet/evolith-sdk: ^1.1.0`, y eso resuelve **exactamente `1.1.0`** — el SDK publica `1.0.0`, `1.1.0` y `2.0.0` y nada en medio, y un caret no cruza un mayor. Las fechas son el hallazgo: `sdk@1.1.0` se publicó el 2026-07-18, `sdk@2.0.0` el 2026-07-27 como parte de la release que cerró [`GT-570`](./gap-reference-catalog.es.md#gt-570), y ambos consumidores el 2026-07-28 — **después** de que 2.0.0 existiera, y seguían en la línea 1.x. Así que la exposición que GT-570 declaró cerrada para "quien instale `latest`" no está cerrada para esta dependencia. Dicho con precisión, porque la afirmación fuerte no está verificada aquí: lo medido son las fechas y la resolución, no el contenido — esta fila no afirma que `sdk@1.1.0` cargue una vulnerabilidad concreta. Encontrado ejecutando el criterio de deprecación de [`GT-624`](./gap-reference-catalog.es.md#gt-624), que es lo que dejó el rango a la vista: deprecar `sdk@1.1.0` avisaría en cada instalación limpia de la línea actual sin ningún sucesor 1.x que ofrecer. | | | `Infra` | Cross | P1 | S | `PENDIENTE` | +| [`GT-635`](./gap-reference-catalog.es.md#gt-635) | **El ratchet de referencias muertas está por encima del presupuesto en main desde el 2026-07-28, así que el job de gobernanza está en rojo permanente.** `41-validate-evidence-commands --strict --max-dead 305` reporta **309 referencias muertas en un checkout limpio**, así que `Governance guards (GT-578)` sale 1 en cada corrida de `ci-cd.yml` — observado en las corridas `30482497995`, `30470648146` y `30466280865`, todas en `main` y sin nada por mergear. El presupuesto se fijó en `4a1b92d6` "donde se midió"; desde entonces diez commits han editado `gap-closure-evidence.json`, cada uno añadiendo `validationCommands`, y la cuenta se fue por encima. El propósito entero del ratchet es que las referencias muertas no CREZCAN, y está reportando exactamente eso mientras se lo ignora, porque un job en rojo permanente se lee como ruido de fondo — el mismo modo de fallo que [`GT-622`](./gap-reference-catalog.es.md#gt-622) registra para la configuración huérfana de CodeQL. El próximo cambio que añada de verdad una referencia muerta será indistinguible de las cuatro que ya están. **Lo que no hay que hacer:** subir el presupuesto a 309, que convierte un ratchet incumplido en uno ratificado. | | | `Governance` | Cross | P2 | S | `PENDIENTE` | | [`GT-633`](./gap-reference-catalog.es.md#gt-633) | **El guard comparaba el snapshot contra seis números copiados del propio snapshot, así que solo podía pasar — y el archivo que no estaba vigilando se estampa en 388 filas de un artefacto mayor.** `native-evaluability-snapshot.json` declaraba en su propia cabecera que era "un SNAPSHOT CAPTURADO, no la fuente de verdad". **No existía ningún script de captura.** Se mantenía a mano y derivó: fijaba `documentation-only: 129` mientras Core fijaba 136, y seguía llamando `unimplemented-native` a reglas que ya tenían handler. Su guard en `iso-5055-mapping.test.mjs` asertaba los seis conteos de clase del snapshot contra seis números escritos a mano en el test — los mismos seis literales que el snapshot ya contenía. **El valor esperado y el valor real eran copias el uno del otro**, así que la aserción se sostenía mientras el archivo no cambiara, fijara Core lo que fijara; informaba de una concordancia que nunca comprobó, lo cual es peor que no tener guard, porque la fila que protege se lee como verificada. **La deriva no se queda donde empieza.** `build-iso-5055-mapping.mjs` estampa `nativeEvaluability` en TODAS las filas del mapeo ISO/IEC 5055 desde ese archivo — 388 tras la recaptura, 381 antes — así que una sola clase rancia se blanquea en un artefacto derivado cinco veces mayor que su entrada, y sobredimensiona el backlog de handlers que es el entregable entero de [`GT-598`](./gap-reference-catalog.es.md#gt-598). El orden recaptura→reconstrucción no estaba escrito en ningún sitio, y el guard del propio mapeo no corría en **ningún workflow**. **CORREGIDO el 2026-07-29:** un script de captura que ejecuta el triage REAL de Core vía `ts-node` en vez de reimplementar la clasificación, la medición extraída del spec de jest a `test/rule-corpus-triage.ts` para que un script pueda alcanzarla — esa inalcanzabilidad es la razón de que el archivo se mantuviera a mano —, aserciones snapshot-contra-triage-fresco en ambas direcciones, un guard del job de documentación que lee los conteos que Core fija LEYENDO el spec y **lanza excepción** cuando el literal falta o cambia de forma, y ambos pasos declarados como eslabones de la cadena de artefactos derivados de [`GT-630`](./gap-reference-catalog.es.md#gt-630). **Recapturado el 2026-07-29 tras mergear el fix en la línea actual, y la deriva había crecido mientras estaba en vuelo:** `native-handler` 139 → 151, `documentation-only` 129 → 136, el backlog de handlers 60 → **48**, corpus 379 → 386, mapeo 381 → 388 filas. Doce de esos handlers los cerraron [`GT-595`](./gap-reference-catalog.es.md#gt-595) y [`GT-632`](./gap-reference-catalog.es.md#gt-632) mientras el snapshot mantenido a mano seguía reportándolos como backlog — el defecto demostrándose una vez más, sobre números que nadie escribió dos veces. | | | `Governance` | Cross | P1 | S | `COMPLETADO` | | [`GT-632`](./gap-reference-catalog.es.md#gt-632) | **El MCP denegaba todas sus herramientas en producción por una ruta mal escrita, y la prueba de que funcionaba era estado local sin versionar.** `abac-evaluator.ts:322` resolvía la política compilada en `/sdk/cli/rulesets/opa/policy.wasm` — la ruta ANTERIOR al refactor `src/`. El fichero vive en `src/sdk/cli/…`. Cuando el stat falla, el evaluador deniega fail-closed en producción, así que en un checkout limpio con `NODE_ENV=production` la política compilada nunca se carga y TODA herramienta MCP se rechaza. Parecía verde porque `policy.wasm` está gitignorado: un fichero rancio dejado en la ruta vieja por una época anterior hacía que la carga funcionara en local, y el test que lo cubre pasaba contra un artefacto que ningún checkout fresco tendría. Misma forma que `GT-625` — un verde que dependía de estado no versionado — pero en el camino de autorización. Encontrado el 2026-07-29 investigando por qué `abac-rego-parity.spec.ts` se había puesto rojo; el rego era correcto (`opa eval` devuelve `allow: true` para un arquitecto en producción). **`40-validate-path-literals` no puede ver esta clase:** barre literales de cadena, y esto es una construcción `path.join(corePath, 'sdk', 'cli', …)`. Sobreviven doce joins así en `src/**` — los otros once están en `opa-input-builder.ts` y `cli-release-rule.handler.ts`, donde una ruta equivocada produce un falso NEGATIVO en un evaluador en vez de una denegación, que es más silencioso y no menos incorrecto. **CORREGIDO para el camino de autorización el 2026-07-29:** el evaluador prueba ahora ambas ubicaciones, la nueva primero — no es dejadez, es lo que mantiene funcionando una imagen ya desplegada mientras se corrige la ruta. Revertir el arreglo pone `abac-rego-parity.spec.ts` en rojo (1 de 11); con él, mcp-server va 434/434. Quedan los otros once joins y el punto ciego del guard. **Criterio 2 CERRADO el 2026-07-29 — y dos de las once estaban mal de una forma que el prefijo por sí solo no arreglaba.** Ocho eran supervivientes simples del movimiento a `src/` y admitieron el prefijo. Las otras tres eran peores: dos apuntaban a `sdk/cli/src/core/mcp/server.ts`, un fichero que salió del CLI por completo cuando mcp-server pasó a ser su propio paquete (hoy `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); y una exigía un `package-lock.json` por paquete, cosa que los WORKSPACES de npm impiden — hay exactamente un lockfile, en la raíz, así que esa comprobación solo podía ser falsa. Una regla que nadie puede satisfacer no es una regla más estricta. **Las specs y las fixtures de paridad también codificaban el layout viejo** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), y por eso llevaban tiempo en verde sobre rutas que no existen: la fixture y el código coincidían entre sí y ninguno coincidía con el repositorio. Ambas corregidas. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterio 3 CERRADO el 2026-07-29 — la primera corrida del guard estuvo casi toda equivocada, que es el hallazgo.** `47-validate-joined-paths` reportó 19 rutas rotas; solo 3 eran defectos. Cada una de las otras 16 habría empeorado el código: seis tenían base `root`, un PARÁMETRO que nombra el workspace del cliente, así que corregirlas rompería la evaluación de todos los satélites; nueve eran miembros de CADENAS DE FALLBACK correctas cuando uno de cuatro layouts resuelve, y la forma natural de callarlas es borrar los fallbacks, rompiendo imágenes desplegadas; una era una PROHIBICIÓN cuya ausencia ES el estado que aprueba, así que corregirla habría invertido la regla. Las cadenas se detectan ahora estructuralmente (hermanos de array, o las ramas de un ternario); las prohibiciones no pueden, y llevan razón escrita. Los tres defectos reales: `opa-evaluator.ts` resolvía `policy.wasm` y los esquemas de entrada sin el prefijo `src/`, misma clase fail-closed que el P0 de arriba; `satellite-upgrade-diff.ts` sondeaba `rulesets` detrás de un return temprano, reportando cero cambios de rulesets en cada upgrade de satélite; `artifact-path-resolver.ts` nombraba un `adr-matrix.json` que no existe en ningún layout. 11 fixtures negativas, los dos suelos anti-vacuos, y `43-validate-guard-negative-fixtures` lo OBSERVÓ en rojo sobre 36 guards. Una indulgencia conocida (dos joins como argumentos hermanos) queda anotada en su cabecera en vez de escondida. | | | `MCP Server` | Cross | P0 | M | `COMPLETADO` | | [`GT-631`](./gap-reference-catalog.es.md#gt-631) | **Extraído de [`GT-573`](./gap-reference-catalog.es.md#gt-573) para que su mitad cross-repo se siga en vez de absorberse en un cierre.** La mitad del Core del tercer criterio de GT-573 está hecha: `evaluation-context` y `evaluation-result` están fijados en `MACHINE_CONTRACT_SET` junto a `gate-evidence` y `output-envelope`, así que un cambio en cualquiera de esas formas ya rompe `10-validate-contract-conformance` en vez de llegar en silencio a los consumidores. Lo que queda está en otro repositorio y no se puede hacer desde aquí: el **Tracker** debe re-fijar esos dos esquemas en su propio `contracts/evolith-core-contracts.json` con su sha256, enlazar las tres fixtures publicadas (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) en un test de binding de DTOs, quitar o documentar `resolvedTopology` (que el resultado canónico nunca llevó), renombrar su `phase` de gate a `phaseId`, y eliminar su caída a `SKIPPED` para un payload que trae un `overallVerdict` no vacío. Hasta entonces, un cambio de forma del lado Core se caza aquí y sigue sorprendiendo al consumidor. | | | `Evolith Tracker` | Cross | P1 | S | `PENDIENTE` | @@ -648,7 +650,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-246`](./gap-reference-catalog.es.md#gt-246) | Implementar experimentos Chaos Mesh/Litmus | | | `QA` | Cross | P3 | L | `COMPLETADO` | -**Progreso:** 593 / 633 completados · 14 en progreso · 22 pendientes · 4 diferidos +**Progreso:** 593 / 635 completados · 14 en progreso · 24 pendientes · 4 diferidos **Oleada 2026-06-23 (auditoría profunda de Winston III):** Añadidos 14 gaps nuevos `GT-212`…`GT-225` del Winston Audit Playbook que cubren: higiene de estado ADR (GT-212), metadata + presupuestos operativos + corpus de guías por topología (GT-213, GT-217, GT-219), observabilidad + OpenAPI en controladores REST (GT-214, GT-215), paridad de input-schemas OPA + densidad de tests por topología (GT-216, GT-222), plantillas de rollback + on-call de Fase 05 (GT-218), cobertura de ramas CLI + paridad de envelope --format + limpieza de skip-list (GT-220, GT-224, GT-225), audit logging HTTP de MCP (GT-221), y tests e2e de paridad cross-surface (GT-223). diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 8742f94d..0f7b7678 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -13,6 +13,8 @@ This board is the single source of truth for technical debt, gaps, opportunities | ID | Gap | What it means | Example | Component | Phase | Criticality | Complexity | Status | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-634`](./gap-reference-catalog.md#gt-634) | **The published CLI and MCP resolve an SDK build that predates the security wave, because a caret range under 1.x pins it.** `@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that resolves to **exactly `1.1.0`** — the SDK publishes `1.0.0`, `1.1.0` and `2.0.0` and nothing in between, and a caret cannot cross a major. The dates are the finding: `sdk@1.1.0` was published 2026-07-18, `sdk@2.0.0` on 2026-07-27 as part of the release that closed [`GT-570`](./gap-reference-catalog.md#gt-570), and both consumers on 2026-07-28 — **after** 2.0.0 existed, still on the 1.x line. So the exposure GT-570 declared closed for "anyone installing `latest`" is not closed for this dependency. Stated precisely, because the stronger claim is unverified here: what is measured is the dates and the resolution, not the contents — this row does not assert that `sdk@1.1.0` carries a specific vulnerability. Found while executing [`GT-624`](./gap-reference-catalog.md#gt-624)'s deprecation criterion, which is what exposed the range: deprecating `sdk@1.1.0` would warn on every fresh install of the current line with no 1.x successor to offer. | | | `Infra` | Cross | P1 | S | `PENDING` | +| [`GT-635`](./gap-reference-catalog.md#gt-635) | **The dead-reference ratchet has been over budget on main since 2026-07-28, so the governance job is permanently red.** `41-validate-evidence-commands --strict --max-dead 305` reports **309 dead references on a clean checkout**, so `Governance guards (GT-578)` exits 1 on every run of `ci-cd.yml` — observed on runs `30482497995`, `30470648146` and `30466280865`, all on `main` with nothing unmerged. The budget was set in `4a1b92d6` "where it was measured"; ten commits have edited `gap-closure-evidence.json` since, each adding `validationCommands`, and the count drifted past it. The ratchet's whole purpose is that dead references must not GROW, and it is reporting exactly that while being ignored, because a permanently red job reads as background noise — the same failure mode [`GT-622`](./gap-reference-catalog.md#gt-622) records for the orphaned CodeQL configuration. The next change that genuinely adds a dead reference will be indistinguishable from the four already there. **What not to do:** raise the budget to 309, which converts a breached ratchet into a ratified one. | | | `Governance` | Cross | P2 | S | `PENDING` | | [`GT-633`](./gap-reference-catalog.md#gt-633) | **The guard compared the snapshot against six numbers copied out of the snapshot, so it could only ever pass — and the file it was failing to guard is stamped onto 388 rows of a larger artifact.** `native-evaluability-snapshot.json` declared in its own header that it was "a CAPTURED SNAPSHOT, not the source of truth". **No capture script existed.** It was hand-maintained and it drifted: it pinned `documentation-only: 129` while Core pinned 136, and went on calling rules `unimplemented-native` after they had handlers. Its guard in `iso-5055-mapping.test.mjs` asserted the snapshot's six class counts against six numbers typed into the test — the same six literals the snapshot already contained. **Expected value and actual value were copies of each other**, so the assertion held for as long as the file was unchanged, whatever Core actually pinned; it reported an agreement it never checked, which is worse than no guard, because the row it protects reads as verified. **The drift does not stay where it starts.** `build-iso-5055-mapping.mjs` stamps `nativeEvaluability` onto EVERY row of the ISO/IEC 5055 mapping from that file — 388 rows after the recapture, 381 before — so one stale class is laundered into a derived artifact five times the size of its input, and overstates the handler backlog that is [`GT-598`](./gap-reference-catalog.md#gt-598)'s entire deliverable. The recapture→rebuild order was written down nowhere, and the mapping's own guard ran in **no workflow at all**. **FIXED 2026-07-29:** a capture script that runs Core's REAL triage through `ts-node` instead of reimplementing the classification, the measurement extracted out of the jest spec into `test/rule-corpus-triage.ts` so a script can reach it — that unreachability is why the file was maintained by hand — snapshot-vs-fresh-triage assertions in both directions, a documentation-job guard that reads Core's pinned counts OUT of the spec and **throws** when the literal is missing or reshaped, and both steps declared as links in the [`GT-630`](./gap-reference-catalog.md#gt-630) derived-artifact chain. **Recaptured on 2026-07-29 after merging the fix into the current line, and the drift had grown while it was in flight:** `native-handler` 139 → 151, `documentation-only` 129 → 136, the handler backlog 60 → **48**, corpus 379 → 386, mapping 381 → 388 rows. Twelve of those handlers were closed by [`GT-595`](./gap-reference-catalog.md#gt-595) and [`GT-632`](./gap-reference-catalog.md#gt-632) while the hand-maintained snapshot went on reporting them as backlog — the defect demonstrating itself one more time, on numbers nobody typed twice. | | | `Governance` | Cross | P1 | S | `DONE` | | [`GT-632`](./gap-reference-catalog.md#gt-632) | **The MCP denied every tool in production from a path typo, and the evidence that it worked was untracked local state.** `abac-evaluator.ts:322` resolved the compiled policy at `/sdk/cli/rulesets/opa/policy.wasm` — the PRE-`src/`-refactor path. The file lives at `src/sdk/cli/…`. When the stat fails the evaluator denies fail-closed in production, so on a clean checkout with `NODE_ENV=production` the compiled policy never loads and EVERY MCP tool is refused. It looked green because `policy.wasm` is gitignored: a stale file left at the old path by an earlier era made the load succeed locally, and the test that covers it passed against an artifact no fresh checkout would have. Same shape as `GT-625` — a green that depended on untracked state — but on the authorization path. Found on 2026-07-29 while investigating why `abac-rego-parity.spec.ts` had gone red; the rego itself was correct (`opa eval` returns `allow: true` for an architect in production). **`40-validate-path-literals` cannot see this class:** it scans string literals, and this is a `path.join(corePath, 'sdk', 'cli', …)` construction. Twelve such joins survive the refactor across `src/**` — the other eleven are in `opa-input-builder.ts` and `cli-release-rule.handler.ts`, where a wrong path produces a false NEGATIVE in an evaluator rather than a denial, which is quieter and no less wrong. **FIXED for the authorization path on 2026-07-29:** the evaluator now tries both locations, newest first — not sloppiness, but what keeps an already-deployed image working while the path is corrected. Reverting the fix turns `abac-rego-parity.spec.ts` red (1 of 11); with it, mcp-server is 434/434. The other eleven joins and the guard blind spot remain. **Criterion 2 CLOSED 2026-07-29 — and two of the eleven were wrong in a way the prefix alone would not have fixed.** Eight were plain survivors of the `src/` move and took the prefix. The other three were worse: two pointed at `sdk/cli/src/core/mcp/server.ts`, a file that left the CLI entirely when mcp-server became its own package (now `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); and one asserted a per-package `package-lock.json`, which npm WORKSPACES forbids — there is exactly one lockfile, at the root, so that check could only ever be false. A rule nobody can satisfy is not a stricter rule. **The specs and parity fixtures encoded the old layout too** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), which is why they had stayed green over paths that do not exist: the fixture and the code agreed with each other and neither agreed with the repository. Both are corrected. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterion 3 CLOSED 2026-07-29 — the guard's first run was mostly wrong, which is the finding.** `47-validate-joined-paths` reported 19 broken paths; only 3 were defects. The other 16 would each have been made WORSE by a fix: six had base `root`, a PARAMETER naming the customer workspace under evaluation, so correcting them would break evaluation for every satellite; nine were members of FALLBACK CHAINS that are correct when one of four layouts resolves, and the natural way to silence those is to delete the fallbacks, breaking deployed images; one was a PROHIBITION whose absence IS the passing state, so correcting it would have inverted the rule. Chains are now detected structurally (array siblings, or the arms of a ternary); prohibitions cannot be, so they carry a written reason. The three real defects: `opa-evaluator.ts` resolved `policy.wasm` and the input schemas without the `src/` prefix, same fail-closed class as the P0 above; `satellite-upgrade-diff.ts` probed `rulesets` behind an early return, reporting zero ruleset changes on every satellite upgrade; `artifact-path-resolver.ts` named an `adr-matrix.json` present in no layout. 11 negative fixtures, both anti-vacuous floors, and `43-validate-guard-negative-fixtures` OBSERVED it red across 36 guards. A known leniency (two joins as sibling arguments) is recorded in its header rather than hidden. | | | `MCP Server` | Cross | P0 | M | `DONE` | | [`GT-631`](./gap-reference-catalog.md#gt-631) | **Carved out of [`GT-573`](./gap-reference-catalog.md#gt-573) so its cross-repo half is tracked rather than absorbed into a closure.** The Core half of GT-573's third criterion is done: `evaluation-context` and `evaluation-result` are pinned in `MACHINE_CONTRACT_SET` alongside `gate-evidence` and `output-envelope`, so a change to either shape now fails `10-validate-contract-conformance` instead of silently reaching consumers. What remains is in another repository and cannot be done from here: the **Tracker** must re-pin those two schemas in its own `contracts/evolith-core-contracts.json` with their sha256, bind the three published fixtures (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) in a DTO binding test, drop or document `resolvedTopology` (which the canonical result never carried), rename its gate `phase` to `phaseId`, and delete its fall-through to `SKIPPED` for a payload carrying a non-blank `overallVerdict`. Until it does, a Core-side shape change is caught here and still surprises the consumer. | | | `Evolith Tracker` | Cross | P1 | S | `PENDING` | @@ -648,7 +650,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-246`](./gap-reference-catalog.md#gt-246) | Implement Chaos Mesh/Litmus experiments | | | `QA` | Cross | P3 | L | `DONE` | -**Progress:** 593 / 633 done · 14 in progress · 22 pending · 4 deferred +**Progress:** 593 / 635 done · 14 in progress · 24 pending · 4 deferred **Wave 2026-06-23 (Winston deep audit III):** Added 14 new gaps `GT-212`…`GT-225` from the Winston Audit Playbook covering: ADR status hygiene (GT-212), topology manifest metadata + operational budgets + guidance corpus (GT-213, GT-217, GT-219), REST controller observability + OpenAPI (GT-214, GT-215), OPA input-schema parity + per-topology test density (GT-216, GT-222), SDLC Phase 05 rollback + on-call templates (GT-218), CLI branch coverage + envelope format coverage + skip-list cleanup (GT-220, GT-224, GT-225), MCP HTTP audit logging (GT-221), and cross-surface parity e2e tests (GT-223). diff --git a/reference/core/control-center/maturity-reports/executive-summary.es.md b/reference/core/control-center/maturity-reports/executive-summary.es.md index 625ef614..634e9594 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -11,7 +11,7 @@ Instantánea estratégica generada desde el tablero canónico de gaps y la recon **Decisión actual:** NO-GO para expansión productiva o release mayor: existen bloqueadores P0 activos. -**Mayor problema ahora:** `Evolith Core` concentra el mayor riesgo abierto ponderado (8 pendientes, 0 P0). Ataca esa concentración antes de ampliar alcance. +**Mayor problema ahora:** `Infra` concentra el mayor riesgo abierto ponderado (6 pendientes, 0 P0). Ataca esa concentración antes de ampliar alcance. **Dónde atacar primero:** [GT-602](../gaps/gap-reference-catalog.es.md#gt-602), [GT-603](../gaps/gap-reference-catalog.es.md#gt-603), [GT-604](../gaps/gap-reference-catalog.es.md#gt-604), [GT-435](../gaps/gap-reference-catalog.es.md#gt-435). @@ -26,10 +26,10 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Orden | Foco | Motivo | IDs | |---:|---|---|---| | 1 | Bloqueadores P0 | Impiden afirmar readiness productivo o release mayor. | [GT-602](../gaps/gap-reference-catalog.es.md#gt-602), [GT-603](../gaps/gap-reference-catalog.es.md#gt-603), [GT-604](../gaps/gap-reference-catalog.es.md#gt-604), [GT-435](../gaps/gap-reference-catalog.es.md#gt-435) | -| 2 | Área de mayor riesgo | `Evolith Core` tiene la mayor carga ponderada abierta. | [GT-583](../gaps/gap-reference-catalog.es.md#gt-583), [GT-589](../gaps/gap-reference-catalog.es.md#gt-589), [GT-614](../gaps/gap-reference-catalog.es.md#gt-614), [GT-587](../gaps/gap-reference-catalog.es.md#gt-587), [GT-591](../gaps/gap-reference-catalog.es.md#gt-591), [GT-590](../gaps/gap-reference-catalog.es.md#gt-590), +2 | -| 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-631](../gaps/gap-reference-catalog.es.md#gt-631) | -| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-631](../gaps/gap-reference-catalog.es.md#gt-631), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-578](../gaps/gap-reference-catalog.es.md#gt-578), [GT-580](../gaps/gap-reference-catalog.es.md#gt-580), [GT-584](../gaps/gap-reference-catalog.es.md#gt-584), [GT-605](../gaps/gap-reference-catalog.es.md#gt-605), +7 | -| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-614](../gaps/gap-reference-catalog.es.md#gt-614), [GT-616](../gaps/gap-reference-catalog.es.md#gt-616), [GT-617](../gaps/gap-reference-catalog.es.md#gt-617), +12 | +| 2 | Área de mayor riesgo | `Infra` tiene la mayor carga ponderada abierta. | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-634](../gaps/gap-reference-catalog.es.md#gt-634), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464) | +| 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-631](../gaps/gap-reference-catalog.es.md#gt-631), [GT-634](../gaps/gap-reference-catalog.es.md#gt-634) | +| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-631](../gaps/gap-reference-catalog.es.md#gt-631), [GT-634](../gaps/gap-reference-catalog.es.md#gt-634), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-578](../gaps/gap-reference-catalog.es.md#gt-578), [GT-580](../gaps/gap-reference-catalog.es.md#gt-580), [GT-584](../gaps/gap-reference-catalog.es.md#gt-584), +8 | +| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-614](../gaps/gap-reference-catalog.es.md#gt-614), [GT-616](../gaps/gap-reference-catalog.es.md#gt-616), [GT-617](../gaps/gap-reference-catalog.es.md#gt-617), +13 | ## Bloqueadores Actuales @@ -45,20 +45,20 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| | Fecha canónica del tablero | 2026-07-26 | -| Gaps totales | 633 | +| Gaps totales | 635 | | Gaps cerrados | 593 | -| Gaps pendientes | 40 | +| Gaps pendientes | 42 | | P0 abiertos | 4 | -| P1 abiertos | 15 | -| P2 abiertos | 18 | -| Cierre total | 93.7% | +| P1 abiertos | 16 | +| P2 abiertos | 19 | +| Cierre total | 93.4% | | Registros de evidencia de cierre | 575 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | |---|---:|---:|---:|---| +| `Infra` | 6 | 0 | 4 | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-634](../gaps/gap-reference-catalog.es.md#gt-634), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), +2 | | `Evolith Core` | 8 | 0 | 2 | [GT-583](../gaps/gap-reference-catalog.es.md#gt-583), [GT-589](../gaps/gap-reference-catalog.es.md#gt-589), [GT-614](../gaps/gap-reference-catalog.es.md#gt-614), [GT-587](../gaps/gap-reference-catalog.es.md#gt-587), +4 | -| `Infra` | 5 | 0 | 3 | [GT-597](../gaps/gap-reference-catalog.es.md#gt-597), [GT-624](../gaps/gap-reference-catalog.es.md#gt-624), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), +1 | | `Cross` | 2 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448) | | `Evolith MCP` | 2 | 1 | 1 | [GT-602](../gaps/gap-reference-catalog.es.md#gt-602), [GT-606](../gaps/gap-reference-catalog.es.md#gt-606) | | `Evolith Suite` | 2 | 1 | 1 | [GT-604](../gaps/gap-reference-catalog.es.md#gt-604), [GT-605](../gaps/gap-reference-catalog.es.md#gt-605) | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 59f0f07b..b519cc42 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -11,7 +11,7 @@ Strategic snapshot generated from the canonical gap board and maturity reconcili **Current decision:** NO-GO for production expansion or a major release: active P0 blockers remain. -**Biggest problem now:** `Evolith Core` carries the highest weighted open risk (8 open, 0 P0). Attack that concentration before expanding scope. +**Biggest problem now:** `Infra` carries the highest weighted open risk (6 open, 0 P0). Attack that concentration before expanding scope. **Where to attack first:** [GT-602](../gaps/gap-reference-catalog.md#gt-602), [GT-603](../gaps/gap-reference-catalog.md#gt-603), [GT-604](../gaps/gap-reference-catalog.md#gt-604), [GT-435](../gaps/gap-reference-catalog.md#gt-435). @@ -26,10 +26,10 @@ Use this summary with a simple rule: if you need context, open only the linked I | Order | Focus | Reason | IDs | |---:|---|---|---| | 1 | P0 blockers | They prevent production-readiness or major-release confidence. | [GT-602](../gaps/gap-reference-catalog.md#gt-602), [GT-603](../gaps/gap-reference-catalog.md#gt-603), [GT-604](../gaps/gap-reference-catalog.md#gt-604), [GT-435](../gaps/gap-reference-catalog.md#gt-435) | -| 2 | Highest-risk area | `Evolith Core` has the largest weighted open load. | [GT-583](../gaps/gap-reference-catalog.md#gt-583), [GT-589](../gaps/gap-reference-catalog.md#gt-589), [GT-614](../gaps/gap-reference-catalog.md#gt-614), [GT-587](../gaps/gap-reference-catalog.md#gt-587), [GT-591](../gaps/gap-reference-catalog.md#gt-591), [GT-590](../gaps/gap-reference-catalog.md#gt-590), +2 | -| 3 | Quick wins | High criticality with XS/S complexity. | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-631](../gaps/gap-reference-catalog.md#gt-631) | -| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-631](../gaps/gap-reference-catalog.md#gt-631), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-578](../gaps/gap-reference-catalog.md#gt-578), [GT-580](../gaps/gap-reference-catalog.md#gt-580), [GT-584](../gaps/gap-reference-catalog.md#gt-584), [GT-605](../gaps/gap-reference-catalog.md#gt-605), +7 | -| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-622](../gaps/gap-reference-catalog.md#gt-622), [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-614](../gaps/gap-reference-catalog.md#gt-614), [GT-616](../gaps/gap-reference-catalog.md#gt-616), [GT-617](../gaps/gap-reference-catalog.md#gt-617), +12 | +| 2 | Highest-risk area | `Infra` has the largest weighted open load. | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-634](../gaps/gap-reference-catalog.md#gt-634), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-622](../gaps/gap-reference-catalog.md#gt-622), [GT-464](../gaps/gap-reference-catalog.md#gt-464) | +| 3 | Quick wins | High criticality with XS/S complexity. | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-631](../gaps/gap-reference-catalog.md#gt-631), [GT-634](../gaps/gap-reference-catalog.md#gt-634) | +| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-631](../gaps/gap-reference-catalog.md#gt-631), [GT-634](../gaps/gap-reference-catalog.md#gt-634), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-578](../gaps/gap-reference-catalog.md#gt-578), [GT-580](../gaps/gap-reference-catalog.md#gt-580), [GT-584](../gaps/gap-reference-catalog.md#gt-584), +8 | +| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-622](../gaps/gap-reference-catalog.md#gt-622), [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-614](../gaps/gap-reference-catalog.md#gt-614), [GT-616](../gaps/gap-reference-catalog.md#gt-616), [GT-617](../gaps/gap-reference-catalog.md#gt-617), +13 | ## Current Blockers @@ -45,20 +45,20 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| | Canonical board date | 2026-07-26 | -| Total gaps | 633 | +| Total gaps | 635 | | Closed gaps | 593 | -| Open gaps | 40 | +| Open gaps | 42 | | Open P0 | 4 | -| Open P1 | 15 | -| Open P2 | 18 | -| Total closure | 93.7% | +| Open P1 | 16 | +| Open P2 | 19 | +| Total closure | 93.4% | | Closure evidence records | 575 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | |---|---:|---:|---:|---| +| `Infra` | 6 | 0 | 4 | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-634](../gaps/gap-reference-catalog.md#gt-634), [GT-324](../gaps/gap-reference-catalog.md#gt-324), +2 | | `Evolith Core` | 8 | 0 | 2 | [GT-583](../gaps/gap-reference-catalog.md#gt-583), [GT-589](../gaps/gap-reference-catalog.md#gt-589), [GT-614](../gaps/gap-reference-catalog.md#gt-614), [GT-587](../gaps/gap-reference-catalog.md#gt-587), +4 | -| `Infra` | 5 | 0 | 3 | [GT-597](../gaps/gap-reference-catalog.md#gt-597), [GT-624](../gaps/gap-reference-catalog.md#gt-624), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-622](../gaps/gap-reference-catalog.md#gt-622), +1 | | `Cross` | 2 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.md#gt-435), [GT-448](../gaps/gap-reference-catalog.md#gt-448) | | `Evolith MCP` | 2 | 1 | 1 | [GT-602](../gaps/gap-reference-catalog.md#gt-602), [GT-606](../gaps/gap-reference-catalog.md#gt-606) | | `Evolith Suite` | 2 | 1 | 1 | [GT-604](../gaps/gap-reference-catalog.md#gt-604), [GT-605](../gaps/gap-reference-catalog.md#gt-605) | diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index aecda681..a7246804 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -3,9 +3,9 @@ "scope": "evolith-core", "asOf": "2026-07-26", "gaps": { - "total": 633, + "total": 635, "done": 593, - "pending": 22, + "pending": 24, "inProgress": 14, "deferred": 4 }, From 14aed93e0a51a52f796d10c657c4a2834f19a7f2 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 18:08:58 -0500 Subject: [PATCH 08/10] fix(deps): the CLI and MCP must resolve the SDK that carries the security wave (GT-634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@beyondnet/evolith-sdk: ^1.1.0` -> `^2.0.0` in `src/sdk/cli/package.json` and `src/packages/mcp-server/package.json`. The SDK publishes 1.0.0, 1.1.0 and 2.0.0 with nothing in between, so a caret under 1.x pinned EXACTLY 1.1.0 — published 2026-07-18, five days before the security wave that 2.0.0 shipped. DOING IT PROVED THE ROW UNDERSTATED THE DEFECT. package-lock.json did not merely record the range; it pinned the registry TARBALL of sdk@1.1.0, with integrity hashes, at both: src/sdk/cli/node_modules/@beyondnet/evolith-sdk src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk So `npm ci` on a clean checkout fetched the pre-wave SDK and nested it inside each consumer, where it SHADOWS the workspace link: the top-level symlink to the local 2.0.0 is what every developer reads, and the nested 1.1.0 is what a fresh install actually resolves. Same class as GT-625 — the workspace hiding what a real install does — and the reason nothing caught this. Both nested entries disappear once the range is ^2.0.0, because the workspace's own 2.0.0 then satisfies it. The lockfile diff is exactly two range strings plus those two removals, with no collateral drift: the hand edit was made first, then `npm install --package-lock-only` was run to confirm npm writes byte-for-byte the same thing, rather than trusting the edit. The SDK surface both consumers use is narrow — `EvolithRestClient` only — so 2.0.0's breaking change (payload types re-exported from core-domain, `.passed` -> `.verdict`, `'info'` severity retired) touches neither call site. Verified against 2.0.0, resolution confirmed by require.resolve rather than assumed: CLI 1436/1436 in 99 suites, mcp-server 433/433 in 53 suites. Board validators green: 08 (635 gaps, 611/611 EN/ES), 04, 01 (1491 files), 46 chain at a fixed point. `tsc --noEmit` on mcp-server reports two errors for `@nestjs/cache-manager`, which is a worktree artifact (that dependency is nested in the main checkout's tree, which this worktree symlinks around) and unrelated to this change. GT-634 stays OPEN: a range in the repository is not a range on the registry, and its first criterion is only met by a release. Co-Authored-By: Claude Opus 5 --- package-lock.json | 16 ++-------------- .../gaps/gap-reference-catalog.es.md | 2 +- .../control-center/gaps/gap-reference-catalog.md | 2 +- src/packages/mcp-server/package.json | 2 +- src/sdk/cli/package.json | 2 +- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index a443ffed..74e58d85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16583,7 +16583,7 @@ "@beyondnet/evolith-core": "^1.2.0", "@beyondnet/evolith-core-domain": "^1.2.0", "@beyondnet/evolith-infra-providers": "^1.2.0", - "@beyondnet/evolith-sdk": "^1.1.0", + "@beyondnet/evolith-sdk": "^2.0.0", "@modelcontextprotocol/sdk": "1.29.0", "@nestjs/cache-manager": "3.1.3", "@nestjs/common": "11.1.27", @@ -16633,12 +16633,6 @@ "node": ">=20.0.0" } }, - "src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@beyondnet/evolith-sdk/-/evolith-sdk-1.1.0.tgz", - "integrity": "sha512-x53iNqKqiNOh13KcxqGCfI52T7X6BO7qXR8PMpUUFrAofwK2cZeIMFQ16Ku2P+GhLsQygQwofx4poB+3qM0F2w==", - "license": "MIT" - }, "src/packages/mcp-server/node_modules/@nestjs/cache-manager": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.1.3.tgz", @@ -16898,7 +16892,7 @@ "@beyondnet/evolith-agent-runtime": "^1.1.0", "@beyondnet/evolith-core-domain": "^1.2.0", "@beyondnet/evolith-infra-providers": "^1.2.0", - "@beyondnet/evolith-sdk": "^1.1.0", + "@beyondnet/evolith-sdk": "^2.0.0", "@clack/prompts": "1.5.1", "@hono/node-server": "1.19.14", "@modelcontextprotocol/sdk": "1.29.0", @@ -16949,12 +16943,6 @@ "node": ">=18.0.0" } }, - "src/sdk/cli/node_modules/@beyondnet/evolith-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@beyondnet/evolith-sdk/-/evolith-sdk-1.1.0.tgz", - "integrity": "sha512-x53iNqKqiNOh13KcxqGCfI52T7X6BO7qXR8PMpUUFrAofwK2cZeIMFQ16Ku2P+GhLsQygQwofx4poB+3qM0F2w==", - "license": "MIT" - }, "src/sdk/cli/node_modules/@typescript-eslint/parser": { "version": "8.64.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 8f7f501c..d2079245 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7873,7 +7873,7 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre **Title:** El CLI y el MCP publicados resuelven una build del SDK anterior a la ola de seguridad, porque un rango caret bajo 1.x la fija - **Purpose:** Que `npm install @beyondnet/evolith-cli@latest` deje de arrastrar una dependencia publicada antes de los fixes que anuncia su propio CHANGELOG. -- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` y `@beyondnet/evolith-mcp@1.2.1` declaran ambos `@beyondnet/evolith-sdk: ^1.1.0`, y eso resuelve EXACTAMENTE `1.1.0`.** El SDK publica `1.0.0`, `1.1.0` y `2.0.0` y nada en medio, así que un rango caret bajo 1.x no puede alcanzar 2.0.0 — un caret está acotado por el siguiente mayor. Las fechas son el hallazgo: `sdk@1.1.0` se publicó el **2026-07-18**, `sdk@2.0.0` el **2026-07-27** como parte de la release que cerró [`GT-570`](./gap-reference-catalog.es.md#gt-570), y `cli@1.2.1` / `mcp@1.2.1` el **2026-07-28** — *después* de que 2.0.0 existiera, y seguían apuntando a la línea 1.x. Así que la exposición que GT-570 declaró cerrada para "quien instale `latest`" no está cerrada para esta dependencia: el `latest` de ambas superficies instala un SDK construido antes de la ola del 2026-07-23. **Dicho con precisión, porque la afirmación fuerte no está verificada aquí:** lo medido son las fechas y la resolución, no el contenido — esta fila no afirma que `sdk@1.1.0` cargue una vulnerabilidad concreta, solo que es anterior a la ola y que la release que la trajo fue un mayor al que los consumidores no llegan. Verificar qué fixes hay en `sdk@2.0.0` es parte de cerrarla. **Encontrado porque bloqueaba otra cosa:** deprecar `sdk@1.1.0` para [`GT-624`](./gap-reference-catalog.es.md#gt-624) habría impreso un aviso en cada instalación limpia de la línea actual sin ningún sucesor 1.x que ofrecer, y eso es lo que dejó el rango a la vista. Arreglo: reapuntar ambos rangos a `^2.0.0` (un salto mayor de dependencia, así que exige revisar los breaking changes del SDK contra ambos call sites) o publicar un `sdk@1.2.x` con los fixes, y entonces deprecar `sdk@1.1.0` y cerrar de verdad el primer criterio de GT-624. +- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` y `@beyondnet/evolith-mcp@1.2.1` declaran ambos `@beyondnet/evolith-sdk: ^1.1.0`, y eso resuelve EXACTAMENTE `1.1.0`.** El SDK publica `1.0.0`, `1.1.0` y `2.0.0` y nada en medio, así que un rango caret bajo 1.x no puede alcanzar 2.0.0 — un caret está acotado por el siguiente mayor. Las fechas son el hallazgo: `sdk@1.1.0` se publicó el **2026-07-18**, `sdk@2.0.0` el **2026-07-27** como parte de la release que cerró [`GT-570`](./gap-reference-catalog.es.md#gt-570), y `cli@1.2.1` / `mcp@1.2.1` el **2026-07-28** — *después* de que 2.0.0 existiera, y seguían apuntando a la línea 1.x. Así que la exposición que GT-570 declaró cerrada para "quien instale `latest`" no está cerrada para esta dependencia: el `latest` de ambas superficies instala un SDK construido antes de la ola del 2026-07-23. **Dicho con precisión, porque la afirmación fuerte no está verificada aquí:** lo medido son las fechas y la resolución, no el contenido — esta fila no afirma que `sdk@1.1.0` cargue una vulnerabilidad concreta, solo que es anterior a la ola y que la release que la trajo fue un mayor al que los consumidores no llegan. Verificar qué fixes hay en `sdk@2.0.0` es parte de cerrarla. **Encontrado porque bloqueaba otra cosa:** deprecar `sdk@1.1.0` para [`GT-624`](./gap-reference-catalog.es.md#gt-624) habría impreso un aviso en cada instalación limpia de la línea actual sin ningún sucesor 1.x que ofrecer, y eso es lo que dejó el rango a la vista. Arreglo: reapuntar ambos rangos a `^2.0.0` (un salto mayor de dependencia, así que exige revisar los breaking changes del SDK contra ambos call sites) o publicar un `sdk@1.2.x` con los fixes, y entonces deprecar `sdk@1.1.0` y cerrar de verdad el primer criterio de GT-624. **RANGOS REAPUNTADOS el 2026-07-29, y hacerlo demostró que la fila se quedaba corta.** `package-lock.json` no sólo registraba el rango — fijaba el tarball del registry de `sdk@1.1.0` en `src/sdk/cli/node_modules/@beyondnet/evolith-sdk` **y** en `src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk`, con hashes de integridad. Así que un `npm ci` en checkout limpio descargaba el SDK anterior a la ola y lo anidaba dentro de cada consumidor, donde **tapa el enlace del workspace**: el symlink de primer nivel al 2.0.0 local es lo que lee todo el mundo, y el 1.1.0 anidado es lo que resuelve de verdad una instalación fresca. Es la misma clase que [`GT-625`](./gap-reference-catalog.es.md#gt-625) — el workspace tapando lo que hace una instalación real — y es la razón de que nada lo cazara. Ambas entradas anidadas desaparecieron al mover el rango a `^2.0.0`, porque entonces el propio 2.0.0 del workspace lo satisface; el diff del lockfile son exactamente dos cadenas de rango y esas dos eliminaciones, sin deriva colateral, y se usó `npm install --package-lock-only` para confirmar que escribe byte a byte lo mismo que la edición a mano en vez de fiarse de ella. La superficie del SDK que ambos consumidores usan de verdad es estrecha — sólo `EvolithRestClient` — así que el breaking change de 2.0.0 (tipos de payload re-exportados de `core-domain`, `.passed` → `.verdict`, severidad `'info'` retirada) no toca ninguno de los dos call sites. Verificado contra 2.0.0: CLI **1436/1436** en 99 suites, mcp-server **433/433** en 53. La fila sigue ABIERTA: un rango en el repositorio no es un rango en el registry, y el criterio 1 sólo se cumple con una release. - **Component:** `Infra` · **Criticality:** P1 · **Complexity:** S - **Provenance:** Encontrado el 2026-07-29 ejecutando el criterio de deprecación de GT-624. Registrado en vez de sorteado: saltarse la deprecación en silencio habría dejado el rango — y la instalación — exactamente como están. - **Acceptance criteria:** diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index b2e4c1ab..11a15d9c 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7968,7 +7968,7 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo **Title:** The published CLI and MCP resolve an SDK build that predates the security wave, because a caret range under 1.x pins it - **Purpose:** Make `npm install @beyondnet/evolith-cli@latest` stop pulling a dependency published before the fixes its own CHANGELOG announces. -- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that resolves to EXACTLY `1.1.0`.** The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` and nothing in between, so a caret range under 1.x cannot reach 2.0.0 — a caret is bounded by the next major. The dates are the finding: `sdk@1.1.0` was published **2026-07-18**, `sdk@2.0.0` on **2026-07-27** as part of the release that closed [`GT-570`](./gap-reference-catalog.md#gt-570), and `cli@1.2.1` / `mcp@1.2.1` on **2026-07-28** — *after* 2.0.0 existed, still pointing at the 1.x line. So the exposure GT-570 declared closed for "anyone installing `latest`" is not closed for this dependency: `latest` of both surfaces installs an SDK built before the 2026-07-23 wave. **Stated precisely, because the stronger claim is not verified here:** what is measured is the dates and the resolution, not the contents — this row does not assert that `sdk@1.1.0` carries a specific vulnerability, only that it predates the wave and that the release which shipped the wave was a major the consumers cannot reach. Verifying which fixes are in `sdk@2.0.0` is part of closing it. **Found because it blocked something else:** deprecating `sdk@1.1.0` for [`GT-624`](./gap-reference-catalog.md#gt-624) would have printed a warning on every fresh install of the current line with no 1.x successor to offer, which is what exposed the range. Fix: retarget both ranges to `^2.0.0` (a major bump of a dependency, so it needs the SDK's breaking changes reviewed against both call sites) or publish an `sdk@1.2.x` carrying the fixes, then deprecate `sdk@1.1.0` and close GT-624's first criterion for real. +- **Evidence:** **`@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both declare `@beyondnet/evolith-sdk: ^1.1.0`, and that resolves to EXACTLY `1.1.0`.** The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` and nothing in between, so a caret range under 1.x cannot reach 2.0.0 — a caret is bounded by the next major. The dates are the finding: `sdk@1.1.0` was published **2026-07-18**, `sdk@2.0.0` on **2026-07-27** as part of the release that closed [`GT-570`](./gap-reference-catalog.md#gt-570), and `cli@1.2.1` / `mcp@1.2.1` on **2026-07-28** — *after* 2.0.0 existed, still pointing at the 1.x line. So the exposure GT-570 declared closed for "anyone installing `latest`" is not closed for this dependency: `latest` of both surfaces installs an SDK built before the 2026-07-23 wave. **Stated precisely, because the stronger claim is not verified here:** what is measured is the dates and the resolution, not the contents — this row does not assert that `sdk@1.1.0` carries a specific vulnerability, only that it predates the wave and that the release which shipped the wave was a major the consumers cannot reach. Verifying which fixes are in `sdk@2.0.0` is part of closing it. **Found because it blocked something else:** deprecating `sdk@1.1.0` for [`GT-624`](./gap-reference-catalog.md#gt-624) would have printed a warning on every fresh install of the current line with no 1.x successor to offer, which is what exposed the range. Fix: retarget both ranges to `^2.0.0` (a major bump of a dependency, so it needs the SDK's breaking changes reviewed against both call sites) or publish an `sdk@1.2.x` carrying the fixes, then deprecate `sdk@1.1.0` and close GT-624's first criterion for real. **RANGES RETARGETED 2026-07-29, and doing it proved the row understated the defect.** `package-lock.json` did not merely record the range — it pinned the registry tarball of `sdk@1.1.0` at `src/sdk/cli/node_modules/@beyondnet/evolith-sdk` **and** `src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk`, with integrity hashes. So `npm ci` on a clean checkout fetched the pre-wave SDK and nested it inside each consumer, where it **shadows the workspace link**: the top-level symlink to the local 2.0.0 is what every developer reads, and the nested 1.1.0 is what a fresh install actually resolves. That is the same class as [`GT-625`](./gap-reference-catalog.md#gt-625) — the workspace hiding what a real install does — and it is why nothing caught this. Both nested entries disappeared once the range moved to `^2.0.0`, because the workspace's own 2.0.0 then satisfies it; the lockfile diff is exactly two range strings and those two removals, with no collateral drift, and `npm install --package-lock-only` was used to confirm it writes byte-for-byte what the hand edit did rather than trusting the edit. The SDK surface both consumers actually use is narrow — `EvolithRestClient` only — so the 2.0.0 breaking change (payload types re-exported from `core-domain`, `.passed` → `.verdict`, `'info'` severity retired) touches neither call site. Verified against 2.0.0: CLI **1436/1436** in 99 suites, mcp-server **433/433** in 53. The row stays OPEN: a range in the repository is not a range on the registry, and criterion 1 is only met by a release. - **Component:** `Infra` · **Criticality:** P1 · **Complexity:** S - **Provenance:** Found on 2026-07-29 while executing GT-624's deprecation criterion. Registered rather than worked around: skipping the deprecation quietly would have left the range — and the install — exactly as they are. - **Acceptance criteria:** diff --git a/src/packages/mcp-server/package.json b/src/packages/mcp-server/package.json index 9d6ab604..7e912df6 100644 --- a/src/packages/mcp-server/package.json +++ b/src/packages/mcp-server/package.json @@ -41,7 +41,7 @@ "@beyondnet/evolith-core": "^1.2.0", "@beyondnet/evolith-core-domain": "^1.2.0", "@beyondnet/evolith-infra-providers": "^1.2.0", - "@beyondnet/evolith-sdk": "^1.1.0", + "@beyondnet/evolith-sdk": "^2.0.0", "@modelcontextprotocol/sdk": "1.29.0", "@nestjs/cache-manager": "3.1.3", "@nestjs/common": "11.1.27", diff --git a/src/sdk/cli/package.json b/src/sdk/cli/package.json index f2b67f41..4b02c2e3 100644 --- a/src/sdk/cli/package.json +++ b/src/sdk/cli/package.json @@ -73,7 +73,7 @@ "@beyondnet/evolith-agent-runtime": "^1.1.0", "@beyondnet/evolith-core-domain": "^1.2.0", "@beyondnet/evolith-infra-providers": "^1.2.0", - "@beyondnet/evolith-sdk": "^1.1.0", + "@beyondnet/evolith-sdk": "^2.0.0", "@hono/node-server": "1.19.14", "@modelcontextprotocol/sdk": "1.29.0", "@nestjs/common": "11.1.27", From b80cb7a246c636cee707d95a0277ba6534869e4b Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 18:18:23 -0500 Subject: [PATCH 09/10] chore(release): prepare 1.2.2 of the CLI and the MCP server (GT-634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bumps and changelogs only — nothing is published by this commit. @beyondnet/evolith-cli and @beyondnet/evolith-mcp go 1.2.1 -> 1.2.2. A patch, and the reason is stated rather than assumed: neither package re-exports the SDK and both use only `EvolithRestClient`, so 2.0.0's breaking change (payload types re-exported from core-domain, `.passed` -> `.verdict`, `'info'` severity retired) reaches no public API of either. cli is a bin; mcp exposes `dist/main` and no SDK type. REHEARSED, and the decisive check is an install rather than a range: - plan-npm-release.mjs -> 2 of 8 packages WILL PUBLISH, in dependency order, the other 6 correctly reported as already on the registry. - `npm pack` + clean install of beyondnet-evolith-cli-1.2.2.tgz in a directory that has never seen this workspace resolves @beyondnet/evolith-sdk@2.0.0. Same for beyondnet-evolith-mcp-1.2.2.tgz. That is the fix demonstrated end to end: the range is not the claim, the resolved version is. - check:install-smoke -> 36 @beyondnet/* specifiers resolve in a clean install and the installed binary boots printing 1.2.2. This is the gate GT-625 was closed with, and it is what makes a workspace-hidden break visible. - check:release-drift -> passed, the built dist carries the expected fixes. - CLI jest 1436/1436 (99 suites), mcp-server 433/433 (53 suites) against sdk 2.0.0. Lockfile touched only where it had to be: two `version` lines. Verified by snapshotting it, running `npm install --package-lock-only`, and diffing — npm writes exactly what is committed. NOT DONE HERE, deliberately: publishing. `npm-release.yml` is a workflow_dispatch whose `dry_run` defaults to true, and it also fires on a `v*` tag — neither was triggered, and no tag was created. GT-634's first criterion stays open until the registry serves the new range. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++ package-lock.json | 4 ++-- src/packages/mcp-server/package.json | 2 +- src/sdk/cli/CHANGELOG.md | 6 ++++++ src/sdk/cli/package.json | 2 +- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 725562a5..b6f3e0fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ ## [Unreleased] +## [1.2.2] - 2026-07-29 + +**Which packages this version covers.** Only `@beyondnet/evolith-cli` and +`@beyondnet/evolith-mcp`, the two that declared the stale dependency range. No +other package changed, and republishing them would only add noise to the +registry. + +### Bug Fixes + +* **deps:** the CLI and the MCP server now resolve `@beyondnet/evolith-sdk@^2.0.0` + instead of `^1.1.0` (GT-634). The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` + with nothing in between, so a caret range under 1.x pinned **exactly 1.1.0** — + published 2026-07-18, five days before the security wave that shipped in 2.0.0 + on 2026-07-27. Both consumers were published on 2026-07-28, after 2.0.0 existed, + still pointing at the 1.x line, so `npm install @beyondnet/evolith-cli@latest` + installed a pre-wave SDK. **The exposure described in the 1.2.0 section below is + therefore not closed by 1.2.0 alone for this dependency; it is closed here.** + + The lockfile made it concrete rather than cosmetic: it pinned the registry + tarball of `sdk@1.1.0`, with integrity hashes, at + `src/sdk/cli/node_modules/@beyondnet/evolith-sdk` and + `src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk`. A clean `npm ci` + fetched the pre-wave SDK and nested it inside each consumer, where it shadows + the workspace link — the top-level symlink to the local 2.0.0 is what a + developer reads, and the nested 1.1.0 is what a fresh install resolves. Both + nested entries disappear once the range is `^2.0.0`. + + Neither package re-exports the SDK, and both use only `EvolithRestClient`, so + 2.0.0's breaking change (payload types re-exported from `core-domain`, + `.passed` → `.verdict`, `'info'` severity retired) reaches no public API of + either. That is why this is a patch and not a minor. + ## [1.2.0] - 2026-07-27 Changes accumulated since the 1.0.0 release of 2026-06-28. Automated changelog diff --git a/package-lock.json b/package-lock.json index 74e58d85..8ef2af7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16576,7 +16576,7 @@ }, "src/packages/mcp-server": { "name": "@beyondnet/evolith-mcp", - "version": "1.2.1", + "version": "1.2.2", "license": "MIT", "dependencies": { "@beyondnet/evolith-agent-runtime": "^1.1.0", @@ -16886,7 +16886,7 @@ }, "src/sdk/cli": { "name": "@beyondnet/evolith-cli", - "version": "1.2.1", + "version": "1.2.2", "license": "MIT", "dependencies": { "@beyondnet/evolith-agent-runtime": "^1.1.0", diff --git a/src/packages/mcp-server/package.json b/src/packages/mcp-server/package.json index 7e912df6..100f89a2 100644 --- a/src/packages/mcp-server/package.json +++ b/src/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@beyondnet/evolith-mcp", - "version": "1.2.1", + "version": "1.2.2", "description": "Evolith MCP Gateway — NestJS-based Model Context Protocol server for AI agents. Supports stdio and SSE transports.", "main": "dist/main.js", "types": "dist/main.d.ts", diff --git a/src/sdk/cli/CHANGELOG.md b/src/sdk/cli/CHANGELOG.md index 851d721c..b6e01a66 100644 --- a/src/sdk/cli/CHANGELOG.md +++ b/src/sdk/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.2.2] + +### Bug Fixes + +* **deps:** resolve `@beyondnet/evolith-sdk@^2.0.0` instead of `^1.1.0` (GT-634). The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` with nothing in between, so the caret range pinned **exactly 1.1.0** — the build of 2026-07-18, five days before the security wave that shipped in 2.0.0. `1.2.1` was published on 2026-07-28, after 2.0.0 existed, so `npm i -g @beyondnet/evolith-cli` installed a pre-wave SDK. The lockfile also pinned that tarball at `src/sdk/cli/node_modules/@beyondnet/evolith-sdk`, where a nested install shadows the workspace link — which is why no local run showed it. Only `EvolithRestClient` is used and the SDK is not re-exported, so 2.0.0's breaking change reaches no part of this package's surface. + ## [1.0.2] ### Bug Fixes diff --git a/src/sdk/cli/package.json b/src/sdk/cli/package.json index 4b02c2e3..6bc901df 100644 --- a/src/sdk/cli/package.json +++ b/src/sdk/cli/package.json @@ -1,6 +1,6 @@ { "name": "@beyondnet/evolith-cli", - "version": "1.2.1", + "version": "1.2.2", "description": "Evolith CLI - Governance, standards validation, and AI agent integration for satellite repositories", "main": "dist/main.js", "bin": { From a53c5b40a86e426a327bc72c7b25fd37eeb5cb06 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 18:26:17 -0500 Subject: [PATCH 10/10] fix(release): the install-smoke gate was reporting the npm version, not the artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 1.2.2 release rehearsal (`npm-release.yml`, dry_run, run 30499155684) failed with: npm pack reported "beyondnet-evolith-cli-1.2.2.tgz" but no such tarball exists in /tmp/evolith-install-smoke-Fr4e7I. The artifact was fine. `--pack-destination` is not honoured identically across npm majors when the cwd is a WORKSPACE package: npm 11 (local, Node 24) writes to the destination, npm 10 (the Node 20 runner) writes to the package directory — and prints the same filename either way. The guard trusted one of them, so it passed on a laptop and failed on the runner, which is the exact shape this repository keeps catching in its own gates. Now it looks in both, and MOVES a stray tarball out of the source tree instead of leaving it for the root-cleanliness guard to trip over. The failure message names both locations and says why both are checked, so the next person does not have to re-derive it. Worth stating: the previous release ran this same gate green on the runner (30356577427, 2026-07-28), so this is not a regression in the workflow — it is an environment dependence that happened not to bite until now. A gate whose result depends on which npm packed is not evidence about the package. Verified locally: check:install-smoke passes, 36 @beyondnet/* specifiers resolve, the installed binary boots printing 1.2.2, and no stray .tgz is left in src/sdk/cli. The npm 10 path is verified by re-running the rehearsal on the runner — the environment that failed is the only honest place to prove it. Co-Authored-By: Claude Opus 5 --- src/sdk/cli/scripts/check-install-smoke.mjs | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/sdk/cli/scripts/check-install-smoke.mjs b/src/sdk/cli/scripts/check-install-smoke.mjs index cc4e87bb..1b805d70 100644 --- a/src/sdk/cli/scripts/check-install-smoke.mjs +++ b/src/sdk/cli/scripts/check-install-smoke.mjs @@ -44,7 +44,7 @@ * 1 - an unresolvable specifier, a boot failure, a pack/install failure, or a vacuous scan */ -import { readdirSync, readFileSync, existsSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, mkdtempSync, mkdirSync, rmSync, renameSync } from 'node:fs'; import { join, resolve, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { createRequire } from 'node:module'; @@ -225,8 +225,29 @@ function main(argv) { fail([`npm pack failed (${packed.status}):`, String(packed.stderr).trim()]); } const tarball = String(packed.stdout).trim().split('\n').pop(); + // `--pack-destination` is not honoured identically across npm majors when + // the cwd is a WORKSPACE package: npm 11 writes to the destination, npm 10 + // (the Node 20 runner) writes to the package directory and prints the same + // filename either way. Trusting one of them made this guard report the npm + // version rather than the artifact — it passed locally and failed the + // release rehearsal on 2026-07-29 with "no such tarball exists". + // + // So LOOK in both, and move a stray tarball out of the source tree rather + // than leaving it for the root-cleanliness guard to find. spec = join(temp, tarball); - if (!existsSync(spec)) fail([`npm pack reported "${tarball}" but no such tarball exists in ${temp}.`]); + if (!existsSync(spec)) { + const strayInPackage = join(pkgRoot, tarball); + if (existsSync(strayInPackage)) { + renameSync(strayInPackage, spec); + } else { + fail([ + `npm pack reported "${tarball}" but it is in neither location:`, + ` --pack-destination: ${temp}`, + ` package directory: ${pkgRoot}`, + 'Both are checked because npm majors disagree about which one a workspace pack writes to.', + ]); + } + } } const installDir = join(temp, 'consumer');