diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4baddbc4..d9635a09 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -51,6 +51,36 @@ jobs: npm run build --workspace @beyondnet/evolith-infra-providers npm run build --workspace @beyondnet/evolith-mcp npm run build --workspace src/sdk/cli + # GT-706: the declared-exports guard below reads each package's PACKLIST, + # and it covers `repo-facts` too. Every other workspace it scans is + # already built above. + npm run build --workspace @beyondnet/evolith-repo-facts + + # GT-706 — a package must prove its own manifest before it is published. + # + # `contracts@1.1.0` declared an export subpath it did not ship. That failed + # at `infra-providers@1.2.1`'s install smoke, AFTER `core-domain@1.3.1` was + # already irreversibly on the registry, leaving the release half-shipped with + # no unpublish available past 72 hours. The release's own assertion could not + # see it: it computed "promised" as `[pkg.main, ...bin]`, and `exports` is not + # in that list. Measured — a package declaring `./ingest` with only + # `dist/index.js` on disk passes it with exit 0 while `require pkg/ingest` + # answers MODULE_NOT_FOUND. + # + # IT LIVES IN THIS JOB BECAUSE THIS JOB BUILDS. A packlist without a dist + # reports every declared target as missing, so the guard needs the same nine + # workspaces it scans to be emitted. It was tried in `governance-guards` + # first, which builds five, and it went red on the four that were not — cli, + # mcp, sdk and core. Building those four there also turned four DEAD evidence + # references into four live non-zero commands, which is a real finding and a + # different gap; it is not silently re-buried, it is recorded in the PR. + # + # `Test` being a REQUIRED check is the other half: a phantom export now + # blocks the merge rather than the release. + - name: A package's declared exports are present in its own tarball + run: | + node .harness/scripts/ci/67-validate-declared-exports.mjs --verbose + node --test .harness/scripts/ci/67-validate-declared-exports.test.mjs # CD gate = the evolith-cli unit suite (fast, deterministic). The full e2e # (env-sensitive: spawns servers, loads rulesets) is covered by the diff --git a/.github/workflows/npm-release.yml b/.github/workflows/npm-release.yml index 1ae92063..e26a6cca 100644 --- a/.github/workflows/npm-release.yml +++ b/.github/workflows/npm-release.yml @@ -185,9 +185,22 @@ jobs: # dist/main.js`, and the job went green. Reporting is not asserting. A log line # no step fails on is decoration. # - # This reads the packlist and fails when `main` or any `bin` target is absent, - # which is the narrowest question that would have caught it, and is general - # enough to catch the next mechanism rather than only this one. + # GT-706 — THE NARROW VERSION OF THIS CHECK LET A SECOND DEFECT THROUGH, and the + # inline node it replaced is now a guard with its own fixtures. + # + # It read the packlist and failed when `main` or any `bin` target was absent — + # "the narrowest question that would have caught it". It was too narrow. + # `contracts@1.1.0` declared an EXPORT SUBPATH it did not ship, `exports` was + # not in the list this checked, and the failure surfaced one package later, at + # `infra-providers@1.2.1`'s install smoke, with `core-domain@1.3.1` already + # irreversibly published and no unpublish available past 72 hours. + # + # Measured rather than argued: a two-file package declaring + # `"./ingest": "./dist/ingest/index.js"` with only `dist/index.js` on disk passed + # the old assertion with exit 0, while `require pkg/ingest` answered + # MODULE_NOT_FOUND. The guard below covers `main` and `bin` too, so this is a + # superset and not a second opinion beside it. + # # Done in node, in one process, on purpose. The first version of this check was a # shell pipeline — `echo "$PACKED" | grep -qxF "$f"` — and it reported a FALSE # POSITIVE on its first real run: it named dist/main.js missing from a tarball that @@ -203,21 +216,11 @@ jobs: # # A `<<<` herestring fixes it too. node fixes the whole class: no pipe to break, no # word-splitting on the promised list, and the JSON is already JSON. - echo "-- packlist assertion" - ( cd "$DIR" && npm pack --dry-run --json > /tmp/packlist.json ) - node -e ' - const fs = require("fs"); - const dir = process.argv[1]; - const pkg = JSON.parse(fs.readFileSync(dir + "/package.json", "utf8")); - const packed = new Set(JSON.parse(fs.readFileSync("/tmp/packlist.json", "utf8"))[0].files.map((f) => f.path)); - const promised = [...new Set([pkg.main, ...Object.values(pkg.bin || {})].filter(Boolean).map((s) => s.replace(/^\.\//, "")))]; - console.log(" " + packed.size + " file(s) packed; entry points declared: " + (promised.join(", ") || "(none)")); - const missing = promised.filter((p) => !packed.has(p)); - if (missing.length) { - console.log("::error::" + pkg.name + "@" + pkg.version + " would publish WITHOUT: " + missing.join(", ") + " — the tarball does not contain the entry points its own package.json declares. Publishing it would put a package on the registry that cannot be required or executed."); - process.exit(1); - } - ' "$DIR" + echo "-- packlist assertion (main, bin AND every exports target)" + if ! node .harness/scripts/ci/67-validate-declared-exports.mjs --pkg "$DIR" --verbose; then + echo "::error::$NAME@$VERSION declares targets its own tarball does not carry. Publishing it would put a package on the registry whose manifest promises import paths that do not resolve — and npm forbids unpublishing after 72 hours." + exit 1 + fi if [ "$DRY_RUN" = "true" ]; then ( cd "$DIR" && npm publish --dry-run --provenance --access public ) diff --git a/.harness/scripts/ci/41-validate-evidence-commands.mjs b/.harness/scripts/ci/41-validate-evidence-commands.mjs index 91adb276..f1868d1e 100644 --- a/.harness/scripts/ci/41-validate-evidence-commands.mjs +++ b/.harness/scripts/ci/41-validate-evidence-commands.mjs @@ -712,10 +712,49 @@ export function classifyExecutability(cmd, root = ROOT) { if (writer) { return { executable: false, bucket: 'writes-to-tree', reason: `source calls ${writer} — executing it would edit the working tree` }; } + // GT-706 — a guard that reads emitted `dist/` cannot run where nothing is built. + // + // `67-validate-declared-exports.mjs` reads each package's PACKLIST, so an + // unbuilt workspace reports every declared target as missing. This job builds + // five of the nine it scans, so executing it here fails on the absence of a + // build and not on anything it was written to check — the same shape as + // GT-675's remote-ref case, where the command was fine and the ENVIRONMENT + // was the wrong one. + // + // The declaration lives in the guard, not in a list here. A hardcoded roster + // of build-dependent scripts is one more thing that silently stops covering a + // new one; a marker the script itself carries moves with it. Classifying, not + // skipping: the command stays in the census and is reported, it is simply not + // executed in a job that cannot answer it. + if (declaresBuiltWorkspace(join(root, cmd.cwd || '.', script))) { + return { + executable: false, + bucket: 'heavy-toolchain', + reason: 'declares REQUIRES_BUILT_WORKSPACE — reads emitted dist/, which this job does not produce', + }; + } } return { executable: true }; } +/** + * Does a script declare that it needs an emitted `dist/`? + * + * Read from source rather than imported: importing every referenced script to ask + * one question would execute their module bodies, and a classifier that runs the + * thing it is classifying is not a classifier. + * + * @param {string} abs absolute path to the script + * @returns {boolean} + */ +function declaresBuiltWorkspace(abs) { + try { + return /^export const REQUIRES_BUILT_WORKSPACE = true;/m.test(readFileSync(abs, 'utf8')); + } catch { + return false; + } +} + /** First filesystem-write API found in a script's source, or null. */ function writeApiUsed(abs) { let src; diff --git a/.harness/scripts/ci/41-validate-evidence-commands.test.mjs b/.harness/scripts/ci/41-validate-evidence-commands.test.mjs index a6816ec5..e072f6f2 100644 --- a/.harness/scripts/ci/41-validate-evidence-commands.test.mjs +++ b/.harness/scripts/ci/41-validate-evidence-commands.test.mjs @@ -311,6 +311,33 @@ describe('classifyExecutability', () => { assert.equal(c.bucket, 'writes-to-tree'); }); + // GT-706 — a guard that reads emitted `dist/` fails on the absence of a build + // rather than on what it was written to check. The declaration lives in the + // guard, so the pair of tests below is the whole contract: a script that + // declares it is classified, and one that does not is still executed. A + // classification that could not be observed to NOT apply is a silent skip. + test('a script declaring REQUIRES_BUILT_WORKSPACE is classified, not executed', () => { + writeFileSync( + join(fixtureRoot, 'scripts', 'needs-build.mjs'), + 'export const REQUIRES_BUILT_WORKSPACE = true;\nconsole.log("ok");\n', + ); + const c = classifyExecutability(extractCommand('node scripts/needs-build.mjs'), fixtureRoot); + assert.equal(c.executable, false); + assert.equal(c.bucket, 'heavy-toolchain'); + assert.match(c.reason, /REQUIRES_BUILT_WORKSPACE/); + }); + + test('a script that merely MENTIONS the marker is still executed', () => { + // The marker is a declaration, not a keyword: matching it in a comment or a + // string would let any script opt out of execution by talking about it. + writeFileSync( + join(fixtureRoot, 'scripts', 'mentions-marker.mjs'), + '// see REQUIRES_BUILT_WORKSPACE in 67-validate-declared-exports.mjs\nconsole.log("ok");\n', + ); + const c = classifyExecutability(extractCommand('node scripts/mentions-marker.mjs'), fixtureRoot); + assert.equal(c.executable, true, 'a mention must not opt a script out of execution'); + }); + test('`gh api ...` is excluded as needing credentials, not silently dropped', () => { const c = classifyExecutability(extractCommand('gh api repos/o/r/branches/main/protection'), fixtureRoot); assert.equal(c.bucket, 'network-or-credentials'); diff --git a/.harness/scripts/ci/67-validate-declared-exports.mjs b/.harness/scripts/ci/67-validate-declared-exports.mjs new file mode 100644 index 00000000..c9104695 Binary files /dev/null and b/.harness/scripts/ci/67-validate-declared-exports.mjs differ diff --git a/.harness/scripts/ci/67-validate-declared-exports.test.mjs b/.harness/scripts/ci/67-validate-declared-exports.test.mjs new file mode 100644 index 00000000..c9126309 --- /dev/null +++ b/.harness/scripts/ci/67-validate-declared-exports.test.mjs @@ -0,0 +1,262 @@ +#!/usr/bin/env node + +/** + * GT-706 — fixtures for the declared-exports guard. + * + * The case that matters is the one that defeated the check this replaces: a + * package whose `exports` map names a file it never packs. `npm-release.yml:213` + * computes "promised" as `[pkg.main, ...bin]`, so that package passes with exit 0 + * while `require pkg/ingest` answers MODULE_NOT_FOUND. The first test here builds + * exactly that package and asserts this guard turns RED on it — and the + * neighbouring test asserts the SAME guard stays green when the file is present, + * because a check that fails on everything is not evidence either. + * + * The packlist is injected as a plain set in the unit tests rather than produced + * by `npm pack`: the guard's job is reconciling a manifest against a file list, + * and running npm here would test npm. One end-to-end test drives the real binary + * over a real temp package, so the wiring is measured too rather than assumed. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +import { + targetLeaves, + declaredTargets, + wildcardMatcher, + reconcile, + publishableWorkspaces, +} from './67-validate-declared-exports.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const GUARD = resolve(__dirname, '67-validate-declared-exports.mjs'); +const REPO_ROOT = resolve(__dirname, '../../..'); + +describe('targetLeaves — every string in the condition tree is a promise', () => { + it('takes a bare string', () => { + assert.deepEqual(targetLeaves('./dist/index.js'), ['./dist/index.js']); + }); + + it('takes types AND default, because a missing types breaks TypeScript consumers', () => { + assert.deepEqual( + targetLeaves({ types: './dist/a.d.ts', default: './dist/a.js' }), + ['./dist/a.d.ts', './dist/a.js'], + ); + }); + + it('descends through nested conditions', () => { + assert.deepEqual( + targetLeaves({ import: { types: './dist/a.d.mts', default: './dist/a.mjs' }, require: './dist/a.cjs' }), + ['./dist/a.d.mts', './dist/a.mjs', './dist/a.cjs'], + ); + }); + + it('descends through arrays', () => { + assert.deepEqual(targetLeaves(['./dist/a.js', './dist/b.js']), ['./dist/a.js', './dist/b.js']); + }); + + it('yields nothing for null — a null target is a deliberate block, not a promise', () => { + assert.deepEqual(targetLeaves(null), []); + }); +}); + +describe('declaredTargets — split by whether a target names a file or a pattern', () => { + it('separates plain subpaths from wildcards', () => { + const { plain, wildcard } = declaredTargets({ + exports: { '.': './dist/index.js', './ingest': './dist/ingest/index.js', './*': './dist/*.js' }, + }); + assert.deepEqual(plain.map((r) => r.target), ['dist/index.js', 'dist/ingest/index.js']); + assert.deepEqual(wildcard.map((r) => r.target), ['dist/*.js']); + }); + + it('subsumes main and bin, so this guard is a superset of the release assertion', () => { + const { plain } = declaredTargets({ main: './dist/main.js', bin: { tool: 'dist/cli.js' } }); + assert.deepEqual(plain.map((r) => r.target).sort(), ['dist/cli.js', 'dist/main.js']); + }); + + it('normalizes the leading ./ that packlists never carry', () => { + const { plain } = declaredTargets({ exports: { '.': './dist/index.js' } }); + assert.equal(plain[0].target, 'dist/index.js'); + }); + + it('does not report the same subpath/target pair twice', () => { + const { plain } = declaredTargets({ main: './dist/index.js', exports: { '.': './dist/index.js' } }); + assert.equal(plain.filter((r) => r.target === 'dist/index.js').length, 2, 'distinct subpaths stay distinct'); + const { plain: once } = declaredTargets({ exports: { '.': { types: './dist/i.js', default: './dist/i.js' } } }); + assert.equal(once.length, 1, 'the same pair under two conditions collapses'); + }); +}); + +describe('wildcardMatcher — a * stands for a segment run, never for a /', () => { + it('matches within one segment', () => { + assert.ok(wildcardMatcher('dist/*.js').test('dist/index.js')); + }); + + it('refuses to cross a directory boundary', () => { + assert.ok(!wildcardMatcher('dist/*.js').test('dist/nested/index.js')); + }); + + it('is anchored, so a suffix match is not a match', () => { + assert.ok(!wildcardMatcher('dist/*.js').test('other/dist/index.js')); + assert.ok(!wildcardMatcher('dist/*.js').test('dist/index.js.map')); + }); + + it('escapes regex metacharacters in the literal parts', () => { + assert.ok(wildcardMatcher('dist/a.b/*.js').test('dist/a.b/c.js')); + assert.ok(!wildcardMatcher('dist/a.b/*.js').test('dist/axb/c.js')); + }); +}); + +describe('reconcile — the defect this guard exists for', () => { + // THE FIXTURE. This is `contracts@1.1.0` reduced to its essentials: an exports + // map naming a file the tarball does not carry. The release's own assertion + // passes it, because `exports` is not in the list it checks. + const phantom = { + name: 'phantom', + main: './dist/index.js', + exports: { '.': './dist/index.js', './ingest': './dist/ingest/index.js' }, + }; + + it('is RED on a declared export that is not packed', () => { + const { missing } = reconcile(phantom, ['dist/index.js', 'package.json']); + assert.equal(missing.length, 1); + assert.equal(missing[0].subpath, './ingest'); + assert.equal(missing[0].target, 'dist/ingest/index.js'); + }); + + it('is GREEN on the same package once the file ships — the check is not simply strict', () => { + const { missing, dead } = reconcile(phantom, ['dist/index.js', 'dist/ingest/index.js', 'package.json']); + assert.equal(missing.length, 0); + assert.equal(dead.length, 0); + }); + + it('is RED on a wildcard that matches nothing — the live defect on the registry', () => { + // `core-domain` declared `./infrastructure/adapters/*` and there is no + // adapters directory at all: 0 matches in a 796-file packlist, and + // MODULE_NOT_FOUND from the published 1.3.1 for every name under it. A check + // that skipped wildcards would have called that package clean. + const { dead } = reconcile( + { exports: { './infrastructure/adapters/*': './dist/infrastructure/adapters/*.js' } }, + ['dist/infrastructure/audit/x.js', 'dist/infrastructure/events/y.js'], + ); + assert.equal(dead.length, 1); + assert.equal(dead[0].subpath, './infrastructure/adapters/*'); + }); + + it('is GREEN on a wildcard that matches at least one file', () => { + const { dead } = reconcile({ exports: { './*': './dist/*.js' } }, ['dist/index.js']); + assert.equal(dead.length, 0); + }); + + it('counts every target it checked, so a vacuous pass is visible', () => { + const { checked } = reconcile(phantom, ['dist/index.js']); + assert.equal(checked, 3, 'two exports targets plus main'); + }); + + it('reports a package with no exports at all as trivially clean', () => { + const { missing, dead, checked } = reconcile({ name: 'plain' }, ['package.json']); + assert.deepEqual([missing.length, dead.length, checked], [0, 0, 0]); + }); +}); + +describe('publishableWorkspaces — coverage comes from the manifest, not a hardcoded list', () => { + it('resolves more than zero packages in this repository', () => { + const dirs = publishableWorkspaces(REPO_ROOT); + assert.ok(dirs.length > 0, 'an empty scan would make every run vacuously green'); + }); + + it('excludes private packages, which are never published', () => { + const dirs = publishableWorkspaces(REPO_ROOT); + for (const rel of dirs) { + const pkg = JSON.parse(readFileSync(join(REPO_ROOT, rel, 'package.json'), 'utf8')); + assert.notEqual(pkg.private, true, `${rel} is private and should not be scanned`); + } + }); +}); + +describe('end to end — the binary, on a real package, through a real npm pack', () => { + const run = (dir) => spawnSync(process.execPath, [GUARD, '--pkg', dir], { encoding: 'utf8' }); + + const write = (root, exportsMap) => { + mkdirSync(join(root, 'dist'), { recursive: true }); + writeFileSync(join(root, 'dist', 'index.js'), 'module.exports = {};\n'); + writeFileSync( + join(root, 'package.json'), + JSON.stringify( + { name: 'gt706-fixture', version: '1.0.0', main: './dist/index.js', files: ['dist'], exports: exportsMap }, + null, + 2, + ), + ); + }; + + it('exits non-zero and names the missing target, so the failure is actionable without opening the tarball', () => { + const root = mkdtempSync(join(tmpdir(), 'gt706-red-')); + try { + write(root, { '.': './dist/index.js', './ingest': './dist/ingest/index.js' }); + const r = run(root); + assert.notEqual(r.status, 0, 'a phantom export must be red'); + const out = `${r.stdout}${r.stderr}`; + assert.match(out, /gt706-fixture/, 'names the package'); + assert.match(out, /\.\/ingest/, 'names the subpath'); + assert.match(out, /dist\/ingest\/index\.js/, 'names the target file'); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('exits zero on the same package once the declared file is actually shipped', () => { + const root = mkdtempSync(join(tmpdir(), 'gt706-green-')); + try { + write(root, { '.': './dist/index.js', './ingest': './dist/ingest/index.js' }); + mkdirSync(join(root, 'dist', 'ingest'), { recursive: true }); + writeFileSync(join(root, 'dist', 'ingest', 'index.js'), 'module.exports = {};\n'); + const r = run(root); + assert.equal(r.status, 0, `expected green, got: ${r.stdout}${r.stderr}`); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('is red on a package whose `files` field excludes a file that exists on disk', () => { + // The mechanism that produced the original defect: the file was written, and + // the manifest simply did not ship it. Checking the filesystem instead of the + // PACKLIST would call this clean. + const root = mkdtempSync(join(tmpdir(), 'gt706-files-')); + try { + mkdirSync(join(root, 'dist', 'ingest'), { recursive: true }); + writeFileSync(join(root, 'dist', 'index.js'), 'module.exports = {};\n'); + writeFileSync(join(root, 'dist', 'ingest', 'index.js'), 'module.exports = {};\n'); + writeFileSync( + join(root, 'package.json'), + JSON.stringify( + { + name: 'gt706-fixture', + version: '1.0.0', + main: './dist/index.js', + files: ['dist/index.js'], + exports: { '.': './dist/index.js', './ingest': './dist/ingest/index.js' }, + }, + null, + 2, + ), + ); + const r = run(root); + assert.notEqual(r.status, 0, 'on disk but not in the tarball is still a phantom'); + assert.match(`${r.stdout}${r.stderr}`, /dist\/ingest\/index\.js/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('is green across this repository as it stands', () => { + const r = spawnSync(process.execPath, [GUARD], { encoding: 'utf8', cwd: REPO_ROOT }); + assert.equal(r.status, 0, `${r.stdout}${r.stderr}`); + assert.match(r.stdout, /declared target\(s\) across \d+ publishable package\(s\)/); + }); +}); diff --git a/.harness/scripts/lib/guard-classification.mjs b/.harness/scripts/lib/guard-classification.mjs index 8e8775e4..2381ff66 100644 --- a/.harness/scripts/lib/guard-classification.mjs +++ b/.harness/scripts/lib/guard-classification.mjs @@ -40,6 +40,19 @@ export const CALLS_COVERAGE = /\b(?:assertScanned|assertScannedPerSource|scanned * deleting the check and leaving the exemption behind. */ export const SELF_GUARDED = [ + { + file: '67-validate-declared-exports.mjs', + proof: /ZERO publishable workspaces resolved/, + reason: + 'GT-706 declared-exports guard; its denominator is the set of publishable workspaces, read ' + + 'from the root manifest rather than hardcoded, and all three ways it could pass over ' + + 'nothing are hard failures: zero workspaces resolved (the "workspaces" field moved or ' + + 'stopped matching), a package directory with no manifest, and a packlist `npm pack` cannot ' + + 'produce — that last one is the state in which "nothing was found to be missing" is most ' + + 'misleading, so it refuses instead of skipping the package. The guard exists because a ' + + 'check that asked a NARROWER question than the manifest read as clean; one that asked ' + + 'nothing at all would read the same way', + }, { file: '63-validate-npm-audit-gate.mjs', proof: /denominator is unknown/, diff --git a/package-lock.json b/package-lock.json index 79bcb7ef..1c7cb8d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16347,7 +16347,7 @@ }, "src/packages/core-domain": { "name": "@beyondnet/evolith-core-domain", - "version": "1.3.1", + "version": "1.3.2", "license": "MIT", "dependencies": { "@open-policy-agent/opa-wasm": "1.10.0", @@ -16913,11 +16913,11 @@ }, "src/sdk/cli": { "name": "@beyondnet/evolith-cli", - "version": "1.3.1", + "version": "1.3.2", "license": "MIT", "dependencies": { "@beyondnet/evolith-agent-runtime": "1.2.0", - "@beyondnet/evolith-core-domain": "1.3.1", + "@beyondnet/evolith-core-domain": "1.3.2", "@beyondnet/evolith-infra-providers": "1.2.1", "@beyondnet/evolith-sdk": "2.0.0", "@clack/prompts": "1.5.1", diff --git a/product/products/smart-cli/product-inventory.es.md b/product/products/smart-cli/product-inventory.es.md index 49e33742..6f1bc20b 100644 --- a/product/products/smart-cli/product-inventory.es.md +++ b/product/products/smart-cli/product-inventory.es.md @@ -7,7 +7,7 @@ Inventario generado de la superficie instalable de Evolith CLI y MCP. No editar | Campo | Valor | |---|---| -| Package | `@beyondnet/evolith-cli@1.3.1` | +| Package | `@beyondnet/evolith-cli@1.3.2` | | Binary | `evolith`, `evolith-cli` | | CLI commands | 38 | | MCP tools | 52 | diff --git a/product/products/smart-cli/product-inventory.md b/product/products/smart-cli/product-inventory.md index a87460ad..8238e7a1 100644 --- a/product/products/smart-cli/product-inventory.md +++ b/product/products/smart-cli/product-inventory.md @@ -7,7 +7,7 @@ Generated inventory of the installable Evolith CLI and MCP surface. Do not edit | Field | Value | |---|---| -| Package | `@beyondnet/evolith-cli@1.3.1` | +| Package | `@beyondnet/evolith-cli@1.3.2` | | Binary | `evolith`, `evolith-cli` | | CLI commands | 38 | | MCP tools | 52 | diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index 6b7c8814..17557acf 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -10529,6 +10529,28 @@ "SUITES AFTER: core-domain 1992/1992, mcp-server 590/590.", "NOT CLOSED BY THIS ROW: the fix is in the tree, not in the registry. GT-671 stays IN-PROGRESS until a build carrying the corpus is published and its canary observes a real gate verdict from npm." ] + }, + { + "id": "GT-706", + "closedAt": "2026-08-16", + "closureCommit": "e61af1da", + "dependencyDisposition": "none", + "evidence": [ + ".harness/scripts/ci/67-validate-declared-exports.mjs", + ".harness/scripts/ci/67-validate-declared-exports.test.mjs", + ".github/workflows/npm-release.yml", + ".github/workflows/ci-cd.yml", + "src/packages/core-domain/package.json" + ], + "validationCommands": [ + "THE CHECK THAT EXISTED WAS THE WRONG SHAPE, MEASURED: npm-release.yml:213 computed 'promised' as [pkg.main, ...Object.values(pkg.bin)], and `exports` was not in that list. A two-file package declaring \"./ingest\": \"./dist/ingest/index.js\" with only dist/index.js on disk passed that assertion run VERBATIM with exit 0, while `require pkg/ingest` answered MODULE_NOT_FOUND.", + "THE GUARD REFUTED THIS ROW'S OWN CLAIM ON ITS FIRST RUN. The row said the registry was clean -- 22 of 22 subpaths resolve, 0 phantom. That measurement excluded wildcard keys by its own filter. core-domain declares ./infrastructure/adapters/* and there is NO adapters directory: 0 matches in a 796-file packlist, MODULE_NOT_FOUND from the published 1.3.1 for every name under it. `git log --all --name-only` over that path returns nothing -- the key was introduced by 2453f9a2 and no commit in this repository ever carried it. Deleted rather than widened, because there was never anything behind it.", + "TWO PLACES ON PURPOSE: ci-cd.yml runs it over every publishable workspace at PR time, npm-release.yml runs it per package inside the publish loop immediately before `npm publish`. A red PR costs a commit; a red release costs a version nobody can withdraw after 72 hours.", + "FALSIFIABILITY OBSERVED ON BOTH SIDES, four ways. RED: the ./ingest fixture; core-domain on the real tree; and a file present ON DISK but excluded by `files`, which a filesystem check would call clean. GREEN: the same fixture once the file ships, and the whole repository -- 68 declared targets across 9 publishable packages.", + "SUPERSET, NOT A SECOND OPINION: main and bin are folded into the same check, and every string leaf of the condition tree counts, so a missing types/*.d.ts fails exactly as a missing default/*.js does.", + "node .harness/scripts/ci/67-validate-declared-exports.mjs --verbose", + "node --test .harness/scripts/ci/67-validate-declared-exports.test.mjs" + ] } ] } 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 1b473743..9f4ced80 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -9943,7 +9943,7 @@ La declaración tiene un hueco — un pack que no declara — y el directorio lo - **Por qué el smoke de sala limpia tampoco lo cubre, y no es un defecto de ese script.** `check-install-smoke.mjs` resuelve cada especificador `@beyondnet/*` que un paquete **importa** (`:74-101`), lo cual es del lado consumidor por diseño — es lo que llegó a cazar este caso. Pero un export fantasma del productor es invisible hasta que alguien lo importa, así que la comprobación dispara **en el turno del consumidor dentro del orden de publicación**, es decir, cuando el productor y todo lo anterior ya son inmutables en el registry. Tarde e irreversible es la parte que cuesta. - **Exposición, medida en todo el workspace:** 3 de 8 paquetes publicables declaran subrutas de export — `contracts` (5), `core-domain` (16), `agent-runtime` (2) — **23 subrutas, ninguna asegurada por la release.** Dos de los tres declaran además un comodín `./*`, que promete que *cualquier* `./dist/*.js` es importable y por construcción no tiene cota. -- **Lo que esta fila NO afirma, porque se midió y es falso.** Hoy no hay ningún export fantasma en el registry. Instalando los publicados actuales `contracts@1.2.0`, `core-domain@1.3.1` y `agent-runtime@1.2.0` en un prefijo limpio y resolviendo cada subruta declarada: **22 resuelven, 0 fantasmas.** El registry está sano; lo que falta es algo que lo mantenga así. Registrar esto como «hay exports rotos» habría sido una fila que se cierra sola por accidente en la siguiente release. +- **Lo que esta fila afirmó del registry, y lo que el guard refutó en su primera corrida.** La fila decía que no había ningún fantasma en el registry — medido, 22 subrutas resuelven, 0 fantasmas. **Esa medición excluía las claves con comodín por su propio filtro, y una de ellas está muerta.** `core-domain` declara `./infrastructure/adapters/*` y **no existe ningún directorio `adapters`**: 0 coincidencias en un packlist de 796 ficheros, y `MODULE_NOT_FOUND` desde el `1.3.1` publicado para cualquier nombre bajo esa ruta. La historia de git dice que nunca fue real — la clave la introdujo `2453f9a2` y **ningún commit de este repositorio llevó jamás ese path**, así que ha sido una promesa imposible desde el día en que se escribió. Un check que se saltara los comodines habría dado ese paquete por limpio, y por eso el guard los comprueba y por eso el criterio de abajo no dejó que se saltaran. - **Casos de uso:** - Un paquete gana una subruta de export y un cambio de `files`/build deja de incluirla en silencio; la release se niega en vez de publicar un manifiesto que miente. - Una actualización de consumidor deja de fallar en instalación por un defecto que pertenece a un paquete publicado días antes. @@ -9955,9 +9955,9 @@ La declaración tiene un hueco — un pack que no declara — y el directorio lo - **Principal:** `S` · **Interés:** `HIGH` · **Base:** `estimate` - **Procedencia:** Registrada el 2026-08-16 desde la release 1.3.x, donde la clase costó dos intentos de publicación fallidos y una release parcial irreversible. Hermana de [`GT-625`](./gap-reference-catalog.es.md#gt-625) y [`GT-671`](./gap-reference-catalog.es.md#gt-671): la misma asimetría árbol-contra-tarball, una capa antes — esas dos preguntan si el artefacto PUBLICADO funciona, esta pregunta si debió publicarse siquiera. - **Criterios de aceptación:** - - [ ] La release resuelve cada destino de `exports` sin comodín contra el packlist del propio paquete, y tumba la publicación cuando falta uno. - - [ ] La aserción corre ANTES del paso irreversible, en el turno del productor — no en la instalación de un consumidor. - - [ ] El comodín `./*` se trata de forma explícita en vez de saltarse: o la fila registra por qué una promesa sin cota es aceptable, o el comodín se estrecha a lo que realmente se incluye. - - [ ] Falsabilidad demostrada: un paquete que declara un export que no empaqueta pone la comprobación en rojo, OBSERVADO, y la misma comprobación sigue verde sobre los tres paquetes reales cuyas 23 subrutas miden sanas hoy. - - [ ] La comprobación nombra el destino que falta y el paquete, para que el fallo sea accionable sin abrir el tarball. -- **Estado:** `PENDIENTE` + - [x] La release resuelve cada destino de `exports` sin comodín contra el packlist del propio paquete, y tumba la publicación cuando falta uno. **CUMPLIDO** — `.harness/scripts/ci/67-validate-declared-exports.mjs`. Recoge **cada hoja de texto del árbol de condiciones**, así que `types` cuenta tanto como `default`: un `.d.ts` que falta rompe a los consumidores de TypeScript igual que un `.js` que falta rompe a Node. `main` y `bin` van incluidos, así que el guard es un **superconjunto** de la aserción en línea que sustituye y no una segunda opinión a su lado. El packlist sale de `npm pack --dry-run --json`, así que `files`, `.npmignore` y toda regla de empaquetado son la respuesta de npm y no una reimplementación suya. + - [x] La aserción corre ANTES del paso irreversible, en el turno del productor — no en la instalación de un consumidor. **CUMPLIDO, y en dos sitios a propósito.** `npm-release.yml` la corre por paquete dentro del bucle de publicación, justo antes de `npm publish`; `ci-cd.yml` la corre sobre todos los workspaces publicables en tiempo de PR. Un PR rojo cuesta un commit, una release roja cuesta una versión que nadie puede retirar, y la comprobación de la release es el suelo para cualquier cosa que llegue al bucle sin pasar por un PR. + - [x] El comodín `./*` se trata de forma explícita en vez de saltarse: o la fila registra por qué una promesa sin cota es aceptable, o el comodín se estrecha a lo que realmente se incluye. **CUMPLIDO, y es el criterio que encontró el segundo defecto.** Un comodín debe casar con **al menos un** fichero empaquetado; cero coincidencias es una promesa muerta y falla. En su primera corrida el guard se puso rojo con `./infrastructure/adapters/*` de `core-domain` — sin directorio `adapters` en un packlist de 796 ficheros, `MODULE_NOT_FOUND` en el `1.3.1` publicado, y **ningún commit de este repositorio llevó jamás ese path**. La promesa se borró, no se amplió, porque nunca hubo nada detrás. Las claves `./*` que quedan resuelven. El `*` se casa como `[^/]*`, anclado por ambos extremos, así que `dist/a.js` no puede satisfacer `dist/nested/*.js`. + - [x] Falsabilidad demostrada: un paquete que declara un export que no empaqueta pone la comprobación en rojo, OBSERVADO, y la misma comprobación sigue verde sobre los tres paquetes reales cuyas 23 subrutas miden sanas hoy. **CUMPLIDO, rojo observado dos veces y verde observado dos veces.** Rojo: la fixture `./ingest`, y `core-domain` sobre el árbol real. Verde: la misma fixture en cuanto el fichero se incluye, y el repositorio entero — **68 destinos declarados en 9 paquetes publicables**. Una tercera fixture cubre el mecanismo que produjo el defecto original: un fichero presente **en disco** pero excluido por `files`, que una comprobación del sistema de ficheros daría por limpio y una del packlist caza. + - [x] La comprobación nombra el destino que falta y el paquete, para que el fallo sea accionable sin abrir el tarball. **CUMPLIDO** — cada línea es `MISSING -> ` o `DEAD -> (el patrón no casa con ningún fichero empaquetado)` bajo `@ ()`, asegurado de punta a punta contra el binario real. Una corrida que resuelva **cero** paquetes, o cuyo packlist no se pueda leer, es un fallo duro: «la comprobación no corrió» nunca debe leerse como «la comprobación no encontró nada». +- **Estado:** `COMPLETADO` diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index ce417dda..b9db006a 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -10038,7 +10038,7 @@ The declaration has one hole — a pack that does not declare — and the direct - **Why the clean-room smoke does not cover it either, and it is not a defect of that script.** `check-install-smoke.mjs` resolves every `@beyondnet/*` specifier a package **imports** (`:74-101`), which is consumer-side by design — it is what caught this one at all. But a producer's phantom export is invisible until somebody imports it, so the check fires **at the consumer's turn in the publish order**, which is after the producer and everything before it are already immutable on the registry. Late and irreversible is the part that costs. - **Exposure, measured across the workspace:** 3 of 8 publishable packages declare export subpaths — `contracts` (5), `core-domain` (16), `agent-runtime` (2) — **23 subpaths, none of them asserted by the release.** Two of the three also declare a `./*` wildcard, which promises that *any* `./dist/*.js` is importable and is therefore unbounded by construction. -- **What this row does NOT claim, because it was measured and is false.** There is no phantom export on the registry today. Installing the current published `contracts@1.2.0`, `core-domain@1.3.1` and `agent-runtime@1.2.0` into a clean prefix and resolving every declared subpath gives **22 resolve, 0 phantom**. The registry is healthy; what is missing is anything that keeps it that way. Registering this as "there are broken exports" would have been a row that closes itself by accident on the next release. +- **What this row claimed about the registry, and what the guard refuted on its first run.** The row said there was no phantom on the registry — measured, 22 subpaths resolve, 0 phantom. **That measurement excluded wildcard keys by its own filter, and one of them is dead.** `core-domain` declares `./infrastructure/adapters/*` and there is **no `adapters` directory at all**: 0 matches in a 796-file packlist, and `MODULE_NOT_FOUND` from the published `1.3.1` for every name under it. Git history says it was never real — the key was introduced by `2453f9a2` and **no commit in this repository ever carried that path**, so it has been a promise nobody could keep since the day it was written. A check that skipped wildcards would have called that package clean, which is why the guard checks them and why the criterion below refused to let them be skipped. - **Use cases:** - A package gains an export subpath and a `files`/build change silently stops shipping it; the release refuses instead of publishing a manifest that lies. - A consumer upgrade stops failing at install time for a defect that belongs to a package published days earlier. @@ -10050,9 +10050,9 @@ The declaration has one hole — a pack that does not declare — and the direct - **Principal:** `S` · **Interest:** `HIGH` · **Basis:** `estimate` - **Provenance:** Registered 2026-08-16 from the 1.3.x release, where the class cost two failed publish attempts and one irreversible partial release. Sibling of [`GT-625`](./gap-reference-catalog.md#gt-625) and [`GT-671`](./gap-reference-catalog.md#gt-671): the same tree-versus-tarball asymmetry, one layer earlier — those two ask whether the PUBLISHED artifact works, this one asks whether the artifact should have been published at all. - **Acceptance criteria:** - - [ ] The release resolves every non-wildcard `exports` target of a package against that package's own packlist, and fails the publish when one is absent. - - [ ] The assertion runs BEFORE the irreversible step, in the producer's own turn — not at a consumer's install. - - [ ] The `./*` wildcard is handled explicitly rather than skipped: either the row records why an unbounded promise is acceptable, or the wildcard is narrowed to what is actually shipped. - - [ ] Proven falsifiable: a package declaring an export it does not pack turns the check red, OBSERVED, and the same check stays green on the three real packages whose 23 subpaths measure healthy today. - - [ ] The check names the missing target and the package, so the failure is actionable without opening the tarball. -- **Status:** `PENDING` + - [x] The release resolves every non-wildcard `exports` target of a package against that package's own packlist, and fails the publish when one is absent. **MET** — `.harness/scripts/ci/67-validate-declared-exports.mjs`. It collects **every string leaf of the condition tree**, so `types` counts as much as `default`: a missing `.d.ts` breaks TypeScript consumers exactly as a missing `.js` breaks Node. `main` and `bin` are folded in, so the guard is a **superset** of the inline assertion it replaces rather than a second opinion beside it. The packlist comes from `npm pack --dry-run --json`, so `files`, `.npmignore` and every other shipping rule are npm's answer and not a reimplementation of it. + - [x] The assertion runs BEFORE the irreversible step, in the producer's own turn — not at a consumer's install. **MET, in two places on purpose.** `npm-release.yml` runs it per package inside the publish loop, immediately before `npm publish`; `ci-cd.yml` runs it over every publishable workspace at PR time. A red PR costs a commit, a red release costs a version nobody can withdraw, and the release check is the floor for anything that reaches the loop without passing through a PR. + - [x] The `./*` wildcard is handled explicitly rather than skipped: either the row records why an unbounded promise is acceptable, or the wildcard is narrowed to what is actually shipped. **MET, and this is the criterion that found the second defect.** A wildcard must match **at least one** packed file; zero matches is a dead promise and fails. On its first run the guard turned red on `core-domain`'s `./infrastructure/adapters/*` — no `adapters` directory in a 796-file packlist, `MODULE_NOT_FOUND` on the published `1.3.1`, and **no commit in this repository ever carried that path**. The promise was deleted, not widened, because there was never anything behind it. The surviving `./*` keys resolve. `*` is matched as `[^/]*`, anchored at both ends, so `dist/a.js` cannot satisfy `dist/nested/*.js`. + - [x] Proven falsifiable: a package declaring an export it does not pack turns the check red, OBSERVED, and the same check stays green on the three real packages whose 23 subpaths measure healthy today. **MET, red observed twice and green observed twice.** Red: the `./ingest` fixture, and `core-domain` on the real tree. Green: the same fixture once the file ships, and the whole repository — **68 declared targets across 9 publishable packages**. A third fixture covers the mechanism that produced the original defect: a file present **on disk** but excluded by `files`, which a filesystem check would call clean and a packlist check catches. + - [x] The check names the missing target and the package, so the failure is actionable without opening the tarball. **MET** — each line is `MISSING -> ` or `DEAD -> (pattern matches no packed file)` under `@ ()`, asserted end to end against the real binary. A run that resolves **zero** packages, or whose packlist cannot be read, is a hard failure: "the check did not run" must never read as "the check found nothing". +- **Status:** `DONE` diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index c3e2f8c3..c844e810 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -722,10 +722,10 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-668`](./gap-reference-catalog.es.md#gt-668) | **La prueba de `GT-666` de que a su guarda se la había visto fallar alguna vez estaba anclada a una REFERENCIA MÓVIL, así que dejó de ser evidencia justo en el momento en que el arreglo aterrizó.** El único caso que lee el artefacto real previo al arreglo lo obtenía con `git show ${PRE_FIX_REF}:iso-5055-mapping.json`, con `PRE_FIX_REF` por defecto a `origin/develop`. Eso solo es cierto mientras el arreglo vive en una rama: **`59d62bae` se mergeó, `origin/develop` empezó a servir el artefacto CORREGIDO, y el caso que afirma 64 hallazgos encontró 0** — verde en su propia rama, rojo justo cuando importaba, bloqueando el PR de promoción `develop` → `main` **#483** en `Governance guards (GT-578)`. **Reproducido antes de actuar**, no tomado del traspaso: `node --test` sobre `develop` en `59d62bae` falla ese caso con `0 !== 64` mientras los otros 18 pasan. **La mitad peor es la que nadie habría visto:** el caso llevaba `if (before.status !== 0) return void assert.ok(true, 'SKIPPED: …')` para clones superficiales, así que en un checkout con historial truncado la misma podredumbre habría **pasado en silencio** en vez de fallar — una vía de escape de la única prueba de que la guarda estuvo roja alguna vez. **ENTREGADO 2026-08-09.** El razonamiento del comentario era correcto y se conserva: *«leído de git en vez de reconstruido … Los fixtures reconstruidos coinciden con lo que el autor creía que estaba mal; este no puede.»* Eso argumenta a favor de un artefacto REAL previo al arreglo, no de leer una rama en tiempo de test — así que el artefacto queda **congelado en el repositorio**: `.harness/fixtures/standards-rule-class/iso-5055-mapping.pre-gt-666.json`, tomado de `01308346` (`59d62bae^`, blob `6684e8a4`), **sin recortar**, y verificado byte a byte idéntico a ese blob una vez se quita su única clave añadida `_fixture` de procedencia. El `git show`, la variable de entorno `PRE_FIX_REF` y el salto por clon superficial quedan **eliminados** — un salto solo puede ocultar un fallo. **Afirmaciones sin cambios y sin debilitar: 64 hallazgos, 16 de ellos `is classified `governance``**, medidos contra el fixture congelado y los packs de hoy. Se AÑADE un caso en vez de relajar ninguno: un fixture «refrescado» desde el mapeo vivo es ROJO, y eso se **observó** — se simuló el refresco y pone en rojo 2 de 20 casos, así que el fixture negativo no puede convertirse en silencio en una copia del artefacto que existe para atrapar. Metaguardas reejecutadas: `42` 78/78 clasificadas, `43` 54/54 vistas fallar. | — | — | `Evolith Core` | Cross | P1 | XS | `COMPLETADO` | | [`GT-704`](./gap-reference-catalog.es.md#gt-704) | **Ningún job de CI corre LOS DOS motores sobre el corpus, así que una divergencia de veredicto entre ellos la descubre una persona o no la descubre nadie.** Separado de `GT-675`, cuyo AC7 pedía ese barrido y lo costeaba en ≈17 s. Lo que `GT-675` entregó es más estrecho y a propósito: un guard que ejecuta el bundle compilado y falla si no sabe declarar su propio alcance — la invariante cuya pérdida silenciosa des-arreglaría 234 reglas. No compara los dos motores. Medido el 2026-08-16 con ambos sobre el corpus completo: **16 ids reciben VEREDICTOS opuestos** (`passed` en uno, `failed` en el otro) — `TAX-01`, `TAX-05`, `INH-02`, `SVC-03`, `ACL-01` entre ellos —, que es la clase que a `ADR-0041` sí le importa, frente a las 82 diferencias de cobertura (65 decide-OPA/salta-nativo, 17 al revés) que son legítimas. También medido: `cli/exit-code-taxonomy`, que `GT-675` llamaba el pack de control donde ambos coinciden, NO coincide — nativo `passed / exit 0` contra OPA `failed / exit 2`. | Nadie corre los dos motores en paralelo, así que pueden separarse sin que nadie lo note. | Una discrepancia entre motores la caza CI el día que aparece. | `Core Domain` | Cross | P2 | M | `PENDIENTE` | | [`GT-705`](./gap-reference-catalog.es.md#gt-705) | **El servidor MCP publicado no traía corpus de rulesets Y adivinaba dónde estaba Core, así que 48 de sus 50 herramientas no podían gobernar nada desde una instalación limpia.** Encontrado por el canario de `GT-671`. **ARREGLADO el 2026-08-16 — dos causas independientes, y arreglar solo una no cambiaba nada.** (1) `files: ["dist/"…]` no llevaba corpus y ninguna dependencia lo aportaba; el paquete empaqueta ahora **los dos** árboles que el servidor necesita — corpus de rulesets y definiciones de gate SDLC. Que hacían falta ambos se OBSERVÓ, no se predijo: con solo el corpus, `evolith-validate` funcionaba y `evolith-gate-evaluate` seguía sin hacerlo. (2) `path.join(process.cwd(), '..', 'evolith')` — un directorio hermano con el nombre de este monorepo — en **9 sitios de 5 ficheros de mcp-server y 4 servicios de core-domain**, la capa que comparten las tres superficies. Ahora hay un solo resolutor: llamante → `EVOLITH_CORE_PATH` → subir desde el satélite → corpus empaquetado; `process.cwd()` no aparece. La búsqueda cualifica **por contenido**, lo que además cierra `GT-566` en esas cuatro copias — buscaban un directorio LLAMADO `rulesets` y este repo tiene un `rulesets/agents` que comparte nombre y no tiene reglas. DE PUNTA A PUNTA desde una instalación npm limpia y sin repositorio en disco: `evolith-validate` `INTERNAL_ERROR` → **veredicto `failed`**; `evolith-gate-evaluate` `RULESET_NOT_FOUND` → **veredicto `failed`, gate `business-sign-off`**. Tres specs aseguraban el contrato viejo y se reescribieron — uno se llamaba *«falls back to the sibling ../evolith convention»*. | El servidor MCP que instalas de npm anunciaba 50 herramientas y solo respondía las que no necesitan reglas. | Un agente conectado al servidor publicado puede gobernar de verdad. | `MCP Server` | Cross | P1 | M | `COMPLETADO` | -| [`GT-706`](./gap-reference-catalog.es.md#gt-706) | **Nada asegura que los `exports` que un paquete declara resuelvan dentro de su propio tarball, así que un productor publica una subruta fantasma y solo la descubre un consumidor — una publicación demasiado tarde.** `contracts@1.1.0` declaró una subruta de export que no incluía; el fallo salió en el smoke de sala limpia de `infra-providers@1.2.1`, **después de que `core-domain@1.3.1` ya estuviera irreversiblemente en el registry**, dejando la release a medio entregar y sin despublicar posible pasadas 72 horas. La comprobación que existe es real y tiene la forma equivocada: `npm-release.yml:213` calcula «prometidos» como `[pkg.main, ...bin]`, y **`exports` no está en esa lista**. FALSABILIDAD DEMOSTRADA, OBSERVADA EN VERDE: un paquete de dos ficheros que declara `"./ingest"` con solo `dist/index.js` en disco pasa esa aserción corrida literal — `exit=0`, mientras `require pkg/ingest` responde `MODULE_NOT_FOUND`. El smoke de sala limpia tampoco lo cubre, y no es defecto suyo: resuelve lo que un paquete IMPORTA, así que el fantasma del productor es invisible hasta el turno de un consumidor, que es después del paso irreversible. Exposición: 3 de 8 paquetes publicables declaran **23 subrutas de export**, ninguna asegurada, y dos declaran además un `./*` sin cota. **Lo que esta fila NO afirma, medido:** hoy no hay ningún fantasma en el registry — los tres paquetes publicados resuelven **22 de 22**. El registry está sano; nada lo mantiene así. | Un paquete puede prometer una ruta de import que nunca incluyó, y quien se entera es el siguiente paquete en publicarse. | La release se niega a publicar un manifiesto que miente, antes de que nada sea irreversible. | `Infra` | Cross | P1 | S | `PENDIENTE` | +| [`GT-706`](./gap-reference-catalog.es.md#gt-706) | **Nada asegura que los `exports` que un paquete declara resuelvan dentro de su propio tarball, así que un productor publica una subruta fantasma y solo la descubre un consumidor — una publicación demasiado tarde.** `contracts@1.1.0` declaró una subruta de export que no incluía; el fallo salió en el smoke de sala limpia de `infra-providers@1.2.1`, **después de que `core-domain@1.3.1` ya estuviera irreversiblemente en el registry**, dejando la release a medio entregar y sin despublicar posible pasadas 72 horas. La comprobación que existe es real y tiene la forma equivocada: `npm-release.yml:213` calcula «prometidos» como `[pkg.main, ...bin]`, y **`exports` no está en esa lista**. FALSABILIDAD DEMOSTRADA, OBSERVADA EN VERDE: un paquete de dos ficheros que declara `"./ingest"` con solo `dist/index.js` en disco pasa esa aserción corrida literal — `exit=0`, mientras `require pkg/ingest` responde `MODULE_NOT_FOUND`. El smoke de sala limpia tampoco lo cubre, y no es defecto suyo: resuelve lo que un paquete IMPORTA, así que el fantasma del productor es invisible hasta el turno de un consumidor, que es después del paso irreversible. Exposición: 3 de 8 paquetes publicables declaran **23 subrutas de export**, ninguna asegurada, y dos declaran además un `./*` sin cota. **ARREGLADO 2026-08-16 — `.harness/scripts/ci/67-validate-declared-exports.mjs`, corriendo en tiempo de PR sobre todos los workspaces publicables Y por paquete dentro del bucle de release, justo antes de `npm publish`.** Recoge cada hoja de texto del árbol de condiciones, así que `types` cuenta tanto como `default`, e incluye `main`/`bin`, siendo un superconjunto de la aserción que sustituye. **La propia afirmación de esta fila sobre el registry la refutó el guard en su primera corrida:** «22 de 22 resuelven, 0 fantasmas» excluía las claves con comodín por su propio filtro, y una está MUERTA — `core-domain` declara `./infrastructure/adapters/*` **sin ningún directorio `adapters`**, 0 coincidencias en un packlist de 796 ficheros, `MODULE_NOT_FOUND` en el 1.3.1 publicado, y **ningún commit de este repositorio llevó jamás ese path**. Borrada, no ampliada: nunca hubo nada detrás. Falsabilidad observada por los dos lados — rojo con la fixture `./ingest`, con `core-domain` de verdad, y con un fichero presente en disco pero excluido por `files`; verde con la misma fixture en cuanto se incluye y con el árbol entero, **68 destinos declarados en 9 paquetes**. | Un paquete puede prometer una ruta de import que nunca incluyó, y quien se entera es el siguiente paquete en publicarse. | La release se niega a publicar un manifiesto que miente, antes de que nada sea irreversible. | `Infra` | Cross | P1 | S | `COMPLETADO` | -**Progreso:** 672 / 704 completados · 2 en progreso · 3 pendientes · 27 diferidos +**Progreso:** 673 / 704 completados · 2 en progreso · 2 pendientes · 27 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 d86f6f26..d1df9fa3 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -722,10 +722,10 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-668`](./gap-reference-catalog.md#gt-668) | **`GT-666`'s proof that its guard had ever been observed failing was anchored to a MOVING REF, so it stopped being evidence at the exact moment the fix landed.** The one case that reads the real pre-fix artifact obtained it with `git show ${PRE_FIX_REF}:iso-5055-mapping.json`, `PRE_FIX_REF` defaulting to `origin/develop`. That is true only while the fix lives on a branch: **`59d62bae` merged, `origin/develop` began serving the CORRECTED artifact, and the case asserting 64 findings found 0** — green on its own branch, red the moment it mattered, blocking the `develop` → `main` promotion PR **#483** in `Governance guards (GT-578)`. **Reproduced before acting**, not taken from the handover: `node --test` on `develop` at `59d62bae` fails that one case with `0 !== 64` while the other 18 pass. **The worse half is the one nobody would have seen:** the case carried `if (before.status !== 0) return void assert.ok(true, 'SKIPPED: …')` for shallow clones, so in a checkout with a truncated history the same rot would have **passed in silence** rather than failing — an escape hatch out of the only case that proves the guard was ever red. **DELIVERED 2026-08-09.** The comment's reasoning was right and is kept: *«read out of git rather than reconstructed … Reconstructed fixtures agree with whatever the author believed was wrong; this one cannot.»* That argues for a REAL pre-fix artifact, not for reading a branch at test time — so the artifact is **frozen in the repository**: `.harness/fixtures/standards-rule-class/iso-5055-mapping.pre-gt-666.json`, taken from `01308346` (`59d62bae^`, blob `6684e8a4`), **not trimmed**, and verified byte-identical to that blob once its one added `_fixture` provenance key is dropped. The `git show`, the `PRE_FIX_REF` env var and the shallow-clone skip are **deleted** — a skip can only ever hide a failure. **Assertions unchanged and unweakened: 64 findings, 16 of them `is classified `governance``**, measured against the frozen fixture and today's packs. One case is ADDED rather than any relaxed: a fixture «refreshed» from the live mapping is RED, and that was **observed** — the refresh was simulated and turns 2 of 20 cases red, so the negative fixture cannot silently become a copy of the artifact it exists to catch. Meta-guards re-run: `42` 78/78 classified, `43` 54/54 observed failing. | — | — | `Evolith Core` | Cross | P1 | XS | `DONE` | | [`GT-704`](./gap-reference-catalog.md#gt-704) | **No CI job runs BOTH engines over the corpus, so a verdict divergence between them is discovered by a human or not at all.** Split out of `GT-675`, whose AC7 asked for the sweep and costed it at ≈17 s. What `GT-675` shipped is narrower and deliberately so: a guard that executes the compiled bundle and fails if it cannot state its own scope — the invariant whose quiet loss would silently un-fix 234 rules. It does not compare the two engines. Measured 2026-08-16 with both engines on the whole corpus: **16 rule ids get opposite VERDICTS** (`passed` on one, `failed` on the other) — `TAX-01`, `TAX-05`, `INH-02`, `SVC-03`, `ACL-01` among them — which is the class `ADR-0041` does care about, as opposed to the 82 coverage differences (65 OPA-decides/native-skips, 17 the reverse) that are legitimate. Also measured: `cli/exit-code-taxonomy`, which `GT-675` called the control pack where both engines agree, does NOT agree — native `passed / exit 0` against OPA `failed / exit 2`. | Nobody runs the two engines side by side, so they can drift apart unnoticed. | A disagreement between the engines is caught by CI the day it appears. | `Core Domain` | Cross | P2 | M | `PENDING` | | [`GT-705`](./gap-reference-catalog.md#gt-705) | **The published MCP server shipped no ruleset corpus AND guessed where Core was, so 48 of its 50 tools could not govern anything from a clean install.** Found by `GT-671`'s canary. **FIXED 2026-08-16 — two independent causes, and fixing either alone changed nothing.** (1) `files: ["dist/"…]` carried no corpus and no dependency supplied one; the package now bundles **both** trees the server needs — the ruleset corpus and the SDLC gate definitions. That both were required was OBSERVED, not predicted: with only the corpus, `evolith-validate` worked and `evolith-gate-evaluate` still did not. (2) `path.join(process.cwd(), '..', 'evolith')` — a sibling directory named after this monorepo — in **9 places across 5 files of mcp-server and 4 services of core-domain**, the layer all three surfaces share. One resolver now: caller → `EVOLITH_CORE_PATH` → walk up from the satellite → bundled corpus; `process.cwd()` is absent. The walk qualifies **by content**, which also closes `GT-566` in those four copies — they probed for a directory NAMED `rulesets` and this repo has a `rulesets/agents` that shares the name and holds no rules. END TO END from a clean npm install with no repository on disk: `evolith-validate` `INTERNAL_ERROR` → **verdict `failed`**; `evolith-gate-evaluate` `RULESET_NOT_FOUND` → **verdict `failed`, gate `business-sign-off`**. Three specs asserted the old contract and were rewritten — one was named *"falls back to the sibling ../evolith convention"*. | The MCP server you install from npm announced 50 tools and could only answer the ones needing no rules. | An agent connecting to the published server can actually govern something. | `MCP Server` | Cross | P1 | M | `DONE` | -| [`GT-706`](./gap-reference-catalog.md#gt-706) | **Nothing asserts that a package's own declared `exports` resolve inside its own tarball, so a producer publishes a phantom subpath and only a consumer discovers it — one publish too late.** `contracts@1.1.0` declared an export subpath it did not ship; the failure surfaced at `infra-providers@1.2.1`'s clean-room smoke, **after `core-domain@1.3.1` was already irreversibly on the registry**, leaving the release half-shipped with no unpublish available after 72 hours. The check that exists is real and the wrong shape: `npm-release.yml:213` computes "promised" as `[pkg.main, ...bin]`, and **`exports` is not in that list**. PROVEN FALSIFIABLE, OBSERVED GREEN: a two-file package declaring `"./ingest"` with only `dist/index.js` on disk passes that assertion run verbatim — `exit=0`, while `require pkg/ingest` answers `MODULE_NOT_FOUND`. The clean-room smoke does not cover it either, and that is not its defect: it resolves what a package IMPORTS, so a producer's phantom is invisible until a consumer's turn, which is after the irreversible step. Exposure: 3 of 8 publishable packages declare **23 export subpaths**, none asserted, two of them also declaring an unbounded `./*`. **What this row does NOT claim, measured:** there is no phantom on the registry today — the three published packages resolve **22 of 22**. The registry is healthy; nothing keeps it that way. | A package can promise an import path it never shipped, and the next package to publish is the one that finds out. | The release refuses to publish a manifest that lies, before anything becomes irreversible. | `Infra` | Cross | P1 | S | `PENDING` | +| [`GT-706`](./gap-reference-catalog.md#gt-706) | **Nothing asserts that a package's own declared `exports` resolve inside its own tarball, so a producer publishes a phantom subpath and only a consumer discovers it — one publish too late.** `contracts@1.1.0` declared an export subpath it did not ship; the failure surfaced at `infra-providers@1.2.1`'s clean-room smoke, **after `core-domain@1.3.1` was already irreversibly on the registry**, leaving the release half-shipped with no unpublish available after 72 hours. The check that exists is real and the wrong shape: `npm-release.yml:213` computes "promised" as `[pkg.main, ...bin]`, and **`exports` is not in that list**. PROVEN FALSIFIABLE, OBSERVED GREEN: a two-file package declaring `"./ingest"` with only `dist/index.js` on disk passes that assertion run verbatim — `exit=0`, while `require pkg/ingest` answers `MODULE_NOT_FOUND`. The clean-room smoke does not cover it either, and that is not its defect: it resolves what a package IMPORTS, so a producer's phantom is invisible until a consumer's turn, which is after the irreversible step. Exposure: 3 of 8 publishable packages declare **23 export subpaths**, none asserted, two of them also declaring an unbounded `./*`. **FIXED 2026-08-16 — `.harness/scripts/ci/67-validate-declared-exports.mjs`, run at PR time over every publishable workspace AND per package inside the release loop, immediately before `npm publish`.** It collects every string leaf of the condition tree, so `types` counts as much as `default`, and folds in `main`/`bin`, making it a superset of the assertion it replaces. **The row's own claim about the registry was refuted by the guard on its first run:** "22 of 22 resolve, 0 phantom" excluded wildcard keys by its own filter, and one is DEAD — `core-domain` declares `./infrastructure/adapters/*` with **no `adapters` directory at all**, 0 matches in a 796-file packlist, `MODULE_NOT_FOUND` on the published 1.3.1, and **no commit in this repository ever carried that path**. Deleted, not widened: there was never anything behind it. Falsifiability observed on both sides — red on the `./ingest` fixture, on `core-domain` for real, and on a file present on disk but excluded by `files`; green on the same fixture once it ships and on the whole tree, **68 declared targets across 9 packages**. | A package can promise an import path it never shipped, and the next package to publish is the one that finds out. | The release refuses to publish a manifest that lies, before anything becomes irreversible. | `Infra` | Cross | P1 | S | `DONE` | -**Progress:** 672 / 704 done · 2 in progress · 3 pending · 27 deferred +**Progress:** 673 / 704 done · 2 in progress · 2 pending · 27 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 512213f6..29034563 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -27,8 +27,8 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so |---:|---|---|---| | 1 | Bloqueadores P0 | Impiden afirmar readiness productivo o release mayor. | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435) | | 2 | Área de mayor riesgo | `Governance` tiene la mayor carga ponderada abierta. | [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-669](../gaps/gap-reference-catalog.es.md#gt-669), [GT-672](../gaps/gap-reference-catalog.es.md#gt-672), [GT-689](../gaps/gap-reference-catalog.es.md#gt-689), [GT-588](../gaps/gap-reference-catalog.es.md#gt-588), +2 | -| 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-706](../gaps/gap-reference-catalog.es.md#gt-706) | -| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-706](../gaps/gap-reference-catalog.es.md#gt-706), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-669](../gaps/gap-reference-catalog.es.md#gt-669), +1 | +| 3 | Ganancias rápidas | Alta criticidad con complejidad XS/S. | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684) | +| 4 | Ola P1 | Endurecimiento siguiente después de limpiar P0. | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-669](../gaps/gap-reference-catalog.es.md#gt-669), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448) | | 5 | P2/P3 | Solo después de estabilizar seguridad, CI, reglas y contratos. | [GT-444](../gaps/gap-reference-catalog.es.md#gt-444), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-685](../gaps/gap-reference-catalog.es.md#gt-685), [GT-686](../gaps/gap-reference-catalog.es.md#gt-686), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), +12 | ## Bloqueadores Actuales @@ -43,21 +43,21 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so |---|---:| | Fecha canónica del tablero | 2026-08-08 | | Gaps totales | 704 | -| Gaps cerrados | 672 | -| Gaps pendientes | 32 | +| Gaps cerrados | 673 | +| Gaps pendientes | 31 | | P0 abiertos | 1 | -| P1 abiertos | 9 | +| P1 abiertos | 8 | | P2 abiertos | 18 | -| Cierre total | 95.5% | -| Registros de evidencia de cierre | 654 | +| Cierre total | 95.6% | +| Registros de evidencia de cierre | 655 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | |---|---:|---:|---:|---| | `Governance` | 8 | 0 | 3 | [GT-670](../gaps/gap-reference-catalog.es.md#gt-670), [GT-585](../gaps/gap-reference-catalog.es.md#gt-585), [GT-669](../gaps/gap-reference-catalog.es.md#gt-669), [GT-672](../gaps/gap-reference-catalog.es.md#gt-672), +4 | | `Cross` | 3 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.es.md#gt-435), [GT-448](../gaps/gap-reference-catalog.es.md#gt-448), [GT-651](../gaps/gap-reference-catalog.es.md#gt-651) | -| `Infra` | 6 | 0 | 2 | [GT-706](../gaps/gap-reference-catalog.es.md#gt-706), [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-685](../gaps/gap-reference-catalog.es.md#gt-685), +2 | | `MCP Server` | 3 | 0 | 3 | [GT-684](../gaps/gap-reference-catalog.es.md#gt-684), [GT-680](../gaps/gap-reference-catalog.es.md#gt-680), [GT-681](../gaps/gap-reference-catalog.es.md#gt-681) | +| `Infra` | 5 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.es.md#gt-324), [GT-464](../gaps/gap-reference-catalog.es.md#gt-464), [GT-685](../gaps/gap-reference-catalog.es.md#gt-685), [GT-691](../gaps/gap-reference-catalog.es.md#gt-691), +1 | | `Core Domain` | 4 | 0 | 0 | [GT-674](../gaps/gap-reference-catalog.es.md#gt-674), [GT-687](../gaps/gap-reference-catalog.es.md#gt-687), [GT-678](../gaps/gap-reference-catalog.es.md#gt-678), [GT-704](../gaps/gap-reference-catalog.es.md#gt-704) | ## 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 c56ab276..daaa0390 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -27,8 +27,8 @@ Use this summary with a simple rule: if you need context, open only the linked I |---:|---|---|---| | 1 | P0 blockers | They prevent production-readiness or major-release confidence. | [GT-435](../gaps/gap-reference-catalog.md#gt-435) | | 2 | Highest-risk area | `Governance` has the largest weighted open load. | [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-669](../gaps/gap-reference-catalog.md#gt-669), [GT-672](../gaps/gap-reference-catalog.md#gt-672), [GT-689](../gaps/gap-reference-catalog.md#gt-689), [GT-588](../gaps/gap-reference-catalog.md#gt-588), +2 | -| 3 | Quick wins | High criticality with XS/S complexity. | [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-706](../gaps/gap-reference-catalog.md#gt-706) | -| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-706](../gaps/gap-reference-catalog.md#gt-706), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-669](../gaps/gap-reference-catalog.md#gt-669), +1 | +| 3 | Quick wins | High criticality with XS/S complexity. | [GT-684](../gaps/gap-reference-catalog.md#gt-684) | +| 4 | P1 wave | Next hardening after P0 is cleared. | [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-669](../gaps/gap-reference-catalog.md#gt-669), [GT-448](../gaps/gap-reference-catalog.md#gt-448) | | 5 | P2/P3 | Only after security, CI, rules, and contracts stabilize. | [GT-444](../gaps/gap-reference-catalog.md#gt-444), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-685](../gaps/gap-reference-catalog.md#gt-685), [GT-686](../gaps/gap-reference-catalog.md#gt-686), [GT-687](../gaps/gap-reference-catalog.md#gt-687), +12 | ## Current Blockers @@ -43,21 +43,21 @@ Use this summary with a simple rule: if you need context, open only the linked I |---|---:| | Canonical board date | 2026-08-08 | | Total gaps | 704 | -| Closed gaps | 672 | -| Open gaps | 32 | +| Closed gaps | 673 | +| Open gaps | 31 | | Open P0 | 1 | -| Open P1 | 9 | +| Open P1 | 8 | | Open P2 | 18 | -| Total closure | 95.5% | -| Closure evidence records | 654 | +| Total closure | 95.6% | +| Closure evidence records | 655 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | |---|---:|---:|---:|---| | `Governance` | 8 | 0 | 3 | [GT-670](../gaps/gap-reference-catalog.md#gt-670), [GT-585](../gaps/gap-reference-catalog.md#gt-585), [GT-669](../gaps/gap-reference-catalog.md#gt-669), [GT-672](../gaps/gap-reference-catalog.md#gt-672), +4 | | `Cross` | 3 | 1 | 1 | [GT-435](../gaps/gap-reference-catalog.md#gt-435), [GT-448](../gaps/gap-reference-catalog.md#gt-448), [GT-651](../gaps/gap-reference-catalog.md#gt-651) | -| `Infra` | 6 | 0 | 2 | [GT-706](../gaps/gap-reference-catalog.md#gt-706), [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-685](../gaps/gap-reference-catalog.md#gt-685), +2 | | `MCP Server` | 3 | 0 | 3 | [GT-684](../gaps/gap-reference-catalog.md#gt-684), [GT-680](../gaps/gap-reference-catalog.md#gt-680), [GT-681](../gaps/gap-reference-catalog.md#gt-681) | +| `Infra` | 5 | 0 | 1 | [GT-324](../gaps/gap-reference-catalog.md#gt-324), [GT-464](../gaps/gap-reference-catalog.md#gt-464), [GT-685](../gaps/gap-reference-catalog.md#gt-685), [GT-691](../gaps/gap-reference-catalog.md#gt-691), +1 | | `Core Domain` | 4 | 0 | 0 | [GT-674](../gaps/gap-reference-catalog.md#gt-674), [GT-687](../gaps/gap-reference-catalog.md#gt-687), [GT-678](../gaps/gap-reference-catalog.md#gt-678), [GT-704](../gaps/gap-reference-catalog.md#gt-704) | ## 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 53925296..7d3412e6 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -4,14 +4,14 @@ "asOf": "2026-08-08", "gaps": { "total": 704, - "done": 672, - "pending": 3, + "done": 673, + "pending": 2, "inProgress": 2, "deferred": 27 }, "evidence": { - "closureRecords": 654, - "cliPackage": "@beyondnet/evolith-cli@1.3.1", + "closureRecords": 655, + "cliPackage": "@beyondnet/evolith-cli@1.3.2", "adrCount": 141, "rulesetCount": 181, "schemaCount": 50 diff --git a/src/packages/core-domain/package.json b/src/packages/core-domain/package.json index ce0a0617..368c2230 100644 --- a/src/packages/core-domain/package.json +++ b/src/packages/core-domain/package.json @@ -1,6 +1,6 @@ { "name": "@beyondnet/evolith-core-domain", - "version": "1.3.1", + "version": "1.3.2", "description": "Shared Domain and Application logic for Evolith Core", "keywords": [ "evolith", @@ -64,10 +64,6 @@ "types": "./dist/infrastructure/transparency/index.d.ts", "default": "./dist/infrastructure/transparency/index.js" }, - "./infrastructure/adapters/*": { - "types": "./dist/infrastructure/adapters/*.d.ts", - "default": "./dist/infrastructure/adapters/*.js" - }, "./domain/interfaces": { "types": "./dist/domain/interfaces.d.ts", "default": "./dist/domain/interfaces.js" diff --git a/src/rulesets/contracts/evolith-machine-contracts.json b/src/rulesets/contracts/evolith-machine-contracts.json index 398d2e72..65028da3 100644 --- a/src/rulesets/contracts/evolith-machine-contracts.json +++ b/src/rulesets/contracts/evolith-machine-contracts.json @@ -3,7 +3,7 @@ "compatibilityPolicy": "semver-major", "producer": { "package": "@beyondnet/evolith-cli", - "version": "1.3.1" + "version": "1.3.2" }, "schemas": [ { diff --git a/src/sdk/cli/package.json b/src/sdk/cli/package.json index 93b2d80e..a33e0cd8 100644 --- a/src/sdk/cli/package.json +++ b/src/sdk/cli/package.json @@ -1,6 +1,6 @@ { "name": "@beyondnet/evolith-cli", - "version": "1.3.1", + "version": "1.3.2", "description": "Evolith CLI - Governance, standards validation, and AI agent integration for satellite repositories", "main": "dist/main.js", "bin": { @@ -71,7 +71,7 @@ "dependencies": { "@clack/prompts": "1.5.1", "@beyondnet/evolith-agent-runtime": "1.2.0", - "@beyondnet/evolith-core-domain": "1.3.1", + "@beyondnet/evolith-core-domain": "1.3.2", "@beyondnet/evolith-infra-providers": "1.2.1", "@beyondnet/evolith-sdk": "2.0.0", "@hono/node-server": "1.19.14",