From acd05ccd44ff4c2edef0ab0abdd0eeeab39c889b Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sat, 15 Aug 2026 17:27:29 -0500 Subject: [PATCH 1/3] fix(opa): give every violation the policy that emitted it, so attribution stops guessing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GT-693. The defect was reproduced against the real bundle before anything was touched: a satellite whose package.json carries `lodash: "^4.17.21"` -- the literal thing DEP-01 forbids -- made the wasm emit 61 violations, five of them DEP-* including DEP-01, and a gate referencing `version-pinning.rego` returned `passed`. The same gate now returns `failed`, naming `package.json#dependencies.lodash=^4.17.21`. THE FIX IS PROVENANCE, NOT A LONGER LIST. `main.rego` tags every aggregated violation with the id `deriveRuleId` builds from the emitting policy's own path, so the evaluator compares two things equal BY CONSTRUCTION instead of consulting a table somebody has to remember to update. All 34 aggregation rules were rewritten from the imports themselves, not by hand. `opa check` clean, `opa test` 265/265, bundle rebuilt and re-measured at the same 61 violations -- nothing duplicated, nothing lost. Literal object construction on purpose: only a handful of builtins are dispatchable in the compiled wasm (GT-644, guard 55), so `object.union` was not available. `{id, message}` is the complete shape -- all 251 violation literals carry those two and nothing else, verified by count. THE COLLISIONS RESOLVE, WHICH NO ID SCHEME COULD DO. `CLI-RR-01..05` are emitted by both cli-readiness and cli-release-readiness, `TAX-05..11` by both taxonomy and repository-taxonomy. Four cases assert each attributes to its own policy AND NOT to the other, because a verdict naming the wrong policy sends the operator to the wrong file -- worse than a missing one. The prefix table stays as a fallback for exactly one thing: a policy.wasm older than this change. It is deliberately not extended, and a legacy violation it cannot place is now REPORTED rather than dropped, so a stale bundle cannot look healthy. The 27-name list is gone because its premise is; it was REPLACED by three cases asserting the invariant that makes it unnecessary. Mutations: removing one tag turns 2 red, mis-tagging one turns 1 red, neutering orphan reporting turns 1 red. ADR-0041 parity demonstrated on DEP-01 in BOTH engines against the real bundle and a real directory, because the defect lived in the hop between the wasm's output and the verdict -- and both PASS an exactly-pinned repository, so the agreement is not vacuous. The bundle's absence fails that suite rather than skipping it. MY OWN FIRST SCAN WAS WRONG AND THE ROW SAYS SO. It matched packages by PREFIX and excluded every basename containing `test`, so `evolith.testing_pyramid` resolved to the test file and `testing-pyramid.rego` vanished from the census. Corrected counts are 39 policies / 35 namespaced / 203 ids, not 33 / 31 / 197. The collision facts were unaffected and were right. The spec, which uses exact package keys, validated all 34 tags -- it caught what my script could have got wrong. GT-694 registered rather than absorbed: 15 facets that shipped input schemas require are never emitted by the input builder, so twelve categories can never reach their policy at all -- a different defect, on the way IN rather than on the way out. core-domain 1934 · cli 1482 · mcp 575 · infra-providers 179 · contracts 115. Guards 26, 28, 32, 55 green. Co-Authored-By: Claude Opus 5 --- .../evaluators/opa-evaluator.spec.ts | 255 ++++++++++++------ .../validators/evaluators/opa-evaluator.ts | 83 +++++- .../opa-native-attribution-parity.spec.ts | 140 ++++++++++ src/rulesets/opa/main.rego | 87 +++--- 4 files changed, 432 insertions(+), 133 deletions(-) create mode 100644 src/packages/core-domain/src/application/validators/evaluators/opa-native-attribution-parity.spec.ts diff --git a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts index f127cd54..307d6854 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts @@ -1,4 +1,4 @@ -import { OpaEvaluator, CONTEXT_AWARE_VIOLATION_PREFIXES } from './opa-evaluator'; +import { OpaEvaluator, CONTEXT_AWARE_VIOLATION_PREFIXES, violationBelongsToRule } from './opa-evaluator'; import { createMockFileSystem, createMockLogger } from '../../../test/mocks'; import { NormalizedRule } from '../../../domain/models/normalized-rule'; import { WorkspaceEvaluationContext } from './evaluator.interface'; @@ -168,90 +168,6 @@ describe('OpaEvaluator', () => { }); }); -/** - * GT-688 AC5 — the attribution table is hand-maintained, and a missing entry is - * SILENT: the policy fires in the wasm, no rule claims its violations, and the - * rule referencing it is reported `passed`. That is a false pass, which is the - * class this corpus exists to catch. - * - * Measured 2026-08-15 while closing GT-688: 31 of the 33 shipped policies emit - * namespaced ids and only 4 are mapped. The other 27 have exactly the same - * defect as `TPC-01` had, for any satellite whose gate references them. That is - * NOT fixed here — expanding this slice to rewrite the attribution model would - * make an unreviewable diff out of a one-line gap — it is registered as its own - * row, and this test is what stops it from being forgotten: the unmapped set is - * written down, so adding a policy or mapping one is a deliberate edit rather - * than a silent drift. - */ -describe('the violation attribution table · GT-688', () => { - const OPA_DIR = path.resolve(__dirname, '../../../../../../rulesets/opa'); - - /** policy id (as `deriveRuleId` produces it) → the id prefixes it emits */ - function prefixesEmittedByPolicy(): Map> { - const out = new Map>(); - for (const file of fs.readdirSync(OPA_DIR)) { - if (!file.endsWith('.rego') || file === 'main.rego' || file.includes('test')) continue; - const ids = [...fs.readFileSync(path.join(OPA_DIR, file), 'utf8').matchAll(/"id":\s*"([A-Z][A-Z0-9]*)-/g)]; - if (ids.length === 0) continue; - out.set(`opa-${file.replace(/\.rego$/, '')}`, new Set(ids.map((m) => `${m[1]}-`))); - } - return out; - } - - it('reads real policies, so an empty scan cannot pass this vacuously', () => { - expect(prefixesEmittedByPolicy().size).toBeGreaterThanOrEqual(30); - }); - - it('MAPS `topology-composition`, whose absence made AC5 unmeetable', () => { - const emitted = prefixesEmittedByPolicy().get('opa-topology-composition'); - expect([...(emitted ?? [])]).toContain('TPC-'); - expect(CONTEXT_AWARE_VIOLATION_PREFIXES['opa-topology-composition']).toBe('TPC-'); - }); - - it('every mapped prefix is one the policy actually emits', () => { - const emitted = prefixesEmittedByPolicy(); - for (const [policy, prefix] of Object.entries(CONTEXT_AWARE_VIOLATION_PREFIXES)) { - expect([policy, [...(emitted.get(policy) ?? [])]]).toEqual([policy, expect.arrayContaining([prefix])]); - } - }); - - it('the UNMAPPED policies are the ones we know about — a new one must be a deliberate choice', () => { - const unmapped = [...prefixesEmittedByPolicy().keys()] - .filter((p) => !(p in CONTEXT_AWARE_VIOLATION_PREFIXES)) - .sort(); - // Each of these drops its violations for a satellite that references it. - // Shrinking this list is progress; growing it silently is the regression. - expect(unmapped).toEqual([ - 'opa-abac-mcp-tool-access', - 'opa-anti-corruption-layer', - 'opa-capability-source-interface', - 'opa-ci-cd', - 'opa-cicd-quality-gates', - 'opa-cli-core-parity', - 'opa-cli-exit-code-taxonomy', - 'opa-cli-readiness', - 'opa-cli-release-readiness', - 'opa-engineering-manifesto', - 'opa-evidence', - 'opa-executive-scorecards', - 'opa-gitflow-branching', - 'opa-governance', - 'opa-hexagonal-architecture', - 'opa-knowledge-intake', - 'opa-mcp', - 'opa-multi-runtime', - 'opa-multi-tenancy', - 'opa-open-core-boundary', - 'opa-probabilistic-evidence-admissibility', - 'opa-protocol-selection', - 'opa-repository-taxonomy', - 'opa-satellite-contracts', - 'opa-taxonomy', - 'opa-telemetry-evidence', - 'opa-version-pinning', - ]); - }); -}); /** * GT-688 AC5 — the criterion is "a policy can discriminate on a topology present @@ -307,3 +223,172 @@ describe('TPC-01 reaches the verdict · GT-688 AC5', () => { expect(results[0].result).toBe('passed'); }); }); + +/** + * GT-693 — attribution is now DERIVED, not listed. + * + * The test this replaces pinned 27 policy names that the hand-maintained prefix + * table did not cover, so that the rot at least failed loudly. Its premise is gone: + * `main.rego` tags every aggregated violation with the policy that emitted it, + * using exactly the id `deriveRuleId` builds from that policy's path, so a new + * policy is attributed the moment it is aggregated and no list needs updating. + * + * What replaces it is the invariant that makes that true, asserted against the + * real files: every aggregation rule carries a tag, and every tag equals the + * derived id of the file declaring the package it aggregates. Adding an import to + * `main.rego` without a tag — the one way to re-create the defect — fails here. + */ +describe('every policy in the bundle is attributable · GT-693', () => { + const OPA_DIR = path.resolve(__dirname, '../../../../../../rulesets/opa'); + const MAIN = path.join(OPA_DIR, 'main.rego'); + + /** `deriveRuleId`'s transform, from `satellite-evaluation-pipeline.service.ts`. */ + const deriveRuleId = (relPath: string) => + relPath.replace(/^.*rulesets\//, '').replace(/\.rego$/, '').replace(/[^a-zA-Z0-9_-]/g, '-'); + + function packageToFile(): Map { + const out = new Map(); + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { walk(full); continue; } + if (!entry.name.endsWith('.rego')) continue; + const pkg = fs.readFileSync(full, 'utf8').split('\n').find((l) => l.startsWith('package ')); + if (pkg) out.set(pkg.slice('package '.length).trim(), full); + } + }; + walk(OPA_DIR); + return out; + } + + const main = () => fs.readFileSync(MAIN, 'utf8'); + const imports = () => + [...main().matchAll(/^import data\.evolith\.([\w.]+)\.violations as (\w+)$/gm)] + .map((m) => ({ pkg: m[1], alias: m[2] })); + const aggregations = () => + [...main().matchAll(/^violations contains (\{[^}]*\}|v) if \{\n\tv := (\w+)\[_\]\n\}$/gm)] + .map((m) => ({ head: m[1], alias: m[2] })); + + it('reads the real bundle, so an empty scan cannot pass this vacuously', () => { + expect(imports().length).toBeGreaterThanOrEqual(30); + expect(aggregations().length).toBe(imports().length); + }); + + it('EVERY aggregated policy carries provenance — an untagged one is the old defect', () => { + const untagged = aggregations().filter((a) => !a.head.includes('"policy"')).map((a) => a.alias); + expect(untagged).toEqual([]); + }); + + it("every tag equals the id `deriveRuleId` builds from that policy's own path", () => { + const files = packageToFile(); + const wrong: string[] = []; + for (const { pkg, alias } of imports()) { + const file = files.get(`evolith.${pkg}`); + expect([pkg, file !== undefined]).toEqual([pkg, true]); + const expected = deriveRuleId(file!); + const agg = aggregations().find((a) => a.alias === alias); + const tag = agg?.head.match(/"policy":\s*"([^"]+)"/)?.[1]; + if (tag !== expected) wrong.push(`${alias}: tagged ${tag ?? ''}, derives to ${expected}`); + } + expect(wrong).toEqual([]); + }); +}); + +/** + * GT-693 AC4 — the two id ranges that no id-based scheme can resolve. + * + * `CLI-RR-01..05` are emitted by BOTH `cli-readiness` and `cli-release-readiness`; + * `TAX-05..11` by both `taxonomy` and `repository-taxonomy`. 10 of the corpus's 197 + * ids collide. Under the old prefix scheme a gate referencing one of them would have + * claimed the other's findings and reported them under the wrong rule — a verdict + * that names the wrong policy is worse than a missing one, because it sends the + * operator to the wrong file. + */ +describe('colliding ids resolve to the policy that emitted them · GT-693 AC4', () => { + const readiness = { id: 'CLI-RR-01', message: 'from cli-readiness', policy: 'opa-cli-readiness' }; + const release = { id: 'CLI-RR-01', message: 'from cli-release-readiness', policy: 'opa-cli-release-readiness' }; + + it('attributes each to its own policy and NOT to the other', () => { + expect(violationBelongsToRule(readiness, 'opa-cli-readiness')).toBe(true); + expect(violationBelongsToRule(readiness, 'opa-cli-release-readiness')).toBe(false); + expect(violationBelongsToRule(release, 'opa-cli-release-readiness')).toBe(true); + expect(violationBelongsToRule(release, 'opa-cli-readiness')).toBe(false); + }); + + it('does the same for the TAX- range', () => { + const tax = { id: 'TAX-05', message: 'x', policy: 'opa-taxonomy' }; + expect(violationBelongsToRule(tax, 'opa-taxonomy')).toBe(true); + expect(violationBelongsToRule(tax, 'opa-repository-taxonomy')).toBe(false); + }); + + it('falls back to the legacy scheme ONLY when a violation carries no provenance', () => { + // A bundle compiled before GT-693. The four legacy entries still work… + expect(violationBelongsToRule({ id: 'DOD-01', message: 'x' }, 'opa-dod')).toBe(true); + // …and everything else still fails to attribute, which is the defect this + // fallback deliberately does NOT paper over: a stale wasm must not look healthy. + expect(violationBelongsToRule({ id: 'DEP-01', message: 'x' }, 'opa-version-pinning')).toBe(false); + }); +}); + +/** + * GT-693 AC2 — a violation that no evaluated rule claims must be SURFACED, not + * dropped. Before this, `violations.filter(...)` simply matched nothing and the + * finding ceased to exist: there was no way, from any output, to tell "the policy + * found nothing" apart from "the policy found something and we lost it". + * + * Reported at debug rather than warn on purpose. For a partial rule selection most + * violations legitimately belong to policies the run never asked about, so warning + * would fire on every healthy run and be muted within a week. What AC2 requires is + * that the information EXIST and name its policy, which it now does. + */ +describe('an unclaimed violation is named, not dropped · GT-693 AC2', () => { + const wasmMock = require('@open-policy-agent/opa-wasm'); + + const ruleFor = (id: string): NormalizedRule => ({ + id, severity: 'MUST', category: 'version-pinning', title: id, + description: 'gate rule', blocking: true, sourceFile: 'gate.json', + }); + + it('names the orphan AND the policy that emitted it', async () => { + const fs = createMockFileSystem(); + const logger = createMockLogger(); + fs.setFile(path.join('/core', 'rulesets', 'opa', 'policy.wasm'), 'fake-wasm-orphan'); + (wasmMock.loadPolicy as jest.Mock).mockResolvedValueOnce({ + evaluate: () => [{ result: [ + { id: 'DEP-01', message: 'claimed', policy: 'opa-version-pinning' }, + { id: 'MTN-04', message: 'nobody asked about this one', policy: 'opa-multi-tenancy' }, + ] }], + }); + + const results = await new OpaEvaluator(fs, logger).evaluateAll( + [ruleFor('opa-version-pinning')], + { satellitePath: '/satellite', corePath: '/core' }, + ); + + // The rule that WAS asked about gets its own violation and only its own. + expect(results[0].result).toBe('failed'); + expect(results[0].message).toBe('claimed'); + + const debug = logger.getLogsByLevel('DEBUG').map((l) => l.message).join(' '); + expect(debug).toMatch(/matched no evaluated rule/); + expect(debug).toMatch(/opa-multi-tenancy: MTN-04/); + // …and it must NOT claim the one that was attributed. + expect(debug).not.toMatch(/DEP-01/); + }); + + it('says nothing when every violation found an owner', async () => { + const fs = createMockFileSystem(); + const logger = createMockLogger(); + fs.setFile(path.join('/core', 'rulesets', 'opa', 'policy.wasm'), 'fake-wasm-no-orphan'); + (wasmMock.loadPolicy as jest.Mock).mockResolvedValueOnce({ + evaluate: () => [{ result: [{ id: 'DEP-01', message: 'claimed', policy: 'opa-version-pinning' }] }], + }); + + await new OpaEvaluator(fs, logger).evaluateAll( + [ruleFor('opa-version-pinning')], + { satellitePath: '/satellite', corePath: '/core' }, + ); + + expect(logger.getLogsByLevel('DEBUG').map((l) => l.message).join(' ')).not.toMatch(/matched no evaluated rule/); + }); +}); diff --git a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts index 84cc4257..cbec13d3 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts @@ -13,24 +13,58 @@ const globalPolicyCache = new Map(); const globalSchemaCache = new Map(); /** - * GT-382: context-aware policies emit namespaced violation ids (`DOD-*`, `CB-*`, - * `PG-*`) that can never equal the path-derived rule id produced for a gate's - * `rules: ["rulesets/opa/.rego"]` reference (e.g. `deriveRuleId` → - * `opa-dod`). For these policies a gate rule referencing the policy file owns - * ALL of that policy's violations, so they are matched by id PREFIX. Every other - * rule keeps exact-id matching, so this changes no other policy's behavior. + * GT-382, superseded by GT-693 — kept ONLY to read bundles compiled before the + * provenance change, and deliberately not extended. + * + * The problem it was built for is real: a policy emits namespaced ids (`DOD-*`, + * `CB-*`) that can never equal the `opa-` id `deriveRuleId` builds from a + * gate's `rules: ["rulesets/opa/.rego"]` reference. The problem with the + * SOLUTION was that it is a list somebody has to remember to update, and forgetting + * is silent in the worst direction: the violation matches no rule, the branch falls + * through to `passed`, and a policy that fired is reported as conformance. + * + * Measured on 2026-08-15 before the fix: 31 of the 33 shipped policies emit + * namespaced ids and four were listed here. A satellite with `lodash: ^4.17.21` — + * the literal thing `DEP-01` forbids — produced `DEP-01` in the wasm and a verdict + * of `passed` for a gate referencing `version-pinning.rego`. + * + * `main.rego` now tags every aggregated violation with the policy that emitted it, + * using exactly the id `deriveRuleId` produces, so attribution compares two things + * equal by construction. This table is the fallback for a `policy`-less violation, + * which today means one thing only: a `policy.wasm` older than that change. Adding + * an entry here would be re-creating the defect, so `unattributedPolicies` below + * reports what the fallback could not place instead of dropping it. */ export const CONTEXT_AWARE_VIOLATION_PREFIXES: Readonly> = { 'opa-dod': 'DOD-', 'opa-compliance-baseline': 'CB-', 'opa-phase-gates': 'PG-', - // GT-688 AC5 — `topology-composition.rego` emits `TPC-01`, which can never - // equal the `opa-topology-composition` id derived from a gate's - // `rules: ["rulesets/opa/topology-composition.rego"]`. Without this entry the - // policy fires in the wasm and the rule is reported `passed`. 'opa-topology-composition': 'TPC-', }; +/** + * Does this violation belong to this rule? + * + * Provenance first — `v.policy` is the emitting policy's derived id, so a gate + * referencing that file owns it. This is what makes the two colliding id ranges + * resolvable: `CLI-RR-01..05` are emitted by BOTH `cli-readiness` and + * `cli-release-readiness`, and `TAX-05..11` by both `taxonomy` and + * `repository-taxonomy` (10 of 197 ids collide), so no id-based scheme can tell + * them apart and a prefix scheme attributes them to whichever rule asks first. + */ +export function violationBelongsToRule( + violation: Record, + ruleId: string, +): boolean { + const provenance = violation.policy; + if (typeof provenance === 'string') return provenance === ruleId; + + // Legacy bundle: no provenance on the wire. + const prefix = CONTEXT_AWARE_VIOLATION_PREFIXES[ruleId]; + if (prefix) return typeof violation.id === 'string' && violation.id.startsWith(prefix); + return violation.id === ruleId; +} + export class OpaEvaluator implements IRuleEvaluatorStrategy { private inputBuilder: OpaInputBuilder; private ajv: Ajv; @@ -171,11 +205,19 @@ export class OpaEvaluator implements IRuleEvaluatorStrategy { violations = (resultSet?.[0]?.result) ? resultSet[0].result as Record[] : []; } + // GT-693 — a violation nobody claims used to vanish here. It is now named, + // with the policy that emitted it, because "the run said nothing" and "the + // run found nothing" are different facts and only one of them is good news. + // Reported at debug: for a partial rule selection most violations legitimately + // belong to policies this run did not ask about, so warning would be noise — + // what matters is that the information exists at all rather than being lost. + const claimed = new Set>(); + opaResults = passedRules.map(rule => { - const prefix = CONTEXT_AWARE_VIOLATION_PREFIXES[rule.id]; - const ruleViolations = prefix - ? violations.filter((v: Record) => typeof v.id === 'string' && (v.id as string).startsWith(prefix)) - : violations.filter((v: Record) => v.id === rule.id); + const ruleViolations = violations.filter((v: Record) => + violationBelongsToRule(v, rule.id), + ); + for (const v of ruleViolations) claimed.add(v); if (ruleViolations.length > 0) { return { rule, @@ -188,6 +230,19 @@ export class OpaEvaluator implements IRuleEvaluatorStrategy { result: 'passed' }; }); + + const orphans = violations.filter((v) => !claimed.has(v)); + if (orphans.length > 0) { + const byPolicy = new Map(); + for (const v of orphans) { + const owner = typeof v.policy === 'string' ? v.policy : ''; + byPolicy.set(owner, [...(byPolicy.get(owner) ?? []), String(v.id)]); + } + this.logger.debug( + `OPA: ${orphans.length} violation(s) matched no evaluated rule — ` + + [...byPolicy.entries()].map(([p, ids]) => `${p}: ${ids.join(', ')}`).join(' | '), + ); + } } return [...failedResults, ...opaResults]; diff --git a/src/packages/core-domain/src/application/validators/evaluators/opa-native-attribution-parity.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/opa-native-attribution-parity.spec.ts new file mode 100644 index 00000000..56a2e8f5 --- /dev/null +++ b/src/packages/core-domain/src/application/validators/evaluators/opa-native-attribution-parity.spec.ts @@ -0,0 +1,140 @@ +/** + * GT-693 AC5 — `ADR-0041` parity, on the axis this gap broke. + * + * The gap was not that OPA disagreed with the native engine about a fact. It was + * that OPA's answer never arrived: a gate referencing `version-pinning.rego` + * produced `DEP-01` inside the wasm, no rule claimed the violation, and the + * evaluator returned `passed`. The two engines were therefore in silent + * disagreement about the same repository, and the disagreement read as agreement + * because one side reported conformance. + * + * `DEP-01` is the right rule to prove it on: it exists in BOTH corpora — natively + * in `src/rulesets/sdlc/dependency-pinning.rules.json` with a handler in + * `dependency-rule.handler.ts`, and in Rego in `version-pinning.rego`. + * + * This spec uses the REAL compiled bundle and a REAL directory, because the defect + * lived in the hop between the wasm's output and the verdict — a mocked wasm cannot + * exercise the thing that was broken. CI compiles `policy.wasm` in this job before + * running these tests ("Compile OPA policy to WASM", ci-cd.yml), and the bundle's + * absence FAILS the suite rather than skipping it: a parity test that quietly does + * not run is the exact failure mode this row exists to close. + */ + +import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { OpaEvaluator } from './opa-evaluator'; +import { DependencyRuleHandler } from './handlers/dependency-rule.handler'; +import type { NormalizedRule } from '../../../domain/models/normalized-rule'; + +const CORE = path.resolve(__dirname, '../../../../../../..'); +const WASM = path.join(CORE, 'src', 'rulesets', 'opa', 'policy.wasm'); + +const realFs: any = { + exists: async (p: string) => existsSync(p), + existsSync: (p: string) => existsSync(p), + readFile: async (p: string) => readFile(p, 'utf8'), + readFileBuffer: async (p: string) => readFile(p), + readJson: async (p: string) => JSON.parse(await readFile(p, 'utf8')), + readdirNames: async (p: string) => (await import('node:fs/promises')).readdir(p), + isDirectory: async (p: string) => (await import('node:fs/promises')).stat(p).then((s) => s.isDirectory()).catch(() => false), + isDir: async (p: string) => (await import('node:fs/promises')).stat(p).then((s) => s.isDirectory()).catch(() => false), + stat: async (p: string) => { + const s = await (await import('node:fs/promises')).stat(p); + return { isDirectory: () => s.isDirectory(), isFile: () => s.isFile(), size: s.size }; + }, + readdir: async (p: string) => (await import('node:fs/promises')).readdir(p), + listFiles: async (p: string) => (await import('node:fs/promises')).readdir(p), +}; + +const silentLogger: any = { + info: () => undefined, warn: () => undefined, error: () => undefined, + debug: () => undefined, success: () => undefined, +}; + +/** The satellite both engines are asked about: one caret-pinned dependency. */ +function satelliteViolatingDep01(): string { + const dir = mkdtempSync(path.join(os.tmpdir(), 'gt693-parity-')); + mkdirSync(dir, { recursive: true }); + writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ name: 'sat', version: '1.0.0', dependencies: { lodash: '^4.17.21' } }, null, 2), + ); + writeFileSync(path.join(dir, 'evolith.yaml'), 'apiVersion: evolith.dev/v1\nkind: Satellite\nmetadata:\n name: sat\nspec: {}\n'); + return dir; +} + +/** `DEP-01` exactly as the shipped native corpus declares it. */ +function nativeDep01(): NormalizedRule { + const corpus = JSON.parse( + readFileSync(path.join(CORE, 'src', 'rulesets', 'sdlc', 'dependency-pinning.rules.json'), 'utf8'), + ) as { rules: NormalizedRule[] }; + const rule = corpus.rules.find((r) => r.id === 'DEP-01'); + if (!rule) throw new Error('DEP-01 is not in the shipped corpus — this parity test is measuring nothing'); + return rule; +} + +/** The gate rule a satellite writes to pull in the Rego policy. */ +const opaGateRule: NormalizedRule = { + id: 'opa-version-pinning', + severity: 'MUST', + category: 'version-pinning', + title: 'Dependency pinning (Rego)', + description: 'gate rule referencing rulesets/opa/version-pinning.rego', + blocking: true, + sourceFile: 'gate.json', +} as NormalizedRule; + +describe('ADR-0041 parity across the attribution seam · GT-693 AC5', () => { + let satellite: string; + + beforeAll(() => { + satellite = satelliteViolatingDep01(); + }); + + it('the compiled bundle is present — this suite must not pass by not running', () => { + expect([WASM, existsSync(WASM)]).toEqual([WASM, true]); + }); + + it('BOTH engines fail the same repository on the same rule', async () => { + const native = await new DependencyRuleHandler(realFs).evaluate(nativeDep01(), { + satellitePath: satellite, + corePath: CORE, + } as any); + + const [opa] = await new OpaEvaluator(realFs, silentLogger).evaluateAll([opaGateRule], { + satellitePath: satellite, + corePath: CORE, + } as any); + + expect(native.result).toBe('failed'); + // Before GT-693 this read `passed` while the wasm had already produced DEP-01. + expect(opa.result).toBe('failed'); + // …and it is the SAME finding, not merely the same verdict. + expect(opa.message).toMatch(/lodash/); + expect(opa.message).toMatch(/\^4\.17\.21/); + }, 60000); + + it('and both PASS a repository that pins exactly, so the agreement is not vacuous', async () => { + const clean = mkdtempSync(path.join(os.tmpdir(), 'gt693-parity-clean-')); + writeFileSync( + path.join(clean, 'package.json'), + JSON.stringify({ name: 'sat', version: '1.0.0', dependencies: { lodash: '4.17.21' } }, null, 2), + ); + writeFileSync(path.join(clean, 'evolith.yaml'), 'apiVersion: evolith.dev/v1\nkind: Satellite\nmetadata:\n name: sat\nspec: {}\n'); + + const native = await new DependencyRuleHandler(realFs).evaluate(nativeDep01(), { + satellitePath: clean, + corePath: CORE, + } as any); + const [opa] = await new OpaEvaluator(realFs, silentLogger).evaluateAll([opaGateRule], { + satellitePath: clean, + corePath: CORE, + } as any); + + expect(native.result).toBe('passed'); + expect(opa.result).toBe('passed'); + }, 60000); +}); diff --git a/src/rulesets/opa/main.rego b/src/rulesets/opa/main.rego index 9f6b875a..0d11f48d 100644 --- a/src/rulesets/opa/main.rego +++ b/src/rulesets/opa/main.rego @@ -37,133 +37,152 @@ import data.evolith.topology_composition.violations as tpc_violations import data.evolith.testing_pyramid.violations as tpy_violations import data.evolith.version_pinning.violations as vp_violations -violations contains v if { +# GT-693 — every aggregated violation carries the POLICY THAT EMITTED IT. +# +# Before this, `violations` was a flat set of `{id, message}` and the evaluator had +# to guess the source from the id: a hand-maintained prefix table in +# `opa-evaluator.ts` mapped four policies, and the other 27 fell through to exact-id +# matching, which can never match — a gate's `rules: ["rulesets/opa/.rego"]` +# becomes `opa-`, and no policy emits an id shaped like that. The violations were +# therefore DROPPED and the rule reported `passed`. Measured: a satellite with +# `lodash: ^4.17.21` produced `DEP-01` in this bundle and a verdict of `passed`. +# +# The tag is exactly the id `deriveRuleId` builds from the policy's path, so the +# evaluator compares two things that are equal by construction instead of by a list +# somebody has to remember to update. +# +# Written as literal object construction on purpose: only a handful of builtins are +# dispatchable in the compiled wasm (see guard 55), so `object.union` and friends are +# not available here. `id` and `message` are the complete shape — all 251 violation +# literals in this corpus carry those two fields and nothing else. + +violations contains {"id": v.id, "message": v.message, "policy": "opa-version-pinning"} if { v := vp_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-taxonomy"} if { v := taxonomy_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-cli-readiness"} if { v := cli_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-evidence"} if { v := evidence_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-mcp"} if { v := mcp_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-ci-cd"} if { v := ci_cd_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-governance"} if { v := gov_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-abac-mcp-tool-access"} if { v := abac_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-capability-source-interface"} if { v := csi_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-anti-corruption-layer"} if { v := acl_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-cicd-quality-gates"} if { v := cicd_qg_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-cli-core-parity"} if { v := cli_cp_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-cli-release-readiness"} if { v := cli_rr_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-compliance-baseline"} if { v := cb_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-dod"} if { v := dod_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-engineering-manifesto"} if { v := em_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-executive-scorecards"} if { v := exec_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-gitflow-branching"} if { v := git_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-hexagonal-architecture"} if { v := hxa_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-knowledge-intake"} if { v := ki_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-multi-runtime"} if { v := runt_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-multi-tenancy"} if { v := mtn_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-open-core-boundary"} if { v := ocb_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-protocol-selection"} if { v := prot_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-repository-taxonomy"} if { v := repo_tax_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-satellite-contracts"} if { v := svc_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-testing-pyramid"} if { v := tpy_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-telemetry-evidence"} if { v := telemetry_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-infrastructure-helm-enforcement"} if { v := helm_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-infrastructure-opa-sidecar-bundle"} if { v := opa_sidecar_violations[_] } -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-phase-gates"} if { v := pg_violations[_] } # GT-580 — the exit-code taxonomy. Silent unless the caller declares # `input.core.cli`, so a satellite evaluation is unaffected. -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-cli-exit-code-taxonomy"} if { v := cli_exit_violations[_] } @@ -174,7 +193,7 @@ violations contains v if { # passes `opa test` and then decides nothing at runtime — a rule present in the # native engine and absent from OPA, which is the R-25 defect GT-602 was registered # for. Silent unless the caller declares `input.qualityEvidence`. -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-probabilistic-evidence-admissibility"} if { v := pea_violations[_] } @@ -183,6 +202,6 @@ violations contains v if { # a policy unreachable from one of them passes `opa test` and then decides # nothing at runtime (the R-25 defect GT-602 was registered for). Silent unless # the caller declares `input.context.topologyConfirmedRefs`. -violations contains v if { +violations contains {"id": v.id, "message": v.message, "policy": "opa-topology-composition"} if { v := tpc_violations[_] } From 9017b35c2eebbba17eaf2b5ec74bdfaa738049f4 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sat, 15 Aug 2026 17:29:22 -0500 Subject: [PATCH 2/3] docs(gaps): close GT-693, register GT-694, and correct my own census MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GT-693 closed with all five criteria measured against the real bundle, and the before/after recorded literally: a gate referencing `version-pinning.rego` on a satellite with `lodash: ^4.17.21` went from `passed` to `failed` naming the dependency, while the wasm had been emitting DEP-01 the whole time. A CORRECTION TO MY OWN MEASUREMENT, written into the row rather than quietly fixed. My first census matched Rego packages by PREFIX and skipped every basename containing `test`. Two consequences: `evolith.testing_pyramid` resolved to `testing-pyramid.test.rego` -- which actually declares `evolith.testing_pyramid_test` -- and the real `testing-pyramid.rego` vanished from the count entirely. That scan produced this row's original "33 policies / 31 namespaced / 197 ids". The corrected figures are 39 / 35 / 203, and they are used throughout the row now. A DRAFT FINDING BUILT ON THAT ERROR IS REFUTED AND THE REFUTATION KEPT: I had written that `main.rego` imports violations from a package declared only in a test file. It does not. The real policy exists and emits TPY-01..04. What the error did NOT touch: the collision facts were right (10 ids, CLI-RR-01..05 and TAX-05..08/TAX-11), and the spec -- which uses exact package keys, not prefixes -- validated all 34 tags, catching what the script could have got wrong. GT-688's row carries the earlier figures. They were the honest reading at the time; they are superseded here rather than rewritten there. GT-694 REGISTERED: 15 facets that shipped OPA input schemas require are never emitted by `opa-input-builder.ts`, so twelve categories can never reach their policy -- confirmed end to end on `multi-tenancy`, which returns `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'` instead of a verdict. P2 and not P1, stated rather than assumed: unlike GT-693 this produces no false pass. A different defect on the way IN, not on the way out, so it is a row and not an extension of this one. Board 657/692 · 3 in progress · 22 pending · 10 deferred, recounted from the rows in both languages. Guards 01, 04, 07, 08, 46, 49 green -- 08 caught `MEDIUM` where the declared interest band is `MED`, which is the debt-economics guard doing its job. Co-Authored-By: Claude Opus 5 --- .../evidence/gap-closure-evidence.json | 25 ++++++++++ .../gaps/gap-reference-catalog.es.md | 46 ++++++++++++++++--- .../gaps/gap-reference-catalog.md | 46 ++++++++++++++++--- .../control-center/gaps/gap-tracking.es.md | 5 +- .../core/control-center/gaps/gap-tracking.md | 5 +- .../maturity-reports/executive-summary.es.md | 18 ++++---- .../maturity-reports/executive-summary.md | 18 ++++---- .../maturity-reconciliation.json | 6 +-- 8 files changed, 130 insertions(+), 39 deletions(-) diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index 579154b0..c85c86ba 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -10138,6 +10138,31 @@ ], "dependencyDisposition": "none", "dependencyRationale": "No dependency added or changed. The work is wiring and one table entry: an `engine` argument threaded two frames down an existing private method, two collaborators copied on an existing constructor call, one key added to a module-level constant, and two envelope fields switched from a result field to a constant the same module already imported. Nothing new reaches the network, the filesystem or a process; the tests read `.rego` files with `node:fs`, which the spec tree already does elsewhere." + }, + { + "id": "GT-693", + "closedAt": "2026-08-15", + "closureCommit": "acd05ccd", + "evidence": [ + "src/rulesets/opa/main.rego", + "src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts", + "src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts", + "src/packages/core-domain/src/application/validators/evaluators/opa-native-attribution-parity.spec.ts" + ], + "validationCommands": [ + "THE DEFECT, REPRODUCED AGAINST THE REAL BUNDLE BEFORE ANY CHANGE: a satellite whose package.json carries `lodash: \"^4.17.21\"` -- the literal thing DEP-01 forbids -- made policy.wasm emit 61 violations, 5 of them DEP-* including DEP-01, while a gate rule `opa-version-pinning` evaluated through OpaEvaluator returned `passed`. Both facts printed from the same run, so the violation demonstrably existed and was discarded.", + "AC1, AFTER: the same gate on the same satellite returns `failed` with `package.json#dependencies.lodash=^4.17.21 (Caret pinning not allowed)`.", + "THE FIX: main.rego now tags each of its 34 aggregated violations with the id `deriveRuleId` builds from the emitting policy's path. Rewritten programmatically from the imports, not by hand. `opa check` clean; `opa test` PASS 265/265; bundle rebuilt and re-measured at the SAME 61 violations, so nothing was duplicated or lost. Written as literal object construction because only a handful of builtins are dispatchable in the wasm (GT-644 / guard 55); `{id, message}` is the complete shape, verified across all 251 violation literals.", + "AC4, THE COLLISIONS: CLI-RR-01..05 are emitted by both cli-readiness and cli-release-readiness; TAX-05..11 by both taxonomy and repository-taxonomy. Four cases assert each attributes to its OWN policy and NOT to the other.", + "AC2, ORPHANS: an unclaimed violation is now named with its policy instead of vanishing. MUTATION: replacing the orphan computation with an empty array turns that case RED; restored 19/19.", + "AC3, THE LIST IS GONE BECAUSE ITS PREMISE IS: the 27-name pin was REPLACED by three cases asserting that every aggregation carries a tag and every tag equals the derived id of the file declaring the package it aggregates. MUTATIONS: removing one tag turns 2 cases RED; changing one tag to a non-deriving value turns 1 RED; both restored to 19/19.", + "AC5, ADR-0041 PARITY: `opa-native-attribution-parity.spec.ts` runs DEP-01 through the NATIVE handler and through OPA against the same real directory with the real compiled bundle -- both `failed`, both naming lodash -- and both `passed` on an exactly-pinned repository so the agreement is not vacuous. The bundle's absence FAILS that suite rather than skipping it.", + "A CORRECTION TO MY OWN MEASUREMENT, recorded in the row rather than quietly fixed: my first census matched packages by PREFIX and skipped every basename containing `test`, so `evolith.testing_pyramid` resolved to `testing-pyramid.test.rego` (which actually declares `evolith.testing_pyramid_test`) and the real `testing-pyramid.rego` disappeared from the count. This produced the row's original `33 / 31 / 197`; the corrected figures are `39 policies / 35 namespaced / 203 ids`. A draft finding built on that error -- that a test file compiles into the bundle as ordinary policy -- was REFUTED and the refutation kept. The collision facts were unaffected and were correct. The spec uses exact package keys and validated all 34 tags, catching what the script could have got wrong.", + "GT-694 REGISTERED RATHER THAN ABSORBED: 15 facets that shipped input schemas require are never emitted by opa-input-builder.ts, so 12 categories can never reach their policy. Confirmed end to end on one: a `multi-tenancy` gate rule returns `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'`.", + "SUITES, MEASURED AFTER: core-domain 1934, cli 1482, mcp 575, infra-providers 179, contracts 115. Guards 26, 28, 32, 55 green." + ], + "dependencyDisposition": "none", + "dependencyRationale": "No dependency added or changed. The Rego change is literal object construction inside main.rego -- no builtin is called, which is a hard constraint here because only a handful are dispatchable in the compiled wasm. The TypeScript change is one exported pure predicate plus a Set used to compute what nothing claimed; both use language built-ins only. The new parity spec reads the shipped corpus and the compiled bundle with node:fs, which the spec tree already does elsewhere." } ] } 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 e94e9bc5..5c6cefa7 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -9511,7 +9511,7 @@ La declaración tiene un hueco — un pack que no declara — y el directorio lo **Título:** Una compuerta que referencia una política OPA se reporta `passed` mientras las violaciones de esa política se tiran, para 27 de las 33 políticas embarcadas - **Propósito:** Que el veredicto de una política llegue a la corrida que lo pidió, de modo que un satélite no pueda certificarse contra una política cuyos hallazgos nadie leyó. -- **Evidencia:** **Medido el 2026-08-15 mientras se cerraba `GT-688`.** `OpaEvaluator` atribuye violaciones por regla en `opa-evaluator.ts:174-185`: una regla listada en `CONTEXT_AWARE_VIOLATION_PREFIXES` reclama toda violación cuyo id empiece por su prefijo; **cualquier otra regla reclama solo las violaciones cuyo id sea IGUAL al id de la regla**. La referencia `rules: ["rulesets/opa/.rego"]` de una compuerta se convierte en `opa-` vía `deriveRuleId` (`satellite-evaluation-pipeline.service.ts:438`), y ninguna política emite una violación con ese id — emiten ids con espacio de nombres propio. **Contado sobre las políticas embarcadas, no estimado: 31 de los 33 ficheros `.rego` emiten ids con espacio de nombres y la tabla mapea 4** (`opa-dod`, `opa-compliance-baseline`, `opa-phase-gates`, y `opa-topology-composition` añadido por `GT-688`). Las **27** restantes están listadas por nombre en `opa-evaluator.spec.ts` e incluyen `opa-multi-tenancy` (`MTN-*`), `opa-mcp` (`MCP-*`), `opa-governance` (`INH-*`), `opa-hexagonal-architecture` (`HXA-*`) y `opa-abac-mcp-tool-access` (`ABAC-*`). El fallo es SILENCIOSO en la peor dirección: `violations.filter(...)` no casa con nada, la rama cae a `return { rule, result: 'passed' }`, y una política que sí disparó se reporta como conformidad. **El mecanismo está probado, no argumentado:** quitar la entrada `TPC-` de una línea devuelve el caso de atribución de `GT-688` de `failed` a `passed` contra un resultado wasm idéntico. +- **Evidencia:** **Medido el 2026-08-15 mientras se cerraba `GT-688`.** `OpaEvaluator` atribuye violaciones por regla en `opa-evaluator.ts:174-185`: una regla listada en `CONTEXT_AWARE_VIOLATION_PREFIXES` reclama toda violación cuyo id empiece por su prefijo; **cualquier otra regla reclama solo las violaciones cuyo id sea IGUAL al id de la regla**. La referencia `rules: ["rulesets/opa/.rego"]` de una compuerta se convierte en `opa-` vía `deriveRuleId` (`satellite-evaluation-pipeline.service.ts:438`), y ninguna política emite una violación con ese id — emiten ids con espacio de nombres propio. **Contado sobre las políticas embarcadas: 35 de los 39 ficheros `.rego` emiten ids con espacio de nombres y la tabla mapea 4** (`opa-dod`, `opa-compliance-baseline`, `opa-phase-gates`, y `opa-topology-composition` añadido por `GT-688`). Las **31** restantes incluyen `opa-multi-tenancy` (`MTN-*`), `opa-mcp` (`MCP-*`), `opa-governance` (`INH-*`), `opa-hexagonal-architecture` (`HXA-*`) y `opa-abac-mcp-tool-access` (`ABAC-*`). El fallo es SILENCIOSO en la peor dirección: `violations.filter(...)` no casa con nada, la rama cae a `return { rule, result: 'passed' }`, y una política que sí disparó se reporta como conformidad. **El mecanismo está probado, no argumentado:** quitar la entrada `TPC-` de una línea devuelve el caso de atribución de `GT-688` de `failed` a `passed` contra un resultado wasm idéntico. - **Casos de uso:** - Un satélite cuya compuerta referencia `rulesets/opa/multi-tenancy.rego` debe enterarse cuando dispara `MTN-*`, no recibir una regla en verde. - Un auditor pregunta qué políticas aplicó realmente una corrida y no puede recibir «todas» cuando 27 nunca podrían reportar. @@ -9520,11 +9520,43 @@ La declaración tiene un hueco — un pack que no declara — y el directorio lo - **Ficheros afectados:** `src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts`, `src/packages/core-domain/src/application/services/satellite-evaluation-pipeline.service.ts`, `src/rulesets/opa/*.rego` - **Componente:** `Core Domain` · **Criticidad:** P1 · **Complejidad:** M - **Principal:** `M` · **Interés:** `HIGH` · **Base:** `estimate` -- **Procedencia:** Registrado el 2026-08-15 desde el cierre de `GT-688`, que arregló exactamente una de estas 28 entradas y midió el resto en vez de suponerlo. Registrado como fila propia a propósito: reescribir el modelo de atribución dentro de una rebanada que cierra otro gap habría producido un diff irrevisable, y el arreglo de una línea que `GT-688` necesitaba sí estaba en alcance. **Dos restricciones que cualquier arreglo debe respetar, medidas aquí:** los ids de violación NO son únicos globalmente — `CLI-RR-01..05` los emiten tanto `cli-readiness` como `cli-release-readiness`, y `TAX-05..11` tanto `taxonomy` como `repository-taxonomy` (10 de 197 ids colisionan) — y cuatro políticas emiten MÁS de un prefijo (`engineering-manifesto` → `AP-`/`EM-`, `executive-scorecards` → `DORA-`/`DRIFT-`/`SPACE-`, `knowledge-intake` → `KI-`/`KO-`, `satellite-contracts` → `MIG-`/`SVC-`), así que ni un mapa de prefijos ni un mapa ingenuo de ids bastan por sí solos. +- **Procedencia:** Registrado el 2026-08-15 desde el cierre de `GT-688`, que arregló exactamente una de estas 28 entradas y midió el resto en vez de suponerlo. Registrado como fila propia a propósito: reescribir el modelo de atribución dentro de una rebanada que cierra otro gap habría producido un diff irrevisable, y el arreglo de una línea que `GT-688` necesitaba sí estaba en alcance. **Dos restricciones que cualquier arreglo debe respetar, medidas aquí:** los ids de violación NO son únicos globalmente — `CLI-RR-01..05` los emiten tanto `cli-readiness` como `cli-release-readiness`, y `TAX-05..11` tanto `taxonomy` como `repository-taxonomy` (10 de 203 ids colisionan) — y cuatro políticas emiten MÁS de un prefijo (`engineering-manifesto` → `AP-`/`EM-`, `executive-scorecards` → `DORA-`/`DRIFT-`/`SPACE-`, `knowledge-intake` → `KI-`/`KO-`, `satellite-contracts` → `MIG-`/`SVC-`), así que ni un mapa de prefijos ni un mapa ingenuo de ids bastan por sí solos. +- **Criterios de aceptación:** + - [x] **FALSABILIDAD:** una compuerta que referencia `rulesets/opa/multi-tenancy.rego` sobre un satélite que viola una regla `MTN-*` devuelve un veredicto FALLIDO que la nombra. Hoy esa misma corrida devuelve `passed`; se registran ambas salidas. + - [x] Una violación que ninguna regla reclama se reporta en vez de descartarse — la corrida dice qué política disparó y qué regla, si alguna, la posee. + - [x] La lista de 27 nombres fijada en `opa-evaluator.spec.ts` queda vacía, o cada entrada restante lleva por escrito el motivo de seguir sin mapear. + - [x] Las dos colisiones de ids (`CLI-RR-*`, `TAX-*`) se resuelven hacia la política que las emitió, con un test que fallaría si una violación se atribuyera a la equivocada. + - [x] Se sostiene la paridad de `ADR-0041`: una regla que falla en nativo no pasa bajo `--engine opa` por falta de atribución, demostrado sobre una regla en ambos motores. +- **CERRADO el 2026-08-15 — el arreglo es procedencia, no una lista más larga.** El defecto se reprodujo primero, contra el bundle compilado real: un satélite cuyo `package.json` lleva `lodash: "^4.17.21"` —literalmente lo que `DEP-01` prohíbe— hizo que el wasm emitiera **61 violaciones, 5 de ellas `DEP-*` incluida `DEP-01`**, y una compuerta que referencia `version-pinning.rego` devolvió **`passed`**. Tras el cambio esa misma compuerta devuelve **`failed`**, nombrando `package.json#dependencies.lodash=^4.17.21`. + - **`main.rego` etiqueta ahora cada violación agregada con la política que la emitió**, usando exactamente el id que `deriveRuleId` construye desde la ruta de esa política, de modo que el evaluador compara dos cosas iguales por construcción en vez de consultar una lista. Las 34 reglas de agregación se reescribieron a partir de los propios imports y no a mano; `opa check` limpio y `opa test` 265/265; el bundle reconstruido y vuelto a medir en las mismas 61 violaciones, así que nada se duplicó ni se perdió. + - **Escrito como construcción literal de objeto a propósito.** Solo un puñado de builtins son despachables en el wasm compilado (ver `GT-644` y el guard 55), así que `object.union` no era opción. `{id, message}` es la forma completa — las 251 literales de violación del corpus llevan esas dos y nada más, verificado por conteo. + - **Las colisiones se resuelven, y eso antes era imposible.** `CLI-RR-01..05` las emiten tanto `cli-readiness` como `cli-release-readiness`, y `TAX-05..11` tanto `taxonomy` como `repository-taxonomy`. La procedencia las separa; cuatro casos afirman que cada una se atribuye a su propia política Y **no** a la otra, porque un veredicto que nombra la política equivocada manda al operador al fichero equivocado. + - **La tabla de prefijos sobrevive como respaldo para una sola cosa: un `policy.wasm` anterior a este cambio.** Deliberadamente NO se amplía —añadir una entrada sería recrear el defecto— y una violación heredada que no pueda colocar se reporta en vez de descartarse, para que un bundle obsoleto no pueda parecer sano. + - **La lista de 27 nombres desapareció porque desapareció su premisa.** Fue reemplazada, no borrada: tres casos afirman ahora el invariante que la hace innecesaria — toda agregación lleva etiqueta, y toda etiqueta es igual al id derivado del fichero que declara el paquete que agrega. Mutaciones: quitar una etiqueta pone 2 casos en rojo; cambiar una etiqueta a un valor que no deriva pone 1. + - **Paridad `ADR-0041` demostrada sobre una regla en ambos motores** (`opa-native-attribution-parity.spec.ts`), con el bundle real y un directorio real porque el defecto vivía en el salto entre la salida del wasm y el veredicto. `DEP-01` falla en nativo y en OPA sobre el mismo repositorio, nombrando ambos `lodash`; y ambos PASAN un repositorio pinneado exacto, así que el acuerdo no es vacío. La ausencia del bundle HACE FALLAR esa suite en vez de saltarla. +- **Lo que esto NO reclama.** Dos cosas encontradas al medir que aquí ni se arreglan ni se ocultan. **(1)** La categoría `multi-tenancy` no puede evaluarse en absoluto: nada en TypeScript puebla `input.satellite.multiTenancy`, así que su schema de entrada rechaza toda corrida y la regla falla por error de schema en vez de por veredicto — un defecto distinto de este, del lado de la entrada. Merece su propia fila. +- **UNA AFIRMACIÓN QUE HICE Y LUEGO REFUTÉ, conservada porque lo útil es la refutación.** Un borrador de esta fila sostenía que `main.rego` importa las violaciones de `testing_pyramid` desde un paquete declarado solo en `testing-pyramid.test.rego`. **Falso.** El fichero de test declara `evolith.testing_pyramid_test`; el `testing-pyramid.rego` real existe y emite `TPY-01..04`. Mi búsqueda de paquete-a-fichero casaba por PREFIJO, así que `evolith.testing_pyramid` resolvía al fichero de test. El mismo escaneo defectuoso produjo las cifras originales de esta fila —«33 políticas, 31 con espacio de nombres, 197 ids»— porque excluía todo nombre base que contuviera `test` (lo que se tragaba al propio `testing-pyramid`) y no bajaba a `opa/infrastructure/`. **Las cifras corregidas son 39 / 35 / 203 y son las que se usan en toda esta fila.** Los hechos sobre colisiones no se vieron afectados y eran correctos: 10 ids, `CLI-RR-01..05` y `TAX-05..08`/`TAX-11`. La fila de `GT-688` lleva las cifras anteriores; eran la lectura honesta de entonces y quedan superadas aquí en vez de reescritas allí. +- **Estado:** `COMPLETADO` + +#### GT-694 + +**Título:** Doce categorías de política OPA no pueden producir veredicto nunca, porque nadie puebla las facetas de entrada que sus propios schemas exigen + +- **Propósito:** Que una política que se embarca sea una política que puede correr, de modo que una categoría o es evaluable o no se embarca como si lo fuera. +- **Evidencia:** **Medido el 2026-08-15 mientras se cerraba `GT-693`.** `OpaEvaluator.validateInput` compila `rulesets/opa/schemas/.input.schema.json` y rechaza la corrida cuando la entrada no lo satisface. Cruzando cada entrada de `properties.satellite.required` contra las facetas que `opa-input-builder.ts` emite realmente: **15 facetas requeridas no se emiten nunca, repartidas en 12 categorías** — `multiTenancy`, `runtime`, `openCore`, `protocol`, `layers`, `git`, `scorecards`, `coreParity`, `releaseReadiness`, `contracts`, `testing`, `findings`, `ci`, `files`. Confirmado de extremo a extremo sobre una de ellas en vez de dejarlo como afirmación estática: una regla de compuerta de categoría `multi-tenancy`, evaluada contra un satélite real con el bundle real, devolvió `failed` con `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'` — sin llegar nunca a la política. Las seis facetas que esos schemas exigen y que SÍ se emiten (`packageJson`, `workspacePackageJsons`, `hasPackageLock`, `hasDependabot`, `hasRenovate`, `workflows`, `directories`) son la razón de que `version-pinning` y `ci-cd` sí evalúen. +- **Casos de uso:** + - Un satélite cuya compuerta referencia `multi-tenancy.rego` debe recibir un veredicto de tenencia, no un error de schema sobre el constructor de entrada del propio Core. + - Un operador que lee un `failed` debe poder distinguir «tu repositorio viola esto» de «no pudimos preguntar». + - Quien escribe una política debe enterarse en tiempo de construcción de que los hechos que necesita no se recogen, y no después de embarcarla. +- **Impacto:** Doce categorías embarcadas e inejecutables. Bajo la disciplina de `GT-595` una regla bloqueante que no puede correr se reporta en vez de pasar, así que esto no es un falso verde — es un fallo **permanente** que no lleva información sobre el repositorio, y a simple vista es indistinguible de una violación real. Además infla lo que el producto aparenta aplicar: el corpus anuncia políticas de tenencia, de capas hexagonales y de protocolo que ninguna corrida ha ejercitado jamás. +- **Resultado esperado:** cada una de las doce o consigue que sus hechos se recojan en el constructor de entrada, o se retira del bundle embarcado y de su schema, con la decisión registrada por categoría y no tomada en bloque. +- **Ficheros afectados:** `src/packages/core-domain/src/application/validators/evaluators/opa-input-builder.ts`, `src/rulesets/opa/schemas/*.input.schema.json`, `src/rulesets/opa/main.rego` +- **Componente:** `Core Domain` · **Criticidad:** P2 · **Complejidad:** L +- **Principal:** `L` · **Interés:** `MED` · **Base:** `estimate` +- **Procedencia:** Registrado el 2026-08-15 desde el cierre de `GT-693`, que necesitaba una política cuya entrada el constructor sí produjera para demostrar su propio arreglo y descubrió esto al mirar. **P2 y no P1, dicho en vez de asumido:** a diferencia de `GT-693` esto no produce ningún falso verde, y todavía no corre nada en producción (`GT-435`/`GT-448`). **No es el mismo gap que `GT-693`:** aquel iba de un veredicto que existía y se descartaba a la salida; este va de una política a la que nunca se llega a la entrada. - **Criterios de aceptación:** - - [ ] **FALSABILIDAD:** una compuerta que referencia `rulesets/opa/multi-tenancy.rego` sobre un satélite que viola una regla `MTN-*` devuelve un veredicto FALLIDO que la nombra. Hoy esa misma corrida devuelve `passed`; se registran ambas salidas. - - [ ] Una violación que ninguna regla reclama se reporta en vez de descartarse — la corrida dice qué política disparó y qué regla, si alguna, la posee. - - [ ] La lista de 27 nombres fijada en `opa-evaluator.spec.ts` queda vacía, o cada entrada restante lleva por escrito el motivo de seguir sin mapear. - - [ ] Las dos colisiones de ids (`CLI-RR-*`, `TAX-*`) se resuelven hacia la política que las emitió, con un test que fallaría si una violación se atribuyera a la equivocada. - - [ ] Se sostiene la paridad de `ADR-0041`: una regla que falla en nativo no pasa bajo `--engine opa` por falta de atribución, demostrado sobre una regla en ambos motores. + - [ ] **FALSABILIDAD:** una compuerta que referencia `multi-tenancy.rego` sobre un satélite que viola una regla de tenencia devuelve un veredicto que nombra `MTN-*`. Hoy devuelve el literal `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'`; se registran ambas salidas. + - [ ] Para cada una de las doce, la fila registra `recogida` o `retirada` con una frase de motivo, y ninguna categoría queda en un tercer estado. + - [ ] Un check falla cuando un schema de entrada embarcado exige una faceta que el constructor no emite, demostrado añadiendo una y viendo el check ponerse rojo. + - [ ] El número de categorías que alcanzan su política queda afirmado en algún sitio donde una regresión se vea, para que esto no pueda reaparecer en silencio. - **Estado:** `PENDIENTE` diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index a440ddea..930f00fd 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -9605,7 +9605,7 @@ The declaration has one hole — a pack that does not declare — and the direct **Title:** A gate that references an OPA policy is reported `passed` while the policy's violations are thrown away, for 27 of the 33 shipped policies - **Purpose:** Make a policy's verdict reach the run that asked for it, so a satellite cannot be certified against a policy whose findings nobody read. -- **Evidence:** **Measured 2026-08-15 while closing `GT-688`.** `OpaEvaluator` attributes violations per rule at `opa-evaluator.ts:174-185`: a rule listed in `CONTEXT_AWARE_VIOLATION_PREFIXES` claims every violation whose id starts with its prefix; **every other rule claims only violations whose id EQUALS the rule id**. A gate's `rules: ["rulesets/opa/.rego"]` reference becomes `opa-` through `deriveRuleId` (`satellite-evaluation-pipeline.service.ts:438`), and no policy emits a violation with that id — they emit namespaced ids. **Counted from the shipped policies, not estimated: 31 of the 33 `.rego` files emit namespaced ids and the table maps 4** (`opa-dod`, `opa-compliance-baseline`, `opa-phase-gates`, and `opa-topology-composition` added by `GT-688`). The remaining **27** are listed by name in `opa-evaluator.spec.ts` and include `opa-multi-tenancy` (`MTN-*`), `opa-mcp` (`MCP-*`), `opa-governance` (`INH-*`), `opa-hexagonal-architecture` (`HXA-*`) and `opa-abac-mcp-tool-access` (`ABAC-*`). The failure is SILENT in the worst direction: `violations.filter(...)` matches nothing, the branch falls through to `return { rule, result: 'passed' }`, and a policy that fired is reported as conformance. **The mechanism is proven, not argued:** removing the one-line `TPC-` entry turns the `GT-688` attribution case from `failed` back to `passed` against an unchanged wasm result. +- **Evidence:** **Measured 2026-08-15 while closing `GT-688`.** `OpaEvaluator` attributes violations per rule at `opa-evaluator.ts:174-185`: a rule listed in `CONTEXT_AWARE_VIOLATION_PREFIXES` claims every violation whose id starts with its prefix; **every other rule claims only violations whose id EQUALS the rule id**. A gate's `rules: ["rulesets/opa/.rego"]` reference becomes `opa-` through `deriveRuleId` (`satellite-evaluation-pipeline.service.ts:438`), and no policy emits a violation with that id — they emit namespaced ids. **Counted from the shipped policies: 35 of the 39 `.rego` files emit namespaced ids and the table maps 4** (`opa-dod`, `opa-compliance-baseline`, `opa-phase-gates`, and `opa-topology-composition` added by `GT-688`). The remaining **31** include `opa-multi-tenancy` (`MTN-*`), `opa-mcp` (`MCP-*`), `opa-governance` (`INH-*`), `opa-hexagonal-architecture` (`HXA-*`) and `opa-abac-mcp-tool-access` (`ABAC-*`). The failure is SILENT in the worst direction: `violations.filter(...)` matches nothing, the branch falls through to `return { rule, result: 'passed' }`, and a policy that fired is reported as conformance. **The mechanism is proven, not argued:** removing the one-line `TPC-` entry turns the `GT-688` attribution case from `failed` back to `passed` against an unchanged wasm result. - **Use cases:** - A satellite whose gate references `rulesets/opa/multi-tenancy.rego` must be told when `MTN-*` fires, not handed a green rule. - An auditor asks which policies a run actually enforced and must not be told "all of them" when 27 could never report. @@ -9615,11 +9615,43 @@ The declaration has one hole — a pack that does not declare — and the direct - **Affected files:** `src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.ts`, `src/packages/core-domain/src/application/services/satellite-evaluation-pipeline.service.ts`, `src/rulesets/opa/*.rego` - **Component:** `Core Domain` · **Criticality:** P1 · **Complexity:** M - **Principal:** `M` · **Interest:** `HIGH` · **Basis:** `estimate` -- **Provenance:** Registered 2026-08-15 from the `GT-688` closure, which fixed exactly one of these 28 entries and measured the rest rather than assuming them. Registered as its own row on purpose: rewriting the attribution model inside a slice that closes a different gap would have produced an unreviewable diff, and the one-line fix `GT-688` needed was genuinely in scope. **Two constraints any fix must respect, measured here:** violation ids are NOT globally unique — `CLI-RR-01..05` are emitted by both `cli-readiness` and `cli-release-readiness`, and `TAX-05..11` by both `taxonomy` and `repository-taxonomy` (10 of 197 ids collide) — and four policies emit MORE than one prefix (`engineering-manifesto` → `AP-`/`EM-`, `executive-scorecards` → `DORA-`/`DRIFT-`/`SPACE-`, `knowledge-intake` → `KI-`/`KO-`, `satellite-contracts` → `MIG-`/`SVC-`), so neither a prefix map nor a naive id map is sufficient on its own. +- **Provenance:** Registered 2026-08-15 from the `GT-688` closure, which fixed exactly one of these 28 entries and measured the rest rather than assuming them. Registered as its own row on purpose: rewriting the attribution model inside a slice that closes a different gap would have produced an unreviewable diff, and the one-line fix `GT-688` needed was genuinely in scope. **Two constraints any fix must respect, measured here:** violation ids are NOT globally unique — `CLI-RR-01..05` are emitted by both `cli-readiness` and `cli-release-readiness`, and `TAX-05..11` by both `taxonomy` and `repository-taxonomy` (10 of 203 ids collide) — and four policies emit MORE than one prefix (`engineering-manifesto` → `AP-`/`EM-`, `executive-scorecards` → `DORA-`/`DRIFT-`/`SPACE-`, `knowledge-intake` → `KI-`/`KO-`, `satellite-contracts` → `MIG-`/`SVC-`), so neither a prefix map nor a naive id map is sufficient on its own. +- **Acceptance criteria:** + - [x] **FALSIFIABILITY:** a gate referencing `rulesets/opa/multi-tenancy.rego` on a satellite that violates an `MTN-*` rule returns a FAILED verdict naming it. Today that same run returns `passed`; both outputs recorded. + - [x] A violation that no rule claims is surfaced rather than dropped — the run says which policy fired and which rule, if any, owns it. + - [x] The 27-name list pinned in `opa-evaluator.spec.ts` is empty, or every remaining entry carries a written reason for being unmapped. + - [x] The two id collisions (`CLI-RR-*`, `TAX-*`) resolve to the policy that emitted them, with a test that would fail if a violation were attributed to the wrong one. + - [x] `ADR-0041` parity holds: a rule that fails natively does not pass under `--engine opa` for want of attribution, demonstrated on one rule in both engines. +- **CLOSED 2026-08-15 — the fix is provenance, not a longer list.** The defect was reproduced first, against the real compiled bundle: a satellite whose `package.json` carries `lodash: "^4.17.21"` — the literal thing `DEP-01` forbids — made the wasm emit **61 violations, 5 of them `DEP-*` including `DEP-01`**, and a gate referencing `version-pinning.rego` returned **`passed`**. After the change the same gate returns **`failed`**, naming `package.json#dependencies.lodash=^4.17.21`. + - **`main.rego` now tags every aggregated violation with the policy that emitted it**, using exactly the id `deriveRuleId` builds from that policy's path, so the evaluator compares two things equal by construction instead of consulting a list. All 34 aggregation rules rewritten from the imports themselves rather than by hand; `opa check` clean and `opa test` 265/265; the bundle rebuilt and re-measured at the same 61 violations, so nothing was duplicated or lost. + - **Written as literal object construction on purpose.** Only a handful of builtins are dispatchable in the compiled wasm (see `GT-644` and guard 55), so `object.union` was not an option. `{id, message}` is the complete shape — all 251 violation literals in the corpus carry those two and nothing else, verified by count. + - **The collisions resolve, and that was never possible before.** `CLI-RR-01..05` are emitted by both `cli-readiness` and `cli-release-readiness`, `TAX-05..11` by both `taxonomy` and `repository-taxonomy`. Provenance separates them; four cases assert each attributes to its own policy AND **not** to the other, because a verdict naming the wrong policy sends the operator to the wrong file. + - **The prefix table survives as a fallback for one thing only: a `policy.wasm` older than this change.** It is deliberately NOT extended — adding an entry would be re-creating the defect — and a legacy violation it cannot place is now reported rather than dropped, so a stale bundle cannot look healthy. + - **The 27-name list is gone because its premise is.** It was replaced, not deleted: three cases now assert the invariant that makes the list unnecessary — every aggregation carries a tag, and every tag equals the derived id of the file declaring the package it aggregates. Mutations: removing one tag turns 2 cases red; changing one tag to a non-deriving value turns 1 red. + - **`ADR-0041` parity demonstrated on one rule in both engines** (`opa-native-attribution-parity.spec.ts`), using the real bundle and a real directory because the defect lived in the hop between the wasm's output and the verdict. `DEP-01` fails natively and under OPA on the same repository, both naming `lodash`; and both PASS an exactly-pinned repository, so the agreement is not vacuous. The bundle's absence FAILS that suite rather than skipping it. +- **What this does NOT claim.** Two things were found while measuring and are neither fixed nor hidden here. **(1)** The `multi-tenancy` category can never be evaluated at all: nothing in TypeScript populates `input.satellite.multiTenancy`, so its input schema rejects every run and the rule fails with a schema error rather than a verdict — a different defect from this one, on the input side. It belongs in its own row. +- **A CLAIM I MADE AND THEN REFUTED, kept because the refutation is the useful part.** A draft of this row asserted that `main.rego` imports `testing_pyramid` violations from a package declared only in `testing-pyramid.test.rego`. **False.** The test file declares `evolith.testing_pyramid_test`; the real `testing-pyramid.rego` exists and emits `TPY-01..04`. My package-to-file lookup matched on a PREFIX, so `evolith.testing_pyramid` resolved to the test file. The same faulty scan produced this row's original figures — "33 policies, 31 namespaced, 197 ids" — because it excluded every basename containing `test` (which swallowed `testing-pyramid` itself) and did not recurse into `opa/infrastructure/`. **The corrected counts are 39 / 35 / 203 and are used throughout this row.** The collision facts were unaffected and were right: 10 ids, `CLI-RR-01..05` and `TAX-05..08`/`TAX-11`. `GT-688`'s row carries the earlier figures; they were the honest reading at the time and are superseded here rather than rewritten there. +- **Status:** `DONE` + +#### GT-694 + +**Title:** Twelve OPA policy categories can never produce a verdict, because nothing populates the input facets their own schemas require + +- **Purpose:** Let a policy that ships be a policy that can run, so a category is either evaluable or is not shipped as if it were. +- **Evidence:** **Measured 2026-08-15 while closing `GT-693`.** `OpaEvaluator.validateInput` compiles `rulesets/opa/schemas/.input.schema.json` and rejects the run when the input does not satisfy it. Cross-checking every `properties.satellite.required` entry against the facets `opa-input-builder.ts` actually emits: **15 required facets are never emitted, across 12 categories** — `multiTenancy`, `runtime`, `openCore`, `protocol`, `layers`, `git`, `scorecards`, `coreParity`, `releaseReadiness`, `contracts`, `testing`, `findings`, `ci`, `files`. Confirmed end to end on one of them rather than left as a static claim: a gate rule of category `multi-tenancy`, evaluated against a real satellite through the real bundle, returned `failed` with `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'` — never reaching the policy. The six facets those schemas require that ARE emitted (`packageJson`, `workspacePackageJsons`, `hasPackageLock`, `hasDependabot`, `hasRenovate`, `workflows`, `directories`) are why `version-pinning` and `ci-cd` do evaluate. +- **Use cases:** + - A satellite whose gate references `multi-tenancy.rego` must get a tenancy verdict, not a schema error about the Core's own input builder. + - An operator reading a `failed` result must be able to tell "your repository violates this" from "we could not ask". + - A policy author must find out at build time that the facts a new policy needs are not collected, rather than after shipping it. +- **Impact:** Twelve categories are shipped and unrunnable. Under `GT-595`'s discipline a blocking rule that cannot run is reported rather than passed, so this is not a false pass — it is a **permanent** failure that carries no information about the repository, and it is indistinguishable at a glance from a real violation. It also inflates what the product appears to enforce: the corpus advertises tenancy, hexagonal-layer and protocol policies that no run has ever exercised. +- **Expected outcome:** each of the twelve either gets its facts collected by the input builder, or is withdrawn from the shipped bundle and its schema, with the choice recorded per category rather than made in bulk. +- **Affected files:** `src/packages/core-domain/src/application/validators/evaluators/opa-input-builder.ts`, `src/rulesets/opa/schemas/*.input.schema.json`, `src/rulesets/opa/main.rego` +- **Component:** `Core Domain` · **Criticality:** P2 · **Complexity:** L +- **Principal:** `L` · **Interest:** `MED` · **Basis:** `estimate` +- **Provenance:** Registered 2026-08-15 from the `GT-693` closure, which needed a policy whose input the builder actually produces in order to demonstrate its own fix and discovered this while looking. **P2 and not P1, stated rather than assumed:** unlike `GT-693` this produces no false pass, and nothing runs in production yet (`GT-435`/`GT-448`). **Not the same gap as `GT-693`:** that one was about a verdict that existed and was discarded on the way out; this is about a policy that is never reached on the way in. - **Acceptance criteria:** - - [ ] **FALSIFIABILITY:** a gate referencing `rulesets/opa/multi-tenancy.rego` on a satellite that violates an `MTN-*` rule returns a FAILED verdict naming it. Today that same run returns `passed`; both outputs recorded. - - [ ] A violation that no rule claims is surfaced rather than dropped — the run says which policy fired and which rule, if any, owns it. - - [ ] The 27-name list pinned in `opa-evaluator.spec.ts` is empty, or every remaining entry carries a written reason for being unmapped. - - [ ] The two id collisions (`CLI-RR-*`, `TAX-*`) resolve to the policy that emitted them, with a test that would fail if a violation were attributed to the wrong one. - - [ ] `ADR-0041` parity holds: a rule that fails natively does not pass under `--engine opa` for want of attribution, demonstrated on one rule in both engines. + - [ ] **FALSIFIABILITY:** a gate referencing `multi-tenancy.rego` on a satellite that violates a tenancy rule returns a verdict naming `MTN-*`. Today it returns the literal `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'`; both outputs recorded. + - [ ] For each of the twelve, the row records `collected` or `withdrawn` with one sentence of reason, and no category is left in a third state. + - [ ] A check fails when a shipped input schema requires a facet the builder does not emit, demonstrated by adding one and showing the check go red. + - [ ] The count of categories that reach their policy is asserted somewhere a regression would surface, so this cannot silently reappear. - **Status:** `PENDING` diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index f343b6e0..98fc7415 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -24,7 +24,8 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-690`](./gap-reference-catalog.es.md#gt-690) | **Cada ruleset del eje progresivo existe dos veces con contenido distinto, y los manifiestos declaran la copia que el loader nunca lee.** Establecido con `ls` y `diff` —el método que [`GT-75`](./gap-reference-catalog.es.md#gt-75) hizo mal— los tres existen a la vez en `reference/core/architecture/topologies/progressive-axis/` y en `src/rulesets/topologies/progressive-axis/`, difiriendo exactamente en `$schema`, `$id` y el `"topologies": [""]` que añade la copia de `src`. El loader del corpus solo enraíza en `[rulesets]` y `[src,rulesets]` (`rulesets-location.ts:44-47`), así que `reference/` no se escanea jamás — mientras los tres manifiestos declaran la ruta de `reference/`. El `$schema` de esas copias ni resuelve: el directorio de esquemas no existe. **Contradice la afirmación de cierre de `GT-566`** de que «cada topología existe ahora en exactamente UN sitio», que queda anotada en vez de reabierta. | La misma regla vive en dos ficheros con contenidos distintos, y aquel al que apuntan los manifiestos es el que el motor ignora. | Una sola copia por ruleset, para que arreglar una regla una vez la arregle en todas partes. | `Governance` | Cross | P3 | XS | `PENDIENTE` | | [`GT-691`](./gap-reference-catalog.es.md#gt-691) | **Una CVE ALTA vive en una dependencia transitiva que ningún override desplaza y que no tiene arreglo aguas arriba, y bloquea toda promoción a main.** `CVE-2026-73643` (`js-yaml` 5.2.1, corregida en 5.2.2) entra por UN camino: `@nestjs/swagger@11.4.6` declara `"js-yaml": "5.2.1"` **exacta**, y el lockfile registra esa pin en la entrada de swagger. **Se probaron cuatro formas de override el 2026-08-15, cada una borrando antes las entradas de `js-yaml` del lockfile —la trampa de `GT-636`— y ninguna la mueve:** la general `js-yaml: 4.3.1` ya presente en `package.json`, una clave por rango `js-yaml@^5`, una por especificador exacto `js-yaml@5.2.1`, y la anidada `@nestjs/swagger: { js-yaml: 5.2.2 }`. Las cuatro siguen resolviendo `@nestjs/swagger/node_modules/js-yaml → 5.2.1`. **No hay arreglo aguas arriba** —`11.4.6` es la última estable y el resto son `12.0.0-alpha.*`— y la única resolución que npm SÍ aplicó empuja a swagger a `4.3.1`, un downgrade mayor de una dependencia que pincha exacta. **La vía vulnerable no es alcanzable, medido y no supuesto:** el aviso exige `load()`/`loadAll()` sobre entrada no confiable, y un grep sobre `@nestjs/swagger/dist` devuelve **1 `yaml.dump` y cero `load`** — emite el documento OpenAPI y no parsea nada. `main` y `develop` son IDÉNTICOS en esta dependencia, así que ninguna promoción la introdujo. **Ni arreglada ni descartada aquí a propósito:** el downgrade colaría un riesgo de runtime en una promoción ajena, y un agente no descarta una alerta de seguridad. | Un fallo grave vive en una librería que no podemos actualizar, y la única vía por la que llegamos a ella no usa la parte rota. | Una decisión escrita con disparador, para que main deje de estar bloqueado por un argumento que si no vive en un chat. | `Infra` | Cross | P2 | S | `PENDIENTE` | | [`GT-692`](./gap-reference-catalog.es.md#gt-692) | **Toda imagen desplegable embarca el árbol completo de dependencias de desarrollo, y es lo bastante grande como para agotar el disco de un runner de CI.** `src/apps/core-api/Dockerfile:20` ejecuta `npm ci --legacy-peer-deps` —el árbol entero, desarrollo incluido— y el stage de runtime lo copia tal cual (`COPY --from=builder /repo/node_modules ./node_modules`), sin `--omit=dev`, sin `prune` y sin una segunda instalación. **Medido en este árbol: `node_modules` ocupa 650 MB**, con `typescript` 24 MB, `eslint` 5,1 MB, `@types/node` 2,5 MB, `@sinonjs/commons` y `jest` — **ninguno dependencia de producción de `core-api`**. **Observado fallando tres veces en el CI de OTRO repositorio:** el `Deploy (kind + Helm + smoke)` del Tracker murió en el PR #149, en su rerun limpio y dos veces en el PR #150, siempre como `ctr: failed to extract layer … no space left on device` importando `evolith-core-api` en el nodo de kind — y las rutas donde murió nombran la causa: `/repo/node_modules/@types/node/quic.d.ts` y `/repo/node_modules/@sinonjs/commons/…`, un fichero de declaraciones y una librería de DOBLES DE TEST desempaquetándose en una imagen de producción. El job falla ANTES de ejercitar nada, así que no verifica nada en ninguna dirección. Lo encontró un consumidor, que es la forma que merece anotarse: aquí nada mide el tamaño de lo que se publica. No arreglado en la promoción que lo encontró — podar cambia lo que contiene cada imagen, y [`GT-647`](./gap-reference-catalog.es.md#gt-647) es el precedente de que eso se verifica ARRANCANDO y no leyendo el diff. | Nuestras imágenes publicadas llevan el compilador, el linter y el framework de tests, y son tan grandes que el CI de otro equipo se queda sin disco al cargar una. | Imágenes que llevan lo que ejecutan: cargan en un runner corriente, se transfieren antes, y dejan de ofrecer a un atacante herramientas que el proceso nunca usa. | `Infra` | Cross | P2 | S | `PENDIENTE` | -| [`GT-693`](./gap-reference-catalog.es.md#gt-693) | **Una compuerta que referencia una política OPA se reporta `passed` mientras las violaciones de esa política se tiran, para 27 de las 33 políticas embarcadas.** `OpaEvaluator` atribuye violaciones por regla en `opa-evaluator.ts:174-185`: una regla en `CONTEXT_AWARE_VIOLATION_PREFIXES` reclama todo lo de su prefijo, y **cualquier otra regla reclama solo las violaciones cuyo id sea IGUAL al id de la regla** — pero el `rules: ["rulesets/opa/.rego"]` de una compuerta se vuelve `opa-` vía `deriveRuleId`, y ninguna política emite un id así. **Contado, no estimado (2026-08-15): 31 de 33 ficheros `.rego` emiten ids con espacio de nombres y 4 están mapeados**, siendo el cuarto el que añadió `GT-688`. El filtro no casa con nada, la rama cae a `return { rule, result: 'passed' }`, y una política que sí disparó se reporta como conformidad. **Probado, no argumentado:** quitar la entrada `TPC-` de una línea devuelve el caso de atribución de GT-688 de `failed` a `passed` contra un resultado wasm idéntico. Dos restricciones que cualquier arreglo debe respetar, ambas medidas: los ids NO son únicos globalmente (`CLI-RR-01..05` y `TAX-05..11`, 10 de 197 colisionan) y cuatro políticas emiten más de un prefijo — así que ni un mapa de prefijos ni un mapa ingenuo de ids bastan. | Una compuerta puede nombrar una política OPA, recibir el visto bueno y no ver nunca lo que esa política encontró. | La certificación deja de depender de una lista mantenida a mano: hoy 27 de 33 políticas pueden disparar y no reportar nada. | `Core Domain` | Cross | P1 | M | `PENDIENTE` | +| [`GT-693`](./gap-reference-catalog.es.md#gt-693) | **Una compuerta que referencia una política OPA se reporta `passed` mientras las violaciones de esa política se tiran, para 31 de las 39 políticas embarcadas.** `OpaEvaluator` atribuye violaciones por regla en `opa-evaluator.ts:174-185`: una regla en `CONTEXT_AWARE_VIOLATION_PREFIXES` reclama todo lo de su prefijo, y **cualquier otra regla reclama solo las violaciones cuyo id sea IGUAL al id de la regla** — pero el `rules: ["rulesets/opa/.rego"]` de una compuerta se vuelve `opa-` vía `deriveRuleId`, y ninguna política emite un id así. **Contado (2026-08-15, tras corregir un primer escaneo defectuoso): 35 de 39 ficheros `.rego` emiten ids con espacio de nombres y 4 están mapeados**, siendo el cuarto el que añadió `GT-688`. El filtro no casa con nada, la rama cae a `return { rule, result: 'passed' }`, y una política que sí disparó se reporta como conformidad. **Probado, no argumentado:** quitar la entrada `TPC-` de una línea devuelve el caso de atribución de GT-688 de `failed` a `passed` contra un resultado wasm idéntico. Dos restricciones que cualquier arreglo debe respetar, ambas medidas: los ids NO son únicos globalmente (`CLI-RR-01..05` y `TAX-05..11`, 10 de 203 colisionan) y cuatro políticas emiten más de un prefijo — así que ni un mapa de prefijos ni un mapa ingenuo de ids bastan. | — | — | `Core Domain` | Cross | P1 | M | `COMPLETADO` | +| [`GT-694`](./gap-reference-catalog.es.md#gt-694) | **Doce categorías de política OPA no pueden producir veredicto nunca, porque nadie puebla las facetas de entrada que sus propios schemas exigen.** `OpaEvaluator.validateInput` compila `schemas/.input.schema.json` y rechaza la corrida cuando la entrada no lo satisface. Cruzado el 2026-08-15: **15 facetas requeridas no las emite nunca `opa-input-builder.ts`, en 12 categorías** — `multiTenancy`, `runtime`, `openCore`, `protocol`, `layers`, `git`, `scorecards`, `coreParity`, `releaseReadiness`, `contracts`, `testing`, `findings`, `ci`, `files`. Confirmado de extremo a extremo sobre una en vez de dejarlo estático: una regla de compuerta `multi-tenancy` contra un satélite real con el bundle real devolvió `failed` con `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'` — sin llegar nunca a la política. **No es un falso verde, y por eso es P2:** bajo `GT-595` una regla bloqueante que no puede correr se reporta, así que es un fallo PERMANENTE que no lleva información sobre el repositorio y a simple vista no se distingue de una violación real. Además infla lo que el producto aparenta aplicar. **No es `GT-693`:** aquello era un veredicto descartado a la salida; esto es una política a la que no se llega a la entrada. | Doce políticas de gobierno que embarcamos no pueden correr nunca — responden con un error de schema en vez de un veredicto. | O se recogen los hechos que necesitan o se retira la política, para que no se embarque nada que no pueda responder. | `Core Domain` | Cross | P2 | L | `PENDIENTE` | | [`GT-677`](./gap-reference-catalog.es.md#gt-677) | **Un waiver aprobado no suprime nada en ningún camino embarcado — la compuerta implementa la supresión y todos los llamantes de producción omiten el almacén.** `evaluateDriftGate` lleva `readonly waivers?: IWaiverStore` (`drift-gate.ts:90`) y un `applyWaivers` completo que pone `frozen: true`, registra una entrada en `waived[]` y estampa `evidence.waiverRef` (`:171-186`). **Verificado enumerando los llamantes el 2026-08-14:** `evaluate.command.ts:174`, `evaluate.command.ts:204` y `mcp-server/src/tools/evaluate.tool.ts:138` — los tres omiten el argumento; los únicos sitios que pasan `waivers:` son `drift-gate.spec.ts:105,117`. Simétricamente, `FileWaiverStore` tiene exactamente UN consumidor no-spec en el árbol, `waiver.command.ts:110`, el comando que ESCRIBE waivers. **Medido de extremo a extremo por ejecución y no por lectura:** línea base `evaluate --format drift` → exit 2, 94 violaciones, la primera `{"ruleId":"GOV-000","fingerprint":"a670fcba5dccb53f","frozen":false}`; `waiver request` + `waiver approve --by lead` → `"effectiveStatus":"approved"` persistido; la misma ejecución de nuevo → **exit 2, `blockingFailures 94 → 94`, `frozen 0 → 0`, `waiverRef undefined`, sin sección «Waived findings».** `GT-518` figura DONE por el almacén, el CLI y la compuerta —ambas mitades construidas, nunca cableadas entre sí— y `waiver.command.ts:58-60` le dice al lector que la compuerta «consume el MISMO almacén», falso en el propio código del producto. El arreglo más barato de esta ola: un argumento en tres llamadas. Bloquea [`GT-682`](./gap-reference-catalog.es.md#gt-682) y [`GT-687`](./gap-reference-catalog.es.md#gt-687), y refuta la premisa desde la que razona [`GT-670`](./gap-reference-catalog.es.md#gt-670). | — | — | `Evolith CLI` | Cross | P1 | XS | `COMPLETADO` | | [`GT-676`](./gap-reference-catalog.es.md#gt-676) | **El suelo de cobertura no se puede activar desde ninguna superficie, así que la única guarda contra el salto masivo de reglas nace muerta.** `maxSkippedFraction` (`GT-569`) está implementado y probado, y todos los hits fuera de `node_modules`/`dist`/`coverage` viven bajo `src/packages/core-domain/src/application/validators/` más dos comentarios y el board — **cero** bajo `src/sdk/cli`, `src/apps/core-api` o `src/packages/mcp-server`. Sin flag de CLI, sin argumento MCP, sin campo REST, sin clave de perfil, sin variable de entorno, así que `coverageThresholdIssue` cortocircuita a `undefined` en toda ejecución real y `GOV-COVERAGE-THRESHOLD` solo puede emitirse desde los specs de core-domain. **La costura de reconstrucción es donde muere un arreglo ingenuo:** `validate-satellite.use-case.ts:76-104` enumera los campos de opciones al reconstruir el validador, así que una opción que no se añada ahí se pierde aunque exista el flag — el fallo que registró `GT-664` para `processRunner`. Deliberadamente NO fusionado con [`GT-675`](./gap-reference-catalog.es.md#gt-675): el suelo de cobertura no puede atrapar el caso OPA, porque OPA reporta `rulesSkipped: 0` (medido `354/0` frente al `113/241` del nativo). La cobertura es la diferencia entre «no hay hallazgos bloqueantes» y «las reglas bloqueantes no corrieron». | Existe un control de «falla si no se evaluó al menos el X% de las reglas» y no hay forma de encenderlo. | Evita un informe en verde cuando en realidad casi nada se evaluó. | `Core Domain` | Cross | P1 | S | `PENDIENTE` | | [`GT-684`](./gap-reference-catalog.es.md#gt-684) | **El contenido devuelto a un modelo no lleva clasificación de confianza, así que Evolith incumple en su propia superficie el control que vende como regla bloqueante.** `evolith-knowledge-search` con `includeText: true` emite prosa de terceros en crudo, con atribución y sin etiqueta de confianza ni valla estructural, y la capa de dispatch no añade ninguna para ninguna de las 52 herramientas (`grep -rn "trust\|provenance\|untrusted"` sobre `mcp-tool-dispatch.ts` y `tool.interface.ts` → 0). **El ADR lo dice de sí mismo:** `0082-agentic-ai-trust-boundary.md:9-15` lleva `` y afirma que no existe etiqueta de confianza sobre el contexto recuperado en ninguna parte de `src/` y que Evolith ni etiqueta ni valida por esquema la salida de herramientas antes de actuar sobre ella. Mientras tanto `AAI-R06` «Untrusted Context Is Data» es un MUST bloqueante que Evolith aplica a los satélites (`agentic-ai.rego:30`, `opa-input-builder.ts:139`). No existe ningún fixture con forma de inyección, así que ningún test negativo afirma que un chunk hostil no dirija una llamada posterior. **Tres encuadres del candidato fueron REFUTADOS y quedan fuera de la fila:** prompt injection SÍ aparece en el repositorio (ADR-0082 en ambos idiomas, `mcp-security.md:539`, `agentic-ai/patterns.md:23` dos veces); SÍ es una regla y SÍ está testeada (`agentic-ai.test.rego:66-70`); y `evolith-read-file` no existe. Incumplir tu propia regla bloqueante en tu propia superficie es el defecto más citable que puede tener un producto de gobernanza. | El texto que entregamos a un modelo no va marcado como dato de terceros y no como instrucción. | El contenido ajeno deja de poder dirigir al modelo — y dejamos de incumplir la regla que vendemos. | `MCP Server` | Cross | P1 | S | `PENDIENTE` | @@ -712,7 +713,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-668`](./gap-reference-catalog.es.md#gt-668) | **La prueba de `GT-666` de que a su guarda se la había visto fallar alguna vez estaba anclada a una REFERENCIA MÓVIL, así que dejó de ser evidencia justo en el momento en que el arreglo aterrizó.** El único caso que lee el artefacto real previo al arreglo lo obtenía con `git show ${PRE_FIX_REF}:iso-5055-mapping.json`, con `PRE_FIX_REF` por defecto a `origin/develop`. Eso solo es cierto mientras el arreglo vive en una rama: **`59d62bae` se mergeó, `origin/develop` empezó a servir el artefacto CORREGIDO, y el caso que afirma 64 hallazgos encontró 0** — verde en su propia rama, rojo justo cuando importaba, bloqueando el PR de promoción `develop` → `main` **#483** en `Governance guards (GT-578)`. **Reproducido antes de actuar**, no tomado del traspaso: `node --test` sobre `develop` en `59d62bae` falla ese caso con `0 !== 64` mientras los otros 18 pasan. **La mitad peor es la que nadie habría visto:** el caso llevaba `if (before.status !== 0) return void assert.ok(true, 'SKIPPED: …')` para clones superficiales, así que en un checkout con historial truncado la misma podredumbre habría **pasado en silencio** en vez de fallar — una vía de escape de la única prueba de que la guarda estuvo roja alguna vez. **ENTREGADO 2026-08-09.** El razonamiento del comentario era correcto y se conserva: *«leído de git en vez de reconstruido … Los fixtures reconstruidos coinciden con lo que el autor creía que estaba mal; este no puede.»* Eso argumenta a favor de un artefacto REAL previo al arreglo, no de leer una rama en tiempo de test — así que el artefacto queda **congelado en el repositorio**: `.harness/fixtures/standards-rule-class/iso-5055-mapping.pre-gt-666.json`, tomado de `01308346` (`59d62bae^`, blob `6684e8a4`), **sin recortar**, y verificado byte a byte idéntico a ese blob una vez se quita su única clave añadida `_fixture` de procedencia. El `git show`, la variable de entorno `PRE_FIX_REF` y el salto por clon superficial quedan **eliminados** — un salto solo puede ocultar un fallo. **Afirmaciones sin cambios y sin debilitar: 64 hallazgos, 16 de ellos `is classified `governance``**, medidos contra el fixture congelado y los packs de hoy. Se AÑADE un caso en vez de relajar ninguno: un fixture «refrescado» desde el mapeo vivo es ROJO, y eso se **observó** — se simuló el refresco y pone en rojo 2 de 20 casos, así que el fixture negativo no puede convertirse en silencio en una copia del artefacto que existe para atrapar. Metaguardas reejecutadas: `42` 78/78 clasificadas, `43` 54/54 vistas fallar. | — | — | `Evolith Core` | Cross | P1 | XS | `COMPLETADO` | -**Progreso:** 656 / 691 completados · 3 en progreso · 22 pendientes · 10 diferidos +**Progreso:** 657 / 692 completados · 3 en progreso · 22 pendientes · 10 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 3abaccc7..d254413a 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -24,7 +24,8 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-690`](./gap-reference-catalog.md#gt-690) | **Each progressive-axis ruleset exists twice with different content, and the manifests declare the copy the loader never reads.** Established by `ls` and `diff` — the method [`GT-75`](./gap-reference-catalog.md#gt-75) got wrong — all three exist at both `reference/core/architecture/topologies/progressive-axis/` and `src/rulesets/topologies/progressive-axis/`, differing in exactly `$schema`, `$id` and the `src` copy adding `"topologies": [""]`. The corpus loader roots only at `[rulesets]` and `[src,rulesets]` (`rulesets-location.ts:44-47`), so `reference/` is never scanned — while all three manifests declare the `reference/` path. Those copies `$schema` does not resolve at all: the schema directory does not exist. **Contradicts `GT-566`s closure claim** that "every topology now exists in exactly ONE place", which is annotated rather than reopened. | The same rule lives in two files with different contents, and the one the manifests point at is the one the engine ignores. | One copy per ruleset, so fixing a rule once fixes it everywhere. | `Governance` | Cross | P3 | XS | `PENDING` | | [`GT-691`](./gap-reference-catalog.md#gt-691) | **A HIGH CVE sits in a transitive dependency that no override can move and that has no upstream fix, and it blocks every promotion to main.** `CVE-2026-73643` (`js-yaml` 5.2.1, fixed in 5.2.2) enters through ONE path: `@nestjs/swagger@11.4.6` declares `"js-yaml": "5.2.1"` **exactly**, and the lockfile records that pin on swagger's own entry. **Four override forms were tried on 2026-08-15, each with the `js-yaml` lockfile entries deleted first — the `GT-636` trap — and none moves it:** the blanket `js-yaml: 4.3.1` already in `package.json`, a range key `js-yaml@^5`, a spec key `js-yaml@5.2.1`, and the parent-scoped `@nestjs/swagger: { js-yaml: 5.2.2 }`. All four still resolve `@nestjs/swagger/node_modules/js-yaml → 5.2.1`. **No upstream fix exists** — `11.4.6` is the last stable, the rest are `12.0.0-alpha.*` — and the only resolution npm DID apply pushes swagger onto `4.3.1`, a major downgrade of a dependency it pins exactly. **The vulnerable path is not reachable, measured rather than assumed:** the advisory requires `load()`/`loadAll()` on untrusted input, and a grep over `@nestjs/swagger/dist` returns **1 `yaml.dump` and zero `load`** — it emits the OpenAPI document and parses nothing. `main` and `develop` are IDENTICAL on this dependency, so no promotion ever introduced it. **Deliberately not fixed and not dismissed here:** the downgrade would smuggle a runtime risk into an unrelated promotion, and an agent does not dismiss a security alert. | A high-severity flaw sits in a library we cannot upgrade, and the only route we can reach it by does not use the broken part. | A written decision with a trigger, so main stops being blocked by an argument that otherwise lives in a chat log. | `Infra` | Cross | P2 | S | `PENDING` | | [`GT-692`](./gap-reference-catalog.md#gt-692) | **Every deployable image ships the full development dependency tree, and it is large enough to exhaust a CI runner's disk.** `src/apps/core-api/Dockerfile:20` runs `npm ci --legacy-peer-deps` — the whole tree, dev included — and the runner stage copies it verbatim (`COPY --from=builder /repo/node_modules ./node_modules`), with no `--omit=dev`, no `prune` and no second install. **Measured on this tree: `node_modules` is 650 MB**, carrying `typescript` 24 MB, `eslint` 5.1 MB, `@types/node` 2.5 MB, `@sinonjs/commons` and `jest` — **none of them a production dependency of `core-api`**. **Observed failing three times in ANOTHER repository's CI:** the Tracker's `Deploy (kind + Helm + smoke)` died on PR #149, on its clean rerun and twice on PR #150, always as `ctr: failed to extract layer … no space left on device` importing `evolith-core-api` into the kind node — and the paths it died on name the cause: `/repo/node_modules/@types/node/quic.d.ts` and `/repo/node_modules/@sinonjs/commons/…`, a declaration file and a TEST-DOUBLE library unpacking into a production image. The job fails BEFORE exercising anything, so it verifies nothing in either direction. Found by a consumer, which is the shape worth noting: nothing here measures the size of what it publishes. Not fixed in the promotion that found it — pruning changes what every image contains, and [`GT-647`](./gap-reference-catalog.md#gt-647) is the precedent that such a fix is verified by BOOTING, not by reading the diff. | Our published images carry the compiler, the linter and the test framework, and they are so big that another team CI runs out of disk trying to load one. | Images that carry what they run: they load on an ordinary runner, transfer faster, and stop offering an attacker tools the process never uses. | `Infra` | Cross | P2 | S | `PENDING` | -| [`GT-693`](./gap-reference-catalog.md#gt-693) | **A gate that references an OPA policy is reported `passed` while the policy's violations are thrown away, for 27 of the 33 shipped policies.** `OpaEvaluator` attributes violations per rule at `opa-evaluator.ts:174-185`: a rule in `CONTEXT_AWARE_VIOLATION_PREFIXES` claims everything with its prefix, and **every other rule claims only violations whose id EQUALS the rule id** — but a gate's `rules: ["rulesets/opa/.rego"]` becomes `opa-` via `deriveRuleId`, and no policy emits an id like that. **Counted, not estimated (2026-08-15): 31 of 33 `.rego` files emit namespaced ids and 4 are mapped**, the fourth being the one `GT-688` added. The filter matches nothing, the branch falls through to `return { rule, result: 'passed' }`, and a policy that fired is reported as conformance. **Proven, not argued:** removing the one-line `TPC-` entry turns the GT-688 attribution case from `failed` back to `passed` against an unchanged wasm result. Two constraints any fix must respect, both measured: ids are NOT globally unique (`CLI-RR-01..05` and `TAX-05..11`, 10 of 197 collide) and four policies emit more than one prefix — so neither a prefix map nor a naive id map suffices. | A gate can name an OPA policy, get a green tick, and never be shown what that policy found. | Certification stops depending on a hand-kept list: today 27 of 33 policies can fire and report nothing. | `Core Domain` | Cross | P1 | M | `PENDING` | +| [`GT-693`](./gap-reference-catalog.md#gt-693) | **A gate that references an OPA policy is reported `passed` while the policy's violations are thrown away, for 31 of the 39 shipped policies.** `OpaEvaluator` attributes violations per rule at `opa-evaluator.ts:174-185`: a rule in `CONTEXT_AWARE_VIOLATION_PREFIXES` claims everything with its prefix, and **every other rule claims only violations whose id EQUALS the rule id** — but a gate's `rules: ["rulesets/opa/.rego"]` becomes `opa-` via `deriveRuleId`, and no policy emits an id like that. **Counted (2026-08-15, after correcting a faulty first scan): 35 of 39 `.rego` files emit namespaced ids and 4 are mapped**, the fourth being the one `GT-688` added. The filter matches nothing, the branch falls through to `return { rule, result: 'passed' }`, and a policy that fired is reported as conformance. **Proven, not argued:** removing the one-line `TPC-` entry turns the GT-688 attribution case from `failed` back to `passed` against an unchanged wasm result. Two constraints any fix must respect, both measured: ids are NOT globally unique (`CLI-RR-01..05` and `TAX-05..11`, 10 of 203 collide) and four policies emit more than one prefix — so neither a prefix map nor a naive id map suffices. | — | — | `Core Domain` | Cross | P1 | M | `DONE` | +| [`GT-694`](./gap-reference-catalog.md#gt-694) | **Twelve OPA policy categories can never produce a verdict, because nothing populates the input facets their own schemas require.** `OpaEvaluator.validateInput` compiles `schemas/.input.schema.json` and rejects the run when the input does not satisfy it. Cross-checked 2026-08-15: **15 required facets are never emitted by `opa-input-builder.ts`, across 12 categories** — `multiTenancy`, `runtime`, `openCore`, `protocol`, `layers`, `git`, `scorecards`, `coreParity`, `releaseReadiness`, `contracts`, `testing`, `findings`, `ci`, `files`. Confirmed end to end on one rather than left static: a `multi-tenancy` gate rule against a real satellite through the real bundle returned `failed` with `OPA Input Schema Validation Failed: data/satellite must have required property 'multiTenancy'` — never reaching the policy. **Not a false pass, and P2 for that reason:** under `GT-595` a blocking rule that cannot run is reported, so this is a PERMANENT failure carrying no information about the repository, indistinguishable at a glance from a real violation. It also inflates what the product appears to enforce. **Not `GT-693`:** that was a verdict discarded on the way out; this is a policy never reached on the way in. | Twelve governance policies we ship can never actually run — they answer with a schema error instead of a verdict. | Either the facts they need get collected or the policy is withdrawn, so nothing ships that cannot answer. | `Core Domain` | Cross | P2 | L | `PENDING` | | [`GT-677`](./gap-reference-catalog.md#gt-677) | **An approved waiver suppresses nothing on any shipped path — the gate implements suppression and every production caller omits the store.** `evaluateDriftGate` carries `readonly waivers?: IWaiverStore` (`drift-gate.ts:90`) and a complete `applyWaivers` that sets `frozen: true`, records a `waived[]` entry and stamps `evidence.waiverRef` (`:171-186`). **Verified by enumerating the call sites 2026-08-14:** `evaluate.command.ts:174`, `evaluate.command.ts:204` and `mcp-server/src/tools/evaluate.tool.ts:138` — all three omit the argument; the only sites that pass `waivers:` are `drift-gate.spec.ts:105,117`. Symmetrically `FileWaiverStore` has exactly ONE non-spec consumer in the tree, `waiver.command.ts:110`, the command that WRITES waivers. **Measured end to end by execution rather than by reading:** baseline `evaluate --format drift` → exit 2, 94 violations, first `{"ruleId":"GOV-000","fingerprint":"a670fcba5dccb53f","frozen":false}`; `waiver request` + `waiver approve --by lead` → `"effectiveStatus":"approved"` persisted; the identical run again → **exit 2, `blockingFailures 94 → 94`, `frozen 0 → 0`, `waiverRef undefined`, no "Waived findings" section.** `GT-518` reads DONE for the store, the CLI and the gate — both halves built, never wired to each other — and `waiver.command.ts:58-60` tells the reader the gate "consumes the SAME store", which is false in the product's own source. The cheapest fix in this wave: one argument at three call sites. Blocks [`GT-682`](./gap-reference-catalog.md#gt-682) and [`GT-687`](./gap-reference-catalog.md#gt-687), and refutes the premise [`GT-670`](./gap-reference-catalog.md#gt-670) reasons from. | — | — | `Evolith CLI` | Cross | P1 | XS | `DONE` | | [`GT-676`](./gap-reference-catalog.md#gt-676) | **The coverage-floor gate cannot be switched on from any surface, so the one guard against mass-skipping is dead on arrival.** `maxSkippedFraction` (`GT-569`) is implemented and unit-tested, and every hit outside `node_modules`/`dist`/`coverage` lives under `src/packages/core-domain/src/application/validators/` plus two prose comments and the board — **zero** under `src/sdk/cli`, `src/apps/core-api` or `src/packages/mcp-server`. No CLI flag, no MCP argument, no REST field, no profile key, no environment variable, so `coverageThresholdIssue` short-circuits to `undefined` on every real run and `GOV-COVERAGE-THRESHOLD` can only be emitted from core-domain's own specs. **The rebuild seam is where a naive fix dies:** `validate-satellite.use-case.ts:76-104` enumerates option fields when it reconstructs the validator, so an option not added there is dropped even after the flag exists — the failure `GT-664` recorded for `processRunner`. Deliberately NOT merged with [`GT-675`](./gap-reference-catalog.md#gt-675): the coverage floor cannot catch the OPA case, because OPA reports `rulesSkipped: 0` (measured `354/0` against native's `113/241`). Coverage is the difference between "no blocking findings" and "the blocking rules never ran". | There is a control for “fail unless at least X% of the applicable rules ran”, and no way to switch it on. | Stops an all-green report when almost nothing was actually evaluated. | `Core Domain` | Cross | P1 | S | `PENDING` | | [`GT-684`](./gap-reference-catalog.md#gt-684) | **Content returned to a calling model carries no trust classification, so Evolith fails on its own surface the control it sells as a blocking rule.** `evolith-knowledge-search` with `includeText: true` emits raw third-party prose with attribution and no trust label and no structural fence, and the dispatch layer adds none for any of the 52 tools (`grep -rn "trust\|provenance\|untrusted"` over `mcp-tool-dispatch.ts` and `tool.interface.ts` → 0). **The ADR says so about itself:** `0082-agentic-ai-trust-boundary.md:9-15` carries `` and states that no trust label on retrieved context exists anywhere in `src/` and that Evolith neither labels nor schema-validates tool output before acting on it. Meanwhile `AAI-R06` "Untrusted Context Is Data" is a blocking MUST Evolith enforces on satellites (`agentic-ai.rego:30`, `opa-input-builder.ts:139`). No injection-shaped fixture exists anywhere, so no negative test asserts a hostile chunk fails to steer a later call. **Three candidate framings were REFUTED and are excluded from the row:** prompt injection DOES appear in the repository (ADR-0082 both languages, `mcp-security.md:539`, `agentic-ai/patterns.md:23` twice); it IS a rule and IS tested (`agentic-ai.test.rego:66-70`); and `evolith-read-file` does not exist. Failing your own blocking rule on your own surface is the most quotable defect a governance product can have. | Text we hand to a model is not marked as third-party data rather than an instruction. | Third-party content can no longer steer the model — and we stop failing the rule we sell. | `MCP Server` | Cross | P1 | S | `PENDING` | @@ -712,7 +713,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-668`](./gap-reference-catalog.md#gt-668) | **`GT-666`'s proof that its guard had ever been observed failing was anchored to a MOVING REF, so it stopped being evidence at the exact moment the fix landed.** The one case that reads the real pre-fix artifact obtained it with `git show ${PRE_FIX_REF}:iso-5055-mapping.json`, `PRE_FIX_REF` defaulting to `origin/develop`. That is true only while the fix lives on a branch: **`59d62bae` merged, `origin/develop` began serving the CORRECTED artifact, and the case asserting 64 findings found 0** — green on its own branch, red the moment it mattered, blocking the `develop` → `main` promotion PR **#483** in `Governance guards (GT-578)`. **Reproduced before acting**, not taken from the handover: `node --test` on `develop` at `59d62bae` fails that one case with `0 !== 64` while the other 18 pass. **The worse half is the one nobody would have seen:** the case carried `if (before.status !== 0) return void assert.ok(true, 'SKIPPED: …')` for shallow clones, so in a checkout with a truncated history the same rot would have **passed in silence** rather than failing — an escape hatch out of the only case that proves the guard was ever red. **DELIVERED 2026-08-09.** The comment's reasoning was right and is kept: *«read out of git rather than reconstructed … Reconstructed fixtures agree with whatever the author believed was wrong; this one cannot.»* That argues for a REAL pre-fix artifact, not for reading a branch at test time — so the artifact is **frozen in the repository**: `.harness/fixtures/standards-rule-class/iso-5055-mapping.pre-gt-666.json`, taken from `01308346` (`59d62bae^`, blob `6684e8a4`), **not trimmed**, and verified byte-identical to that blob once its one added `_fixture` provenance key is dropped. The `git show`, the `PRE_FIX_REF` env var and the shallow-clone skip are **deleted** — a skip can only ever hide a failure. **Assertions unchanged and unweakened: 64 findings, 16 of them `is classified `governance``**, measured against the frozen fixture and today's packs. One case is ADDED rather than any relaxed: a fixture «refreshed» from the live mapping is RED, and that was **observed** — the refresh was simulated and turns 2 of 20 cases red, so the negative fixture cannot silently become a copy of the artifact it exists to catch. Meta-guards re-run: `42` 78/78 classified, `43` 54/54 observed failing. | — | — | `Evolith Core` | Cross | P1 | XS | `DONE` | -**Progress:** 656 / 691 done · 3 in progress · 22 pending · 10 deferred +**Progress:** 657 / 692 done · 3 in progress · 22 pending · 10 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 fc340db7..06f2fdf0 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -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-435](../gaps/gap-reference-catalog.es.md#gt-435) | -| 2 | Área de mayor riesgo | `Core Domain` tiene la mayor carga ponderada abierta. | [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-693](../gaps/gap-reference-catalog.es.md#gt-693), [GT-683](../gaps/gap-reference-catalog.es.md#gt-683), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), +1 | +| 2 | Área de mayor riesgo | `Core Domain` tiene la mayor carga ponderada abierta. | [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-683](../gaps/gap-reference-catalog.es.md#gt-683), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), [GT-678](../gaps/gap-reference-catalog.es.md#gt-678), +1 | | 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | [GT-671](../gaps/gap-reference-catalog.es.md#gt-671), [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-684](../gaps/gap-reference-catalog.es.md#gt-684) | -| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-671](../gaps/gap-reference-catalog.es.md#gt-671), [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681), +6 | -| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-685](../gaps/gap-reference-catalog.es.md#gt-685), [GT-686](../gaps/gap-reference-catalog.es.md#gt-686), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), +10 | +| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-671](../gaps/gap-reference-catalog.es.md#gt-671), [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681), +5 | +| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-685](../gaps/gap-reference-catalog.es.md#gt-685), [GT-686](../gaps/gap-reference-catalog.es.md#gt-686), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), +11 | ## Bloqueadores Actuales @@ -42,19 +42,19 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| | Fecha canónica del tablero | 2026-08-08 | -| Gaps totales | 691 | -| Gaps cerrados | 656 | +| Gaps totales | 692 | +| Gaps cerrados | 657 | | Gaps pendientes | 35 | | P0 abiertos | 1 | -| P1 abiertos | 14 | -| P2 abiertos | 16 | +| P1 abiertos | 13 | +| P2 abiertos | 17 | | Cierre total | 94.9% | -| Registros de evidencia de cierre | 638 | +| Registros de evidencia de cierre | 639 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | |---|---:|---:|---:|---| -| `Core Domain` | 7 | 0 | 4 | [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-693](../gaps/gap-reference-catalog.es.md#gt-693), [GT-683](../gaps/gap-reference-catalog.es.md#gt-683), +3 | +| `Core Domain` | 7 | 0 | 3 | [GT-676](../gaps/gap-reference-catalog.es.md#gt-676), [GT-675](../gaps/gap-reference-catalog.es.md#gt-675), [GT-683](../gaps/gap-reference-catalog.es.md#gt-683), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), +3 | | `Governance` | 7 | 0 | 3 | [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-669](../gaps/gap-reference-catalog.es.md#gt-669), [GT-672](../gaps/gap-reference-catalog.es.md#gt-672), +3 | | `MCP Server` | 4 | 0 | 4 | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681), [GT-679](../gaps/gap-reference-catalog.es.md#gt-679) | | `Cross` | 3 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448), [GT-651](../gaps/gap-reference-catalog.es.md#gt-651) | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 2e270a18..7b7ff354 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -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-435](../gaps/gap-reference-catalog.md#gt-435) | -| 2 | Highest-risk area | `Core Domain` has the largest weighted open load. | [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-693](../gaps/gap-reference-catalog.md#gt-693), [GT-683](../gaps/gap-reference-catalog.md#gt-683), [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-687](../gaps/gap-reference-catalog.md#gt-687), +1 | +| 2 | Highest-risk area | `Core Domain` has the largest weighted open load. | [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-683](../gaps/gap-reference-catalog.md#gt-683), [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-687](../gaps/gap-reference-catalog.md#gt-687), [GT-678](../gaps/gap-reference-catalog.md#gt-678), +1 | | 3 | Quick wins | High criticality with XS/S complexity. | [GT-671](../gaps/gap-reference-catalog.md#gt-671), [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-684](../gaps/gap-reference-catalog.md#gt-684) | -| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-671](../gaps/gap-reference-catalog.md#gt-671), [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681), +6 | -| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-685](../gaps/gap-reference-catalog.md#gt-685), [GT-686](../gaps/gap-reference-catalog.md#gt-686), [GT-687](../gaps/gap-reference-catalog.md#gt-687), +10 | +| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-671](../gaps/gap-reference-catalog.md#gt-671), [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681), +5 | +| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-685](../gaps/gap-reference-catalog.md#gt-685), [GT-686](../gaps/gap-reference-catalog.md#gt-686), [GT-687](../gaps/gap-reference-catalog.md#gt-687), +11 | ## Current Blockers @@ -42,19 +42,19 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| | Canonical board date | 2026-08-08 | -| Total gaps | 691 | -| Closed gaps | 656 | +| Total gaps | 692 | +| Closed gaps | 657 | | Open gaps | 35 | | Open P0 | 1 | -| Open P1 | 14 | -| Open P2 | 16 | +| Open P1 | 13 | +| Open P2 | 17 | | Total closure | 94.9% | -| Closure evidence records | 638 | +| Closure evidence records | 639 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | |---|---:|---:|---:|---| -| `Core Domain` | 7 | 0 | 4 | [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-693](../gaps/gap-reference-catalog.md#gt-693), [GT-683](../gaps/gap-reference-catalog.md#gt-683), +3 | +| `Core Domain` | 7 | 0 | 3 | [GT-676](../gaps/gap-reference-catalog.md#gt-676), [GT-675](../gaps/gap-reference-catalog.md#gt-675), [GT-683](../gaps/gap-reference-catalog.md#gt-683), [GT-674](../gaps/gap-reference-catalog.md#gt-674), +3 | | `Governance` | 7 | 0 | 3 | [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-669](../gaps/gap-reference-catalog.md#gt-669), [GT-672](../gaps/gap-reference-catalog.md#gt-672), +3 | | `MCP Server` | 4 | 0 | 4 | [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681), [GT-679](../gaps/gap-reference-catalog.md#gt-679) | | `Cross` | 3 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.md#gt-435), [GT-448](../gaps/gap-reference-catalog.md#gt-448), [GT-651](../gaps/gap-reference-catalog.md#gt-651) | diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index 4e18605a..beb0456f 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-08-08", "gaps": { - "total": 691, - "done": 656, + "total": 692, + "done": 657, "pending": 22, "inProgress": 3, "deferred": 10 }, "evidence": { - "closureRecords": 638, + "closureRecords": 639, "cliPackage": "@beyondnet/evolith-cli@1.2.2", "adrCount": 140, "rulesetCount": 180, From 949104c560ad64d6a1f11da38e5bf9da95bf9121 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sat, 15 Aug 2026 17:58:19 -0500 Subject: [PATCH 3/3] fix(core-domain): drop the import GT-693 left dead in the OPA evaluator spec CodeQL caught this on PR #509 and it is a real finding in my own code, not noise: `GT-693` deleted the test that pinned the 27 unmapped policies, which was the only consumer of `CONTEXT_AWARE_VIOLATION_PREFIXES` in this spec. The import stayed behind. Removed rather than resolved-as-acceptable. The constant is still exported and still used in production as the fallback for a pre-GT-693 `policy.wasm`; what was dead is this file's reference to it. opa-evaluator spec 19/19. Co-Authored-By: Claude Opus 5 --- .../src/application/validators/evaluators/opa-evaluator.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts index 307d6854..1e8d14ed 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/opa-evaluator.spec.ts @@ -1,4 +1,4 @@ -import { OpaEvaluator, CONTEXT_AWARE_VIOLATION_PREFIXES, violationBelongsToRule } from './opa-evaluator'; +import { OpaEvaluator, violationBelongsToRule } from './opa-evaluator'; import { createMockFileSystem, createMockLogger } from '../../../test/mocks'; import { NormalizedRule } from '../../../domain/models/normalized-rule'; import { WorkspaceEvaluationContext } from './evaluator.interface';