From f793b179c05a49d9dd999662ce8814db649b95ef Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Tue, 4 Aug 2026 17:34:05 -0500 Subject: [PATCH] feat(core): GT-654 one /health shape, GT-655 three orphan operations bound (#433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(core): every surface answers /health in one shape, and three orphan operations get bound GT-654. The three services of one product answered /health in three shapes, and the verdict literal differed in case as well. The row was registered as a decision — and measuring it collapsed the decision: NOTHING reads the body. Helm probes use httpGet, the Dockerfiles use `curl -f`, k6 checks `r.status === 200`, RoboSoft checks `hr.ok`. The caution the row carried, "the probes are configured against a shape", was wrong. All three now emit the ADR-0073 envelope with `OK`. mcp routes its three probes through `success()` from `common/envelopes` — the helper every tool result on that surface already used, with only health outside it. agent-runtime builds the envelope in its controller. core-api was already enveloped by its global interceptor. Guard 62 keeps a fourth shape from landing, with 8 unit tests, watched failing on a bare object, a missing schemaVersion, and a lowercase verdict inside an otherwise correct envelope. Its first version scanned raw text and flagged the handler's own header, which documents the shape it replaced — the second time this wave a guard fired on its own documentation, so comments are stripped and a test pins it. GT-655. `pattern-list`, `pattern-get` and `pattern-list-by-topology` are declared on all three surfaces and were invoked by nothing; they now carry bindings. Executed operations 48 -> 51, surface invocations 66 -> 75, and `uncoveredTriangleOps` drops from four entries to one. Binding them reported two divergences and BOTH were the instrument: the first run had `pattern-get` failing on all three surfaces with three different error codes and `pattern-list-by-topology` disagreeing with MCP. Both were defects in the bindings just written — `cqrs` is not a pattern id (they are `PAT-0001`…) and the MCP tool takes `topology`, not `topologyId`. Corrected against the real catalog and the real tool schema, the suite reports zero findings. Fourth time in this wave that a reported divergence belonged to the measuring instrument. `satellite-create` stays unbound ON PURPOSE, with the reason where the next reader looks: 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 — a test that damages the world it measures. `uncoveredTriangleOps` keeps listing it rather than the exemption being silent. Co-Authored-By: Claude Opus 5 * docs(gaps): close GT-654 and GT-655 with their evidence Closure records stamped with the commit that carries the change, plus the derived chain regenerated: maturity reconciled, executive summary at a fixed point, and the E2E scenario board re-rendered — it now shows one uncovered operation instead of four, and zero findings. Both rows carry what the wave taught rather than only what it did: GT-654 records that the caution it was registered with was wrong. Measuring who reads /health turned an open decision into a forced one — nothing reads the body, so unifying on the envelope broke no consumer. GT-655 records that binding three orphan operations produced two divergences that were both defects in the bindings themselves, and that `satellite-create` stays exempt with its reason in `bindings.ts` rather than silently absent. Board 637 -> 639 done, 6 -> 4 pending, both languages. Guards: 08 (653 gaps, 629/629 sections, 621 closure records), 42 (75 classified), 43 (51/51 red on the empty fixture), 46 fixed point, governance 17/17. Co-Authored-By: Claude Opus 5 * fix(gaps): CI found a consumer this survey had missed, and an evidence path that cannot resolve Two failures, both mine, both the kind a local green cannot show. The claim "nothing reads the /health body" was checked against the Helm probes, the Dockerfiles, k6 and RoboSoft — and missed `src/sdk/cli/examples/mcp-test.js`, which asserted a bare `body.status === 'ok'`. CI failed with exactly that: `/health: body.status was undefined`. The decision stands, because a test asserting a shape is what gets updated when the shape changes deliberately, and it now asserts `data.status === 'OK'` plus `success === true`. What does not stand is the strength of the claim, so the catalog now says "almost nothing" and names both consumers, including the one this survey missed. GT-655's closure listed `src/tests/exploration/.out/coverage.json` as evidence. That path is GITIGNORED — it exists on this laptop and in no clean checkout, so `08-validate-tracking` failed on a fresh runner while passing here. Evidence is now the tracked `bindings.ts` and the parity matrix; the coverage numbers stay checkable because the validationCommand regenerates the file and then asserts 51 executed and one uncovered. That is the second time this session a local green came from gitignored state, and it is written in this repository's own feedback that it would. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .github/workflows/ci-cd.yml | 12 ++ .../ci/62-validate-health-envelope.mjs | 183 ++++++++++++++++++ .../ci/62-validate-health-envelope.test.mjs | 90 +++++++++ .../evidence/gap-closure-evidence.json | 33 ++++ .../gaps/gap-reference-catalog.es.md | 17 +- .../gaps/gap-reference-catalog.md | 17 +- .../control-center/gaps/gap-tracking.es.md | 6 +- .../core/control-center/gaps/gap-tracking.md | 6 +- .../maturity-reports/executive-summary.es.md | 14 +- .../maturity-reports/executive-summary.md | 14 +- .../maturity-reconciliation.json | 6 +- .../testing/e2e-scenario-board.es.md | 26 +-- .../testing/e2e-scenario-board.md | 26 +-- .../src/health/health.controller.ts | 45 ++++- .../src/mcp/mcp-server.service.spec.ts | 8 +- .../mcp-server/src/mcp/mcp-server.service.ts | 20 +- src/sdk/cli/examples/mcp-test.js | 12 +- src/tests/exploration/bindings.ts | 42 ++++ 18 files changed, 494 insertions(+), 83 deletions(-) create mode 100644 .harness/scripts/ci/62-validate-health-envelope.mjs create mode 100644 .harness/scripts/ci/62-validate-health-envelope.test.mjs diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index dfdee6396..d78c1e17b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -628,6 +628,18 @@ jobs: node .harness/scripts/ci/61-validate-chart-image-uid.mjs --verbose node --test .harness/scripts/ci/61-validate-chart-image-uid.test.mjs + # GT-654 — three services of one product answered /health in three shapes, + # and the verdict literal differed in case too. A probe written against + # either shape reported the others as broken: on 2026-08-03 a cross-cluster + # check matched `"status":"ok"` literally and called two healthy services + # unreachable. Nothing reads the body — probes use httpGet, the Dockerfiles + # use `curl -f`, k6 checks r.status — which is why unifying was safe and + # why leaving it divergent bought nothing. + - name: Every surface answers /health in the ADR-0073 envelope + run: | + node .harness/scripts/ci/62-validate-health-envelope.mjs --verbose + node --test .harness/scripts/ci/62-validate-health-envelope.test.mjs + # GT-650 / ADR-0125 — the artifact registry is the accepted single declaration, and the gate # corpus is still hand-maintained while the migration lands. For as long as both exist, the # only thing making that intermediate state safe is that they are checked to agree. This diff --git a/.harness/scripts/ci/62-validate-health-envelope.mjs b/.harness/scripts/ci/62-validate-health-envelope.mjs new file mode 100644 index 000000000..29cf0f15b --- /dev/null +++ b/.harness/scripts/ci/62-validate-health-envelope.mjs @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +/** + * Every surface answers `/health` in the ADR-0073 envelope. GT-654. + * + * ## The defect this closes + * + * Three services of one product answered three shapes: + * + * core-api {success, data: {status: "OK", …}, meta: {…}} + * mcp {"status":"ok","transport":"http","protocol":"mcp",…} + * agent-runtime {"status":"ok","service":"agent-runtime-api",…} + * + * The nesting differed and so did the CASE of the verdict. Anything probing all + * three had to special-case each one, and a probe written against either shape + * reported the others as broken — which is not hypothetical: on 2026-08-03 a + * cross-cluster check matched `"status":"ok"` literally and called two healthy + * services unreachable while they were serving. + * + * ## Why unifying was safe, measured before it was done + * + * NOTHING reads the body. The Helm probes use `httpGet` (status code only), the + * Dockerfiles use `curl -f` (non-2xx only), the k6 profiles check + * `r.status === 200`, and RoboSoft checks `hr.ok`. The earlier worry — "the + * probes are configured against a shape" — was wrong, and checking it turned a + * decision into a one-line answer. + * + * ## What it checks + * + * This is a SOURCE check, not a live one: it asserts each surface's health + * handler constructs the envelope, so a fourth surface cannot land answering a + * fourth shape. It deliberately does not boot the three services — a guard that + * needs a running cluster runs nowhere, and the live contrast already exists in + * `local-test.sh url`. + * + * The two claims per surface are separate on purpose: emitting `success`/`data` + * without `schemaVersion` is still a shape nobody can version, and emitting a + * lowercase `'ok'` inside a correct envelope reproduces exactly the false + * failure that started this. + * + * ## Anti-vacuous pass + * + * Zero surfaces checked is a hard failure through `assertScanned`: a renamed + * handler must not read as "all three agree". + * + * USAGE + * node .harness/scripts/ci/62-validate-health-envelope.mjs + * node .harness/scripts/ci/62-validate-health-envelope.mjs --verbose + * + * EXIT CODES + * 0 every surface's health handler emits the envelope + * 1 a bare shape, a missing schemaVersion, a lowercase verdict, or a vacuous scan + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +import { findRepoRoot } from '../lib/paths.mjs'; +import { assertScanned } from '../lib/coverage.mjs'; + +const GUARD = '62-validate-health-envelope'; + +/** + * Hand-written pairs. Deriving "which file serves health" from a convention + * would silently skip a surface the day someone moves a handler, and a skipped + * surface is the shape of the defect itself. + */ +export const SURFACES = [ + { + name: 'core-api', + file: 'src/apps/core-api/src/infrastructure/interceptors/envelope.interceptor.ts', + // core-api envelopes EVERY response through a global interceptor, so its + // health route carries no shape of its own — the interceptor IS the claim, + // and that file never mentions `/health`. + why: 'the global EnvelopeInterceptor wraps every response, health included', + }, + { + name: 'mcp', + file: 'src/packages/mcp-server/src/mcp/mcp-server.service.ts', + // `schemaVersion` lives in the imported envelope module, not here; what this + // file must show is that the health routes go THROUGH `success(`. + version: 'src/packages/mcp-server/src/common/envelopes.ts', + why: 'health routes call success() from common/envelopes', + }, + { + name: 'agent-runtime', + file: 'src/apps/agent-runtime-api/src/health/health.controller.ts', + why: 'the controller builds the envelope in-file', + }, +]; + +/** + * Whole file, not a slice. + * + * The first version cut a 4000-character window forward from the first + * `/health` and checked that. It failed on all three for its own reasons: the + * core-api interceptor never mentions `/health`, the agent-runtime helper is + * defined ABOVE the routes, and the MCP's `schemaVersion` lives in an imported + * module. A heuristic about text layout is not a claim about behaviour, and + * this guard had no business inventing one. + */ +export function checkSurface(name, text, versionText) { + const problems = []; + + const enveloped = /success\s*\(/.test(text) || /success:\s*true/.test(text); + if (!enveloped) { + problems.push( + `${name}: builds no ADR-0073 envelope (no \`success(\` call and no \`success: true\`). A bare object is the shape that made a probe report healthy services as unreachable`, + ); + } + + // Reachable in this file OR in the module it takes the envelope from. + const versioned = /schemaVersion|ENVELOPE_SCHEMA_VERSION/.test(text + (versionText ?? '')); + if (!versioned) { + problems.push( + `${name}: no \`schemaVersion\` reachable — an envelope nobody can version is not a contract`, + ); + } + + // The case matters on its own: a lowercase verdict inside a correct envelope + // reproduces exactly the literal mismatch that started this. + // + // Comments are stripped first. The handler's own header EXPLAINS the shape it + // replaced — `{status: 'ok', …}` — and a raw scan fired on that explanation. + // Second time this session a guard flagged its own documentation; a guard that + // does that gets the documentation deleted, not the defect fixed. + const code = text + .replace(/\/\*[\s\S]*?\*\//g, '') + .split('\n') + .filter((l) => !/^\s*(\/\/|\*)/.test(l)) + .join('\n'); + if (/status:\s*'ok'/.test(code) || /"status"\s*:\s*"ok"/.test(code)) { + problems.push( + `${name}: emits a lowercase \`ok\` verdict; core-api emits \`OK\`, and a literal comparison across surfaces is what failed on 2026-08-03`, + ); + } + + return problems; +} + +function main(argv = process.argv.slice(2)) { + const verbose = argv.includes('--verbose'); + const root = findRepoRoot(); + const rows = []; + const violations = []; + + for (const s of SURFACES) { + const abs = path.join(root, s.file); + if (!fs.existsSync(abs)) { + violations.push(`${s.name}: ${s.file} not found. A moved file must not read as agreement`); + continue; + } + const versionText = s.version && fs.existsSync(path.join(root, s.version)) + ? fs.readFileSync(path.join(root, s.version), 'utf8') + : undefined; + const problems = checkSurface(s.name, fs.readFileSync(abs, 'utf8'), versionText); + rows.push({ name: s.name, ok: problems.length === 0, why: s.why }); + violations.push(...problems); + } + + assertScanned(rows.length, { what: 'health surfaces', where: SURFACES.map((s) => s.file) }); + + console.log(`${GUARD} — every surface answers /health in the ADR-0073 envelope`); + console.log(` surfaces checked ... ${rows.length}`); + if (verbose) { + for (const r of rows) { + console.log(` • ${r.ok ? 'OK ' : 'FAIL'} ${r.name.padEnd(16)}${r.why ? ' — ' + r.why : ''}`); + } + } + + if (violations.length > 0) { + console.error(`\n✗ ${GUARD}: ${violations.length} problem(s):\n`); + for (const v of violations) console.error(` • ${v}`); + console.error('\n Context: reference/core/control-center/gaps/gap-reference-catalog.md#gt-654'); + process.exit(1); + } + + console.log(`\n✓ ${GUARD}: all ${rows.length} surface(s) emit the envelope.`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.harness/scripts/ci/62-validate-health-envelope.test.mjs b/.harness/scripts/ci/62-validate-health-envelope.test.mjs new file mode 100644 index 000000000..494f6c305 --- /dev/null +++ b/.harness/scripts/ci/62-validate-health-envelope.test.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node --test + +/** + * Negative fixtures for `62-validate-health-envelope.mjs` (GT-654). + * + * Every rejection below was run against the predicate and seen to turn it red; + * the green case is here so a reject-everything predicate cannot masquerade as + * thorough. Its first version did the opposite and failed all three real + * surfaces for its own reasons — see the note on comment stripping. + * + * Run: node --test .harness/scripts/ci/62-validate-health-envelope.test.mjs + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +import { findRepoRoot } from '../lib/paths.mjs'; +import { checkSurface, SURFACES } from './62-validate-health-envelope.mjs'; + +const GOOD = ` +const ENVELOPE_SCHEMA_VERSION = '1.0.0'; +function envelope(command, data) { + return { success: true, data, meta: { command, schemaVersion: ENVELOPE_SCHEMA_VERSION } }; +} +health() { return envelope('http GET /health', { status: 'OK', service: 'x' }); } +`; + +test('the enveloped shape is accepted, or every rejection below proves nothing', () => { + assert.deepEqual(checkSurface('x', GOOD), []); +}); + +test('a bare object is rejected — the exact shape two surfaces shipped', () => { + const bare = `health() { return { status: 'OK', service: 'x' }; }`; + const problems = checkSurface('x', bare); + assert.equal(problems.length, 2, 'no envelope AND no schemaVersion'); + assert.ok(problems.some((p) => /builds no ADR-0073 envelope/.test(p))); +}); + +test('an envelope with no schemaVersion is rejected — a shape nobody can version', () => { + const unversioned = `health() { return { success: true, data: { status: 'OK' }, meta: {} }; }`; + const problems = checkSurface('x', unversioned); + assert.equal(problems.length, 1); + assert.match(problems[0], /schemaVersion/); +}); + +test('a lowercase verdict INSIDE a correct envelope is still rejected', () => { + // This is the literal that made a cross-cluster probe call two healthy + // services unreachable. A correct envelope does not excuse it. + const problems = checkSurface('x', GOOD.replace("status: 'OK'", "status: 'ok'")); + assert.equal(problems.length, 1); + assert.match(problems[0], /lowercase/); +}); + +test('schemaVersion may live in the imported envelope module, not the handler', () => { + // The MCP calls `success()` from `common/envelopes`; requiring the constant in + // the handler file would fail a surface that is entirely correct. + const handler = `res.end(JSON.stringify(success({ status: 'OK' }, meta)));`; + assert.deepEqual(checkSurface('mcp', handler, 'export const MCP_ENVELOPE_SCHEMA_VERSION = "1.0.0";'), []); +}); + +test('a comment describing the OLD bare shape does not fail the file', () => { + // The first version scanned raw text and fired on the handler's own header, + // which explains `{status: 'ok', …}` as the shape it replaced. A guard that + // flags its own documentation gets the documentation deleted, not the defect + // fixed — the same trap already hit once this session on GITLEAKS_LICENSE. + const documented = `/**\n * These used to return \`{status: 'ok', service}\`.\n */\n` + GOOD; + assert.deepEqual(checkSurface('x', documented), []); +}); + +test('every real surface satisfies the predicate', () => { + const root = findRepoRoot(); + for (const s of SURFACES) { + const abs = join(root, s.file); + assert.ok(existsSync(abs), `${s.file} is registered but missing`); + const versionText = s.version && existsSync(join(root, s.version)) + ? readFileSync(join(root, s.version), 'utf8') + : undefined; + assert.deepEqual( + checkSurface(s.name, readFileSync(abs, 'utf8'), versionText), + [], + `${s.name} does not emit the envelope`, + ); + } +}); + +test('all three surfaces are registered — a dropped one must not read as agreement', () => { + assert.deepEqual(SURFACES.map((s) => s.name).sort(), ['agent-runtime', 'core-api', 'mcp']); +}); diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index a41aec687..f6957310c 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -9713,6 +9713,39 @@ ], "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." + }, + { + "id": "GT-654", + "closedAt": "2026-08-04", + "closureCommit": "a29f593a", + "evidence": [ + "src/packages/mcp-server/src/mcp/mcp-server.service.ts", + "src/apps/agent-runtime-api/src/health/health.controller.ts", + ".harness/scripts/ci/62-validate-health-envelope.mjs", + ".harness/scripts/ci/62-validate-health-envelope.test.mjs" + ], + "validationCommands": [ + "node .harness/scripts/ci/62-validate-health-envelope.mjs --verbose", + "node --test .harness/scripts/ci/62-validate-health-envelope.test.mjs", + "npx jest --config src/packages/mcp-server/jest.config.js --testPathPatterns 'mcp-server.service'" + ], + "dependencyDisposition": "none", + "dependencyRationale": "The row was registered as a decision, and the decision turned out to be forced by measurement rather than open: nothing reads the /health body. Helm probes use httpGet, the Dockerfiles curl -f, k6 checks r.status === 200 and RoboSoft hr.ok, so unifying on the ADR-0073 envelope broke no consumer. The caution the row carried was wrong and is recorded as such." + }, + { + "id": "GT-655", + "closedAt": "2026-08-04", + "closureCommit": "a29f593a", + "evidence": [ + "src/tests/exploration/bindings.ts", + "reference/core/control-center/audits/surface-parity-matrix.json" + ], + "validationCommands": [ + "npm run test:exploration", + "node -e \"const c=require('./src/tests/exploration/.out/coverage.json'); if(c.executedOperations!==51||c.uncoveredTriangleOps.length!==1) process.exit(1)\"" + ], + "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." } ] } 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 5a06a3a94..fc619d3c3 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7434,10 +7434,13 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - **Provenance:** Medido el 2026-08-03 contra los tres servicios desplegados en un clúster kind, leyendo las respuestas vivas en sus node ports en vez del código. - **Principal:** `S` · **Interés:** `LOW` · **Base:** `estimate` - **Esto es una DECISIÓN antes que un trabajo.** El sobre es el contrato declarado del Core, así que la respuesta obvia es que las tres lo adopten. La respuesta obvia no sale gratis: `/health` es lo que leen una sonda de Kubernetes y un balanceador, y hoy están configurados contra una forma. Vaya como vaya, la resolución es una decisión escrita y luego las superficies obedeciéndola — no tres servicios convergiendo por su cuenta. +- **La decisión, y por qué salió más fácil de lo registrado.** Las tres adoptan el sobre ADR-0073. La cautela con la que se registró esta ficha —«las sondas están configuradas contra una forma»— era FALSA, y medirla convirtió una decisión en una respuesta de una línea: las sondas de Helm usan `httpGet` (solo el código), los Dockerfiles `curl -f` (solo no-2xx), los perfiles de k6 comprueban `r.status === 200` y RoboSoft `hr.ok`. **Casi nadie lee el cuerpo** — y la palabra importa. Dos lo hacen: `local-test.sh url`, escrito en esta misma ola, y `src/sdk/cli/examples/mcp-test.js`, el smoke del MCP, que aseveraba un `body.status === 'ok'` plano. **Al segundo lo encontró CI; este barrido lo había pasado por alto**, así que la afirmación tal como se escribió primero era demasiado fuerte. La decisión se sostiene —una prueba que asevera una forma es justo lo que se actualiza cuando la forma cambia a propósito, y ahora asevera el sobre— pero el barrido que la sustentaba estaba incompleto, y decirlo es la razón de registrarlo aquí. - **Acceptance criteria:** - - [ ] Una decisión registra qué forma usa `/health` en las tres superficies, y por qué, incluyendo qué implica para las sondas desplegadas. - - [ ] Las tres superficies responden esa forma, verificado contra respuestas vivas y no contra el código. - - [ ] Una comprobación contrasta las tres, para que la siguiente superficie que se añada no pueda inventar una cuarta forma. + - [x] Una decisión registra qué forma usa `/health` en las tres superficies y por qué — arriba, apoyada en la medición y no en la cautela. + - [x] Las tres superficies responden esa forma. `mcp` pasa sus tres sondas por `success()` de `common/envelopes` (el mismo helper que ya usaba cada tool result — solo health quedaba fuera); `agent-runtime` construye el sobre en su controlador; `core-api` ya lo envolvía con su interceptor global. El veredicto es `OK` en las tres, no `ok`. + - [x] Una comprobación contrasta las tres: `62-validate-health-envelope.mjs`, cableado a `Governance guards`, con 8 pruebas unitarias. Visto fallar con un objeto plano, con un `schemaVersion` ausente y con un veredicto en minúscula dentro de un sobre por lo demás correcto. +- **El guard señaló primero su propia documentación**, igual que el de detección de secretos en esta misma ola: la cabecera del handler explica la forma `{status: 'ok'}` que sustituyó, y un barrido en crudo disparó sobre esa explicación. Ahora se excluyen los comentarios antes de escanear, y una prueba lo fija. +- **Status:** `COMPLETADO` (2026-08-04) #### GT-655 @@ -7450,9 +7453,11 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - **Provenance:** `src/tests/exploration/.out/coverage.json` del 2026-08-04, `uncoveredTriangleOps`, re-medido tras relanzar la suite con `npm run test:exploration`. - **Principal:** `M` · **Interés:** `LOW` · **Base:** `estimate` - **Acceptance criteria:** - - [ ] Las tres de solo lectura (`pattern-list`, `pattern-get`, `pattern-list-by-topology`) llevan binding y se invocan en las tres superficies. - - [ ] `satellite-create` lleva binding con un camino deshacible o de dry-run, o la matriz registra por qué queda exenta — una operación no comprobable que no dice nada de sí misma es peor que una declarada no comprobable. - - [ ] `uncoveredTriangleOps` está vacío, o cada entrada restante tiene una razón escrita. + - [x] Las tres de solo lectura llevan binding y se invocan en las tres superficies. Medido: operaciones atadas y ejecutadas pasaron de 48 a **51**, invocaciones de superficie de 66 a **75**. + - [x] `satellite-create` registra por qué queda exenta, en `bindings.ts`, donde mira el siguiente lector: aprovisiona un repositorio real de GitHub y escribe el registro local, así que atarla tal cual haría que la suite creara repositorios reales en cada corrida de CI — una prueba que daña el mundo que mide. Necesita un efecto deshacible o un dry-run fiable, y hoy no tiene ninguno. + - [x] `uncoveredTriangleOps` conserva una entrada, `satellite-create`, con su razón escrita arriba. +- **Atarlas encontró dos divergencias, y las dos eran del instrumento.** La primera corrida reportó `pattern-get` fallando en las tres superficies con tres códigos de error distintos, y `pattern-list-by-topology` discrepando entre MCP y el resto. Ambas eran defectos de los bindings que yo acababa de escribir: `cqrs` no es un id de patrón (son `PAT-0001`…) y el tool MCP recibe `topology`, no `topologyId`. Corregidos contra el catálogo real y el esquema real del tool, la suite reporta **cero hallazgos** — la cuarta vez en esta ola que una divergencia reportada pertenecía al instrumento de medida y no al producto. +- **Status:** `COMPLETADO` (2026-08-04) #### GT-653 diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index d0588113a..f38653834 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7529,10 +7529,13 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - **Provenance:** Measured on 2026-08-03 against all three services deployed to a kind cluster, reading the live responses on their node ports rather than the source. - **Principal:** `S` · **Interest:** `LOW` · **Basis:** `estimate` - **This is a DECISION before it is work.** The envelope is the Core's stated contract, so the obvious answer is that all three adopt it. The obvious answer is not free: `/health` is what a Kubernetes probe and a load balancer read, and those are configured against a shape today. Whichever way it goes, the resolution is a written decision followed by the surfaces obeying it — not three services quietly converging. +- **The decision, and why it was easier than registered.** All three adopt the ADR-0073 envelope. The caution this row was registered with — "the probes are configured against a shape" — was WRONG, and measuring it turned a decision into a one-line answer: the Helm probes use `httpGet` (status code only), the Dockerfiles use `curl -f` (non-2xx only), the k6 profiles check `r.status === 200`, and RoboSoft checks `hr.ok`. **Almost nothing reads the body** — and the word matters. Two consumers do: `local-test.sh url`, written during this same wave, and `src/sdk/cli/examples/mcp-test.js`, the MCP smoke, which asserted a bare `body.status === 'ok'`. **CI found the second one; this survey had missed it**, so the claim as first written was too strong. The decision stands — a test asserting a shape is exactly what gets updated when the shape changes deliberately, and it now asserts the envelope — but the survey behind it was incomplete and saying so is the point of recording it here. - **Acceptance criteria:** - - [ ] A decision records which shape `/health` uses across the three surfaces, and why, including what it means for the deployed probes. - - [ ] The three surfaces answer that shape, verified against live responses rather than source. - - [ ] A check contrasts the three, so the next surface added cannot invent a fourth shape. + - [x] A decision records which shape `/health` uses across the three surfaces, and why, including what it means for the deployed probes — recorded above, on the measurement rather than on caution. + - [x] The three surfaces answer that shape. `mcp` routes its three probes through `success()` from `common/envelopes` (the same helper every tool result already used — only health was outside it); `agent-runtime` builds the envelope in its controller; `core-api` was already enveloped by its global interceptor. The verdict is `OK` on all three, not `ok`. + - [x] A check contrasts the three: `62-validate-health-envelope.mjs`, wired into `Governance guards`, with 8 unit tests. Watched failing on a bare object, on a missing `schemaVersion`, and on a lowercase verdict inside an otherwise correct envelope. +- **The guard flagged its own documentation first**, exactly as the secret-scan guard did earlier in the same wave: the handler's header explains the `{status: 'ok'}` shape it replaced, and a raw text scan fired on that explanation. Comments are stripped before scanning, and a test pins it. +- **Status:** `DONE` (2026-08-04) #### GT-655 @@ -7545,9 +7548,11 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - **Provenance:** `src/tests/exploration/.out/coverage.json` on 2026-08-04, `uncoveredTriangleOps`, re-measured after the suite was re-run through `npm run test:exploration`. - **Principal:** `M` · **Interest:** `LOW` · **Basis:** `estimate` - **Acceptance criteria:** - - [ ] The three read-only ones (`pattern-list`, `pattern-get`, `pattern-list-by-topology`) carry a binding and are invoked on all three surfaces. - - [ ] `satellite-create` either carries a binding with an undoable or dry-run path, or the matrix records why it is exempt — an untestable operation that says nothing about itself is worse than one declared untestable. - - [ ] `uncoveredTriangleOps` is empty, or every remaining entry has a written reason. + - [x] The three read-only ones carry a binding and are invoked on all three surfaces. Measured: bound and executed operations went 48 → **51**, surface invocations 66 → **75**. + - [x] `satellite-create` records why it is exempt, in `bindings.ts` where the next reader looks: it provisions a live GitHub repository and writes the local registry, so binding it as-is would have the suite create real repositories on every CI run — a test that damages the world it measures. It needs an undoable effect or a trustworthy dry-run, and has neither today. + - [x] `uncoveredTriangleOps` holds one entry, `satellite-create`, with the reason written above. +- **Binding them found two divergences, and both were the instrument.** The first run reported `pattern-get` failing on all three surfaces with three different error codes, and `pattern-list-by-topology` disagreeing between MCP and the rest. Both were defects in the bindings I had just written: `cqrs` is not a pattern id (they are `PAT-0001`…) and the MCP tool takes `topology`, not `topologyId`. Corrected against the real catalog and the real tool schema, the suite reports **zero findings** — the fourth time in this wave that a reported divergence belonged to the measuring instrument rather than the product. +- **Status:** `DONE` (2026-08-04) #### GT-653 diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 3056cc31d..0c9a5427c 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -666,11 +666,11 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`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-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 | `PENDIENTE` | -| [`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 | `PENDIENTE` | +| [`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` | -**Progreso:** 637 / 653 completados · 3 en progreso · 6 pendientes · 7 diferidos +**Progreso:** 639 / 653 completados · 3 en progreso · 4 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). diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 423d1b4b4..b81a927fa 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -666,11 +666,11 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`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-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 | `PENDING` | -| [`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 | `PENDING` | +| [`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` | -**Progress:** 637 / 653 done · 3 in progress · 6 pending · 7 deferred +**Progress:** 639 / 653 done · 3 in progress · 4 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). 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 f8b43a76e..6fcc34b22 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-654](../gaps/gap-reference-catalog.es.md#gt-654), [GT-536](../gaps/gap-reference-catalog.es.md#gt-536), [GT-655](../gaps/gap-reference-catalog.es.md#gt-655), +3 | +| 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 | ## Bloqueadores Actuales @@ -43,13 +43,13 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so |---|---:| | Fecha canónica del tablero | 2026-07-26 | | Gaps totales | 653 | -| Gaps cerrados | 637 | -| Gaps pendientes | 16 | +| Gaps cerrados | 639 | +| Gaps pendientes | 14 | | P0 abiertos | 1 | | P1 abiertos | 3 | -| P2 abiertos | 9 | -| Cierre total | 97.5% | -| Registros de evidencia de cierre | 619 | +| P2 abiertos | 7 | +| Cierre total | 97.9% | +| Registros de evidencia de cierre | 621 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | @@ -57,7 +57,7 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | `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) | -| `Evolith Core` | 3 | 0 | 0 | [GT-654](../gaps/gap-reference-catalog.es.md#gt-654), [GT-655](../gaps/gap-reference-catalog.es.md#gt-655), [GT-600](../gaps/gap-reference-catalog.es.md#gt-600) | +| `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) | ## Fuente y Regla de Actualización diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index ec957a496..838c8a22e 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-654](../gaps/gap-reference-catalog.md#gt-654), [GT-536](../gaps/gap-reference-catalog.md#gt-536), [GT-655](../gaps/gap-reference-catalog.md#gt-655), +3 | +| 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 | ## Current Blockers @@ -43,13 +43,13 @@ Use this summary with a simple rule: if you need context, open only the linked I |---|---:| | Canonical board date | 2026-07-26 | | Total gaps | 653 | -| Closed gaps | 637 | -| Open gaps | 16 | +| Closed gaps | 639 | +| Open gaps | 14 | | Open P0 | 1 | | Open P1 | 3 | -| Open P2 | 9 | -| Total closure | 97.5% | -| Closure evidence records | 619 | +| Open P2 | 7 | +| Total closure | 97.9% | +| Closure evidence records | 621 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | @@ -57,7 +57,7 @@ Use this summary with a simple rule: if you need context, open only the linked I | `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) | -| `Evolith Core` | 3 | 0 | 0 | [GT-654](../gaps/gap-reference-catalog.md#gt-654), [GT-655](../gaps/gap-reference-catalog.md#gt-655), [GT-600](../gaps/gap-reference-catalog.md#gt-600) | +| `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) | ## Source and Refresh Rule diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index 4de7837f4..8499bb9e2 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -4,13 +4,13 @@ "asOf": "2026-07-26", "gaps": { "total": 653, - "done": 637, - "pending": 6, + "done": 639, + "pending": 4, "inProgress": 3, "deferred": 7 }, "evidence": { - "closureRecords": 619, + "closureRecords": 621, "cliPackage": "@beyondnet/evolith-cli@1.2.2", "adrCount": 140, "rulesetCount": 177, diff --git a/reference/core/control-center/testing/e2e-scenario-board.es.md b/reference/core/control-center/testing/e2e-scenario-board.es.md index 5d44096d2..15b98f85f 100644 --- a/reference/core/control-center/testing/e2e-scenario-board.es.md +++ b/reference/core/control-center/testing/e2e-scenario-board.es.md @@ -10,16 +10,16 @@ Regenerar con: `node .harness/scripts/generate-e2e-scenario-board.mjs` ## Core — exploración cross-superficie (CLI · MCP · REST) -Medido `2026-08-04T10:24:38.541Z`. Producido por: `npm run test:exploration`. +Medido `2026-08-04T22:17:22.757Z`. Producido por: `npm run test:exploration`. | Medida | Valor | |---|---| | Operations declared | 73 | | Exposed per surface | CLI 42 · MCP 47 · REST 31 | | Declared on all three | 14 | -| With a binding | 48 | -| Actually executed | 48 | -| Surface invocations | 66 | +| With a binding | 51 | +| Actually executed | 51 | +| Surface invocations | 75 | | No-effect contracts | 3/3 checked · 3 contrast-verified | ### Declaradas en las tres superficies pero NO ejercitadas @@ -27,9 +27,6 @@ Medido `2026-08-04T10:24:38.541Z`. Producido por: `npm run test:exploration`. Tienen binding en cada superficie y ninguna invocación llegó a ellas. Son el borde honesto de esta corrida: se listan en vez de redondearse. - `satellite-create` -- `pattern-list` -- `pattern-get` -- `pattern-list-by-topology` ### Observaciones @@ -37,20 +34,11 @@ Sin observaciones en la corrida registrada. ## Tracker — robots RoboSoft contra un clúster vivo -**Veredicto: `PASS`** — Medido `2026-08-04T03:52:26.502Z`, de la corrida `robosoft-2026-08-04T03-52-26-502Z.json`. +**Veredicto: `PASS`** — Medido `2026-08-04T15:50:43.110Z`, de la corrida `robosoft-2026-08-04T15-50-43-110Z.json`. -229 passed · 0 failed · 1 soft · 0 crashed +13 passed · 0 failed · 1 soft · 0 crashed | Escenario | Veredicto | Comprobaciones | |---|---|---| -| `audit-trail` | PASS | 25 ok · 0 failed | -| `exception-governance` | PASS | 21 ok · 0 failed | -| `gate-enforcement` | PASS | 25 ok · 0 failed | -| `governance-journey` | PASS | 53 ok · 0 failed · 1 soft | -| `intake` | PASS | 12 ok · 0 failed | -| `phase-artifact-catalog` | PASS | 18 ok · 0 failed | -| `provider-connections` | PASS | 19 ok · 0 failed | -| `qa-quality-gate` | PASS | 13 ok · 0 failed | -| `scorecard` | PASS | 33 ok · 0 failed | -| `tenant-isolation` | PASS | 10 ok · 0 failed | +| `core-integration` | PASS | 13 ok · 0 failed · 1 soft | diff --git a/reference/core/control-center/testing/e2e-scenario-board.md b/reference/core/control-center/testing/e2e-scenario-board.md index 9bbb6d44d..09395b493 100644 --- a/reference/core/control-center/testing/e2e-scenario-board.md +++ b/reference/core/control-center/testing/e2e-scenario-board.md @@ -10,16 +10,16 @@ Regenerate with: `node .harness/scripts/generate-e2e-scenario-board.mjs` ## Core — cross-surface exploration (CLI · MCP · REST) -Measured `2026-08-04T10:24:38.541Z`. Produced by: `npm run test:exploration`. +Measured `2026-08-04T22:17:22.757Z`. Produced by: `npm run test:exploration`. | Measure | Value | |---|---| | Operations declared | 73 | | Exposed per surface | CLI 42 · MCP 47 · REST 31 | | Declared on all three | 14 | -| With a binding | 48 | -| Actually executed | 48 | -| Surface invocations | 66 | +| With a binding | 51 | +| Actually executed | 51 | +| Surface invocations | 75 | | No-effect contracts | 3/3 checked · 3 contrast-verified | ### Declared on all three surfaces but NOT exercised @@ -27,9 +27,6 @@ Measured `2026-08-04T10:24:38.541Z`. Produced by: `npm run test:exploration`. These carry a binding on every surface and no invocation reached them. They are the honest edge of this run, listed rather than rounded away. - `satellite-create` -- `pattern-list` -- `pattern-get` -- `pattern-list-by-topology` ### Observations @@ -37,20 +34,11 @@ No observations in the recorded run. ## Tracker — RoboSoft robots against a live cluster -**Verdict: `PASS`** — Measured `2026-08-04T03:52:26.502Z`, from run `robosoft-2026-08-04T03-52-26-502Z.json`. +**Verdict: `PASS`** — Measured `2026-08-04T15:50:43.110Z`, from run `robosoft-2026-08-04T15-50-43-110Z.json`. -229 passed · 0 failed · 1 soft · 0 crashed +13 passed · 0 failed · 1 soft · 0 crashed | Scenario | Verdict | Checks | |---|---|---| -| `audit-trail` | PASS | 25 ok · 0 failed | -| `exception-governance` | PASS | 21 ok · 0 failed | -| `gate-enforcement` | PASS | 25 ok · 0 failed | -| `governance-journey` | PASS | 53 ok · 0 failed · 1 soft | -| `intake` | PASS | 12 ok · 0 failed | -| `phase-artifact-catalog` | PASS | 18 ok · 0 failed | -| `provider-connections` | PASS | 19 ok · 0 failed | -| `qa-quality-gate` | PASS | 13 ok · 0 failed | -| `scorecard` | PASS | 33 ok · 0 failed | -| `tenant-isolation` | PASS | 10 ok · 0 failed | +| `core-integration` | PASS | 13 ok · 0 failed · 1 soft | diff --git a/src/apps/agent-runtime-api/src/health/health.controller.ts b/src/apps/agent-runtime-api/src/health/health.controller.ts index 64927baee..4e27b59f2 100644 --- a/src/apps/agent-runtime-api/src/health/health.controller.ts +++ b/src/apps/agent-runtime-api/src/health/health.controller.ts @@ -1,6 +1,41 @@ import { Controller, Get } from '@nestjs/common'; +import { randomUUID } from 'node:crypto'; import { Public } from '../auth/public.decorator'; +/** ADR-0073 envelope shape version for this surface. */ +const ENVELOPE_SCHEMA_VERSION = '1.0.0'; + +/** + * GT-654 — every probe answers the ADR-0073 envelope. + * + * These used to return bare objects (`{status: 'ok', service, version, ...}`) + * while core-api answered `{success, data: {status: 'OK'}, meta}`. Three + * services of one product, three shapes, and the verdict literal differing in + * case as well. A probe written against either shape reported the others as + * broken — that produced a false failure on 2026-08-03, when a cross-cluster + * check matched `"status":"ok"` literally and called two healthy services + * unreachable while they were serving. + * + * Unifying is safe because NOTHING reads the body: the Helm probes use + * `httpGet` (status code only), the Dockerfile uses `curl -f` (non-2xx only), + * k6 checks `r.status === 200` and RoboSoft checks `hr.ok`. Measured before + * changing it, not assumed — the earlier worry that "the probes are configured + * against a shape" was wrong. + */ +function envelope(command: string, data: Record) { + return { + success: true, + data, + meta: { + command, + executedAt: new Date().toISOString(), + durationMs: 0, + correlationId: `evl-${randomUUID()}`, + schemaVersion: ENVELOPE_SCHEMA_VERSION, + }, + }; +} + /** * Liveness/readiness + service banner. Public (no API key) so Traefik/Coolify * health checks and `curl /health` work without credentials. @@ -12,26 +47,26 @@ export class HealthController { @Public() @Get('health') health() { - return { - status: 'ok', + return envelope('http GET /health', { + status: 'OK', service: 'agent-runtime-api', version: process.env.npm_package_version ?? '0.1.0', uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1000), - }; + }); } /** Liveness probe — the process is up (always ok while serving). */ @Public() @Get('health/live') live() { - return { status: 'ok', probe: 'live' }; + return envelope('http GET /health/live', { status: 'OK', probe: 'live', service: 'agent-runtime-api' }); } /** Readiness probe — the app is ready to accept traffic. */ @Public() @Get('health/ready') ready() { - return { status: 'ok', probe: 'ready' }; + return envelope('http GET /health/ready', { status: 'OK', probe: 'ready', service: 'agent-runtime-api' }); } @Public() diff --git a/src/packages/mcp-server/src/mcp/mcp-server.service.spec.ts b/src/packages/mcp-server/src/mcp/mcp-server.service.spec.ts index a41c684e9..6513f6f37 100644 --- a/src/packages/mcp-server/src/mcp/mcp-server.service.spec.ts +++ b/src/packages/mcp-server/src/mcp/mcp-server.service.spec.ts @@ -206,7 +206,13 @@ describe('McpServerService — HTTP transport', () => { // /health is a public liveness probe — reachable without credentials. const health = await httpGet(port, '/health'); expect(health.status).toBe(200); - expect(JSON.parse(health.body)).toMatchObject({ status: 'ok', transport: 'http' }); + // GT-654 — the ADR-0073 envelope, same as every tool result here and as + // core-api. It answered a bare `{status:'ok',...}` until 2026-08-04. + expect(JSON.parse(health.body)).toMatchObject({ + success: true, + data: { status: 'OK', transport: 'http' }, + meta: { schemaVersion: '1.0.0' }, + }); // MCP endpoints require the API key. const unauthorized = await httpGet(port, '/mcp'); diff --git a/src/packages/mcp-server/src/mcp/mcp-server.service.ts b/src/packages/mcp-server/src/mcp/mcp-server.service.ts index 4934fa013..fe2778047 100644 --- a/src/packages/mcp-server/src/mcp/mcp-server.service.ts +++ b/src/packages/mcp-server/src/mcp/mcp-server.service.ts @@ -20,7 +20,7 @@ import { MetricsService } from './metrics.service'; import { AuditLogger } from './audit-logger'; import { ResourcesService } from './resources.service'; import { PromptsService } from './prompts.service'; -import { generateCorrelationId } from '../common/envelopes'; +import { generateCorrelationId, success } from '../common/envelopes'; import { ErrorCodes } from '../common/errors'; import { AbacEvaluator } from './abac-evaluator'; import { mcpContextStorage, McpUserContext } from './mcp-user-context'; @@ -503,16 +503,30 @@ export class McpServerService { // liveness/readiness probes without credentials. They expose no MCP data. // /health is kept for back-compat; /health/live (liveness) and /health/ready // (readiness) are the split probes. + // GT-654 — the ADR-0073 envelope, same as every tool result on this surface + // and same as core-api. It used to answer a bare `{status, transport, ...}`, + // so the three services of one product returned three shapes and the + // verdict literal differed in case too (`OK` here, `ok` there). Nothing + // reads the BODY — probes use `httpGet`, the Dockerfile uses `curl -f`, k6 + // checks `r.status === 200`, RoboSoft checks `hr.ok` — which is exactly + // why unifying it is safe and why leaving it divergent bought nothing. if (req.method === 'GET' && (url.pathname === '/health' || url.pathname === '/health/live')) { + const probe = url.pathname === '/health/live' ? 'live' : 'health'; res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', transport: 'http', protocol: 'mcp', probe: url.pathname === '/health/live' ? 'live' : 'health' })); + res.end(JSON.stringify(success( + { status: 'OK', transport: 'http', protocol: 'mcp', probe, service: 'Evolith MCP Server' }, + { correlationId: generateCorrelationId(), tool: `http GET ${url.pathname}`, durationMs: 0 }, + ))); return; } if (req.method === 'GET' && url.pathname === '/health/ready') { // Ready once the HTTP server is accepting connections and the registry is built. const ready = this.httpServer !== null; res.writeHead(ready ? 200 : 503, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: ready ? 'ok' : 'starting', probe: 'ready' })); + res.end(JSON.stringify(success( + { status: ready ? 'OK' : 'STARTING', probe: 'ready', service: 'Evolith MCP Server' }, + { correlationId: generateCorrelationId(), tool: 'http GET /health/ready', durationMs: 0 }, + ))); return; } diff --git a/src/sdk/cli/examples/mcp-test.js b/src/sdk/cli/examples/mcp-test.js index 719b0dc83..15a20ba05 100644 --- a/src/sdk/cli/examples/mcp-test.js +++ b/src/sdk/cli/examples/mcp-test.js @@ -361,7 +361,17 @@ async function runHttpSmoke() { const healthRes = await httpGet(`http://localhost:${port}/health`); assert(healthRes.status === 200, `/health: status ${healthRes.status}`); const healthBody = parseJsonLine(healthRes.body); - assert(healthBody?.status === 'ok', `/health: body.status was ${JSON.stringify(healthBody?.status)}`); + // GT-654 — the ADR-0073 envelope, same as every tool result on this surface. + // It asserted a bare `body.status === 'ok'` until 2026-08-04, and finding it + // corrected a claim made while closing that gap: "nothing reads the /health + // body" was checked against the Helm probes, the Dockerfiles, k6 and + // RoboSoft — and missed this smoke, which reads it and is the only consumer + // that did. The decision stands; the survey behind it was incomplete. + assert( + healthBody?.data?.status === 'OK', + `/health: body.data.status was ${JSON.stringify(healthBody?.data?.status)}`, + ); + assert(healthBody?.success === true, '/health: body.success was not true'); console.log(' /health OK'); // 3. JSON-RPC over Streamable HTTP using official SDK Client diff --git a/src/tests/exploration/bindings.ts b/src/tests/exploration/bindings.ts index 9948415d6..329edb040 100644 --- a/src/tests/exploration/bindings.ts +++ b/src/tests/exploration/bindings.ts @@ -208,6 +208,48 @@ export const BINDINGS: Record = { // Topology Operations // ========================================================================= + // GT-655 — declared `exposed: true` on all three surfaces and, until + // 2026-08-04, invoked by nothing. The matrix asserted they existed in three + // places and no test had ever asked them to prove it; the harness reported + // them honestly in `uncoveredTriangleOps`, which is not the same as covering + // them. All three are read-only, so a binding costs nothing but the writing. + 'pattern-list': { + verified: false, + cli: (c) => ['patterns', 'list', '--core', c.corePath, '--format', 'json'], + mcp: (c) => ({ tool: 'evolith-pattern-list', args: { corePath: c.corePath } }), + rest: (c) => ({ method: 'GET', path: '/api/v1/architecture/patterns' }), + }, + + 'pattern-get': { + verified: false, + cli: (c) => ['patterns', 'get', 'PAT-0001', '--core', c.corePath, '--format', 'json'], + mcp: (c) => ({ tool: 'evolith-pattern-get', args: { id: 'PAT-0001', corePath: c.corePath } }), + rest: (c) => ({ method: 'GET', path: '/api/v1/architecture/patterns/PAT-0001' }), + }, + + 'pattern-list-by-topology': { + verified: false, + cli: (c) => ['patterns', 'for-topology', 'monolithic-layered', '--core', c.corePath, '--format', 'json'], + mcp: (c) => ({ + tool: 'evolith-pattern-list-by-topology', + args: { topology: 'monolithic-layered', corePath: c.corePath }, + }), + rest: (c) => ({ method: 'GET', path: '/api/v1/architecture/topologies/monolithic-layered/patterns' }), + }, + + // `satellite-create` is DELIBERATELY not bound, and this note is the reason + // rather than its absence. + // + // It provisions a live GitHub repository and writes the local registry + // (`satellite-create.tool.ts:219`). Exercising it needs either an effect the + // harness can undo or a dry-run path it can trust, and it has neither today. + // Binding it as-is would have the exploration suite create real repositories + // on every CI run — a test that damages the world it measures. + // + // Registered as GT-655's remaining criterion. An untestable operation that + // says nothing about itself is worse than one declared untestable, so it says + // so here and in `uncoveredTriangleOps`, which will keep listing it. + 'topology-list': { verified: false, mcp: (c) => ({ tool: 'evolith-topology-list', args: {} }),