diff --git a/.harness/scripts/ci/73-validate-engine-coverage-parity.mjs b/.harness/scripts/ci/73-validate-engine-coverage-parity.mjs index b66726629..787855cda 100644 --- a/.harness/scripts/ci/73-validate-engine-coverage-parity.mjs +++ b/.harness/scripts/ci/73-validate-engine-coverage-parity.mjs @@ -50,6 +50,21 @@ * machine alike. A rule whose native verdict needs one of those is skipped identically * everywhere, which is the fact the baseline should carry. * + * ## Nothing is coverage-only by omission (GT-716 AC4) + * + * A registered entry whose class is DEBT — no policy in the bundle, no native handler, + * a handler that declined, an OPA path that gave no reason — must carry a recorded + * decision in `engine-coverage-decisions.json`: `native-only` / `opa-only` (the + * difference is accepted, and the entry says why and what would reopen it) or + * `neither` (no engine decides the rule as written, e.g. the generated ADR-conformance + * rules, documentation on both sides). An entry with no decision fails: the debt is + * real, but it has to be somebody's. A decision that no longer describes the runs fails + * too — its rule is decided by both engines now (stale), or by the engine the decision + * said would not (contradicted) — so the register cannot outlive what it decided. + * Entries whose class is already a declaration of the rule itself (`supplied-facet-absent`, + * `needs-supplied-facts`, `needs-external-system`, `needs-runtime`, `documentation-only`, + * `underspecified` — GT-716 AC2) need no second one. + * * ## Anti-vacuous pass * * Both engine runs of both scenarios go through `assertScannedPerSource`; a missing @@ -63,8 +78,8 @@ * node .harness/scripts/ci/73-validate-engine-coverage-parity.mjs --write # regenerate the baseline (review the diff) * * Exit codes: - * 0 - every coverage-only rule is registered with its reason, and every entry still holds - * 1 - an unregistered rule, a stale entry, a changed reason, or an engine that produced nothing + * 0 - every coverage-only rule is registered with its reason, every debt entry carries a decision, and every entry still holds + * 1 - an unregistered rule, a stale entry, a changed reason, a debt entry nobody decided, a decision the runs contradict, or an engine that produced nothing */ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; @@ -108,6 +123,111 @@ export const FOLLOW_UP = Object.freeze({ undecided: 'The report states no class for the skip — make the engine say why.', }); +export const DECISIONS_PATH = resolve(HERE, 'engine-coverage-decisions.json'); + +/** + * Classes that are debt until somebody decides (GT-716 AC4). A baseline entry in one + * of these without a recorded decision is coverage-only BY OMISSION and fails. The + * other classes are the rule's own declaration (AC2) and need no second one. + */ +export const DECISION_REQUIRED = new Set(['no-policy-in-bundle', 'unimplemented-native', 'handler-declined', 'opa-gave-no-reason', 'undecided']); + +/** The kinds a decision can take, and the baseline direction each one accepts. */ +export const DECISION_KINDS = Object.freeze({ 'native-only': 'nativeOnly', 'opa-only': 'opaOnly', neither: null }); + +const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; + +/** + * Validate the register's shape: every decision has an id, a kind, a dated + * rationale and the rules it covers (ids, a pattern, or both). Malformed is a throw, + * not a warning — a register that cannot be read must not pass as "nothing decided". + */ +export function validateDecisions(decisions) { + if (!Array.isArray(decisions)) throw new Error('engine-coverage-decisions.json: `decisions` must be an array'); + const ids = new Set(); + for (const d of decisions) { + const at = `decision ${JSON.stringify(d?.id ?? '(no id)')}`; + if (typeof d?.id !== 'string' || d.id.length === 0) throw new Error(`${at}: \`id\` is required`); + if (ids.has(d.id)) throw new Error(`${at}: duplicate id`); + ids.add(d.id); + if (!(d.kind in DECISION_KINDS)) throw new Error(`${at}: \`kind\` must be one of ${Object.keys(DECISION_KINDS).join(', ')}`); + if (typeof d.why !== 'string' || d.why.trim().length < 20) throw new Error(`${at}: \`why\` must say why (20+ characters)`); + if (typeof d.recordedOn !== 'string' || !ISO_DATE.test(d.recordedOn)) throw new Error(`${at}: \`recordedOn\` must be YYYY-MM-DD`); + const hasRules = Array.isArray(d.rules) && d.rules.length > 0 && d.rules.every((r) => typeof r === 'string' && r.length > 0); + const hasPattern = typeof d.pattern === 'string' && d.pattern.length > 0; + if (!hasRules && !hasPattern) throw new Error(`${at}: \`rules\` (ids) or \`pattern\` (a regex over rule ids) is required`); + if (hasPattern) new RegExp(d.pattern); // throws on a bad pattern + } + return decisions; +} + +export function readDecisions(path = DECISIONS_PATH) { + if (!existsSync(path)) return []; + return validateDecisions(JSON.parse(readFileSync(path, 'utf8')).decisions ?? []); +} + +/** The rule ids a decision covers within a universe: its explicit ids plus every id its pattern matches. */ +export function rulesOf(decision, universe) { + const out = new Set(decision.rules ?? []); + if (decision.pattern) { + const re = new RegExp(decision.pattern); + for (const id of universe) if (re.test(id)) out.add(id); + } + return out; +} + +/** ruleId → decision, over a universe. Two decisions on one id is a contradiction in the register itself. */ +export function decisionIndex(decisions, universe) { + const index = new Map(); + for (const d of decisions) { + for (const id of rulesOf(d, universe)) { + const prior = index.get(id); + if (prior && prior.id !== d.id) throw new Error(`rule ${id} is covered by two decisions: ${prior.id} and ${d.id}`); + index.set(id, d); + } + } + return index; +} + +/** + * Hold the measured entries and the register to each other. + * - undecided: a coverage-only entry of a DEBT class with no decision — by omission. + * - mismatched: the decision accepts the other direction (a \`native-only\` rule that OPA alone decides). + * - stale: a decided rule both engines now decide — the decision outlived its difference. + * - contradicted: a \`neither\` rule some engine decided, or a one-engine decision whose engine is the other one. + * `measured.decided` is ruleId → Set of the engines that decided it in this scenario. + */ +export function reconcileDecisions(measured, decisions) { + const universe = new Set(measured.decided.keys()); + const index = decisionIndex(decisions, universe); + const out = { undecided: [], mismatched: [], stale: [], contradicted: [] }; + for (const direction of ['nativeOnly', 'opaOnly']) { + for (const e of measured[direction] ?? []) { + if (!DECISION_REQUIRED.has(e.reason.class)) continue; + const d = index.get(e.ruleId); + if (!d) out.undecided.push({ direction, ruleId: e.ruleId, class: e.reason.class }); + else if (DECISION_KINDS[d.kind] !== direction) out.mismatched.push({ direction, ruleId: e.ruleId, decision: d.id, kind: d.kind }); + } + } + const decidedOnly = { 'native-only': 'native', 'opa-only': 'opa' }; + for (const d of decisions) { + for (const id of rulesOf(d, universe)) { + const engines = measured.decided.get(id) ?? new Set(); + if (engines.size === 0) continue; + if (d.kind === 'neither') out.contradicted.push({ ruleId: id, decision: d.id, decidedBy: [...engines].sort() }); + else if (engines.size === 2) out.stale.push({ ruleId: id, decision: d.id }); + else if (!engines.has(decidedOnly[d.kind])) out.contradicted.push({ ruleId: id, decision: d.id, decidedBy: [...engines] }); + } + } + return out; +} + +/** Decision ids that name a rule no scenario saw at all: a typo, or a rule that left the corpus. */ +export function unknownDecisionRules(decisions, universes) { + const seen = new Set(universes.flatMap((u) => [...u])); + return decisions.flatMap((d) => (d.rules ?? []).filter((id) => !seen.has(id)).map((ruleId) => ({ decision: d.id, ruleId }))); +} + /** * Rule ids decided by exactly one engine, with the OTHER engine's outcome. * Exported for the unit tests; the precedence is 68's. @@ -308,9 +428,11 @@ function measureScenario(name, runs, manifest, snapshot, corpus, vocabulary, emi ); const universe = new Set([...outcomes.native.keys(), ...outcomes.opa.keys()]); const { nativeOnly, opaOnly } = coverageOnly(outcomes.native, outcomes.opa, universe); + const decided = new Map([...universe].map((id) => [id, new Set(ENGINES.filter((e) => DECIDED.has(outcomeOf(outcomes[e], id))))])); return { nativeOnly: nativeOnly.map((e) => ({ ...e, reason: opaReason(e.ruleId, manifest, corpus, vocabulary, runs.opa, emitted) })), opaOnly: opaOnly.map((e) => ({ ...e, reason: nativeReason(e.ruleId, runs.native, snapshot, corpus) })), + decided, }; } @@ -335,6 +457,7 @@ async function main() { const corpus = readCorpusFacts(resolve(root, 'src/rulesets'), root); const vocabulary = readVocabulary(root); const emitted = builderEmits(root); + const decisions = readDecisions(); const measured = {}; let satellite = null; @@ -381,6 +504,10 @@ async function main() { for (const s of SCENARIOS) { console.log(` ${s}: native-only ${measured[s].nativeOnly.length}, opa-only ${measured[s].opaOnly.length} (${measured[s].durationMs} ms)`); } + for (const s of SCENARIOS) { + const { undecided } = reconcileDecisions(measured[s], decisions); + if (undecided.length > 0) console.log(` ${s}: ${undecided.length} debt entry(ies) carry no decision yet — record them in ${DECISIONS_PATH.replace(`${root}/`, '')}: ${undecided.map((e) => e.ruleId).join(', ')}`); + } console.log(`✓ baseline written to ${BASELINE_PATH.replace(`${root}/`, '')} — review the diff before committing it.`); return; } @@ -422,6 +549,37 @@ async function main() { console.error(`❌ ${s}: ${changed.length} entry(ies) changed class — the same id, a different debt:`); for (const e of changed) console.error(` - ${e.direction} ${e.ruleId}: ${e.from} → ${e.to}`); } + + const decided = reconcileDecisions(measured[s], decisions); + report.scenarios[s].decisions = Object.fromEntries(Object.entries(decided).map(([k, v]) => [k, v.map((e) => e.ruleId)])); + if (decided.undecided.length > 0) { + failed = true; + console.error(`❌ ${s}: ${decided.undecided.length} debt entry(ies) carry no recorded decision — coverage-only by omission (GT-716 AC4):`); + for (const e of decided.undecided) console.error(` - ${e.direction} ${e.ruleId} (${e.class}): implement it, or record a decision in ${DECISIONS_PATH.replace(`${root}/`, '')}`); + } + if (decided.mismatched.length > 0) { + failed = true; + console.error(`❌ ${s}: ${decided.mismatched.length} entry(ies) sit in the direction their decision does not accept:`); + for (const e of decided.mismatched) console.error(` - ${e.direction} ${e.ruleId}: decision ${e.decision} says ${e.kind}`); + } + if (decided.stale.length > 0) { + failed = true; + console.error(`❌ ${s}: ${decided.stale.length} decided rule(s) are decided by BOTH engines now — retire the decision:`); + for (const e of decided.stale) console.error(` - ${e.ruleId} (decision ${e.decision})`); + } + if (decided.contradicted.length > 0) { + failed = true; + console.error(`❌ ${s}: ${decided.contradicted.length} decision(s) are contradicted by the runs:`); + for (const e of decided.contradicted) console.error(` - ${e.ruleId}: decision ${e.decision} — decided by ${e.decidedBy.join(' and ')}`); + } + } + + const unknown = unknownDecisionRules(decisions, SCENARIOS.map((s) => new Set(measured[s].decided.keys()))); + report.unknownDecisionRules = unknown.map((e) => e.ruleId); + if (unknown.length > 0) { + failed = true; + console.error(`❌ ${unknown.length} decision rule id(s) were seen by no scenario — a typo, or a rule that left the corpus:`); + for (const e of unknown) console.error(` - ${e.ruleId} (decision ${e.decision})`); } if (asJson) console.log(`ENGINE_COVERAGE_PARITY ${JSON.stringify(report)}`); @@ -430,7 +588,7 @@ async function main() { console.error(' A coverage difference is legitimate (ADR-0041); an unregistered one is not. Register it with its reason, or fix it.'); process.exit(1); } - console.log('✓ 73-validate-engine-coverage-parity: every coverage-only rule is registered with its reason, in both directions, on both scenarios.'); + console.log('✓ 73-validate-engine-coverage-parity: every coverage-only rule is registered with its reason and every debt entry with a decision, in both directions, on both scenarios.'); } if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { diff --git a/.harness/scripts/ci/73-validate-engine-coverage-parity.test.mjs b/.harness/scripts/ci/73-validate-engine-coverage-parity.test.mjs index b098ea600..d3b754a80 100644 --- a/.harness/scripts/ci/73-validate-engine-coverage-parity.test.mjs +++ b/.harness/scripts/ci/73-validate-engine-coverage-parity.test.mjs @@ -6,15 +6,24 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { deriveOutcomes } from './68-validate-engine-verdict-parity.mjs'; +import { resolve } from 'node:path'; import { FOLLOW_UP, classFromReport, coverageOnly, + decisionIndex, nativeReason, opaReason, + readDecisions, reconcileCoverage, + reconcileDecisions, + rulesOf, toBaselineScenario, + unknownDecisionRules, + validateDecisions, } from './73-validate-engine-coverage-parity.mjs'; +import { REPO_ROOT } from '../lib/paths.mjs'; +import { readCorpusFacts } from '../lib/rule-facts.mjs'; const outcomes = (data) => deriveOutcomes(data); @@ -120,3 +129,88 @@ test('a native-side class stated on the OPA side is OPA giving no reason of its assert.match(r.why, /dependency-cruiser/); assert.match(FOLLOW_UP['opa-gave-no-reason'], /state why it declined/); }); + +// --------------------------------------------------------------------------- +// GT-716 AC4 — nothing is coverage-only by omission: the decisions register +// --------------------------------------------------------------------------- + +const decision = (over) => ({ id: 'd', kind: 'native-only', rules: ['A-01'], why: 'twenty characters or more of reason', recordedOn: '2026-09-21', ...over }); +const measuredWith = ({ nativeOnly = [], opaOnly = [], decided = {} }) => ({ + nativeOnly: nativeOnly.map(([ruleId, cls]) => ({ ruleId, reason: { class: cls, why: '' } })), + opaOnly: opaOnly.map(([ruleId, cls]) => ({ ruleId, reason: { class: cls, why: '' } })), + decided: new Map(Object.entries(decided).map(([id, engines]) => [id, new Set(engines)])), +}); + +test('the register refuses a decision without an id, a kind, a dated reason, or the rules it covers', () => { + assert.doesNotThrow(() => validateDecisions([decision()])); + assert.doesNotThrow(() => validateDecisions([decision({ rules: undefined, pattern: '^CORE-\\d{4}-\\d{2}$' })])); + assert.throws(() => validateDecisions([decision({ id: '' })]), /`id` is required/); + assert.throws(() => validateDecisions([decision(), decision()]), /duplicate id/); + assert.throws(() => validateDecisions([decision({ kind: 'maybe' })]), /`kind` must be one of/); + assert.throws(() => validateDecisions([decision({ why: 'short' })]), /`why` must say why/); + assert.throws(() => validateDecisions([decision({ recordedOn: 'yesterday' })]), /`recordedOn` must be YYYY-MM-DD/); + assert.throws(() => validateDecisions([decision({ rules: [] })]), /`rules` \(ids\) or `pattern`/); + assert.throws(() => validateDecisions([decision({ rules: undefined, pattern: '(' })]), /Invalid regular expression/); + assert.throws(() => validateDecisions({ not: 'an array' }), /must be an array/); +}); + +test('a decision covers its ids and whatever its pattern matches in the universe; two decisions on one id is a throw', () => { + const universe = new Set(['CORE-0001-01', 'CORE-0002-01', 'MTN-01', 'A-01']); + const byPattern = decision({ id: 'p', kind: 'neither', rules: undefined, pattern: '^CORE-\\d{4}-\\d{2}$' }); + assert.deepEqual([...rulesOf(byPattern, universe)].sort(), ['CORE-0001-01', 'CORE-0002-01']); + const index = decisionIndex([decision(), byPattern], universe); + assert.equal(index.get('A-01').id, 'd'); + assert.equal(index.get('CORE-0002-01').id, 'p'); + assert.equal(index.has('MTN-01'), false); + assert.throws(() => decisionIndex([decision(), decision({ id: 'again' })], universe), /covered by two decisions: d and again/); +}); + +test('a debt entry with no decision is coverage-only by omission; a declaration of the rule itself needs none', () => { + const measured = measuredWith({ + nativeOnly: [['A-01', 'no-policy-in-bundle'], ['F-01', 'supplied-facet-absent'], ['H-01', 'opa-gave-no-reason']], + opaOnly: [['B-01', 'unimplemented-native'], ['R-01', 'needs-runtime']], + decided: { 'A-01': ['native'], 'F-01': ['native'], 'H-01': ['native'], 'B-01': ['opa'], 'R-01': ['opa'] }, + }); + const r = reconcileDecisions(measured, [decision()]); + assert.deepEqual(r.undecided.map((e) => `${e.direction} ${e.ruleId} ${e.class}`), ['nativeOnly H-01 opa-gave-no-reason', 'opaOnly B-01 unimplemented-native']); + assert.deepEqual(r.mismatched, []); + assert.deepEqual(r.stale, []); + assert.deepEqual(r.contradicted, []); +}); + +test('a decision that accepts the other direction is mismatched, not satisfied', () => { + const measured = measuredWith({ opaOnly: [['A-01', 'unimplemented-native']], decided: { 'A-01': ['opa'] } }); + const r = reconcileDecisions(measured, [decision()]); // native-only, but OPA alone decides it + assert.deepEqual(r.undecided, []); + assert.deepEqual(r.mismatched.map((e) => `${e.ruleId}:${e.kind}`), ['A-01:native-only']); + // …and the same fact seen from the decision's side: its engine is not the one that decided. + assert.deepEqual(r.contradicted.map((e) => `${e.ruleId}:${e.decidedBy.join('+')}`), ['A-01:opa']); +}); + +test('the register cannot outlive its difference: both engines deciding a decided rule is stale', () => { + const measured = measuredWith({ decided: { 'A-01': ['native', 'opa'] } }); + const r = reconcileDecisions(measured, [decision()]); + assert.deepEqual(r.stale.map((e) => e.ruleId), ['A-01']); +}); + +test('a `neither` decision is contradicted by any engine deciding one of its rules — pattern included — and silent otherwise', () => { + const neither = decision({ id: 'docs', kind: 'neither', rules: undefined, pattern: '^CORE-\\d{4}-\\d{2}$' }); + const quiet = measuredWith({ decided: { 'CORE-0001-01': [], 'CORE-0002-01': [], 'MTN-01': ['native', 'opa'] } }); + assert.deepEqual(reconcileDecisions(quiet, [neither]), { undecided: [], mismatched: [], stale: [], contradicted: [] }); + const loud = measuredWith({ decided: { 'CORE-0001-01': ['native'], 'CORE-0002-01': [] } }); + assert.deepEqual(reconcileDecisions(loud, [neither]).contradicted.map((e) => `${e.ruleId}:${e.decidedBy.join('+')}`), ['CORE-0001-01:native']); +}); + +test('a decision naming a rule no scenario saw is a typo or a departed rule, and is reported once across scenarios', () => { + const unknown = unknownDecisionRules([decision({ rules: ['A-01', 'GONE-99'] })], [new Set(['A-01']), new Set(['B-01'])]); + assert.deepEqual(unknown, [{ decision: 'd', ruleId: 'GONE-99' }]); +}); + +test('the committed register is well-formed and every rule it names exists in the corpus', () => { + const decisions = readDecisions(); + assert.ok(decisions.length >= 5, 'the register carries the AC4 decisions'); + const ids = new Set(readCorpusFacts(resolve(REPO_ROOT, 'src/rulesets'), REPO_ROOT).keys()); + const missing = decisions.flatMap((d) => (d.rules ?? []).filter((id) => !ids.has(id))); + assert.deepEqual(missing, [], 'decision rule ids must be corpus rule ids'); + assert.ok(decisions.some((d) => d.kind === 'neither' && d.pattern), 'the ADR-conformance decision is a pattern over the generated id shape'); +}); diff --git a/.harness/scripts/ci/engine-coverage-decisions.json b/.harness/scripts/ci/engine-coverage-decisions.json new file mode 100644 index 000000000..585ef277e --- /dev/null +++ b/.harness/scripts/ci/engine-coverage-decisions.json @@ -0,0 +1,120 @@ +{ + "$comment": [ + "GT-716 AC4 — the recorded decision behind every coverage difference that is DEBT rather than a declaration of the rule itself.", + "Read by `73-validate-engine-coverage-parity.mjs`: a baseline entry whose class is no-policy-in-bundle, unimplemented-native,", + "handler-declined, opa-gave-no-reason or undecided must be covered here, or the guard fails it as coverage-only BY OMISSION.", + "`kind` is what was decided — native-only / opa-only (the difference is accepted, with the reason and what would reopen it) or", + "neither (no engine decides the rule as written, and the runs must keep agreeing with that). A decision the runs contradict fails:", + "its rule is decided by both engines now, or by the engine the decision said would not. Hand-written; `--write` never touches it." + ], + "decisions": [ + { + "id": "ssdf-native-only", + "kind": "native-only", + "rules": [ + "SSDF-PO.3.1", + "SSDF-PS.3.2", + "SSDF-PW.4.1", + "SSDF-PW.4.4", + "SSDF-PW.6.1", + "SSDF-PW.7.2", + "SSDF-RV.1.2", + "SSDF-RV.1.3" + ], + "why": "`SsdfRuleHandler` decides the eight SSDF v1.1 practices from the tree — workflow files, the security policy, lockfiles, signing and review configuration. The OPA input builder projects none of those facts, so a Rego twin would begin by emitting them a second time to reach the same verdict: the same check, twice, with a projection in between. Complementary reach under ADR-0041.", + "reopenWhen": "The builder emits the workflow and security-policy facets for another policy; the twin then costs a policy, not a projection.", + "recordedOn": "2026-09-21" + }, + { + "id": "slsa-native-only", + "kind": "native-only", + "rules": [ + "SLSA-BUILD-L1", + "SLSA-PROV-L1", + "SLSA-HOSTED-L2", + "SLSA-AUTH-L2" + ], + "why": "`SlsaRuleHandler` decides the four SLSA levels from the build configuration in the tree (hosted build, provenance generation, authenticated provenance). As with SSDF, nothing the OPA input builder emits carries those facts; a Rego twin is a second projection of the same files. Complementary reach under ADR-0041.", + "reopenWhen": "The builder projects the build/provenance configuration for another policy.", + "recordedOn": "2026-09-21" + }, + { + "id": "sec-rl-native-only", + "kind": "native-only", + "rules": [ + "SEC-RL-01", + "SEC-RL-02" + ], + "why": "`GovernanceRuleHandler` (GT-595) scans the HTTP surface for a rate limit and a body cap read from the environment — a source scan over the tree that the OPA input builder does not project. Native-only; the scan is the check, and the policy would need the scan's result as input.", + "reopenWhen": "A scanner presents the finding through the enforcer seam (GT-514) as `satellite.findings`; both engines then read the same fact.", + "recordedOn": "2026-09-21" + }, + { + "id": "modular-monolith-native-only", + "kind": "native-only", + "rules": [ + "MM-R01", + "MM-R02", + "MM-R04", + "MM-R05", + "MM-R06", + "MM-R07", + "MM-R08", + "MM-R09", + "MM-R10", + "MM-R11", + "MM-R12" + ], + "why": "The modular-monolith topology rules are import-graph and AST checks over the satellite's layers, decided natively by `ArchitectureRuleHandler` — its structural, AST and module-boundary rule sets (GT-632). The OPA path has no projection of the module graph — `satellite.layers` belongs to the enforcer (GT-514) and the builder does not emit it — so a generated Rego twin would have nothing to read. Native-only, by decision, rather than a twin that decides from an absent facet. MM-R03 (ports-and-adapters boundary) is not here: `modular-monolith.rego` already decides it, and both engines agree — guard 73 flagged the decision as stale the first time it ran, which is what the register is for.", + "reopenWhen": "The enforcer seam feeds `satellite.layers` to the OPA path; the twin is then a translation of the same clauses.", + "recordedOn": "2026-09-21" + }, + { + "id": "hxa-enforcer-route-native-only", + "kind": "native-only", + "rules": [ + "HXA-01", + "HXA-02", + "HXA-04", + "HXA-05" + ], + "why": "`ArchitectureRuleHandler` decides these hexagonal-architecture clauses natively from the import graph. The OPA path routes them to the dependency-cruiser enforcer, which needs a compiled configuration the tracked tree does not carry — guard 73 measures an export of it — and so reports its failure without a class of its own (`opa-gave-no-reason`). Native-only on the committed tree.", + "reopenWhen": "The enforcer route materialises its configuration from the tree, or states a class when it cannot run (FOLLOW_UP for `opa-gave-no-reason`).", + "recordedOn": "2026-09-21" + }, + { + "id": "adr-conformance-documentation-only-both", + "kind": "neither", + "pattern": "^[A-Z]{2,4}-\\d{4}-\\d{2}$", + "why": "The generated ADR-conformance rules (`generate-adr-rulesets.mjs`; 138 on 2026-09-21) declare `facts: []` — the generator wired no check — so the native engine classes them `documentation-only`, and since GT-716 AC4 the OPA engine states the same class for a policy-less rule whose declaration says there is nothing to check. Neither engine decides them, and a generated Rego twin would decide nothing either: documentation-only on both engines, by decision. The pattern is the generator's id shape; a generated rule some engine starts deciding contradicts this entry and fails the guard.", + "reopenWhen": "An ADR's conformance rule gains an authored check (facts and a handler or a policy); it then leaves this decision by leaving the pattern's silence.", + "recordedOn": "2026-09-21" + }, + { + "id": "source-scan-rules-declared-scanner", + "kind": "neither", + "rules": [ + "SEC-INJ-01", + "SEC-INJ-02", + "SEC-PATH-01", + "SEC-PATH-02", + "SEC-TIMING-01", + "SEC-TIMING-02", + "SEC-RL-03" + ], + "why": "Whether `child_process.exec` receives interpolated input, whether every path argument is sanitised and contained, whether a credential comparison is constant-time, whether the HTTP server sets its timeouts — each is a scanner's finding over the AST, not a regex over the tree; MM-R10 in this same corpus forbids exactly that kind of handler. The seven declare `satellite.findings` (external) since GT-716 AC4 and no engine decides them until an adapter presents the findings through the enforcer seam (GT-514).", + "reopenWhen": "A SAST adapter supplies `satellite.findings`; the native handler and the policy then read the same finding.", + "recordedOn": "2026-09-21" + }, + { + "id": "qt-05-declared-runtime", + "kind": "neither", + "rules": [ + "QT-05" + ], + "why": "`SdlcRuleHandler` used to return `passed` for QT-05 with the message 'requires runtime analysis' — a fixed answer, not a verdict, and the only reason the rule was native-only. The testing-pyramid distribution is a test run's fact: QT-05 declares `satellite.testing` (runtime) since GT-716 AC4, no handler claims it, and neither engine decides it until a run supplies the mix.", + "reopenWhen": "A test-run adapter supplies `satellite.testing`; `testing-pyramid.rego` and a native handler then decide it from the same numbers.", + "recordedOn": "2026-09-21" + } + ] +} diff --git a/.harness/scripts/ci/engine-coverage-parity.baseline.json b/.harness/scripts/ci/engine-coverage-parity.baseline.json index a337a265c..f84fd7896 100644 --- a/.harness/scripts/ci/engine-coverage-parity.baseline.json +++ b/.harness/scripts/ci/engine-coverage-parity.baseline.json @@ -5,7 +5,7 @@ "changed class fails. ADR-0041 never promised equal coverage; this file makes every coverage difference a diff somebody reads.", "`why` is measured — the class the report states, the facets the policy reads — and `followUp` says what would REMOVE the entry." ], - "measuredOn": "2026-09-20", + "measuredOn": "2026-09-21", "method": "evolith validate --engine {native,opa} --format json, on an export of the tracked tree (git ls-files + policy.wasm) and on a satellite fresh from `evolith init` with --core pointed at that export; outcomes per 68-validate-engine-verdict-parity.mjs.", "scenarios": { "repository": { @@ -265,11 +265,6 @@ "why": "The report states the policy reads satellite.openCore this run did not supply.", "followUp": "Supply the facet through `facts.satellite` (GT-694), or stop reading it in the policy." }, - "QT-05": { - "class": "no-policy-in-bundle", - "why": "The report states `no-policy-in-bundle`.", - "followUp": "Author the Rego twin, or record that the rule is native-only." - }, "QT-06": { "class": "supplied-facet-absent", "why": "The report states the policy reads behaviorChangedWithoutDocUpdate this run did not supply.", @@ -421,43 +416,7 @@ "followUp": "Supply the facet through `facts.satellite` (GT-694), or stop reading it in the policy." } }, - "opaOnly": { - "MCP-01": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-02": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-03": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-05": { - "class": "handler-declined", - "why": "The declaration gives `handler-declined`; declared facts: core.cli, repository.", - "followUp": "The native handler found nothing to judge here; a fixture with the subject would decide it." - }, - "OBS-EVD-01": { - "class": "needs-runtime", - "why": "The report states `needs-runtime`; declared facts: satellite.packageJson, traces.", - "followUp": "An adapter that observes the running system, through the enforcer seam." - }, - "OBS-EVD-02": { - "class": "needs-runtime", - "why": "The report states `needs-runtime`; declared facts: satellite.packageJson, traces.", - "followUp": "An adapter that observes the running system, through the enforcer seam." - }, - "OBS-EVD-03": { - "class": "needs-external-system", - "why": "The report states `needs-external-system`; declared facts: satellite.packageJson, telemetryBackend.", - "followUp": "An adapter over the external system, through the enforcer seam." - } - } + "opaOnly": {} }, "init-satellite": { "nativeOnly": { @@ -591,11 +550,6 @@ "why": "The policy reads satellite.contracts, which the input builder does not emit on a bare run.", "followUp": "Supply the facet through `facts.satellite` (GT-694), or stop reading it in the policy." }, - "QT-05": { - "class": "no-policy-in-bundle", - "why": "The report states `no-policy-in-bundle`.", - "followUp": "Author the Rego twin, or record that the rule is native-only." - }, "QT-06": { "class": "supplied-facet-absent", "why": "The report states the policy reads behaviorChangedWithoutDocUpdate this run did not supply.", @@ -707,43 +661,7 @@ "followUp": "Supply the facet through `facts.satellite` (GT-694), or stop reading it in the policy." } }, - "opaOnly": { - "MCP-01": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-02": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-03": { - "class": "unimplemented-native", - "why": "The report states `unimplemented-native`; declared facts: core.evidence, repository.", - "followUp": "Write the native handler — the rule declares an observed fact." - }, - "MCP-05": { - "class": "handler-declined", - "why": "The declaration gives `handler-declined`; declared facts: core.cli, repository.", - "followUp": "The native handler found nothing to judge here; a fixture with the subject would decide it." - }, - "OBS-EVD-01": { - "class": "needs-runtime", - "why": "The report states `needs-runtime`; declared facts: satellite.packageJson, traces.", - "followUp": "An adapter that observes the running system, through the enforcer seam." - }, - "OBS-EVD-02": { - "class": "needs-runtime", - "why": "The report states `needs-runtime`; declared facts: satellite.packageJson, traces.", - "followUp": "An adapter that observes the running system, through the enforcer seam." - }, - "OBS-EVD-03": { - "class": "needs-external-system", - "why": "The report states `needs-external-system`; declared facts: satellite.packageJson, telemetryBackend.", - "followUp": "An adapter over the external system, through the enforcer seam." - } - } + "opaOnly": {} } } } diff --git a/.harness/scripts/ci/engine-verdict-parity.baseline.json b/.harness/scripts/ci/engine-verdict-parity.baseline.json index 8c097a6aa..18b6a6e99 100644 --- a/.harness/scripts/ci/engine-verdict-parity.baseline.json +++ b/.harness/scripts/ci/engine-verdict-parity.baseline.json @@ -16,9 +16,15 @@ "over the in-scope rules the triage table called non-executable on this repository — KI-R01..07 (no validationQuery) and", "PROT-03/06 (judgement) — while the OPA engine decided them. With the class derived from each rule's `facts`, those rules", "declare the facets their policies read and are executable (`needs-supplied-facts`); nothing non-executable remains in scope", - "here, the row no longer fires, and the two engines stop disagreeing about it. CLI-EXIT-01/03 are the conflicts that remain." + "here, the row no longer fires, and the two engines stop disagreeing about it. CLI-EXIT-01/03 are the conflicts that remain.", + "2026-09-21 (GT-716 AC4): GOV-RULE-NON-EXECUTABLE is back, for a different reason than the one removed above, and it is a reason about", + "WHERE each run found its corpus rather than about the rule. This guard runs the CLI from the repository root without `--core`; on that", + "run the native engine resolves the corpus to the CLI's bundled copy and FAILS the 138 generated ADR-conformance rules (their referenced", + "ADRs are not in the copy — the false failures guard 73 moved to an export to escape), so it counts 0 non-executable and never emits the", + "row. The OPA engine, since AC4, states `documentation-only` for a policy-less rule whose declaration says there is nothing to check, counts", + "the 138 as non-executable and emits the row (COULD, non-blocking). With `--core .` both engines count 138 and emit the identical row." ], - "measuredOn": "2026-09-20", + "measuredOn": "2026-09-21", "method": "evolith validate --engine {native,opa} --format json over the whole corpus, both from the repository root; only rules BOTH engines decided are compared.", "conflicts": [ { @@ -36,6 +42,14 @@ "family": "native-context-assumption", "reason": "Same doubled root as CLI-EXIT-01, reported through the other branch of the same handler: 'exit-code taxonomy not declared: .../src/sdk/cli/src/sdk/cli/src/infrastructure/cli/exit-codes.ts not found'. Registered separately because a partial fix that repaired only the scan and not the taxonomy read is a real state, and the guard should be able to say which half moved.", "followUp": "Same fix as CLI-EXIT-01." + }, + { + "ruleId": "GOV-RULE-NON-EXECUTABLE", + "native": "passed", + "opa": "failed", + "family": "corpus-resolution-artifact", + "reason": "The synthetic row counts rules whose evaluability is `documentation-only` or `underspecified`. On this guard's run (repository root, no `--core`) the native engine resolves the corpus to the CLI's bundled rulesets copy and fails the 138 generated ADR-conformance rules — `CORE-0001-01: the ruleset cites 1 decision record(s) that do not exist: reference/core/architecture/adrs/core/0001-…` — so `rulesNonExecutable` is 0 and the row is absent (`passed` by 68's default). The OPA engine states the declaration's class for a policy-less rule with `facts: []` since GT-716 AC4, counts 138 non-executable and emits the row: `138 corpus rules are not executable by any engine` (COULD, non-blocking). Measured with `--core .` on 2026-09-21: both engines count 138 and emit the identical row — the disagreement is the corpus each run found, not the rule.", + "followUp": "Run this guard with `--core` at the repository root (or on the export guard 73 measures), which makes the two runs read the same corpus — and re-baseline CLI-EXIT-01/03, which pass once `corePath` is a resolvable root, plus whatever the same-corpus run disagrees on (measured 2026-09-21: GOV-ENGINE-COVERAGE and CLI-EXIT-02). GT-716 AC5 territory: the report saying the same thing on both engines." } ] } diff --git a/docs/known-limitations.es.md b/docs/known-limitations.es.md index 4c3680e55..8a416072c 100644 --- a/docs/known-limitations.es.md +++ b/docs/known-limitations.es.md @@ -17,7 +17,7 @@ Auditoría completa de nuestras propias afirmaciones, con qué bloquea cada pend | `--engine opa` | 133 de 159 | 26 | | nativo (por defecto) | 41 de 159 | 118 | -CI exige que coincidan sobre **hechos**, no sobre cobertura; eso es por diseño — y desde el AC3 de GT-716 cada regla que solo un motor decide queda registrada por regla, en ambas direcciones, sobre este repositorio y sobre un satélite recién salido de `init` (`73-validate-engine-coverage-parity.mjs`), de modo que una diferencia de cobertura es un diff que alguien lee y no un número que nadie lee. Que el comando por defecto no lo diga, no lo es ([#628](https://github.com/beyondnetcode/evolith_arch32/issues/628)). Por eso la portada usa `--engine opa` en todas partes. Medido de nuevo el 2026-09-20 con la CLI construida desde este árbol: el motor por defecto decide 56 de las mismas 159, y de las 76 reglas que solo `--engine opa` decide, 73 son veredictos sobre facetas que una ejecución a secas nunca suministra — así que la mayor parte de esa cobertura extra no es cobertura. Se sigue como GT-716 en el [Tablero de Gaps](../reference/core/control-center/gaps/gap-tracking.es.md). Desde `b2840947` (en el árbol, aún no en una CLI publicada) el motor OPA reporta esas reglas como `skipped` con la faceta que le falta: sobre el mismo satélite decide 10 de 159 a partir de lo que una ejecución a secas observa, y el resto solo cuando el llamador suministra los hechos por `facts.satellite`. +CI exige que coincidan sobre **hechos**, no sobre cobertura; eso es por diseño — y desde el AC3 de GT-716 cada regla que solo un motor decide queda registrada por regla, en ambas direcciones, sobre este repositorio y sobre un satélite recién salido de `init` (`73-validate-engine-coverage-parity.mjs`), de modo que una diferencia de cobertura es un diff que alguien lee y no un número que nadie lee. Desde el AC4 (2026-09-21) nada en ese registro está por omisión: cada entrada de deuda lleva una decisión registrada (`engine-coverage-decisions.json`) que el guard contrasta con las ejecuciones, y el lado solo-OPA está vacío en ambos escenarios — las últimas reglas que solo `--engine opa` decidía desde el árbol (`OBS-EVD-01..03`, `MCP-05`) tienen gemelos nativos de sus políticas. Que el comando por defecto no lo diga, no lo es ([#628](https://github.com/beyondnetcode/evolith_arch32/issues/628)). Por eso la portada usa `--engine opa` en todas partes. Medido de nuevo el 2026-09-20 con la CLI construida desde este árbol: el motor por defecto decide 56 de las mismas 159, y de las 76 reglas que solo `--engine opa` decide, 73 son veredictos sobre facetas que una ejecución a secas nunca suministra — así que la mayor parte de esa cobertura extra no es cobertura. Se sigue como GT-716 en el [Tablero de Gaps](../reference/core/control-center/gaps/gap-tracking.es.md). Desde `b2840947` (en el árbol, aún no en una CLI publicada) el motor OPA reporta esas reglas como `skipped` con la faceta que le falta: sobre el mismo satélite decide 10 de 159 a partir de lo que una ejecución a secas observa, y el resto solo cuando el llamador suministra los hechos por `facts.satellite`. ## Dos reglas de infraestructura no están en ningún denominador diff --git a/docs/known-limitations.md b/docs/known-limitations.md index eb9fb506f..c3b20234a 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -17,7 +17,7 @@ Full audit of our own claims, with what blocks each pending item and who can unb | `--engine opa` | 133 of 159 | 26 | | native (default) | 41 of 159 | 118 | -CI holds them to agreement over **facts**, not over coverage; that part is by design — and since GT-716 AC3 every rule only one engine decides is registered per rule, in both directions, on this repository and on a satellite fresh from `init` (`73-validate-engine-coverage-parity.mjs`), so a coverage difference is a diff somebody reads rather than a number nobody does. That the default command never says so is not ([#628](https://github.com/beyondnetcode/evolith_arch32/issues/628)). That is why the front page uses `--engine opa` everywhere. Measured again on 2026-09-20 with the CLI built from this tree: the default decides 56 of the same 159, and of the 76 rules only `--engine opa` decides, 73 are verdicts on facets a bare run never supplies — so most of that extra coverage is not coverage. Tracked as GT-716 in the [Gap Tracking Board](../reference/core/control-center/gaps/gap-tracking.md). Since `b2840947` (in the tree, not yet in a published CLI) the OPA engine reports those rules as `skipped` with the facet it lacks: on the same satellite it decides 10 of 159 from what a bare run observes, and the rest only when the caller supplies the facts through `facts.satellite`. +CI holds them to agreement over **facts**, not over coverage; that part is by design — and since GT-716 AC3 every rule only one engine decides is registered per rule, in both directions, on this repository and on a satellite fresh from `init` (`73-validate-engine-coverage-parity.mjs`), so a coverage difference is a diff somebody reads rather than a number nobody does. Since AC4 (2026-09-21) nothing in that register is there by omission: every debt entry carries a recorded decision (`engine-coverage-decisions.json`) the guard holds to the runs, and the OPA-only side is empty on both scenarios — the last rules only `--engine opa` decided from the tree (`OBS-EVD-01..03`, `MCP-05`) have native twins of their policies. That the default command never says so is not ([#628](https://github.com/beyondnetcode/evolith_arch32/issues/628)). That is why the front page uses `--engine opa` everywhere. Measured again on 2026-09-20 with the CLI built from this tree: the default decides 56 of the same 159, and of the 76 rules only `--engine opa` decides, 73 are verdicts on facets a bare run never supplies — so most of that extra coverage is not coverage. Tracked as GT-716 in the [Gap Tracking Board](../reference/core/control-center/gaps/gap-tracking.md). Since `b2840947` (in the tree, not yet in a published CLI) the OPA engine reports those rules as `skipped` with the facet it lacks: on the same satellite it decides 10 of 159 from what a bare run observes, and the rest only when the caller supplies the facts through `facts.satellite`. ## Two infrastructure rules are in no denominator 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 d5d8c974b..eefa46f4a 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -10355,7 +10355,7 @@ Los dos se arreglaron de forma estructural y no como correcciones: el rethrow no - [x] **Una faceta suministrada ausente es `skipped` en OPA, nunca un veredicto.** El manifiesto del bundle lleva, por id de regla, las rutas de input que lee su política (`compile-opa-wasm.mjs` ya parsea el AST de cada política), y `OpaEvaluator` devuelve `skipped` con una clase que nombra la faceta ausente cuando el input no la lleva. **FALSABILIDAD:** sobre el satélite nuevo `--engine opa` pasa de 133 decididas a 60 como máximo, las siete entradas `supplied-facet-absent` de `engine-verdict-parity.baseline.json` se eliminan porque el guard 68 las reporta obsoletas, y suministrar la faceta por el canal de GT-694 sigue devolviendo `MTN-01` — una evaluación real, no una respuesta fija. Ambas salidas registradas. **CUMPLIDO en `b2840947`, y el falsador se movió más lejos de lo que pedía el criterio.** `compile-opa-wasm.mjs` compila un segundo entrypoint de manifiesto, `evolith/manifest/rule_input_paths` (200 de los 238 ids declarados enuncian qué leen; los otros 38 son ids de gate), extraído del AST del compilador por `.harness/scripts/lib/rego-rule-inputs.mjs` — lecturas directas, cabeceras, reglas auxiliares seguidas transitivamente — y `OpaEvaluator` devuelve `skipped` / `supplied-facet-absent` nombrando la faceta cuando el input no lleva nada de lo que la regla lee; `27-opa-parity-gate` hace fallar un bundle que deje de exponerlo. Medido sobre el mismo satélite nuevo: `--engine opa` pasó de **133 decididas a 10**, no a 60 — el «73» que registró esta fila contaba solo las reglas que el nativo saltaba, y 47 de las 57 reglas que ambos motores «decidían» eran también veredictos de OPA sobre input ausente (`DOD-*` sobre `context`, `TAX-*` sobre `repository`, `KI-R*` sobre `knowledge_id`/`review`, `INH-02` sobre `contracts`…); las 10 que quedan leen lo que el builder observa (`INH-06` falla por el `DECISIONS.md` ausente, `OBS-EVD-01/02/03` por los paquetes ausentes — `OBS-EVD-03` lee `packageJson`, no `scorecards` como dice la tabla de arriba; gana el AST). El guard 68 reportó **ocho** entradas obsoletas en la línea base, no siete: las siete `supplied-facet-absent` y `TAX-01`, cuya razón registrada (listas de exención distintas) era errónea — `repository-taxonomy.rego` lee `input.repository.files`, una faceta que ningún canal transporta, así que su `passed` era una lista ausente; las ocho eliminadas, quedan 3 conflictos (`CLI-EXIT-01/03`, `GOV-RULE-NON-EXECUTABLE`), decididas por ambos 28 → 14. Suministrar la faceta por el canal de GT-694 devuelve `MTN-01` `failed` con `applicationFiltering: false` y `passed` con `true` (`opa-supplied-facts.spec.ts`, bundle real). Sobrevive una excepción mantenida a mano, `ABSENCE_IS_A_FACT` (`qualityEvidence`, `evaluationDate`, `qualityAdmissibilityPolicy`, `evidence`, `waiver`, `tenantId`): facetas cuya ausencia ambos motores ya tratan como hecho (`PEA-01..04` pasan sobre una ejecución a secas en nativo y en Rego, ADR-0111) — el AC2 la lleva a los ficheros de regla. `GOV-ENGINE-COVERAGE` ya no le dice al lector que OPA «decide más». - [x] **Una sola declaración de evaluabilidad por regla, y ambos motores derivan de ella.** Cada regla de `*.rules.json` declara los hechos que necesita y su procedencia (`observed` / `supplied` / `runtime` / `external` / `none`); `RULE_TRIAGE` pasa a ser una proyección de ella (o una comprobación contra ella), y el manifiesto del bundle también. Un guard falla cuando un `.rego` lee una faceta que su regla no declara, cuando un handler nativo reclama una regla declarada no ejecutable, y cuando los dos motores clasifican distinto la misma regla. **FALSABILIDAD:** las 12 reglas que hoy el nativo clasifica como no ejecutables y OPA decide (`KI-R01..07`, `INH-03..05`, `PROT-03`, `PROT-06`) lo ponen en rojo hasta que un lado cambie; `PEA-01..04` siguen en verde sin tocarlas. **CUMPLIDO en `3d76f4a6`.** Cada regla de `*.rules.json` (415 de 415) declara `facts` — las facetas que lee su comprobación, por id desde el nuevo vocabulario `src/rulesets/schema/facets.json` (85 facetas, cada una con su procedencia: `observed` / `supplied` / `external` / `runtime`) — y ambos motores derivan de ella: `classifyRule` toma la clase de la procedencia más exigente (`runtime` > `external` > `supplied` > `observed`; `facts: []` es documentación tras un juicio o un placeholder del generador, sin especificar tras nada), `RULE_TRIAGE` ya no existe, y `npm run build:policy` rechaza un bundle cuando una política lee una faceta que su regla no declaró o cuando una entrada del vocabulario no la declara ninguna regla ni la lee ninguna política. Se añadió una clase nativa, `needs-supplied-facts` (una postura que solo los dueños pueden declarar; OPA la decide cuando se suministra, el nativo nunca). **FALSADOR, ambas salidas registradas:** declarar `facts: []` para las doce pone el chequeo del build en rojo con doce hallazgos, uno por regla nombrando las facetas que lee su política (`INH-03 … reads corePath, satellite.contracts, satellitePath which its facts (none) do not declare`); declarar lo que leen las políticas lo devuelve a verde y mueve las doce al denominador ejecutable; `PEA-01..04` siguen `native-handler` con `qualityEvidence` declarado. **Lo que se movió cuando la declaración sustituyó a la tabla — 44 de 415 reglas, sin implementar nada:** `unimplemented-native` 52 → 21 (25 de esas filas las decidían políticas sobre una postura declarada, el sistema de CI o una ejecución de tests, nunca un handler sobre el árbol), `needs-external-system` 20 → 27, `needs-runtime` 17 → 23, `needs-supplied-facts` 0 → 31, `documentation-only` 141 → 138, `underspecified` 14 → 4; las 73 reglas bloqueantes que no corren son las mismas 73, recosteadas (11 handlers, 16 adaptadores, 15 observadores de runtime, 27 posturas declaradas, 4 decisiones de autoría). El guard 68 declaró entonces obsoleto `GOV-RULE-NON-EXECUTABLE` — con las doce ejecutables, nada no ejecutable queda en alcance en este repositorio —, así que la línea base queda en `CLI-EXIT-01/03`. Sobre el satélite nuevo no cambió lo que decide ningún motor (nativo 56, OPA 10); las siete filas `KI-R` hacen fallar ahora la ejecución como `needs-supplied-facts` en vez de esconderse en el conteo de no ejecutables. Snapshot, mapeo ISO 5055 (backlog 21: 9 adoptables, 2 parciales, 10 por escribir) y el README de estándares rederivados. Un juicio queda en código: `ABSENCE_IS_A_FACT`, reducido a las cuatro facetas cuya ausencia ambos motores ya tratan como hecho. - [x] **La diferencia de cobertura es un ratchet por regla, en ambas direcciones y en ambos escenarios.** `68-validate-engine-verdict-parity.mjs` (o un hermano que reutilice su `deriveOutcomes`) deja en línea base cada id de `coverageOnly` con su propia razón medida y su seguimiento, sobre la raíz del repositorio Y sobre un satélite producido por `init`; una regla de un solo motor sin registrar falla, y una registrada que ahora deciden ambos falla hasta que se retire su entrada. **FALSABILIDAD:** quitar un `import` de `main.rego` lo pone en rojo nombrando los ids que el bundle dejó de decidir; restaurarlo lo devuelve a verde. **CUMPLIDO en `29c8a4ba`.** `73-validate-engine-coverage-parity.mjs` — hermano de 68 que reutiliza su `deriveOutcomes` — corre ambos motores sobre la raíz del repositorio y sobre un satélite que crea con `evolith init` en un directorio temporal, y sujeta cada regla que solo un motor decide a `engine-coverage-parity.baseline.json`: por escenario, por dirección, por regla, con la razón que dio el otro motor — la clase que enuncia su informe y las facetas que nombra su fila de salto cuando la fila existe; el manifiesto del bundle y el conjunto que emite el constructor de input cuando no — y un seguimiento por clase. Una regla sin registrar, una entrada obsoleta o una clase cambiada hacen fallar; `--write` regenera el fichero para revisión. Ambos escenarios leen el Core desde una exportación del árbol versionado más el bundle compilado — la primera corrida en CI discrepó del portátil que escribió la línea base (un directorio `coverage/` local decidía `EM-Y-01`/`QT-01`, el historial git decidía `DRIFT-01`, y la corrida sobre el árbol de trabajo resolvía las referencias de las reglas ADR-conformance contra la copia empaquetada de la CLI y fallaba 138 de ellas falsamente), así que los artefactos no versionados no pueden voltear una regla entre máquinas. Medido el 2026-09-20 sobre esa exportación: repositorio 82 solo-nativo (52 `supplied-facet-absent`, 26 `no-policy-in-bundle`, 4 en que la vía OPA saltó sin una razón propia — las `HXA-01/02/04/05` enrutadas al enforcer, archivadas como `opa-gave-no-reason` y no como deuda de handler) y 7 solo-OPA (`MCP-01..03` `unimplemented-native`, `OBS-EVD-01/02` `needs-runtime`, `OBS-EVD-03` `needs-external-system`, `MCP-05` handler que declinó); satélite `init` 49 solo-nativo (34 / 11 / 4) y las mismas 7 solo-OPA. **FALSADOR, ambas salidas registradas:** la prueba que nombraba este criterio — quitar un `import` de `main.rego` — no puede correr desde GT-675, porque el build del bundle rechaza una política alcanzable que nadie importa; la prueba equivalente es renombrar `OBS-EVD-03` en `telemetry-evidence.rego` para que el bundle deje de decidirla: el guard se puso en rojo en ambos escenarios nombrando el id (`opaOnly OBS-EVD-03 … no longer coverage-only`), y volvió a verde al restaurarla. Corre en el job `Test` tras 68 (≈17 s + ≈3 s); clasificado INSTRUMENTED por el guard 42 y en rojo en el sandbox vacío del guard 43; 11 tests unitarios. `known-limitations` dice ahora que CI registra las diferencias de cobertura por regla en vez de solo permitirlas. - - [ ] **El backlog nombrado queda implementado o declarado, nada de él dejado en `coverageOnly` por omisión:** handlers nativos para `MCP-05`, `OBS-EVD-01`, `OBS-EVD-02` (y, sobre el corpus, `MCP-01..04`, `DEP-08`, `TAX-07/08`); un `.rego` para las 11 reglas `no-policy-in-bundle` nombradas arriba; ambos para las 7 que no decide ningún motor; y una decisión registrada para las 138 reglas ADR-conformance y `MM-R*` — un gemelo `.rego` generado, o `documentation-only` en los dos motores. + - [x] **El backlog nombrado queda implementado o declarado, nada de él dejado en `coverageOnly` por omisión:** handlers nativos para `MCP-05`, `OBS-EVD-01`, `OBS-EVD-02` (y, sobre el corpus, `MCP-01..04`, `DEP-08`, `TAX-07/08`); un `.rego` para las 11 reglas `no-policy-in-bundle` nombradas arriba; ambos para las 7 que no decide ningún motor; y una decisión registrada para las 138 reglas ADR-conformance y `MM-R*` — un gemelo `.rego` generado, o `documentation-only` en los dos motores. **CUMPLIDO en `ea736a75`.** Implementado: `TelemetryEvidenceRuleHandler` decide `OBS-EVD-01..03` desde las dependencias del satélite — el mismo proxy y las mismas listas de paquetes que `telemetry-evidence.rego`, luego el mismo veredicto (las reglas declaran `satellite.packageJson` y nada que no lean; `telemetryBackend` salió del vocabulario, 84 facetas); `McpRuleHandler` decide `MCP-05` desde la fuente del servidor y falla `MCP-01..03` por evidencia de humo ausente con las palabras de la propia política, donde antes saltaba; `MCP-04`, `DEP-08` y `TAX-07/08` ya las decidían ambos motores o estaban registradas como `supplied-facet-absent`. Sobre un scaffold en fase 0 las tres reglas de telemetría quedan **no aplicables** en vez de fallidas: declaran `appliesFromSdlcPhase: 3` (Construcción), que es lo que dice su texto — rutas de petición y servicios *en producción* — y lo que MTN-05 ya hacía para Diseño; el invariante de GT-571 de que un satélite recién inicializado no hace nada mal se cumplía en el motor nativo y se cumple ahora en los dos (`--engine opa` fallaba las tres en cada scaffold; CI cazó al gemelo nativo haciendo lo mismo antes de la anotación). La misma corrida cazó a `MCP-01..03` fallando un satélite recién creado validado contra un Core sin evidencia de humo — tres hallazgos bloqueantes dirigidos al Core, la queja de GT-571 al pie de la letra —, así que el pack MCP declara `audience: core` donde lo lee la aplicabilidad, como hacen los packs de la CLI y como su `scope: core-cli` siempre dijo; sobre el Core ambos motores siguen decidiendo las cinco. Declarado, en la regla: las siete que no decidía ningún motor (`SEC-INJ-01/02`, `SEC-PATH-01/02`, `SEC-TIMING-01/02`, `SEC-RL-03`) declaran `satellite.findings` — si `child_process.exec` recibe input interpolado o si una comparación de credenciales es en tiempo constante es el hallazgo de un escáner sobre el AST, no una regex sobre el árbol, que `MM-R10` en este mismo corpus prohíbe — y `QT-05`, que `SdlcRuleHandler` respondía `passed` con «requiere análisis en runtime», declara `satellite.testing` y no la reclama nadie. Registrado y exigido: `engine-coverage-decisions.json`, ocho decisiones — `SSDF-*` ×8, `SLSA-*` ×4, `SEC-RL-01/02`, `MM-R*` ×11 y las `HXA-01/02/04/05` enrutadas al enforcer como `native-only`, cada una con su porqué y con lo que la reabriría; las 138 reglas ADR-conformance generadas como un patrón `neither`, `documentation-only` en los dos motores ahora que `OpaEvaluator` enuncia la clase de la declaración para una regla sin política cuyo `facts: []` dice que no hay nada que comprobar; las siete reglas de escáner y `QT-05` como `neither` — y el guard 73 hace fallar una entrada de clase deuda sin decisión como solo-un-motor por omisión, y una decisión que las ejecuciones contradigan. **FALSADOR, observado antes de confiar en él:** la primera corrida del guard extendido se puso en rojo con `MM-R03 (decision modular-monolith-native-only)` — `modular-monolith.rego` la decide y ambos motores coinciden, cosa que el registro no decía — y en verde en cuanto el id salió de la decisión. Medido el 2026-09-21 sobre la exportación: solo-OPA **0 / 0** (era 7 / 7); solo-nativo 81 / 48 (era 82 / 49, salió `QT-05`) — 52 / 34 `supplied-facet-absent`, 25 / 10 `no-policy-in-bundle` con una decisión cada una, 4 `opa-gave-no-reason` con una. **No hecho tal como el criterio lo formuló al principio:** no se escribió ningún gemelo `.rego` para las 11 reglas sin política — cada una es una decisión registrada (`SSDF`/`SLSA`: el constructor de input no proyecta nada de lo que lee el handler, así que un gemelo es la misma comprobación tras una segunda proyección; `SEC-RL-01/02`: un escaneo de fuente; `MM-R*`: un grafo de imports que la vía OPA no lleva hasta que la costura de GT-514 lo alimente). Costes: `unimplemented-native` 21 → 14, reglas bloqueantes que no corren 73 → 71 (5 handlers, 21 adaptadores, 14 runtime, 27 posturas, 4 de autoría), backlog ISO 5055 14 (5 adoptables, 9 por escribir). El guard 68 ganó un conflicto registrado, `GOV-RULE-NON-EXECUTABLE`: en su corrida sin `--core` el motor nativo resuelve el corpus a la copia empaquetada de la CLI y falla las 138 reglas ADR-conformance (0 no ejecutables) mientras OPA ahora cuenta 138; con `--core .` ambos motores emiten la misma fila — llevar el 68 a la exportación es del AC5. - [ ] **El informe y la página dicen lo mismo.** `GOV-ENGINE-COVERAGE` y `docs/known-limitations.es.md` enuncian la cobertura por motor con el desglose de facetas suministradas, y la portada o deja de necesitar `--engine opa` por razones de cobertura o dice qué razón queda. - **Dependencias:** GT-694 (el canal de facetas suministradas, COMPLETADO), GT-675 (el manifiesto del bundle, COMPLETADO), GT-704 (la línea base de veredictos que esta extiende, COMPLETADO); la costura del enforcer (GT-514) para `satellite.layers`. - **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 b6cc58646..e95a60d06 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -10448,7 +10448,7 @@ Both were fixed structurally rather than corrected: the rethrow now names BOTH f - [x] **An absent supplied facet is `skipped` on OPA, never a verdict.** The bundle manifest carries, per rule id, the input paths its policy reads (`compile-opa-wasm.mjs` already parses each policy's AST), and `OpaEvaluator` reports `skipped` with a class that names the missing facet when the input does not carry it. **FALSIFIABILITY:** on the fresh satellite `--engine opa` moves from 133 decided to at most 60, the seven `supplied-facet-absent` entries of `engine-verdict-parity.baseline.json` are removed because guard 68 reports them stale, and supplying the facet through GT-694's channel still returns `MTN-01` — a real evaluation, not a fixed answer. Both outputs recorded. **MET in `b2840947`, and the falsifier moved further than the criterion asked.** `compile-opa-wasm.mjs` compiles a second manifest entrypoint, `evolith/manifest/rule_input_paths` (200 of the 238 declared ids state what they read; the 38 others are gate ids), extracted from the compiler's AST by `.harness/scripts/lib/rego-rule-inputs.mjs` — direct reads, heads, helper rules followed transitively — and `OpaEvaluator` reports `skipped` / `supplied-facet-absent` naming the facet when the input carries none of what the rule reads; `27-opa-parity-gate` fails a bundle that stops exposing it. Measured on the same fresh satellite: `--engine opa` went from **133 decided to 10**, not to 60 — the "73" this row registered counted only the rules the native engine skipped, and 47 of the 57 rules both engines "decided" were OPA verdicts on absent input as well (`DOD-*` on `context`, `TAX-*` on `repository`, `KI-R*` on `knowledge_id`/`review`, `INH-02` on `contracts`…); the 10 that remain read what the builder observes (`INH-06` fails on the missing `DECISIONS.md`, `OBS-EVD-01/02/03` on the missing packages — `OBS-EVD-03` reads `packageJson`, not `scorecards` as the table above says; the AST wins). Guard 68 reported **eight** baseline entries stale, not seven: the `supplied-facet-absent` seven and `TAX-01`, whose registered reason (differing exemption lists) was wrong — `repository-taxonomy.rego` reads `input.repository.files`, a facet no channel carries, so its `passed` was an absent list; all eight removed, 3 conflicts remain (`CLI-EXIT-01/03`, `GOV-RULE-NON-EXECUTABLE`), jointly decided 28 → 14. Supplying the facet through GT-694's channel returns `MTN-01` `failed` on `applicationFiltering: false` and `passed` on `true` (`opa-supplied-facts.spec.ts`, real bundle). One hand-kept exemption survives, `ABSENCE_IS_A_FACT` (`qualityEvidence`, `evaluationDate`, `qualityAdmissibilityPolicy`, `evidence`, `waiver`, `tenantId`): facets whose absence both engines already treat as a fact (`PEA-01..04` pass on a bare run natively and in Rego, ADR-0111) — AC2 moves it into the rule files. `GOV-ENGINE-COVERAGE` no longer tells the reader OPA "decides more". - [x] **One evaluability declaration per rule, and both engines derive from it.** Each rule in `*.rules.json` declares the facts it needs and their provenance (`observed` / `supplied` / `runtime` / `external` / `none`); `RULE_TRIAGE` becomes a projection of it (or a check against it), and so does the bundle manifest. A guard fails when a `.rego` reads a facet its rule does not declare, when a native handler claims a rule declared non-executable, and when the two engines class the same rule differently. **FALSIFIABILITY:** the 12 rules native classes non-executable and OPA decides today (`KI-R01..07`, `INH-03..05`, `PROT-03`, `PROT-06`) turn it red until one side changes; `PEA-01..04` stay green untouched. **MET in `3d76f4a6`.** Every rule in `*.rules.json` (415 of 415) declares `facts` — the facets its check reads, by id from the new vocabulary `src/rulesets/schema/facets.json` (85 facets, each with its provenance: `observed` / `supplied` / `external` / `runtime`) — and both engines derive from it: `classifyRule` takes the class of the most demanding provenance (`runtime` > `external` > `supplied` > `observed`; `facts: []` is documentation behind a judgement or a generator placeholder, underspecified behind nothing), `RULE_TRIAGE` is gone, and `npm run build:policy` refuses a bundle when a policy reads a facet its rule did not declare or when a vocabulary entry is declared by no rule and read by no policy. One native class was added, `needs-supplied-facts` (a posture only the owners can declare; OPA decides it when supplied, native never). **FALSIFIER, both outputs recorded:** declaring `facts: []` for the twelve turns the build check red with twelve findings, one per rule naming the facets its policy reads (`INH-03 … reads corePath, satellite.contracts, satellitePath which its facts (none) do not declare`); declaring what the policies read turns it green and moves all twelve into the executable denominator; `PEA-01..04` stay `native-handler` with `qualityEvidence` declared. **What moved when the declaration replaced the table — 44 of 415 rules, nothing implemented:** `unimplemented-native` 52 → 21 (25 of those rows were decided by policies over a declared posture, the CI system or a test run, never by a handler over the tree), `needs-external-system` 20 → 27, `needs-runtime` 17 → 23, `needs-supplied-facts` 0 → 31, `documentation-only` 141 → 138, `underspecified` 14 → 4; the 73 blocking rules that do not run are the same 73, re-costed (11 handlers, 16 adapters, 15 runtime observers, 27 declared postures, 4 authoring decisions). Guard 68 then declared `GOV-RULE-NON-EXECUTABLE` stale — with the twelve executable, nothing non-executable remains in scope on this repository — so the baseline is down to `CLI-EXIT-01/03`. On the fresh satellite nothing changed in what either engine decides (native 56, OPA 10); the seven `KI-R` rows now fail the run as `needs-supplied-facts` instead of hiding inside the non-executable count. Snapshot, ISO 5055 mapping (backlog 21: 9 adoptable, 2 partial, 10 to author) and the standards README re-derived. One judgement stays in code: `ABSENCE_IS_A_FACT`, down to the four facets whose absence both engines already treat as a fact. - [x] **The coverage difference is a per-rule ratchet, both directions, both scenarios.** `68-validate-engine-verdict-parity.mjs` (or a sibling reusing its `deriveOutcomes`) baselines every id in `coverageOnly` with its own measured reason and follow-up, over the repository root AND a satellite produced by `init`; an unregistered one-engine rule fails, and a registered one that both engines now decide fails until its entry is removed. **FALSIFIABILITY:** removing one `import` from `main.rego` turns it red naming the ids the bundle stopped deciding; restoring it turns it green. **MET in `29c8a4ba`.** `73-validate-engine-coverage-parity.mjs` — a sibling of 68 reusing its `deriveOutcomes` — runs both engines on the repository root and on a satellite it creates with `evolith init` in a temporary directory, and holds every rule only one engine decides to `engine-coverage-parity.baseline.json`: per scenario, per direction, per rule, with the reason the other engine gave — the class its report states and the facets its skip row names when the row exists; the bundle manifest and the input builder's emitted set when it does not — and a follow-up per class. An unregistered rule, a stale entry or a changed class fails; `--write` regenerates the file for review. Both scenarios read the Core from an export of the tracked tree plus the compiled bundle — the first CI run disagreed with the laptop that wrote the baseline (a local `coverage/` directory decided `EM-Y-01`/`QT-01`, git history decided `DRIFT-01`, and the working-tree run resolved the ADR-conformance rules' references against the CLI's bundled copy and failed 138 of them falsely), so untracked artifacts cannot flip a rule between machines. Measured 2026-09-20 on that export: repository 82 native-only (52 `supplied-facet-absent`, 26 `no-policy-in-bundle`, 4 where the OPA path skipped without a reason of its own — the enforcer-routed `HXA-01/02/04/05`, filed as `opa-gave-no-reason` rather than as handler debt) and 7 opa-only (`MCP-01..03` `unimplemented-native`, `OBS-EVD-01/02` `needs-runtime`, `OBS-EVD-03` `needs-external-system`, `MCP-05` handler declined); init satellite 49 native-only (34 / 11 / 4) and the same 7 opa-only. **FALSIFIER, both outputs recorded:** the probe this criterion named — removing an import from `main.rego` — cannot run since GT-675, because the bundle build refuses a reachable policy nobody imports; the equivalent probe is renaming `OBS-EVD-03` in `telemetry-evidence.rego` so the bundle stops deciding it: the guard went red on both scenarios naming the id (`opaOnly OBS-EVD-03 … no longer coverage-only`), and green again once restored. Runs in the `Test` job after 68 (≈17 s + ≈3 s); classified INSTRUMENTED by guard 42 and red in guard 43's empty sandbox; 11 unit tests. `known-limitations` now says CI registers coverage differences per rule rather than merely allowing them. - - [ ] **The named backlog is implemented or declared, none of it left in `coverageOnly` by omission:** native handlers for `MCP-05`, `OBS-EVD-01`, `OBS-EVD-02` (and, over the corpus, `MCP-01..04`, `DEP-08`, `TAX-07/08`); a `.rego` for the 11 `no-policy-in-bundle` rules named above; both for the 7 neither engine decides; and one recorded decision for the 138 ADR-conformance rules and `MM-R*` — a generated `.rego` twin, or `documentation-only` on both engines. + - [x] **The named backlog is implemented or declared, none of it left in `coverageOnly` by omission:** native handlers for `MCP-05`, `OBS-EVD-01`, `OBS-EVD-02` (and, over the corpus, `MCP-01..04`, `DEP-08`, `TAX-07/08`); a `.rego` for the 11 `no-policy-in-bundle` rules named above; both for the 7 neither engine decides; and one recorded decision for the 138 ADR-conformance rules and `MM-R*` — a generated `.rego` twin, or `documentation-only` on both engines. **MET in `ea736a75`.** Implemented: `TelemetryEvidenceRuleHandler` decides `OBS-EVD-01..03` from the satellite's dependencies — the same proxy and the same package lists as `telemetry-evidence.rego`, so the same verdict (the rules declare `satellite.packageJson` and nothing they do not read; `telemetryBackend` left the vocabulary, 84 facets); `McpRuleHandler` decides `MCP-05` from the server source and fails `MCP-01..03` on absent smoke evidence with the policy's own words, where it used to skip; `MCP-04`, `DEP-08` and `TAX-07/08` were already decided by both engines or registered as `supplied-facet-absent`. On a phase-0 scaffold the three telemetry rules are **not applicable** rather than failed: they declare `appliesFromSdlcPhase: 3` (Construction), which is what their text says — *production* request paths and services — and what MTN-05 already did for Design; the GT-571 invariant that a freshly initialised satellite does nothing wrong held on the native engine and holds on both now (`--engine opa` used to fail the three on every scaffold; CI caught the native twin doing the same before the annotation). The same run caught `MCP-01..03` failing a fresh satellite validated against a Core with no smoke evidence — three blocking findings addressed to the Core, the GT-571 complaint verbatim — so the MCP pack declares `audience: core` where applicability reads it, as the CLI packs do and as its `scope: core-cli` always said; on the Core both engines still decide all five. Declared, in the rule: the seven neither engine decided (`SEC-INJ-01/02`, `SEC-PATH-01/02`, `SEC-TIMING-01/02`, `SEC-RL-03`) declare `satellite.findings` — whether `child_process.exec` receives interpolated input or a credential comparison is constant-time is a scanner's finding over the AST, not a regex over the tree, which `MM-R10` in this same corpus forbids — and `QT-05`, which `SdlcRuleHandler` answered `passed` with "requires runtime analysis", declares `satellite.testing` and is claimed by nobody. Recorded and enforced: `engine-coverage-decisions.json`, eight decisions — `SSDF-*` ×8, `SLSA-*` ×4, `SEC-RL-01/02`, `MM-R*` ×11 and the enforcer-routed `HXA-01/02/04/05` as `native-only`, each with why and what reopens it; the 138 generated ADR-conformance rules as a `neither` pattern, `documentation-only` on both engines now that `OpaEvaluator` states the declaration's class for a policy-less rule whose `facts: []` say there is nothing to check; the seven scanner rules and `QT-05` as `neither` — and guard 73 fails a debt-class entry with no decision as coverage-only by omission, and a decision the runs contradict. **FALSIFIER, observed before it was trusted:** the extended guard's first run went red on `MM-R03 (decision modular-monolith-native-only)` — `modular-monolith.rego` decides it and both engines agree, which the register had not said — and green once the id left the decision. Measured 2026-09-21 on the export: opa-only **0 / 0** (was 7 / 7); native-only 81 / 48 (was 82 / 49, `QT-05` left) — 52 / 34 `supplied-facet-absent`, 25 / 10 `no-policy-in-bundle` with a decision each, 4 `opa-gave-no-reason` with one. **Not done as the criterion first phrased it:** no `.rego` twin was authored for the 11 no-policy rules — each is a recorded decision instead (`SSDF`/`SLSA`: the input builder projects none of what the handler reads, so a twin is the same check behind a second projection; `SEC-RL-01/02`: a source scan; `MM-R*`: an import graph the OPA path does not carry until GT-514's seam feeds it). Costs: `unimplemented-native` 21 → 14, blocking rules that do not run 73 → 71 (5 handlers, 21 adapters, 14 runtime, 27 postures, 4 authoring), ISO 5055 backlog 14 (5 adoptable, 9 to author). Guard 68 gained one registered conflict, `GOV-RULE-NON-EXECUTABLE`: on its `--core`-less run the native engine resolves the corpus to the CLI's bundled copy and fails the 138 ADR-conformance rules (0 non-executable) while OPA now counts 138; with `--core .` both engines emit the identical row — moving 68 onto the export is AC5's. - [ ] **The report and the page say the same thing.** `GOV-ENGINE-COVERAGE` and `docs/known-limitations.md` state coverage per engine with the supplied-facet split, and the front page either stops needing `--engine opa` for coverage reasons or says which reason remains. - **Dependencies:** GT-694 (the supplied-facet channel, DONE), GT-675 (the bundle manifest, DONE), GT-704 (the verdict baseline this extends, DONE); the enforcer seam (GT-514) for `satellite.layers`. - **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 fb9434279..0334e8582 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -4,6 +4,7 @@ **Estado:** Seguimiento Activo **Responsable:** Evolith Architecture Board +**Última Actualización:** 2026-09-21 (**Aterrizó el AC4 de GT-716: nada en el registro de cobertura está por omisión.** `ea736a75` — el backlog nombrado queda implementado o declarado, y `73-validate-engine-coverage-parity.mjs` rechaza ahora una entrada de clase deuda sin decisión registrada. Implementado: `OBS-EVD-01..03` decididas en nativo desde las dependencias del satélite (el mismo proxy que lee su política), `MCP-05` desde la fuente del servidor, `MCP-01..03` fallando por evidencia de humo ausente en los dos motores en vez de en uno — el lado solo-OPA de la línea base está **vacío en ambos escenarios** (era 7 / 7). Declarado: las siete reglas que no decidía ningún motor (`SEC-INJ/PATH/TIMING-01/02`, `SEC-RL-03`) nombran `satellite.findings`, el hallazgo de un escáner sobre el AST y no una regex sobre el árbol (`MM-R10`); `QT-05`, que el motor nativo respondía `passed` con «requiere análisis en runtime», nombra `satellite.testing` y no la decide ninguno. Registrado: `engine-coverage-decisions.json`, ocho decisiones (`SSDF`, `SLSA`, `SEC-RL-01/02`, `MM-R*`, las cláusulas `HXA` enrutadas al enforcer como `native-only`; las 138 reglas ADR-conformance generadas, las siete reglas de escáner y `QT-05` como `neither`), cada una contrastada con las ejecuciones — la primera corrida del guard se puso en rojo con `MM-R03`, que ambos motores deciden y el registro había reclamado, y en verde en cuanto salió. Las 11 reglas sin política recibieron decisiones con razones, no gemelos `.rego`. Backlog de handlers 21 → 14; reglas bloqueantes que no corren 73 → 71; el guard 68 ganó un conflicto registrado (`GOV-RULE-NON-EXECUTABLE`, un artefacto de resolución del corpus de su corrida sin `--core`, que el AC5 retirará). AC5 abierto; contadores sin cambio: **688 / 715 completados · 3 en progreso · 3 pendientes · 21 diferidos**.) **Última Actualización:** 2026-09-20 (**Aterrizó el AC3 de GT-716: lo que solo un motor decide es una diferencia registrada, no libre.** `29c8a4ba` — `73-validate-engine-coverage-parity.mjs` corre ambos motores sobre este repositorio y sobre un satélite que crea con `evolith init`, y sujeta cada regla de un solo motor a una línea base por escenario, por dirección, por regla, con la razón que dio el otro motor (la clase que enuncia su informe; las facetas que la política lee y una ejecución a secas no suministra). Una regla sin registrar, una entrada obsoleta o una clase cambiada hacen fallar. Ambos escenarios leen el Core desde una exportación del árbol versionado, porque la primera corrida en CI discrepó del portátil que escribió la línea base (un directorio `coverage/` local, el historial git y la copia empaquetada del corpus en la CLI habían decidido seis reglas y fallado 138 reglas ADR-conformance falsamente). Medido sobre esa exportación: repositorio 82 solo-nativo / 7 solo-OPA, satélite `init` 49 / 7 — 52 de las 82 son políticas que leen facetas que nadie suministró, 26 no tienen política alguna, y cuatro son las `HXA-01/02/04/05` enrutadas al enforcer, donde la vía OPA salta sin una razón propia. La prueba del criterio (quitar un `import` de `main.rego`) no puede correr desde GT-675 — el build la rechaza —, así que se registró la equivalente: renombrar `OBS-EVD-03` en su política y el guard se pone en rojo en ambos escenarios nombrando el id. AC4–AC5 abiertos; contadores sin cambio: **688 / 715 completados · 3 en progreso · 3 pendientes · 21 diferidos**.) **Última Actualización:** 2026-09-20 (**Aterrizó el AC2 de GT-716: una declaración de evaluabilidad por regla, y ambos motores derivan de ella.** `3d76f4a6` — cada regla de `*.rules.json` declara `facts`, las facetas que lee su comprobación, desde un vocabulario que dice dónde vive la verdad de cada una (`observed` / `supplied` / `external` / `runtime`); `classifyRule` deriva la clase nativa de la procedencia más exigente y la tabla de triaje por id de regla ya no existe; `npm run build:policy` rechaza un bundle cuya política lea una faceta que su regla no declaró. Declarar `facts: []` para las doce reglas que la tabla llamaba no ejecutables mientras Rego las decidía puso ese chequeo en rojo con doce hallazgos; declarar lo que leen las políticas lo devolvió a verde. **Lo que se movió, sin implementar nada: 44 de 415 reglas cambiaron de clase.** El «backlog de handlers» es 21, no 52 — 25 de esas filas las decidían políticas sobre una postura declarada, el sistema de CI o una ejecución de tests, nunca un handler sobre el árbol — y una nueva clase nativa, `needs-supplied-facts`, recoge las 31 reglas que necesitan una postura que solo los dueños del satélite pueden declarar. Las 73 reglas bloqueantes que no corren son las mismas 73, recosteadas. Snapshot, mapeo ISO 5055 y README de estándares rederivados; la línea base del guard 68 queda en `CLI-EXIT-01/03`. AC3–AC5 abiertos; contadores sin cambio: **688 / 715 completados · 3 en progreso · 3 pendientes · 21 diferidos**.) **Última Actualización:** 2026-09-20 (**Aterrizó el AC1 de GT-716: el motor OPA ya no llama veredicto a un hecho ausente.** `b2840947` — el bundle compila un segundo manifiesto, `rule_input_paths` (por id de regla, las rutas `input.…` que lee su política, desde el AST del compilador), y `OpaEvaluator` devuelve `skipped` / `supplied-facet-absent` nombrando la faceta cuando una ejecución no lleva nada de lo que una regla lee. Medido sobre un satélite recién salido de `init`: `--engine opa` pasó de **133 decididas a 10**. La fila había previsto ≤ 60 a partir de un conteo de 73; el conteo era corto, no el arreglo excesivo — 47 de las 57 reglas que ambos motores «decidían» eran también veredictos de OPA sobre input ausente (`DOD-*` sobre `context`, `TAX-*` sobre `repository`, `INH-02` sobre `contracts`). El guard 68 declaró obsoletos ocho conflictos de la línea base — los siete `supplied-facet-absent` y `TAX-01`, cuyas «listas de exención distintas» eran en realidad ninguna lista (`repository-taxonomy.rego` lee `input.repository.files`, que ningún canal transporta) —, quedando 3 desacuerdos reales sobre 14 reglas decididas por ambos. Suministrar la faceta sigue decidiendo: `MTN-01` falla con `applicationFiltering: false` y pasa con `true`, contra el bundle real. `GOV-ENGINE-COVERAGE` deja de decirle al lector que OPA «decide más». AC2–AC5 abiertos; contadores sin cambio: **688 / 715 completados · 3 en progreso · 3 pendientes · 21 diferidos**.) @@ -32,7 +33,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | ID | Gap | En simple | Qué resuelve | Componente | Fase | Criticidad | Complejidad | Estado | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| | [`GT-717`](./gap-reference-catalog.es.md#gt-717) | **Las dos vistas de GitHub Pages se publicaban a mano y llevaban tres meses envejeciendo; nada derivaba sus números del árbol.** Medido el 2026-09-19/20 contra `origin/gh-pages` (`2c8f487a`, tocado por última vez el 2026-07-07) y develop `6491c3bc`: el Atlas desplegado era anterior a la mudanza de taxonomía del 2026-07-04 — 18 de sus 21 enlaces a fuentes respondían 404 desde github.io porque `docHref` construía rutas `../` contra la raíz de Pages —, era monolingüe y tenía 4 escenarios sin explicaciones; el propio `architecture-map.json` del árbol nombraba 20 rutas de docs de las que 10 ya no existían. La vista maestra imprimía `8 controllers`, `26 tools · 9 resources`, `20 commands`, `5 KindEvaluators`, `PWA · Web · Mobile` + `BFF (NestJS · ADR-0075)` para un Tracker que es .NET 10 + React 19, y la cabecera `ADR-0101 · 0074 · 0075 · 0102`; su generador imprimía `12 hexagonal ports · 30 adapters` (línea 149) mientras el SVG versionado decía `11 of 20 ports · 53 adapters` bajo una anotación `` antepuesta a mano que el generador nunca emitió — frente a un árbol que mide 21 interfaces de puerto declaradas (20 ficheros) · 11 en el camino caliente (7 + 4) · 47 clases adaptador · 55 tools · 12 resources · 8 prompts · 38 ficheros de comando · 12 controladores · 33 endpoints · 12 kinds / 7 evaluadores. Ningún workflow referenciaba `gh-pages` (`git grep -l gh-pages 6491c3bc -- .github/workflows` = nada). **CERRADO 2026-09-20** en `a09a1fc7` + `7eac8445`: `.harness/scripts/pages/build-pages.mjs` deriva cada número impreso (`derive-page-metrics.mjs`; puertos y adaptadores por las funciones propias del guard 45), guarda los hechos de fuera del árbol en un `observed-facts.json` fechado, valida el modelo autorado, resuelve 129 `{{placeholders}}` distintos por idioma, rechaza cualquier `../` en el sitio construido, y `--check` más un self-test de 8 pruebas corren en el job requerido `Validate documentation`; el Atlas son 45 nodos · 62 aristas · 11 capítulos · 3 niveles de lectura · EN/ES (197 cadenas de UI), el tour del póster recorre los mismos `chapters[]` sobre regiones emitidas por el generador, y `pages.yml` proyecta `main` sobre `gh-pages` (acciones fijadas por SHA, permiso de escritura solo en el job de publicación). Observado en rojo antes de confiar: mergear develop (release 1.4.0) por debajo hizo fallar `--check` sobre el SVG versionado («generated files are stale»), y `--write-svg` movió las tarjetas de las puertas a 1.4.0 sin teclear un número. **La prosa se puso al día el mismo día en [#787](https://github.com/beyondnetcode/evolith_arch32/pull/787) → [#789](https://github.com/beyondnetcode/evolith_arch32/pull/789):** los conteos que las vistas tecleaban vivían también en `SECURITY.md`, `known-limitations`, la visión maestra, los READMEs de MCP / CLI / OPA / interfaces, el catálogo de tools y la línea base del scorecard (una integración de salida → dos; 47 tools → 55; 26 categorías → 21; 17 puertos → 21; 6 mutativas → 20; 8 contextos requeridos → 10), corregidos contra el árbol con un puntero al inventario generado allí donde un puntero basta; gitleaks se puso en rojo por el camino porque sus huellas llevan número de línea y las filas nuevas las movieron, y `gh-pages` no necesitó reconstruirse — su `metrics.json` ya derivaba los mismos números (una clave distinta: `commit`). | Las dos imágenes públicas del producto tenían tres meses, la mayoría de sus enlaces estaban muertos y cada número se había tecleado a mano. | Las dos páginas se construyen desde el repositorio en cada promoción a `main`, en los dos idiomas; un número que envejece pone en rojo el check requerido, no la imagen. | `Documentation` | Cross | P2 | L | `COMPLETADO` | -| [`GT-716`](./gap-reference-catalog.es.md#gt-716) | **Sobre un `evolith validate` a secas, la brecha de cobertura entre los dos motores es en su mayoría veredictos sobre facetas que nadie suministró, y nada en CI fija esa brecha en ninguna de las dos direcciones.** Medido el 2026-09-20 con la CLI construida desde este árbol (1.4.0, `b3df7e96`) sobre un satélite recién salido de `init`, 159 reglas en alcance: el nativo decide 56 y salta 103; `--engine opa` decide 133 y salta 26. Cruzado regla por regla, 76 reglas ejecutables las decide solo OPA, y el cuerpo de la política de **73** de ellas lee una faceta que una ejecución a secas nunca envía (`input.satellite.{git,runtime,testing,multiTenancy,ci,findings,protocol,scorecards,layers,contracts}`, `input.adapter`, `input.context.dod`, `input.user`, las métricas `QT-*`) — un hecho ausente leído como veredicto, la familia a la que pertenecen seis de los ocho conflictos de veredicto de la ejecución y que la línea base de GT-704 ya registra como `supplied-facet-absent`. Solo `MCP-05`, `OBS-EVD-01` y `OBS-EVD-02` se deciden desde el árbol: ese es el backlog real de handlers. En sentido contrario, el nativo decide 11 reglas para las que el bundle no declara política (`SSDF-*` ×7, `SEC-RL-01/02`, `QT-05`, `SLSA-HOSTED-L2`), 7 no las decide ninguno (`SEC-INJ/PATH/TIMING-*`, `SEC-RL-03`), y 12 que el nativo clasifica como no ejecutables (`KI-R01..07`, `INH-03..05`, `PROT-03/06`) OPA las decide igualmente — dos clasificaciones de la misma regla desde dos fuentes sin relación (`RULE_TRIAGE` y `declared_rule_ids`), que coinciden por construcción solo para `PEA-01..04`. Sobre el corpus completo el signo se invierte — nativo 247 / OPA 187 de 358, porque 138 reglas ADR-conformance y `MM-R*` tienen handler nativo y ningún `.rego` — y `68-validate-engine-verdict-parity.mjs` imprime ambas cifras como `coverageOnly` y no bloquea por ninguna. **AC1 cerrado el 2026-09-20 en `b2840947`:** el bundle enuncia ahora qué lee cada regla (`rule_input_paths`) y OPA salta una regla cuyo hecho la ejecución no suministró; medido sobre el mismo satélite, `--engine opa` pasó de 133 decididas a **10** — las 73 contadas aquí eran solo las reglas que el nativo saltaba; 47 de las decididas por ambos también eran veredictos sobre input ausente — y ocho conflictos de la línea base (los siete `supplied-facet-absent` más `TAX-01`, mal diagnosticado) quedaron obsoletos y se retiraron. **AC2 cerrado el 2026-09-20 en `3d76f4a6`:** cada regla declara `facts` y ambos motores derivan de ello (`classifyRule` desde la procedencia, el build del bundle rechazando lecturas no declaradas); la tabla de triaje ya no existe, 44 reglas cambiaron de clase sin implementar nada — el backlog de handlers es 21, no 52, y 31 reglas necesitan una postura declarada (`needs-supplied-facts`) — y las doce contradicciones están declaradas y son ejecutables. **AC3 cerrado el 2026-09-20 en `29c8a4ba`:** `73-validate-engine-coverage-parity.mjs` registra cada regla que solo un motor decide — por escenario (este repositorio, un satélite `init`), por dirección, con la razón del otro motor — en `engine-coverage-parity.baseline.json`; una regla sin registrar, una entrada obsoleta o una clase cambiada hacen fallar. | `--engine opa` parece comprobar más del doble que el motor por defecto, y sobre un repositorio nuevo casi todo ese extra son veredictos sobre hechos que nadie le dio; nada avisaría si la brecha creciera. | Un hecho ausente significa «no evaluado» en los dos motores; una sola declaración por regla alimenta la tabla de handlers, el manifiesto del bundle y el informe; y lo que un motor decide y el otro no es una línea base por regla que CI hace fallar, en ambas direcciones y en ambos escenarios. | `Core Domain` | Cross | P1 | L | `PENDIENTE` | +| [`GT-716`](./gap-reference-catalog.es.md#gt-716) | **Sobre un `evolith validate` a secas, la brecha de cobertura entre los dos motores es en su mayoría veredictos sobre facetas que nadie suministró, y nada en CI fija esa brecha en ninguna de las dos direcciones.** Medido el 2026-09-20 con la CLI construida desde este árbol (1.4.0, `b3df7e96`) sobre un satélite recién salido de `init`, 159 reglas en alcance: el nativo decide 56 y salta 103; `--engine opa` decide 133 y salta 26. Cruzado regla por regla, 76 reglas ejecutables las decide solo OPA, y el cuerpo de la política de **73** de ellas lee una faceta que una ejecución a secas nunca envía (`input.satellite.{git,runtime,testing,multiTenancy,ci,findings,protocol,scorecards,layers,contracts}`, `input.adapter`, `input.context.dod`, `input.user`, las métricas `QT-*`) — un hecho ausente leído como veredicto, la familia a la que pertenecen seis de los ocho conflictos de veredicto de la ejecución y que la línea base de GT-704 ya registra como `supplied-facet-absent`. Solo `MCP-05`, `OBS-EVD-01` y `OBS-EVD-02` se deciden desde el árbol: ese es el backlog real de handlers. En sentido contrario, el nativo decide 11 reglas para las que el bundle no declara política (`SSDF-*` ×7, `SEC-RL-01/02`, `QT-05`, `SLSA-HOSTED-L2`), 7 no las decide ninguno (`SEC-INJ/PATH/TIMING-*`, `SEC-RL-03`), y 12 que el nativo clasifica como no ejecutables (`KI-R01..07`, `INH-03..05`, `PROT-03/06`) OPA las decide igualmente — dos clasificaciones de la misma regla desde dos fuentes sin relación (`RULE_TRIAGE` y `declared_rule_ids`), que coinciden por construcción solo para `PEA-01..04`. Sobre el corpus completo el signo se invierte — nativo 247 / OPA 187 de 358, porque 138 reglas ADR-conformance y `MM-R*` tienen handler nativo y ningún `.rego` — y `68-validate-engine-verdict-parity.mjs` imprime ambas cifras como `coverageOnly` y no bloquea por ninguna. **AC1 cerrado el 2026-09-20 en `b2840947`:** el bundle enuncia ahora qué lee cada regla (`rule_input_paths`) y OPA salta una regla cuyo hecho la ejecución no suministró; medido sobre el mismo satélite, `--engine opa` pasó de 133 decididas a **10** — las 73 contadas aquí eran solo las reglas que el nativo saltaba; 47 de las decididas por ambos también eran veredictos sobre input ausente — y ocho conflictos de la línea base (los siete `supplied-facet-absent` más `TAX-01`, mal diagnosticado) quedaron obsoletos y se retiraron. **AC2 cerrado el 2026-09-20 en `3d76f4a6`:** cada regla declara `facts` y ambos motores derivan de ello (`classifyRule` desde la procedencia, el build del bundle rechazando lecturas no declaradas); la tabla de triaje ya no existe, 44 reglas cambiaron de clase sin implementar nada — el backlog de handlers es 21, no 52, y 31 reglas necesitan una postura declarada (`needs-supplied-facts`) — y las doce contradicciones están declaradas y son ejecutables. **AC3 cerrado el 2026-09-20 en `29c8a4ba`:** `73-validate-engine-coverage-parity.mjs` registra cada regla que solo un motor decide — por escenario (este repositorio, un satélite `init`), por dirección, con la razón del otro motor — en `engine-coverage-parity.baseline.json`; una regla sin registrar, una entrada obsoleta o una clase cambiada hacen fallar. **AC4 cerrado el 2026-09-21 en `ea736a75`:** el lado solo-OPA está vacío en ambos escenarios — `OBS-EVD-01..03` y `MCP-05` tienen gemelos nativos de sus políticas, `MCP-01..03` fallan por evidencia ausente en los dos motores — y cada entrada de deuda que queda lleva una decisión registrada (`engine-coverage-decisions.json`) que el guard 73 contrasta con las ejecuciones, haciendo fallar una entrada sin ella como solo-un-motor por omisión; las siete reglas de seguridad sin decidir y `QT-05` quedan declaradas (hallazgos de un escáner, una ejecución de tests) en vez de respondidas. | `--engine opa` parece comprobar más del doble que el motor por defecto, y sobre un repositorio nuevo casi todo ese extra son veredictos sobre hechos que nadie le dio; nada avisaría si la brecha creciera. | Un hecho ausente significa «no evaluado» en los dos motores; una sola declaración por regla alimenta la tabla de handlers, el manifiesto del bundle y el informe; y lo que un motor decide y el otro no es una línea base por regla que CI hace fallar, en ambas direcciones y en ambos escenarios. | `Core Domain` | Cross | P1 | L | `PENDIENTE` | | [`GT-715`](./gap-reference-catalog.es.md#gt-715) | **La Core API rechazaba cualquier contexto de evaluación inline de más de 100 KB con un 500 enmascarado y sin línea de log, así que la llamada de conformidad del repositorio del Tracker nunca funcionó contra un repositorio real.** Medido el 2026-09-20 desde el entorno UAT del Tracker: `POST /products/{id}/evaluate-architecture` sobre un producto que apunta a este repositorio leyó 150 ficheros (~1 MB) de GitHub y se los envió al Core, que respondió `500 INTERNAL_ERROR "An unexpected error occurred"`; el Tracker registró un `synthetic BLOCKED`. Reproducido sobre la misma imagen (`main@142b8324`): 14 ficheros / cuerpo de 99.797 bytes → `200`, 15 ficheros / 101.578 bytes → `500`; un cuerpo sintético de 92.956 bytes → `200`, de 126.556 → `500`. El límite de 100 KB por omisión de Express para json, lanzado como un `PayloadTooLargeError` que no es `HttpException`, clasificado por mensaje, enmascarado para el cable y escrito en ninguna parte. **CERRADO 2026-09-20** en `3b276c9a`: `EVOLITH_MAX_BODY_BYTES` (2 MiB por defecto) registra los parsers explícitamente, los errores del body parser conservan el estado del parser y el 413 nombra los dos tamaños y la variable, y todo 5xx enmascarado se registra con su traza. Rojo primero: 4 de las 5 specs nuevas fallan sobre el filtro viejo; después: el mismo cuerpo de 1,1 MB → `200` con el veredicto de `gate-f1`, 3 MB → `413`. Promovido en #778 (`9c5deedf`) y redesplegado por el job `Deploy UAT (Coolify)` de la corrida 35490911533 el 2026-09-20; medido justo después: la misma llamada `evaluate-architecture` responde `200`, `provenance: core`, `status: COMPLETED`, `resultDecision: FAILED` — un veredicto real sobre 150 ficheros (gates f1–f5 fallidos por artefactos de fase ausentes), 174 ms en el Core. La portada conserva la captura de la compuerta de fase. | El Tracker no podía obtener un veredicto de arquitectura sobre ningún repositorio real: el Core rechazaba la petición por su tamaño y no le decía nada útil a nadie. | El Core evalúa un repositorio real enviado inline, rechaza con una razón que nombra cuando debe, y deja una traza que el operador puede leer. | `Core API` | Cross | P1 | S | `COMPLETADO` | | [`GT-714`](./gap-reference-catalog.es.md#gt-714) | **`gate evaluate` y `phase advance` en la CLI publicada necesitan un checkout de este repositorio en disco, porque el tarball trae las reglas pero no las definiciones de gate.** Medido el 2026-09-20 con `@beyondnet/evolith-cli@1.3.2` en un contenedor `node:20` limpio sobre un satélite recién salido de `init`: sin `--core` las dos órdenes salen con `1` y `ENOENT … reference/governance/sdlc/gates` (el tarball trae `rulesets/sdlc/phase-gates.rules.json` y el registro de artefactos en las rutas propias del paquete, pero el validador compone `/reference/governance/sdlc/gates` y `/src/rulesets/sdlc/artifact-registry.json`, y `findCorePath` cae en el propio satélite); con `--core ../evolith` las dos salen con `2` y el veredicto real del gate. GT-705 arregló el mismo defecto para el paquete MCP empaquetando los dos árboles e instalando un único resolutor; el paquete de la CLI no entró en ese cambio, y `sdlc gate-status` (sub-hallazgo de GT-461) no tiene `--core` en absoluto. Hasta que aterrice, la portada muestra las dos órdenes con `--core ../evolith` y lo explica en el pie. | Las dos órdenes que hacen de Evolith algo más que un linter no corren desde el paquete publicado sin un clon de este repositorio junto al proyecto. | `npx -y @beyondnet/evolith-cli gate evaluate --phase discovery` sobre un satélite nuevo da el veredicto, sin opción, y la portada pierde su salvedad. | `Evolith CLI` | Cross | P1 | S | `PENDIENTE` | | [`GT-713`](./gap-reference-catalog.es.md#gt-713) | **El análisis al que están ligadas las alertas de la pestaña Security solo lo produce la corrida `push` de `sdk-cli-ci.yml`, y su filtro de rutas saltaba casi todo el código.** El job `CodeQL SAST` es el único que sube el análisis `/language:javascript-typescript` para `refs/heads/main`; las corridas de pull request son diff-informed (se recortan al diff y nunca mueven las alertas de la rama) y la configuración por defecto "Code Quality" es otra suite. El trigger `push` estaba filtrado a `src/sdk/cli/**`, `.harness/**` y los lockfiles. Medido el 2026-09-19: la promoción `19d736da` (cambios solo bajo `src/packages` y `src/apps`) llegó a `main` sin análisis alguno, así que la pestaña siguió mostrando 10 alertas sobre código que ya no existía; `c5547114` hizo lo mismo 40 minutos después. Las dos necesitaron `gh workflow run sdk-cli-ci.yml --ref main` a mano. **CERRADO 2026-09-19** en `72aceb70`: el filtro cubre ahora `src/packages/**` y `src/apps/**` — todo lo que CodeQL escanea — y un push solo de documentación sigue sin gastar la corrida. | El escáner que decide qué muestra la pestaña Security no volvía a correr cuando cambiaba la mayor parte del código, así que la pestaña describía el commit anterior. | Una promoción de código a `main` re-analiza `main`; la pestaña está al día sin que nadie tenga que acordarse de lanzarla. | `Infra` | Cross | P2 | XS | `COMPLETADO` | diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 8a07b0345..84e36cba6 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -4,6 +4,7 @@ **Status:** Active Tracking **Owner:** Evolith Architecture Board +**Last Updated:** 2026-09-21 (**GT-716 AC4 landed: nothing in the coverage register is there by omission.** `ea736a75` — the named backlog is implemented or declared, and `73-validate-engine-coverage-parity.mjs` now refuses a debt-class entry with no recorded decision. Implemented: `OBS-EVD-01..03` decided natively from the satellite's dependencies (the same proxy their policy reads), `MCP-05` from the server source, `MCP-01..03` failing on absent smoke evidence on both engines instead of one — the OPA-only side of the baseline is **empty on both scenarios** (was 7 / 7). Declared: the seven rules neither engine decided (`SEC-INJ/PATH/TIMING-01/02`, `SEC-RL-03`) name `satellite.findings`, a scanner's finding over the AST rather than a regex over the tree (`MM-R10`); `QT-05`, which the native engine answered `passed` with "requires runtime analysis", names `satellite.testing` and is decided by neither. Recorded: `engine-coverage-decisions.json`, eight decisions (`SSDF`, `SLSA`, `SEC-RL-01/02`, `MM-R*`, the enforcer-routed `HXA` clauses as `native-only`; the 138 generated ADR-conformance rules, the seven scanner rules and `QT-05` as `neither`), each held to the runs — the guard's first run went red on `MM-R03`, which both engines decide and the register had claimed, and green once it left. The 11 no-policy rules got decisions with reasons, not `.rego` twins. Handler backlog 21 → 14; blocking rules that do not run 73 → 71; guard 68 gained one registered conflict (`GOV-RULE-NON-EXECUTABLE`, a corpus-resolution artifact of its `--core`-less run, AC5's to remove). AC5 open; counters unchanged: **688 / 715 done · 3 in progress · 3 pending · 21 deferred**.) **Last Updated:** 2026-09-20 (**GT-716 AC3 landed: what only one engine decides is a registered difference, not a free one.** `29c8a4ba` — `73-validate-engine-coverage-parity.mjs` runs both engines on this repository and on a satellite it creates with `evolith init`, and holds every coverage-only rule to a baseline per scenario, per direction, per rule, with the reason the other engine gave (the class its report states; the facets the policy reads that a bare run does not supply). An unregistered rule, a stale entry or a changed class fails. Both scenarios read the Core from an export of the tracked tree, because the first CI run disagreed with the laptop that wrote the baseline (a local `coverage/` directory, git history and the CLI's bundled corpus copy had decided six rules and failed 138 ADR-conformance rules falsely). Measured on that export: repository 82 native-only / 7 opa-only, init satellite 49 / 7 — 52 of the 82 are policies reading facets nobody supplied, 26 have no policy at all, and four are the enforcer-routed `HXA-01/02/04/05`, where the OPA path skips without a reason of its own. The criterion's probe (drop an import from `main.rego`) cannot run since GT-675 — the build refuses it — so the equivalent one was recorded: rename `OBS-EVD-03` in its policy and the guard goes red on both scenarios naming the id. AC4–AC5 open; counters unchanged: **688 / 715 done · 3 in progress · 3 pending · 21 deferred**.) **Last Updated:** 2026-09-20 (**GT-716 AC2 landed: one evaluability declaration per rule, and both engines derive from it.** `3d76f4a6` — every rule in `*.rules.json` declares `facts`, the facets its check reads, from a vocabulary that says where the truth of each one lives (`observed` / `supplied` / `external` / `runtime`); `classifyRule` derives the native class from the most demanding provenance and the triage table keyed by rule id is gone; `npm run build:policy` refuses a bundle whose policy reads a facet its rule did not declare. Declaring `facts: []` for the twelve rules the table called non-executable while Rego decided them turned that check red with twelve findings; declaring what the policies read turned it green. **What moved, with nothing implemented: 44 of 415 rules changed class.** The "handler backlog" is 21, not 52 — 25 of those rows were decided by policies over a declared posture, the CI system or a test run, never by a handler over the tree — and a new native class, `needs-supplied-facts`, holds the 31 rules that need a posture only the satellite's owners can declare. The 73 blocking rules that do not run are the same 73, re-costed. Snapshot, ISO 5055 mapping and the standards README re-derived; guard 68's baseline down to `CLI-EXIT-01/03`. AC3–AC5 open; counters unchanged: **688 / 715 done · 3 in progress · 3 pending · 21 deferred**.) **Last Updated:** 2026-09-20 (**GT-716 AC1 landed: the OPA engine no longer calls an absent fact a verdict.** `b2840947` — the bundle compiles a second manifest, `rule_input_paths` (per rule id, the `input.…` paths its policy reads, from the compiler's AST), and `OpaEvaluator` reports `skipped` / `supplied-facet-absent` naming the facet when a run carries none of what a rule reads. Measured on a satellite fresh from `init`: `--engine opa` went from **133 decided to 10**. The row had predicted ≤ 60 from a count of 73; the count was too small, not the fix too large — 47 of the 57 rules both engines "decided" were OPA verdicts on absent input as well (`DOD-*` on `context`, `TAX-*` on `repository`, `INH-02` on `contracts`). Guard 68 declared eight baseline conflicts stale — the seven `supplied-facet-absent` and `TAX-01`, whose "differing exemption lists" were in fact no list at all (`repository-taxonomy.rego` reads `input.repository.files`, which no channel carries) — leaving 3 real disagreements over 14 jointly-decided rules. Supplying the facet still decides: `MTN-01` fails on `applicationFiltering: false` and passes on `true`, against the real bundle. `GOV-ENGINE-COVERAGE` stops telling the reader OPA "decides more". AC2–AC5 open; counters unchanged: **688 / 715 done · 3 in progress · 3 pending · 21 deferred**.) @@ -32,7 +33,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | ID | Gap | In plain terms | What it fixes | Component | Phase | Criticality | Complexity | Status | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| | [`GT-717`](./gap-reference-catalog.md#gt-717) | **The two GitHub Pages views were published by hand and had aged three months; nothing derived their numbers from the tree.** Measured 2026-09-19/20 against `origin/gh-pages` (`2c8f487a`, last touched 2026-07-07) and develop `6491c3bc`: the deployed Atlas predated the 2026-07-04 taxonomy move — 18 of its 21 source links answered 404 from github.io because `docHref` built `../` paths against the Pages root — was monolingual and had 4 scenarios with no explanations; the tree's own `architecture-map.json` named 20 docs paths of which 10 no longer existed. The master view printed `8 controllers`, `26 tools · 9 resources`, `20 commands`, `5 KindEvaluators`, `PWA · Web · Mobile` + `BFF (NestJS · ADR-0075)` for a Tracker that is .NET 10 + React 19, and the header `ADR-0101 · 0074 · 0075 · 0102`; its generator printed `12 hexagonal ports · 30 adapters` (line 149) while the tracked SVG said `11 of 20 ports · 53 adapters` under a hand-prepended `` annotation the generator never emitted — against a tree measuring 21 declared port interfaces (20 files) · 11 on the hot path (7 + 4) · 47 adapter classes · 55 tools · 12 resources · 8 prompts · 38 command files · 12 controllers · 33 endpoints · 12 kinds / 7 evaluators. No workflow referenced `gh-pages` (`git grep -l gh-pages 6491c3bc -- .github/workflows` = nothing). **CLOSED 2026-09-20** in `a09a1fc7` + `7eac8445`: `.harness/scripts/pages/build-pages.mjs` derives every printed number (`derive-page-metrics.mjs`; ports and adapters through guard 45's own functions), keeps out-of-tree facts in a dated `observed-facts.json`, validates the authored model, resolves 129 distinct `{{placeholders}}` per language, refuses any `../` in the built site, and `--check` plus an 8-test self-test run in the required `Validate documentation` job; the Atlas is 45 nodes · 62 edges · 11 chapters · 3 reading levels · EN/ES (197 UI strings), the poster's tour rides the same `chapters[]` on generator-emitted regions, and `pages.yml` projects `main` onto `gh-pages` (SHA-pinned, write permission only on the publish job). Observed red before trusted: merging develop (release 1.4.0) underneath made `--check` fail on the tracked SVG ("generated files are stale"), and `--write-svg` moved the door cards to 1.4.0 with no number typed. **The prose caught up the same day in [#787](https://github.com/beyondnetcode/evolith_arch32/pull/787) → [#789](https://github.com/beyondnetcode/evolith_arch32/pull/789):** the counts the views had typed also lived in `SECURITY.md`, `known-limitations`, the vision master, the MCP / CLI / OPA / interfaces READMEs, the tools catalog and the scorecard baseline (one outbound integration → two; 47 tools → 55; 26 categories → 21; 17 ports → 21; 6 mutative → 20; 8 required contexts → 10), corrected against the tree with a pointer to the generated inventory wherever a pointer suffices; gitleaks went red on the way because its fingerprints carry line numbers and the new rows moved them, and `gh-pages` needed no rebuild — its `metrics.json` already derived the same numbers (one differing key: `commit`). | The two public pictures of the product were three months old, most of their links were dead, and every number on them had been typed by hand. | Both pages are built from the repository on every promotion to `main`, in both languages; a number that ages turns the required check red instead of the picture. | `Documentation` | Cross | P2 | L | `DONE` | -| [`GT-716`](./gap-reference-catalog.md#gt-716) | **On a bare `evolith validate` the coverage gap between the two engines is mostly verdicts on facets nobody supplied, and nothing in CI pins the gap in either direction.** Measured 2026-09-20 with the CLI built from this tree (1.4.0, `b3df7e96`) on a satellite fresh from `init`, 159 rules in scope: native decides 56 and skips 103; `--engine opa` decides 133 and skips 26. Crossed rule by rule, 76 executable rules are decided by OPA alone, and the policy body of **73** of them reads a facet a bare run never sends (`input.satellite.{git,runtime,testing,multiTenancy,ci,findings,protocol,scorecards,layers,contracts}`, `input.adapter`, `input.context.dod`, `input.user`, the `QT-*` metrics) — an absent fact read as a verdict, the family six of the run's eight verdict conflicts belong to and GT-704's baseline already carries as `supplied-facet-absent`. Only `MCP-05`, `OBS-EVD-01` and `OBS-EVD-02` are decided from the tree: that is the real handler backlog. The other way, native decides 11 rules the bundle declares no policy for (`SSDF-*` ×7, `SEC-RL-01/02`, `QT-05`, `SLSA-HOSTED-L2`), 7 are decided by neither (`SEC-INJ/PATH/TIMING-*`, `SEC-RL-03`), and 12 that native classes non-executable (`KI-R01..07`, `INH-03..05`, `PROT-03/06`) OPA decides anyway — two classifications of the same rule from two unrelated sources (`RULE_TRIAGE` and `declared_rule_ids`), agreeing by construction only for `PEA-01..04`. Over the whole corpus the sign flips — native 247 / OPA 187 of 358, because 138 ADR-conformance rules and `MM-R*` have a native handler and no `.rego` — and `68-validate-engine-verdict-parity.mjs` prints both figures as `coverageOnly` and gates neither. **AC1 closed 2026-09-20 in `b2840947`:** the bundle now states what each rule reads (`rule_input_paths`) and OPA skips a rule whose fact the run did not supply; measured on the same satellite, `--engine opa` went from 133 decided to **10** — the 73 counted here were only the rules native skipped; 47 of the jointly-decided ones were absent-input verdicts too — and eight baseline conflicts (the seven `supplied-facet-absent` plus `TAX-01`, misdiagnosed) went stale and were removed. **AC2 closed 2026-09-20 in `3d76f4a6`:** every rule declares `facts` and both engines derive from it (`classifyRule` from the provenance, the bundle build refusing undeclared reads); the triage table is gone, 44 rules changed class with nothing implemented — the handler backlog is 21, not 52, and 31 rules need a declared posture (`needs-supplied-facts`) — and the twelve contradictions are declared and executable. **AC3 closed 2026-09-20 in `29c8a4ba`:** `73-validate-engine-coverage-parity.mjs` registers every rule only one engine decides — per scenario (this repository, an `init` satellite), per direction, with the other engine's reason — in `engine-coverage-parity.baseline.json`; an unregistered rule, a stale entry or a changed class fails. | `--engine opa` looks like it checks more than twice what the default does, and on a fresh repository almost all of the extra is verdicts on facts nobody gave it; nothing would notice the gap growing. | An absent fact means "not evaluated" on both engines; one declaration per rule feeds the handler table, the bundle manifest and the report; and what one engine decides and the other does not is a per-rule baseline CI fails on, in both directions, on both scenarios. | `Core Domain` | Cross | P1 | L | `PENDING` | +| [`GT-716`](./gap-reference-catalog.md#gt-716) | **On a bare `evolith validate` the coverage gap between the two engines is mostly verdicts on facets nobody supplied, and nothing in CI pins the gap in either direction.** Measured 2026-09-20 with the CLI built from this tree (1.4.0, `b3df7e96`) on a satellite fresh from `init`, 159 rules in scope: native decides 56 and skips 103; `--engine opa` decides 133 and skips 26. Crossed rule by rule, 76 executable rules are decided by OPA alone, and the policy body of **73** of them reads a facet a bare run never sends (`input.satellite.{git,runtime,testing,multiTenancy,ci,findings,protocol,scorecards,layers,contracts}`, `input.adapter`, `input.context.dod`, `input.user`, the `QT-*` metrics) — an absent fact read as a verdict, the family six of the run's eight verdict conflicts belong to and GT-704's baseline already carries as `supplied-facet-absent`. Only `MCP-05`, `OBS-EVD-01` and `OBS-EVD-02` are decided from the tree: that is the real handler backlog. The other way, native decides 11 rules the bundle declares no policy for (`SSDF-*` ×7, `SEC-RL-01/02`, `QT-05`, `SLSA-HOSTED-L2`), 7 are decided by neither (`SEC-INJ/PATH/TIMING-*`, `SEC-RL-03`), and 12 that native classes non-executable (`KI-R01..07`, `INH-03..05`, `PROT-03/06`) OPA decides anyway — two classifications of the same rule from two unrelated sources (`RULE_TRIAGE` and `declared_rule_ids`), agreeing by construction only for `PEA-01..04`. Over the whole corpus the sign flips — native 247 / OPA 187 of 358, because 138 ADR-conformance rules and `MM-R*` have a native handler and no `.rego` — and `68-validate-engine-verdict-parity.mjs` prints both figures as `coverageOnly` and gates neither. **AC1 closed 2026-09-20 in `b2840947`:** the bundle now states what each rule reads (`rule_input_paths`) and OPA skips a rule whose fact the run did not supply; measured on the same satellite, `--engine opa` went from 133 decided to **10** — the 73 counted here were only the rules native skipped; 47 of the jointly-decided ones were absent-input verdicts too — and eight baseline conflicts (the seven `supplied-facet-absent` plus `TAX-01`, misdiagnosed) went stale and were removed. **AC2 closed 2026-09-20 in `3d76f4a6`:** every rule declares `facts` and both engines derive from it (`classifyRule` from the provenance, the bundle build refusing undeclared reads); the triage table is gone, 44 rules changed class with nothing implemented — the handler backlog is 21, not 52, and 31 rules need a declared posture (`needs-supplied-facts`) — and the twelve contradictions are declared and executable. **AC3 closed 2026-09-20 in `29c8a4ba`:** `73-validate-engine-coverage-parity.mjs` registers every rule only one engine decides — per scenario (this repository, an `init` satellite), per direction, with the other engine's reason — in `engine-coverage-parity.baseline.json`; an unregistered rule, a stale entry or a changed class fails. **AC4 closed 2026-09-21 in `ea736a75`:** the OPA-only side is empty on both scenarios — `OBS-EVD-01..03` and `MCP-05` have native twins of their policies, `MCP-01..03` fail on absent evidence on both engines — and every remaining debt entry carries a recorded decision (`engine-coverage-decisions.json`) guard 73 holds to the runs, failing an entry without one as coverage-only by omission; the seven undecided security rules and `QT-05` are declared (a scanner's findings, a test run) rather than answered. | `--engine opa` looks like it checks more than twice what the default does, and on a fresh repository almost all of the extra is verdicts on facts nobody gave it; nothing would notice the gap growing. | An absent fact means "not evaluated" on both engines; one declaration per rule feeds the handler table, the bundle manifest and the report; and what one engine decides and the other does not is a per-rule baseline CI fails on, in both directions, on both scenarios. | `Core Domain` | Cross | P1 | L | `PENDING` | | [`GT-715`](./gap-reference-catalog.md#gt-715) | **The Core API rejected any inline evaluation context over 100 KB with a masked 500 and no log line, so the Tracker's repository-conformance call never worked against a real repository.** Measured 2026-09-20 from the Tracker's UAT environment: `POST /products/{id}/evaluate-architecture` on a product pointing at this repository read 150 files (~1 MB) from GitHub and posted them to the Core, which answered `500 INTERNAL_ERROR "An unexpected error occurred"`; the Tracker recorded a `synthetic BLOCKED`. Reproduced on the same image (`main@142b8324`): 14 files / 99,797-byte body → `200`, 15 files / 101,578 bytes → `500`; a synthetic 92,956-byte body → `200`, 126,556 → `500`. Express's 100 KB json default, thrown as a `PayloadTooLargeError` that is not an `HttpException`, classified by message, masked for the wire and written nowhere. **CLOSED 2026-09-20** in `3b276c9a`: `EVOLITH_MAX_BODY_BYTES` (2 MiB by default) registers the parsers explicitly, body-parser errors keep the parser's status and the 413 names both sizes and the variable, and every masked 5xx is logged with its stack. Red first: 4 of 5 new specs fail on the old filter; after: the same 1.1 MB payload → `200` with the `gate-f1` verdict, 3 MB → `413`. Promoted in #778 (`9c5deedf`) and redeployed by the `Deploy UAT (Coolify)` job of run 35490911533 on 2026-09-20; measured right after: the same `evaluate-architecture` call answers `200`, `provenance: core`, `status: COMPLETED`, `resultDecision: FAILED` — a real verdict on 150 files (gates f1–f5 failed for missing phase artifacts), 174 ms in the Core. The front page keeps the phase-gate capture. | The Tracker could not get an architecture verdict on any real repository: the Core refused the request for its size and said nothing useful to anyone. | The Core evaluates a real repository sent inline, refuses with a reason it names when it must, and leaves a trace the operator can read. | `Core API` | Cross | P1 | S | `DONE` | | [`GT-714`](./gap-reference-catalog.md#gt-714) | **`gate evaluate` and `phase advance` in the published CLI need a checkout of this repository on disk, because the tarball carries the rules but not the gate definitions.** Measured 2026-09-20 with `@beyondnet/evolith-cli@1.3.2` in a clean `node:20` container on a satellite fresh from `init`: without `--core` both commands exit `1` with `ENOENT … reference/governance/sdlc/gates` (the tarball ships `rulesets/sdlc/phase-gates.rules.json` and the artifact registry at the package's own paths, but the validator composes `/reference/governance/sdlc/gates` and `/src/rulesets/sdlc/artifact-registry.json`, and `findCorePath` falls back to the satellite itself); with `--core ../evolith` both exit `2` with the real gate verdict. GT-705 fixed the same defect for the MCP package by bundling both trees and installing one resolver; the CLI package was not part of that change, and `sdlc gate-status` (GT-461 sub-finding) has no `--core` at all. Until it lands, the front page shows the two commands with `--core ../evolith` and says why in the caption. | The two commands that make Evolith more than a linter do not run from the published package without a clone of this repository next to the project. | `npx -y @beyondnet/evolith-cli gate evaluate --phase discovery` on a fresh satellite gives the verdict, no flag, and the front page loses its caveat. | `Evolith CLI` | Cross | P1 | S | `PENDING` | | [`GT-713`](./gap-reference-catalog.md#gt-713) | **The analysis that the Security tab's alerts are keyed to is produced only by the `push` run of `sdk-cli-ci.yml`, and its path filter skipped most of the code.** The `CodeQL SAST` job is the sole uploader of the `/language:javascript-typescript` analysis for `refs/heads/main`; pull-request runs are diff-informed (they prune to the diff and never move the branch's alerts) and the default "Code Quality" setup is a different suite. The `push` trigger was filtered to `src/sdk/cli/**`, `.harness/**` and the lockfiles. Measured 2026-09-19: promotion `19d736da` (changes under `src/packages` and `src/apps` only) reached `main` with no analysis at all, so the tab kept reporting 10 alerts on code that no longer existed; `c5547114` did the same 40 minutes later. Both needed `gh workflow run sdk-cli-ci.yml --ref main` by hand. **CLOSED 2026-09-19** in `72aceb70`: the filter now spans `src/packages/**` and `src/apps/**` — every tree CodeQL scans — while a docs-only push still skips the run. | The scanner that decides what the Security tab shows was not re-run when most of the code changed, so the tab described the previous commit. | A code promotion to `main` re-analyses `main`; the tab is current without anyone remembering to dispatch it. | `Infra` | Cross | P2 | XS | `DONE` | 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 5d8de532d..3072f9c22 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -41,7 +41,7 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| -| Fecha canónica del tablero | 2026-09-20 | +| Fecha canónica del tablero | 2026-09-21 | | Gaps totales | 715 | | Gaps cerrados | 688 | | Gaps pendientes | 27 | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 6a6b7d26f..7ad9c0fab 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -41,7 +41,7 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| -| Canonical board date | 2026-09-20 | +| Canonical board date | 2026-09-21 | | Total gaps | 715 | | Closed gaps | 688 | | Open gaps | 27 | diff --git a/reference/core/control-center/maturity-reports/maturity-evidence.json b/reference/core/control-center/maturity-reports/maturity-evidence.json index 4b3a09e87..7a59d330b 100644 --- a/reference/core/control-center/maturity-reports/maturity-evidence.json +++ b/reference/core/control-center/maturity-reports/maturity-evidence.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0.0", - "asOf": "2026-09-20", + "asOf": "2026-09-21", "checks": [ { "id": "cli-baseline", diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index 76d780920..9e29a4e41 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -1,7 +1,7 @@ { "schemaVersion": "1.0.0", "scope": "evolith-core", - "asOf": "2026-09-20", + "asOf": "2026-09-21", "gaps": { "total": 715, "done": 688, diff --git a/reference/core/sdlc/assets/master-view.svg b/reference/core/sdlc/assets/master-view.svg index 3100ee849..fb3d8067f 100644 --- a/reference/core/sdlc/assets/master-view.svg +++ b/reference/core/sdlc/assets/master-view.svg @@ -2,7 +2,7 @@ -Evolith — E2E Product Vision (numbers as of 2026-09-20) +Evolith — E2E Product Vision (numbers as of 2026-09-21) Band 1, four doors: the CLI, the GitHub Action, the MCP server and the REST Core API apply the same rules and return the same envelope. Band 2, the Core: a stateless evaluation engine that turns an EvaluationContext into an EvaluationResult, beside the Constitution of 144 ADRs, 184 rule packs and 50 schemas, and the strip of who can say yes. Band 3, Evolith Tracker: the commercial control plane that decides gates and owns state, with the five SDLC phases and their gates and the topology axis beside them. Band 4, governed agents: the experimental agent runtime with 21 declared port interfaces of which 11 are on the hot path, the LLM providers a tenant chooses, and the governed-composition rail of ports, adapters and anti-corruption layers. Band 5, federated governance: the Core as level 0, satellites as level 1 inheritors, and the Architecture Board approving upstream proposals. Band 6, evidence: eight generated, dated and linked tiles on how honest this picture is. @@ -17,7 +17,7 @@ EVOLITH · E2E PRODUCT VISION Governed Composition · Stateless Evaluation Core · Federated Five-Phase SDLC -repo · evolith_arch32 · numbers as of 2026-09-20 +repo · evolith_arch32 · numbers as of 2026-09-21 ADR-0101 · ADR-0074 · ADR-0102 · ADR-0116 · ADR-0125 · ADR-0128 · ADR-0129 @@ -108,7 +108,7 @@ never persists tenant · product · initiative -Native TS · 18 handlers +Native TS · 19 handlers handler throw → errored OPA → Wasm · 35 policies @@ -432,7 +432,7 @@ Gap board 688 / 715 · 27 not done P0 GT-435 · NO-GO -generated 2026-09-20 +generated 2026-09-21 10 required checks · 18 workflows 74 guards + 32 self-tests @@ -448,7 +448,7 @@ Tests 107 suites · 1,492 tests 87.33% statements · 87.68% lines -(2026-09-20) +(2026-09-21) Engines on this repo: opa 133/159 native 41/159 @@ -463,7 +463,7 @@ 7/8 packages attested (2026-07-30) -Source of truth: product/suite/vision/evolith-product-vision-master.md · counts from generated inventories (inventory-summary.md 2026-09-01 · product-inventory.md · maturity-reconciliation.json 2026-09-20) -and tree measurements · rendered 2026-09-20 +Source of truth: product/suite/vision/evolith-product-vision-master.md · counts from generated inventories (inventory-summary.md 2026-09-01 · product-inventory.md · maturity-reconciliation.json 2026-09-21) +and tree measurements · rendered 2026-09-21 diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.spec.ts index 026e1421b..ef4c185a6 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.spec.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.spec.ts @@ -25,9 +25,13 @@ describe('McpRuleHandler', () => { expect(new McpRuleHandler(fsMock()).canHandle(rule('DEP-01'))).toBe(false); }); - it('skips when no mcp smoke evidence exists', async () => { + it('FAILS when no mcp smoke evidence exists — the same verdict and words as mcp.rego (GT-716 AC4)', async () => { const h = new McpRuleHandler(fsMock({ existing: [evDir], dirs: { [evDir]: ['other.json'] } })); - expect((await h.evaluate(rule('MCP-01'), ctx)).result).toBe('skipped'); + for (const id of ['MCP-01', 'MCP-02', 'MCP-03']) { + const r = await h.evaluate(rule(id), ctx); + expect(r.result).toBe('failed'); + expect(r.message).toBe('Run .harness/scripts/mcp-smoke.mjs to generate evidence'); + } }); it('MCP-01 fails when initialize is missing and passes when present', async () => { @@ -63,6 +67,18 @@ describe('McpRuleHandler', () => { expect((await fail.evaluate(rule('MCP-04'), ctx)).result).toBe('failed'); }); + it('MCP-05 reads the server source for the tokens mcp.rego looks for (GT-716 AC4)', async () => { + const server = path.join(CORE, 'src', 'packages', 'mcp-server', 'src', 'mcp', 'mcp-server.service.ts'); + expect((await new McpRuleHandler(fsMock()).evaluate(rule('MCP-05'), ctx)).result).toBe('skipped'); + + const pass = new McpRuleHandler(fsMock({ existing: [server], files: { [server]: 'const h = new Histogram(); // latency' } })); + expect((await pass.evaluate(rule('MCP-05'), ctx)).result).toBe('passed'); + + const fail = await new McpRuleHandler(fsMock({ existing: [server], files: { [server]: 'const open = true' } })).evaluate(rule('MCP-05'), ctx); + expect(fail.result).toBe('failed'); + expect(fail.message).toMatch(/no metrics instrumentation detected/); + }); + it('skips unhandled MCP rules', async () => { expect((await new McpRuleHandler(fsMock()).evaluate(rule('MCP-99'), ctx)).result).toBe('skipped'); }); diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.ts index 64a3b61e5..3cf7534d6 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/mcp-rule.handler.ts @@ -4,6 +4,17 @@ import { NormalizedRule } from '../../../../domain/models/normalized-rule'; import { WorkspaceEvaluationContext, RuleEvaluationResult } from '../evaluator.interface'; import { INativeRuleHandler } from './rule-handler.interface'; +/** + * The native twin of `mcp.rego`. GT-716 AC4 aligned the two where they disagreed: + * + * - MCP-01..03 with no smoke evidence under `/.harness/evidence/` used to be + * `skipped` here and `failed` in Rego. The evidence is an observed fact of the + * checkout (`core.evidence`), and its absence is the policy's finding — "nothing + * proves the server answers `initialize`" — so both engines now fail it, with the + * same message telling the reader how to produce the evidence. + * - MCP-05 was "Unhandled" here while Rego decided it from the server source; the + * same token check now runs natively over the same file. + */ export class McpRuleHandler implements INativeRuleHandler { constructor(private readonly fs: IFileSystem) {} @@ -18,6 +29,9 @@ export class McpRuleHandler implements INativeRuleHandler { if (rule.id === 'MCP-04') { return this.evalMcpSecurity(rule, ctx); } + if (rule.id === 'MCP-05') { + return this.evalMcpMetrics(rule, ctx); + } return { rule, result: 'skipped', message: 'Unhandled MCP rule' }; } @@ -29,7 +43,8 @@ export class McpRuleHandler implements INativeRuleHandler { const smokeFile = files.find(f => f.includes('mcp') && f.endsWith('.json')); if (!smokeFile) { - return { rule, result: 'skipped', message: 'Run .harness/scripts/mcp-smoke.mjs to generate evidence' }; + // Same verdict and words as `mcp.rego`: absent evidence is the finding. + return { rule, result: 'failed', message: 'Run .harness/scripts/mcp-smoke.mjs to generate evidence' }; } const evidence = JSON.parse( @@ -52,8 +67,12 @@ export class McpRuleHandler implements INativeRuleHandler { return { rule, result: 'passed' }; } + private serverFile(ctx: WorkspaceEvaluationContext): string { + return path.join(ctx.corePath, 'src', 'packages', 'mcp-server', 'src', 'mcp', 'mcp-server.service.ts'); + } + private async evalMcpSecurity(rule: NormalizedRule, ctx: WorkspaceEvaluationContext): Promise { - const serverFile = path.join(ctx.corePath, 'src', 'packages', 'mcp-server', 'src', 'mcp', 'mcp-server.service.ts'); + const serverFile = this.serverFile(ctx); if (!await this.fs.exists(serverFile)) { return { rule, result: 'skipped', message: 'MCP server.ts not found' }; } @@ -63,4 +82,21 @@ export class McpRuleHandler implements INativeRuleHandler { } return { rule, result: 'failed', message: 'MCP transport config missing apiKey or local-only restriction' }; } + + /** MCP-05, the tokens `mcp.rego` looks for in the same file. */ + private async evalMcpMetrics(rule: NormalizedRule, ctx: WorkspaceEvaluationContext): Promise { + const serverFile = this.serverFile(ctx); + if (!await this.fs.exists(serverFile)) { + return { rule, result: 'skipped', message: 'MCP server.ts not found' }; + } + const content = await this.fs.readFile(serverFile); + if (['latency', 'metrics', 'histogram', 'counter'].some(token => content.includes(token))) { + return { rule, result: 'passed' }; + } + return { + rule, + result: 'failed', + message: 'MCP tool calls SHOULD emit latency, success, failure, and error class metrics — no metrics instrumentation detected in MCP server source', + }; + } } diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.spec.ts index 95a991efc..9dc70a486 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.spec.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.spec.ts @@ -40,7 +40,11 @@ describe('SdlcRuleHandler', () => { expect((await h.evaluate(rule('QT-03'), ctx)).result).toBe('passed'); }); - it('QT-05 always passes (runtime analysis)', async () => { + it('QT-05 is not claimed — it declares a runtime fact, and "always passes" was a fixed answer (GT-716 AC4)', async () => { + expect(new SdlcRuleHandler(fsMock()).canHandle(rule('QT-05'))).toBe(false); + }); + + it.skip('QT-05 always passes (runtime analysis) — retired by GT-716 AC4', async () => { const h = new SdlcRuleHandler(fsMock()); expect((await h.evaluate(rule('QT-05'), ctx)).result).toBe('passed'); }); diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.ts index b94433cf9..354ab0af8 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/sdlc-rule.handler.ts @@ -8,7 +8,11 @@ export class SdlcRuleHandler implements INativeRuleHandler { constructor(private readonly fs: IFileSystem) {} canHandle(rule: NormalizedRule): boolean { - return rule.id.startsWith('QT-'); + // GT-716 AC4: QT-05 (testing-pyramid distribution) is a test run's fact — + // the rule declares `satellite.testing` — and this handler used to answer + // `passed` for it with "requires runtime analysis": a fixed answer, not a + // verdict. Unclaimed, so the engine reports the declaration's class instead. + return rule.id.startsWith('QT-') && rule.id !== 'QT-05'; } async evaluate(rule: NormalizedRule, ctx: WorkspaceEvaluationContext): Promise { @@ -17,7 +21,6 @@ export class SdlcRuleHandler implements INativeRuleHandler { case 'QT-02': return this.checkForEvidence(rule, ctx, 'complexity report', ['complexity-report.json']); case 'QT-03': return this.checkForEvidence(rule, ctx, 'security scan', ['security-scan.json']); case 'QT-04': return this.checkForEvidence(rule, ctx, 'debt report', ['debt-report.json']); - case 'QT-05': return { rule, result: 'passed', message: 'Testing pyramid distribution requires runtime analysis' }; case 'QT-06': return this.evalDocumentationDelta(rule, ctx); case 'QT-07': return this.checkForEvidence(rule, ctx, 'observability config', [ 'otel.config.js', 'opentelemetry.config.js', 'src/instrumentation.ts', diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.spec.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.spec.ts new file mode 100644 index 000000000..fb211dc58 --- /dev/null +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.spec.ts @@ -0,0 +1,54 @@ +import { TelemetryEvidenceRuleHandler } from './telemetry-evidence-rule.handler'; +import { NormalizedRule } from '../../../../domain/models/normalized-rule'; + +const ctx = { satellitePath: '/sat', corePath: '/core' }; + +function fsMock(pkg?: Record, broken = false) { + return { + exists: jest.fn(async (p: string) => p === '/sat/package.json' && pkg !== undefined), + readJson: jest.fn(async () => { if (broken) throw new Error('bad json'); return pkg; }), + } as unknown as ConstructorParameters[0]; +} + +const rule = (id: string): NormalizedRule => + ({ id, severity: 'MUST', category: 'observability', title: id, description: '', blocking: true, sourceFile: 's' }); + +const outcome = async (id: string, pkg?: Record, broken = false) => + (await new TelemetryEvidenceRuleHandler(fsMock(pkg, broken)).evaluate(rule(id), ctx)).result; + +describe('TelemetryEvidenceRuleHandler · GT-716 AC4 (the native twin of telemetry-evidence.rego)', () => { + it('claims exactly OBS-EVD-01..03', () => { + const h = new TelemetryEvidenceRuleHandler(fsMock()); + expect(['OBS-EVD-01', 'OBS-EVD-02', 'OBS-EVD-03'].every((id) => h.canHandle(rule(id)))).toBe(true); + expect(h.canHandle(rule('OBS-EVD-04'))).toBe(false); + }); + + it('OBS-EVD-01: a tracing package passes — any @opentelemetry/*, dd-trace or elastic-apm-node', async () => { + expect(await outcome('OBS-EVD-01', { dependencies: { '@opentelemetry/api': '1' } })).toBe('passed'); + expect(await outcome('OBS-EVD-01', { devDependencies: { 'dd-trace': '5' } })).toBe('passed'); + expect(await outcome('OBS-EVD-01', { dependencies: { express: '4' } })).toBe('failed'); + }); + + it('OBS-EVD-02: pino, winston, bunyan or @nestjs/common passes', async () => { + expect(await outcome('OBS-EVD-02', { dependencies: { pino: '9' } })).toBe('passed'); + expect(await outcome('OBS-EVD-02', { dependencies: { '@nestjs/common': '11' } })).toBe('passed'); + expect(await outcome('OBS-EVD-02', { dependencies: { debug: '4' } })).toBe('failed'); + }); + + it('OBS-EVD-03: prom-client or any @opentelemetry/* passes', async () => { + expect(await outcome('OBS-EVD-03', { dependencies: { 'prom-client': '15' } })).toBe('passed'); + expect(await outcome('OBS-EVD-03', { dependencies: { '@opentelemetry/sdk-metrics': '1' } })).toBe('passed'); + expect(await outcome('OBS-EVD-03', { dependencies: { pino: '9' } })).toBe('failed'); + }); + + it('no manifest, or an unreadable one, is "no packages" — a verdict, not a skip, exactly as the policy reads it', async () => { + expect(await outcome('OBS-EVD-01')).toBe('failed'); + expect(await outcome('OBS-EVD-02', {}, true)).toBe('failed'); + }); + + it('every failure names the packages that would have satisfied it', async () => { + const r = await new TelemetryEvidenceRuleHandler(fsMock({})).evaluate(rule('OBS-EVD-03'), ctx); + expect(r.result).toBe('failed'); + expect(r.message).toMatch(/prom-client, @opentelemetry\/\*/); + }); +}); diff --git a/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.ts b/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.ts new file mode 100644 index 000000000..091a3ae5b --- /dev/null +++ b/src/packages/core-domain/src/application/validators/evaluators/handlers/telemetry-evidence-rule.handler.ts @@ -0,0 +1,80 @@ +import * as path from 'path'; +import { IFileSystem } from '../../../../domain/interfaces'; +import { NormalizedRule } from '../../../../domain/models/normalized-rule'; +import { WorkspaceEvaluationContext, RuleEvaluationResult } from '../evaluator.interface'; +import { INativeRuleHandler } from './rule-handler.interface'; + +/** + * GT-716 AC4 — the native twin of `telemetry-evidence.rego` for OBS-EVD-01..03. + * + * The policy decides the three rules from the satellite's declared dependencies: + * a distributed-tracing package, a structured-logging package, a metrics package. + * Until this handler existed the native engine skipped them as `needs-runtime` / + * `needs-external-system` — the rules' full intent IS runtime evidence — while the + * OPA engine decided them from `package.json`, so the same repository got a verdict + * on one engine and a skip on the other. Both engines now read the same proxy and + * say the same thing; the proxy's limits are stated in each message. + * + * The package lists are the policy's, verbatim. A dependency in `dependencies` or + * `devDependencies` counts, exactly as `all_deps` in the Rego does. + */ +export class TelemetryEvidenceRuleHandler implements INativeRuleHandler { + constructor(private readonly fs: IFileSystem) {} + + canHandle(rule: NormalizedRule): boolean { + return rule.id === 'OBS-EVD-01' || rule.id === 'OBS-EVD-02' || rule.id === 'OBS-EVD-03'; + } + + async evaluate(rule: NormalizedRule, ctx: WorkspaceEvaluationContext): Promise { + const deps = await this.allDependencies(ctx.satellitePath); + const has = (name: string) => deps.has(name); + const anyOtel = [...deps].some((d) => d.startsWith('@opentelemetry/')); + + if (rule.id === 'OBS-EVD-01') { + const ok = anyOtel || has('dd-trace') || has('elastic-apm-node'); + return ok + ? { rule, result: 'passed' } + : { + rule, + result: 'failed', + message: + 'Production request paths must emit TraceId, SpanId, and CorrelationId. No distributed tracing package ' + + '(@opentelemetry/*, dd-trace, elastic-apm-node) detected in satellite dependencies.', + }; + } + if (rule.id === 'OBS-EVD-02') { + const ok = has('pino') || has('winston') || has('bunyan') || has('@nestjs/common'); + return ok + ? { rule, result: 'passed' } + : { + rule, + result: 'failed', + message: + 'Structured logs must include request correlation fields and avoid raw PII. No structured logging package ' + + '(pino, winston, bunyan, @nestjs/common) detected in satellite dependencies.', + }; + } + const ok = has('prom-client') || anyOtel; + return ok + ? { rule, result: 'passed' } + : { + rule, + result: 'failed', + message: + 'Production services must report error rate, latency percentile, throughput, and availability metrics. ' + + 'No metrics package (prom-client, @opentelemetry/*) detected in satellite dependencies.', + }; + } + + /** `dependencies` ∪ `devDependencies` of the satellite's root manifest; empty when there is none. */ + private async allDependencies(satellitePath: string): Promise> { + const manifest = path.join(satellitePath, 'package.json'); + if (!(await this.fs.exists(manifest))) return new Set(); + try { + const pkg = (await this.fs.readJson(manifest)) as { dependencies?: Record; devDependencies?: Record } | null; + return new Set([...Object.keys(pkg?.dependencies ?? {}), ...Object.keys(pkg?.devDependencies ?? {})]); + } catch { + return new Set(); + } + } +} diff --git a/src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts b/src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts index fe708ef69..8c8c5c708 100644 --- a/src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts +++ b/src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts @@ -20,6 +20,7 @@ import { AclRuleHandler } from './handlers/acl-rule.handler'; import { AdrConformanceRuleHandler } from './handlers/adr-conformance-rule.handler'; import { ModuleBoundaryRuleHandler } from './handlers/module-boundary-rule.handler'; import { ProbabilisticEvidenceRuleHandler } from './handlers/probabilistic-evidence-rule.handler'; +import { TelemetryEvidenceRuleHandler } from './handlers/telemetry-evidence-rule.handler'; import { classifyRule } from '../rule-evaluability'; export class NativeEvaluator implements IRuleEvaluatorStrategy { @@ -60,6 +61,11 @@ export class NativeEvaluator implements IRuleEvaluatorStrategy { // projected facts and delegates to the same admissibility function, so the // two engines cannot drift by construction. new ProbabilisticEvidenceRuleHandler(), + // GT-716 AC4: OBS-EVD-01..03 — the native twin of `telemetry-evidence.rego`, + // deciding from the satellite's declared dependencies exactly as the policy + // does, so the same repository no longer gets a verdict on one engine and a + // `needs-runtime` skip on the other. + new TelemetryEvidenceRuleHandler(fs), // GT-632: rules that author their own `from`/`to` module-graph clause // (HXA-01/02/04/05). Registered BEFORE the ADR-conformance catch-all and // after the id-specific handlers: it claims by clause, so an existing 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 de411da6e..bca2660dc 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 @@ -4,6 +4,7 @@ import { NormalizedRule } from '../../../domain/models/normalized-rule'; import { IRuleEvaluatorStrategy, WorkspaceEvaluationContext, RuleEvaluationResult } from './evaluator.interface'; import { loadPolicy } from '@open-policy-agent/opa-wasm'; import { OpaInputBuilder } from './opa-input-builder'; +import { classifyRule } from '../rule-evaluability'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import * as crypto from 'crypto'; @@ -192,6 +193,21 @@ function skippedForAbsentFacets( }; } +/** + * GT-716 AC4 — a policy-less rule whose own declaration says there is nothing to + * check (`facts: []`: documentation behind a judgement, or a check nobody authored) + * is skipped for THAT reason on both engines. "No policy in the bundle" is not news + * about such a rule; stating the declaration's class is what makes the recorded + * decision "documentation-only on both engines" a fact the reports carry. Null when + * the declaration says the rule IS decidable — then the missing policy is the news. + */ +function skippedByOwnDeclaration(rule: NormalizedRule): RuleEvaluationResult | null { + if (!rule.facts) return null; + const own = classifyRule(rule, false); + if (own.evaluability !== 'documentation-only' && own.evaluability !== 'underspecified') return null; + return { rule, result: 'skipped', evaluability: own.evaluability, message: own.why } as RuleEvaluationResult; +} + export class OpaEvaluator implements IRuleEvaluatorStrategy { private inputBuilder: OpaInputBuilder; private ajv: Ajv; @@ -459,7 +475,7 @@ export class OpaEvaluator implements IRuleEvaluatorStrategy { }; } if (declared && !declared.has(rule.id)) { - return { + return skippedByOwnDeclaration(rule) ?? { rule, result: 'skipped', evaluability: 'no-policy-in-bundle', diff --git a/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts b/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts index b3d7a4dc3..9e66c6076 100644 --- a/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts +++ b/src/packages/core-domain/src/application/validators/rule-corpus-triage.spec.ts @@ -116,7 +116,16 @@ const PINNED_CLASS_COUNTS: Readonly> = { // unimplemented-native 52 -> 21 needs-external-system 20 -> 27 // needs-runtime 17 -> 23 needs-supplied-facts 0 -> 31 // documentation-only 141 -> 138 underspecified 14 -> 4 - 'native-handler': 171, + // GT-716 AC4 (2026-09-21) — three in, one out: `TelemetryEvidenceRuleHandler` decides + // OBS-EVD-01..03 from the satellite's dependencies (the same proxy telemetry-evidence.rego + // reads; the rules declare `satellite.packageJson` and nothing they do not read), and + // QT-05 left `SdlcRuleHandler`, whose "always passes — requires runtime analysis" was a + // fixed answer: it declares `satellite.testing` now. The seven source-scan security rules + // (SEC-INJ/PATH/TIMING-01/02, SEC-RL-03) declare `satellite.findings`: a scanner's finding + // over the AST, not a regex over the tree (MM-R10), so they are adapter work, not handler work. + // native-handler 171 -> 173 unimplemented-native 21 -> 14 + // needs-external-system 27 -> 33 needs-runtime 23 -> 22 + 'native-handler': 173, // 137 -> 138 on 2026-08-16: ADR-0126's generated conformance ruleset. An accepted // ADR owes one, `generate-adr-rulesets.mjs` wrote it, and it lands here for the same // reason every generated ADR ruleset does — its validationQuery says nothing a native @@ -129,9 +138,9 @@ const PINNED_CLASS_COUNTS: Readonly> = { // costs one more rule nothing can run, because the superseding ADR owes a conformance // placeholder of its own while the superseded one keeps the placeholder it already had. 'documentation-only': 138, - 'unimplemented-native': 21, - 'needs-external-system': 27, - 'needs-runtime': 23, + 'unimplemented-native': 14, + 'needs-external-system': 33, + 'needs-runtime': 22, 'needs-supplied-facts': 31, underspecified: 4, 'no-policy-in-bundle': 0, @@ -366,7 +375,10 @@ describe('GT-595 · the handler slice that landed', () => { // for a capability that was ADDED is the honest reading, and pretending // otherwise would be the claim this file exists to prevent. const unclaimed = CORPUS.filter(r => !claims(r)); - expect(unclaimed).toHaveLength(106); + // 106 -> 104 on 2026-09-21 (GT-716 AC4): OBS-EVD-01..03 claimed by + // `TelemetryEvidenceRuleHandler` (−3); QT-05 released by `SdlcRuleHandler` (+1), + // whose "always passes" was never a claim on the rule, only on the reader. + expect(unclaimed).toHaveLength(104); // ...and every one of the 134 ADR-conformance rules is now claimed. // 126 -> 133 on 2026-07-28: the committed corpus was seven rulesets behind @@ -393,7 +405,8 @@ describe('GT-595 · the handler slice that landed', () => { // module-boundary (GT-632). Every one of the twelve is `blocking: true`, // which is why the whole of each closure lands on this figure. const unclaimedBlocking = CORPUS.filter(r => !claims(r) && r.blocking); - expect(unclaimedBlocking).toHaveLength(73); + // 73 -> 71 on 2026-09-21 (GT-716 AC4): the same movement, blocking rules only. + expect(unclaimedBlocking).toHaveLength(71); }); it('claims each of the four module-boundary rules closed on 2026-07-29', () => { @@ -471,15 +484,18 @@ describe('GT-595 AC2 · the corpus rules that still declare `blocking` and canno // four were module-boundary clauses the corpus already carried and the // engine never read (GT-632). The other three classes are untouched — no // adapter was written and no rule was re-authored. - expect(offenders).toHaveLength(73); + // 73 -> 71 on 2026-09-21 (GT-716 AC4): OBS-EVD-01..03 are decided now (−3); QT-05 joins + // as `needs-runtime` (+1) — it was never decided, it was answered. Six blocking source-scan + // rules moved from "write the handler" to "write the adapter" (SEC-RL-03 is not blocking). + expect(offenders).toHaveLength(71); // 2026-09-20 (GT-716 AC2): the 73 are the same 73 — a declaration moves a rule // between classes, never in or out of "blocking and did not run" — but what each // one costs changed: 25 of the 36 "write the handler" rows were never handler work // (a declared posture, the CI system, a test run), and the seven KI-R rows are no // longer "author the check" but "supply the intake record". - expect(countOf('unimplemented-native')).toBe(11); - expect(countOf('needs-external-system')).toBe(16); - expect(countOf('needs-runtime')).toBe(15); + expect(countOf('unimplemented-native')).toBe(5); + expect(countOf('needs-external-system')).toBe(21); + expect(countOf('needs-runtime')).toBe(14); expect(countOf('needs-supplied-facts')).toBe(27); expect(countOf('underspecified')).toBe(4); @@ -502,13 +518,18 @@ describe('GT-595 · the remaining backlog is costed, not a lump', () => { // OCB-02 left this list on 2026-09-20 (GT-716 AC2): `open-core-boundary.rego` // decides it from a declared boundary, so it is `needs-supplied-facts` — see the // vacuity note below, which still holds. - expect(of('unimplemented-native')).toEqual(expect.arrayContaining(['SEC-INJ-01', 'HXA-03'])); + // SEC-INJ-01 left this list on 2026-09-21 (GT-716 AC4): it declares `satellite.findings` + // — a scanner's finding, not a handler's regex (MM-R10) — so it is adapter work now. + expect(of('unimplemented-native')).toEqual(expect.arrayContaining(['HXA-03'])); + expect(of('unimplemented-native')).not.toContain('SEC-INJ-01'); expect(of('unimplemented-native')).not.toContain('OCB-02'); expect(of('needs-supplied-facts')).toEqual(expect.arrayContaining(['OCB-02', 'MTN-01', 'RUNT-01', 'KI-R01', 'PROT-03'])); expect(of('unimplemented-native')).not.toContain('MTN-05'); expect(of('unimplemented-native')).not.toContain('HXA-01'); - expect(of('needs-external-system')).toEqual(expect.arrayContaining(['GIT-02', 'MTN-02', 'OBS-EVD-03'])); - expect(of('needs-runtime')).toEqual(expect.arrayContaining(['OBS-EVD-01', 'TPY-05', 'ABAC-01'])); + expect(of('needs-external-system')).toEqual(expect.arrayContaining(['GIT-02', 'MTN-02', 'SEC-INJ-01'])); + expect(of('needs-runtime')).toEqual(expect.arrayContaining(['QT-05', 'TPY-05', 'ABAC-01'])); + // OBS-EVD-01..03 are `native-handler` since 2026-09-21 (GT-716 AC4). + expect(of('native-handler')).toEqual(expect.arrayContaining(['OBS-EVD-01', 'OBS-EVD-02', 'OBS-EVD-03'])); expect(of('underspecified')).toEqual(expect.arrayContaining(['EC-SEC-01', 'SV-SEC-01'])); expect(of('underspecified')).not.toContain('KI-R01'); }); diff --git a/src/packages/core-domain/test/parity-fixtures/mcp.fixture.json b/src/packages/core-domain/test/parity-fixtures/mcp.fixture.json index 29dfaba7a..06f87e667 100644 --- a/src/packages/core-domain/test/parity-fixtures/mcp.fixture.json +++ b/src/packages/core-domain/test/parity-fixtures/mcp.fixture.json @@ -1,6 +1,6 @@ { "domain": "mcp", - "description": "MCP protocol compliance rule evaluator parity — MCP rules", + "description": "MCP protocol compliance rule evaluator parity — MCP rules. GT-716 AC4: absent smoke evidence FAILS MCP-01..03, the same verdict and words as mcp.rego — the evidence is an observed fact of the checkout, and its absence is the finding.", "rules": [ { "id": "MCP-01", "severity": "MUST", "category": "mcp", "title": "Initialize Request Must Return Capabilities", "description": "The MCP server must respond to initialize with protocolVersion, capabilities, and serverInfo", "blocking": true, "sourceFile": "protocol-compliance.rules.json" }, { "id": "MCP-02", "severity": "MUST", "category": "mcp", "title": "Tools List Must Be Complete and Stable", "description": "tools/list must expose every supported tool with name, description, and inputSchema", "blocking": true, "sourceFile": "protocol-compliance.rules.json" }, @@ -33,9 +33,9 @@ }, "context": { "satellitePath": "/sat", "corePath": "/core" }, "expectedResults": [ - { "ruleId": "MCP-01", "result": "skipped" }, - { "ruleId": "MCP-02", "result": "skipped" }, - { "ruleId": "MCP-03", "result": "skipped" }, + { "ruleId": "MCP-01", "result": "failed" }, + { "ruleId": "MCP-02", "result": "failed" }, + { "ruleId": "MCP-03", "result": "failed" }, { "ruleId": "MCP-04", "result": "skipped" } ] }, diff --git a/src/packages/core-domain/test/parity-fixtures/sdlc-quality-thresholds.fixture.json b/src/packages/core-domain/test/parity-fixtures/sdlc-quality-thresholds.fixture.json index b409c197e..b272c82c4 100644 --- a/src/packages/core-domain/test/parity-fixtures/sdlc-quality-thresholds.fixture.json +++ b/src/packages/core-domain/test/parity-fixtures/sdlc-quality-thresholds.fixture.json @@ -1,6 +1,6 @@ { "domain": "sdlc-quality-thresholds", - "description": "SDLC quality threshold rule evaluator parity — QT rules", + "description": "SDLC quality threshold rule evaluator parity — QT rules. GT-716 AC4: QT-05 (testing-pyramid distribution) is unclaimed — it declares a runtime fact, and \"always passes\" was a fixed answer — so it is skipped in every scenario.", "rules": [ { "id": "QT-01", "severity": "MUST", "category": "testing", "title": "Code Coverage", "description": "Coverage below 80% on business logic blocks merge", "blocking": true, "sourceFile": "quality-thresholds.rules.json" }, { "id": "QT-02", "severity": "MUST", "category": "code-quality", "title": "Cyclomatic Complexity", "description": "Methods exceeding cyclomatic complexity of 15 block merge", "blocking": true, "sourceFile": "quality-thresholds.rules.json" }, @@ -36,7 +36,7 @@ { "ruleId": "QT-02", "result": "passed" }, { "ruleId": "QT-03", "result": "passed" }, { "ruleId": "QT-04", "result": "passed" }, - { "ruleId": "QT-05", "result": "passed" }, + { "ruleId": "QT-05", "result": "skipped" }, { "ruleId": "QT-06", "result": "passed" }, { "ruleId": "QT-07", "result": "passed" }, { "ruleId": "QT-08", "result": "passed" } @@ -54,7 +54,7 @@ { "ruleId": "QT-02", "result": "skipped" }, { "ruleId": "QT-03", "result": "skipped" }, { "ruleId": "QT-04", "result": "skipped" }, - { "ruleId": "QT-05", "result": "passed" }, + { "ruleId": "QT-05", "result": "skipped" }, { "ruleId": "QT-06", "result": "skipped" }, { "ruleId": "QT-07", "result": "skipped" }, { "ruleId": "QT-08", "result": "skipped" } @@ -82,7 +82,7 @@ { "ruleId": "QT-02", "result": "passed" }, { "ruleId": "QT-03", "result": "passed" }, { "ruleId": "QT-04", "result": "passed" }, - { "ruleId": "QT-05", "result": "passed" }, + { "ruleId": "QT-05", "result": "skipped" }, { "ruleId": "QT-06", "result": "skipped" }, { "ruleId": "QT-07", "result": "passed" }, { "ruleId": "QT-08", "result": "passed" } @@ -100,7 +100,7 @@ { "ruleId": "QT-02", "result": "skipped" }, { "ruleId": "QT-03", "result": "skipped" }, { "ruleId": "QT-04", "result": "skipped" }, - { "ruleId": "QT-05", "result": "passed" }, + { "ruleId": "QT-05", "result": "skipped" }, { "ruleId": "QT-06", "result": "passed" }, { "ruleId": "QT-07", "result": "skipped" }, { "ruleId": "QT-08", "result": "skipped" } diff --git a/src/packages/core-domain/test/parity-fixtures/telemetry-evidence.fixture.json b/src/packages/core-domain/test/parity-fixtures/telemetry-evidence.fixture.json new file mode 100644 index 000000000..a4bc63689 --- /dev/null +++ b/src/packages/core-domain/test/parity-fixtures/telemetry-evidence.fixture.json @@ -0,0 +1,17 @@ +{ + "domain": "telemetry-evidence", + "description": "GT-716 AC4 — OBS-EVD-01..03 decided natively from the satellite's declared dependencies, the same proxy telemetry-evidence.rego reads: pino present, no tracing, no metrics.", + "input": { + "satellitePath": "/sat", + "corePath": "/core", + "existingFiles": ["/sat/package.json"], + "fileContents": { + "/sat/package.json": "{\"name\":\"sat\",\"dependencies\":{\"pino\":\"9.0.0\",\"express\":\"4.19.0\"}}" + } + }, + "expectedNative": [ + { "ruleId": "OBS-EVD-01", "severity": "MUST", "result": "failed" }, + { "ruleId": "OBS-EVD-02", "severity": "MUST", "result": "passed" }, + { "ruleId": "OBS-EVD-03", "severity": "MUST", "result": "failed" } + ] +} diff --git a/src/packages/infra-providers/src/rule-applicability.integration.spec.ts b/src/packages/infra-providers/src/rule-applicability.integration.spec.ts index 36e485d06..5859cfd31 100644 --- a/src/packages/infra-providers/src/rule-applicability.integration.spec.ts +++ b/src/packages/infra-providers/src/rule-applicability.integration.spec.ts @@ -177,7 +177,13 @@ describe('GT-571 · the first validate of a freshly initialized satellite', () = const fired = new Set(result.issues.map(i => i.ruleId)); // The two the audit named by hand, plus the rest of their families. - for (const ruleId of ['CLI-RR-01', 'CLI-RR-02', 'CLI-PAR-01', 'TAX-05']) { + // MCP-01 joined this list on 2026-09-21 (GT-716 AC4): the pack judges the Core's + // MCP server — its smoke evidence, its server source — and once the native + // engine decided MCP-01..03 the way the policy did (absent evidence FAILS), a + // fresh satellite validated against a Core with no evidence got three blocking + // findings addressed to the Core. `scope: core-cli` always said so; `audience: + // core` now says it where applicability reads it, for both engines. + for (const ruleId of ['CLI-RR-01', 'CLI-RR-02', 'CLI-PAR-01', 'TAX-05', 'MCP-01']) { expect(fired.has(ruleId)).toBe(false); expect(result.notApplicableRuleIds).toContain(ruleId); } @@ -277,6 +283,38 @@ describe('GT-571 · the Core monorepo keeps its own rules', () => { expect(index.get('TAX-01')?.appliesFromSdlcPhase).toBeUndefined(); }); + it('defers OBS-EVD-01..03 to Construction without weakening them (GT-716 AC4)', async () => { + // The three telemetry-evidence rules speak of PRODUCTION request paths and + // services, and since GT-716 AC4 the native engine decides them the way the + // policy always did: from the satellite's declared dependencies. On a phase-0 + // scaffold that verdict would be "no tracing package" about a repository with + // no request path yet — the same kind of finding MTN-05 used to produce, and + // the one this suite's first test exists to refuse. `appliesFromSdlcPhase: 3` + // (Construction: the first phase at which a codebase with request paths and + // dependencies exists) is the applicability fact the rules always stated in + // prose, and it excludes them BEFORE either engine runs — so `--engine opa`, + // which used to fail all three on every fresh satellite, stops doing so too. + // Deferred, not disabled: from Construction on, a satellite without a tracing, + // logging or metrics package is failed for exactly the reason the rule states. + const fs = new NodeFileSystemProvider() as any; + const rules = await new DiskRulesetRepository(fs, silentLogger).loadAllRulesets(CORE); + const index = await RuleApplicabilityIndex.load(fs, CORE, path.sep); + + for (const id of ['OBS-EVD-01', 'OBS-EVD-02', 'OBS-EVD-03']) expect(index.get(id)?.appliesFromSdlcPhase).toBe(3); + + const excludedAt = (sdlcPhase: number) => { + const ctx: ApplicabilityContext = { audience: 'satellite', declaredTopologies: [], sdlcPhase }; + const { notApplicable } = partitionByApplicability(rules as NormalizedRule[], { index, context: ctx }); + const ids = new Set(notApplicable.map(n => n.rule.id)); + return ['OBS-EVD-01', 'OBS-EVD-02', 'OBS-EVD-03'].every(id => ids.has(id)); + }; + + expect(excludedAt(0)).toBe(true); // the freshly scaffolded satellite + expect(excludedAt(2)).toBe(true); // Design — nothing is built yet + expect(excludedAt(3)).toBe(false); // Construction — the rules bind, unchanged + expect(excludedAt(5)).toBe(false); + }); + it('defers MTN-05 to Design without weakening it', async () => { // MTN-05's own description says the multi-tenant schema strategy "MUST be // defined before Phase 2 Design". That was prose; it is now an applicability diff --git a/src/rulesets/mcp/protocol-compliance.rules.json b/src/rulesets/mcp/protocol-compliance.rules.json index 96819c7f0..63e6ea1ae 100644 --- a/src/rulesets/mcp/protocol-compliance.rules.json +++ b/src/rulesets/mcp/protocol-compliance.rules.json @@ -6,6 +6,7 @@ "version": "1.0.0", "effectiveDate": "2026-06-08", "scope": "core-cli", + "audience": "core", "category": "mcp", "rules": [ { diff --git a/src/rulesets/observability/telemetry-evidence.rules.json b/src/rulesets/observability/telemetry-evidence.rules.json index a790c209f..4a4527312 100644 --- a/src/rulesets/observability/telemetry-evidence.rules.json +++ b/src/rulesets/observability/telemetry-evidence.rules.json @@ -11,9 +11,9 @@ { "id": "OBS-EVD-01", "facts": [ - "satellite.packageJson", - "traces" + "satellite.packageJson" ], + "appliesFromSdlcPhase": 3, "severity": "MUST", "category": "tracing", "title": "Production Paths Emit Trace Context", @@ -24,9 +24,9 @@ { "id": "OBS-EVD-02", "facts": [ - "satellite.packageJson", - "traces" + "satellite.packageJson" ], + "appliesFromSdlcPhase": 3, "severity": "MUST", "category": "logging", "title": "Structured Logs Carry Request Context", @@ -37,9 +37,9 @@ { "id": "OBS-EVD-03", "facts": [ - "satellite.packageJson", - "telemetryBackend" + "satellite.packageJson" ], + "appliesFromSdlcPhase": 3, "severity": "MUST", "category": "metrics", "title": "Service Health Metrics Are Reported", diff --git a/src/rulesets/opa/README.es.md b/src/rulesets/opa/README.es.md index edc9ce150..609ef1108 100644 --- a/src/rulesets/opa/README.es.md +++ b/src/rulesets/opa/README.es.md @@ -50,6 +50,8 @@ Añadir una regla significa, por tanto, declarar qué lee; añadir una lectura e Lo que un motor decide y el otro no es entonces una **diferencia registrada, no libre** (AC3 de GT-716): [`73-validate-engine-coverage-parity.mjs`](../../../.harness/scripts/ci/73-validate-engine-coverage-parity.mjs) corre ambos motores sobre este repositorio y sobre un satélite recién salido de `evolith init`, y sujeta cada regla de un solo motor a [`engine-coverage-parity.baseline.json`](../../../.harness/scripts/ci/engine-coverage-parity.baseline.json) — por regla, por escenario, por dirección, con la razón que dio el otro motor (la clase que enuncia su informe, las facetas que la política lee y una ejecución a secas no suministra). Una regla sin registrar, una entrada obsoleta o una clase cambiada hacen fallar; `--write` regenera el fichero para revisión. Nada en él es una tolerancia: es la lista de lo que cada motor aún no puede decidir, y por qué. +Y nada en esa lista está por omisión (AC4 de GT-716). Cada entrada cuya clase es deuda y no una declaración de la propia regla — sin política en el bundle, sin handler nativo, un handler que declinó, una vía OPA que no dio razón — debe estar cubierta por una decisión registrada en [`engine-coverage-decisions.json`](../../../.harness/scripts/ci/engine-coverage-decisions.json): `native-only` / `opa-only` (la diferencia se acepta, con la razón y con lo que la reabriría) o `neither` (ningún motor decide la regla tal como está escrita — las 138 reglas ADR-conformance generadas, `documentation-only` en ambos lados, son un patrón ahí). Una entrada sin decisión hace fallar el guard como solo-un-motor por omisión; una decisión que las ejecuciones contradicen — su regla decidida ya por los dos motores, o por el motor que ella decía que no — también lo hace fallar, de modo que el registro no puede sobrevivir a lo que decidió. Medido el 2026-09-21 sobre la misma exportación: el lado solo-OPA está **vacío** en ambos escenarios. `OBS-EVD-01..03` y `MCP-05` tienen gemelos nativos de sus políticas (las mismas comprobaciones de dependencias y de fuente, luego el mismo veredicto), `MCP-01..03` fallan por evidencia de humo ausente en los dos motores en vez de en uno, y `QT-05` — que el motor nativo respondía `passed` con «requiere análisis en runtime» — declara el hecho de runtime que necesita y no lo decide ninguno. Lo que queda solo-nativo (81 sobre este repositorio, 48 sobre un satélite `init`) son 52 / 34 políticas que leen facetas que una ejecución a secas no suministra, 25 / 10 reglas sin política y con una decisión cada una (`SSDF-*`, `SLSA-*`, `SEC-RL-01/02`, `MM-R*`), y las cuatro cláusulas `HXA` enrutadas al enforcer. + ## Políticas de enforcement agregadas Estas 35 políticas son importadas y unidas por [`main.rego`](./main.rego) en el entrypoint Wasm `evolith/main/violations`. Cada una tiene un `*.test.rego` co-ubicado y (salvo indicación) un schema de entrada en `schemas/`. La lista autoritativa es el bloque `import data.evolith.*` de `main.rego`; el build rechaza una política que emite rule ids sin estar importada allí. diff --git a/src/rulesets/opa/README.md b/src/rulesets/opa/README.md index 8876b916a..21eb1e385 100644 --- a/src/rulesets/opa/README.md +++ b/src/rulesets/opa/README.md @@ -50,6 +50,8 @@ Adding a rule therefore means declaring what it reads; adding a policy read mean What one engine decides and the other does not is then a **registered difference, not a free one** (GT-716 AC3): [`73-validate-engine-coverage-parity.mjs`](../../../.harness/scripts/ci/73-validate-engine-coverage-parity.mjs) runs both engines on this repository and on a satellite fresh from `evolith init`, and holds every coverage-only rule to [`engine-coverage-parity.baseline.json`](../../../.harness/scripts/ci/engine-coverage-parity.baseline.json) — per rule, per scenario, per direction, with the reason the other engine gave (the class its report states, the facets the policy reads that a bare run does not supply). An unregistered rule, a stale entry or a changed class fails; `--write` regenerates the file for review. Nothing in it is a tolerance: it is the list of what each engine cannot yet decide, and why. +Nothing in that list is there by omission either (GT-716 AC4). Every entry whose class is debt rather than a declaration of the rule itself — no policy in the bundle, no native handler, a handler that declined, an OPA path that gave no reason — must be covered by a recorded decision in [`engine-coverage-decisions.json`](../../../.harness/scripts/ci/engine-coverage-decisions.json): `native-only` / `opa-only` (the difference is accepted, with the reason and what would reopen it) or `neither` (no engine decides the rule as written — the 138 generated ADR-conformance rules, documentation-only on both sides, are one pattern there). An entry without a decision fails the guard as coverage-only by omission; a decision the runs contradict — its rule decided by both engines now, or by the engine it said would not — fails it too, so the register cannot outlive what it decided. Measured 2026-09-21 on the same export: the OPA-only side is **empty** on both scenarios. `OBS-EVD-01..03` and `MCP-05` have native twins of their policies (the same dependency and source checks, so the same verdict), `MCP-01..03` fail on absent smoke evidence on both engines instead of on one, and `QT-05` — which the native engine answered `passed` with "requires runtime analysis" — declares the runtime fact it needs and is decided by neither. What remains native-only (81 on this repository, 48 on an `init` satellite) is 52 / 34 policies reading facets a bare run does not supply, 25 / 10 rules with no policy and a decision each (`SSDF-*`, `SLSA-*`, `SEC-RL-01/02`, `MM-R*`), and the four enforcer-routed `HXA` clauses. + ## Aggregated enforcement policies These 35 policies are imported and unioned by [`main.rego`](./main.rego) into the `evolith/main/violations` Wasm entrypoint. Each has a co-located `*.test.rego` and (unless noted) an input schema under `schemas/`. The authoritative list is the `import data.evolith.*` block of `main.rego`; the build refuses a policy that emits rule ids without being imported there. diff --git a/src/rulesets/schema/facets.json b/src/rulesets/schema/facets.json index b0b7c013f..f69b5c312 100644 --- a/src/rulesets/schema/facets.json +++ b/src/rulesets/schema/facets.json @@ -210,10 +210,6 @@ "provenance": "external", "why": "Acceptance is a property of deployed infrastructure — service mesh, mTLS, on-call." }, - "telemetryBackend": { - "provenance": "external", - "why": "Acceptance is a query against a telemetry or metrics backend." - }, "satellite.testing": { "provenance": "runtime", "why": "Test-mix percentages and coverage come from a test run, not from source on disk." diff --git a/src/rulesets/sdlc/quality-thresholds.rules.json b/src/rulesets/sdlc/quality-thresholds.rules.json index b581e89e4..501c19cf4 100644 --- a/src/rulesets/sdlc/quality-thresholds.rules.json +++ b/src/rulesets/sdlc/quality-thresholds.rules.json @@ -75,7 +75,7 @@ { "id": "QT-05", "facts": [ - "repository" + "satellite.testing" ], "severity": "MUST", "category": "testing", diff --git a/src/rulesets/security/injection-prevention.rules.json b/src/rulesets/security/injection-prevention.rules.json index 5e59477ce..a5dbe114d 100644 --- a/src/rulesets/security/injection-prevention.rules.json +++ b/src/rulesets/security/injection-prevention.rules.json @@ -6,7 +6,7 @@ { "id": "SEC-INJ-01", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", @@ -19,7 +19,7 @@ { "id": "SEC-INJ-02", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", diff --git a/src/rulesets/security/path-containment.rules.json b/src/rulesets/security/path-containment.rules.json index 840fb72fc..9bb0bf198 100644 --- a/src/rulesets/security/path-containment.rules.json +++ b/src/rulesets/security/path-containment.rules.json @@ -6,7 +6,7 @@ { "id": "SEC-PATH-01", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", @@ -19,7 +19,7 @@ { "id": "SEC-PATH-02", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", diff --git a/src/rulesets/security/rate-limiting.rules.json b/src/rulesets/security/rate-limiting.rules.json index 56097b61d..e1f50c546 100644 --- a/src/rulesets/security/rate-limiting.rules.json +++ b/src/rulesets/security/rate-limiting.rules.json @@ -32,7 +32,7 @@ { "id": "SEC-RL-03", "facts": [ - "repository" + "satellite.findings" ], "severity": "SHOULD", "category": "security", diff --git a/src/rulesets/security/timing-safe-comparison.rules.json b/src/rulesets/security/timing-safe-comparison.rules.json index eb18c3047..052c4d7ee 100644 --- a/src/rulesets/security/timing-safe-comparison.rules.json +++ b/src/rulesets/security/timing-safe-comparison.rules.json @@ -6,7 +6,7 @@ { "id": "SEC-TIMING-01", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", @@ -19,7 +19,7 @@ { "id": "SEC-TIMING-02", "facts": [ - "repository" + "satellite.findings" ], "severity": "MUST", "category": "security", diff --git a/src/rulesets/standards/README.es.md b/src/rulesets/standards/README.es.md index 12a47a3fd..b2c65dc8f 100644 --- a/src/rulesets/standards/README.es.md +++ b/src/rulesets/standards/README.es.md @@ -132,7 +132,7 @@ Las ocho reglas del SSDF y las cuatro de SLSA se quedan en `no`, y eso es un ver El enunciado del gap dimensionaba el beneficio contra "~240 handlers por escribir". **Esa cifra ya está retirada.** GT-595 hizo el triage del corpus y el backlog real, decidible desde el repositorio, es la -clase `unimplemented-native`: **52 reglas** cuando se escribió esta sección, **21** desde el 2026-09-20 +clase `unimplemented-native`: **52 reglas** cuando se escribió esta sección, **21** el 2026-09-20 y **14** desde 2026-09-21 (ver más abajo). De las 410 reglas que cargaba entonces el triage del Core, 170 ya se ejecutaban y las otras 188 eran 137 placeholders de generador solo documentales, 14 reglas sin check redactado, 20 que requerían un sistema externo y 17 que requerían uno en ejecución. @@ -155,28 +155,43 @@ triaje explícito. Las políticas que deciden 25 de esas filas leen una postura satélite pueden declarar (21, la nueva clase `needs-supplied-facts`), el sistema de CI o el almacén de hallazgos (4), o una ejecución de tests (6 — más 3 desde otras clases). No se implementó nada entre las dos cifras: 31 filas que nunca fueron trabajo de handler dejaron de contarse como tal. Los conteos por -clase ahora: 171 se ejecutan, 21 por escribir, 27 necesitan un sistema externo, 23 uno en ejecución, 31 -una postura declarada, 138 son documentación, 4 están sin especificar. +clase tras el AC2: 171 se ejecutan, 21 por escribir, 27 necesitan un sistema externo, 23 uno en ejecución, +31 una postura declarada, 138 son documentación, 4 están sin especificar. + +21 → 14 el 2026-09-21 (AC4 de GT-716): las siete que salieron de esta clase tampoco se implementaron. +`SEC-INJ-01/02`, `SEC-PATH-01/02`, `SEC-TIMING-01/02` y `SEC-RL-03` declaran ahora `satellite.findings` — +si `child_process.exec` recibe input interpolado o si una comparación de credenciales es en tiempo +constante es el hallazgo de un escáner sobre el AST, no una regex sobre el árbol (`MM-R10`, en este mismo +corpus, prohíbe esa clase de handler) — y cuentan en `needs-external-system`, donde los analizadores a los +que esta tabla las apuntaba son el adaptador que la regla declara necesitar. Lo que SÍ se implementó salió +del conjunto no ejecutable por completo: `OBS-EVD-01..03` se deciden desde las dependencias del satélite, +como ya hacía su política. `QT-05` fue en sentido contrario — `SdlcRuleHandler` la respondía `passed` con +«requiere análisis en runtime», una respuesta fija y no un veredicto; declara `satellite.testing` y cuenta +como `needs-runtime`. Los conteos por clase ahora: 173 se ejecutan, 14 por escribir, 33 necesitan un +sistema externo, 22 uno en ejecución, 31 una postura declarada, 138 son documentación, 4 están sin +especificar. Proyectar este mapeo sobre esa clase es la cifra que importa: -| Del backlog de 21 handlers | Cantidad | +| Del backlog de 14 handlers | Cantidad | |---|---| -| Decidibles hoy por un analizador estándar | 9 | -| Decidibles parcialmente (señal necesaria pero no suficiente) | 2 | -| Que hay que escribir de verdad | 10 | - -Las 9 son `HXA-03` (estructura de capas — dependency-cruiser o ArchUnit), `SEC-INJ-01`, `SEC-PATH-01`, -`SEC-PATH-02` (consultas de inyección y path traversal de CodeQL/Semgrep), `SEC-TIMING-01` (comparación -en tiempo constante) y las cuatro reglas `ISO5055-*`, que declaran ellas mismas su analizador. Las 2 -parciales (`SEC-INJ-02`, `SEC-TIMING-02`) están listadas en `handlerBacklog.byEvaluabilityClass` del JSON -de mapeo; las otras tres parciales de antes salieron de la clase con GT-716, al decidirse sobre una postura -declarada. - -Es decir, adoptar vale **42,9% del backlog por completo, 52,4% incluyendo parciales** — 11 de 21 reglas -que no necesitan handlers a medida. (Antes de GT-716 las mismas 11 se leían como 14 de 52 — 17,3% / 26,9% -— contra un backlog inflado por filas que nunca fueron trabajo de handler; la proporción se movió porque -se corrigió el denominador, no porque se adoptara nada.) +| Decidibles hoy por un analizador estándar | 5 | +| Decidibles parcialmente (señal necesaria pero no suficiente) | 0 | +| Que hay que escribir de verdad | 9 | + +Las 5 son `HXA-03` (estructura de capas — dependency-cruiser o ArchUnit) y las cuatro reglas `ISO5055-*`, +que declaran ellas mismas su analizador. Las reglas de seguridad que esta lista solía llevar — `SEC-INJ-01`, +`SEC-PATH-01`, `SEC-PATH-02`, `SEC-TIMING-01` adoptables, `SEC-INJ-02` y `SEC-TIMING-02` parciales — están +en `needs-external-system` desde el AC4 de GT-716 (7 adoptables y 4 parciales allí, listadas bajo esa clase +en `handlerBacklog.byEvaluabilityClass` del JSON de mapeo): el analizador ya no es una forma de evitar +escribir un handler, es el adaptador que la regla declara necesitar. Las otras tres parciales de antes se +deciden sobre una postura declarada. + +Es decir, adoptar vale **35,7% del backlog** — 5 de 14 reglas que no necesitan handlers a medida, sin +parciales en la clase. (El 2026-09-20 la misma lectura era 11 de 21 — 42,9%, 52,4% incluyendo parciales — +y antes de GT-716, 14 de 52 — 17,3% / 26,9%. Cada paso corrigió el denominador, y el último movió seis +reglas con forma de analizador fuera de él, a la clase de adaptadores, donde cuentan los mismos +analizadores; entre ninguna de las cifras se adoptó nada.) 5 → 9 el 2026-08-09 (GT-667): las cuatro reglas de ISO/IEC 5055 se contaban como trabajo por escribir mientras las decidía un analizador, así que `remainderToAuthor` exageraba el backlog real en cuatro. **La diff --git a/src/rulesets/standards/README.md b/src/rulesets/standards/README.md index 3024c4148..b2171c967 100644 --- a/src/rulesets/standards/README.md +++ b/src/rulesets/standards/README.md @@ -130,8 +130,8 @@ The eight SSDF and four SLSA rules stay at `no`, and that is a verdict rather th The gap statement sized the payoff against "~240 handlers to write". **That figure is already retired.** GT-595 triaged the corpus and the real, decidable-from-the-repository backlog is the -`unimplemented-native` class: **52 rules** when this section was written, **21** since 2026-09-20 (see -below). Of the 410 rules Core's triage loaded then, 170 already ran and the other 188 were 137 +`unimplemented-native` class: **52 rules** when this section was written, **21** on 2026-09-20 and **14** +since 2026-09-21 (see below). Of the 410 rules Core's triage loaded then, 170 already ran and the other 188 were 137 documentation-only generator placeholders, 14 underspecified rules with no authored check, 20 that needed an external system and 17 that needed a running one. @@ -151,28 +151,43 @@ not read from a table keyed by rule id, and the table had defaulted every un-tri from the tree". The policies that decide 25 of those rows read a posture only the satellite's owners can declare (21, the new `needs-supplied-facts` class), the CI system or the findings store (4), or a test run (6 — with 3 more from elsewhere). Nothing was implemented between the two figures: 31 rows -that were never handler work stopped being counted as handler work. The counts per class now: 171 +that were never handler work stopped being counted as handler work. The counts per class after AC2: 171 run, 21 to author, 27 need an external system, 23 need a running one, 31 need a declared posture, 138 are documentation, 4 are underspecified. +21 → 14 on 2026-09-21 (GT-716 AC4): the seven that left this class were not implemented either. +`SEC-INJ-01/02`, `SEC-PATH-01/02`, `SEC-TIMING-01/02` and `SEC-RL-03` declare `satellite.findings` now — +whether `child_process.exec` receives interpolated input or a credential comparison is constant-time is a +scanner's finding over the AST, not a regex over the tree (`MM-R10` in this same corpus forbids that kind +of handler) — and count under `needs-external-system`, where the analysers this table pointed them at are +the adapter the rule declares it needs. What WAS implemented left the non-executable set altogether: +`OBS-EVD-01..03` are decided from the satellite's dependencies, as their policy already did. `QT-05` went +the other way — `SdlcRuleHandler` answered it `passed` with "requires runtime analysis", a fixed answer +rather than a verdict; it declares `satellite.testing` and counts as `needs-runtime`. The counts per class +now: 173 run, 14 to author, 33 need an external system, 22 need a running one, 31 need a declared +posture, 138 are documentation, 4 are underspecified. + Folding this mapping onto that class is the number that matters: -| Of the 21-rule handler backlog | Count | +| Of the 14-rule handler backlog | Count | |---|---| -| Decidable today by an off-the-shelf analyser | 9 | -| Decidable partially (analyser gives a necessary-but-not-sufficient signal) | 2 | -| Genuinely has to be authored | 10 | - -The 9 are `HXA-03` (layer structure — dependency-cruiser or ArchUnit), `SEC-INJ-01`, `SEC-PATH-01`, -`SEC-PATH-02` (CodeQL/Semgrep injection and path-traversal queries), `SEC-TIMING-01` (timing-safe -comparison) and the four `ISO5055-*` rules, which declare their analyser themselves. The 2 partials -(`SEC-INJ-02`, `SEC-TIMING-02`) are listed in `handlerBacklog.byEvaluabilityClass` in the mapping JSON; -the other three former partials left the class with GT-716, being decided over a declared posture. - -So adoption is worth **42.9% of the backlog outright, 52.4% including partials** — 11 of 21 rules that -do not need bespoke handlers. (Before GT-716 the same 11 read as 14 of 52 — 17.3% / 26.9% — against a -backlog inflated by rows that were never handler work; the share moved because the denominator was -corrected, not because anything was adopted.) +| Decidable today by an off-the-shelf analyser | 5 | +| Decidable partially (analyser gives a necessary-but-not-sufficient signal) | 0 | +| Genuinely has to be authored | 9 | + +The 5 are `HXA-03` (layer structure — dependency-cruiser or ArchUnit) and the four `ISO5055-*` rules, +which declare their analyser themselves. The security rules this list used to carry — `SEC-INJ-01`, +`SEC-PATH-01`, `SEC-PATH-02`, `SEC-TIMING-01` adoptable, `SEC-INJ-02` and `SEC-TIMING-02` partial — sit in +`needs-external-system` since GT-716 AC4 (7 adoptable and 4 partial there, listed under that class in +`handlerBacklog.byEvaluabilityClass` in the mapping JSON): the analyser is no longer a way to avoid +writing a handler, it is the adapter the rule declares it needs. The three former partials that left +earlier are decided over a declared posture. + +So adoption is worth **35.7% of the backlog** — 5 of 14 rules that do not need bespoke handlers, with no +partials left in the class. (On 2026-09-20 the same reading was 11 of 21 — 42.9%, 52.4% including +partials — and before GT-716, 14 of 52 — 17.3% / 26.9%. Each step corrected the denominator, and the last +one moved six analyser-shaped rules out of it into the adapter class, where the same analysers count; +nothing was adopted between any two figures.) 5 → 9 on 2026-08-09 (GT-667): the four ISO/IEC 5055 rules were counted as work to author while being decided by an analyser, so `remainderToAuthor` overstated the real backlog by four. **The share moved diff --git a/src/rulesets/standards/iso-5055-mapping.csv b/src/rulesets/standards/iso-5055-mapping.csv index c5ea4e9c1..1e5a2c2d9 100644 --- a/src/rulesets/standards/iso-5055-mapping.csv +++ b/src/rulesets/standards/iso-5055-mapping.csv @@ -302,9 +302,9 @@ MCP-02,mcp/protocol-compliance.rules.json,platform-surface,,,none,no,native-hand MCP-03,mcp/protocol-compliance.rules.json,platform-surface,,,none,no,native-handler MCP-04,mcp/protocol-compliance.rules.json,platform-surface,,,none,no,native-handler MCP-05,mcp/protocol-compliance.rules.json,platform-surface,,,none,no,native-handler -OBS-EVD-01,observability/telemetry-evidence.rules.json,operations,,,none,no,needs-runtime -OBS-EVD-02,observability/telemetry-evidence.rules.json,operations,,,none,no,needs-runtime -OBS-EVD-03,observability/telemetry-evidence.rules.json,operations,,,none,no,needs-external-system +OBS-EVD-01,observability/telemetry-evidence.rules.json,operations,,,none,no,native-handler +OBS-EVD-02,observability/telemetry-evidence.rules.json,operations,,,none,no,native-handler +OBS-EVD-03,observability/telemetry-evidence.rules.json,operations,,,none,no,native-handler OBS-EVD-04,observability/telemetry-evidence.rules.json,operations,,,none,no,needs-external-system DEP-01,sdlc/dependency-pinning.rules.json,supply-chain,,,none,yes,native-handler DEP-02,sdlc/dependency-pinning.rules.json,supply-chain,,,none,yes,native-handler @@ -320,19 +320,19 @@ QT-01,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,yes,native-handler QT-02,sdlc/quality-thresholds.rules.json,code-hygiene,CWE-1121,Maintainability,direct,yes,native-handler QT-03,sdlc/quality-thresholds.rules.json,code-hygiene,CWE-22 CWE-78 CWE-89 CWE-79 CWE-798 CWE-732,Security,partial,yes,native-handler QT-04,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,yes,native-handler -QT-05,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,no,native-handler +QT-05,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,no,needs-runtime QT-06,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,no,native-handler QT-07,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,no,native-handler QT-08,sdlc/quality-thresholds.rules.json,code-hygiene,,,none,partial,native-handler -SEC-INJ-01,security/injection-prevention.rules.json,code-structure,CWE-78 CWE-77,Security,direct,yes,unimplemented-native -SEC-INJ-02,security/injection-prevention.rules.json,code-structure,CWE-77 CWE-88,Security,partial,partial,unimplemented-native -SEC-PATH-01,security/path-containment.rules.json,code-structure,CWE-22 CWE-23 CWE-36,Security,direct,yes,unimplemented-native -SEC-PATH-02,security/path-containment.rules.json,code-structure,CWE-22,Security,direct,yes,unimplemented-native +SEC-INJ-01,security/injection-prevention.rules.json,code-structure,CWE-78 CWE-77,Security,direct,yes,needs-external-system +SEC-INJ-02,security/injection-prevention.rules.json,code-structure,CWE-77 CWE-88,Security,partial,partial,needs-external-system +SEC-PATH-01,security/path-containment.rules.json,code-structure,CWE-22 CWE-23 CWE-36,Security,direct,yes,needs-external-system +SEC-PATH-02,security/path-containment.rules.json,code-structure,CWE-22,Security,direct,yes,needs-external-system SEC-RL-01,security/rate-limiting.rules.json,code-structure,,,none,partial,native-handler SEC-RL-02,security/rate-limiting.rules.json,code-structure,CWE-789,Security,partial,partial,native-handler -SEC-RL-03,security/rate-limiting.rules.json,code-structure,,,none,no,unimplemented-native -SEC-TIMING-01,security/timing-safe-comparison.rules.json,code-structure,,,none,yes,unimplemented-native -SEC-TIMING-02,security/timing-safe-comparison.rules.json,code-structure,,,none,partial,unimplemented-native +SEC-RL-03,security/rate-limiting.rules.json,code-structure,,,none,no,needs-external-system +SEC-TIMING-01,security/timing-safe-comparison.rules.json,code-structure,,,none,yes,needs-external-system +SEC-TIMING-02,security/timing-safe-comparison.rules.json,code-structure,,,none,partial,needs-external-system ISO5055-MAINT,standards/iso-5055.rules.json,international-standard,,,none,yes,unimplemented-native ISO5055-PERF,standards/iso-5055.rules.json,international-standard,,,none,yes,unimplemented-native ISO5055-REL,standards/iso-5055.rules.json,international-standard,,,none,yes,unimplemented-native diff --git a/src/rulesets/standards/iso-5055-mapping.json b/src/rulesets/standards/iso-5055-mapping.json index d5436c3fe..1634015ae 100644 --- a/src/rulesets/standards/iso-5055-mapping.json +++ b/src/rulesets/standards/iso-5055-mapping.json @@ -94,51 +94,50 @@ "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, classification and this renderer)" ], "note": "The handler backlog is the `unimplemented-native` class only. `documentation-only` and `underspecified` rules are not handler work at all, the two adapter classes are closed by the enforcer seam rather than by a rule handler, and `needs-supplied-facts` (GT-716) is closed by the caller declaring the posture through `facts.satellite`.", - "realBacklogSize": 21, - "adoptableFromAnalyser": 9, - "adoptableFromAnalyserIncludingPartial": 11, - "adoptedFractionOfBacklog": 0.4286, - "adoptedFractionOfBacklogIncludingPartial": 0.5238, - "remainderToAuthor": 10, + "realBacklogSize": 14, + "adoptableFromAnalyser": 5, + "adoptableFromAnalyserIncludingPartial": 5, + "adoptedFractionOfBacklog": 0.3571, + "adoptedFractionOfBacklogIncludingPartial": 0.3571, + "remainderToAuthor": 9, "byEvaluabilityClass": { "unimplemented-native": { - "rules": 21, - "mappedToIso5055": 5, - "analyserAdoptable": 9, - "analyserAdoptablePartial": 2, + "rules": 14, + "mappedToIso5055": 1, + "analyserAdoptable": 5, + "analyserAdoptablePartial": 0, "adoptableRuleIds": [ "HXA-03", - "SEC-INJ-01", - "SEC-PATH-01", - "SEC-PATH-02", - "SEC-TIMING-01", "ISO5055-MAINT", "ISO5055-PERF", "ISO5055-REL", "ISO5055-SEC" ], - "adoptablePartialRuleIds": [ - "SEC-INJ-02", - "SEC-TIMING-02" - ] + "adoptablePartialRuleIds": [] }, "needs-external-system": { - "rules": 27, - "mappedToIso5055": 1, - "analyserAdoptable": 3, - "analyserAdoptablePartial": 2, + "rules": 33, + "mappedToIso5055": 5, + "analyserAdoptable": 7, + "analyserAdoptablePartial": 4, "adoptableRuleIds": [ "CICD-03", "GIT-01", - "GIT-04" + "GIT-04", + "SEC-INJ-01", + "SEC-PATH-01", + "SEC-PATH-02", + "SEC-TIMING-01" ], "adoptablePartialRuleIds": [ "CICD-01", - "CICD-02" + "CICD-02", + "SEC-INJ-02", + "SEC-TIMING-02" ] }, "needs-runtime": { - "rules": 23, + "rules": 22, "mappedToIso5055": 2, "analyserAdoptable": 0, "analyserAdoptablePartial": 0, @@ -187,7 +186,7 @@ "adoptablePartialRuleIds": [] }, "native-handler": { - "rules": 171, + "rules": 173, "mappedToIso5055": 19, "analyserAdoptable": 28, "analyserAdoptablePartial": 11, @@ -6309,7 +6308,7 @@ "adoptable": "no", "examples": [] }, - "nativeEvaluability": "needs-runtime", + "nativeEvaluability": "native-handler", "note": "An operational telemetry expectation, evaluated from runtime evidence rather than source structure." }, { @@ -6328,7 +6327,7 @@ "adoptable": "no", "examples": [] }, - "nativeEvaluability": "needs-runtime", + "nativeEvaluability": "native-handler", "note": "An operational telemetry expectation, evaluated from runtime evidence rather than source structure." }, { @@ -6347,7 +6346,7 @@ "adoptable": "no", "examples": [] }, - "nativeEvaluability": "needs-external-system", + "nativeEvaluability": "native-handler", "note": "An operational telemetry expectation, evaluated from runtime evidence rather than source structure." }, { @@ -6704,7 +6703,7 @@ "adoptable": "no", "examples": [] }, - "nativeEvaluability": "native-handler", + "nativeEvaluability": "needs-runtime", "note": "Code-adjacent convention with no ISO/IEC 5055 weakness, though a linter may still decide it." }, { @@ -6795,7 +6794,7 @@ "SonarQube S2076" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "Shell execution with interpolated user input is OS command injection verbatim." }, { @@ -6824,7 +6823,7 @@ "CodeQL taint tracking to child_process sinks" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "Taint analysis proves the sink is unsanitised; it cannot prove an allowlist exists, which is what the rule actually demands." }, { @@ -6857,7 +6856,7 @@ "SonarQube S2083" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "Structural rule with no counterpart among the 138 weaknesses." }, { @@ -6884,7 +6883,7 @@ "CodeQL js/path-injection" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "Structural rule with no counterpart among the 138 weaknesses." }, { @@ -6951,7 +6950,7 @@ "adoptable": "no", "examples": [] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "HTTP server timeout configuration has no 5055 weakness and no off-the-shelf check." }, { @@ -6973,7 +6972,7 @@ "CodeQL js/timing-attack" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "CWE-208 (observable timing discrepancy) is NOT one of the 138, so this is adoptable from an analyser but not countable against 5055." }, { @@ -6994,7 +6993,7 @@ "Semgrep timing-attack rules" ] }, - "nativeEvaluability": "unimplemented-native", + "nativeEvaluability": "needs-external-system", "note": "Same as SEC-TIMING-01; the early-length-rejection variant needs a bespoke pattern." }, { diff --git a/src/rulesets/standards/native-evaluability-snapshot.json b/src/rulesets/standards/native-evaluability-snapshot.json index d931dc710..aced010b3 100644 --- a/src/rulesets/standards/native-evaluability-snapshot.json +++ b/src/rulesets/standards/native-evaluability-snapshot.json @@ -3,7 +3,7 @@ "title": "Native-engine evaluability class per rule (snapshot)", "description": "Per-rule evaluability class as computed by the Core native evaluator triage. This is a GENERATED CAPTURE, not the source of truth: the authority is src/packages/core-domain/src/application/validators/rule-evaluability.ts and the handler set registered in native-evaluator.ts. It is recorded here so the ISO/IEC 5055 mapping can be scoped to the real handler backlog without src/rulesets depending on a package it does not own. Do not hand-edit — regenerate.", "version": "1.1.0", - "capturedOn": "2026-09-20", + "capturedOn": "2026-09-21", "capturedFrom": [ "src/packages/core-domain/src/application/validators/rule-evaluability.ts (RULE_TRIAGE, classifyRule, ADR_CONFORMANCE_CATEGORY)", "src/packages/core-domain/src/application/validators/evaluators/native-evaluator.ts (registered handler set)", @@ -11,15 +11,15 @@ "src/packages/core-domain/test/rule-corpus-triage.ts (corpus loader, classification and this renderer)" ], "regenerateWith": "node src/rulesets/standards/capture-native-evaluability-snapshot.mjs", - "validation": "Rendered by test/rule-corpus-triage.ts from the live triage, written by capture-native-evaluability-snapshot.mjs and pinned byte-for-byte by rule-corpus-triage.spec.ts, so a divergence between this file and Core is a failing test rather than silent drift (corpus 415; native-handler 171, documentation-only 138, unimplemented-native 21, needs-external-system 27, needs-runtime 23, needs-supplied-facts 31, underspecified 4).", + "validation": "Rendered by test/rule-corpus-triage.ts from the live triage, written by capture-native-evaluability-snapshot.mjs and pinned byte-for-byte by rule-corpus-triage.spec.ts, so a divergence between this file and Core is a failing test rather than silent drift (corpus 415; native-handler 173, documentation-only 138, unimplemented-native 14, needs-external-system 33, needs-runtime 22, needs-supplied-facts 31, underspecified 4).", "corpusSize": 415, "distinctRuleIds": 415, "counts": { - "native-handler": 171, + "native-handler": 173, "documentation-only": 138, - "unimplemented-native": 21, - "needs-external-system": 27, - "needs-runtime": 23, + "unimplemented-native": 14, + "needs-external-system": 33, + "needs-runtime": 22, "needs-supplied-facts": 31, "underspecified": 4 }, @@ -325,9 +325,9 @@ "MCP-03": "native-handler", "MCP-04": "native-handler", "MCP-05": "native-handler", - "OBS-EVD-01": "needs-runtime", - "OBS-EVD-02": "needs-runtime", - "OBS-EVD-03": "needs-external-system", + "OBS-EVD-01": "native-handler", + "OBS-EVD-02": "native-handler", + "OBS-EVD-03": "native-handler", "OBS-EVD-04": "needs-external-system", "DEP-01": "native-handler", "DEP-02": "native-handler", @@ -343,19 +343,19 @@ "QT-02": "native-handler", "QT-03": "native-handler", "QT-04": "native-handler", - "QT-05": "native-handler", + "QT-05": "needs-runtime", "QT-06": "native-handler", "QT-07": "native-handler", "QT-08": "native-handler", - "SEC-INJ-01": "unimplemented-native", - "SEC-INJ-02": "unimplemented-native", - "SEC-PATH-01": "unimplemented-native", - "SEC-PATH-02": "unimplemented-native", + "SEC-INJ-01": "needs-external-system", + "SEC-INJ-02": "needs-external-system", + "SEC-PATH-01": "needs-external-system", + "SEC-PATH-02": "needs-external-system", "SEC-RL-01": "native-handler", "SEC-RL-02": "native-handler", - "SEC-RL-03": "unimplemented-native", - "SEC-TIMING-01": "unimplemented-native", - "SEC-TIMING-02": "unimplemented-native", + "SEC-RL-03": "needs-external-system", + "SEC-TIMING-01": "needs-external-system", + "SEC-TIMING-02": "needs-external-system", "ISO5055-SEC": "unimplemented-native", "ISO5055-REL": "unimplemented-native", "ISO5055-PERF": "unimplemented-native",