From 05c3f62bf5be6af88dbbed03fbddb3e293f0f760 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:03:54 -0500 Subject: [PATCH 01/22] fix(init): make a freshly scaffolded satellite pass its own governance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GT-595 config-shaped slice gave MTN-05 and GIT-08 real handlers, and the first thing they reported was that `evolith init` produces a repository that fails its own first `evolith validate` with two REAL verdicts — not skips. Both were pinned by name in rule-applicability.integration.spec.ts so that closing them would be visible. They are closed here, by different means, because they are different kinds of defect. GIT-08 was a SCAFFOLD gap. Conventional Commits bind from the first commit and the corpus already states the convention and its type list verbatim, so there is nothing to decide and nothing to invent. `init` now emits all three legs the handler reads: commitlint.config.mjs, the commitlint packages in devDependencies, and — with `--features hooks` — a .husky/commit-msg that runs `npx --no-install commitlint`, so a missing tool is an ERROR rather than the silent skip GT-623 found. When hooks are not selected the config is still written and the result carries a warning saying nothing runs it, rather than leaving a config that looks like enforcement and is not. It is runtime-independent on purpose: commitlint is a Node tool, so a .NET or Python satellite carries a tooling-only package.json — which is what such repositories do in practice and what GIT-08's handler actually reads. Emitting the config alone would have left the rule failing for every non-Node runtime. MTN-05 was NOT a scaffold gap, and emitting a `spec.boundedContexts` stanza would have been the wrong fix. The rule's own description says the strategy MUST be defined "before Phase 2 Design", and a phase-0 scaffold has no bounded contexts to declare; satisfying it at init would have meant writing an invented persistence decision into every new repository, after which MTN-05 would pass for a reason nobody chose. Worse, per the handler's own note the two vocabularies are irreconcilable — evolith-yaml.schema.json restricts `persistence` to a database-engine enum under additionalProperties:false — so any value written either breaks the contract schema or cannot match the rule. It is annotated `appliesFromSdlcPhase: 2` instead, which is the applicability fact the rule always stated in prose. The rule is DEFERRED, not weakened: a repository that has reached Design is still failed by it for the same reason, and that is asserted rather than assumed. The pinned assertion now reads [] — every remaining blocking finding on a fresh satellite is the GT-595 blocking-and-skipped invariant, not a verdict against anything the user did. Verified: core-domain 1442 passed, infra-providers 129 passed, sdk/cli 1436 passed, ruleset schema gate 168/168, native-evaluator parity 451 assertions, doc validation 1491 files. Confirmed per-rule rather than by the suite alone: GIT-08 now executes and PASSES (typescript and python), and MTN-05 is reported not-applicable rather than silently passing. Co-Authored-By: Claude Opus 5 --- .../services/project-scaffolder.service.ts | 82 ++++++++++ .../initialize-project.use-case.spec.ts | 152 ++++++++++++++++++ .../use-cases/initialize-project.use-case.ts | 15 ++ .../rule-applicability.integration.spec.ts | 61 +++++-- .../adr/adr-0010-multi-tenancy.rules.json | 1 + 5 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts diff --git a/src/packages/core-domain/src/application/services/project-scaffolder.service.ts b/src/packages/core-domain/src/application/services/project-scaffolder.service.ts index 5666b86a8..06a7fbbb5 100644 --- a/src/packages/core-domain/src/application/services/project-scaffolder.service.ts +++ b/src/packages/core-domain/src/application/services/project-scaffolder.service.ts @@ -2,6 +2,18 @@ import { IFileSystem } from '../../domain/interfaces'; import { IPlatformProviders } from '../ports/platform-detection.port'; import { InitProjectInput } from './index'; +/** + * The commit types GIT-08 itself enumerates, in its own `pattern`. Kept in the + * same order so the two documents can be diffed by eye. + */ +const CONVENTIONAL_COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']; + +/** Pinned alongside the config, because GIT-08 asserts the tool is installable. */ +const COMMITLINT_DEPENDENCIES: Readonly> = Object.freeze({ + '@commitlint/cli': '^19.0.0', + '@commitlint/config-conventional': '^19.0.0', +}); + export class ProjectScaffolderService { private readonly fs: IFileSystem; @@ -188,6 +200,76 @@ evolith sdlc gate-status await this.fs.writeFile(`${projectDir}/src/main.py`, `def main():\n print("${input.name} initialized")\n\nif __name__ == "__main__":\n main()\n`); } + /** + * Scaffold the Conventional Commits enforcement GIT-08 requires. + * + * GIT-08 is `blocking` and `MUST`, and it binds from the first commit — unlike + * the artifacts of a later lifecycle phase, there is nothing to decide and + * nothing to invent here: the corpus already states the convention and its type + * list, so the scaffold can write it verbatim. Before this, `evolith init` + * produced a repository that mandated Conventional Commits and enforced them + * with nothing, and its own first `evolith validate` said so. + * + * Three things are written, because GIT-08's handler checks all three legs and + * the middle one is the failure GT-623 found: + * + * 1. `commitlint.config.mjs` — the configuration; + * 2. the commitlint packages in `devDependencies` — a config whose tool is + * never installed cannot run, and a `commit-msg` hook that shrugs and exits + * zero when it is missing is worse than no hook at all; + * 3. `.husky/commit-msg` — the thing that actually runs it, when the caller + * asked for hooks. `npx --no-install` is deliberate: if commitlint is not + * installed the hook FAILS, it does not skip. + * + * Runtime-independent on purpose. commitlint is a Node tool, so a .NET or + * Python satellite that adopts it carries a tooling-only `package.json` — which + * is exactly what such repositories do in practice, and what GIT-08's handler + * reads. Emitting the config without it would leave the rule failing for the + * runtimes that are not TypeScript. + * + * @returns the artifact paths written, relative to the project directory. + */ + async scaffoldCommitConventions(input: InitProjectInput, projectDir: string): Promise { + const artifacts: string[] = []; + + const config = `export default { + extends: ['@commitlint/config-conventional'], + rules: { + // GIT-08 — Conventional Commits: type(scope): description + 'type-enum': [2, 'always', ${JSON.stringify(CONVENTIONAL_COMMIT_TYPES)}], + }, +}; +`; + await this.fs.writeFile(`${projectDir}/commitlint.config.mjs`, config); + artifacts.push('commitlint.config.mjs'); + + const packageJsonPath = `${projectDir}/package.json`; + const packageJson = await this.fs.exists(packageJsonPath) + ? await this.fs.readJson>(packageJsonPath) + : { name: input.name, version: '0.1.0', private: true }; + + packageJson.devDependencies = { + ...(packageJson.devDependencies as Record | undefined), + ...COMMITLINT_DEPENDENCIES, + }; + await this.fs.writeJson(packageJsonPath, packageJson); + artifacts.push('package.json'); + + if (input.features.includes('hooks')) { + await this.fs.ensureDir(`${projectDir}/.husky`); + await this.fs.writeFile( + `${projectDir}/.husky/commit-msg`, + '#!/bin/sh\n' + + '# GIT-08 — reject non-conforming commit messages. `--no-install` makes a\n' + + '# missing commitlint an ERROR rather than a silent skip (GT-623).\n' + + 'npx --no-install commitlint --edit "$1"\n', + ); + artifacts.push('.husky/commit-msg'); + } + + return artifacts; + } + async checkRuntimePlatform(runtime: string): Promise<{ available: boolean; installHint?: string }> { switch (runtime) { case 'nodejs': diff --git a/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts b/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts new file mode 100644 index 000000000..e60f18805 --- /dev/null +++ b/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts @@ -0,0 +1,152 @@ +/** + * GT-595 follow-on — `evolith init` must produce a repository that can pass its + * own governance on the first run. + * + * GIT-08 gained a real handler in the config-shaped slice, and the first thing it + * reported was that the scaffold mandates Conventional Commits and enforces them + * with nothing. These tests assert the three legs GIT-08's handler actually reads + * — config, installed package, and something that runs it — rather than just + * "a file was written". + * + * The sibling half of that finding, MTN-05, is deliberately NOT here: it is not a + * scaffold gap. See `rule-applicability.integration.spec.ts`. + */ + +import { IFileSystem } from '../../domain/interfaces'; +import { InitializeProjectUseCase } from './initialize-project.use-case'; + +const catalogLoader = { + loadRuntimeCatalog: () => [{ id: 'typescript' }, { id: 'python' }, { id: 'dotnet' }], + getMonorepoOptions: () => [{ id: 'nx' }], + getArchitecturePatterns: () => [{ id: 'clean' }], +} as any; + +/** A real in-memory tree: the devDependency merge needs write→read to round-trip. */ +function memoryFs() { + const files = new Map(); + const dirs = new Set(); + const fs = { + files, + dirs, + exists: async (p: string) => files.has(p) || dirs.has(p), + existsSync: (p: string) => files.has(p) || dirs.has(p), + readFile: async (p: string) => { + const content = files.get(p); + if (content === undefined) throw new Error(`ENOENT: ${p}`); + return content; + }, + readJson: async (p: string) => JSON.parse(await fs.readFile(p)), + writeFile: async (p: string, content: string) => { files.set(p, content); }, + writeJson: async (p: string, content: unknown) => { files.set(p, JSON.stringify(content, null, 2)); }, + ensureDir: async (p: string) => { dirs.add(p); }, + readdir: async () => [], + readdirNames: async () => [], + stat: async () => ({ isDirectory: () => false, isFile: () => true }), + remove: async (p: string) => { files.delete(p); }, + }; + return fs as unknown as IFileSystem & typeof fs; +} + +const INPUT = { + name: 'my-sat', + runtime: 'typescript', + monorepo: 'nx', + architecture: 'clean', + database: 'postgres', + apiProtocol: 'rest', + ciCd: 'github-actions', + observability: 'otel', + features: ['adr', 'hooks', 'acl'], + agents: [], +}; + +async function init(overrides: Partial = {}) { + const fs = memoryFs(); + const result = await new InitializeProjectUseCase(fs, catalogLoader).execute( + { ...INPUT, ...overrides } as any, + '/tmp', + ); + expect(result.success).toBe(true); + return { fs, result, root: `/tmp/${(overrides.name ?? INPUT.name)}` }; +} + +describe('InitializeProjectUseCase · GIT-08 — the scaffold enforces what it mandates', () => { + it('writes a commitlint config carrying the commit types GIT-08 names', async () => { + const { fs, root } = await init(); + + const config = fs.files.get(`${root}/commitlint.config.mjs`); + expect(config).toBeDefined(); + expect(config).toContain('@commitlint/config-conventional'); + // GIT-08's own `pattern` enumerates exactly these seven. + for (const type of ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']) { + expect(config).toContain(`"${type}"`); + } + }); + + it('declares the commitlint package, because a config nothing installs cannot run', async () => { + // This is the GT-623 failure mode the handler encodes: a check that is + // present, absent from node_modules, and therefore passes by shrugging. + const { fs, root } = await init(); + + const pkg = JSON.parse(fs.files.get(`${root}/package.json`)!); + expect(pkg.devDependencies['@commitlint/cli']).toBeDefined(); + expect(pkg.devDependencies['@commitlint/config-conventional']).toBeDefined(); + }); + + it('merges into the runtime scaffold\'s devDependencies instead of replacing them', async () => { + const { fs, root } = await init(); + + const pkg = JSON.parse(fs.files.get(`${root}/package.json`)!); + expect(pkg.devDependencies.typescript).toBe('^5.0.0'); + expect(pkg.devDependencies.jest).toBe('^29.0.0'); + expect(pkg.name).toBe('my-sat'); + }); + + it('installs a commit-msg hook that FAILS when commitlint is missing, never skips', async () => { + const { fs, root } = await init(); + + const hook = fs.files.get(`${root}/.husky/commit-msg`); + expect(hook).toBeDefined(); + expect(hook).toContain('commitlint --edit'); + // `--no-install` is the whole point: without it npx would fetch-or-skip and + // the hook would exit zero on a repository with no commitlint at all. + expect(hook).toContain('--no-install'); + expect(hook).not.toMatch(/else/); + }); + + it('reports every artifact it wrote, exactly once', async () => { + const { result } = await init(); + + expect(result.artifacts).toContain('my-sat/commitlint.config.mjs'); + expect(result.artifacts).toContain('my-sat/.husky/commit-msg'); + expect(result.artifacts.filter(a => a === 'my-sat/package.json')).toHaveLength(1); + }); + + it('says so when nothing will run the config, rather than pretending it is enforced', async () => { + const { fs, result, root } = await init({ features: ['adr'] }); + + expect(fs.files.has(`${root}/commitlint.config.mjs`)).toBe(true); + expect(fs.files.has(`${root}/.husky/commit-msg`)).toBe(false); + expect(result.warnings.join('\n')).toMatch(/hooks.*not selected|not selected.*hook/i); + }); + + it('does not warn when the hook is installed', async () => { + const { result } = await init(); + expect(result.warnings.join('\n')).not.toMatch(/commitlint/i); + }); + + it.each(['python', 'dotnet'])( + 'gives a %s satellite the tooling package.json GIT-08 reads', + async (runtime) => { + // commitlint is a Node tool; a polyglot repo that adopts it carries a + // tooling-only package.json. Emitting the config without one would leave + // GIT-08 failing for every runtime that is not Node. + const { fs, root } = await init({ runtime, features: ['adr'] }); + + expect(fs.files.has(`${root}/commitlint.config.mjs`)).toBe(true); + const pkg = JSON.parse(fs.files.get(`${root}/package.json`)!); + expect(pkg.private).toBe(true); + expect(pkg.devDependencies['@commitlint/cli']).toBeDefined(); + }, + ); +}); diff --git a/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts b/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts index bb0fe5cf5..956343c6a 100644 --- a/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts +++ b/src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts @@ -53,6 +53,21 @@ export class InitializeProjectUseCase { await this.projectScaffolder.scaffoldByRuntime(input, projectDir); artifacts.push(`${input.name}/package.json`); + // GIT-08 — after the runtime scaffold, so the commitlint devDependencies + // merge into the package.json that scaffold just wrote rather than racing it. + const commitArtifacts = await this.projectScaffolder.scaffoldCommitConventions(input, projectDir); + for (const artifact of commitArtifacts) { + const qualified = `${input.name}/${artifact}`; + if (!artifacts.includes(qualified)) artifacts.push(qualified); + } + if (!input.features.includes('hooks')) { + warnings.push( + 'Conventional Commits are configured (commitlint.config.mjs) but nothing runs them: ' + + 'the `hooks` feature was not selected, so no commit-msg hook was installed. ' + + 'Wire commitlint into CI or re-run with --features hooks.', + ); + } + if (input.features.includes('adr')) { await this.fs.ensureDir(`${projectDir}/reference/architecture/adrs`); await this.fs.writeJson(`${projectDir}/reference/architecture/adrs/adr-matrix.json`, { adrs: [] }); diff --git a/src/packages/infra-providers/src/rule-applicability.integration.spec.ts b/src/packages/infra-providers/src/rule-applicability.integration.spec.ts index 27f9ee1c1..dd05682e8 100644 --- a/src/packages/infra-providers/src/rule-applicability.integration.spec.ts +++ b/src/packages/infra-providers/src/rule-applicability.integration.spec.ts @@ -30,6 +30,7 @@ import { RulesetValidatorService, partitionByApplicability, } from '@beyondnet/evolith-core-domain/application/validators'; +import type { ApplicabilityContext } from '@beyondnet/evolith-core-domain/application/validators'; import { InitializeProjectUseCase } from '@beyondnet/evolith-core-domain/application/use-cases'; import type { NormalizedRule } from '@beyondnet/evolith-core-domain/domain/models/normalized-rule'; @@ -128,21 +129,32 @@ describe('GT-571 · the first validate of a freshly initialized satellite', () = const blockingIds = new Set(blocking.map(i => i.ruleId)); for (const id of result.blockingSkippedRuleIds!) expect(blockingIds.has(id)).toBe(true); - // …and the ONLY blocking findings that are not the GT-595 invariant are - // these two, which are REAL verdicts from rules that really executed. + // …and NO blocking finding is a real verdict: every one of them is the + // GT-595 invariant firing on a rule the corpus itself declares + // blocking-and-unrunnable. A freshly scaffolded satellite does nothing wrong. // - // Both were closed by the GT-595 config-shaped slice, and both fail on a - // freshly scaffolded satellite because the scaffold does not emit what they - // require. That is a live defect in `InitializeProjectUseCase`, not a - // testing artifact, and it is pinned here rather than filtered out so that - // closing it moves this list to []: - // MTN-05 the scaffolded evolith.yaml declares no `spec.boundedContexts`, - // so the multi-tenant persistence decision is undeclared; - // GIT-08 the scaffold writes no commitlint configuration, so Conventional - // Commits are mandated by the corpus and enforced by nothing. + // This list used to read ['GIT-08', 'MTN-05'] and was pinned by name so that + // closing the gap would show up here. Both were closed, by different means, + // because they are different kinds of defect: + // + // GIT-08 was a SCAFFOLD gap. Conventional Commits bind from the first + // commit and the corpus already states the convention verbatim, so + // `init` now emits commitlint.config.mjs, the commitlint packages + // in devDependencies, and (with --features hooks) a commit-msg hook + // that fails rather than skips when the tool is absent. + // + // MTN-05 was NOT a scaffold gap, and emitting a `spec.boundedContexts` + // stanza would have been the wrong fix: the rule's own text says + // the strategy MUST be defined "before Phase 2 Design", and a + // phase-0 scaffold has no bounded contexts to declare. Satisfying + // it at init would have meant writing an invented persistence + // decision into every new repository — and then MTN-05 would pass + // for a reason nobody chose. It is annotated + // `appliesFromSdlcPhase: 2` instead, which is the applicability + // fact the rule always stated in prose. const skippedInvariant = new Set(result.blockingSkippedRuleIds!); const realViolations = blocking.filter(i => !skippedInvariant.has(i.ruleId)); - expect(realViolations.map(i => i.ruleId).sort()).toEqual(['GIT-08', 'MTN-05']); + expect(realViolations.map(i => i.ruleId).sort()).toEqual([]); }); it('never fires a blocking rule that applicability excluded', async () => { @@ -264,4 +276,29 @@ describe('GT-571 · the Core monorepo keeps its own rules', () => { expect(index.get('TAX-01')?.topologies).toBeUndefined(); expect(index.get('TAX-01')?.appliesFromSdlcPhase).toBeUndefined(); }); + + it('defers MTN-05 to Design without weakening it', async () => { + // MTN-05's own description says the multi-tenant schema strategy "MUST be + // defined before Phase 2 Design". That was prose; it is now an applicability + // fact, which is why a phase-0 scaffold with no bounded contexts is no longer + // failed by it. The half that matters is the second assertion: the rule is + // DEFERRED, not disabled — a repository that has reached Design is still + // judged by it, and would be failed for exactly the same reason as before. + const fs = new NodeFileSystemProvider() as any; + const rules = await new DiskRulesetRepository(fs, silentLogger).loadAllRulesets(CORE); + const index = await RuleApplicabilityIndex.load(fs, CORE, path.sep); + + expect(index.get('MTN-05')?.appliesFromSdlcPhase).toBe(2); + + const excludedAt = (sdlcPhase: number) => { + const ctx: ApplicabilityContext = { audience: 'satellite', declaredTopologies: [], sdlcPhase }; + const { notApplicable } = partitionByApplicability(rules as NormalizedRule[], { index, context: ctx }); + return new Set(notApplicable.map(n => n.rule.id)).has('MTN-05'); + }; + + expect(excludedAt(0)).toBe(true); // the freshly scaffolded satellite + expect(excludedAt(1)).toBe(true); // Conception — still nothing to declare + expect(excludedAt(2)).toBe(false); // Design — the rule binds, unchanged + expect(excludedAt(3)).toBe(false); + }); }); \ No newline at end of file diff --git a/src/rulesets/adr/adr-0010-multi-tenancy.rules.json b/src/rulesets/adr/adr-0010-multi-tenancy.rules.json index e4d641ac1..7e1f16678 100644 --- a/src/rulesets/adr/adr-0010-multi-tenancy.rules.json +++ b/src/rulesets/adr/adr-0010-multi-tenancy.rules.json @@ -56,6 +56,7 @@ }, { "id": "MTN-05", + "appliesFromSdlcPhase": 2, "severity": "MUST", "category": "schema-per-tenant", "title": "Multi-tenant schema strategy defined upfront", From c8270e48e51b728889c454ee8b0ed4a3cb9cadb3 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:26:33 -0500 Subject: [PATCH 02/22] docs(board): register GT-633 and close it on the scaffold fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The init-scaffold defect that GT-595 exposed now has a row of its own rather than living only in a pinned test assertion. Registered as GT-633 (P1/S, `Core Domain`) and closed on 05c3f62b. The row records the part that is not obvious from the diff: the two halves were closed by OPPOSITE means. GIT-08 was a scaffold gap and nothing else, so `init` now emits the config, the package and the hook. MTN-05 was not — its own text defers it to Phase 2 Design, and satisfying it at init would have written an invented persistence decision into every new repository, so it is annotated `appliesFromSdlcPhase: 2` instead. A board that recorded only "both fixed" would lose exactly the distinction worth keeping. Also recorded, because it nearly produced a false CLOSURE: the premise did not reproduce on first contact. The suite resolves core-domain through the workspace symlink to a `dist/` that was nine days stale and predated the GT-595 handlers, so both rules reported `skipped` and the pinned assertion already read `[]`. Same untracked-build-state class as GT-625 and GT-632, but inverted — here the stale artifact would have "proved" the gap closed before a line was written. Counters recomputed rather than incremented by hand: 633 gaps EN/ES, 609/609 catalog sections, 575 closure records. The executive summary and maturity reconciliation were regenerated, and their diff is exactly the three +1 deltas, which is what shows nothing else drifted. Verified: 08-validate-tracking, 05-validate-executive-summary, 09-reconcile-maturity --check, 01-validate-docs (1491 files) and 04-check-bilingual-parity all exit 0. Co-Authored-By: Claude Opus 5 --- .../evidence/gap-closure-evidence.json | 26 +++++++++++++++++++ .../gaps/gap-reference-catalog.es.md | 16 ++++++++++++ .../gaps/gap-reference-catalog.md | 16 ++++++++++++ .../control-center/gaps/gap-tracking.es.md | 3 ++- .../core/control-center/gaps/gap-tracking.md | 3 ++- .../maturity-reports/executive-summary.es.md | 6 ++--- .../maturity-reports/executive-summary.md | 6 ++--- .../maturity-reconciliation.json | 6 ++--- 8 files changed, 71 insertions(+), 11 deletions(-) diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index dd0560ead..0e25c4b44 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -8828,6 +8828,32 @@ ], "dependencyDisposition": "accepted-scope", "dependencyRationale": "TWO deliberate non-adoptions are recorded as part of the closure, not deferred silently. (1) dependency-cruiser was NOT adopted for the module-boundary rules: the Core is a stateless engine that reads satellites through an overlay filesystem a subprocess cannot see, and the tool's config matches node_modules/, so an UNINSTALLED satellite would yield zero violations and PASS four blocking rules. A false pass is worse than the skip it would replace. (2) OCB-02 was NOT closed: no artifact anywhere carries an `availability` marker, so the rule quantifies over the empty set and can only pass; that is recorded as three executable tests instead of prose. The residual unclaimed-blocking corpus stands at 73 and is pinned." + }, + { + "id": "GT-633", + "closedAt": "2026-07-29", + "closureCommit": "05c3f62b", + "evidence": [ + "src/packages/core-domain/src/application/services/project-scaffolder.service.ts", + "src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts", + "src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts", + "src/packages/infra-providers/src/rule-applicability.integration.spec.ts", + "src/rulesets/adr/adr-0010-multi-tenancy.rules.json", + "src/packages/core-domain/src/application/validators/evaluators/handlers/governance-rule.handler.ts", + "src/packages/core-domain/src/application/validators/rule-applicability.ts" + ], + "validationCommands": [ + "rule-applicability.integration.spec.ts -> 10/10. The pinned assertion moved from ['GIT-08','MTN-05'] to [] because the scaffold changed, not because the assertion was relaxed.", + "initialize-project.use-case.spec.ts -> 9/9, covering all three legs GIT-08 reads: config content (the seven types GIT-08 names), the merged devDependencies, and a commit-msg hook asserted to carry --no-install and to contain no else branch.", + "The deferral of MTN-05 is asserted over phases 0->3: excluded at 0 and 1, binding at 2 and 3. Weakening the annotation turns the test red.", + "Verified per-rule rather than by the suite alone: GIT-08 executes and PASSES for the typescript and python scaffolds; MTN-05 appears in notApplicableRuleIds rather than passing silently.", + "core-domain 1442/1442, infra-providers 129/129, sdk/cli 1436/1436, mcp-server init.tools 7/7.", + "32-validate-ruleset-schemas -> 168 passed, 0 failed, exit 0. 28-native-evaluator-parity -> 17 fixtures, 451 verdict assertions, exit 0. 01-validate-docs -> 1491 files, exit 0.", + "The premise did NOT reproduce on first contact: the suite resolves @beyondnet/evolith-core-domain through the workspace symlink to dist/, nine days stale and predating the GT-595 handlers, so both rules were skipped and the pinned assertion already read []. A green run in that state would have produced a false CLOSURE. Reproduced only after re-anchoring node_modules at the worktree and rebuilding -- the same untracked-build-state class as GT-625 and GT-632.", + "NOT done, deliberately: multi-tenancy.rego still asserts MTN-05 unconditionally. Applicability is a native-engine pre-filter by construction (GT-571) and TAX-06 already carries the same shape, so this is a pre-existing engine divergence rather than one introduced here." + ], + "dependencyDisposition": "accepted-scope", + "dependencyRationale": "The prerequisite is satisfied and closed: GT-595 gave MTN-05 and GIT-08 real handlers, which is the only reason either failure was visible. One part is deliberately left out of scope: the parallel multi-tenancy.rego still asserts MTN-05 unconditionally. Applicability is a native-engine pre-filter by construction (GT-571) and TAX-06 already carries the same shape -- a phase annotation alongside a rego that does not read it -- so this is a pre-existing divergence between the two engines, not one introduced here, and closing it belongs to whatever unifies applicability across engines." } ] } 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 3632e23b7..a317625bd 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -7855,6 +7855,22 @@ Serie histórica de gaps registrada en el antiguo `gap-analysis-core.es.md`, pre - [x] El tipo `security` está declarado en la config de commitlint con un mapeo explícito de salto de versión, o se deja de usar. - [x] Ningún hook de `.husky/` imprime un mensaje de "skipping" y sale con cero — un hook que no puede correr se borra, no se silencia. +#### GT-633 + +**Title:** `evolith init` scaffoldea un repositorio que no puede pasar su propia gobernanza en la primera corrida + +- **Purpose:** Que el comando de arranque del propio producto genere un repositorio que el producto acepta — y que, allí donde una regla genuinamente no se dirige a un scaffold greenfield, eso se diga en las anotaciones de aplicabilidad en vez de obligar al scaffold a fabricar una respuesta. +- **Evidence:** **`evolith init` producía un repositorio que suspende su propio primer `evolith validate`, y solo los handlers de GT-595 lo hicieron visible.** Con `MTN-05` y `GIT-08` dotados de handler real por la ola de reglas con forma de configuración, un satélite recién scaffoldeado reportaba dos hallazgos bloqueantes que eran veredictos REALES, no el invariante bloqueante-y-saltada de GT-595: el scaffold no declaraba `spec.boundedContexts`, y no escribía configuración de commitlint en absoluto. La segunda es la más afilada — el corpus EXIGE Conventional Commits (`GIT-08`, `MUST`, `blocking`) y el propio comando de arranque del producto entregaba un repositorio donde nada los hacía cumplir. Ambas estaban fijadas por nombre en `rule-applicability.integration.spec.ts` (`expect(realViolations.map(i => i.ruleId).sort()).toEqual(['GIT-08', 'MTN-05'])`) justamente para que cerrarlas llevara esa lista a `[]`. +- **Component:** `Core Domain` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Encontrado el 2026-07-29 como continuación directa de `GT-595`, que cerró ambas reglas de verdad y fijó los dos fallos que destapó en vez de filtrarlos. +- **Acceptance criteria:** + - [x] Un satélite recién inicializado no reporta NINGÚN hallazgo bloqueante que sea un veredicto real — todos los restantes son el invariante bloqueante-y-saltada de `GT-595`, y el array fijado dice `[]`. + - [x] `GIT-08` pasa porque el scaffold emite las tres patas que lee su handler — configuración, paquete declarado y algo que lo ejecuta — no porque la regla haya dejado de aplicar. + - [x] `MTN-05` queda DIFERIDA, no desactivada: excluida en las fases 0 y 1, y obligando de nuevo desde Diseño (fase 2), afirmado por test. + - [x] Cada regla verificada individualmente contra un `dist` recién construido, no solo a través de la suite. + +**Evidencia de cierre:** Las dos mitades se cerraron por medios OPUESTOS, porque no son el mismo tipo de defecto. **`GIT-08` era un hueco del scaffold y nada más.** Conventional Commits obliga desde el primer commit y el corpus ya enuncia la convención y sus siete tipos literalmente, así que no había nada que decidir ni nada que inventar. `ProjectScaffolderService.scaffoldCommitConventions` emite ahora `commitlint.config.mjs`, fusiona `@commitlint/cli` y `@commitlint/config-conventional` en `devDependencies` y — con `--features hooks` — escribe un `.husky/commit-msg` que ejecuta `npx --no-install commitlint --edit "$1"`. El `--no-install` es justamente el punto: convierte la ausencia de commitlint en un ERROR en lugar del salto silencioso que encontró `GT-623`, donde la rama `else` de un hook imprimía "commitlint is not installed — skipping" y salía con cero. Cuando `hooks` no se selecciona la configuración se escribe igual y el resultado lleva un warning diciendo que nada la ejecuta, en vez de dejar un fichero que parece enforcement sin serlo. Es independiente del runtime a propósito: commitlint es una herramienta Node, así que un satélite .NET o Python recibe un `package.json` solo de tooling — lo que esos repositorios llevan en la práctica, y lo que el handler realmente lee; emitir solo la configuración habría dejado la regla suspendiendo para todo runtime que no sea Node. **`MTN-05` NO era un hueco del scaffold, y emitir un bloque `spec.boundedContexts` habría sido el arreglo equivocado.** La propia descripción de la regla dice que la estrategia DEBE definirse "antes de la Fase 2 Diseño", y un scaffold en fase-0 no tiene contextos acotados que declarar; satisfacerla en el `init` habría supuesto escribir una decisión de persistencia INVENTADA en cada repositorio nuevo, tras lo cual `MTN-05` pasaría por un motivo que nadie eligió. Peor: según la nota del propio handler los dos vocabularios son irreconciliables — `evolith-yaml.schema.json` restringe `persistence` a un enum de motor de base de datos (`PostgreSQL | MongoDB | SQLServer | SQLite`) bajo `additionalProperties: false`, mientras que `MTN-05` nombra tres ESTRATEGIAS de multi-tenencia — así que cualquier valor escrito o rompe el esquema del contrato o no puede casar con la regla tal como está redactada. Lleva `appliesFromSdlcPhase: 2` en su lugar, que es el hecho de aplicabilidad que la regla siempre enunció en prosa, y sigue el precedente de `TAX-06` (anotación de fase junto a un rego paralelo). El diferimiento se afirma, no se supone: un test recorre las fases 0→3 y exige que la regla esté excluida en 0 y 1 y vuelva a obligar en 2 y 3, de modo que debilitar la anotación en el futuro lo pone en rojo. **Un hallazgo metodológico que vale más que el arreglo.** La premisa no se reprodujo al primer contacto. La suite resuelve `@beyondnet/evolith-core-domain` por el symlink de npm workspaces hacia `dist/`, que llevaba nueve días obsoleto y era anterior a los handlers de GT-595, así que ambas reglas se reportaban `skipped` y la aserción fijada YA decía `[]`. Una corrida verde en ese estado habría "demostrado" el gap cerrado antes de escribir una línea — la misma clase que `GT-625` y `GT-632`, un pase que depende de estado de build sin versionar, y aquí habría producido un CIERRE falso en vez de un fallo falso. Solo se reprodujo tras reanclar `node_modules` en el worktree (los enlaces del árbol compartido apuntan al checkout principal, así que un worktree prueba en silencio las fuentes de otro) y reconstruir. Entonces cada regla se verificó individualmente y no a través de la suite: `GIT-08` se ejecuta y PASA para los scaffolds typescript y python; `MTN-05` aparece en `notApplicableRuleIds` en vez de pasar en silencio. Verificado: core-domain 1442/1442, infra-providers 129/129, sdk/cli 1436/1436, `32-validate-ruleset-schemas` 168/168, `28-native-evaluator-parity` 451 aserciones, `01-validate-docs` 1491 ficheros. **No hecho y deliberadamente:** el `multi-tenancy.rego` paralelo sigue afirmando `MTN-05` sin condición. La aplicabilidad es un pre-filtro del motor nativo por construcción (`GT-571`), y `TAX-06` ya tiene la misma forma, así que es una divergencia de motores preexistente y no una introducida aquí. + #### GT-632 **Title:** La política ABAC compilada se resuelve en una ruta previa al refactor, denegando toda herramienta MCP en producción diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 61aa87352..d670b7c89 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -7950,6 +7950,22 @@ Historical gap series tracked in the former `gap-analysis-core.md`, preserved fo - [x] The `security` type is either declared in the commitlint config with an explicit version-bump mapping, or removed from use. - [x] No hook in `.husky/` prints a "skipping" message and exits zero — a hook that cannot run is deleted, not silenced. +#### GT-633 + +**Title:** `evolith init` scaffolds a repository that cannot pass its own governance on the first run + +- **Purpose:** Make the product's own bootstrap command produce a repository the product itself accepts — and, where a rule genuinely does not address a greenfield scaffold, say so in the applicability annotations instead of forcing the scaffold to fabricate an answer. +- **Evidence:** **`evolith init` produced a repository that fails its own first `evolith validate`, and only the GT-595 handlers made it visible.** With `MTN-05` and `GIT-08` given real handlers by the config-shaped slice, a freshly scaffolded satellite reported two blocking findings that were REAL verdicts rather than the GT-595 blocking-and-skipped invariant: the scaffold declared no `spec.boundedContexts`, and it wrote no commitlint configuration at all. The second is the sharper one — the corpus MANDATES Conventional Commits (`GIT-08`, `MUST`, `blocking`) and the product's own bootstrap command shipped a repository where nothing enforced them. Both were pinned by name in `rule-applicability.integration.spec.ts` (`expect(realViolations.map(i => i.ruleId).sort()).toEqual(['GIT-08', 'MTN-05'])`) precisely so that closing them would move that list to `[]`. +- **Component:** `Core Domain` · **Criticality:** P1 · **Complexity:** S +- **Provenance:** Found on 2026-07-29 as the direct follow-on to `GT-595`, which closed both rules for real and pinned the two failures it exposed rather than filtering them out. +- **Acceptance criteria:** + - [x] A freshly initialized satellite reports NO blocking finding that is a real verdict — every remaining one is the `GT-595` blocking-and-skipped invariant, and the pinned array reads `[]`. + - [x] `GIT-08` passes because the scaffold emits all three legs its handler reads — configuration, declared package, and something that runs it — not because the rule stopped applying. + - [x] `MTN-05` is DEFERRED rather than disabled: excluded at phases 0 and 1, still binding from Design (phase 2) onward, asserted by test. + - [x] Each rule verified individually against a freshly built `dist`, not through the suite alone. + +**Closure evidence:** The two halves were closed by OPPOSITE means, because they are not the same kind of defect. **`GIT-08` was a scaffold gap and nothing else.** Conventional Commits bind from the first commit and the corpus already states the convention and its seven types verbatim, so there was nothing to decide and nothing to invent. `ProjectScaffolderService.scaffoldCommitConventions` now emits `commitlint.config.mjs`, merges `@commitlint/cli` and `@commitlint/config-conventional` into `devDependencies`, and — with `--features hooks` — writes a `.husky/commit-msg` running `npx --no-install commitlint --edit "$1"`. The `--no-install` is the whole point: it makes a missing commitlint an ERROR rather than the silent skip `GT-623` found, where a hook's `else` branch printed "commitlint is not installed — skipping" and exited zero. When `hooks` is not selected the config is still written and the result carries a warning saying nothing runs it, rather than leaving a file that looks like enforcement and is not. It is runtime-independent on purpose: commitlint is a Node tool, so a .NET or Python satellite receives a tooling-only `package.json` — what such repositories carry in practice, and what the handler actually reads; emitting the config alone would have left the rule failing for every non-Node runtime. **`MTN-05` was NOT a scaffold gap, and emitting a `spec.boundedContexts` stanza would have been the wrong fix.** The rule's own description says the strategy MUST be defined "before Phase 2 Design", and a phase-0 scaffold has no bounded contexts to declare; satisfying it at `init` would have meant writing an INVENTED persistence decision into every new repository, after which `MTN-05` would pass for a reason nobody chose. Worse, per the handler's own note the two vocabularies are irreconcilable — `evolith-yaml.schema.json` restricts `persistence` to a database-engine enum (`PostgreSQL | MongoDB | SQLServer | SQLite`) under `additionalProperties: false`, while `MTN-05` names three multi-tenancy STRATEGIES — so any value written either breaks the contract schema or cannot match the rule as worded. It carries `appliesFromSdlcPhase: 2` instead, which is the applicability fact the rule always stated in prose, and follows the `TAX-06` precedent (a phase annotation alongside a parallel rego). The deferral is asserted, not assumed: a test walks phases 0→3 and requires the rule to be excluded at 0 and 1 and to bind again at 2 and 3, so a future weakening of the annotation turns it red. **A methodological finding worth more than the fix.** The premise did not reproduce on first contact. The suite resolves `@beyondnet/evolith-core-domain` through the npm-workspace symlink to `dist/`, which was nine days stale and predated the GT-595 handlers, so both rules were reported `skipped` and the pinned assertion ALREADY read `[]`. A green run in that state would have "proved" the gap closed before a line was written — the same class as `GT-625` and `GT-632`, a pass that depends on untracked build state, and here it would have produced a false CLOSURE rather than a false failure. It was reproduced only after re-anchoring `node_modules` at the worktree (the shared tree's links point at the main checkout, so a worktree silently tests somebody else's sources) and rebuilding. Each rule was then verified individually rather than through the suite: `GIT-08` executes and PASSES for both the typescript and python scaffolds; `MTN-05` appears in `notApplicableRuleIds` rather than passing silently. Verified: core-domain 1442/1442, infra-providers 129/129, sdk/cli 1436/1436, `32-validate-ruleset-schemas` 168/168, `28-native-evaluator-parity` 451 assertions, `01-validate-docs` 1491 files. **Not done and deliberately so:** the parallel `multi-tenancy.rego` still asserts `MTN-05` unconditionally. Applicability is a native-engine pre-filter by construction (`GT-571`), and `TAX-06` already carries the same shape, so this is a pre-existing engine divergence rather than one introduced here. + #### GT-632 **Title:** The compiled ABAC policy is resolved at a pre-refactor path, denying every MCP tool in production diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 630ff9921..453242f86 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -13,6 +13,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | ID | Gap | Qué significa | Ejemplo | Componente | Fase | Criticidad | Complejidad | Estado | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-633`](./gap-reference-catalog.es.md#gt-633) | **`evolith init` producía un repositorio que suspende su propio primer `evolith validate`, y solo los handlers de GT-595 lo hicieron visible.** Con `MTN-05` y `GIT-08` dotados de handler real por la ola de reglas con forma de configuración, un satélite recién scaffoldeado reportaba dos hallazgos bloqueantes que eran veredictos REALES, no el invariante bloqueante-y-saltada de GT-595: el scaffold no declaraba `spec.boundedContexts`, y no escribía configuración de commitlint en absoluto. La segunda es la más afilada — el corpus EXIGE Conventional Commits (`GIT-08`, `MUST`, `blocking`) y el propio comando de arranque del producto entregaba un repositorio donde nada los hacía cumplir. Ambas estaban fijadas por nombre en `rule-applicability.integration.spec.ts` justamente para que cerrarlas se viera. **CERRADO el 2026-07-29, y las dos mitades se cerraron por medios OPUESTOS, porque no son el mismo tipo de defecto.** `GIT-08` era un hueco del scaffold y nada más: Conventional Commits obliga desde el primer commit y el corpus ya enuncia la convención y sus siete tipos literalmente, así que no había nada que decidir ni nada que inventar. `init` emite ahora las tres patas que lee el handler — `commitlint.config.mjs`, los paquetes de commitlint en `devDependencies` y (con `--features hooks`) un `.husky/commit-msg` que ejecuta `npx --no-install commitlint`, de modo que la ausencia de la herramienta es un ERROR y no el salto silencioso que encontró `GT-623`. Sin `hooks` la configuración se escribe igual y el resultado lleva un warning diciendo que nada la ejecuta, en vez de dejar un fichero que parece enforcement sin serlo. Es independiente del runtime a propósito: commitlint es una herramienta Node, así que un satélite .NET o Python recibe un `package.json` solo de tooling — que es lo que esos repositorios llevan en la práctica y lo que el handler realmente lee; emitir solo la configuración habría dejado la regla suspendiendo para todo runtime que no sea Node. **`MTN-05` NO era un hueco del scaffold, y emitir un bloque `spec.boundedContexts` habría sido el arreglo equivocado.** La propia descripción de la regla dice que la estrategia DEBE definirse "antes de la Fase 2 Diseño", y un scaffold en fase-0 no tiene contextos acotados que declarar; satisfacerla en el `init` habría supuesto escribir una decisión de persistencia INVENTADA en cada repositorio nuevo, tras lo cual `MTN-05` pasaría por un motivo que nadie eligió. Peor: según la nota del propio handler los dos vocabularios son irreconciliables — `evolith-yaml.schema.json` restringe `persistence` a un enum de motor de base de datos bajo `additionalProperties: false` — así que cualquier valor escrito o rompe el esquema del contrato o no puede casar con la regla tal como está redactada. Lleva `appliesFromSdlcPhase: 2` en su lugar, que es el hecho de aplicabilidad que la regla siempre enunció en prosa, y sigue el precedente de `TAX-06` (anotación más un rego paralelo). La regla queda DIFERIDA, no debilitada, y eso se afirma en vez de suponerse: un test recorre las fases 0→3 y exige que vuelva a obligar en Diseño. **Un hallazgo metodológico que vale más que el arreglo.** La premisa no se reprodujo al primer contacto: la suite resuelve `@beyondnet/evolith-core-domain` por el symlink del workspace hacia `dist/`, que llevaba nueve días obsoleto, así que ambas reglas se reportaban `skipped` y la aserción fijada YA decía `[]`. Una corrida verde en ese estado habría "demostrado" el gap cerrado antes de escribir una línea — la misma clase que `GT-625` y `GT-632`, un pase que depende de estado de build sin versionar. Solo se reprodujo tras reanclar `node_modules` en el worktree y reconstruir, y entonces cada regla se verificó individualmente y no a través de la suite: `GIT-08` se ejecuta y PASA (typescript y python), `MTN-05` se reporta `notApplicable` en vez de pasar en silencio. | | | `Core Domain` | Cross | P1 | S | `COMPLETADO` | | [`GT-632`](./gap-reference-catalog.es.md#gt-632) | **El MCP denegaba todas sus herramientas en producción por una ruta mal escrita, y la prueba de que funcionaba era estado local sin versionar.** `abac-evaluator.ts:322` resolvía la política compilada en `/sdk/cli/rulesets/opa/policy.wasm` — la ruta ANTERIOR al refactor `src/`. El fichero vive en `src/sdk/cli/…`. Cuando el stat falla, el evaluador deniega fail-closed en producción, así que en un checkout limpio con `NODE_ENV=production` la política compilada nunca se carga y TODA herramienta MCP se rechaza. Parecía verde porque `policy.wasm` está gitignorado: un fichero rancio dejado en la ruta vieja por una época anterior hacía que la carga funcionara en local, y el test que lo cubre pasaba contra un artefacto que ningún checkout fresco tendría. Misma forma que `GT-625` — un verde que dependía de estado no versionado — pero en el camino de autorización. Encontrado el 2026-07-29 investigando por qué `abac-rego-parity.spec.ts` se había puesto rojo; el rego era correcto (`opa eval` devuelve `allow: true` para un arquitecto en producción). **`40-validate-path-literals` no puede ver esta clase:** barre literales de cadena, y esto es una construcción `path.join(corePath, 'sdk', 'cli', …)`. Sobreviven doce joins así en `src/**` — los otros once están en `opa-input-builder.ts` y `cli-release-rule.handler.ts`, donde una ruta equivocada produce un falso NEGATIVO en un evaluador en vez de una denegación, que es más silencioso y no menos incorrecto. **CORREGIDO para el camino de autorización el 2026-07-29:** el evaluador prueba ahora ambas ubicaciones, la nueva primero — no es dejadez, es lo que mantiene funcionando una imagen ya desplegada mientras se corrige la ruta. Revertir el arreglo pone `abac-rego-parity.spec.ts` en rojo (1 de 11); con él, mcp-server va 434/434. Quedan los otros once joins y el punto ciego del guard. **Criterio 2 CERRADO el 2026-07-29 — y dos de las once estaban mal de una forma que el prefijo por sí solo no arreglaba.** Ocho eran supervivientes simples del movimiento a `src/` y admitieron el prefijo. Las otras tres eran peores: dos apuntaban a `sdk/cli/src/core/mcp/server.ts`, un fichero que salió del CLI por completo cuando mcp-server pasó a ser su propio paquete (hoy `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); y una exigía un `package-lock.json` por paquete, cosa que los WORKSPACES de npm impiden — hay exactamente un lockfile, en la raíz, así que esa comprobación solo podía ser falsa. Una regla que nadie puede satisfacer no es una regla más estricta. **Las specs y las fixtures de paridad también codificaban el layout viejo** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), y por eso llevaban tiempo en verde sobre rutas que no existen: la fixture y el código coincidían entre sí y ninguno coincidía con el repositorio. Ambas corregidas. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterio 3 CERRADO el 2026-07-29 — la primera corrida del guard estuvo casi toda equivocada, que es el hallazgo.** `47-validate-joined-paths` reportó 19 rutas rotas; solo 3 eran defectos. Cada una de las otras 16 habría empeorado el código: seis tenían base `root`, un PARÁMETRO que nombra el workspace del cliente, así que corregirlas rompería la evaluación de todos los satélites; nueve eran miembros de CADENAS DE FALLBACK correctas cuando uno de cuatro layouts resuelve, y la forma natural de callarlas es borrar los fallbacks, rompiendo imágenes desplegadas; una era una PROHIBICIÓN cuya ausencia ES el estado que aprueba, así que corregirla habría invertido la regla. Las cadenas se detectan ahora estructuralmente (hermanos de array, o las ramas de un ternario); las prohibiciones no pueden, y llevan razón escrita. Los tres defectos reales: `opa-evaluator.ts` resolvía `policy.wasm` y los esquemas de entrada sin el prefijo `src/`, misma clase fail-closed que el P0 de arriba; `satellite-upgrade-diff.ts` sondeaba `rulesets` detrás de un return temprano, reportando cero cambios de rulesets en cada upgrade de satélite; `artifact-path-resolver.ts` nombraba un `adr-matrix.json` que no existe en ningún layout. 11 fixtures negativas, los dos suelos anti-vacuos, y `43-validate-guard-negative-fixtures` lo OBSERVÓ en rojo sobre 36 guards. Una indulgencia conocida (dos joins como argumentos hermanos) queda anotada en su cabecera en vez de escondida. | | | `MCP Server` | Cross | P0 | M | `COMPLETADO` | | [`GT-631`](./gap-reference-catalog.es.md#gt-631) | **Extraído de [`GT-573`](./gap-reference-catalog.es.md#gt-573) para que su mitad cross-repo se siga en vez de absorberse en un cierre.** La mitad del Core del tercer criterio de GT-573 está hecha: `evaluation-context` y `evaluation-result` están fijados en `MACHINE_CONTRACT_SET` junto a `gate-evidence` y `output-envelope`, así que un cambio en cualquiera de esas formas ya rompe `10-validate-contract-conformance` en vez de llegar en silencio a los consumidores. Lo que queda está en otro repositorio y no se puede hacer desde aquí: el **Tracker** debe re-fijar esos dos esquemas en su propio `contracts/evolith-core-contracts.json` con su sha256, enlazar las tres fixtures publicadas (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) en un test de binding de DTOs, quitar o documentar `resolvedTopology` (que el resultado canónico nunca llevó), renombrar su `phase` de gate a `phaseId`, y eliminar su caída a `SKIPPED` para un payload que trae un `overallVerdict` no vacío. Hasta entonces, un cambio de forma del lado Core se caza aquí y sigue sorprendiendo al consumidor. | | | `Evolith Tracker` | Cross | P1 | S | `PENDIENTE` | | [`GT-630`](./gap-reference-catalog.es.md#gt-630) | **Los artefactos derivados tienen un ORDEN de dependencia y nada lo exigía — costó tres checks requeridos en rojo solo el 2026-07-28.** `generate-executive-summary.mjs` lee `maturity-reconciliation.json`, que a su vez se deriva del tablero de gaps. Generar el resumen ANTES de reconciliar captura un valor que el reconciliador está a punto de mover. El `--check` de cada artefacto pasa en ese momento — el resumen coincide de verdad con lo que se acaba de escribir — así que `ci-runner governance` sale verde en local y `Validate documentation` sale rojo en el runner, donde los pasos corren en el orden declarado. No es un fallo de artefacto rancio, que los `--check` individuales ya cazan; es un fallo de ORDEN, invisible para cualquier comprobación que mire un artefacto cada vez. La secuencia correcta (reconciliar → suite → generar → validar) no estaba escrita en ningún sitio del repositorio, que es la razón de que el mismo error se cometiera tres veces en una sesión por alguien a quien ya le había mordido dos. **CERRADO al registrarse.** La cadena es ahora DATOS en `46-validate-derived-artifact-order.mjs` — productor, qué consume, qué escribe — recorrida en orden de dependencia y cableada en `Validate documentation`. Informa del PRIMER eslabón rancio con el comando que lo arregla y las entradas que deben estar al día antes que él, porque un artefacto construido sobre entrada rancia no está mal por sí mismo. Escribir los tests acotó cuánto vale la pasada de punto fijo, y ese límite queda recogido en el guard: con generadores deterministas, la comprobación de orden ya la subsume; se gana su sitio ante un generador cuya salida no depende solo de sus entradas declaradas, que es el caso que ejercita el autotest. 8 autotests, cuatro de ellos anti-vacuos. Registrado en `guard-classification.mjs`; 58 guards clasificados, 35 vistos fallar. | | | `Governance` | Cross | P2 | S | `COMPLETADO` | @@ -647,7 +648,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-246`](./gap-reference-catalog.es.md#gt-246) | Implementar experimentos Chaos Mesh/Litmus | | | `QA` | Cross | P3 | L | `COMPLETADO` | -**Progreso:** 592 / 632 completados · 14 en progreso · 22 pendientes · 4 diferidos +**Progreso:** 593 / 633 completados · 14 en progreso · 22 pendientes · 4 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 52c7b9a4c..426c575d6 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -13,6 +13,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | ID | Gap | What it means | Example | Component | Phase | Criticality | Complexity | Status | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-633`](./gap-reference-catalog.md#gt-633) | **`evolith init` produced a repository that fails its own first `evolith validate`, and only the GT-595 handlers made it visible.** With `MTN-05` and `GIT-08` given real handlers by the config-shaped slice, a freshly scaffolded satellite reported two blocking findings that were REAL verdicts rather than the GT-595 blocking-and-skipped invariant: the scaffold declared no `spec.boundedContexts`, and it wrote no commitlint configuration at all. The second is the sharper one — the corpus MANDATES Conventional Commits (`GIT-08`, `MUST`, `blocking`) and the product's own bootstrap command shipped a repository where nothing enforced them. Both were pinned by name in `rule-applicability.integration.spec.ts` precisely so that closing them would be visible. **CLOSED 2026-07-29, and the two halves were closed by OPPOSITE means, because they are not the same kind of defect.** `GIT-08` was a scaffold gap and nothing else: Conventional Commits bind from the first commit and the corpus already states the convention and its seven types verbatim, so there was nothing to decide and nothing to invent. `init` now emits all three legs the handler reads — `commitlint.config.mjs`, the commitlint packages in `devDependencies`, and (with `--features hooks`) a `.husky/commit-msg` running `npx --no-install commitlint`, so a missing tool is an ERROR rather than the silent skip `GT-623` found. Without `hooks` the config is still written and the result carries a warning saying nothing runs it, rather than leaving a file that looks like enforcement and is not. It is runtime-independent on purpose: commitlint is a Node tool, so a .NET or Python satellite gets a tooling-only `package.json` — which is what such repositories carry in practice and what the handler actually reads; emitting the config alone would have left the rule failing for every non-Node runtime. **`MTN-05` was NOT a scaffold gap, and emitting a `spec.boundedContexts` stanza would have been the wrong fix.** The rule's own description says the strategy MUST be defined "before Phase 2 Design", and a phase-0 scaffold has no bounded contexts to declare; satisfying it at `init` would have meant writing an INVENTED persistence decision into every new repository, after which `MTN-05` would pass for a reason nobody chose. Worse, per the handler's own note the two vocabularies are irreconcilable — `evolith-yaml.schema.json` restricts `persistence` to a database-engine enum under `additionalProperties: false` — so any value written either breaks the contract schema or cannot match the rule as worded. It carries `appliesFromSdlcPhase: 2` instead, which is the applicability fact the rule always stated in prose, and follows the `TAX-06` precedent (annotation plus a parallel rego). The rule is DEFERRED, not weakened, and that is asserted rather than assumed: a test walks phases 0→3 and requires it to bind again at Design. **A methodological finding worth more than the fix.** The premise did not reproduce on first contact: the suite resolves `@beyondnet/evolith-core-domain` through the workspace symlink to `dist/`, which was nine days stale, so both rules were reported `skipped` and the pinned assertion ALREADY read `[]`. A green run in that state would have "proved" the gap closed before a line was written — the same class as `GT-625` and `GT-632`, a pass that depends on untracked build state. It was reproduced only after re-anchoring `node_modules` at the worktree and rebuilding, and each rule was then verified individually rather than through the suite: `GIT-08` executes and PASSES (typescript and python), `MTN-05` is reported `notApplicable` rather than silently passing. | | | `Core Domain` | Cross | P1 | S | `DONE` | | [`GT-632`](./gap-reference-catalog.md#gt-632) | **The MCP denied every tool in production from a path typo, and the evidence that it worked was untracked local state.** `abac-evaluator.ts:322` resolved the compiled policy at `/sdk/cli/rulesets/opa/policy.wasm` — the PRE-`src/`-refactor path. The file lives at `src/sdk/cli/…`. When the stat fails the evaluator denies fail-closed in production, so on a clean checkout with `NODE_ENV=production` the compiled policy never loads and EVERY MCP tool is refused. It looked green because `policy.wasm` is gitignored: a stale file left at the old path by an earlier era made the load succeed locally, and the test that covers it passed against an artifact no fresh checkout would have. Same shape as `GT-625` — a green that depended on untracked state — but on the authorization path. Found on 2026-07-29 while investigating why `abac-rego-parity.spec.ts` had gone red; the rego itself was correct (`opa eval` returns `allow: true` for an architect in production). **`40-validate-path-literals` cannot see this class:** it scans string literals, and this is a `path.join(corePath, 'sdk', 'cli', …)` construction. Twelve such joins survive the refactor across `src/**` — the other eleven are in `opa-input-builder.ts` and `cli-release-rule.handler.ts`, where a wrong path produces a false NEGATIVE in an evaluator rather than a denial, which is quieter and no less wrong. **FIXED for the authorization path on 2026-07-29:** the evaluator now tries both locations, newest first — not sloppiness, but what keeps an already-deployed image working while the path is corrected. Reverting the fix turns `abac-rego-parity.spec.ts` red (1 of 11); with it, mcp-server is 434/434. The other eleven joins and the guard blind spot remain. **Criterion 2 CLOSED 2026-07-29 — and two of the eleven were wrong in a way the prefix alone would not have fixed.** Eight were plain survivors of the `src/` move and took the prefix. The other three were worse: two pointed at `sdk/cli/src/core/mcp/server.ts`, a file that left the CLI entirely when mcp-server became its own package (now `src/packages/mcp-server/src/mcp/mcp-server.service.ts`); and one asserted a per-package `package-lock.json`, which npm WORKSPACES forbids — there is exactly one lockfile, at the root, so that check could only ever be false. A rule nobody can satisfy is not a stricter rule. **The specs and parity fixtures encoded the old layout too** (`cli-release-readiness.fixture.json`, `mcp.fixture.json`), which is why they had stayed green over paths that do not exist: the fixture and the code agreed with each other and neither agreed with the repository. Both are corrected. core-domain 1367/1367, `28-native-evaluator-parity` exit 0. **Criterion 3 CLOSED 2026-07-29 — the guard's first run was mostly wrong, which is the finding.** `47-validate-joined-paths` reported 19 broken paths; only 3 were defects. The other 16 would each have been made WORSE by a fix: six had base `root`, a PARAMETER naming the customer workspace under evaluation, so correcting them would break evaluation for every satellite; nine were members of FALLBACK CHAINS that are correct when one of four layouts resolves, and the natural way to silence those is to delete the fallbacks, breaking deployed images; one was a PROHIBITION whose absence IS the passing state, so correcting it would have inverted the rule. Chains are now detected structurally (array siblings, or the arms of a ternary); prohibitions cannot be, so they carry a written reason. The three real defects: `opa-evaluator.ts` resolved `policy.wasm` and the input schemas without the `src/` prefix, same fail-closed class as the P0 above; `satellite-upgrade-diff.ts` probed `rulesets` behind an early return, reporting zero ruleset changes on every satellite upgrade; `artifact-path-resolver.ts` named an `adr-matrix.json` present in no layout. 11 negative fixtures, both anti-vacuous floors, and `43-validate-guard-negative-fixtures` OBSERVED it red across 36 guards. A known leniency (two joins as sibling arguments) is recorded in its header rather than hidden. | | | `MCP Server` | Cross | P0 | M | `DONE` | | [`GT-631`](./gap-reference-catalog.md#gt-631) | **Carved out of [`GT-573`](./gap-reference-catalog.md#gt-573) so its cross-repo half is tracked rather than absorbed into a closure.** The Core half of GT-573's third criterion is done: `evaluation-context` and `evaluation-result` are pinned in `MACHINE_CONTRACT_SET` alongside `gate-evidence` and `output-envelope`, so a change to either shape now fails `10-validate-contract-conformance` instead of silently reaching consumers. What remains is in another repository and cannot be done from here: the **Tracker** must re-pin those two schemas in its own `contracts/evolith-core-contracts.json` with their sha256, bind the three published fixtures (`EVALUATION_RESULT_PASS/FAIL/OPA_GATE_FAIL`) in a DTO binding test, drop or document `resolvedTopology` (which the canonical result never carried), rename its gate `phase` to `phaseId`, and delete its fall-through to `SKIPPED` for a payload carrying a non-blank `overallVerdict`. Until it does, a Core-side shape change is caught here and still surprises the consumer. | | | `Evolith Tracker` | Cross | P1 | S | `PENDING` | | [`GT-630`](./gap-reference-catalog.md#gt-630) | **Derived artifacts have a dependency ORDER and nothing enforced it — it cost three red required checks on 2026-07-28 alone.** `generate-executive-summary.mjs` reads `maturity-reconciliation.json`, which is itself derived from the gap board. Generate the summary BEFORE reconciling and it captures a value the reconciler is about to move. Each artifact's own `--check` then passes at that moment — the summary genuinely matches what was just written — so `ci-runner governance` goes green locally and `Validate documentation` goes red on the runner, where the steps run in the declared order. That is not a stale-artifact bug, which the individual `--check` modes already catch; it is an ORDER bug, invisible to any check that looks at one artifact at a time. The correct sequence (reconcile → suite → generate → validate) was written down nowhere in the repository, which is why the same mistake was made three times in one session by someone who had already been bitten by it twice. **CLOSED on registration.** The chain is now DATA in `46-validate-derived-artifact-order.mjs` — producer, what it consumes, what it writes — walked in dependency order, wired into `Validate documentation`. It reports the FIRST stale link with the command that fixes it and the inputs that must be current before it, because an artifact built on stale input is not independently wrong. Writing the tests bounded what the fixed-point pass is worth, and the bound is recorded in the guard: with deterministic generators, currency-in-order already subsumes it; it earns its place on a generator whose output does not depend only on its declared inputs, which is the case the self-test exercises. 8 self-tests, four of them anti-vacuous. Registered in `guard-classification.mjs`; 58 guards classified, 35 observed failing. | | | `Governance` | Cross | P2 | S | `DONE` | @@ -647,7 +648,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-246`](./gap-reference-catalog.md#gt-246) | Implement Chaos Mesh/Litmus experiments | | | `QA` | Cross | P3 | L | `DONE` | -**Progress:** 592 / 632 done · 14 in progress · 22 pending · 4 deferred +**Progress:** 593 / 633 done · 14 in progress · 22 pending · 4 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 6dff5d417..625ef6147 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -45,14 +45,14 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| | Fecha canónica del tablero | 2026-07-26 | -| Gaps totales | 632 | -| Gaps cerrados | 592 | +| Gaps totales | 633 | +| Gaps cerrados | 593 | | Gaps pendientes | 40 | | P0 abiertos | 4 | | P1 abiertos | 15 | | P2 abiertos | 18 | | Cierre total | 93.7% | -| Registros de evidencia de cierre | 574 | +| Registros de evidencia de cierre | 575 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index 2b6caa8cc..59f0f07bd 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -45,14 +45,14 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| | Canonical board date | 2026-07-26 | -| Total gaps | 632 | -| Closed gaps | 592 | +| Total gaps | 633 | +| Closed gaps | 593 | | Open gaps | 40 | | Open P0 | 4 | | Open P1 | 15 | | Open P2 | 18 | | Total closure | 93.7% | -| Closure evidence records | 574 | +| Closure evidence records | 575 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index 722d157da..aecda681b 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -3,14 +3,14 @@ "scope": "evolith-core", "asOf": "2026-07-26", "gaps": { - "total": 632, - "done": 592, + "total": 633, + "done": 593, "pending": 22, "inProgress": 14, "deferred": 4 }, "evidence": { - "closureRecords": 574, + "closureRecords": 575, "cliPackage": "@beyondnet/evolith-cli@1.2.1", "adrCount": 139, "rulesetCount": 174, From ca035bd10234c70b354e6f1e7008a639f2d08da3 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 14:40:39 -0500 Subject: [PATCH 03/22] docs(interfaces): regenerate the derived how-tos after the MTN-05 annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exploration tester's anti-drift check went red on `construction` and `qa`. Regenerated from the live capture rather than hand-edited, which is the whole point of those documents. The drift is exactly `rulesTotal: 178 -> 177`, in six captured responses and nothing else. That single decrement IS the MTN-05 annotation: on the phase-0 satellite the tester scaffolds, the rule moved out of the evaluated denominator and into `rulesNotApplicable`. One rule left, the one annotated, and no other captured surface behaviour moved — so the regeneration doubles as evidence that the change does what it claims and no more. `findings = 0` throughout: no CLI/MCP/REST divergence was introduced. Verified: test:exploration 6/6, 01-validate-docs 1491 files, 04-check-bilingual-parity 1492 scanned. Reproducing this locally first needed @nestjs/cache-manager, which is declared in core-api and mcp-server but absent from the shared node_modules — an install gap in that tree, not a code defect; CI installs it from the lockfile. Co-Authored-By: Claude Opus 5 --- reference/core/interfaces/how-to-construction.md | 6 +++--- reference/core/interfaces/how-to-qa.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/reference/core/interfaces/how-to-construction.md b/reference/core/interfaces/how-to-construction.md index b55fb8fdf..fa42cff08 100644 --- a/reference/core/interfaces/how-to-construction.md +++ b/reference/core/interfaces/how-to-construction.md @@ -115,7 +115,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", @@ -226,7 +226,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", @@ -331,7 +331,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", diff --git a/reference/core/interfaces/how-to-qa.md b/reference/core/interfaces/how-to-qa.md index 416855210..c0f7f76e8 100644 --- a/reference/core/interfaces/how-to-qa.md +++ b/reference/core/interfaces/how-to-qa.md @@ -50,7 +50,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", @@ -161,7 +161,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", @@ -266,7 +266,7 @@ Response (captured live): "rulesChecked": "", "rulesSkipped": "", "rulesErrored": 0, - "rulesTotal": 178, + "rulesTotal": 177, "skippedRuleIds": [ "ACL-02", "ACL-03", From 5acb29ed3182233692494e84fdf2035b9bd9a518 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Wed, 29 Jul 2026 15:50:56 -0500 Subject: [PATCH 04/22] fix(ci): the GT-578 dead-reference ratchet was stuck by two bugs in itself Investigating why GT-633's PR had `Governance guards (GT-578)` in the fail column surfaced an uncommitted, unfinished refactor already sitting in this worktree: `resolveCommand` called `npmScriptOperand(...)`, a function that was never defined, so the guard crashed with a ReferenceError on every invocation -- even bare `--report` mode, before `--strict`/`--max-dead` were ever reached. 309 was never a real measurement of the evidence corpus; it was whatever count the guard produced before it hit the crash path on a given run. Restoring that function (equivalent to the pre-refactor inline behaviour) dropped the count to 78 and exposed a second, real bug the first one had been masking: the corpus uses `npm run --workspace