diff --git a/.harness/scripts/ci/49-validate-gap-id-allocation.mjs b/.harness/scripts/ci/49-validate-gap-id-allocation.mjs index dc8c32ad..36fb6fda 100644 --- a/.harness/scripts/ci/49-validate-gap-id-allocation.mjs +++ b/.harness/scripts/ci/49-validate-gap-id-allocation.mjs @@ -36,6 +36,38 @@ * sides is either a collision or a deliberate retitle, and both deserve a human * looking at them. * + * ## GT-656 — declaring a retitle, because forbidding one was worse + * + * The first version had no way to say "this is a retitle, not a collision", so a + * required check turned red on any title change and the practical effect was that + * titles became IMMUTABLE. That is a real cost, not a theoretical one: `GT-622` + * was re-measured twice (82 -> 201 -> 210 analyses, and the branch it affects + * turned out to be `develop`, not `main`) while its headline went on saying + * "Eighty-two ... every PR", because correcting it would have blocked the PR. A + * board whose whole purpose is not lying accumulated rows whose first line lies. + * + * The fix is NOT to soften the check. It is to make the human judgement the guard + * was deferring to into DATA, so the guard can read it: + * + * reference/core/control-center/gaps/gap-retitles.json + * { "retitles": [ { id, from, to, declaredAt, reason } ] } + * + * A collision is exempt only if a declaration names the id and reproduces BOTH + * titles exactly. That exactness is the whole design: it cannot be written as a + * blanket "GT-622 may be retitled", so a genuine collision that later lands on the + * same id still fails, because its titles are not the two that were declared. + * + * Declarations are themselves checked, because an exemption registry that rots + * silently is a worse defect than the one it fixes: + * + * - active — reproduces a live collision -> exempts it + * - spent — the retitle has reached the base branch -> reported, not fatal + * - rot — describes NEITHER side -> FAILS + * + * and a registry that exists but cannot be parsed is a hard failure, never an + * empty list. "Stopped seeing anything and started reporting a pass" is the + * failure mode this corpus keeps finding; it must not be introduced here. + * * ## Anti-vacuous pass * * Zero ids parsed on either side is a hard failure: a moved file or a reshaped @@ -65,6 +97,13 @@ const REPO_ROOT = path.resolve(__dirname, '../../..'); /** The catalog is where a row's title lives; the board carries prose, not a title. */ export const CATALOG = 'reference/core/control-center/gaps/gap-reference-catalog.md'; +/** + * GT-656: where a deliberate retitle is declared, read from HEAD because the + * declaration lands with the change it describes. Absent file = no exemptions, + * which is the strict reading; an UNPARSEABLE file is a failure, not an empty one. + */ +export const RETITLES = 'reference/core/control-center/gaps/gap-retitles.json'; + /** * The base to compare against, in the order CI and a laptop actually mean it. * @@ -123,6 +162,82 @@ export function newlyAllocated(base, head) { return [...head.keys()].filter((id) => !base.has(id)).sort(); } +/** + * GT-656: validate the shape of a retitle declaration. + * + * Every field is load-bearing. `from`/`to` are what make the exemption specific + * to two exact titles rather than to an id; `reason` is the human judgement the + * guard is deferring to, and a declaration without one is an unexplained + * exemption, which is what this registry must never become. + * + * @param {unknown} entry + * @param {number} index + * @returns {string[]} problems, empty when the entry is well-formed + */ +export function validateRetitle(entry, index) { + const at = `retitles[${index}]`; + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [`${at} is not an object`]; + const problems = []; + if (!/^GT-\d+$/.test(entry.id ?? '')) problems.push(`${at}.id is not a GT id: ${JSON.stringify(entry.id)}`); + for (const field of ['from', 'to', 'reason']) { + if (typeof entry[field] !== 'string' || entry[field].trim() === '') { + problems.push(`${at}.${field} must be a non-empty string`); + } + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.declaredAt ?? '')) { + problems.push(`${at}.declaredAt must be YYYY-MM-DD`); + } + if (typeof entry.from === 'string' && entry.from === entry.to) { + problems.push(`${at} declares a retitle from a title to itself, which exempts nothing`); + } + return problems; +} + +/** + * GT-656: classify declarations against what the two branches actually say, and + * report which collisions they exempt. + * + * A declaration is `active` only when it reproduces BOTH sides of a live + * collision exactly. `spent` means the retitle already reached the base branch, + * so there is nothing left to exempt — reported so the pile is visible, but not + * fatal, because failing there would block every unrelated pull request. `rot` + * describes neither side and is fatal: an exemption that no longer matches + * reality is how a registry starts laundering things it was never shown. + * + * @param {Array<{id:string,from:string,to:string}>} declarations + * @param {Map} baseTitles + * @param {Map} headTitles + * @param {Array<{id:string,baseTitle:string,headTitle:string}>} collisions + */ +export function classifyRetitles(declarations, baseTitles, headTitles, collisions) { + const byKey = new Map(collisions.map((c) => [`${c.id}${c.baseTitle}${c.headTitle}`, c])); + const active = []; + const spent = []; + const rot = []; + + for (const d of declarations) { + const collision = byKey.get(`${d.id}${d.from}${d.to}`); + if (collision) { + active.push(d); + continue; + } + // Already merged into the base: both sides now carry the new title. + if (baseTitles.get(d.id) === d.to && headTitles.get(d.id) === d.to) { + spent.push(d); + continue; + } + rot.push({ + ...d, + baseTitle: baseTitles.get(d.id) ?? '(id absent from base)', + headTitle: headTitles.get(d.id) ?? '(id absent from HEAD)', + }); + } + + const exempted = new Set(active.map((d) => `${d.id}${d.from}${d.to}`)); + const unexplained = collisions.filter((c) => !exempted.has(`${c.id}${c.baseTitle}${c.headTitle}`)); + return { active, spent, rot, unexplained }; +} + // --------------------------------------------------------------------------- // I/O edges // --------------------------------------------------------------------------- @@ -133,6 +248,49 @@ function fail(lines) { process.exit(1); } +/** + * GT-656: read the declared retitles from HEAD. + * + * Absent is legitimate and means "no exemptions". Present-but-unreadable is NOT: + * a registry that fails to parse must stop the run, because the alternative is a + * guard that silently starts exempting nothing while reporting a pass — or, once + * someone corrects the parse error, silently exempting everything it names. + */ +function readRetitles(root) { + const file = path.join(root, RETITLES); + if (!fs.existsSync(file)) return []; + + let parsed; + try { + parsed = JSON.parse(fs.readFileSync(file, 'utf8')); + } catch (error) { + fail([ + `${RETITLES} exists but is not valid JSON: ${error.message}`, + 'An exemption registry that cannot be read must stop the run. Treating it as', + 'empty would report a pass over a file nobody could check.', + ]); + } + + if (!Array.isArray(parsed?.retitles)) { + fail([ + `${RETITLES} has no \`retitles\` array.`, + 'Expected: { "retitles": [ { id, from, to, declaredAt, reason } ] }', + ]); + } + + const problems = parsed.retitles.flatMap((entry, i) => validateRetitle(entry, i)); + if (problems.length) { + fail([ + `${problems.length} malformed retitle declaration(s) in ${RETITLES}:`, + ...problems.map((p) => ` • ${p}`), + '', + ' Every field is load-bearing: `from`/`to` scope the exemption to two exact', + ' titles instead of to an id, and `reason` is the judgement being deferred to.', + ]); + } + return parsed.retitles; +} + function readAtRef(root, ref, file) { try { return execFileSync('git', ['show', `${ref}:${file}`], { @@ -186,6 +344,10 @@ function main(argv) { const collisions = findIdCollisions(baseTitles, headTitles); const fresh = newlyAllocated(baseTitles, headTitles); + const declarations = readRetitles(root); + const { active, spent, rot, unexplained } = classifyRetitles( + declarations, baseTitles, headTitles, collisions, + ); console.log(`${GUARD} — one gap id, one gap`); console.log(` base ................ ${base}`); @@ -193,15 +355,38 @@ function main(argv) { console.log(` ids on HEAD ......... ${headTitles.size}`); console.log(` newly allocated ..... ${fresh.length}${fresh.length ? ` (${fresh.join(', ')})` : ''}`); console.log(` collisions .......... ${collisions.length}`); + console.log(` declared retitles ... ${active.length} active, ${spent.length} spent, ${rot.length} rot`); if (verbose && fresh.length) { for (const id of fresh) console.log(` · ${id} is new here — check it against every OTHER open branch, not just this base`); } + // Never silent: an exemption that nobody sees is indistinguishable from a hole. + for (const d of active) console.log(` · ${d.id} retitled by declaration (${d.declaredAt}): ${d.reason}`); + if (verbose) { + for (const d of spent) console.log(` · ${d.id} declaration is SPENT — the retitle reached ${base}; drop it from ${RETITLES}`); + } + + if (rot.length > 0) { + fail([ + `${rot.length} retitle declaration(s) describe neither side of the catalog:`, + ...rot.flatMap((d) => [ + ` • ${d.id} declared ${JSON.stringify(d.from)} -> ${JSON.stringify(d.to)}`, + ` on ${base}: ${d.baseTitle}`, + ` on HEAD: ${d.headTitle}`, + ]), + '', + ' A declaration must reproduce BOTH titles exactly, or it exempts something', + ' nobody described. Fix the two strings, or delete the entry if the retitle', + ' it covered is long merged.', + '', + ` Registry: ${RETITLES}`, + ]); + } - if (collisions.length > 0) { + if (unexplained.length > 0) { fail([ - `${collisions.length} gap id(s) name a DIFFERENT gap on each side:`, - ...collisions.flatMap((c) => [ + `${unexplained.length} gap id(s) name a DIFFERENT gap on each side:`, + ...unexplained.flatMap((c) => [ ` • ${c.id}`, ` on ${base}: ${c.baseTitle}`, ` on HEAD: ${c.headTitle}`, @@ -210,8 +395,16 @@ function main(argv) { ' Two sessions allocated the same number, or a row was retitled. If it is a', ' collision, renumber the NEWER row and update every place the id appears:', ' the board row, the catalog anchor, the closure-evidence record and the', - ' cross-references in BOTH languages. If it is a deliberate retitle, say so', - ' in the commit — this guard cannot tell the two apart, and should not guess.', + ' cross-references in BOTH languages. If it is a deliberate retitle, declare', + ' it — this guard cannot tell the two apart, and should not guess:', + '', + ` ${RETITLES}`, + ' { "retitles": [ { "id": "GT-NNN", "from": "",', + ' "to": "", "declaredAt": "YYYY-MM-DD",', + ' "reason": "why the old title was wrong" } ] }', + '', + ' Both titles must match exactly, so the exemption covers this retitle and', + ' not the id — a real collision landing on the same number still fails.', '', ' Allocate a new id from the UNION of ids across branches, never the maximum', ' on one:', @@ -220,7 +413,8 @@ function main(argv) { ]); } - console.log(`\n✓ ${GUARD}: every one of ${headTitles.size} id(s) names the same gap it names on ${base}.`); + const exempted = active.length ? `, ${active.length} retitle(s) declared and matched exactly` : ''; + console.log(`\n✓ ${GUARD}: every one of ${headTitles.size} id(s) names the same gap it names on ${base}${exempted}.`); return 0; } diff --git a/.harness/scripts/ci/49-validate-gap-id-allocation.test.mjs b/.harness/scripts/ci/49-validate-gap-id-allocation.test.mjs index 8d559fe4..9c8334aa 100644 --- a/.harness/scripts/ci/49-validate-gap-id-allocation.test.mjs +++ b/.harness/scripts/ci/49-validate-gap-id-allocation.test.mjs @@ -25,8 +25,11 @@ import { parseCatalogTitles, findIdCollisions, newlyAllocated, + validateRetitle, + classifyRetitles, defaultBase, CATALOG, + RETITLES, } from './49-validate-gap-id-allocation.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -102,8 +105,12 @@ describe('defaultBase', () => { // Integration, over real git history // --------------------------------------------------------------------------- -/** A repo with the catalog committed on `base`, then rewritten on the checked-out branch. */ -const repoWith = (name, baseEntries, headEntries) => { +/** + * A repo with the catalog committed on `base`, then rewritten on the checked-out + * branch. `retitles` is written on the branch only, which is where a declaration + * lands in practice — with the change it describes. + */ +const repoWith = (name, baseEntries, headEntries, retitles) => { const root = join(sandbox, name); mkdirSync(join(root, dirname(CATALOG)), { recursive: true }); const g = (...args) => execFileSync('git', args, { cwd: root, stdio: 'ignore' }); @@ -117,11 +124,31 @@ const repoWith = (name, baseEntries, headEntries) => { g('checkout', '-q', '-b', 'work'); writeFileSync(join(root, CATALOG), catalog(headEntries)); + if (retitles !== undefined) { + mkdirSync(join(root, dirname(RETITLES)), { recursive: true }); + writeFileSync(join(root, RETITLES), typeof retitles === 'string' ? retitles : JSON.stringify(retitles, null, 2)); + } g('add', '-A'); - g('commit', '-q', '-m', 'branch catalog'); + // `--allow-empty`: some fixtures deliberately leave the catalog identical, to + // check what happens with no collision at all. + g('commit', '-q', '--allow-empty', '-m', 'branch catalog'); return root; }; +/** The retitle this mechanism was built for: GT-622's headline was measurably wrong. */ +const GT622_OLD = 'Eighty-two orphaned code-scanning analyses keep every PR warning about a configuration that died in June'; +const GT622_NEW = 'Two hundred and ten orphaned code-scanning analyses keep PRs into develop warning about a configuration that died in June'; +const declaration = (over = {}) => ({ + retitles: [{ + id: 'GT-622', + from: GT622_OLD, + to: GT622_NEW, + declaredAt: '2026-08-08', + reason: 'Re-measured: 210 analyses, not 82, and the base still carrying the warning is develop, not every PR.', + ...over, + }], +}); + describe('the collision this guard exists for', () => { it('THE FIXTURE: two branches allocate GT-634 for different gaps — RED', () => { const root = repoWith( @@ -155,6 +182,116 @@ describe('the collision this guard exists for', () => { }); }); +// --------------------------------------------------------------------------- +// GT-656 — declaring a retitle +// --------------------------------------------------------------------------- + +describe('validateRetitle', () => { + const ok = { id: 'GT-622', from: 'a', to: 'b', declaredAt: '2026-08-08', reason: 'measured' }; + + it('accepts a complete declaration', () => { + assert.deepEqual(validateRetitle(ok, 0), []); + }); + + it('rejects a declaration with no reason — an unexplained exemption', () => { + assert.match(validateRetitle({ ...ok, reason: ' ' }, 0)[0], /reason must be a non-empty string/); + }); + + it('rejects a no-op declaration that would exempt nothing', () => { + assert.match(validateRetitle({ ...ok, to: 'a' }, 0)[0], /from a title to itself/); + }); + + it('rejects a missing or malformed date, and a non-GT id', () => { + assert.match(validateRetitle({ ...ok, declaredAt: 'ayer' }, 0)[0], /declaredAt must be YYYY-MM-DD/); + assert.match(validateRetitle({ ...ok, id: '622' }, 0)[0], /is not a GT id/); + }); +}); + +describe('classifyRetitles', () => { + const base = new Map([['GT-622', 'old']]); + const head = new Map([['GT-622', 'new']]); + const collisions = [{ id: 'GT-622', baseTitle: 'old', headTitle: 'new' }]; + const d = (over = {}) => ({ id: 'GT-622', from: 'old', to: 'new', ...over }); + + it('an exact declaration exempts its collision', () => { + const r = classifyRetitles([d()], base, head, collisions); + assert.equal(r.active.length, 1); + assert.deepEqual(r.unexplained, []); + }); + + it('THE ABUSE CASE: a declaration whose `from` is wrong exempts NOTHING', () => { + // Otherwise "GT-622 may be retitled" becomes a blanket pass on that id. + const r = classifyRetitles([d({ from: 'something else' })], base, head, collisions); + assert.equal(r.active.length, 0); + assert.equal(r.unexplained.length, 1); + assert.equal(r.rot.length, 1); + }); + + it('a real collision landing on an already-retitled id still fails', () => { + const later = [{ id: 'GT-622', baseTitle: 'new', headTitle: 'a different gap entirely' }]; + const r = classifyRetitles([d()], new Map([['GT-622', 'new']]), new Map([['GT-622', 'a different gap entirely']]), later); + assert.equal(r.unexplained.length, 1); + }); + + it('a declaration whose retitle reached the base is SPENT, not fatal', () => { + const merged = new Map([['GT-622', 'new']]); + const r = classifyRetitles([d()], merged, merged, []); + assert.equal(r.spent.length, 1); + assert.equal(r.rot.length, 0); + }); +}); + +describe('the retitle escape hatch, end to end', () => { + it('THE FIXTURE: a declared retitle is green, and says so out loud', () => { + const root = repoWith('retitle-ok', [['GT-622', GT622_OLD]], [['GT-622', GT622_NEW]], declaration()); + const { status, out } = run(root, ['--base', 'base']); + assert.equal(status, 0, out); + assert.match(out, /declared retitles \.+ 1 active/); + // An exemption nobody can see is indistinguishable from a hole. + assert.match(out, /GT-622 retitled by declaration \(2026-08-08\)/); + }); + + it('THE NEGATIVE TWIN: the same retitle WITHOUT a declaration is still RED', () => { + const root = repoWith('retitle-undeclared', [['GT-622', GT622_OLD]], [['GT-622', GT622_NEW]]); + const { status, out } = run(root, ['--base', 'base']); + assert.equal(status, 1, out); + assert.match(out, /name a DIFFERENT gap on each side/); + assert.match(out, /gap-retitles\.json/); + }); + + it('a declaration that quotes the wrong old title does NOT launder the change', () => { + const root = repoWith( + 'retitle-wrong-from', + [['GT-622', GT622_OLD]], + [['GT-622', GT622_NEW]], + declaration({ from: 'a title this catalog never carried' }), + ); + const { status, out } = run(root, ['--base', 'base']); + assert.equal(status, 1, out); + assert.match(out, /describe neither side of the catalog/); + }); + + it('an unreadable registry stops the run rather than reading as "no exemptions"', () => { + const root = repoWith('retitle-broken', [['GT-001', 'a']], [['GT-001', 'a']], '{ not json'); + const { status, out } = run(root, ['--base', 'base']); + assert.equal(status, 1, out); + assert.match(out, /is not valid JSON/); + assert.match(out, /Treating it as/); + }); + + it('a registry missing its array is a failure, not an empty list', () => { + const root = repoWith('retitle-shape', [['GT-001', 'a']], [['GT-001', 'a']], { entries: [] }); + const { status, out } = run(root, ['--base', 'base']); + assert.equal(status, 1, out); + assert.match(out, /has no `retitles` array/); + }); + + it('no registry at all means no exemptions, and a clean branch stays green', () => { + const root = repoWith('retitle-absent', [['GT-001', 'a']], [['GT-001', 'a']]); + assert.equal(run(root, ['--base', 'base']).status, 0); + }); +}); + describe('anti-vacuous floor', () => { it('refuses a tree where the catalog is not where it looks', () => { const root = join(sandbox, 'no-catalog'); diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index f6957310..84e6e5eb 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -641,7 +641,7 @@ "npm run test:e2e --workspace src/sdk/cli -- --runTestsByPath test/gate.e2e-spec.ts" ], "dependencyDisposition": "satisfied", - "dependencyRationale": "Builds on the ADR-0073 GateEvidence contract (GT-02/GT-03) and the deepened phase-gate content checks (GT-08\u2026GT-11)." + "dependencyRationale": "Builds on the ADR-0073 GateEvidence contract (GT-02/GT-03) and the deepened phase-gate content checks (GT-08…GT-11)." }, { "id": "GT-52", @@ -2693,7 +2693,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "none", - "closureNote": "Resolved by GT-184 \u2014 zero @ts-nocheck directives remain in the codebase" + "closureNote": "Resolved by GT-184 — zero @ts-nocheck directives remain in the codebase" }, { "id": "GT-187", @@ -2893,7 +2893,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "none", - "closureNote": "Resolved by file deletion \u2014 the files containing the Moscoww typo were removed in commit c4835e0db" + "closureNote": "Resolved by file deletion — the files containing the Moscoww typo were removed in commit c4835e0db" }, { "id": "GT-199", @@ -2920,7 +2920,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "none", - "closureNote": "Resolved by file deletion \u2014 server.ts was removed in commit c4835e0db" + "closureNote": "Resolved by file deletion — server.ts was removed in commit c4835e0db" }, { "id": "GT-201", @@ -2934,7 +2934,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "none", - "closureNote": "Resolved by file deletion \u2014 server.ts was removed in commit c4835e0db" + "closureNote": "Resolved by file deletion — server.ts was removed in commit c4835e0db" }, { "id": "GT-202", @@ -5191,7 +5191,7 @@ "node .harness/scripts/ci/09-reconcile-maturity.mjs --check" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "GT-341 (inventory reconciliation) satisfied; GT-357 reconciles board status against executed build/test evidence (closure-evidence + maturity), which is independent of GT-347\u2019s separate core-OPA-suite remediation." + "dependencyRationale": "GT-341 (inventory reconciliation) satisfied; GT-357 reconciles board status against executed build/test evidence (closure-evidence + maturity), which is independent of GT-347’s separate core-OPA-suite remediation." }, { "id": "GT-358", @@ -6078,7 +6078,7 @@ "src/packages/core-domain/src/evaluation/design-flow.e2e.spec.ts" ], "validationCommands": [ - "EPIC \u2014 all children GT-426..GT-433 DONE; E2E against the real corpus passes; tsc -b clean" + "EPIC — all children GT-426..GT-433 DONE; E2E against the real corpus passes; tsc -b clean" ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "GT-425 umbrella closed via F0-F8 (GT-426..GT-433). One follow-on remains outside the epic scope: CLI/MCP topology-recommend parity (task_0dfc5f71) mirroring the Core API endpoint for full BR-008 parity on the recommender." @@ -6144,7 +6144,7 @@ "npx jest --config src/packages/core-domain/jest.config.js --testPathPatterns evaluation (63 pass, 3 new design specs)" ], "dependencyDisposition": "none", - "dependencyRationale": "Advisory maturity evaluator (TS KindEvaluator); no new Native rule \u2192 no OPA parity twin required (R-25 N/A). Delivers the content/maturity evaluation capability behind the existence-only gate gaps." + "dependencyRationale": "Advisory maturity evaluator (TS KindEvaluator); no new Native rule → no OPA parity twin required (R-25 N/A). Delivers the content/maturity evaluation capability behind the existence-only gate gaps." }, { "id": "GT-430", @@ -6228,7 +6228,7 @@ "jest phase-artifact (6 pass incl. real-corpus E2E) + architecture.controller (10 pass)" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Delivered: spec.phaseProfiles in manifests + PhaseArtifactProfileService + Core API endpoint + phase-artifact-registry + E2E. Follow-on (task_1b27a217): unified 'phase-artifacts' EvaluationKind so it auto-exposes on CLI/MCP via evaluate(), for full BR-008 parity \u2014 mirrors the design evaluator; the capability is already exposed on the Core API (the Tracker's surface)." + "dependencyRationale": "Delivered: spec.phaseProfiles in manifests + PhaseArtifactProfileService + Core API endpoint + phase-artifact-registry + E2E. Follow-on (task_1b27a217): unified 'phase-artifacts' EvaluationKind so it auto-exposes on CLI/MCP via evaluate(), for full BR-008 parity — mirrors the design evaluator; the capability is already exposed on the Core API (the Tracker's surface)." }, { "id": "GT-436", @@ -6255,7 +6255,7 @@ ], "validationCommands": [ "ci-cd.yml parses as YAML and resolves to 3 docker-services matrix entries (core-api -> evolith-core-api, mcp-server -> evolith-mcp, agent-runtime -> evolith-agent-runtime) and 3 Coolify deploy steps; the referenced ./src/apps/agent-runtime-api/Dockerfile exists.", - "The gap's premise was partly stale: agent-runtime was already built/tested by ci-cd.yml (3 workspace builds). The real hole \u2014 absent from the docker-services matrix and from the deploy job on main (docker-images.yml only covered it on tags/workflow_dispatch) \u2014 is now closed; agent-runtime-api sits in the same CI/CD posture as its two sibling services." + "The gap's premise was partly stale: agent-runtime was already built/tested by ci-cd.yml (3 workspace builds). The real hole — absent from the docker-services matrix and from the deploy job on main (docker-images.yml only covered it on tags/workflow_dispatch) — is now closed; agent-runtime-api sits in the same CI/CD posture as its two sibling services." ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "Workflow wiring is complete and validated declaratively (YAML parse + Dockerfile path + parity with two already-working sibling services). The deploy step uses the established fail-soft contract (unset hook warns and exits 0, never failing CD). Provisioning COOLIFY_AGENTRUNTIME_DEPLOY_HOOK / COOLIFY_API_TOKEN is owner-gated and already tracked by GT-324; the first live build+deploy runs on the next merge to main, since this job is main/tag-only and cannot be exercised from develop." @@ -6274,7 +6274,7 @@ "validationCommands": [ "agent-runtime 131/131 (+8 workspace-context tests); agent-runtime-api 80/80 (+3 factory wiring tests); tsc --noEmit clean.", "The runtime assembles the workspace and passes it inline as evaluationInput.files (the OverlayFileSystem shape the Core API already consumes), so the stateless Core no longer evaluates an empty context; a workspace outage degrades to the prior flow (never fails a run).", - "LIVE-VERIFIED end-to-end in kind `evolith-cluster` (2026-07-18) \u2014 and the verification EXPOSED A REGRESSION in this very change, now fixed (`e361b52b`). The wiring fell back to AGENT_RUNTIME_WORKSPACE_ROOT, a pre-existing variable the service image hard-codes (`ENV AGENT_RUNTIME_WORKSPACE_ROOT=/repo/corpus`), so whole-corpus inlining was silently ON in every containerised deployment: the runtime inlined the entire bundled corpus into one evaluate request, the Core rejected the oversized body BEFORE its audit interceptor (so nothing appeared in core-api logs, making it look like the request never arrived), and the chain died on an opaque `Core evaluation failed: HTTP 500`. Inlining is now strictly opt-in via AGENT_RUNTIME_WORKSPACE_CONTEXT_ROOT only, with the old fallback spec inverted into a regression guard. A second, independent Core defect surfaced in the same hunt and was fixed (`6c3d78d3`): the `architecture` kind 500'd for every request because ArchitectureDriftService's history WRITE was unguarded while its READ already tolerated failure. After both fixes the governed chain completes: resolve-context \u2192 ground \u2192 select-capability \u2192 policy-preflight \u2192 harness-execute \u2192 core-evaluate \u2192 policy-validate \u2192 completed, returning the correct governed `blocked` verdict with 39 findings (was `exception`). All 13 evaluation kinds return 200." + "LIVE-VERIFIED end-to-end in kind `evolith-cluster` (2026-07-18) — and the verification EXPOSED A REGRESSION in this very change, now fixed (`e361b52b`). The wiring fell back to AGENT_RUNTIME_WORKSPACE_ROOT, a pre-existing variable the service image hard-codes (`ENV AGENT_RUNTIME_WORKSPACE_ROOT=/repo/corpus`), so whole-corpus inlining was silently ON in every containerised deployment: the runtime inlined the entire bundled corpus into one evaluate request, the Core rejected the oversized body BEFORE its audit interceptor (so nothing appeared in core-api logs, making it look like the request never arrived), and the chain died on an opaque `Core evaluation failed: HTTP 500`. Inlining is now strictly opt-in via AGENT_RUNTIME_WORKSPACE_CONTEXT_ROOT only, with the old fallback spec inverted into a regression guard. A second, independent Core defect surfaced in the same hunt and was fixed (`6c3d78d3`): the `architecture` kind 500'd for every request because ArchitectureDriftService's history WRITE was unguarded while its READ already tolerated failure. After both fixes the governed chain completes: resolve-context → ground → select-capability → policy-preflight → harness-execute → core-evaluate → policy-validate → completed, returning the correct governed `blocked` verdict with 39 findings (was `exception`). All 13 evaluation kinds return 200." ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "The HttpCoreEvaluationAdapter was already wired+verified in kind; this landed the last non-gated piece (real workspace-context assembly). Gated remainder stays out of scope by design: the real reasoning engine (GT-385/Hermes) and durable memory/scheduler needing external services; a Tracker-driven context assembler is a future sibling adapter behind the same IWorkspaceContextPort." @@ -6290,7 +6290,7 @@ "src/apps/agent-runtime-api/src/app.module.ts" ], "validationCommands": [ - "cd src/apps/agent-runtime-api && npx jest (33/33) \u2014 fail-closed auth (unset-key\u21d2denied, dev-bypass\u21d2allowed), JWT tenant-claim extraction, TenantCorpusGuard cross-tenant denial; both guards in APP_GUARD" + "cd src/apps/agent-runtime-api && npx jest (33/33) — fail-closed auth (unset-key⇒denied, dev-bypass⇒allowed), JWT tenant-claim extraction, TenantCorpusGuard cross-tenant denial; both guards in APP_GUARD" ], "dependencyDisposition": "none" }, @@ -6324,7 +6324,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs -> green with this record + the counter flip (557/568)." ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-441 extends GT-387 (the governed-executor approval port, done in a prior wave) and its receiving half is CD-23 on the Tracker board, shipped 2026-07-19 (RuntimeApprovalEndpoints + migration AddRuntimeApprovals) \u2014 both satisfied, so the Core-side TrackerApprovalHttpClient could finally be written against a real, agreed contract instead of a fabricated one (the exact reason it was deliberately left unimplemented before). The approve->grant path is intentionally NOT live-smoked here: granting requires a designated UMS approver (CD-31) and dev-bypass carries every permission but no authority, which the endpoint deliberately rejects ('permission is not authority'). That path is covered by the adapter unit test (approved->grant) and the Tracker's own RuntimeApprovalEndpointTests; the pending, idempotency and human-reject paths ARE live-smoked against the real Tracker." + "dependencyRationale": "GT-441 extends GT-387 (the governed-executor approval port, done in a prior wave) and its receiving half is CD-23 on the Tracker board, shipped 2026-07-19 (RuntimeApprovalEndpoints + migration AddRuntimeApprovals) — both satisfied, so the Core-side TrackerApprovalHttpClient could finally be written against a real, agreed contract instead of a fabricated one (the exact reason it was deliberately left unimplemented before). The approve->grant path is intentionally NOT live-smoked here: granting requires a designated UMS approver (CD-31) and dev-bypass carries every permission but no authority, which the endpoint deliberately rejects ('permission is not authority'). That path is covered by the adapter unit test (approved->grant) and the Tracker's own RuntimeApprovalEndpointTests; the pending, idempotency and human-reject paths ARE live-smoked against the real Tracker." }, { "id": "GT-442", @@ -6338,12 +6338,12 @@ "product/infra/helm/evolith-agent-runtime/values.yaml" ], "validationCommands": [ - "Secret wiring verified in all three charts: each takes credentials from a pre-created K8s Secret BY NAME, injected via secretKeyRef and gated on auth.existingSecretName \u2014 evolith-core-api -> core-api-auth/EVOLITH_API_KEY, evolith-mcp -> mcp-auth/EVOLITH_API_KEY, evolith-agent-runtime -> agent-runtime-auth/AGENT_RUNTIME_API_KEY; evolith-mcp additionally consumes opa-bundle-credentials and opa-bundle-signing-key by name. No chart embeds a literal credential.", + "Secret wiring verified in all three charts: each takes credentials from a pre-created K8s Secret BY NAME, injected via secretKeyRef and gated on auth.existingSecretName — evolith-core-api -> core-api-auth/EVOLITH_API_KEY, evolith-mcp -> mcp-auth/EVOLITH_API_KEY, evolith-agent-runtime -> agent-runtime-auth/AGENT_RUNTIME_API_KEY; evolith-mcp additionally consumes opa-bundle-credentials and opa-bundle-signing-key by name. No chart embeds a literal credential.", "DB-connectivity claim disproved against the code: core-api and agent-runtime-api declare ZERO database dependencies (no pg/typeorm/prisma/mongo driver or ORM) in their package.json. The `postgresql` occurrences in projects.controller.ts / core-domain.module.ts are the scaffolding generator selecting a database for the GENERATED project, not a Core runtime connection.", "Consolidated the previously scattered secret configuration into product/infra/README.md and README.es.md, section 'Secrets and Data Connectivity' / 'Secretos y Conectividad de Datos', including the kubectl create secret example and the Coolify encrypted-env-var equivalent." ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Both halves of the original framing were misaligned with the codebase. The secret store was substantially already wired (K8s Secrets by name via secretKeyRef across all three charts); what was genuinely missing was documentation, now consolidated bilingually. The DB half is NOT APPLICABLE to the Core: ADR-0101 makes it a stateless evaluation engine (EvaluationContext -> EvaluationResult, no persisted entities), so there is no DATABASE_URL to configure and adding one would contradict the ADR \u2014 persistence lives in the Tracker (own Postgres, tracker_governance), a separate repository whose board owns any DB-connectivity work. Remaining is owner-gated and not code: provisioning the real secret VALUES on the VPS/cluster, the same blocker already tracked by GT-324 and GT-437." + "dependencyRationale": "Both halves of the original framing were misaligned with the codebase. The secret store was substantially already wired (K8s Secrets by name via secretKeyRef across all three charts); what was genuinely missing was documentation, now consolidated bilingually. The DB half is NOT APPLICABLE to the Core: ADR-0101 makes it a stateless evaluation engine (EvaluationContext -> EvaluationResult, no persisted entities), so there is no DATABASE_URL to configure and adding one would contradict the ADR — persistence lives in the Tracker (own Postgres, tracker_governance), a separate repository whose board owns any DB-connectivity work. Remaining is owner-gated and not code: provisioning the real secret VALUES on the VPS/cluster, the same blocker already tracked by GT-324 and GT-437." }, { "id": "GT-445", @@ -6379,7 +6379,7 @@ "kind load docker-image evolith-tracker-web:local --name evolith-tracker; kubectl apply -f product/infra/kind/tracker-web.local.yaml (rollout 1/1) + port-forward svc/tracker-web 8088:80 -> the Evolith Tracker React SPA loads (HTTP 200, dev-auth as user AN / tenant root), the Control Tower renders governance KPIs from the live backend, and the nginx /api proxy reaches tracker-api -> real Core (/api/health 200). Objective-1 UI chain (tracker-web -> tracker-api -> core-api) verified in a browser" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Cross-repo (evolith_tracker): the M1 pilot closure \u2014 DB persistence + real Core evaluate() + Discovery gate E2E \u2014 was already met in the Tracker repo; this closes the last tail (verify the React UI in a browser) against the local Core chain. Broader per-phase UI coverage is follow-on. Core provides the stable contract (ADR-0101/0104)." + "dependencyRationale": "Cross-repo (evolith_tracker): the M1 pilot closure — DB persistence + real Core evaluate() + Discovery gate E2E — was already met in the Tracker repo; this closes the last tail (verify the React UI in a browser) against the local Core chain. Broader per-phase UI coverage is follow-on. Core provides the stable contract (ADR-0101/0104)." }, { "id": "GT-447", @@ -6441,8 +6441,8 @@ "src/packages/mcp-server/package.json" ], "validationCommands": [ - "Verified against the LIVE npm registry: a clean `npm i @beyondnet/evolith-cli@1.1.0` in an empty project resolves the full closure from npm (agent-runtime/core-domain/infra-providers/sdk all 1.1.0) and the CLI boots \u2014 `--version` -> 1.1.0, `evaluate --help` OK, `enforce --help` OK (exercises enforcement/policy-compiler, the subpath missing from the stale 1.0.0). cli dist-tag latest: 1.1.0.", - "check:release-drift hardened to fail the publish when the shipped dist imports any @beyondnet/* not declared in dependencies \u2014 proven: aborts without the infra-providers dep, passes with it. npm publish --dry-run green for all 8 packages." + "Verified against the LIVE npm registry: a clean `npm i @beyondnet/evolith-cli@1.1.0` in an empty project resolves the full closure from npm (agent-runtime/core-domain/infra-providers/sdk all 1.1.0) and the CLI boots — `--version` -> 1.1.0, `evaluate --help` OK, `enforce --help` OK (exercises enforcement/policy-compiler, the subpath missing from the stale 1.0.0). cli dist-tag latest: 1.1.0.", + "check:release-drift hardened to fail the publish when the shipped dist imports any @beyondnet/* not declared in dependencies — proven: aborts without the infra-providers dep, passes with it. npm publish --dry-run green for all 8 packages." ], "dependencyDisposition": "satisfied", "dependencyRationale": "The real defect was the whole @beyondnet/evolith-*@1.0.0 line being stale vs develop (CLI deep-imports subpaths absent from the 1.0.0 tarballs) plus cli@1.0.1 crashing on an undeclared infra-providers dep. Resolved by a coordinated 1.1.0 re-release of all 8 packages (owner published in dependency order); the owner-publish dependency is now satisfied and verified live." @@ -6562,7 +6562,7 @@ "src/packages/mcp-server/src/tools/api-catalog-parity.spec.ts" ], "validationCommands": [ - "cd src/packages/mcp-server && npx jest api-catalog-parity (3/3) \u2014 CLI catalog == live MCP registry (47 tools / 11 resources incl. GT-520 capabilities+contracts)" + "cd src/packages/mcp-server && npx jest api-catalog-parity (3/3) — CLI catalog == live MCP registry (47 tools / 11 resources incl. GT-520 capabilities+contracts)" ], "dependencyDisposition": "none" }, @@ -6594,7 +6594,7 @@ "rg -n \"Exchange|Queue|Binding|Permission\" reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.md reference/core/architecture/adrs/core/0108-masstransit-owned-message-topology.es.md product/infra/kubernetes/README.md" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Topology decision recorded (ADR-0108) and message-path CRDs retired in favor of per-product User/Permission RBAC. The G1 'consumer endpoint started' assertion (bus health, not pod Ready) is delegated to the G1 integration-gate automation (\u00a713) and tracked there \u2014 it is not a blocker for the topology decision." + "dependencyRationale": "Topology decision recorded (ADR-0108) and message-path CRDs retired in favor of per-product User/Permission RBAC. The G1 'consumer endpoint started' assertion (bus health, not pod Ready) is delegated to the G1 integration-gate automation (§13) and tracked there — it is not a blocker for the topology decision." }, { "id": "GT-463", @@ -6606,7 +6606,7 @@ "product/operations/incident-response-poison-message-reprocess.es.md" ], "validationCommands": [ - "ruby -ryaml -e 'YAML.load_file(...)' \u2014 3 groups; messaging-alerts: MassTransitPoisonMessages + TenantProjectionQueueMissing", + "ruby -ryaml -e 'YAML.load_file(...)' — 3 groups; messaging-alerts: MassTransitPoisonMessages + TenantProjectionQueueMissing", "grep '_error' product/operations/alerts/prometheus-alerts.yml" ], "dependencyDisposition": "none" @@ -6620,7 +6620,7 @@ "product/infra/kubernetes/README.md" ], "validationCommands": [ - "ruby -ryaml -e 'YAML.load_stream(...network-policies.yaml)' \u2014 22 NetworkPolicy docs parse", + "ruby -ryaml -e 'YAML.load_stream(...network-policies.yaml)' — 22 NetworkPolicy docs parse", "rg -n \"NetworkPolicy|CNI|disableDefaultCNI\" product/infra/kubernetes/README.md product/infra/helm/evolith-core-api/templates/networkpolicy.yaml" ], "dependencyDisposition": "deferred", @@ -6836,7 +6836,7 @@ "src/sdk/cli/templates/evolith.yaml.example" ], "validationCommands": [ - "npx jest --config test/jest-e2e.json --runInBand --no-bail (workspace src/sdk/cli) \u2014 18 suites / 132 tests pass" + "npx jest --config test/jest-e2e.json --runInBand --no-bail (workspace src/sdk/cli) — 18 suites / 132 tests pass" ], "closureNote": "mcp refs removed (fc28a318). Residual e2e reds were NOT fixture debt: the 98a20dca taxonomy split (reference/@root vs rulesets/@src) broke gate.e2e REPO_ROOT + gate-status reference resolution, and 5 validate/arch tests asserted an unattainable passed|warning against a bare fixture (94-rule corpus fails blocking MUSTs regardless of yaml). Test-only fixes: v1 fixtures (V1_MANIFEST), REPO_ROOT->repo root + schemas from src/rulesets/schema, validate/arch rewritten to the real ADR-0073 contract (envelope well-formed; exit reflects verdict), gate-status self-contained mock Core. No product-code change.", "dependencyDisposition": "deferred", @@ -7268,7 +7268,7 @@ "src/apps/core-api/src/presentation/controllers/capabilities.controller.ts" ], "validationCommands": [ - "cd src/packages/contracts && npx tsc -p tsconfig.json && npx jest (2 suites, 13/13) \u2014 @beyondnet/evolith-contracts SemVer + CONTRACT_SET_SHA256; parity spec binds package to live buildCapabilityManifest and FAILS on added-engine / single-consumer drift. GET /api/v1/capabilities served by CapabilitiesController (on develop)." + "cd src/packages/contracts && npx tsc -p tsconfig.json && npx jest (2 suites, 13/13) — @beyondnet/evolith-contracts SemVer + CONTRACT_SET_SHA256; parity spec binds package to live buildCapabilityManifest and FAILS on added-engine / single-consumer drift. GET /api/v1/capabilities served by CapabilitiesController (on develop)." ], "dependencyDisposition": "none" }, @@ -7348,7 +7348,7 @@ "src/packages/core-domain/src/evaluation/drift-gate.ts" ], "validationCommands": [ - "FileWaiverStore infra-providers 2/2 (persists+reloads across instances, retains versions, fails closed on corrupt file); CLI smoke green; LIVE: evolith waiver request/approve/revise persists v1,v2 to JSON across separate invocations; evolith evaluate --format drift on a violating workspace exits 1 and renders a PR-comment body (Blocked, 37 blocking violations, each citing rule + CODEOWNERS owner) \u2014 the mandated fallback meets the block+comment criterion; native GitHub-App Checks-API publish is a follow-on. SARIF/evidence criterion already done." + "FileWaiverStore infra-providers 2/2 (persists+reloads across instances, retains versions, fails closed on corrupt file); CLI smoke green; LIVE: evolith waiver request/approve/revise persists v1,v2 to JSON across separate invocations; evolith evaluate --format drift on a violating workspace exits 1 and renders a PR-comment body (Blocked, 37 blocking violations, each citing rule + CODEOWNERS owner) — the mandated fallback meets the block+comment criterion; native GitHub-App Checks-API publish is a follow-on. SARIF/evidence criterion already done." ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "The native GitHub/GitLab Checks-API publish needs a GitHub App with checks:write (owner infra); the mandated PrCommentFallbackPublisher + non-zero exit satisfy the block+comment requirement, verified live. The durable waiver store + CLI are complete." @@ -7369,7 +7369,7 @@ "Public seam verified: RulesetValidatorOptions.metrics forwarded into the enforcer subsystem factory and the metrics API (IEnforcerMetrics/NoopEnforcerMetrics/RecordingEnforcerMetrics/ENFORCER_METRICS) re-exported from core-domain, so a host can construct a real Meter-backed adapter (previously the seam was permanently no-op and un-wireable through any surface)." ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Parity wiring (processRunner injected in CLI/MCP/REST) and enforcer-catalog<->tool-catalog version pins were already live (f8310fac/4b6db4d5); this closed the remaining OTel-metrics seam. Ops-only remainder stays out of scope: per-runtime composable CI images + vuln-scan/Renovate, and activating a live OTel Meter-backed IEnforcerMetrics adapter inside each surface module (host config) \u2014 the code seam is complete and importable." + "dependencyRationale": "Parity wiring (processRunner injected in CLI/MCP/REST) and enforcer-catalog<->tool-catalog version pins were already live (f8310fac/4b6db4d5); this closed the remaining OTel-metrics seam. Ops-only remainder stays out of scope: per-runtime composable CI images + vuln-scan/Renovate, and activating a live OTel Meter-backed IEnforcerMetrics adapter inside each surface module (host config) — the code seam is complete and importable." }, { "id": "GT-520", @@ -7382,7 +7382,7 @@ "src/packages/mcp-server/src/mcp/mcp-server-auth.ts" ], "validationCommands": [ - "cd src/packages/mcp-server && npx jest oauth abac mcp-server-auth (5 suites, 35/35) + full 326/326 \u2014 all three ACs met: OAuth 2.1 resource-server validator (JWKS RS/PS/ES + shared HS, iss/aud/exp/nbf) wired into the Streamable HTTP bearer path (401 on missing/invalid/expired/spoofed); every tools/call ABAC-checked per verified identity + audited; evolith://capabilities and evolith://contracts resources served (resources.service.ts)" + "cd src/packages/mcp-server && npx jest oauth abac mcp-server-auth (5 suites, 35/35) + full 326/326 — all three ACs met: OAuth 2.1 resource-server validator (JWKS RS/PS/ES + shared HS, iss/aud/exp/nbf) wired into the Streamable HTTP bearer path (401 on missing/invalid/expired/spoofed); every tools/call ABAC-checked per verified identity + audited; evolith://capabilities and evolith://contracts resources served (resources.service.ts)" ], "dependencyDisposition": "none" }, @@ -7403,7 +7403,7 @@ "Security-SARIF (Checkov/Trivy) LIVE-verified: a real Checkov SARIF over a Terraform corpus (8 findings) parses via the shared GT-515 ingestSarif to 8 Violations, ALL category='security'; a clean scan yields 0." ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-514 (ShellEnforcerAdapter/IProcessRunner seam) + GT-515 (SARIF 2.1.0 ingester, reused wholesale by the security adapters) both DONE. Criterion #1's own gate held: import-linter (Python) + Checkov/Trivy (IaC/security) landed and were exercised against real corpora; Deptrac (PHP) / ArchUnit (JVM) / jQAssistant (Neo4j) correctly stay adapter-pending \u2014 no real repo of those runtimes exists to exercise them, so building them would be speculative and untestable per the gap's Risk/Evidence." + "dependencyRationale": "GT-514 (ShellEnforcerAdapter/IProcessRunner seam) + GT-515 (SARIF 2.1.0 ingester, reused wholesale by the security adapters) both DONE. Criterion #1's own gate held: import-linter (Python) + Checkov/Trivy (IaC/security) landed and were exercised against real corpora; Deptrac (PHP) / ArchUnit (JVM) / jQAssistant (Neo4j) correctly stay adapter-pending — no real repo of those runtimes exists to exercise them, so building them would be speculative and untestable per the gap's Risk/Evidence." }, { "id": "GT-523", @@ -7439,7 +7439,7 @@ "src/rulesets/enforcement/enforcer-catalog.json" ], "validationCommands": [ - "dotnet 10: scaffolded a real .NET corpus (Domain/Infrastructure/ArchTests) with NetArchTest.Rules 1.3.2; DOTNET_CLI_UI_LANGUAGE=en dotnet test \u2014 a Domain->Infrastructure violation FAILS the arch test and parseNetArchTestReport parses it into 1 canonical Violation (tool=NetArchTest, error, locationless); the CLEAN corpus (no violation) PASSES and parses to 0 violations. 0 false positives on a real .NET corpus." + "dotnet 10: scaffolded a real .NET corpus (Domain/Infrastructure/ArchTests) with NetArchTest.Rules 1.3.2; DOTNET_CLI_UI_LANGUAGE=en dotnet test — a Domain->Infrastructure violation FAILS the arch test and parseNetArchTestReport parses it into 1 canonical Violation (tool=NetArchTest, error, locationless); the CLEAN corpus (no violation) PASSES and parses to 0 violations. 0 false positives on a real .NET corpus." ], "dependencyDisposition": "satisfied", "dependencyRationale": "GT-512 restored-.NET-checkout requirement met: a real dotnet restore+build+test runs NetArchTest against a compiled corpus; the adapter parses real failures 1:1 and a clean corpus yields 0." @@ -7473,7 +7473,7 @@ "src/sdk/cli/docs/edit-time-enforcement.md" ], "validationCommands": [ - "cd src/sdk/cli && npm run build && npx jest (71 suites, 967/967; edit-hook+enforce 45/45) \u2014 `evolith enforce edit` blocks a boundary-violating edit (exit 2) with canonical Violation; conforming edit exit 0; cross-agent VendorHookAdapter registry" + "cd src/sdk/cli && npm run build && npx jest (71 suites, 967/967; edit-hook+enforce 45/45) — `evolith enforce edit` blocks a boundary-violating edit (exit 2) with canonical Violation; conforming edit exit 0; cross-agent VendorHookAdapter registry" ], "dependencyDisposition": "none" }, @@ -7507,7 +7507,7 @@ "cd src/packages/core-domain && npx jest --config jest.config.js --runInBand (950/950 core-domain)" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "GT-528 required an executable C4 model: compileC4ToBoundaryRules (prior) + parseStructurizrDsl (this) close the intent\u2192executable path end-to-end. GT-516 (enforce: compiler) is a sibling engine, satisfied." + "dependencyRationale": "GT-528 required an executable C4 model: compileC4ToBoundaryRules (prior) + parseStructurizrDsl (this) close the intent→executable path end-to-end. GT-516 (enforce: compiler) is a sibling engine, satisfied." }, { "id": "GT-529", @@ -7548,7 +7548,7 @@ "src/packages/core-domain/src/evaluation/evaluation-orchestrator.spec.ts" ], "validationCommands": [ - "cd src/packages/core-domain && npx tsc && npx jest (95 suites, 966/966) \u2014 evaluate() folds ctx.qualitySignals via resolveEvidenceSignals onto EvaluationResult.qualitySignals; WITH evidence \u21d2 present signal surfaced, verdict PASS; WITHOUT \u21d2 no-evidence signal, evaluation still succeeds. Core stays provider-free (grep-clean)." + "cd src/packages/core-domain && npx tsc && npx jest (95 suites, 966/966) — evaluate() folds ctx.qualitySignals via resolveEvidenceSignals onto EvaluationResult.qualitySignals; WITH evidence ⇒ present signal surfaced, verdict PASS; WITHOUT ⇒ no-evidence signal, evaluation still succeeds. Core stays provider-free (grep-clean)." ], "dependencyDisposition": "none" }, @@ -7562,7 +7562,7 @@ "reference/core/architecture/adrs/core/0113-nodejs-lighthouse-evidence-adapter.md" ], "validationCommands": [ - "cd src/packages/infra-providers && npx tsc && npx jest (13 suites, 105/105) \u2014 LighthouseEvidenceProvider emits deterministic canonical Evidence with full provenance; categories\u2192EvidenceFinding severity; stubbed LHR (no Chrome)" + "cd src/packages/infra-providers && npx tsc && npx jest (13 suites, 105/105) — LighthouseEvidenceProvider emits deterministic canonical Evidence with full provenance; categories→EvidenceFinding severity; stubbed LHR (no Chrome)" ], "dependencyDisposition": "none" }, @@ -7577,7 +7577,7 @@ "src/packages/agent-runtime/src/adapters/skills/default-skills.ts" ], "validationCommands": [ - "cd src/packages/agent-runtime && npx tsc && npx jest (16 suites, 110/110) \u2014 rubric ranked by severity; StructuralReviewProvider emits probabilistic Evidence + provenance and registers in the GT-533 registry; evaluateStructuralGate blocks high/critical; GT-424 skill-registry parity green" + "cd src/packages/agent-runtime && npx tsc && npx jest (16 suites, 110/110) — rubric ranked by severity; StructuralReviewProvider emits probabilistic Evidence + provenance and registers in the GT-533 registry; evaluateStructuralGate blocks high/critical; GT-424 skill-registry parity green" ], "dependencyDisposition": "none" }, @@ -7619,7 +7619,7 @@ "reference/core/control-center/maturity-reports/maturity-assessment.md" ], "validationCommands": [ - "cd src/packages/agent-runtime && npx tsc && npx jest (17 suites, 118/118) + cd src/apps/agent-runtime-api && npx nest build && npx jest (5 suites, 67/67) \u2014 PgVectorKnowledgeAdapter cosine top-k over rag_chunks with injected embedder+pg seams, ranked KnowledgeChunk + citation; runtime.factory selects pgvector via env, in-memory default preserved" + "cd src/packages/agent-runtime && npx tsc && npx jest (17 suites, 118/118) + cd src/apps/agent-runtime-api && npx nest build && npx jest (5 suites, 67/67) — PgVectorKnowledgeAdapter cosine top-k over rag_chunks with injected embedder+pg seams, ranked KnowledgeChunk + citation; runtime.factory selects pgvector via env, in-memory default preserved" ], "dependencyDisposition": "none" }, @@ -7636,7 +7636,7 @@ "agent-runtime 123/123 (+2 grounding tests): a run with a seeded corpus records trace.groundedBy.corpusVersion + citations (queried BEFORE recommending), and an empty corpus grounds to empty without blocking. A reference/**/*.md commit runs 14-rag-index-sync.mjs (rag-index-sync.yml) which detects the 2 changed files and emits a delta receipt (verified). pgvector adapter now returns rag_chunks.corpus_version on the chunk." ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-538 (durable pgvector store) + GT-539 (real embedder) + GT-540 (read adapter) all DONE \u2014 the workflow drives the durable delta sync and the runtime grounds against the read adapter, citing the corpus_version those produce." + "dependencyRationale": "GT-538 (durable pgvector store) + GT-539 (real embedder) + GT-540 (read adapter) all DONE — the workflow drives the durable delta sync and the runtime grounds against the read adapter, citing the corpus_version those produce." }, { "id": "GT-542", @@ -7649,7 +7649,7 @@ "src/apps/core-api/src/presentation/controllers/gates.controller.ts" ], "validationCommands": [ - "cd src/apps/core-api && npx tsc -p tsconfig.json --noEmit && npx jest metrics (3 suites, 10/10) + full 150/150 \u2014 recordGateEvaluation emits evolith_gate_evaluations_total{status,gateId,phase} + evolith_gate_evaluation_duration_seconds, wired into EvaluationController (inline/canonical/legacy) and GatesController; spec asserts the increment + duration bucket appear in /metrics output" + "cd src/apps/core-api && npx tsc -p tsconfig.json --noEmit && npx jest metrics (3 suites, 10/10) + full 150/150 — recordGateEvaluation emits evolith_gate_evaluations_total{status,gateId,phase} + evolith_gate_evaluation_duration_seconds, wired into EvaluationController (inline/canonical/legacy) and GatesController; spec asserts the increment + duration bucket appear in /metrics output" ], "dependencyDisposition": "none" }, @@ -7665,7 +7665,7 @@ "src/apps/core-api/src/infrastructure/metrics/metric-drift.spec.ts" ], "validationCommands": [ - "cd src/apps/core-api && npx jest metrics metric-drift (10/10) \u2014 added evolith_http_request_duration_seconds histogram (recorded from the SecurityAudit interceptor) and reconciled prometheus-alerts.yml + core-api-slo.md/.es.md from bare http_requests_total / http_request_duration_seconds_bucket to the evolith_-prefixed emitted names; the GT-550 drift guard asserts every referenced metric is emitted" + "cd src/apps/core-api && npx jest metrics metric-drift (10/10) — added evolith_http_request_duration_seconds histogram (recorded from the SecurityAudit interceptor) and reconciled prometheus-alerts.yml + core-api-slo.md/.es.md from bare http_requests_total / http_request_duration_seconds_bucket to the evolith_-prefixed emitted names; the GT-550 drift guard asserts every referenced metric is emitted" ], "dependencyDisposition": "satisfied", "dependencyRationale": "GT-542 (gate metric emitted) and GT-550 (drift guard) both landed in the same commit; the histogram + name reconciliation are verified by the guard." @@ -7681,10 +7681,10 @@ "product/operations/grafana/provisioning/datasources/datasources.yml" ], "validationCommands": [ - "docker compose -f docker-compose.evolith.yml -f docker-compose.observability.yml up -d; curl -su admin:admin http://localhost:3009/api/search?type=dash-db -> BOTH \"Evolith \u2014 Governance Health\" and \"Evolith \u2014 Platform SRE (RED)\" provisioned; the Prometheus datasource (uid evolith-prometheus) resolves and panels render live gate/HTTP/agent series" + "docker compose -f docker-compose.evolith.yml -f docker-compose.observability.yml up -d; curl -su admin:admin http://localhost:3009/api/search?type=dash-db -> BOTH \"Evolith — Governance Health\" and \"Evolith — Platform SRE (RED)\" provisioned; the Prometheus datasource (uid evolith-prometheus) resolves and panels render live gate/HTTP/agent series" ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-542/543 (metrics emitted) and GT-545 (scrape) all DONE, so the datasource + dashboards render real series \u2014 verified live." + "dependencyRationale": "GT-542/543 (metrics emitted) and GT-545 (scrape) all DONE, so the datasource + dashboards render real series — verified live." }, { "id": "GT-545", @@ -7699,7 +7699,7 @@ "curl http://localhost:9090/api/v1/targets -> core-api, mcp, agent-runtime all health=up (scraped on :3000 with the API-key Bearer credential); fixed the canonical otel/prometheus-config.yml (was a single wrong bff:8000 target) to the same 3 jobs" ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-547 (runnable stack) and GT-549 (guarded /metrics) DONE; the scrape authenticates against the fail-closed /metrics and all three targets are up \u2014 verified live." + "dependencyRationale": "GT-547 (runnable stack) and GT-549 (guarded /metrics) DONE; the scrape authenticates against the fail-closed /metrics and all three targets are up — verified live." }, { "id": "GT-546", @@ -7712,7 +7712,7 @@ "src/apps/agent-runtime-api/src/agent-runtime/agent-runtime.module.ts" ], "validationCommands": [ - "cd src/apps/agent-runtime-api && npx tsc -p tsconfig.json --noEmit && npx jest (6 suites, 72/72) \u2014 AgentMetricsService emits evolith_agent_runs_total{engine,verdict}, evolith_agent_run_duration_seconds, evolith_skill_invocations_total{skill}, evolith_agent_core_calls_total{outcome}, evolith_hitl_approvals_total{decision} on the default registry (so the existing /metrics exposes them); recorded from the AgentRuntimeResult via the controller; spec asserts each series" + "cd src/apps/agent-runtime-api && npx tsc -p tsconfig.json --noEmit && npx jest (6 suites, 72/72) — AgentMetricsService emits evolith_agent_runs_total{engine,verdict}, evolith_agent_run_duration_seconds, evolith_skill_invocations_total{skill}, evolith_agent_core_calls_total{outcome}, evolith_hitl_approvals_total{decision} on the default registry (so the existing /metrics exposes them); recorded from the AgentRuntimeResult via the controller; spec asserts each series" ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "The catalog named a GT-519 noop metrics port to hang these on, but no such port exists in the agent-runtime package. Instrumented at the API/adapter boundary instead (per ADR-0102, metrics stay out of the domain), which satisfies both acceptance criteria without GT-519." @@ -7745,7 +7745,7 @@ "core-api 152/152 (+2 bounded-tenant tests) + agent-runtime 77/77; LIVE against the local stack rebuilt with EVOLITH_METRICS_TENANT_ALLOWLIST=t-acme,local: evolith_gate_evaluations_total carries tenant=t-acme/local for allowlisted tenants and tenant=other for the rest (3 t-secret requests collapsed to other, the raw id never leaked); Governance Health dashboard has a $tenant template variable + a per-tenant panel; cap (|allowlist|+1, hard-capped 100) documented in code + SLO doc." ], "dependencyDisposition": "satisfied", - "dependencyRationale": "GT-542 (gate metric) and GT-546 (agent metric) DONE \u2014 the tenant label extends both; the per-tenant panel needs the Grafana datasource from GT-544 (DONE)." + "dependencyRationale": "GT-542 (gate metric) and GT-546 (agent metric) DONE — the tenant label extends both; the per-tenant panel needs the Grafana datasource from GT-544 (DONE)." }, { "id": "GT-549", @@ -7758,7 +7758,7 @@ "src/packages/mcp-server/src/mcp/mcp-server.service.ts" ], "validationCommands": [ - "cd src/packages/mcp-server && npx tsc && npx jest (44 suites, 326/326 \u2014 +2 HTTP tests: anon GET /metrics\u2192401, x-api-key\u2192200) + cd src/apps/agent-runtime-api && npx tsc && npx jest (7 suites, 77/77 \u2014 +5 MetricsAuthGuard tests) \u2014 /metrics now fail-closed on both services with a trusted-network opt-out, matching core-api MetricsAuthGuard (GT-393)" + "cd src/packages/mcp-server && npx tsc && npx jest (44 suites, 326/326 — +2 HTTP tests: anon GET /metrics→401, x-api-key→200) + cd src/apps/agent-runtime-api && npx tsc && npx jest (7 suites, 77/77 — +5 MetricsAuthGuard tests) — /metrics now fail-closed on both services with a trusted-network opt-out, matching core-api MetricsAuthGuard (GT-393)" ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "Catalog lists GT-545 (scrape config) as a dependency; the acceptance criterion \"scraper still succeeds with credentials\" is proven at the code/test level (the guard accepts the credential and rejects anonymous callers). Wiring the actual Prometheus scrape job with the credential is GT-545 (infra)." @@ -7773,7 +7773,7 @@ "product/operations/slo/core-api-slo.md" ], "validationCommands": [ - "cd src/apps/core-api && npx jest metric-drift (2/2) \u2014 the guard extracts PromQL metric names from prometheus-alerts.yml + core-api-slo.md/.es.md and fails when a non-external (not node/kube/rabbitmq/opa/up) metric is emitted by no Core service; runs in the Test core-api CI job on every PR touching those files" + "cd src/apps/core-api && npx jest metric-drift (2/2) — the guard extracts PromQL metric names from prometheus-alerts.yml + core-api-slo.md/.es.md and fails when a non-external (not node/kube/rabbitmq/opa/up) metric is emitted by no Core service; runs in the Test core-api CI job on every PR touching those files" ], "dependencyDisposition": "satisfied", "dependencyRationale": "GT-543 reconciled the metric names in the same commit, so the guard is green while enforcing no future drift." @@ -7788,11 +7788,11 @@ "product/infra/helm/evolith-mcp/templates/service.yaml" ], "validationCommands": [ - "Empirical metric discovery against the pinned OPA binary (.harness/bin/opa): `opa run --server` + GET /metrics exposes ONLY go_*, process_* and http_request_duration_seconds{code,handler,method} \u2014 ZERO opa_* series, so the pre-existing `opa_evaluation_errors_total` expression could never fire (false coverage).", + "Empirical metric discovery against the pinned OPA binary (.harness/bin/opa): `opa run --server` + GET /metrics exposes ONLY go_*, process_* and http_request_duration_seconds{code,handler,method} — ZERO opa_* series, so the pre-existing `opa_evaluation_errors_total` expression could never fire (false coverage).", "Alert selector verified against live series: handler=\"v1/data\" is emitted (so handler=~\"v1/data.*\" matches) and the `code` label tracks HTTP status (200 and 400 observed), so code=~\"5..\" matches real 5xx policy-decision failures.", - "helm template product/infra/helm/evolith-mcp \u2014 Service renders the `opa-metrics` port (targetPort opa-http) with opa.enabled=true and omits it with opa.enabled=false (1 vs 0 occurrences); helm lint passes (0 charts failed).", + "helm template product/infra/helm/evolith-mcp — Service renders the `opa-metrics` port (targetPort opa-http) with opa.enabled=true and omits it with opa.enabled=false (1 vs 0 occurrences); helm lint passes (0 charts failed).", "YAML parse green for prometheus-alerts.yml and prometheus-config.yml; scrape jobs resolve to core-api, mcp, agent-runtime, opa, otel-collector, prometheus.", - "LIVE-VERIFIED in a kind cluster (`evolith-cluster`, namespace evolith-local, 2026-07-18): `helm upgrade --set opa.enabled=true` made the REAL k8s Service object gain `opa-metrics 8181 -> targetPort opa-http`, and the pod came up with the `opa` sidecar container. Fetching http://:8181/metrics from another pod returned HTTP 200 with 109 metrics, ZERO opa_* series, and `http_request_duration_seconds` present \u2014 confirming in a deployed cluster what the binary probe showed, and discharging the earlier deploy-gated caveat for the exposure half. A real 5xx sample was observed (http_request_duration_seconds_count{code=\"500\",handler=\"health\"}), proving the alert's code=~\"5..\" selector matches live series; it is correctly scoped to handler=~\"v1/data.*\" so a health-probe 500 is not mistaken for a policy-decision failure." + "LIVE-VERIFIED in a kind cluster (`evolith-cluster`, namespace evolith-local, 2026-07-18): `helm upgrade --set opa.enabled=true` made the REAL k8s Service object gain `opa-metrics 8181 -> targetPort opa-http`, and the pod came up with the `opa` sidecar container. Fetching http://:8181/metrics from another pod returned HTTP 200 with 109 metrics, ZERO opa_* series, and `http_request_duration_seconds` present — confirming in a deployed cluster what the binary probe showed, and discharging the earlier deploy-gated caveat for the exposure half. A real 5xx sample was observed (http_request_duration_seconds_count{code=\"500\",handler=\"health\"}), proving the alert's code=~\"5..\" selector matches live series; it is correctly scoped to handler=~\"v1/data.*\" so a health-probe 500 is not mistaken for a policy-decision failure." ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "Both halves the gap names are closed: the sidecar is now EXPOSED (Service opa-metrics port; it was pod-local and unreachable) and SCRAPED (new `opa` job whose name matches the alert scope). A third, deeper defect was found and fixed: the alert referenced a metric OPA does not emit, so scraping alone would not have made it fire. The exposure half is now LIVE-VERIFIED in a kind cluster (see validationCommands); what remains deploy-gated is only an actual Prometheus instance scraping it end-to-end; promtool was unavailable locally so the rule was validated by YAML parse + empirical series matching instead of `promtool check rules`; and the docker-compose stack does not run OPA yet, tracked by GT-547." @@ -8148,7 +8148,7 @@ "Falsification: the new spec run against a pristine `git archive HEAD` copy of core-domain fails 12 of 17 (errored->skipped, undefined counters, missing advisories, no threshold issue)" ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Closes the REPORTING half only, by design: the envelope now publishes checked/skipped/errored/total and an exception is no longer laundered into skipped. Widening the corpus itself (the ~240 rules with no native handler) is a separate, far larger body of work and stays open \u2014 this record must not be read as coverage having improved." + "dependencyRationale": "Closes the REPORTING half only, by design: the envelope now publishes checked/skipped/errored/total and an exception is no longer laundered into skipped. Widening the corpus itself (the ~240 rules with no native handler) is a separate, far larger body of work and stays open — this record must not be read as coverage having improved." }, { "id": "GT-570", @@ -8164,13 +8164,13 @@ ], "validationCommands": [ "gh run view 30273290417 -> conclusion=success, workflow_dispatch on main @ 9a82d9f2 with dry_run=false.", - "curl https://registry.npmjs.org/@beyondnet%2Fevolith-mcp -> latest 1.2.0, published 2026-07-27T14:06:39Z, dist.attestations present. Same shape for evolith-agent-runtime@1.2.0 (14:06:10), evolith-sdk@2.0.0 (14:06:24) and evolith-cli@1.2.0 (14:06:58) \u2014 timestamps confirm dependency order.", + "curl https://registry.npmjs.org/@beyondnet%2Fevolith-mcp -> latest 1.2.0, published 2026-07-27T14:06:39Z, dist.attestations present. Same shape for evolith-agent-runtime@1.2.0 (14:06:10), evolith-sdk@2.0.0 (14:06:24) and evolith-cli@1.2.0 (14:06:58) — timestamps confirm dependency order.", "The audit finding \"0 of 8 published packages carry dist.attestations\" no longer holds: four now do, produced by GitHub Actions with id-token: write rather than by a developer machine.", - "Scope corrected before shipping: `git show --name-only` (not --stat, which truncates long paths) showed the security wave touches mcp (15 files), cli (7) and agent-runtime (1, the CWE-78 shell runner) \u2014 the original plan of bumping mcp alone would have left two vulnerable.", + "Scope corrected before shipping: `git show --name-only` (not --stat, which truncates long paths) showed the security wave touches mcp (15 files), cli (7) and agent-runtime (1, the CWE-78 shell runner) — the original plan of bumping mcp alone would have left two vulnerable.", "Rehearsals: run 30265298045 found no LICENSE in any of the four tarballs and repository.url auto-corrected in all eight; run 30271837941 found bin paths auto-corrected in two. All three fixed before publishing and all three would have been permanent." ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "Re-scoped rather than over-claimed. Two of the three original acceptance criteria are NOT met by this closure and were moved to GT-624: deprecating the 1.1.0 versions (needs npm credentials, owner action) and a release gate that fails when a security-tagged commit is absent from the published tag. This row closes on what it was about \u2014 an installable artifact older than its own published security fixes \u2014 and the remainder is tracked, not absorbed." + "dependencyRationale": "Re-scoped rather than over-claimed. Two of the three original acceptance criteria are NOT met by this closure and were moved to GT-624: deprecating the 1.1.0 versions (needs npm credentials, owner action) and a release gate that fails when a security-tagged commit is absent from the published tag. This row closes on what it was about — an installable artifact older than its own published security fixes — and the remainder is tracked, not absorbed." }, { "id": "GT-571", @@ -8256,7 +8256,7 @@ "validationCommands": [ "agentic-ai-self-conformance.spec.ts -> 13/13, evaluating the repository against the nine blocking AAI-* rules it ships.", "NEGATIVE FIXTURE: without agent.config.json all nine evaluate to failed.", - "CAVEAT recorded on the row: the spec runs in Test agent-runtime, which is NOT one of main\u2019s seven required contexts (verified against the live branch protection). It reports; it does not govern. Making it required is an owner action." + "CAVEAT recorded on the row: the spec runs in Test agent-runtime, which is NOT one of main’s seven required contexts (verified against the live branch protection). It reports; it does not govern. Making it required is an owner action." ], "closedAt": "2026-07-28", "closureCommit": "b16bd9fd", @@ -8290,12 +8290,12 @@ ], "validationCommands": [ "gh run view 30228607519 -> branch=develop, conclusion=success; jobs \"Action vs non-conforming satellite\" and \"Action contract (recorded envelopes)\" both success.", - "gh run view 30228607519 --log -> \"OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.\" \u2014 the counter is non-zero AND cross-checks against the report file the action writes, which is what the broken '.summary.violations' jq path made impossible.", - "gh run view 30228607519 --log -> \"OK: the gate blocked as expected with 34 blocking violation(s).\" \u2014 the negative half: fail-on-violation=true must fail the step.", + "gh run view 30228607519 --log -> \"OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.\" — the counter is non-zero AND cross-checks against the report file the action writes, which is what the broken '.summary.violations' jq path made impossible.", + "gh run view 30228607519 --log -> \"OK: the gate blocked as expected with 34 blocking violation(s).\" — the negative half: fail-on-violation=true must fail the step.", "gh run list --workflow=evolith-validate-dogfood.yml --limit 5 -> 5 consecutive success." ], "dependencyDisposition": "accepted-scope", - "dependencyRationale": "The action is dogfooded and under regression, which is what this gap was about. Making the workflow a REQUIRED check is branch-protection configuration, not a code change, and is deliberately left outside this closure \u2014 a gate nobody requires is how this action rotted in the first place, so it is worth doing, just not here." + "dependencyRationale": "The action is dogfooded and under regression, which is what this gap was about. Making the workflow a REQUIRED check is branch-protection configuration, not a code change, and is deliberately left outside this closure — a gate nobody requires is how this action rotted in the first place, so it is worth doing, just not here." }, { "id": "GT-579", @@ -8725,7 +8725,7 @@ "45-validate-port-inventory-honesty --verbose -> 19 ports declared, 11 on the hot path (7 required + 4 optional), 8 declared-not-reached, 53 adapters.", "Before the fix the guard exits 1 against master-view.svg: \"publishes a port/adapter count without saying how many are actually reached\".", "node --test .harness/scripts/ci/45-validate-port-inventory-honesty.test.mjs -> 9/9, including four anti-vacuous fixtures (zero ports, unparseable deps, zero adapters, missing scan corpus).", - "Row evidence corrected: 17/49/9 as registered vs 19/53/11 measured; the diagnostic\u2019s \"two interaction adapters with no callers\" is six." + "Row evidence corrected: 17/49/9 as registered vs 19/53/11 measured; the diagnostic’s \"two interaction adapters with no callers\" is six." ], "dependencyDisposition": "none" }, @@ -8843,7 +8843,7 @@ ".harness/scripts/ci/08-validate-tracking.test.mjs" ], "validationCommands": [ - "08-validate-tracking.mjs -> \"Acceptance-criteria audit: 47 non-DONE GT row(s) checked against 605/605 EN/ES catalog sections \u2014 0 with no criteria at all.\" exit 0.", + "08-validate-tracking.mjs -> \"Acceptance-criteria audit: 47 non-DONE GT row(s) checked against 605/605 EN/ES catalog sections — 0 with no criteria at all.\" exit 0.", "It exited 1 on its first run against the real board, naming GT-435, GT-444 and GT-448; criteria were written for all three rather than exempting them.", "node --test .harness/scripts/ci/08-validate-tracking.test.mjs -> 17/17, including the negative fixture and the vacuity floor." ], @@ -8891,7 +8891,7 @@ "43-validate-guard-negative-fixtures OBSERVED it red: 36 of 36 guards turned red against the empty fixture. 42 reports 11 self-guarded, 39 scanning, 0 PENDING.", "core-domain 1367/1367, mcp-server 434/434, core-api 179/179, CLI unit 1436/1436, all four tsc projects clean.", "Wired into docs.yml, which backs the required Validate documentation check.", - "The guard's first CI run went RED with 8 paths that pass locally: dist/, .harness/evidence and the gitignored policy.wasm exist on a developer machine and in no clean checkout. As written it would have demanded build output be committed. Corrected to check ANCHORING, not existence \u2014 a missing segment may be generated, but its parent must be real \u2014 and re-verified against a tracked-files-only `git archive` export. Proven not to have blinded it: reintroducing the original P0 (`src/rulesets/opa/policy.wasm`) and the ADR straggler in that clean export turns it red again, because the failure is at `rulesets`, not at the generated leaf. node --test now 14/14." + "The guard's first CI run went RED with 8 paths that pass locally: dist/, .harness/evidence and the gitignored policy.wasm exist on a developer machine and in no clean checkout. As written it would have demanded build output be committed. Corrected to check ANCHORING, not existence — a missing segment may be generated, but its parent must be real — and re-verified against a tracked-files-only `git archive` export. Proven not to have blinded it: reintroducing the original P0 (`src/rulesets/opa/policy.wasm`) and the ADR straggler in that clean export turns it red again, because the failure is at `rulesets`, not at the generated leaf. node --test now 14/14." ], "dependencyDisposition": "none" }, @@ -9638,7 +9638,7 @@ "node .harness/scripts/ci/08-validate-tracking.mjs" ], "dependencyDisposition": "satisfied", - "dependencyRationale": "The persistent half \u2014 the evidence_edges table, its backfill and the depth-bounded endpoint \u2014 is owned by beyondnetcode/evolith_tracker, exactly as EVIDENCE_EDGE_STORAGE_CONTRACT declares. It landed there as PRs #89, #90 and #91 and is verified against a real Postgres 16. This record lists only files resolvable in THIS repository, per the standard; the satellite evidence is cited in the catalog section." + "dependencyRationale": "The persistent half — the evidence_edges table, its backfill and the depth-bounded endpoint — is owned by beyondnetcode/evolith_tracker, exactly as EVIDENCE_EDGE_STORAGE_CONTRACT declares. It landed there as PRs #89, #90 and #91 and is verified against a real Postgres 16. This record lists only files resolvable in THIS repository, per the standard; the satellite evidence is cited in the catalog section." }, { "id": "GT-532", @@ -9650,7 +9650,7 @@ ], "note": "Closed on its two acceptance criteria, not on its title. The marketplace adapters the title names were never in the criteria and did not ship; they are registered as GT-651 so the closure does not bury them.", "dependencyDisposition": "accepted-scope", - "dependencyRationale": "The row's two acceptance criteria are met and its TITLE names a third element \u2014 marketplace-style adapters \u2014 that no criterion ever asked for and that did not ship. Closing on the criteria is the standard this board applies to epics ('an epic closes when its children do, never on its own narrative'), and the unshipped element is carved out as GT-651 rather than left implicit inside a DONE row. The implementing files live in beyondnetcode/evolith_tracker (PR #115); this record cites them with a repository prefix and the catalog section carries the reasoning. The implementing code is not resolvable here by design: every file lives in beyondnetcode/evolith_tracker (PR #115) \u2014 GovernancePackage.cs, the export/apply handlers, GovernancePackageEndpoints.cs and 13 tests \u2014 and the portfolio half is TowerMd3.tsx, which pre-existed. This record therefore cites only in-repository paths, as GT-605's does for the same reason.", + "dependencyRationale": "The row's two acceptance criteria are met and its TITLE names a third element — marketplace-style adapters — that no criterion ever asked for and that did not ship. Closing on the criteria is the standard this board applies to epics ('an epic closes when its children do, never on its own narrative'), and the unshipped element is carved out as GT-651 rather than left implicit inside a DONE row. The implementing files live in beyondnetcode/evolith_tracker (PR #115); this record cites them with a repository prefix and the catalog section carries the reasoning. The implementing code is not resolvable here by design: every file lives in beyondnetcode/evolith_tracker (PR #115) — GovernancePackage.cs, the export/apply handlers, GovernancePackageEndpoints.cs and 13 tests — and the portfolio half is TowerMd3.tsx, which pre-existed. This record therefore cites only in-repository paths, as GT-605's does for the same reason.", "validationCommands": [ "node .harness/scripts/ci/08-validate-tracking.mjs" ] @@ -9712,7 +9712,7 @@ "gh api repos/beyondnetcode/evolith_arch32/branches/main/protection --jq '.required_status_checks.contexts'" ], "dependencyDisposition": "satisfied", - "dependencyRationale": "The licence dependency was removed rather than satisfied: only the gitleaks-action wrapper required an org licence, so installing the MIT binary closed the Dependabot blind spot without any secret. The remaining dependency was ordering \u2014 the required-context promotion had to follow a green run on both protected branches, or every open pull request would have deadlocked as in PR #218. Both runs are recorded (develop 30867424760, main 30868306980) and the promotion was read back from the API: 8 required contexts on each branch, up from 7, with enforce_admins true." + "dependencyRationale": "The licence dependency was removed rather than satisfied: only the gitleaks-action wrapper required an org licence, so installing the MIT binary closed the Dependabot blind spot without any secret. The remaining dependency was ordering — the required-context promotion had to follow a green run on both protected branches, or every open pull request would have deadlocked as in PR #218. Both runs are recorded (develop 30867424760, main 30868306980) and the promotion was read back from the API: 8 required contexts on each branch, up from 7, with enforce_admins true." }, { "id": "GT-654", @@ -9746,6 +9746,39 @@ ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "Three of the four are bound and invoked on all three surfaces (executed 48 -> 51, invocations 66 -> 75). satellite-create stays unbound with its reason written in bindings.ts: it provisions a live GitHub repository and writes the local registry, so binding it as-is would have CI create real repositories on every run. It needs an undoable effect or a trustworthy dry-run and has neither, so uncoveredTriangleOps keeps listing it rather than the exemption being silent." + }, + { + "id": "GT-622", + "closedAt": "2026-08-08", + "closureCommit": "88d278ee", + "evidence": [ + "reference/core/control-center/gaps/gap-tracking.md", + "reference/core/control-center/gaps/gap-reference-catalog.md" + ], + "validationCommands": [ + "gh api 'repos/beyondnetcode/evolith_arch32/code-scanning/analyses?ref=refs/heads/main&per_page=100' --paginate --jq '.[].analysis_key' | sort -u -> no '.github/workflows/ci.yml:codeql'; same for ref=refs/heads/develop. Both keys were present before (201 and 9).", + "gh pr checks 440 -> 'CodeQL pass', and the check-run output title is 'No new alerts in code changed by this pull request', not '1 configuration not found'. PR #440 targets develop, which is the only base that still carried the warning.", + "gh api repos/beyondnetcode/evolith_arch32/branches/main/protection --jq '.required_status_checks.contexts' -> contains 'CodeQL SAST'; same for develop. The cleanup did not touch the scanning that works.", + "gh api 'repos/beyondnetcode/evolith_arch32/code-scanning/alerts?per_page=100' --paginate --jq '.[].state' | sort | uniq -c -> 242 open / 82 dismissed / 60 fixed, identical before and after the 210 deletions." + ], + "dependencyDisposition": "accepted-scope", + "dependencyRationale": "22 analyses under the dead key are left on refs/pull/{4..17}/merge. Those are ephemeral per-PR refs from May 2026: they can never be the base of a pull request, so they cannot produce the missing-configuration warning this gap was opened for. Deleting them would buy no observable change and cost 22 more irreversible calls against code-scanning history. The exemption is recorded rather than silent, and the deletions that were performed were scoped to refs/heads/main and refs/heads/develop only." + }, + { + "id": "GT-656", + "closedAt": "2026-08-08", + "closureCommit": "4ecd1ff0", + "evidence": [ + ".harness/scripts/ci/49-validate-gap-id-allocation.mjs", + ".harness/scripts/ci/49-validate-gap-id-allocation.test.mjs", + "reference/core/control-center/gaps/gap-retitles.json" + ], + "validationCommands": [ + "node --test .harness/scripts/ci/49-validate-gap-id-allocation.test.mjs", + "node .harness/scripts/ci/43-validate-guard-negative-fixtures.mjs" + ], + "dependencyDisposition": "accepted-scope", + "dependencyRationale": "The mechanism reads the ENGLISH catalog only, because that is the document the guard has always parsed and the id/title pair it protects lives there. A Spanish-only title change is therefore not covered, which is deliberate rather than overlooked: 04-check-bilingual-parity is the control for EN/ES divergence, and duplicating title comparison here would put two guards on one invariant. The declaration for GT-622 states the English titles; the Spanish title was corrected in the same change. The two branch comparisons that prove the retitle lands green (--base origin/main and --base origin/develop) are recorded in the catalog section as prose, NOT here: this registry is EXECUTED by 41-validate-evidence-commands, and a remote ref is not resolvable in the runner checkout — recording them here made the Governance guards job red, which is the correct verdict on an unrunnable command rather than a nuisance." } ] } 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 fc619d3c..3ba5934d 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7994,17 +7994,17 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre #### GT-622 -**Título:** Ochenta y dos análisis huérfanos de code scanning hacen que cada PR avise de una configuración muerta en junio +**Título:** Doscientos diez análisis huérfanos de code scanning hacen que los PR contra `develop` avisen de una configuración muerta en junio - **Propósito:** Que ningún PR arrastre un check rojo permanente que no describe un defecto — y que el rojo vuelva a significar algo. -- **Evidencia:** **Cada pull request arrastra un check `CodeQL` en rojo que dice "1 configuration not found", y no es un hallazgo de seguridad — es contabilidad huérfana.** **RE-MEDIDO EL 2026-08-01, y las cifras de la fila estaban mal en ambas direcciones.** GitHub conserva **201** análisis de code scanning en `refs/heads/main` bajo la clave `.github/workflows/ci.yml:codeql` — no 82 — que van del **2026-05-13 al 2026-06-01**, así que el último es del 1 de junio y no del 6. La cifra importa para la limpieza (201 llamadas DELETE, no 82) y la fecha importa porque es la evidencia de que nada produce esa clave desde hace dos meses: `ci.yml` ya no existe en `.github/workflows/`. **Cómo estuvo a punto de repetirse el error:** la primera consulta devolvió solo la página más reciente y mostró la clave ausente del todo, lo que se lee como «ya resuelto». No lo está — las huérfanas son más viejas que una página. Toda re-comprobación de esta fila DEBE paginar. Como la configuración sigue *registrada* en `main` pero nada la produce, GitHub la reporta como ausente en cada PR. Lleva así 51 días. Verificado con paginación completa: `code-scanning/analyses?ref=refs/heads/main` devuelve tres claves — `ci.yml:codeql` (**201**, último **2026-06-01**), `sdk-cli-ci.yml:codeql-analysis` (**193**, al día) y `sdk-cli-ci.yml:trivy-scan` (**193**, al día). El escaneo que importa está sano; `CodeQL SAST` pasa y es check requerido. **El workflow muerto ya está eliminado** (corría un job `Disabled` que sólo hacía `echo`, en cada PR y push a `main` y `develop`); borrar el fichero NO limpia la configuración registrada, y por eso existe esta fila. **La acción restante queda deliberadamente sin automatizar:** eliminar los **201** análisis vía `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}?confirm_delete=true` es irreversible y destruye historial de code scanning de una rama protegida. Su valor histórico es nulo —describen una configuración muerta desde junio— pero tirar historial de escaneo de seguridad es decisión del dueño, no de la herramienta. **Por qué importa más allá del ruido:** un check permanentemente rojo enseña a los revisores a descontar los checks rojos, y `CodeQL SAST` —que comparte el nombre CodeQL y SÍ es requerido— es precisamente el check que nadie se puede permitir aprender a ignorar. +- **Evidencia:** **Cada pull request arrastra un check `CodeQL` en rojo que dice "1 configuration not found", y no es un hallazgo de seguridad — es contabilidad huérfana.** **RE-MEDIDO EL 2026-08-01, y las cifras de la fila estaban mal en ambas direcciones.** GitHub conserva **201** análisis de code scanning en `refs/heads/main` bajo la clave `.github/workflows/ci.yml:codeql` — no 82 — que van del **2026-05-13 al 2026-06-01**, así que el último es del 1 de junio y no del 6. La cifra importa para la limpieza (201 llamadas DELETE, no 82) y la fecha importa porque es la evidencia de que nada produce esa clave desde hace dos meses: `ci.yml` ya no existe en `.github/workflows/`. **Cómo estuvo a punto de repetirse el error:** la primera consulta devolvió solo la página más reciente y mostró la clave ausente del todo, lo que se lee como «ya resuelto». No lo está — las huérfanas son más viejas que una página. Toda re-comprobación de esta fila DEBE paginar. Como la configuración sigue *registrada* en `main` pero nada la produce, GitHub la reporta como ausente en cada PR. Lleva así 51 días. Verificado con paginación completa: `code-scanning/analyses?ref=refs/heads/main` devuelve tres claves — `ci.yml:codeql` (**201**, último **2026-06-01**), `sdk-cli-ci.yml:codeql-analysis` (**193**, al día) y `sdk-cli-ci.yml:trivy-scan` (**193**, al día). El escaneo que importa está sano; `CodeQL SAST` pasa y es check requerido. **El workflow muerto ya está eliminado** (corría un job `Disabled` que sólo hacía `echo`, en cada PR y push a `main` y `develop`); borrar el fichero NO limpia la configuración registrada, y por eso existe esta fila. **La acción restante queda deliberadamente sin automatizar:** eliminar los **201** análisis vía `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}?confirm_delete=true` es irreversible y destruye historial de code scanning de una rama protegida. Su valor histórico es nulo —describen una configuración muerta desde junio— pero tirar historial de escaneo de seguridad es decisión del dueño, no de la herramienta. **Por qué importa más allá del ruido:** un check permanentemente rojo enseña a los revisores a descontar los checks rojos, y `CodeQL SAST` —que comparte el nombre CodeQL y SÍ es requerido— es precisamente el check que nadie se puede permitir aprender a ignorar. **CERRADO EL 2026-08-08, sobre evidencia producida por el propio PR que trae el cierre y no sobre el supuesto de que aparecería — y medir primero cambió tanto qué se borró como por qué era seguro.** Tres afirmaciones de esta fila no sobrevivieron a la re-medición, y la fila ya se había re-medido dos veces sin que ninguna saliera: cada re-medición comprobó el RECUENTO y ninguna comprobó la AFIRMACIÓN. **(1) No estaba en rojo.** La conclusión del check-run era `neutral` —`gh pr checks` lo pinta como `skipping`— desde el PR #250 (2026-07-28); solo el PR #217 llegó a ser `failure`. El argumento central de la fila, que un check permanentemente ROJO enseña a los revisores a descontar los rojos, describe un estado que terminó once días después de escribirla. Un check gris neutro es un daño mucho más débil que el que la fila argumentaba, y el argumento nunca se revisó. **(2) La rama estaba mal.** El propio resumen del aviso nombra la ref: *"1 configuration present on `refs/heads/develop` was not found"*. Solo los PR basados en `develop` seguían arrastrándolo (#425, #427, #429, #431, #433, todos del 2026-08-04); los basados en `main` salían limpios desde el #420 (2026-08-04 04:38Z) **con los 201 huérfanos todavía puestos**, a cuatro minutos del #433 que seguía avisando en `develop`. `main` dejó de avisar sin borrado alguno, lo que refuta el modelo causal de la fila ("sigue *registrado* en `main`, luego se reporta en cada PR") y significa que GitHub retira la expectativa por su cuenta mediante un mecanismo que esta fila nunca identificó. De no haberlo medido, al borrado se le habría atribuido un arreglo que ya estaba ocurriendo solo. **(3) Los 9 análisis de `develop` nunca se contaron** — el total real era **210**, no 201, y los 9 que más importaban eran justo los que quedaban fuera del alcance de la fila. **Lo que abarató la decisión del dueño, y ninguna pasada anterior lo había medido:** los 210 tienen `results_count: 0`. Ni uno contiene un hallazgo; son 210 registros de "escaneé y no encontré nada" de una configuración muerta desde junio. La irreversibilidad era real, la pérdida no, y ese único campo es lo que convirtió un juicio de valor en una decisión fácil. **Ejecutado:** 210 × `DELETE /code-scanning/analyses/{id}?confirm_delete=true`, cada uno precedido de un GET que verifica `analysis_key` y `results_count == 0` contra una lista de ids construida de antemano, para que ningún error de orden pudiera alcanzar un análisis de las claves vivas — 210 borrados, 0 rechazados, 0 errores. **Dejado a propósito:** sobreviven 22 análisis de la clave muerta en `refs/pull/{4..17}/merge`, refs efímeras por PR de mayo que jamás pueden ser base de un PR y no producen síntoma; borrarlas no compra nada y cuesta 22 llamadas irreversibles más. **El título está CORREGIDO, y llegar ahí necesitó un segundo gap.** Dijo "Ochenta y dos" durante dos re-mediciones, porque `49-validate-gap-id-allocation` leía cualquier cambio de título contra la base como posible colisión de id y no tenía canal para un retítulo deliberado — así que corregirlo ponía en rojo un check requerido en el propio PR que traía la corrección, y la re-medición del 2026-08-01 ya había sentado el precedente de arreglar la evidencia y dejar el titular. En vez de seguir ese precedente por tercera vez, el guard creció uno: [`GT-656`](./gap-reference-catalog.es.md#gt-656) declara el retítulo como dato, reproduciendo ambos títulos exactamente, para que esta corrección aterrice con el check de colisiones intacto. - **Componente:** `Infra` · **Criticidad:** P2 · **Complejidad:** XS - **Principal:** `XS` · **Interés:** `MED` · **Base:** `estimate` - **Procedencia:** Diagnosticado el 2026-07-27 al revisar por qué `CodeQL` salía rojo en el PR #217. La parte reversible (borrar el workflow muerto `.github/workflows/ci.yml`) se hizo en ese mismo commit; la irreversible se registra aquí en vez de ejecutarse. - **Criterios de aceptación:** - - [ ] `gh pr checks` sobre un PR nuevo no muestra ningún check `CodeQL` reportando "configuration not found". - - [ ] Las únicas claves de análisis en `refs/heads/main` son las dos que produce `sdk-cli-ci.yml`. - - [ ] `CodeQL SAST` sigue pasando y sigue siendo check requerido — la limpieza no debe tocar el escaneo que funciona. + - [x] `gh pr checks` sobre un PR nuevo **contra `develop`** no muestra ningún check `CodeQL` reportando "configuration not found" — `develop`, no `main`, porque es la ref que nombra el aviso y la única base que aún lo arrastraba. **Observado en vivo en el PR #440**, abierto contra `develop` justo para esto: `gh pr checks 440` devuelve `CodeQL pass` y el título del check-run es `No new alerts in code changed by this pull request`, donde los cinco PR contra `develop` del 2026-08-04 devolvían `neutral` / `1 configuration not found`. El check reportó 105s después de abrir el PR; el cierre se escribió tras leerlo, no antes. + - [x] **Reformulado, porque la redacción original quedó incomprobable:** exigía que las únicas claves en `refs/heads/main` fueran las dos de `sdk-cli-ci.yml`, pero desde entonces apareció una tercera legítima (`openssf-scorecard.yml:analysis`). La prueba es la AUSENCIA de `.github/workflows/ci.yml:codeql` — verificada en **ambas** ramas: `refs/heads/main` (201 borrados, quedan `sdk-cli-ci.yml:codeql-analysis` 200, `sdk-cli-ci.yml:trivy-scan` 200, `openssf-scorecard.yml:analysis` 9) y `refs/heads/develop` (9 borrados, quedan 280 / 280), la segunda de las cuales esta fila nunca siguió. + - [x] `CodeQL SAST` sigue `REQUIRED` en `main` y en `develop` (releído de branch protection después del borrado) y verde en sus últimas 5 corridas. El escaneo que funciona quedó demostrablemente intacto: los alerts son idénticos antes y después —242 abiertos / 82 descartados / 60 corregidos, y por herramienta CodeQL 75 / Scorecard 158 / Trivy 9— porque los 210 análisis borrados tenían todos `results_count: 0`. **RE-MEDIDO EL 2026-08-03. La cifra de la fila —201— ES LA BUENA; quien se equivocó fue la medición del 2026-08-03.** Aquella contó 395 filtrando por `test("ci.yml")`, y `sdk-cli-ci.yml` CONTIENE esa cadena: sumó las dos configuraciones en una. El desglose real sobre `refs/heads/main`, por clave exacta: @@ -8406,3 +8406,23 @@ La lección es la del propio tablero y esta vez la pagó quien medía: un `conta - [x] **Los veredictos no cambian, verificado en vez de argumentado.** Se capturó el `EvaluationResult` completo del payload de smoke de k6 desde una build de `origin/develop` y desde este cambio con los datos de fase de [`GT-649`](./gap-reference-catalog.es.md#gt-649) retenidos, y los dos son **idénticos byte a byte, 442 204 bytes**. Aparte, el propio corpus normalizado se comparó contra el loader previo al cambio: **393 reglas, JSON idéntico**. - [x] Medido después: el corpus carga una vez en arranque (393 reglas, 181–590 ms), la mediana de evaluate baja 52,7 → 38,2 ms, y un `/health` emitido 5 ms dentro de un evaluate en vuelo baja **11,6 → 3,5 ms**. El bloque de WARN por petición desaparece del log por completo. - [x] Re-medido en un runner de CI y no en una laptop, contra el `k6-smoke.json` de una corrida Reliability posterior, para confirmar que la cola de 498 ms desapareció donde se observó. **CERRADO el 2026-08-01 con evidencia de runner:** la corrida Reliability `30680099586`, job `k6 load profile (thresholds + published metrics)`, head `6fc31587`, publicó `k6-smoke.json`, `k6-average.json` y `core-api.log`. `RulesetCorpusWarmupService` cargó el corpus una sola vez al arrancar — 393 reglas en 75 ms — y el bloque de WARN por petición está ausente (`Skipping non-standard ruleset`, `Phases directory not found` y `WARN undefined` todos en 0). El artefacto smoke reporta 20 requests / 50 checks / 0 fallos, `health_latency` p99 **4,24 ms** y máximo **4,33 ms**, mientras `evaluate_latency` tiene mediana **64,1 ms** y p95 **66,9 ms**. El perfil average más largo confirma la misma forma sobre 1.143 requests: `health_latency` p99 **4,87 ms**, máximo **7,11 ms**, y sin reaparición de la cola de 498 ms en health allí donde se observó originalmente. + +#### GT-656 + +**Título:** Un guard contra colisiones de id volvió inmutables los títulos de los gaps + +- **Propósito / Problema:** Permitir que el titular de una fila se corrija cuando se mide equivocado, sin debilitar el check que impide que dos sesiones asignen un mismo número a dos gaps distintos. +- **Evidencia:** `49-validate-gap-id-allocation` compara cada `**Title:**` del catálogo contra la rama base y falla ante cualquier diferencia. El título es el discriminador correcto —evidencia, estado y criterios cambian en una rama, y solo el título dice QUÉ gap nombra el número—, pero el guard trataba un retítulo deliberado y una colisión como el mismo evento, y no tenía canal para la diferencia. Su propio texto de fallo lo admitía y pedía la distinción "en el commit"; nada lee mensajes de commit, y un squash merge los reescribe, así que en la práctica la respuesta siempre era "renumera o déjalo mal". +- **Qué significa:** los títulos eran inmutables de hecho. Un tablero que existe para impedir que la deuda técnica se describa de forma inexacta acumuló filas cuya PRIMERA LÍNEA era inexacta, que es la línea que ve quien lee. +- **El coste ya estaba pagado.** [`GT-622`](./gap-reference-catalog.es.md#gt-622) se re-midió dos veces —82 → 201 → 210 análisis huérfanos, y la rama que aún arrastraba el aviso resultó ser `develop`, no `main`— mientras su titular seguía diciendo "Ochenta y dos ... cada PR". Su evidencia se corrigió el 2026-08-01 y otra vez el 2026-08-08; el título no, las dos veces porque corregirlo pone en rojo un check REQUERIDO en el propio PR que trae la corrección. La primera vez sentó el precedente en silencio; la segunda encontró el precedente y estaba a punto de seguirlo. +- **Componente:** `.harness` · **Criticidad:** P2 · **Complejidad:** S +- **Principal:** `S` · **Interés:** `MED` · **Base:** `estimate` +- **Procedencia:** Encontrado el 2026-08-08 al cerrar [`GT-622`](./gap-reference-catalog.es.md#gt-622), cuando corregir el título medible­mente equivocado de esa fila puso en rojo `Validate documentation`. Registrado en vez de sorteado: dejar el título mal por tercera vez habría convertido el rodeo en la convención. +- **Criterios de aceptación:** + - [x] Un retítulo deliberado puede aterrizar sin debilitar el check de colisiones — declarado en `reference/core/control-center/gaps/gap-retitles.json` como `{ id, from, to, declaredAt, reason }`, y eximiendo solo cuando AMBOS títulos coinciden exactamente con los de las dos ramas. + - [x] La exención cubre un retítulo, nunca un id. Cubierto por `classifyRetitles` y por el fixture *"una colisión real que caiga sobre un id ya retitulado sigue fallando"*: en cuanto los títulos declarados no son los dos de las ramas, la colisión se reporta otra vez. + - [x] Una declaración no puede pudrirse hasta convertirse en un pase silencioso. `active` exime, `spent` se reporta (el retítulo llegó a la base, no queda nada que eximir), `rot` —que no describe ninguno de los dos lados— FALLA, igual que un registro que existe pero no se puede parsear o no tiene array `retitles`. + - [x] Toda exención se imprime, nunca se aplica en silencio: `declared retitles ... N active, N spent, N rot` más una línea por declaración activa con su razón. + - [x] El gemelo negativo se observa en rojo, no se supone: el mismo retítulo SIN declaración sigue fallando, y una declaración que cita un título que el catálogo nunca llevó no lo blanquea. `node --test` 27/27. +- **Primer uso:** el título de GT-622 se corrige en el mismo cambio que construyó el mecanismo —82 → 210, "cada PR" → "los PR contra `develop`"— con la declaración cargando el porqué. Esa es la prueba del propio cierre: el guard sale verde con el retítulo aplicado contra `origin/main` y contra `origin/develop`. +- **Estado:** `COMPLETADO` (2026-08-08) diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index f3865383..9f17dceb 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -8089,17 +8089,17 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo #### GT-622 -**Title:** Eighty-two orphaned code-scanning analyses keep every PR warning about a configuration that died in June +**Title:** Two hundred and ten orphaned code-scanning analyses keep PRs into `develop` warning about a configuration that died in June - **Purpose:** Stop every PR carrying a permanently red check that describes no defect — and make red mean something again. -- **Evidence:** **Every pull request carries a red `CodeQL` check reading "1 configuration not found", and it is not a security finding — it is orphaned bookkeeping.** **RE-MEASURED 2026-08-01, and the row's figures were wrong in both directions.** GitHub holds **201** code-scanning analyses on `refs/heads/main` under the analysis key `.github/workflows/ci.yml:codeql` — not 82 — spanning **2026-05-13 to 2026-06-01**, so the last one is 2026-06-01 and not 2026-06-06. The count matters for the cleanup (201 DELETE calls, not 82) and the date matters because it is the evidence that nothing has produced the key for two months: `ci.yml` no longer exists in `.github/workflows/` at all. **How the wrong figure was nearly repeated:** the first query returned only the newest page and showed the key absent entirely, which reads as "already resolved". It is not — the orphans are older than one page. Any re-check of this row MUST paginate. Because the configuration is still *recorded* on `main` but nothing produces it, GitHub reports it missing on every PR. It has done so for 51 days. Verified with full pagination: `code-scanning/analyses?ref=refs/heads/main` returns three analysis keys — `ci.yml:codeql` (**201**, last **2026-06-01**), `sdk-cli-ci.yml:codeql-analysis` (**193**, current) and `sdk-cli-ci.yml:trivy-scan` (**193**, current). The scanning that matters is healthy; `CodeQL SAST` passes and is a required check. **The dead workflow itself is already deleted** (it ran a no-op `Disabled` job on every PR and push to `main` and `develop`); deleting the file does NOT clear the recorded configuration, which is why this row exists. **The remaining action is deliberately not automated:** removing the **201** analyses via `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}?confirm_delete=true` is irreversible and destroys code-scanning history on a protected branch. Their historical value is nil — they describe a configuration dead since June — but discarding security-scan history is an owner decision, not a tooling one. **Why it matters beyond the noise:** a permanently red check trains reviewers to discount red checks, and `CodeQL SAST` — which shares the CodeQL name and IS required — is exactly the check nobody can afford to learn to ignore. +- **Evidence:** **Every pull request carries a red `CodeQL` check reading "1 configuration not found", and it is not a security finding — it is orphaned bookkeeping.** **RE-MEASURED 2026-08-01, and the row's figures were wrong in both directions.** GitHub holds **201** code-scanning analyses on `refs/heads/main` under the analysis key `.github/workflows/ci.yml:codeql` — not 82 — spanning **2026-05-13 to 2026-06-01**, so the last one is 2026-06-01 and not 2026-06-06. The count matters for the cleanup (201 DELETE calls, not 82) and the date matters because it is the evidence that nothing has produced the key for two months: `ci.yml` no longer exists in `.github/workflows/` at all. **How the wrong figure was nearly repeated:** the first query returned only the newest page and showed the key absent entirely, which reads as "already resolved". It is not — the orphans are older than one page. Any re-check of this row MUST paginate. Because the configuration is still *recorded* on `main` but nothing produces it, GitHub reports it missing on every PR. It has done so for 51 days. Verified with full pagination: `code-scanning/analyses?ref=refs/heads/main` returns three analysis keys — `ci.yml:codeql` (**201**, last **2026-06-01**), `sdk-cli-ci.yml:codeql-analysis` (**193**, current) and `sdk-cli-ci.yml:trivy-scan` (**193**, current). The scanning that matters is healthy; `CodeQL SAST` passes and is a required check. **The dead workflow itself is already deleted** (it ran a no-op `Disabled` job on every PR and push to `main` and `develop`); deleting the file does NOT clear the recorded configuration, which is why this row exists. **The remaining action is deliberately not automated:** removing the **201** analyses via `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}?confirm_delete=true` is irreversible and destroys code-scanning history on a protected branch. Their historical value is nil — they describe a configuration dead since June — but discarding security-scan history is an owner decision, not a tooling one. **Why it matters beyond the noise:** a permanently red check trains reviewers to discount red checks, and `CodeQL SAST` — which shares the CodeQL name and IS required — is exactly the check nobody can afford to learn to ignore. **CLOSED 2026-08-08, on evidence produced by the PR that carries the closure rather than on the assumption it would appear — and measuring first changed both what was deleted and why it was safe.** Three of this row's claims did not survive re-measurement, and the row had been re-measured twice already without any of them surfacing, because each re-measure checked the COUNT and none checked the CLAIM. **(1) It was not red.** The check-run conclusion was `neutral` — `gh pr checks` renders it `skipping` — from PR #250 (2026-07-28) onward; only PR #217 was ever `failure`. The row's central argument, that a permanently RED check trains reviewers to discount red checks, describes a state that ended eleven days after the row was written. A neutral grey check is a much weaker harm than the one this row was arguing, and the argument was never revisited. **(2) The branch was wrong.** The warning's own summary names the ref: *"1 configuration present on `refs/heads/develop` was not found"*. Only PRs based on `develop` still carried it (#425, #427, #429, #431, #433, all 2026-08-04); PRs based on `main` came back clean from #420 (2026-08-04 04:38Z) onward — **with all 201 orphans still in place**, four minutes apart from #433 still warning on `develop`. `main` therefore stopped warning without any deletion, which refutes the row's causal model ("still *recorded* on `main`, therefore reported on every PR") and means GitHub retires the expectation on its own by some mechanism this row never identified. Had that not been measured, the deletion would have been credited with a fix that was already happening. **(3) The 9 analyses on `develop` were never counted** — the real total was **210**, not 201, and the 9 that mattered most were the ones outside the row's scope. **What made the owner decision cheap, and no prior pass had measured it:** all 210 carry `results_count: 0`. Not one contains a finding; they are 210 records of "scanned, found nothing" from a configuration dead since June. The irreversibility was real, the loss was not, and that single field is what turned a judgement call into an easy one. **Executed:** 210 × `DELETE /code-scanning/analyses/{id}?confirm_delete=true`, each preceded by a GET asserting `analysis_key` and `results_count == 0` against a pre-built id allowlist, so no ordering mistake could reach a live-key analysis — 210 deleted, 0 refused, 0 errors. **Deliberately left:** 22 analyses under the dead key survive on `refs/pull/{4..17}/merge`, ephemeral per-PR refs from May that can never be the base of a PR and produce no symptom; deleting them buys nothing and costs 22 more irreversible calls. **The title is CORRECTED, and getting there needed a second gap.** It said "Eighty-two" through two re-measures, because `49-validate-gap-id-allocation` read any title change against the base as a possible id collision and had no channel for a deliberate retitle — so correcting it turned a required check red on the PR carrying the correction, and the 2026-08-01 re-measure had already set the precedent of fixing the evidence and leaving the headline. Rather than follow that precedent a third time, the guard grew one: [`GT-656`](./gap-reference-catalog.md#gt-656) declares a retitle as data, reproducing both titles exactly, so this correction lands with the collision check intact. - **Component:** `Infra` · **Criticality:** P2 · **Complexity:** XS - **Principal:** `XS` · **Interest:** `MED` · **Basis:** `estimate` - **Provenance:** Diagnosed on 2026-07-27 while investigating why `CodeQL` was red on PR #217. The reversible half (deleting the dead `.github/workflows/ci.yml`) landed in that same commit; the irreversible half is registered here instead of executed. - **Acceptance criteria:** - - [ ] `gh pr checks` on a fresh PR shows no `CodeQL` check reporting "configuration not found". - - [ ] The only analysis keys on `refs/heads/main` are the two produced by `sdk-cli-ci.yml`. - - [ ] `CodeQL SAST` still passes and is still a required check — the cleanup must not touch the scanning that works. + - [x] `gh pr checks` on a fresh PR **into `develop`** shows no `CodeQL` check reporting "configuration not found" — `develop`, not `main`, because that is the ref the warning names and the only base still carrying it. **Observed live on PR #440**, opened into `develop` for exactly this purpose: `gh pr checks 440` returns `CodeQL pass` and the check-run output title is `No new alerts in code changed by this pull request`, where the five PRs into `develop` on 2026-08-04 all returned `neutral` / `1 configuration not found`. The check reported 105s after the PR opened; the closure was written after reading it, not before. + - [x] **Restated, because the original wording became untestable:** it demanded the only keys on `refs/heads/main` be the two from `sdk-cli-ci.yml`, but a third legitimate key has since appeared (`openssf-scorecard.yml:analysis`). The test is the ABSENCE of `.github/workflows/ci.yml:codeql` — verified on **both** `refs/heads/main` (201 deleted, leaving `sdk-cli-ci.yml:codeql-analysis` 200, `sdk-cli-ci.yml:trivy-scan` 200, `openssf-scorecard.yml:analysis` 9) and `refs/heads/develop` (9 deleted, leaving 280 / 280), the second of which this row never tracked. + - [x] `CodeQL SAST` is still `REQUIRED` on both `main` and `develop` (read back from branch protection after the deletion) and green on its last 5 runs. The scanning that works was provably untouched: alerts are identical before and after — 242 open / 82 dismissed / 60 fixed, open-by-tool CodeQL 75 / Scorecard 158 / Trivy 9 — because every one of the 210 deleted analyses carried `results_count: 0`. **RE-MEASURED 2026-08-03. The row's figure -- 201 -- IS the right one; what was wrong was the 2026-08-03 measurement.** That one counted 395 by filtering on `test("ci.yml")`, and `sdk-cli-ci.yml` CONTAINS that string: it folded two configurations into one. The real breakdown over `refs/heads/main`, by exact key: @@ -8501,3 +8501,23 @@ The lesson is the board's own, and this time the measurer paid it: a `contains` - [x] **Verdicts are unchanged, verified rather than argued.** The full `EvaluationResult` for the k6 smoke payload was captured from a build of `origin/develop` and from this change with the [`GT-649`](./gap-reference-catalog.md#gt-649) phase data withheld, and the two are **byte-for-byte identical at 442,204 bytes**. Separately, the normalized corpus itself was diffed against the pre-change loader: **393 rules, identical JSON**. - [x] Measured after: the corpus loads once at boot (393 rules, 181–590 ms), evaluate median falls 52.7 → 38.2 ms, and a `/health` issued 5 ms into an in-flight evaluate falls **11.6 → 3.5 ms**. The per-request WARN block is gone from the log entirely. - [x] Re-measured on a CI runner rather than a laptop, against the `k6-smoke.json` of a later Reliability run, to confirm the 498 ms tail is gone where it was observed. **CLOSED 2026-08-01 on runner evidence:** Reliability run `30680099586`, job `k6 load profile (thresholds + published metrics)`, head `6fc31587`, published `k6-smoke.json`, `k6-average.json`, and `core-api.log`. `RulesetCorpusWarmupService` loaded the corpus once at startup — 393 rules in 75 ms — and the per-request WARN block is absent (`Skipping non-standard ruleset`, `Phases directory not found`, and `WARN undefined` all 0). The smoke artifact reports 20 requests / 50 checks / 0 failures, `health_latency` p99 **4.24 ms** and max **4.33 ms**, while `evaluate_latency` median is **64.1 ms** and p95 **66.9 ms**. The longer average profile confirms the same shape over 1,143 requests: `health_latency` p99 **4.87 ms**, max **7.11 ms**, and no recurrence of the 498 ms health tail where the defect was first observed. + +#### GT-656 + +**Title:** A guard against id collisions made gap titles immutable + +- **Purpose / Problem:** Let a row's headline be corrected when it is measured wrong, without weakening the check that stops two sessions from allocating one number to two different gaps. +- **Evidence:** `49-validate-gap-id-allocation` compares each catalog `**Title:**` against the base branch and fails on any difference. The title is the right discriminator — evidence, status and criteria all change on a branch, and only the title says WHICH gap the number names — but the guard treated a deliberate retitle and a collision as the same event, and had no channel for the difference. Its own failure text admitted this and asked for the distinction "in the commit"; nothing reads commit messages, and a squash merge rewrites them, so in practice the answer was always "renumber or leave it wrong". +- **What it means:** titles were effectively immutable. A board that exists to stop technical debt from being described inaccurately accumulated rows whose FIRST LINE was inaccurate, which is the line a reader sees. +- **The cost was already paid.** [`GT-622`](./gap-reference-catalog.md#gt-622) was re-measured twice — 82 → 201 → 210 orphaned analyses, and the branch still carrying the warning turned out to be `develop`, not `main` — while its headline went on reading "Eighty-two ... every PR". Its evidence field was corrected on 2026-08-01 and again on 2026-08-08; the title was not, both times because correcting it turns a REQUIRED check red on the very PR carrying the correction. The first occurrence set the precedent silently; the second found the precedent and was about to follow it. +- **Component:** `.harness` · **Criticality:** P2 · **Complexity:** S +- **Principal:** `S` · **Interest:** `MED` · **Basis:** `estimate` +- **Provenance:** Found on 2026-08-08 while closing [`GT-622`](./gap-reference-catalog.md#gt-622), when correcting that row's measurably wrong title turned `Validate documentation` red. Registered rather than worked around: leaving the title wrong a third time would have made the workaround the convention. +- **Acceptance criteria:** + - [x] A deliberate retitle can land without weakening the collision check — declared in `reference/core/control-center/gaps/gap-retitles.json` as `{ id, from, to, declaredAt, reason }`, and exempting only when BOTH titles match the two branches exactly. + - [x] The exemption is scoped to a retitle, never to an id. Covered by `classifyRetitles` and by the fixture *"a real collision landing on an already-retitled id still fails"*: once the declared titles are not the two on the branches, the collision is reported again. + - [x] A declaration cannot rot into a silent pass. `active` exempts, `spent` is reported (the retitle reached the base, so nothing is left to exempt), `rot` — describing neither side — FAILS, as does a registry that exists but cannot be parsed or has no `retitles` array. + - [x] Every exemption is printed, never applied silently: `declared retitles ... N active, N spent, N rot` plus one line per active declaration carrying its reason. + - [x] The negative twin is observed red, not assumed: the same retitle WITHOUT a declaration still fails, and a declaration quoting a title the catalog never carried does not launder it. `node --test` 27/27. +- **First use:** GT-622's title is corrected in the same change that built the mechanism — 82 → 210, "every PR" → "PRs into `develop`" — with the declaration carrying why. That is the closure's own proof: the guard is green with the retitle applied against both `origin/main` and `origin/develop`. +- **Status:** `DONE` (2026-08-08) diff --git a/reference/core/control-center/gaps/gap-retitles.json b/reference/core/control-center/gaps/gap-retitles.json new file mode 100644 index 00000000..62c40fdd --- /dev/null +++ b/reference/core/control-center/gaps/gap-retitles.json @@ -0,0 +1,11 @@ +{ + "retitles": [ + { + "id": "GT-622", + "from": "Eighty-two orphaned code-scanning analyses keep every PR warning about a configuration that died in June", + "to": "Two hundred and ten orphaned code-scanning analyses keep PRs into `develop` warning about a configuration that died in June", + "declaredAt": "2026-08-08", + "reason": "Re-measured twice and wrong both times in the headline: 210 orphaned analyses, not 82, and the base still carrying the warning is `develop` alone, not every PR. The evidence field was corrected on 2026-08-01 and 2026-08-08; the title could not be, because this guard had no way to tell a retitle from a collision. GT-656 is that mechanism, and this is its first use." + } + ] +} diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 0c9a5427..510ed5f4 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -4,7 +4,7 @@ **Estado:** Seguimiento Activo **Responsable:** Evolith Architecture Board -**Última Actualización:** 2026-07-26 (**Aterrizó el enforcement, y GT-577 se cerró con evidencia de CI, no con un tick verde.** `GT-577` → COMPLETADO: el workflow de dogfood corrió en un runner real (corrida 30228607519, `develop`) con ambos jobs verdes, y el cierre se apoya en lo que asserta el log, no en el color — `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` más la mitad negativa `OK: the gate blocked as expected`. La composite action queda dogfooded y bajo regresión, que era la mitad mayor de ese gap. El registro de cierre de `GT-574` se re-verificó contra la API de GitHub y se enriqueció con los valores observados: **`enforce_admins` ahora es TRUE en `main` y en `develop`**, los contextos requeridos pasaron de 6 a 7, y **`CodeQL SAST` es requerido**, así que el hallazgo de la auditoría "0 de los 6 checks requeridos es de seguridad" ya no se sostiene; `develop`, que antes devolvía 404 *Branch not protected* siendo la rama donde aterriza todo cambio, lleva ahora los mismos siete. Es la primera vez que un peldaño *Enforced* de este corpus está respaldado por configuración y no por prosa. Registrados aparte en esta ventana: `GT-596`…`GT-600` (artefactos internacionales de deuda técnica — ISO/IEC 33020:2019, 5055:2021, 25040:2024, OMG ATDM V2, OpenSSF Scorecard/SLSA/SSDF) y `GT-601`…`GT-608` (evaluación de componentes). **Sin mover y ahora en el camino crítico:** `GT-570` — npm sigue sirviendo 1.1.0 del 2026-07-18 en los tres paquetes comprobados, así que el artefacto publicado sigue siendo anterior a la ola de seguridad que el CHANGELOG público enumera. En esta ventana no aterrizó ningún cambio en `src/`; los gaps de código están inalterados por construcción, y se re-verificó que sus fixes siguen en pie (denominador 111 comprobadas / 269 saltadas / 380 totales, un envelope de 123 KB que sobrevive a una pipe, el alias de bin `evolith`, MCP stdio invocable). Contadores recalculados desde las filas en ambos vocabularios de estado. +**Última Actualización:** 2026-08-08 (**Un gap cerrado haciendo lo irreversible que el board había diferido a propósito — y lo que vale registrar es la medición que se tomó antes.** `GT-622` → COMPLETADO, y lo que lo cerró es la mitad irreversible que el board había diferido: los 210 análisis huérfanos de `.github/workflows/ci.yml:codeql` están borrados de `refs/heads/main` (201) y `refs/heads/develop` (9), la clave muerta no aparece en ninguna de las dos, y el corpus de alerts es idéntico a través del borrado — 242 abiertos / 82 descartados / 60 corregidos, CodeQL 75 / Scorecard 158 / Trivy 9. **Tres afirmaciones de esa misma fila no sobrevivieron a la re-medición, y la fila ya se había re-medido dos veces sin que ninguna saliera, porque cada pasada comprobó el RECUENTO y ninguna comprobó la AFIRMACIÓN:** el check era `neutral`, no rojo, desde el PR #250; la configuración que nombra el aviso está en `refs/heads/develop`, no en `main`, así que solo los PR contra `develop` seguían arrastrándolo mientras `main` salía limpio por su cuenta desde el PR #420 con los 201 huérfanos todavía puestos; y los 9 análisis de `develop` —los que importaban— nunca se contaron. **El campo que convirtió un juicio del dueño en una decisión fácil no se había leído nunca:** los 210 análisis tienen `results_count: 0`, así que lo descartado son 210 registros de “escaneé y no encontré nada” de una configuración muerta desde junio. La irreversibilidad era real; la pérdida no. Quedan a propósito 22 análisis de la clave muerta en `refs/pull/{4..17}/merge`, refs efímeras por PR que jamás pueden ser base de un PR. **Observado también al medir, y fuera de este cierre:** el conjunto de contextos requeridos en `main` y `develop` es ahora de **8**, tras ganar `Secret Detection (gitleaks)` — la promoción que `GT-653` registraba como su único pendiente. El criterio de cierre que solo podía observarse en un PR contra `develop` se observó en el PR #440, el que trae este mismo cierre, 105s después de abrirlo — escrito tras leer el check, no antes. Contadores recalculados desde las filas: **640 / 653 completados · 3 en progreso · 3 pendientes · 7 diferidos**.) **Detalle de Gaps:** [Catálogo de Referencia de Gaps](./gap-reference-catalog.es.md) Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunidades, habilitadores, prioridad y estado. Selecciona un ID para abrir la descripción del problema, propósito, evidencia, criterios de cierre y referencias. @@ -41,7 +41,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-625`](./gap-reference-catalog.es.md#gt-625) | **El artefacto publicado no es instalable, y lo que lo tapa es el workspace.** Descubierto al PROBAR el criterio de aceptación de GT-571 contra el registro en vez de asumirlo. `npm i @beyondnet/evolith-cli@1.2.0` en un directorio limpio instala, y entonces `evolith-cli --version` muere con `MODULE_NOT_FOUND` en `@beyondnet/evolith-core-domain/application/paths/rulesets-location` — un import de subruta profunda que el `@beyondnet/evolith-core-domain` publicado no expone. En este repositorio ese mismo import resuelve, porque el symlink del workspace apunta al árbol de fuentes y no a lo que se empaquetó. Toda comprobación que hacemos es por tanto ciega a esto: la suite del CLI (1382 tests), la suite e2e y el tester exploratorio corren dentro del workspace. 1 de 36 especificadores `@beyondnet/*` que importa el `dist` publicado no resuelve en una instalación limpia; `src/sdk/cli/scripts/check-install-smoke.mjs`, escrito durante la Ola 2, es lo que lo detecta. Es la misma clase que el hallazgo de la auditoría de madurez de que un verde local no es evidencia — aquí el mecanismo de ocultación son los workspaces de npm y no un `dist` rancio o un directorio en gitignore. **CERRADO el 2026-07-28.** `core-domain@1.2.0`, `core@1.2.0`, `infra-providers@1.2.0`, `mcp@1.2.1` y `cli@1.2.1` están en el registro, los cinco con attestations. Verificado contra el REGISTRO, no contra un exit code: el tarball de core-domain contiene `application/paths/rulesets-location`, y en un directorio que jamás ha visto este workspace `cli --version` imprime 1.2.1 y el MCP arranca. **La puerta se pagó sola en su primer uso real**: paró la release antes de publicar el MCP y destapó un SEGUNDO paquete publicado roto que la auditoría no vio — `evolith-mcp@1.2.0` revienta al arrancar con `PatternCatalogService is not a constructor`, porque `evolith-core@1.1.0` no lo exporta. Dos paquetes publicados rotos por una sola causa; este se detectó antes de enviar, no cuatro días después. Se registra también porque cambia lo que es cierto: publicar core-domain@1.2.0 repara retroactivamente `cli@1.2.0`, cuyo rango `^1.1.0` ahora resuelve al hermano corregido — que es justo por lo que se estrecharon los rangos a `^1.2.0`, para que un resolutor no pueda volver a elegir 1.1.0. | `Evolith CLI` | Cross | P0 | M | `COMPLETADO` | | [`GT-624`](./gap-reference-catalog.es.md#gt-624) | **Extraído de [`GT-570`](./gap-reference-catalog.es.md#gt-570) para que su remanente quede registrado y no absorbido en un cierre.** 1.2.0 se publicó el 2026-07-27 con provenance y la exposición está cerrada para quien instale `latest` — pero dos de los tres criterios originales de GT-570 no se cumplen con eso, y fingir lo contrario es el patrón que este tablero lleva pillándose a sí mismo (GT-12, GT-568, GT-254, GT-424). **(a) Las versiones 1.1.0 no están deprecadas.** `npm install @beyondnet/evolith-mcp@1.1.0` sigue resolviendo la build anterior a la ola de seguridad del 2026-07-23, en silencio, y el CHANGELOG público nombra los ficheros vulnerables. Deprecar es un comando por paquete pero exige credenciales de npm, así que es acción del dueño: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, e igual para `evolith-cli@1.1.0` y `evolith-agent-runtime@1.1.0`. **(b) Ningún gate de release falla cuando un commit etiquetado de seguridad no está en el tag publicado.** Esa ausencia es exactamente lo que permitió que la ola siguiera sin publicar del 2026-07-23 al 2026-07-27 mientras `SECURITY.md` declaraba la línea 1.1.x "actively patched". No lo detectó nada; lo detectó una auditoría. Relacionado: `GT-623` — release-please deriva los saltos de versión de los mensajes de commit, y el tipo `security(...)` que usan 2 de los últimos 60 commits no es un tipo de Conventional Commits, así que no aporta nada al bump. Los dos defectos dejan que un cambio de seguridad no llegue a una versión, por caminos distintos. **CERRADO el 2026-07-30 — y la mitad que importaba era el gate, no las deprecaciones.** Criterio (a): `1.1.0` queda deprecada en seis paquetes (`evolith-cli`, `evolith-mcp`, `evolith-agent-runtime`, `evolith-core-domain`, `evolith-core`, `evolith-infra-providers`), con mensaje que nombra al sucesor; dos se dejaron aparte a propósito y el motivo es un hallazgo, no un recorte de alcance — `evolith-sdk@1.1.0` es lo que la línea publicada RESOLVÍA de verdad (ver `GT-634` (registrado aparte)) y `evolith-contracts@1.1.0` es la única versión de ese paquete. Criterios (b) y (c): `48-validate-security-publish-lag` le pregunta al **registry** qué está publicado — el tag `v*` más nuevo aquí es `v1.1.0` mientras npm sirve `1.2.2`, así que un gate basado en tags habría reportado un retraso inexistente —, localiza el commit donde se FIJÓ la versión publicada de cada paquete y falla ante cualquier commit posterior cuyo **tipo o scope** lo marque como seguridad. Rechaza la prosa a propósito: `fix(deps)!: … the security wave` es un arreglo de dependencias, y un gate que grita lobo se acaba apagando. **Se encontraron y corrigieron dos falsos negativos al construirlo, cada uno fijado con una fixture:** `git log -S` casaba también el commit que BORRÓ la cadena de versión, y tomar el commit MÁS NUEVO con la versión publicada permitía que un bump de dependencia posterior empujara la frontera y escondiera la ventana. El denominador corregido es 47 commits examinados en 8 paquetes, frente a 31 de la primera implementación. Observado en rojo por `43-validate-guard-negative-fixtures` (37/37), así que es una fixture que este repositorio ha VISTO fallar. | `Infra` | Cross | P1 | S | `COMPLETADO` | | [`GT-623`](./gap-reference-catalog.es.md#gt-623) | **El hook existe, está cableado, y no puede aplicar nada.** `.husky/commit-msg` ejecuta `npx --no -- commitlint --version`; cuando falla imprime `commitlint is not installed — skipping commit message lint` y sale con **éxito**. Verificado: `commitlint` no aparece ni en `dependencies` ni en `devDependencies` del `package.json` raíz, no hay `commitlint.config.*` ni `.commitlintrc*`, y no existe clave `commitlint` en `package.json`. Así que la rama `else` es la única que se ejecuta, en cada commit. Observado en vivo el 2026-07-27 al mergear `develop` en una rama de trabajo. **Qué depende de la convención que no aplica:** `CONTRIBUTING.md` y `.github/pull_request_template.md` exigen Conventional Commits en tres sitios, y —la parte que cuesta dinero— **release-please deriva los saltos de versión de los mensajes de commit**, cableado en `sdk-cli-release.yml` y `sdk-cli-ci.yml`. **Ya está derivando, y con consecuencia:** 2 de los últimos 60 commits no-merge usan el tipo `security(...)` (`security(fase-7): add Docker/K8s hardening checklist`, `security(fase-6): add executable security rulesets`), que no es un tipo de Conventional Commits. release-please no lo reconoce, así que **un commit que se anuncia como cambio de seguridad no aporta nada al salto de versión** — el mismo modo de fallo que [`GT-570`](./gap-reference-catalog.es.md#gt-570), donde una ola de seguridad sigue sin publicarse. Fix: instalar y configurar commitlint para que el hook tome su rama real, o borrar el hook y dejar de reclamar la convención. Fallar abierto es la peor de las tres opciones, porque produce la apariencia de enforcement. Si se quiere el tipo `security`, declararlo en la config y mapearlo a un bump — no dejarlo a un linter que nunca corre. **Cierre (ola 2026-07-28).** commitlint está instalado, configurado y el hook toma su rama real — verificado en ambos sentidos: un mensaje `feat:` válido se acepta y uno malformado se **rechaza**. El tipo `security(...)` que usan commits reales aquí queda DECLARADO en la config con su significado de bump, en vez de dejarlo fallar el lint o ignorarse en silencio. **Este aterrizaje rompió el repositorio de tres formas antes de funcionar**, todas por respetar el agente correctamente sus paths: `package.json` ganó las dependencias y `package-lock.json` no, así que `npm ci` falló en todo el repo; el hook falló CERRADO sin commitlint, bloqueando cualquier commit; y `03-validate-root-cleanliness` rechazó el fichero nuevo en la raíz. Los tres reparados en la misma ola. La lección es sobre la ola, no sobre el fix: **quien pueda editar `package.json` debe poder editar también el lockfile**. | `Governance` | Cross | P2 | S | `COMPLETADO` | -| [`GT-622`](./gap-reference-catalog.es.md#gt-622) | **Cada pull request arrastra un check `CodeQL` en rojo que dice "1 configuration not found", y no es un hallazgo de seguridad — es contabilidad huérfana.** GitHub conserva **82 análisis de code scanning** en `refs/heads/main` bajo la clave `.github/workflows/ci.yml:codeql`. Esa configuración existió de verdad: el commit `87f50ce3` añadió un job `codeql` a `ci.yml`, y `f50030cd` lo quitó el 2026-06-06 al vaciar ese workflow — el último análisis bajo esa clave es de ese mismo día. Como la configuración sigue *registrada* en `main` pero nada la produce, GitHub la reporta como ausente en cada PR. Lleva así 51 días. Verificado: `code-scanning/analyses?ref=refs/heads/main` devuelve tres claves — `ci.yml:codeql` (82, último 2026-06-06), `sdk-cli-ci.yml:codeql-analysis` (159, al día) y `sdk-cli-ci.yml:trivy-scan` (159, al día). El escaneo que importa está sano; `CodeQL SAST` pasa y es check requerido. **El workflow muerto ya está eliminado** (corría un job `Disabled` que sólo hacía `echo`, en cada PR y push a `main` y `develop`); borrar el fichero NO limpia la configuración registrada, y por eso existe esta fila. **La acción restante queda deliberadamente sin automatizar:** eliminar los 82 análisis vía `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}` es irreversible y destruye historial de code scanning de una rama protegida. Su valor histórico es nulo —describen una configuración muerta desde junio— pero tirar historial de escaneo de seguridad es decisión del dueño, no de la herramienta. **Por qué importa más allá del ruido:** un check permanentemente rojo enseña a los revisores a descontar los checks rojos, y `CodeQL SAST` —que comparte el nombre CodeQL y SÍ es requerido— es precisamente el check que nadie se puede permitir aprender a ignorar. | `Infra` | Cross | P2 | XS | `PENDIENTE` | +| [`GT-622`](./gap-reference-catalog.es.md#gt-622) | **Cada pull request arrastra un check `CodeQL` en rojo que dice "1 configuration not found", y no es un hallazgo de seguridad — es contabilidad huérfana.** GitHub conserva **82 análisis de code scanning** en `refs/heads/main` bajo la clave `.github/workflows/ci.yml:codeql`. Esa configuración existió de verdad: el commit `87f50ce3` añadió un job `codeql` a `ci.yml`, y `f50030cd` lo quitó el 2026-06-06 al vaciar ese workflow — el último análisis bajo esa clave es de ese mismo día. Como la configuración sigue *registrada* en `main` pero nada la produce, GitHub la reporta como ausente en cada PR. Lleva así 51 días. Verificado: `code-scanning/analyses?ref=refs/heads/main` devuelve tres claves — `ci.yml:codeql` (82, último 2026-06-06), `sdk-cli-ci.yml:codeql-analysis` (159, al día) y `sdk-cli-ci.yml:trivy-scan` (159, al día). El escaneo que importa está sano; `CodeQL SAST` pasa y es check requerido. **El workflow muerto ya está eliminado** (corría un job `Disabled` que sólo hacía `echo`, en cada PR y push a `main` y `develop`); borrar el fichero NO limpia la configuración registrada, y por eso existe esta fila. **La acción restante queda deliberadamente sin automatizar:** eliminar los 82 análisis vía `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}` es irreversible y destruye historial de code scanning de una rama protegida. Su valor histórico es nulo —describen una configuración muerta desde junio— pero tirar historial de escaneo de seguridad es decisión del dueño, no de la herramienta. **Por qué importa más allá del ruido:** un check permanentemente rojo enseña a los revisores a descontar los checks rojos, y `CodeQL SAST` —que comparte el nombre CodeQL y SÍ es requerido— es precisamente el check que nadie se puede permitir aprender a ignorar. **CERRADO EL 2026-08-08, sobre evidencia producida por el propio PR que trae el cierre y no sobre el supuesto de que aparecería — y medir primero cambió tanto qué se borró como por qué era seguro.** Tres afirmaciones de esta fila no sobrevivieron a la re-medición. **(1) No estaba en rojo.** `gh pr checks` lo pintaba como `skipping` y la conclusión del check-run era `neutral`, no `failure`, desde el PR #250 (2026-07-28); solo el PR #217 llegó a ser `failure`. El argumento central de la fila —que un check permanentemente ROJO enseña a descontar los rojos— describe un estado que terminó once días después de escribirla, y la fila nunca se corrigió. **(2) La rama estaba mal.** El propio texto del aviso nombra la ref: *"1 configuration present on `refs/heads/develop` was not found"*. Solo los PR contra `develop` seguían arrastrándolo (#425, #427, #429, #431, #433, todos del 2026-08-04); los PR contra `main` salían limpios desde el #420 (2026-08-04) **con los 201 huérfanos todavía puestos**. Es decir: `main` dejó de avisar sin que nadie borrara nada, lo que refuta el modelo causal de la fila ("registrado en `main`, luego reportado en cada PR") y significa que la limpieza nunca fue lo único que podía resolverlo. `develop` —la rama donde aterriza cada cambio— era justo la que esta fila no estaba siguiendo. **(3) Los 9 análisis de `develop` nunca se contaron.** El total real era **210**, no 201. **Lo que abarató la decisión del dueño, y nadie lo había medido:** los 210 análisis tienen `results_count: 0`. Ni uno contiene un hallazgo — son 210 registros de "escaneé y no encontré nada" de una configuración muerta desde junio. La irreversibilidad era real; la pérdida no. **Ejecutado:** 210 × `DELETE /code-scanning/analyses/{id}?confirm_delete=true`, cada uno precedido de un GET que verifica `analysis_key` y `results_count == 0` para que ningún error de orden pudiera alcanzar un análisis de las claves vivas — 210 borrados, 0 rechazados, 0 errores. **Verificado sin cambios a través del borrado:** 242 abiertos / 82 descartados / 60 corregidos antes y después, y abiertos por herramienta CodeQL 75 / Scorecard 158 / Trivy 9 — los 280 análisis vigentes de `sdk-cli-ci.yml:codeql-analysis` por rama quedan intactos. **Dejado a propósito:** sobreviven 22 análisis de la clave muerta en `refs/pull/{4..17}/merge`, refs efímeras por PR de mayo que jamás pueden ser base de un PR y por tanto no producen síntoma; borrarlas no compra nada y añade 22 llamadas irreversibles. **El criterio 2 estaba obsoleto y se reformula:** exigía que las únicas claves en `main` fueran las dos de `sdk-cli-ci.yml`, pero desde entonces apareció una tercera legítima —`openssf-scorecard.yml:analysis` (9, vigente)—, así que la prueba es la AUSENCIA de la clave muerta, no un recuento de las vivas. | `Infra` | Cross | P2 | XS | `COMPLETADO` | | [`GT-609`](./gap-reference-catalog.es.md#gt-609) | **Fuga de autorización en la superficie de descubrimiento.** `mcp-cache.service.ts:8` declara `toolsList: 'mcp:tools:list'` — una única clave literal, sin principal, tenant ni scope — y la lista se cachea ANTES de aplicar el filtro de scope. Así que el primer llamador que calienta la caché decide el inventario que ven todos los siguientes durante el TTL: un admin que la caliente publica el inventario con capacidad de escritura a los lectores. **Verificado aquí contra el código.** Fix: indexar la caché por principal (hash de scopes + tenant), o eliminarla — una superficie de descubrimiento que responde con la vista de otro principal es peor que una sin caché. Origen: hallazgo 4.2 del diagnóstico de producto (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **Cierre (ola 2026-07-28).** La caché de tools/list ya no responde con la vista de otro principal. Verificado con el test que importa y no con el formato de la clave: calentar como principal privilegiado, leer como uno sin privilegios, y asertar que el segundo sólo ve su inventario. mcp-server 409/409. | `MCP Server` | Cross | P0 | S | `COMPLETADO` | | [`GT-610`](./gap-reference-catalog.es.md#gt-610) | **La peor clase de fallo posible para un producto de auditoría: la acción correcta, registrada, ejecutada con las entradas equivocadas.** Los tres motores rellenan `proposedArguments` — `swarms-agent.adapter.ts:99`, `hermes-agent.adapter.ts:98`, `stub-agent-engine.adapter.ts:46` — y el servicio lee sólo `plan.proposedTool` (`agent-runtime.service.ts:168-169`). Los argumentos propuestos se calculan, cruzan el puerto y se tiran; la skill se ejecuta con lo que hubiera en `request.parameters`. **Verificado aquí contra el código.** Fix: fusionar los argumentos propuestos con revalidación contra el contrato de entrada declarado por la skill antes de ejecutar, y registrar en la traza qué conjunto se usó. Origen: hallazgo 5.5 del diagnóstico de producto (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **Cierre (ola 2026-07-28).** El servicio ahora fusiona los `proposedArguments` del motor **con revalidación contra el contrato de entrada declarado por la skill** antes de ejecutar — un motor es una fuente no confiable, así que pasarlos a ciegas habría cambiado un defecto por otro — y registra en la traza qué conjunto se usó. agent-runtime 333/333. | `agent-runtime` | Cross | P0 | S | `COMPLETADO` | | [`GT-611`](./gap-reference-catalog.es.md#gt-611) | **Más amplio de lo que reportó el diagnóstico, y más amplio de lo que arregló GT-571.** Los prompts no viven en cada comando: pasan por el `src/sdk/cli/src/infrastructure/prompts/prompt.service.ts` compartido, que consumen `init`, `validate`, `upgrade`, `phase-advance`, `adr`, `waiver`, `chat`, `enforce`, `agents` y más (`profile.command.ts` importa `@clack/prompts` directamente). **Verificado aquí contra el código.** GT-571 le dio a `init` un contrato no-interactivo definido — un stdin cerrado no pregunta, el fallo fija exit code distinto de cero, `--format json` emite un envelope parseable y nada más — y dejó a los demás consumidores como estaban. Un paso de CI que canalice cualquiera de ellos a `jq` sigue recibiendo un menú ANSI y leyendo exit 0. Fix: imponer el contrato máquina en la frontera del `PromptService` y no comando a comando, de modo que un stdin no-TTY no pueda producir un prompt en ningún sitio, y añadir un test de superficie que lo asserte para cada comando registrado. Origen: hallazgo 3.1 del diagnóstico de producto (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **+ola 2026-07-28 — impuesto en la frontera, con un matiz honesto.** El contrato máquina se impone dentro de `PromptService` y no comando a comando, así que ningún comando puede abrir un segundo canal de prompt — asertado por un invariante estructural sobre el fuente de todos ellos. **NO cerrado, y la razón es deliberada:** `EVOLITH_FORCE_INTERACTIVE=1` reactiva el prompt sin TTY. Existe porque la suite unitaria debe poder recorrer las ramas interactivas y porque algunas terminales reportan mal `isTTY`, pero un job de CI que la fije puede seguir colgándose en un prompt — así que la lectura estricta de "un stdin no-TTY NUNCA puede producir un prompt" sólo se sostiene en ausencia de esa variable. La mitad de cobertura es además estática, no un barrido conductual de los ~40 comandos registrados con stdin cerrado. **CERRADO el 2026-07-28 sobre una mutación, no sobre una lectura.** La suite de regresión afirma la propiedad donde de verdad es cierta o falsa: `PromptService` — el único canal de prompts — rechaza todo método interactivo cuando stdin no es un TTY, clasificado como entrada inválida (exit 3) y no como fallo de herramienta, y sigue preguntando cuando hay TTY, así que el producto interactivo no cambia. La mitad que importa es la de superficie: ningún fichero de comando puede abrir un SEGUNDO canal. Verificado añadiendo un comando que importa `@clack/prompts` directamente — la suite se pone roja. 19/19, y cubre comandos escritos después, que es la razón de que sea un barrido y no una lista. | `Evolith CLI` | Cross | P1 | M | `COMPLETADO` | @@ -665,12 +665,13 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-650`](./gap-reference-catalog.es.md#gt-650) | **El Core guarda DOS respuestas sin reconciliar a «qué artefactos exige la fase X», así que no puede publicar ningún catálogo de artefactos.** `reference/governance/sdlc/gates/gate-f*.json` nombra 24 artefactos en forma humana con `schemaRef`/`producedBy`; `UNIVERSAL_PHASE_ARTIFACTS` en `core-domain` nombra 18 como slugs y sólo para las tres fases downstream. Discrepan en nombres (`CI Pipeline` vs `ci-pipeline-result`), en pertenencia (`source-change-set`, `architecture-drift-result` y `spec-traceability-map` sólo están en la constante; `Documentation Delta` y `Acceptance Validation` sólo en las compuertas) y —peor— en FASE: `Coverage Report` es artefacto de construcción en `gate-f3` y de calidad en la constante. Sólo el primer corpus es alcanzable por HTTP, y no es el que usa el evaluador. La consecuencia en el satélite está medida: el `GAP-004` de `evolith_tracker` no puede sustituir su espejo `core-standin` por una fuente `core-sync`, porque no hay un catálogo único que sincronizar. | `SDLC Governance` | Cross | P1 | M | `COMPLETADO` | | [`GT-651`](./gap-reference-catalog.es.md#gt-651) | **La mitad de adaptadores de marketplace de `GT-532`, separada para que su cierre no la sepulte.** `GT-532` se cerró por sus dos criterios de aceptación —vistas de portafolio y paquetes de gobernanza por tenant, ambos ya ciertos— pero su TÍTULO nombraba además adaptadores estilo marketplace, que los criterios nunca pidieron y que no se entregaron. Se registra aparte en vez de dejarla dentro de una fila cerrada, porque una intención no dicha dentro de un `DONE` es justo la obsolescencia que este tablero sigue encontrando. **Deliberadamente sin acotar:** qué es un marketplace de adaptadores para Evolith —un catálogo de conectores de proveedor, un canal de distribución de paquetes de gobernanza, o ambos— es una decisión de producto, y adivinarla aquí produciría criterios de aceptación que nadie ha acordado. | `Tracker` | Cross | P3 | L | `PENDIENTE` | | [`GT-652`](./gap-reference-catalog.es.md#gt-652) | **El cable no puede llevar cinco campos que el motor lee, y una nota de cierre ya llamaba al DTO «full canonical mirror».** `main.ts` corre el ValidationPipe global con `forbidNonWhitelisted: true`, así que un campo ausente de `EvaluationContextDto` no llega recortado: hace 400 la evaluación entera. Y el controlador hace `body as unknown as EvaluationContext`, un cast directo, lo que convierte esa clase en la superficie ALCANZABLE del contrato. `requester`, `repositoryRevision`, `qualitySignals`, `repoFacts` y `baselineRepoFacts` están declarados en `EvaluationContext` y el dominio los consume hoy, y ninguno se podía enviar. **Por qué nadie lo vio:** cada prueba unitaria construye un `EvaluationContext` en TypeScript y pasa, mientras que un llamante real que mande ese mismo objeto por HTTP es rechazado — el defecto es invisible desde dentro del Core y sólo lo sufre un consumidor. Es lo que bloquea el criterio 2 de CP-04 en `evolith_tracker`. **Qué significa:** el motor sabe leer cinco datos que la puerta de entrada se niega a aceptar. **Ejemplo:** el Tracker no puede decirle al Core quién pidió una evaluación ni qué revisión juzgó, así que ambos hechos viajan como cadenas sin tipo en una bolsa de paso. | `Evolith Core` | Cross | P1 | S | `COMPLETADO` | -| [`GT-653`](./gap-reference-catalog.es.md#gt-653) | **La detección de secretos es estructuralmente incapaz de detener nada, y en los PR de Dependabot directamente no corre.** El job `secret-detection` de `sdk-cli-ci.yml` lleva `continue-on-error: true` y no está entre los siete contextos requeridos de `main` ni de `develop`, así que una fuga real aparece como una marca roja que ninguna compuerta consulta. La segunda mitad es más estrecha y se midió: el secreto `GITLEAKS_LICENSE` existe en el almacén de Actions, pero el **almacén de secretos de Dependabot está vacío**, y las corridas disparadas por Dependabot solo leen ese almacén — así que la licencia llega en blanco y el paso falla antes de escanear. **Por qué nadie lo vio:** en `develop`, `main` y ramas humanas el job está verde (últimas 8 corridas), que es justo donde nadie buscaba el agujero; el fallo solo asoma en los PR de Dependabot, la única clase de cambio que escribe un actor externo automatizado. **Qué significa:** la superficie que más merece un escaneo independiente de secretos es la única que nunca se escanea, y donde sí escanea no puede bloquear. **Ejemplo:** los PR #370–#374 se mergearon el 2026-08-03 con `Secret Detection (gitleaks)` fallando en los cinco; ninguna compuerta objetó, correctamente, porque ninguna está cableada para hacerlo. **ARREGLO (2026-08-03):** la dependencia de licencia desaparece en vez de satisfacerse — el job instala el binario pineado de gitleaks (MIT; solo el envoltorio de la action exigía licencia) y corre `gitleaks dir . --no-banner --redact --exit-code 1`, así que el punto ciego de Dependabot no puede reabrirse y no hace falta ningún secreto de admin. `continue-on-error` retirado. El guard `60-validate-secret-scan-gate.mjs` corre en cada corrida: extrae el comando del workflow, planta una credencial y exige exit 1, y exige que un árbol limpio salga 0. Su primera versión plantaba el canónico `AKIAIOSFODNN7EXAMPLE`, que gitleaks lleva como stopword — pasó sin haber visto nunca bloquear la compuerta, el mismo defecto reproducido dentro de su propia prueba. Los 15 hallazgos preexistentes eran todos sintéticos (fixtures de las pruebas de redacción y ejemplos `curl` de README) y quedan fijados en `.gitleaksignore` por huella, no por ruta. **Pendiente:** la promoción a contexto requerido en `main`/`develop`, retenida hasta que el job reporte verde en ambas — activarla antes dejaría bloqueado todo PR abierto, como pasó con el PR #218. | `Security` | Cross | P2 | S | `COMPLETADO` | +| [`GT-653`](./gap-reference-catalog.es.md#gt-653) | **La detección de secretos es estructuralmente incapaz de detener nada, y en los PR de Dependabot directamente no corre.** El job `secret-detection` de `sdk-cli-ci.yml` lleva `continue-on-error: true` y no está entre los siete contextos requeridos de `main` ni de `develop`, así que una fuga real aparece como una marca roja que ninguna compuerta consulta. La segunda mitad es más estrecha y se midió: el secreto `GITLEAKS_LICENSE` existe en el almacén de Actions, pero el **almacén de secretos de Dependabot está vacío**, y las corridas disparadas por Dependabot solo leen ese almacén — así que la licencia llega en blanco y el paso falla antes de escanear. **Por qué nadie lo vio:** en `develop`, `main` y ramas humanas el job está verde (últimas 8 corridas), que es justo donde nadie buscaba el agujero; el fallo solo asoma en los PR de Dependabot, la única clase de cambio que escribe un actor externo automatizado. **Qué significa:** la superficie que más merece un escaneo independiente de secretos es la única que nunca se escanea, y donde sí escanea no puede bloquear. **Ejemplo:** los PR #370–#374 se mergearon el 2026-08-03 con `Secret Detection (gitleaks)` fallando en los cinco; ninguna compuerta objetó, correctamente, porque ninguna está cableada para hacerlo. **ARREGLO (2026-08-03):** la dependencia de licencia desaparece en vez de satisfacerse — el job instala el binario pineado de gitleaks (MIT; solo el envoltorio de la action exigía licencia) y corre `gitleaks dir . --no-banner --redact --exit-code 1`, así que el punto ciego de Dependabot no puede reabrirse y no hace falta ningún secreto de admin. `continue-on-error` retirado. El guard `60-validate-secret-scan-gate.mjs` corre en cada corrida: extrae el comando del workflow, planta una credencial y exige exit 1, y exige que un árbol limpio salga 0. Su primera versión plantaba el canónico `AKIAIOSFODNN7EXAMPLE`, que gitleaks lleva como stopword — pasó sin haber visto nunca bloquear la compuerta, el mismo defecto reproducido dentro de su propia prueba. Los 15 hallazgos preexistentes eran todos sintéticos (fixtures de las pruebas de redacción y ejemplos `curl` de README) y quedan fijados en `.gitleaksignore` por huella, no por ruta. **Pendiente:** la promoción a contexto requerido en `main`/`develop`, retenida hasta que el job reporte verde en ambas — activarla antes dejaría bloqueado todo PR abierto, como pasó con el PR #218. **HECHO, observado el 2026-08-08 al medir `GT-622`:** `Secret Detection (gitleaks)` YA es contexto requerido en `main` y en `develop`, releído de branch protection — el conjunto requerido pasó de 7 a 8. Se registra aquí en vez de dejarlo como un "Pendiente" abierto, porque si no la fila seguiría describiendo como pendiente algo que ya ocurrió. | `Security` | Cross | P2 | S | `COMPLETADO` | | [`GT-654`](./gap-reference-catalog.es.md#gt-654) | **Tres servicios de un mismo producto responden `/health` en tres formas.** `core-api` devuelve el sobre ADR-0073 (`data.status = "OK"`); `mcp` y `agent-runtime` devuelven objetos planos (`status = "ok"`) — cambia el anidamiento y también la caja. **Qué significa:** quien sondee las tres trata cada una como caso especial, y una sonda escrita contra cualquiera de las formas reporta las otras dos como rotas. **Ejemplo:** el 2026-08-03 una sonda cross-cluster casó `"status":"ok"` literalmente y reportó como inalcanzables dos servicios que estaban sirviendo. **Es una decisión antes que un trabajo:** el sobre es el contrato declarado del Core, pero `/health` es lo que lee una sonda de Kubernetes y hoy están configuradas contra la forma actual. | `Evolith Core` | Cross | P2 | S | `COMPLETADO` | | [`GT-655`](./gap-reference-catalog.es.md#gt-655) | **Cuatro operaciones declaradas en las tres superficies no las ha invocado nunca ninguna prueba.** `satellite-create`, `pattern-list`, `pattern-get` y `pattern-list-by-topology` están `exposed: true` en CLI, MCP y REST, y el arnés de exploración no tiene binding para ninguna — 48 de 73 operaciones lo llevan. **Qué significa:** la matriz de paridad afirma que existen en tres superficies y nada les ha pedido nunca demostrarlo; el arnés las reporta en `uncoveredTriangleOps` en vez de redondearlas, pero reportar no es cubrir. **`satellite-create` es la difícil:** aprovisiona un repo real de GitHub y escribe el registro, así que su binding exige un camino deshacible o de dry-run. | `Evolith Core` | Cross | P2 | M | `COMPLETADO` | +| [`GT-656`](./gap-reference-catalog.es.md#gt-656) | **Un guard contra colisiones de id volvió inmutables los títulos de los gaps, así que un tablero cuyo propósito es no mentir acumuló filas cuya primera línea miente.** `49-validate-gap-id-allocation` distingue una colisión de una edición normal comparando el `**Title:**` del catálogo contra la rama base, y cualquier diferencia falla. Eso es correcto para una colisión e incorrecto para un retítulo, y el guard no podía distinguirlos — su propio mensaje lo decía y pedía la distinción "en el commit", que nada lee. **El coste ya estaba pagado, no es hipotético:** [`GT-622`](./gap-reference-catalog.es.md#gt-622) se re-midió dos veces —82 → 201 → 210 análisis, y la rama afectada resultó ser `develop`, no `main`— mientras su titular seguía diciendo "Ochenta y dos ... cada PR", porque corregirlo habría puesto en rojo un check REQUERIDO en el propio PR que traía la corrección. La re-medición del 2026-08-01 sentó el precedente arreglando la evidencia y dejando el título, y este cierre lo encontró a punto de repetirse por tercera vez. **Arreglado convirtiendo en dato el juicio humano que el guard delegaba, no ablandando el check:** un retítulo se declara en `gap-retitles.json` reproduciendo AMBOS títulos exactamente, y solo una coincidencia exacta exime. La exactitud es el diseño — no se puede escribir como un "este id puede retitularse" en bloque, así que una colisión real que caiga después sobre el mismo número sigue fallando. Las declaraciones se clasifican `active` / `spent` / `rot`, con rot fatal y un registro ilegible fatal, porque una lista de exenciones que se pudre en silencio es peor que el defecto que arregla. | `.harness` | Cross | P2 | S | `COMPLETADO` | -**Progreso:** 639 / 653 completados · 3 en progreso · 4 pendientes · 7 diferidos +**Progreso:** 641 / 654 completados · 3 en progreso · 3 pendientes · 7 diferidos **Oleada 2026-06-23 (auditoría profunda de Winston III):** Añadidos 14 gaps nuevos `GT-212`…`GT-225` del Winston Audit Playbook que cubren: higiene de estado ADR (GT-212), metadata + presupuestos operativos + corpus de guías por topología (GT-213, GT-217, GT-219), observabilidad + OpenAPI en controladores REST (GT-214, GT-215), paridad de input-schemas OPA + densidad de tests por topología (GT-216, GT-222), plantillas de rollback + on-call de Fase 05 (GT-218), cobertura de ramas CLI + paridad de envelope --format + limpieza de skip-list (GT-220, GT-224, GT-225), audit logging HTTP de MCP (GT-221), y tests e2e de paridad cross-surface (GT-223). @@ -726,6 +727,8 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida **Oleada 2026-07-03 (auditoría UltraCode de productización del harness):** Re-ejecutada la ruta de auditoría Winston/UltraCode después de limpieza. La evidencia de cobertura sigue fuerte (`run-evolith-deep.mjs --markdown` = 9/9 sólido; `run-evolith-topology.mjs --markdown` = 168/168 sobre las 8 topologías aceptadas), pero la productización real del Agent Runtime expuso 6 gaps abiertos: `GT-413` (el adaptador OPA real no puede evaluar porque schemas se cargan como datos OPA), `GT-414` (drift del namespace `policyRef`), `GT-415` (drift de superficie pública/SemVer; tests de agent-runtime en rojo), `GT-416` (manifest `.harness` expone solo 7 capacidades gobernadas de 110 activos script/playbook ejecutables), `GT-417` (tracking semántico falla porque varios gaps `COMPLETADO` carecen de registros de cierre y criterios marcados) y `GT-418` (bucle de mejora continua sembrado pero no enforced en CI/Agent Runtime). Ambos motores de reglas fueron revisados: las auditorías Native/topológicas pasan, mientras la ruta runtime OPA CLI falla cerrada hasta resolver `GT-413`/`GT-414`. +**Ola 2026-07-26 (enforcement aterrizado; GT-577 / GT-574):** **Aterrizó el enforcement, y GT-577 se cerró con evidencia de CI, no con un tick verde.** `GT-577` → COMPLETADO: el workflow de dogfood corrió en un runner real (corrida 30228607519, `develop`) con ambos jobs verdes, y el cierre se apoya en lo que asserta el log, no en el color — `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` más la mitad negativa `OK: the gate blocked as expected`. La composite action queda dogfooded y bajo regresión, que era la mitad mayor de ese gap. El registro de cierre de `GT-574` se re-verificó contra la API de GitHub y se enriqueció con los valores observados: **`enforce_admins` ahora es TRUE en `main` y en `develop`**, los contextos requeridos pasaron de 6 a 7, y **`CodeQL SAST` es requerido**, así que el hallazgo de la auditoría "0 de los 6 checks requeridos es de seguridad" ya no se sostiene; `develop`, que antes devolvía 404 *Branch not protected* siendo la rama donde aterriza todo cambio, lleva ahora los mismos siete. Es la primera vez que un peldaño *Enforced* de este corpus está respaldado por configuración y no por prosa. Registrados aparte en esta ventana: `GT-596`…`GT-600` (artefactos internacionales de deuda técnica — ISO/IEC 33020:2019, 5055:2021, 25040:2024, OMG ATDM V2, OpenSSF Scorecard/SLSA/SSDF) y `GT-601`…`GT-608` (evaluación de componentes). **Sin mover y ahora en el camino crítico:** `GT-570` — npm sigue sirviendo 1.1.0 del 2026-07-18 en los tres paquetes comprobados, así que el artefacto publicado sigue siendo anterior a la ola de seguridad que el CHANGELOG público enumera. En esta ventana no aterrizó ningún cambio en `src/`; los gaps de código están inalterados por construcción, y se re-verificó que sus fixes siguen en pie (denominador 111 comprobadas / 269 saltadas / 380 totales, un envelope de 123 KB que sobrevive a una pipe, el alias de bin `evolith`, MCP stdio invocable). Contadores recalculados desde las filas en ambos vocabularios de estado. + **Ordenamiento:** una sola tabla, ordenada por estado (pendientes luego completados), luego criticidad (`P0` → `P1` → `P2` → `P3`), luego complejidad (`XS` → `S` → `M` → `L` → `XL`). Los IDs `GT-*` enlazan al [Catálogo de Referencia de Gaps](./gap-reference-catalog.es.md); los IDs `MT-A*` enlazan al [plan de implementación Multi-Topology](../audits/multi-topology-reference-corpus-implementation-plan.es.md). --- diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index b81a927f..67af7834 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -4,7 +4,7 @@ **Status:** Active Tracking **Owner:** Evolith Architecture Board -**Last Updated:** 2026-07-26 (**Enforcement landed, and GT-577 closed on CI evidence rather than on a green tick.** `GT-577` → DONE: the dogfood workflow ran on a real runner (run 30228607519, `develop`) with both jobs green, and the closure rests on what the log asserts, not on the colour — `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` plus the negative half `OK: the gate blocked as expected`. The composite action is now dogfooded and under regression, which was the larger half of that gap. `GT-574`'s closure record was re-verified against the GitHub API and enriched with the observed values: **`enforce_admins` is now TRUE on both `main` and `develop`**, required contexts went 6 → 7, and **`CodeQL SAST` is required**, so the audit finding "0 of the 6 required checks is a security check" no longer holds; `develop`, which previously returned 404 *Branch not protected* while being the branch every change lands on, now carries the same seven. This is the first time an *Enforced* rung in this corpus is backed by configuration rather than by prose. Registered separately in this window: `GT-596`…`GT-600` (international technical-debt artifacts — ISO/IEC 33020:2019, 5055:2021, 25040:2024, OMG ATDM V2, OpenSSF Scorecard/SLSA/SSDF) and `GT-601`…`GT-608` (component assessment). **Unmoved and now the critical path:** `GT-570` — npm still serves 1.1.0 of 2026-07-18 on all three packages checked, so the published artifact still predates the security wave the public CHANGELOG enumerates. No `src/` change landed in this window; the code-level gaps are unchanged by construction, and their fixes were re-verified as still holding (denominator 111 checked / 269 skipped / 380 total, a 123 KB envelope surviving a pipe, the `evolith` bin alias, MCP stdio invocable). Counters recomputed from the rows across both status vocabularies. +**Last Updated:** 2026-08-08 (**One gap closed by doing the irreversible thing the board had deliberately deferred — and the measurement taken first is what makes it worth recording.** `GT-622` → DONE, and the irreversible half the board had deferred is what closed it: the 210 orphaned `.github/workflows/ci.yml:codeql` code-scanning analyses are deleted from `refs/heads/main` (201) and `refs/heads/develop` (9), the dead key is absent from both, and the alert corpus is byte-identical across the deletion — 242 open / 82 dismissed / 60 fixed, CodeQL 75 / Scorecard 158 / Trivy 9. **Three of that row’s own claims did not survive re-measurement, and the row had already been re-measured twice without any of them surfacing, because each pass checked the COUNT and none checked the CLAIM:** the check was `neutral`, not red, from PR #250 onward; the configuration the warning names is on `refs/heads/develop`, not `main`, so only PRs into `develop` still carried it while `main` came back clean on its own from PR #420 with all 201 orphans still in place; and the 9 analyses on `develop` — the ones that mattered — were never counted. **The field that turned an owner judgement call into an easy one had never been read:** all 210 analyses carry `results_count: 0`, so what was discarded is 210 records of “scanned, found nothing” from a configuration dead since June. The irreversibility was real; the loss was not. 22 analyses under the dead key are deliberately left on `refs/pull/{4..17}/merge`, ephemeral per-PR refs that can never be the base of a PR. **Also observed while measuring, and not part of this closure:** the required-context set on `main` and `develop` is now **8**, having gained `Secret Detection (gitleaks)` — the promotion `GT-653` recorded as its one remaining item. The closure criterion that could only be observed on a PR into `develop` was observed on PR #440, the PR carrying this very closure, 105s after it opened — written after reading the check, not before. Counters recomputed from the rows: **640 / 653 done · 3 in progress · 3 pending · 7 deferred**.) **Gap Details:** [Gap Reference Catalog](./gap-reference-catalog.md) This board is the single source of truth for technical debt, gaps, opportunities, enablers, priority, and status. Select a gap ID to open its problem statement, purpose, evidence, closure criteria, and references. @@ -41,7 +41,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-625`](./gap-reference-catalog.md#gt-625) | **The published artifact is not installable, and the workspace is what hides it.** Found by GT-571's acceptance criterion being TESTED against the registry instead of assumed. `npm i @beyondnet/evolith-cli@1.2.0` in a clean directory installs, and then `evolith-cli --version` dies with `MODULE_NOT_FOUND` on `@beyondnet/evolith-core-domain/application/paths/rulesets-location` — a deep subpath import that the published `@beyondnet/evolith-core-domain` does not expose. In this repository the same import resolves, because the workspace symlink points at the source tree rather than at what was packed. Every check we run is therefore blind to it: the CLI suite (1382 tests), the e2e suite and the exploratory tester all run inside the workspace. 1 of 36 `@beyondnet/*` specifiers imported by the shipped `dist` fails to resolve on a clean install; `src/sdk/cli/scripts/check-install-smoke.mjs`, written during Wave 2, is what detects it. This is the same class as the maturity audit's finding that a green local run is not evidence — here the hiding mechanism is npm workspaces rather than a stale `dist` or a gitignored directory. **CLOSED 2026-07-28.** `core-domain@1.2.0`, `core@1.2.0`, `infra-providers@1.2.0`, `mcp@1.2.1` and `cli@1.2.1` are on the registry, all five with attestations. Verified against the REGISTRY, not an exit code: the core-domain tarball contains `application/paths/rulesets-location`, and in a directory that has never seen this workspace `cli --version` prints 1.2.1 and the MCP boots. **The gate paid for itself on its first real run**: it stopped the release before publishing the MCP and exposed a SECOND broken published package the audit had missed — `evolith-mcp@1.2.0` crashes at boot with `PatternCatalogService is not a constructor`, because `evolith-core@1.1.0` does not export it. Two published packages were broken by one cause; this one was caught before shipping instead of four days after. Also recorded because it changes what is true: publishing core-domain@1.2.0 retroactively repairs `cli@1.2.0`, whose `^1.1.0` range now resolves to the fixed sibling — which is exactly why the ranges were tightened to `^1.2.0`, so a resolver can never pick 1.1.0 again. | `Evolith CLI` | Cross | P0 | M | `DONE` | | [`GT-624`](./gap-reference-catalog.md#gt-624) | **Carved out of [`GT-570`](./gap-reference-catalog.md#gt-570) so its remainder is tracked rather than absorbed into a closure.** 1.2.0 shipped on 2026-07-27 with provenance and the exposure is closed for anyone installing `latest` — but two of GT-570's three original criteria are not met by that, and pretending otherwise is the pattern this board keeps catching in itself (GT-12, GT-568, GT-254, GT-424). **(a) The 1.1.0 versions are not deprecated.** `npm install @beyondnet/evolith-mcp@1.1.0` still resolves the build that predates the 2026-07-23 security wave, silently, and the public CHANGELOG names the vulnerable files. Deprecating is one command per package but requires npm credentials, so it is an owner action: `npm deprecate @beyondnet/evolith-mcp@1.1.0 "Security fixes in 1.2.0 — see CHANGELOG"`, likewise for `evolith-cli@1.1.0` and `evolith-agent-runtime@1.1.0`. **(b) No release gate fails when a security-tagged commit is absent from the published tag.** That absence is exactly what let the wave sit unpublished from 2026-07-23 to 2026-07-27 while `SECURITY.md` declared the 1.1.x line "actively patched". Nothing detected it; an audit did. Related: `GT-623` — release-please derives version bumps from commit messages, and the `security(...)` type used by 2 of the last 60 commits is not a Conventional Commits type, so it contributes nothing to a bump. Both defects let a security change fail to reach a version, by different routes. **CLOSED 2026-07-30 — and the half that mattered was the gate, not the deprecations.** Criterion (a): `1.1.0` is deprecated on six packages (`evolith-cli`, `evolith-mcp`, `evolith-agent-runtime`, `evolith-core-domain`, `evolith-core`, `evolith-infra-providers`), each message naming the successor; two were deliberately left alone and the reason is a finding rather than a scope cut — `evolith-sdk@1.1.0` is what the published line actually RESOLVED (see `GT-634` (registered separately)) and `evolith-contracts@1.1.0` is that package's only version. Criteria (b) and (c): `48-validate-security-publish-lag` asks the **registry** what is published — the newest `v*` tag here is `v1.1.0` while npm serves `1.2.2`, so a tag-based gate would have reported a lag that does not exist — finds the commit where each package's published version was SET, and fails on any later commit whose **type or scope** marks it as security. It refuses prose on purpose: `fix(deps)!: … the security wave` is a dependency fix, and a gate that cries wolf gets switched off. **Two false negatives were found and fixed while building it, each pinned by a fixture:** `git log -S` matched the commit that DELETED a version string as well as the one that added it, and taking the NEWEST commit at the published version let a later dependency bump push the boundary forward and hide the window. The corrected denominator is 47 commits examined across 8 packages, against 31 for the first implementation. Observed red by `43-validate-guard-negative-fixtures` (37/37), so the fixture is one this repository has SEEN fail. | `Infra` | Cross | P1 | S | `DONE` | | [`GT-623`](./gap-reference-catalog.md#gt-623) | **The hook exists, is wired, and cannot enforce anything.** `.husky/commit-msg` runs `npx --no -- commitlint --version`; when that fails it prints `commitlint is not installed — skipping commit message lint` and exits **successfully**. Verified: `commitlint` appears in neither `dependencies` nor `devDependencies` of the root `package.json`, there is no `commitlint.config.*` or `.commitlintrc*`, and no `commitlint` key in `package.json`. So the else-branch is the only branch that ever runs, on every commit. Observed live on 2026-07-27 while merging `develop` into a feature branch. **What depends on the convention it does not enforce:** `CONTRIBUTING.md` and `.github/pull_request_template.md` mandate Conventional Commits in three places, and — the part that costs money — **release-please derives version bumps from commit messages**, wired into `sdk-cli-release.yml` and `sdk-cli-ci.yml`. **It is already drifting, with a consequence:** 2 of the last 60 non-merge commits use the type `security(...)` (`security(fase-7): add Docker/K8s hardening checklist`, `security(fase-6): add executable security rulesets`), which is not a Conventional Commits type. release-please does not recognise it, so **a commit that announces itself as a security change contributes nothing to the version bump** — which is the same failure mode as [`GT-570`](./gap-reference-catalog.md#gt-570), where a security wave sits unpublished. Fix: install and configure commitlint so the hook takes its real branch, or delete the hook and stop claiming the convention. Failing open is the worst of the three options, because it produces the appearance of enforcement. If the `security` type is wanted, declare it in the config and map it to a bump — do not leave it to a linter that never runs. **Closure (wave 2026-07-28).** commitlint is installed, configured and the hook takes its real branch — verified in both directions: a valid `feat:` message is accepted and a malformed one is **rejected**. The `security(...)` type used by real commits here is DECLARED in the config with its intended bump meaning rather than left to fail lint or be silently ignored. **This landing broke the repository three ways before it worked**, all from the agent correctly refusing to leave its owned paths: `package.json` gained the dependencies while `package-lock.json` did not, so `npm ci` failed repo-wide; the hook failed CLOSED with commitlint absent, blocking every commit; and `03-validate-root-cleanliness` rejected the new root file. All three repaired in the same wave. The lesson is about the wave, not the fix: **whoever may edit `package.json` must also be able to edit the lockfile**. | `Governance` | Cross | P2 | S | `DONE` | -| [`GT-622`](./gap-reference-catalog.md#gt-622) | **Every pull request carries a red `CodeQL` check reading "1 configuration not found", and it is not a security finding — it is orphaned bookkeeping.** GitHub still has **82 code-scanning analyses** on `refs/heads/main` under the analysis key `.github/workflows/ci.yml:codeql`. That configuration was real: commit `87f50ce3` added a `codeql` job to `ci.yml`, and `f50030cd` removed it on 2026-06-06 while gutting that workflow — the last analysis under the key is from that same day. Because the configuration is still *recorded* on `main` but nothing produces it, GitHub reports it missing on every PR. It has done so for 51 days. Verified: `code-scanning/analyses?ref=refs/heads/main` returns three analysis keys — `ci.yml:codeql` (82, last 2026-06-06), `sdk-cli-ci.yml:codeql-analysis` (159, current) and `sdk-cli-ci.yml:trivy-scan` (159, current). The scanning that matters is healthy; `CodeQL SAST` passes and is a required check. **The dead workflow itself is already deleted** (it ran a no-op `Disabled` job on every PR and push to `main` and `develop`); deleting the file does NOT clear the recorded configuration, which is why this row exists. **The remaining action is deliberately not automated:** removing the 82 analyses via `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}` is irreversible and destroys code-scanning history on a protected branch. Their historical value is nil — they describe a configuration dead since June — but discarding security-scan history is an owner decision, not a tooling one. **Why it matters beyond the noise:** a permanently red check trains reviewers to discount red checks, and `CodeQL SAST` — which shares the CodeQL name and IS required — is exactly the check nobody can afford to learn to ignore. | `Infra` | Cross | P2 | XS | `PENDING` | +| [`GT-622`](./gap-reference-catalog.md#gt-622) | **Every pull request carries a red `CodeQL` check reading "1 configuration not found", and it is not a security finding — it is orphaned bookkeeping.** GitHub still has **82 code-scanning analyses** on `refs/heads/main` under the analysis key `.github/workflows/ci.yml:codeql`. That configuration was real: commit `87f50ce3` added a `codeql` job to `ci.yml`, and `f50030cd` removed it on 2026-06-06 while gutting that workflow — the last analysis under the key is from that same day. Because the configuration is still *recorded* on `main` but nothing produces it, GitHub reports it missing on every PR. It has done so for 51 days. Verified: `code-scanning/analyses?ref=refs/heads/main` returns three analysis keys — `ci.yml:codeql` (82, last 2026-06-06), `sdk-cli-ci.yml:codeql-analysis` (159, current) and `sdk-cli-ci.yml:trivy-scan` (159, current). The scanning that matters is healthy; `CodeQL SAST` passes and is a required check. **The dead workflow itself is already deleted** (it ran a no-op `Disabled` job on every PR and push to `main` and `develop`); deleting the file does NOT clear the recorded configuration, which is why this row exists. **The remaining action is deliberately not automated:** removing the 82 analyses via `DELETE /repos/{owner}/{repo}/code-scanning/analyses/{id}` is irreversible and destroys code-scanning history on a protected branch. Their historical value is nil — they describe a configuration dead since June — but discarding security-scan history is an owner decision, not a tooling one. **Why it matters beyond the noise:** a permanently red check trains reviewers to discount red checks, and `CodeQL SAST` — which shares the CodeQL name and IS required — is exactly the check nobody can afford to learn to ignore. **CLOSED 2026-08-08, on evidence produced by the PR that carries the closure rather than on the assumption it would appear — and measuring first changed both what was deleted and why it was safe.** Three of this row's claims did not survive re-measurement. **(1) It was not red.** `gh pr checks` rendered it `skipping`, and the check-run conclusion was `neutral`, not `failure`, from PR #250 (2026-07-28) onward; only PR #217 was ever `failure`. The row's central argument — that a permanently RED check trains reviewers to discount red — describes a state that ended eleven days after the row was written, and the row was never corrected. **(2) The branch was wrong.** The warning's own text names the ref: *"1 configuration present on `refs/heads/develop` was not found"*. Only PRs targeting `develop` still carried it (#425, #427, #429, #431, #433, all 2026-08-04); PRs targeting `main` came back clean from #420 (2026-08-04) onward — **with all 201 orphans still in place**. So `main` stopped warning without anyone deleting anything, which refutes the row's causal model ("recorded on `main`, therefore reported on every PR") and means the cleanup was never the only thing that could have resolved it. `develop` — the branch every change actually lands on — was the one this row was never tracking. **(3) The 9 analyses on `develop` were never counted.** The real total was **210**, not 201. **What made the owner decision cheap, and nobody had measured it:** all 210 analyses carry `results_count: 0`. Not one contains a finding — they are 210 records of "scanned, found nothing" from a configuration dead since June. The irreversibility was real; the loss was not. **Executed:** 210 × `DELETE /code-scanning/analyses/{id}?confirm_delete=true`, each preceded by a GET asserting `analysis_key` and `results_count == 0` so no live-key analysis could be reached by an ordering mistake — 210 deleted, 0 refused, 0 errors. **Verified unchanged across the deletion:** 242 open / 82 dismissed / 60 fixed alerts before and after, and open-by-tool CodeQL 75 / Scorecard 158 / Trivy 9 — the 280 current `sdk-cli-ci.yml:codeql-analysis` analyses per branch are untouched. **Deliberately left:** 22 analyses under the dead key survive on `refs/pull/{4..17}/merge`, ephemeral per-PR refs from May that can never be the base of a PR and therefore produce no symptom; deleting them buys nothing and adds 22 irreversible calls. **Criterion 2 was stale and is restated:** it demanded that the only keys on `main` be the two from `sdk-cli-ci.yml`, but a third legitimate key has since appeared — `openssf-scorecard.yml:analysis` (9, current) — so the test is the ABSENCE of the dead key, not a count of live ones. | `Infra` | Cross | P2 | XS | `DONE` | | [`GT-609`](./gap-reference-catalog.md#gt-609) | **Authorization leak in the discovery surface.** `mcp-cache.service.ts:8` declares `toolsList: 'mcp:tools:list'` — a single literal key, with no principal, tenant or scope in it — and the list is cached BEFORE the scope filter runs. So the first caller to warm the cache decides the inventory every subsequent caller sees for the TTL: an admin warming it publishes the write-capable inventory to readers. **Verificado aquí contra el código.** Fix: key the cache by principal (a hash of scopes + tenant), or delete the cache — a discovery surface that answers from another principal's view is worse than an uncached one. Origin: finding 4.2 of the product diagnostic (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **Closure (wave 2026-07-28).** The tools/list cache no longer answers from another principal's view. Verified by the test that matters rather than by key format: warm the cache as a high-privilege principal, read as a low-privilege one, assert the second sees only its own inventory. mcp-server 409/409. | `MCP Server` | Cross | P0 | S | `DONE` | | [`GT-610`](./gap-reference-catalog.md#gt-610) | **The worst failure class available to an audit product: the right action, recorded, executed with the wrong inputs.** All three engines populate `proposedArguments` — `swarms-agent.adapter.ts:99`, `hermes-agent.adapter.ts:98`, `stub-agent-engine.adapter.ts:46` — and the service reads only `plan.proposedTool` (`agent-runtime.service.ts:168-169`). The proposed arguments are computed, carried across the port, and dropped on the floor; the skill then runs with whatever `request.parameters` held. **Verificado aquí contra el código.** Fix: merge the proposed arguments with revalidation against the skill's declared input contract before execution, and record which set was used in the trace. Origin: finding 5.5 of the product diagnostic (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **Closure (wave 2026-07-28).** The service now merges the engine's `proposedArguments` **with revalidation against the skill's declared input contract** before execution — an engine is an untrusted source, so passing them through unchecked would have swapped one defect for another — and records in the trace which set was used. agent-runtime 333/333. | `agent-runtime` | Cross | P0 | S | `DONE` | | [`GT-611`](./gap-reference-catalog.md#gt-611) | **Broader than the diagnostic reported, and broader than what GT-571 fixed.** Prompts do not live in individual commands: they go through the shared `src/sdk/cli/src/infrastructure/prompts/prompt.service.ts`, which is consumed by `init`, `validate`, `upgrade`, `phase-advance`, `adr`, `waiver`, `chat`, `enforce`, `agents` and more (`profile.command.ts` imports `@clack/prompts` directly). **Verificado aquí contra el código.** GT-571 gave `init` a defined non-interactive contract — closed stdin does not prompt, failure sets a non-zero exit code, `--format json` emits a parseable envelope and nothing else — and left every other consumer as it was. A CI step that pipes any of them into `jq` still receives an ANSI menu and reads exit 0. Fix: enforce the machine contract at the `PromptService` boundary rather than per command, so a non-TTY stdin can never produce a prompt anywhere, and add a surface-wide test that asserts it for every registered command. Origin: finding 3.1 of the product diagnostic (https://github.com/beyondnetcode/why-architecture/blob/main/docs/evolith-diagnostico-es.md). **+wave 2026-07-28 — enforced at the boundary, with one honest caveat.** The machine contract is imposed inside `PromptService` rather than command by command, so no command can open a second prompt channel — asserted by a structural invariant over every command source. **NOT closed, and the reason is deliberate:** `EVOLITH_FORCE_INTERACTIVE=1` re-enables prompting without a TTY. It exists because the unit suite must drive the interactive branches and because some terminals misreport `isTTY`, but a CI job that sets it can still hang on a prompt — so the strict reading of "a non-TTY stdin can NEVER produce a prompt" holds only absent that variable. The coverage half is also static rather than a behavioural sweep of all ~40 registered commands under a closed stdin. **CLOSED 2026-07-28 on a mutation, not on a reading.** The regression suite asserts the property where it is actually true or false: `PromptService` — the single prompt channel — refuses every interactive method when stdin is not a TTY, classified as invalid input (exit 3) rather than a tool failure, and still prompts when a TTY is present so the interactive product is unchanged. The surface half is the one that matters: no command file may open a SECOND channel. Verified by adding a command that imports `@clack/prompts` directly — the suite goes red. 19/19, and it covers commands written after it, which is why it is a scan and not a list. | `Evolith CLI` | Cross | P1 | M | `DONE` | @@ -665,12 +665,13 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-650`](./gap-reference-catalog.md#gt-650) | **The Core holds TWO unreconciled answers to "which artifacts does phase X require", so it cannot publish an artifact catalog at all.** `reference/governance/sdlc/gates/gate-f*.json` names 24 artifacts in human form with `schemaRef`/`producedBy`; `UNIVERSAL_PHASE_ARTIFACTS` in `core-domain` names 18 as slugs across only the three downstream phases. They disagree on names (`CI Pipeline` vs `ci-pipeline-result`), on membership (`source-change-set`, `architecture-drift-result` and `spec-traceability-map` exist only in the constant; `Documentation Delta` and `Acceptance Validation` only in the gates) and — worse — on PHASE: `Coverage Report` is a construction artifact in `gate-f3` and a quality artifact in the constant. Only the first corpus is reachable over HTTP, and it is not the one the evaluator uses. The satellite consequence is measured: `evolith_tracker` `GAP-004` cannot replace its `core-standin` mirror with a `core-sync` source, because there is no single catalog to sync. | `SDLC Governance` | Cross | P1 | M | `DONE` | | [`GT-651`](./gap-reference-catalog.md#gt-651) | **The marketplace-adapter half of `GT-532`, carved out so its closure does not bury it.** `GT-532` closed on its two acceptance criteria — portfolio views and per-tenant governance packages, both now true — but its TITLE also named marketplace-style adapters, which the criteria never asked for and which did not ship. Registered separately rather than left inside a closed row, because an unstated intent inside a `DONE` row is exactly the staleness this board keeps finding. **Deliberately unscoped:** what an adapter marketplace is for Evolith — a catalogue of provider connectors, a distribution channel for governance packages, or both — is a product decision, and guessing it here would produce acceptance criteria nobody agreed to. | `Tracker` | Cross | P3 | L | `PENDING` | | [`GT-652`](./gap-reference-catalog.md#gt-652) | **The wire cannot carry five fields the engine reads, and a closure note already called the DTO a "full canonical mirror".** `main.ts` runs the global ValidationPipe with `forbidNonWhitelisted: true`, so a field absent from `EvaluationContextDto` does not arrive stripped -- it 400s the whole evaluation. The controller then does `body as unknown as EvaluationContext`, a straight cast, which makes that class the REACHABLE surface of the contract. `requester`, `repositoryRevision`, `qualitySignals`, `repoFacts` and `baselineRepoFacts` are declared on `EvaluationContext` and consumed by the domain today, and none of them could be sent. **Why nobody noticed:** every unit test builds an `EvaluationContext` in TypeScript and passes, while a real caller sending the same object over HTTP is rejected -- the defect is invisible from inside the Core and only a consumer can feel it. It is what blocks `evolith_tracker` CP-04 criterion 2. **What it means:** the engine can read five pieces of information that the front door refuses to accept. **Example:** the Tracker cannot tell the Core who asked for an evaluation or which revision it judged, so both facts travel as untyped strings in a passthrough bag instead. | `Evolith Core` | Cross | P1 | S | `DONE` | -| [`GT-653`](./gap-reference-catalog.md#gt-653) | **Secret detection is structurally incapable of stopping anything, and on Dependabot PRs it does not run at all.** The `secret-detection` job in `sdk-cli-ci.yml` carries `continue-on-error: true` and is absent from the seven required contexts on `main` and `develop`, so a real leak surfaces as a red tick that no gate consults. The second half is narrower and was measured: the `GITLEAKS_LICENSE` secret exists in the Actions store, but the **Dependabot secret store is empty**, and Dependabot-triggered runs read only that store — so the licence arrives blank and the step fails before scanning. **Why nobody noticed:** on `develop`, `main` and human branches the job is green (last 8 runs), which is exactly where nobody was looking for the hole; the failure only appears on Dependabot PRs, the one class of change authored by an automated external actor. **What it means:** the surface that most deserves an independent secret scan is the one surface that never gets scanned, and even where it does scan it cannot block. **Example:** PRs #370–#374 merged on 2026-08-03 with `Secret Detection (gitleaks)` failing on all five; no gate objected, correctly, because none is wired to. **FIX (2026-08-03):** the licence dependency is gone rather than satisfied — the job installs the pinned gitleaks binary (MIT; only the action wrapper needed a licence) and runs `gitleaks dir . --no-banner --redact --exit-code 1`, so the Dependabot blind spot cannot reopen and no admin secret is needed. `continue-on-error` removed. Guard `60-validate-secret-scan-gate.mjs` runs on every CI run: it extracts the command from the workflow, plants a credential and requires exit 1, and requires a clean tree to exit 0. Its first version planted the canonical `AKIAIOSFODNN7EXAMPLE`, which gitleaks carries as a stopword — it passed having never seen the gate block, the same defect reproduced inside its own proof. The 15 pre-existing findings were all synthetic (redaction-test fixtures and README `curl` examples) and are pinned in `.gitleaksignore` by fingerprint, not by path. **Remaining:** promotion to required context on `main`/`develop`, held until the job reports green on both — flipping it early would deadlock every open PR, as PR #218 did. | `Security` | Cross | P2 | S | `DONE` | +| [`GT-653`](./gap-reference-catalog.md#gt-653) | **Secret detection is structurally incapable of stopping anything, and on Dependabot PRs it does not run at all.** The `secret-detection` job in `sdk-cli-ci.yml` carries `continue-on-error: true` and is absent from the seven required contexts on `main` and `develop`, so a real leak surfaces as a red tick that no gate consults. The second half is narrower and was measured: the `GITLEAKS_LICENSE` secret exists in the Actions store, but the **Dependabot secret store is empty**, and Dependabot-triggered runs read only that store — so the licence arrives blank and the step fails before scanning. **Why nobody noticed:** on `develop`, `main` and human branches the job is green (last 8 runs), which is exactly where nobody was looking for the hole; the failure only appears on Dependabot PRs, the one class of change authored by an automated external actor. **What it means:** the surface that most deserves an independent secret scan is the one surface that never gets scanned, and even where it does scan it cannot block. **Example:** PRs #370–#374 merged on 2026-08-03 with `Secret Detection (gitleaks)` failing on all five; no gate objected, correctly, because none is wired to. **FIX (2026-08-03):** the licence dependency is gone rather than satisfied — the job installs the pinned gitleaks binary (MIT; only the action wrapper needed a licence) and runs `gitleaks dir . --no-banner --redact --exit-code 1`, so the Dependabot blind spot cannot reopen and no admin secret is needed. `continue-on-error` removed. Guard `60-validate-secret-scan-gate.mjs` runs on every CI run: it extracts the command from the workflow, plants a credential and requires exit 1, and requires a clean tree to exit 0. Its first version planted the canonical `AKIAIOSFODNN7EXAMPLE`, which gitleaks carries as a stopword — it passed having never seen the gate block, the same defect reproduced inside its own proof. The 15 pre-existing findings were all synthetic (redaction-test fixtures and README `curl` examples) and are pinned in `.gitleaksignore` by fingerprint, not by path. **Remaining:** promotion to required context on `main`/`develop`, held until the job reports green on both — flipping it early would deadlock every open PR, as PR #218 did. **DONE, observed 2026-08-08 while measuring `GT-622`:** `Secret Detection (gitleaks)` IS now a required context on both `main` and `develop`, read back from branch protection — the required set went 7 → 8. Recorded here rather than left as an open "Remaining", because the row would otherwise go on describing as pending something that already happened. | `Security` | Cross | P2 | S | `DONE` | | [`GT-654`](./gap-reference-catalog.md#gt-654) | **Three services of one product answer `/health` in three shapes.** `core-api` returns the ADR-0073 envelope (`data.status = "OK"`); `mcp` and `agent-runtime` return bare objects (`status = "ok"`) — the nesting differs and so does the case. **What it means:** anything probing all three special-cases each one, and a probe written against either shape reports the other two as broken. **Example:** on 2026-08-03 a cross-cluster probe matched `"status":"ok"` literally and reported two healthy services as unreachable while they were serving. **A decision before it is work:** the envelope is the Core's stated contract, but `/health` is what a Kubernetes probe reads and those are configured against today's shape. | `Evolith Core` | Cross | P2 | S | `DONE` | | [`GT-655`](./gap-reference-catalog.md#gt-655) | **Four operations declared on all three surfaces have never been invoked by any test.** `satellite-create`, `pattern-list`, `pattern-get` and `pattern-list-by-topology` are `exposed: true` on CLI, MCP and REST, and the exploration harness has no binding for any of them — 48 of 73 operations carry one. **What it means:** the parity matrix asserts they exist on three surfaces and nothing has ever asked them to prove it; the harness reports them in `uncoveredTriangleOps` rather than rounding them away, but reporting is not covering. **`satellite-create` is the hard one:** it provisions a live GitHub repo and writes the registry, so a binding needs an undoable or dry-run path. | `Evolith Core` | Cross | P2 | M | `DONE` | +| [`GT-656`](./gap-reference-catalog.md#gt-656) | **A guard against id collisions made gap titles immutable, so a board whose purpose is not lying accumulated rows whose first line lies.** `49-validate-gap-id-allocation` discriminates a collision from an ordinary edit by comparing the catalog `**Title:**` against the base branch, and any difference fails. That is right for a collision and wrong for a retitle, and the guard could not tell them apart — its own message said so and asked for the distinction "in the commit", which nothing reads. **The cost was already paid, not hypothetical:** [`GT-622`](./gap-reference-catalog.md#gt-622) was re-measured twice — 82 → 201 → 210 analyses, and the branch it affects turned out to be `develop`, not `main` — while its headline went on saying "Eighty-two ... every PR", because correcting it would have turned a REQUIRED check red on the PR carrying the correction. The 2026-08-01 re-measure set the precedent by fixing the evidence and leaving the title, and this closure found it about to be repeated a third time. **Fixed by making the deferred human judgement into data rather than by softening the check:** a retitle is declared in `gap-retitles.json` reproducing BOTH titles exactly, and only an exact match exempts. The exactness is the design — it cannot be written as a blanket "this id may be retitled", so a genuine collision later landing on the same number still fails. Declarations are themselves classified `active` / `spent` / `rot`, with rot fatal and an unparseable registry fatal, because an exemption list that rots silently is worse than the defect it fixes. | `.harness` | Cross | P2 | S | `DONE` | -**Progress:** 639 / 653 done · 3 in progress · 4 pending · 7 deferred +**Progress:** 641 / 654 done · 3 in progress · 3 pending · 7 deferred **Wave 2026-06-23 (Winston deep audit III):** Added 14 new gaps `GT-212`…`GT-225` from the Winston Audit Playbook covering: ADR status hygiene (GT-212), topology manifest metadata + operational budgets + guidance corpus (GT-213, GT-217, GT-219), REST controller observability + OpenAPI (GT-214, GT-215), OPA input-schema parity + per-topology test density (GT-216, GT-222), SDLC Phase 05 rollback + on-call templates (GT-218), CLI branch coverage + envelope format coverage + skip-list cleanup (GT-220, GT-224, GT-225), MCP HTTP audit logging (GT-221), and cross-surface parity e2e tests (GT-223). @@ -748,6 +749,8 @@ This board is the single source of truth for technical debt, gaps, opportunities **Wave 2026-07-03 (UltraCode harness productization audit):** Re-ran the Winston/UltraCode audit path after cleanup. Coverage evidence stayed strong (`run-evolith-deep.mjs --markdown` = 9/9 solid; `run-evolith-topology.mjs --markdown` = 168/168 across all 8 accepted topologies), but real Agent Runtime productization surfaced 6 new open gaps: `GT-413` (real OPA adapter cannot evaluate because schemas are loaded as OPA data), `GT-414` (policyRef namespace drift), `GT-415` (public-surface/SemVer drift; agent-runtime tests red), `GT-416` (`.harness` manifest exposes only 7 governed capabilities out of 110 executable script/playbook assets), `GT-417` (semantic tracking fails because several `DONE` gaps lack closure registry records and checked criteria), and `GT-418` (self-improving loop seeded but not enforced in CI/Agent Runtime). Both rule engines were checked: Native/topology audits pass, while the runtime OPA CLI path fails closed until `GT-413`/`GT-414` are fixed. +**Wave 2026-07-26 (enforcement landed; GT-577 / GT-574):** **Enforcement landed, and GT-577 closed on CI evidence rather than on a green tick.** `GT-577` → DONE: the dogfood workflow ran on a real runner (run 30228607519, `develop`) with both jobs green, and the closure rests on what the log asserts, not on the colour — `OK: 34 blocking violation(s) of 274 issue(s), agreeing with the report.` plus the negative half `OK: the gate blocked as expected`. The composite action is now dogfooded and under regression, which was the larger half of that gap. `GT-574`'s closure record was re-verified against the GitHub API and enriched with the observed values: **`enforce_admins` is now TRUE on both `main` and `develop`**, required contexts went 6 → 7, and **`CodeQL SAST` is required**, so the audit finding "0 of the 6 required checks is a security check" no longer holds; `develop`, which previously returned 404 *Branch not protected* while being the branch every change lands on, now carries the same seven. This is the first time an *Enforced* rung in this corpus is backed by configuration rather than by prose. Registered separately in this window: `GT-596`…`GT-600` (international technical-debt artifacts — ISO/IEC 33020:2019, 5055:2021, 25040:2024, OMG ATDM V2, OpenSSF Scorecard/SLSA/SSDF) and `GT-601`…`GT-608` (component assessment). **Unmoved and now the critical path:** `GT-570` — npm still serves 1.1.0 of 2026-07-18 on all three packages checked, so the published artifact still predates the security wave the public CHANGELOG enumerates. No `src/` change landed in this window; the code-level gaps are unchanged by construction, and their fixes were re-verified as still holding (denominator 111 checked / 269 skipped / 380 total, a 123 KB envelope surviving a pipe, the `evolith` bin alias, MCP stdio invocable). Counters recomputed from the rows across both status vocabularies. + **Ordering:** one table, ordered by status (pending then completed), then criticality (`P0` → `P1` → `P2` → `P3`), then complexity (`XS` → `S` → `M` → `L` → `XL`). `GT-*` IDs link to the [Gap Reference Catalog](./gap-reference-catalog.md); `MT-A*` IDs link to the supporting [Multi-Topology implementation plan](../audits/multi-topology-reference-corpus-implementation-plan.md). --- 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 6fcc34b2..50d08c73 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -29,7 +29,7 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | 2 | Área de mayor riesgo | `Cross` tiene la mayor carga ponderada abierta. | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448) | | 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | - | | 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448) | -| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-536](../gaps/gap-reference-catalog.es.md#gt-536), [GT-443](../gaps/gap-reference-catalog.es.md#gt-443), [GT-588](../gaps/gap-reference-catalog.es.md#gt-588), +1 | +| 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-536](../gaps/gap-reference-catalog.es.md#gt-536), [GT-443](../gaps/gap-reference-catalog.es.md#gt-443), [GT-588](../gaps/gap-reference-catalog.es.md#gt-588), [GT-600](../gaps/gap-reference-catalog.es.md#gt-600) | ## Bloqueadores Actuales @@ -41,22 +41,22 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| -| Fecha canónica del tablero | 2026-07-26 | -| Gaps totales | 653 | -| Gaps cerrados | 639 | -| Gaps pendientes | 14 | +| Fecha canónica del tablero | 2026-08-08 | +| Gaps totales | 654 | +| Gaps cerrados | 641 | +| Gaps pendientes | 13 | | P0 abiertos | 1 | | P1 abiertos | 3 | -| P2 abiertos | 7 | -| Cierre total | 97.9% | -| Registros de evidencia de cierre | 621 | +| P2 abiertos | 6 | +| Cierre total | 98% | +| Registros de evidencia de cierre | 623 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | |---|---:|---:|---:|---| | `Cross` | 2 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448) | -| `Infra` | 3 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-622](../gaps/gap-reference-catalog.es.md#gt-622), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464) | | `Governance` | 2 | 0 | 1 | [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-588](../gaps/gap-reference-catalog.es.md#gt-588) | +| `Infra` | 2 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464) | | `Evolith Core` | 1 | 0 | 0 | [GT-600](../gaps/gap-reference-catalog.es.md#gt-600) | | `infra-providers` | 1 | 0 | 0 | [GT-536](../gaps/gap-reference-catalog.es.md#gt-536) | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 838c8a22..acae7d2e 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -29,7 +29,7 @@ Use this summary with a simple rule: if you need context, open only the linked I | 2 | Highest-risk area | `Cross` has the largest weighted open load. | [GT-435](../gaps/gap-reference-catalog.md#gt-435), [GT-448](../gaps/gap-reference-catalog.md#gt-448) | | 3 | Quick wins | High criticality with XS/S complexity. | - | | 4 | P1 wave | Next hardening after P0 is cleared. | [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-448](../gaps/gap-reference-catalog.md#gt-448) | -| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-622](../gaps/gap-reference-catalog.md#gt-622), [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-536](../gaps/gap-reference-catalog.md#gt-536), [GT-443](../gaps/gap-reference-catalog.md#gt-443), [GT-588](../gaps/gap-reference-catalog.md#gt-588), +1 | +| 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-536](../gaps/gap-reference-catalog.md#gt-536), [GT-443](../gaps/gap-reference-catalog.md#gt-443), [GT-588](../gaps/gap-reference-catalog.md#gt-588), [GT-600](../gaps/gap-reference-catalog.md#gt-600) | ## Current Blockers @@ -41,22 +41,22 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| -| Canonical board date | 2026-07-26 | -| Total gaps | 653 | -| Closed gaps | 639 | -| Open gaps | 14 | +| Canonical board date | 2026-08-08 | +| Total gaps | 654 | +| Closed gaps | 641 | +| Open gaps | 13 | | Open P0 | 1 | | Open P1 | 3 | -| Open P2 | 7 | -| Total closure | 97.9% | -| Closure evidence records | 621 | +| Open P2 | 6 | +| Total closure | 98% | +| Closure evidence records | 623 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | |---|---:|---:|---:|---| | `Cross` | 2 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.md#gt-435), [GT-448](../gaps/gap-reference-catalog.md#gt-448) | -| `Infra` | 3 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-622](../gaps/gap-reference-catalog.md#gt-622), [GT-464](../gaps/gap-reference-catalog.md#gt-464) | | `Governance` | 2 | 0 | 1 | [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-588](../gaps/gap-reference-catalog.md#gt-588) | +| `Infra` | 2 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-464](../gaps/gap-reference-catalog.md#gt-464) | | `Evolith Core` | 1 | 0 | 0 | [GT-600](../gaps/gap-reference-catalog.md#gt-600) | | `infra-providers` | 1 | 0 | 0 | [GT-536](../gaps/gap-reference-catalog.md#gt-536) | diff --git a/reference/core/control-center/maturity-reports/maturity-evidence.json b/reference/core/control-center/maturity-reports/maturity-evidence.json index cef186c1..1381650b 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-07-26", + "asOf": "2026-08-08", "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 8499bb9e..9d952d9a 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -1,16 +1,16 @@ { "schemaVersion": "1.0.0", "scope": "evolith-core", - "asOf": "2026-07-26", + "asOf": "2026-08-08", "gaps": { - "total": 653, - "done": 639, - "pending": 4, + "total": 654, + "done": 641, + "pending": 3, "inProgress": 3, "deferred": 7 }, "evidence": { - "closureRecords": 621, + "closureRecords": 623, "cliPackage": "@beyondnet/evolith-cli@1.2.2", "adrCount": 140, "rulesetCount": 177,