From 4ea76378c72d8bdf4ccb80b618db3682aa8b1ce3 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Mon, 29 Jun 2026 07:29:13 -0500 Subject: [PATCH] =?UTF-8?q?feat(core-domain):=20GT-377=20AC-3=20=E2=80=94?= =?UTF-8?q?=20enforce=20stateless-Core=20*Repository=20ban=20in=20ESLint?= =?UTF-8?q?=20+=20gate=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate core-domain to ESLint 9 flat config (eslint.config.mjs) and add a no-restricted-syntax guard banning *Repository identifiers for product/initiative/evidence/decision — Core is a stateless Evaluation Engine, those are context not entities (ADR-0101). The rule is shared from eslint.guards.cjs (single source of truth) and covered by a 14-case negative regression test (stateless-core-repository.guard.spec.ts) proving it fires on the four banned entities and never on legit *Repository. Wire lint:boundaries into ci-cd.yml (test-core-domain); delete the dead .eslintrc.js. Root cause of the long-standing "lint:boundaries broken" note was NOT the boundaries plugin: the root global override "ajv":"8.17.1" conflicts with the workspaces' direct ajv:8.20.0 (EOVERRIDE), so npm dropped all ajv overrides and starved eslint/@eslint/eslintrc of ajv@6, throwing missingRefs/defaultMeta at load. Fixed with scoped overrides (ajv aligned to 8.20.0 + eslint/@eslint/eslintrc carved out to ajv@^6.12.6); lockfile change scoped to the eslint subtree (~82 lines, no cross-workspace drift), verified under npm ci. GT-377: AC-2 + AC-3 MET; AC-1 (schemas) still open -> stays IN-PROGRESS. Gap board + catalog updated (EN/ES). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci-cd.yml | 7 ++ package-lock.json | 82 ++++++++++-- package.json | 10 +- packages/core-domain/.eslintrc.js | 95 -------------- packages/core-domain/eslint.config.mjs | 117 ++++++++++++++++++ packages/core-domain/eslint.guards.cjs | 29 +++++ packages/core-domain/package.json | 2 +- .../stateless-core-repository.guard.spec.ts | 104 ++++++++++++++++ .../vision/gap-reference-catalog.es.md | 2 +- .../standards/vision/gap-reference-catalog.md | 2 +- .../standards/vision/gap-tracking.es.md | 2 + .../standards/vision/gap-tracking.md | 2 + 12 files changed, 343 insertions(+), 111 deletions(-) delete mode 100644 packages/core-domain/.eslintrc.js create mode 100644 packages/core-domain/eslint.config.mjs create mode 100644 packages/core-domain/eslint.guards.cjs create mode 100644 packages/core-domain/src/evaluation/stateless-core-repository.guard.spec.ts diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0bbc7614..6d927a7a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -61,6 +61,13 @@ jobs: - name: Install dependencies run: npm ci + # GT-377 AC-3: stateless-Core architecture boundary guard. Fails the build + # if a *Repository for product/initiative/evidence/decision appears in + # core-domain (Core is a stateless Evaluation Engine — those are context, + # not entities — per ADR-0101), and enforces the inner-layer import rules. + - name: Run core-domain architecture boundary guard (GT-377) + run: npm run lint:boundaries --workspace @evolith/core-domain + - name: Run core-domain tests with coverage run: npm run test:cov --workspace @evolith/core-domain diff --git a/package-lock.json b/package-lock.json index c05ef5a1..0c69de81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1057,6 +1057,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -1068,6 +1085,13 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7666,6 +7690,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -7707,6 +7748,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9847,6 +9895,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9910,6 +9959,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -11219,6 +11269,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -12776,7 +12836,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12941,6 +13001,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14049,16 +14119,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "sdk/cli/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "sdk/cli/node_modules/log-symbols": { "version": "7.0.1", "license": "MIT", diff --git a/package.json b/package.json index ae0d452f..db290275 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,13 @@ "multer": "2.2.0", "js-yaml": "4.2.0", "micromatch": "4.0.8", - "ajv": "8.17.1", - "typescript": "6.0.3" + "ajv": "8.20.0", + "typescript": "6.0.3", + "eslint": { + "ajv": "^6.12.6" + }, + "@eslint/eslintrc": { + "ajv": "^6.12.6" + } } } diff --git a/packages/core-domain/.eslintrc.js b/packages/core-domain/.eslintrc.js deleted file mode 100644 index 7b0ed6a2..00000000 --- a/packages/core-domain/.eslintrc.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * ESLint boundaries configuration for @evolith/core-domain. - * - * Enforces that core-domain is a pure inner layer: - * - NO imports from apps/ (core-api, etc.) - * - NO imports from packages/mcp-server - * - Only intra-layer (domain, application, infrastructure) imports allowed - * - * Layer hierarchy for this package: - * common / types - * └─ domain (entities, value objects, domain services) - * └─ application (use cases) - * └─ infrastructure (adapters) - */ - -'use strict'; - -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - plugins: ['@typescript-eslint', 'boundaries'], - extends: ['plugin:boundaries/recommended'], - - settings: { - 'import/resolver': { - node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, - }, - - 'boundaries/elements': [ - { type: 'common', pattern: 'src/common/**/*' }, - { type: 'domain', pattern: 'src/domain/**/*' }, - { type: 'application', pattern: 'src/application/**/*' }, - { type: 'infrastructure', pattern: 'src/infrastructure/**/*' }, - { type: 'gates', pattern: 'src/gates/**/*' }, - { type: 'phases', pattern: 'src/phases/**/*' }, - { type: 'tenancy', pattern: 'src/tenancy/**/*' }, - { type: 'providers', pattern: 'src/providers/**/*' }, - { type: 'evidence', pattern: 'src/evidence/**/*' }, - { type: 'evaluation', pattern: 'src/evaluation/**/*' }, - ], - 'boundaries/ignore': [ - 'src/**/*.spec.ts', - 'src/**/*.test.ts', - ], - }, - - rules: { - '@typescript-eslint/no-explicit-any': 'warn', - - 'boundaries/element-types': [ - 'error', - { - default: 'disallow', - rules: [ - // common: no internal imports - { from: 'common', allow: [] }, - - // domain: innermost — only common - { from: 'domain', allow: ['domain', 'common'] }, - - // application: use cases — domain + common - { from: 'application', allow: ['application', 'domain', 'common'] }, - - // infrastructure: adapters — can use all inner layers - { from: 'infrastructure', allow: ['infrastructure', 'application', 'domain', 'common'] }, - - // gates/phases/tenancy/providers/evidence: cross-cutting within this package - { from: 'gates', allow: ['gates', 'domain', 'application', 'common'] }, - { from: 'phases', allow: ['phases', 'domain', 'application', 'common'] }, - { from: 'tenancy', allow: ['tenancy', 'domain', 'common'] }, - { from: 'providers', allow: ['providers', 'infrastructure', 'domain', 'common'] }, - { from: 'evidence', allow: ['evidence', 'domain', 'application', 'common'] }, - - // evaluation: stateless Core Evaluation Engine (GT-377/ADR-0101) — - // canonical contracts + orchestrator; composes domain + application. - { from: 'evaluation', allow: ['evaluation', 'domain', 'application', 'common'] }, - ], - }, - ], - - // External npm packages are allowed everywhere - 'boundaries/no-external': 'off', - }, - - ignorePatterns: [ - 'dist/', - 'node_modules/', - 'coverage/', - '**/*.js', - ], -}; diff --git a/packages/core-domain/eslint.config.mjs b/packages/core-domain/eslint.config.mjs new file mode 100644 index 00000000..ab8dcb76 --- /dev/null +++ b/packages/core-domain/eslint.config.mjs @@ -0,0 +1,117 @@ +// @ts-check +/** + * ESLint flat config for @evolith/core-domain (ESLint 9+). + * + * Migrated from the legacy `.eslintrc.js` + `--no-eslintrc` invocation, which is + * incompatible with ESLint 9 (the `--no-eslintrc` flag was removed and the legacy + * eslintrc loader broke eslint-plugin-boundaries with "Cannot set properties of + * undefined (setting defaultMeta)"). + * + * Enforces two architectural guards: + * + * 1. Layer boundaries — core-domain is a pure inner package. Imports may only + * flow inward across the layer hierarchy: + * + * common / types + * └─ domain (entities, value objects, domain services) + * └─ application (use cases) + * └─ infrastructure (adapters) + * + * 2. Stateless Core (GT-377 / ADR-0101) — Core is a stateless Evaluation Engine. + * `product` / `initiative` / `evidence` / `decision` are CONTEXT, never + * persisted entities, so a `*Repository` for any of them must NEVER appear in + * this package. The `no-restricted-syntax` rule below fails the build (and CI) + * if such an identifier is declared, imported, or referenced. + */ +import boundaries from 'eslint-plugin-boundaries'; +import tseslint from 'typescript-eslint'; +// Shared (CommonJS) so the negative regression test enforces the EXACT same rule. +import guards from './eslint.guards.cjs'; + +const { STATELESS_CORE_REPOSITORY_BAN } = guards; + +export default tseslint.config( + { + ignores: [ + 'dist/**', + 'coverage/**', + 'node_modules/**', + '**/*.js', + '**/*.cjs', + '**/*.mjs', + ], + }, + { + files: ['src/**/*.ts'], + plugins: { + '@typescript-eslint': tseslint.plugin, + boundaries, + }, + languageOptions: { + parser: tseslint.parser, + ecmaVersion: 2022, + sourceType: 'module', + }, + settings: { + 'boundaries/elements': [ + { type: 'common', pattern: 'src/common/**/*' }, + { type: 'domain', pattern: 'src/domain/**/*' }, + { type: 'application', pattern: 'src/application/**/*' }, + { type: 'infrastructure', pattern: 'src/infrastructure/**/*' }, + { type: 'gates', pattern: 'src/gates/**/*' }, + { type: 'phases', pattern: 'src/phases/**/*' }, + { type: 'tenancy', pattern: 'src/tenancy/**/*' }, + { type: 'providers', pattern: 'src/providers/**/*' }, + { type: 'evidence', pattern: 'src/evidence/**/*' }, + { type: 'evaluation', pattern: 'src/evaluation/**/*' }, + ], + 'boundaries/ignore': ['src/**/*.spec.ts', 'src/**/*.test.ts'], + }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + + 'boundaries/element-types': [ + 'error', + { + default: 'disallow', + rules: [ + // common: no internal imports + { from: 'common', allow: [] }, + + // domain: innermost — only common + { from: 'domain', allow: ['domain', 'common'] }, + + // application: use cases — domain + common + { from: 'application', allow: ['application', 'domain', 'common'] }, + + // infrastructure: adapters — can use all inner layers + { + from: 'infrastructure', + allow: ['infrastructure', 'application', 'domain', 'common'], + }, + + // gates/phases/tenancy/providers/evidence: cross-cutting within this package + { from: 'gates', allow: ['gates', 'domain', 'application', 'common'] }, + { from: 'phases', allow: ['phases', 'domain', 'application', 'common'] }, + { from: 'tenancy', allow: ['tenancy', 'domain', 'common'] }, + { + from: 'providers', + allow: ['providers', 'infrastructure', 'domain', 'common'], + }, + { from: 'evidence', allow: ['evidence', 'domain', 'application', 'common'] }, + + // evaluation: stateless Core Evaluation Engine (GT-377/ADR-0101) — + // canonical contracts + orchestrator; composes domain + application. + { + from: 'evaluation', + allow: ['evaluation', 'domain', 'application', 'common'], + }, + ], + }, + ], + + // Stateless-Core guard (GT-377 AC-3 / ADR-0101): no business-entity repositories. + 'no-restricted-syntax': ['error', STATELESS_CORE_REPOSITORY_BAN], + }, + }, +); diff --git a/packages/core-domain/eslint.guards.cjs b/packages/core-domain/eslint.guards.cjs new file mode 100644 index 00000000..0997707d --- /dev/null +++ b/packages/core-domain/eslint.guards.cjs @@ -0,0 +1,29 @@ +'use strict'; + +/** + * Architecture guards shared between the ESLint flat config (`eslint.config.mjs`) + * and the regression test (`stateless-core-repository.guard.spec.ts`), so both + * enforce the EXACT same rule — single source of truth for GT-377 AC-3. + * + * CommonJS on purpose: the `.mjs` config imports it statically and the ts-jest + * (CommonJS) test `require`s it, with no ESM/dynamic-import interop hazard. + */ + +/** + * Bans any identifier shaped like a repository for a business entity that Core + * treats as pure context (GT-377 / ADR-0101 — Core is a stateless Evaluation + * Engine). Matches declarations, imports and type references such as + * `ProductRepository`, `IInitiativeRepository`, `EvidenceRepositoryPort`, + * `InMemoryDecisionRepository`, … while leaving legitimate infrastructure + * repositories (`AuditRepository`, `SubscriptionRepository`, `DeliveryRepository`) + * untouched. + * + * @type {{ selector: string, message: string }} + */ +const STATELESS_CORE_REPOSITORY_BAN = { + selector: 'Identifier[name=/(Product|Initiative|Evidence|Decision)Repository/]', + message: + 'GT-377/ADR-0101: Core is a stateless Evaluation Engine. product/initiative/evidence/decision are context, not entities — a *Repository for them must not appear in core-domain.', +}; + +module.exports = { STATELESS_CORE_REPOSITORY_BAN }; diff --git a/packages/core-domain/package.json b/packages/core-domain/package.json index 9f413af4..d4883cc9 100644 --- a/packages/core-domain/package.json +++ b/packages/core-domain/package.json @@ -70,7 +70,7 @@ }, "scripts": { "build": "tsc", - "lint:boundaries": "eslint \"src/**/*.ts\" --no-eslintrc -c .eslintrc.js", + "lint:boundaries": "eslint \"src/**/*.ts\"", "test": "jest --config jest.config.js --runInBand", "test:cov": "jest --config jest.config.js --runInBand --coverage", "test:e2e": "jest --config jest.e2e.config.js --runInBand" diff --git a/packages/core-domain/src/evaluation/stateless-core-repository.guard.spec.ts b/packages/core-domain/src/evaluation/stateless-core-repository.guard.spec.ts new file mode 100644 index 00000000..b17a6ac8 --- /dev/null +++ b/packages/core-domain/src/evaluation/stateless-core-repository.guard.spec.ts @@ -0,0 +1,104 @@ +/** + * GT-377 AC-3 — architecture guard regression test. + * + * Asserts that the stateless-Core repository ban actually fails when a + * `*Repository` for a business entity that Core treats as pure context + * (product / initiative / evidence / decision) appears — and, crucially, does + * NOT fire for legitimate infrastructure repositories. This is the negative test + * proving the guard from `gap-reference-catalog.md` "#### GT-377" AC-3 ("ESLint + * boundary guard fails CI if a `*Repository` for product/initiative/evidence/ + * decision appears") is wired and effective, not merely declared. + * + * It exercises the SAME rule object that ships in `eslint.config.mjs` by + * importing it from the shared `eslint.guards.cjs` (single source of truth), and + * runs it through ESLint's synchronous flat-config `Linter` — no config-file + * dynamic import, so it is jest/ts-jest safe. + * + * The banned identifiers below live ONLY inside string literals (lint input), + * never as real code, so this spec itself stays green under `lint:boundaries`. + */ +import { Linter } from 'eslint'; + +// require (not import) so the runtime object matches what ESLint expects, free of +// ts-jest ESM-interop wrapping (`@typescript-eslint/parser` sets `__esModule`). +const tsParser = require('@typescript-eslint/parser') as Linter.Parser; +const { STATELESS_CORE_REPOSITORY_BAN } = require('../../eslint.guards.cjs') as { + STATELESS_CORE_REPOSITORY_BAN: { selector: string; message: string }; +}; + +const GUARD_RULE = 'no-restricted-syntax'; + +const linter = new Linter({ configType: 'flat' }); + +function guardErrors(code: string): string[] { + const messages = linter.verify(code, { + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + [GUARD_RULE]: ['error', STATELESS_CORE_REPOSITORY_BAN], + }, + }); + return messages + .filter((m) => m.ruleId === GUARD_RULE && m.severity === 2) + .map((m) => m.message); +} + +describe('GT-377 AC-3 — stateless-Core repository guard', () => { + const BANNED_ENTITIES = ['Product', 'Initiative', 'Evidence', 'Decision']; + + it.each(BANNED_ENTITIES)( + 'FAILS when a %sRepository is imported and referenced', + (entity) => { + const code = [ + `import { ${entity}Repository } from '../infrastructure/persistence';`, + `export function use(repo: ${entity}Repository): void { void repo; }`, + '', + ].join('\n'); + + const errors = guardErrors(code); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.every((m) => m.includes('GT-377'))).toBe(true); + }, + ); + + it.each(BANNED_ENTITIES)( + 'FAILS when a %sRepository interface is declared inline', + (entity) => { + const code = `export interface ${entity}Repository { findById(id: string): unknown; }\n`; + + const errors = guardErrors(code); + + expect(errors.length).toBeGreaterThan(0); + }, + ); + + it.each([ + 'AuditRepository', + 'SubscriptionRepository', + 'DeliveryRepository', + 'createRepository', + 'getRepository', + ])('does NOT flag the legitimate identifier %s (no false positive)', (ident) => { + const code = [ + `import { ${ident} } from '../infrastructure/persistence';`, + `export const ref = ${ident};`, + '', + ].join('\n'); + + expect(guardErrors(code)).toEqual([]); + }); + + it('produces zero guard errors for a clean evaluation module', () => { + const code = [ + "import type { EvaluationContext } from './contracts';", + 'export function evaluate(ctx: EvaluationContext): EvaluationContext { return ctx; }', + '', + ].join('\n'); + + expect(guardErrors(code)).toEqual([]); + }); +}); diff --git a/reference/governance/standards/vision/gap-reference-catalog.es.md b/reference/governance/standards/vision/gap-reference-catalog.es.md index 60e08428..85a7264d 100644 --- a/reference/governance/standards/vision/gap-reference-catalog.es.md +++ b/reference/governance/standards/vision/gap-reference-catalog.es.md @@ -56,7 +56,7 @@ Este catálogo explica cada gap: problema, propósito, evidencia, criterios de c - **Criterios de aceptación:** - [ ] `evaluation-context.schema.json` / `evaluation-result.schema.json` validan round-trip; `schemaVersion` obligatorio. - [ ] `tenantId`/`productId`/`initiativeId` son `string`; `DecisionRecommendation.binding` literal `false`. - - [ ] Guard ESLint falla el CI si aparece un `*Repository` de producto/iniciativa/evidencia/decisión. + - [x] Guard ESLint falla el CI si aparece un `*Repository` de producto/iniciativa/evidencia/decisión. **(MET — Oleada 2026-06-29)** `packages/core-domain/eslint.config.mjs` (flat config, ESLint 9) prohíbe `Identifier[name=/(Product|Initiative|Evidence|Decision)Repository/]` vía `no-restricted-syntax` (regla compartida desde `eslint.guards.cjs`); gateado por el step `Run core-domain architecture boundary guard (GT-377)` en `ci-cd.yml` (`test-core-domain`); test de regresión negativo `src/evaluation/stateless-core-repository.guard.spec.ts` (14 casos) prueba que dispara en las cuatro entidades prohibidas y nunca en `*Repository` legítimo (`AuditRepository`, `createRepository`, …). La rotura previa (override global `ajv:8.17.1` en conflicto con el dep directo `ajv:8.20.0` de los workspaces, dejando a `eslint`/`@eslint/eslintrc` sin `ajv@6`) se corrige con `overrides` acotados en el `package.json` raíz. - **Dependencias:** `GT-376`. #### GT-378 diff --git a/reference/governance/standards/vision/gap-reference-catalog.md b/reference/governance/standards/vision/gap-reference-catalog.md index b07da846..41ff610c 100644 --- a/reference/governance/standards/vision/gap-reference-catalog.md +++ b/reference/governance/standards/vision/gap-reference-catalog.md @@ -56,7 +56,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Acceptance criteria:** - [ ] `evaluation-context.schema.json` / `evaluation-result.schema.json` validate round-trip; `schemaVersion` mandatory. - [ ] `tenantId`/`productId`/`initiativeId` are `string`; `DecisionRecommendation.binding` literal `false`. - - [ ] ESLint boundary guard fails CI if a `*Repository` for product/initiative/evidence/decision appears. + - [x] ESLint boundary guard fails CI if a `*Repository` for product/initiative/evidence/decision appears. **(MET — Wave 2026-06-29)** `packages/core-domain/eslint.config.mjs` (flat config, ESLint 9) bans `Identifier[name=/(Product|Initiative|Evidence|Decision)Repository/]` via `no-restricted-syntax` (rule shared from `eslint.guards.cjs`); gated by the `Run core-domain architecture boundary guard (GT-377)` step in `ci-cd.yml` (`test-core-domain`); negative regression test `src/evaluation/stateless-core-repository.guard.spec.ts` (14 cases) proves it fires on the four banned entities and never on legit `*Repository` (`AuditRepository`, `createRepository`, …). The prior breakage (global `ajv:8.17.1` override conflicting with workspaces' direct `ajv:8.20.0`, starving `eslint`/`@eslint/eslintrc` of `ajv@6`) is fixed with scoped `overrides` in the root `package.json`. - **Dependencies:** `GT-376`. #### GT-378 diff --git a/reference/governance/standards/vision/gap-tracking.es.md b/reference/governance/standards/vision/gap-tracking.es.md index 6aece1f9..1b5044ad 100644 --- a/reference/governance/standards/vision/gap-tracking.es.md +++ b/reference/governance/standards/vision/gap-tracking.es.md @@ -441,6 +441,8 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida **Oleada 2026-06-29 (reconciliación GT-376–379 — verificación por-AC contra el código):** corrí una verificación de 4 agentes de los criterios R0–R3. **`GT-378` → `COMPLETADO`** (ambos ACs MET, verificado: `/v1/evaluate` acepta `EvaluationContextDto`→`EvaluationResult` vía `EnvelopeInterceptor`; adapter `mapPipelineVerdict`; opa-parity 0-drift; SDK/CLI/Tracker intactos). Los otros tres siguen `EN-PROGRESO` con blockers precisos (chips creados): **`GT-376`** — solo AC-1 abierto: ADR-0101 está `Proposed`, no `Accepted` por el Board (los otros 3 ACs MET — ADR-0100 D1 superseded, deliverables UP-002/redesign SUPERSEDED, sin `IProductRepository`/`POST /products` vivos, `CoreGateVerdict`/`WAIVE` en código). **`GT-377`** — AC-2 MET; AC-1 abierto (`evaluation-context.schema.json`/`evaluation-result.schema.json` no existen — solo los contratos TS + constantes `SCHEMA_VERSION`); AC-3 abierto (el guard de boundaries `.eslintrc.js:78-80` está roto bajo ESLint 9.39.4 Y no lo invoca ningún workflow). **`GT-379`** — AC-1 ahora MET (añadido test explícito de no-mutación del checkpoint: propuesta no-binding, idempotente, read-only — `propose-phase-advance.use-case.spec.ts`); AC-2 MET; AC-3 abierto (kind-evaluators blueprint/deployment no implementados — contratos/enums existen, evaluators no; diferidos como especulativos → chip). +**Oleada 2026-06-29 (GT-377 AC-3 — el guard de boundaries de ESLint ahora ENFORCEA + gatea CI):** el guard de repositorios de Core-stateless está vivo. Migré `packages/core-domain` del legacy `.eslintrc.js` + `--no-eslintrc` (incompatible con ESLint 9) a flat config (`eslint.config.mjs`); el guard prohíbe `Identifier[name=/(Product|Initiative|Evidence|Decision)Repository/]` vía `no-restricted-syntax` (regla compartida desde `eslint.guards.cjs` como única fuente de verdad) — acotado por entidad, así los legítimos `AuditRepository`/`createRepository` no se tocan. **Causa raíz de la nota "lint:boundaries roto" diagnosticada y corregida:** NO era el plugin de boundaries — el `package.json` raíz tenía un override global `"ajv":"8.17.1"` que *entra en conflicto* con el dep directo `ajv:8.20.0` de los workspaces (`EOVERRIDE`), así npm descartaba silenciosamente *todos* los overrides de ajv bajo `--legacy-peer-deps` y colapsaba `ajv@8` sobre `eslint`/`@eslint/eslintrc` (que necesitan `ajv@6`), haciendo que el loader de ESLint lanzara `missingRefs`/`defaultMeta`. Corregido con `overrides` acotados (`ajv` global alineado a `8.20.0`; `eslint`+`@eslint/eslintrc` recortados a `ajv@^6.12.6`); el cambio de lockfile está acotado al toolchain de eslint (≈82 líneas, sin drift cross-workspace) y verificado bajo `npm ci`. **Cableado en CI**: nuevo step `Run core-domain architecture boundary guard (GT-377)` en `ci-cd.yml` `test-core-domain` (`grep lint:boundaries .github/workflows` era 0, ahora 1). **Test de regresión negativo** `src/evaluation/stateless-core-repository.guard.spec.ts` (14 casos) prueba que el guard dispara en las cuatro entidades prohibidas (import + interface inline) y nunca en `*Repository` legítimo. Borrado el `.eslintrc.js` muerto. Verificado: `npm ci` verde → `lint:boundaries` **0 errores**; build `tsc` verde; core-domain **692/692** tests. **`GT-377`: AC-2 + AC-3 MET; AC-1 (schemas) aún abierto → sigue `EN-PROGRESO`** (pasa a `COMPLETADO` cuando aterricen `rulesets/schema/evaluation-context.schema.json`/`evaluation-result.schema.json`). Follow-up (chips): los scripts `lint:boundaries` hermanos en `packages/mcp-server` y `apps/core-api` usan la misma invocación rota `--no-eslintrc -c .eslintrc.js` y no están gateados por CI. + **Ordenamiento:** una sola tabla, ordenada por estado (pendientes luego completados), luego criticidad (`P0` → `P1` → `P2` → `P3`), luego complejidad (`XS` → `S` → `M` → `L` → `XL`). Los IDs `GT-*` enlazan al [Catálogo de Referencia de Gaps](./gap-reference-catalog.es.md); los IDs `MT-A*` enlazan al [plan de implementación Multi-Topology](./multi-topology-reference-corpus-implementation-plan.es.md). --- diff --git a/reference/governance/standards/vision/gap-tracking.md b/reference/governance/standards/vision/gap-tracking.md index e93bc504..fea77f99 100644 --- a/reference/governance/standards/vision/gap-tracking.md +++ b/reference/governance/standards/vision/gap-tracking.md @@ -455,6 +455,8 @@ This board is the single source of truth for technical debt, gaps, opportunities **Wave 2026-06-29 (GT-376–379 reconciliation — per-AC verification against code):** ran a 4-agent verification of the R0–R3 acceptance criteria. **`GT-378` → `DONE`** (both ACs MET, verified: `/v1/evaluate` accepts `EvaluationContextDto`→`EvaluationResult` via `EnvelopeInterceptor`; `mapPipelineVerdict` adapter; opa-parity 0-drift; SDK/CLI/Tracker unbroken). The other three stay `IN-PROGRESS` with precise remaining blockers (chips filed): **`GT-376`** — only AC-1 open: ADR-0101 is `Proposed`, not board-`Accepted` (the other 3 ACs MET — ADR-0100 D1 superseded, UP-002/redesign deliverables SUPERSEDED, no live `IProductRepository`/`POST /products`, `CoreGateVerdict`/`WAIVE` landed). **`GT-377`** — AC-2 MET; AC-1 open (`rulesets/schema/evaluation-context.schema.json`/`evaluation-result.schema.json` do not exist — only the TS contracts + `SCHEMA_VERSION` constants); AC-3 open (the `.eslintrc.js:78-80` boundary guard is broken under ESLint 9.39.4 AND not invoked by any workflow — `grep lint:boundaries .github/workflows` = 0). **`GT-379`** — AC-1 now MET (added an explicit checkpoint non-mutation test: non-binding, idempotent, read-only agent proposal — `propose-phase-advance.use-case.spec.ts`); AC-2 MET; AC-3 open (blueprint/deployment kind-evaluators not implemented — contracts/enums exist, evaluators don't; deferred as speculative → chip). +**Wave 2026-06-29 (GT-377 AC-3 — ESLint boundary guard now enforces + gates CI):** the stateless-Core repository guard is live. Migrated `packages/core-domain` from the ESLint-9-incompatible legacy `.eslintrc.js` + `--no-eslintrc` to a flat config (`eslint.config.mjs`); the guard bans `Identifier[name=/(Product|Initiative|Evidence|Decision)Repository/]` via `no-restricted-syntax` (rule shared from `eslint.guards.cjs` as the single source of truth) — entity-scoped so legit `AuditRepository`/`createRepository` are untouched. **Root cause of the long-standing "lint:boundaries broken" note diagnosed and fixed:** it was NOT the boundaries plugin — the root `package.json` had a global `"ajv":"8.17.1"` override that *conflicts* with the workspaces' direct `ajv:8.20.0` dep (`EOVERRIDE`), so npm silently dropped *all* ajv overrides under `--legacy-peer-deps` and collapsed `ajv@8` onto `eslint`/`@eslint/eslintrc` (which need `ajv@6`), making ESLint's loader throw `missingRefs`/`defaultMeta`. Fixed with scoped `overrides` (global `ajv` aligned to `8.20.0`; `eslint`+`@eslint/eslintrc` carved out to `ajv@^6.12.6`); lockfile change is scoped to the eslint toolchain (≈82 lines, no cross-workspace drift) and verified under `npm ci`. **Wired into CI**: new `Run core-domain architecture boundary guard (GT-377)` step in `ci-cd.yml` `test-core-domain` (`grep lint:boundaries .github/workflows` was 0, now 1). **Negative regression test** `src/evaluation/stateless-core-repository.guard.spec.ts` (14 cases) proves the guard fires on all four banned entities (import + inline interface) and never on legit `*Repository`. Deleted dead `.eslintrc.js`. Verified: `npm ci` green → `lint:boundaries` **0 errors**; `tsc` build green; core-domain **692/692** tests. **`GT-377`: AC-2 + AC-3 MET; AC-1 (schemas) still open → stays `IN-PROGRESS`** (flips to `DONE` once `rulesets/schema/evaluation-context.schema.json`/`evaluation-result.schema.json` land). Follow-up (chips): the sibling `lint:boundaries` scripts in `packages/mcp-server` and `apps/core-api` use the same broken `--no-eslintrc -c .eslintrc.js` invocation and are not CI-gated. + **Ordering:** one table, ordered by status (pending then completed), then criticality (`P0` → `P1` → `P2` → `P3`), then complexity (`XS` → `S` → `M` → `L` → `XL`). `GT-*` IDs link to the [Gap Reference Catalog](./gap-reference-catalog.md); `MT-A*` IDs link to the supporting [Multi-Topology implementation plan](./multi-topology-reference-corpus-implementation-plan.md). ---