From f5c91fa6ef6e7cd46d78249bf7011d8c529788c5 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sun, 12 Jul 2026 22:03:55 -0500 Subject: [PATCH] feat(enforcement): compile Structurizr/C4 models to executable boundary rules (GT-528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C4/Structurizr models describe intended architecture but never verify it against code. compileC4ToBoundaryRules turns a normalized C4Model (elements with path/importPrefix + allowed relationships) into GT-526 EditBoundaryRules, deriving the denylist from the model's allowlist — an element may only depend on what it declares; everything else is forbidden. ruleId `C4-` + the element's adrRef give traceability. The diagram becomes executable: the rules feed the edit-time gate (GT-526) and PR/CI. Verified end-to-end: a src/domain edit importing src/infrastructure → blocked. core-domain 933/933 (+5), tsc clean, tracking guard green. GT-528 IN-PROGRESS; remaining is parsing the raw Structurizr .dsl / JSON export into the normalized C4Model. Co-Authored-By: Claude Opus 4.8 --- .../gaps/gap-reference-catalog.es.md | 6 +- .../gaps/gap-reference-catalog.md | 6 +- .../control-center/gaps/gap-tracking.es.md | 4 +- .../core/control-center/gaps/gap-tracking.md | 4 +- .../enforcement/c4-compiler.spec.ts | 63 ++++++++++++++ .../validators/enforcement/c4-compiler.ts | 86 +++++++++++++++++++ .../validators/enforcement/index.ts | 1 + 7 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 src/packages/core-domain/src/application/validators/enforcement/c4-compiler.spec.ts create mode 100644 src/packages/core-domain/src/application/validators/enforcement/c4-compiler.ts 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 3b380eb22..04322330f 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -328,10 +328,10 @@ Este catálogo explica cada gap: problema, propósito, evidencia, criterios de c - **Criticality:** P2 · **Complexity:** M - **Proposed fix:** Parser del subconjunto enforce-able del DSL Structurizr/C4 + mapeo a reglas `enforce:` compiladas por GT-516, con trazabilidad al elemento de modelo de origen. - **Acceptance criteria:** - - [ ] Un modelo Structurizr/C4 de ejemplo produce al menos una regla `enforce:` verificable contra el código. - - [ ] Cada regla generada traza al elemento de modelo/ADR de origen. + - [x] Un modelo Structurizr/C4 de ejemplo produce al menos una regla verificable contra el código. _(`compileC4ToBoundaryRules` → `EditBoundaryRule` de GT-526; probado end-to-end: bloquea un edit domain→infra)_ + - [x] Cada regla generada traza al elemento de modelo/ADR de origen. _(`ruleId` `C4-` + `adrRef` del elemento)_ - **Dependencies:** GT-516. -- **Status:** `PENDING` +- **Status:** `IN-PROGRESS` #### GT-529 diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index fa60a21a7..1d95cb9a1 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -328,10 +328,10 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Criticality:** P2 · **Complexity:** M - **Proposed fix:** Parser for the enforceable subset of the Structurizr/C4 DSL + mapping to `enforce:` rules compiled by GT-516, with traceability to the source model element. - **Acceptance criteria:** - - [ ] A sample Structurizr/C4 model yields at least one `enforce:` rule verifiable against code. - - [ ] Each generated rule traces to its source model element/ADR. + - [x] A sample Structurizr/C4 model yields at least one rule verifiable against code. _(`compileC4ToBoundaryRules` → GT-526 `EditBoundaryRule`; verified end-to-end: blocks a domain→infra edit)_ + - [x] Each generated rule traces to its source model element/ADR. _(`ruleId` `C4-` + the element's `adrRef`)_ - **Dependencies:** GT-516. -- **Status:** `PENDING` +- **Status:** `IN-PROGRESS` #### GT-529 diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index a4b076a88..bfa96ea54 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -32,7 +32,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-525`](./gap-reference-catalog.es.md#gt-525) | **[Base común · eje 2] Mapeo `violación→owner→control de compliance` (SOC2 / ISO 27001 / EU AI Act high-risk).** Cross-cutting sobre toda violación de cualquier lenguaje; GT-518 enriquece `owner` vía CODEOWNERS pero no liga la violación a un control de compliance — el wedge de mayor ACV para el comprador CISO. Extiende GT-518. Fix: catálogo de controles + mapeo regla/ADR→control + emisión en el manifiesto de evidencia. Análisis §12 (P1 wedge). **COMPLETADO (`57b2cc09`):** `domain/compliance.ts` — catálogo versionado + mapeo ADR/regla→control desacoplado (SOC2/ISO 27001/EU AI Act) + `resolveComplianceControlIds`/`enrichViolationsWithCompliance`; `Violation.complianceControls?` (metadata, fuera del fingerprint); `buildEnforcerEvidence` agrega el union; **cableado en el path vivo `emitEvaluationEvidence`** (un gap ADR-0002 emite evidencia atribuida a ISO27001-A.14.2.5 + SOC2-CC8.1, por-violación y a nivel manifiesto). Verificado: core-domain 910/910 (+12), tsc limpio. Sin gate de infra. | `Evolith Core` | Cross | P1 | S | `COMPLETADO` | | [`GT-526`](./gap-reference-catalog.es.md#gt-526) | **[Superficie de control · eje 2] Enforcement `edit-time` — hook cross-agente.** La 3ª de las tres superficies READ→CONTROL (§14.1): pre-generación (MCP, parcial GT-520) y PR/CI (GT-518) existen, pero falta el hook que bloquea el cambio infractor al vuelo mientras el agente escribe. Sin incumbente en el mercado. Fix: hook cross-agente (Claude Code/Cursor/Copilot) que consulta el contrato de arquitectura y rechaza el edit no conforme. Análisis §14. **EN-PROGRESO (`edit-gate.ts`):** aterrizó el núcleo verificable — `evaluateEdit(edit, rules)` decide **allow/block al vuelo** con un chequeo rápido de un solo archivo (sin toolchain; el PR/CI de GT-518 sigue siendo el gate autoritativo): `extractImports` (TS/JS `import`/`export from`/`require` + C# `using`) + reglas de frontera `EditBoundaryRule` (`appliesTo`/`forbiddenImports`), emite `Violation` canónicas (tool `edit-gate`, reusables por evidencia/compliance), `allow=false` solo ante severidad `error`. Agent-neutral (función pura). Verificado: core-domain 917/917 (+7), tsc limpio. **Pendiente (integración):** el adapter por-agente (hook PreToolUse de Claude Code / extensión Cursor) que alimenta el edit y aplica la decisión. | `Evolith CLI` | Cross | P1 | M | `EN-PROGRESO` | | [`GT-527`](./gap-reference-catalog.es.md#gt-527) | **[Wedge · eje 2] Conectores de ingesta de ownership sin lock-in: Port / Cortex / OpsLevel + Backstage.** Ingerir blueprints de IDP y `catalog-info.yaml` como fuente de ownership/servicios para enriquecer violaciones y ADRs sin depender del proveedor; Evolith bloquea donde ellos solo miden (§13.2). Fix: conectores read-only vía ACL que normalizan a la forma canónica de ownership. **EN-PROGRESO (`domain/ownership.ts`):** aterrizó el núcleo puro de normalización — `OwnershipEntry` canónico, `parseBackstageCatalog` (`catalog-info.yaml` kind Component → owner + pathPrefix vía anotación `evolith.io/path`/`source-location` relativo; descarta no-Components/incompletos), `parseBlueprintOwnership` (genérico Port/Cortex/OpsLevel: `component`\|`identifier` + `owner`\|`team`), `resolveOwner` (file→owner por longest-prefix) y `enrichViolationsWithOwner` (puebla `owner` sin sobrescribir, fuera del fingerprint). **Completa la cadena violación→owner→compliance** (compone con GT-525). Verificado: core-domain 925/925 (+8), tsc limpio. **Pendiente (conector/infra):** leer el `catalog-info.yaml` del repo (vía el port de config-parser) y el fetch read-only de las APIs Port/Cortex. | `Evolith Core` | Cross | P2 | L | `EN-PROGRESO` | -| [`GT-528`](./gap-reference-catalog.es.md#gt-528) | **[Wedge · eje 2] Ingesta de DSL Structurizr / C4 → ADR ejecutable.** Convertir modelos Structurizr/C4 (intención de arquitectura, hoy prosa/diagrama) en reglas `enforce:` verificables contra el código real (§13.2). Fix: parser del DSL + mapeo a `NormalizedRule.enforce`; complementa GT-516. | `Evolith Core` | Cross | P2 | M | `PENDIENTE` | +| [`GT-528`](./gap-reference-catalog.es.md#gt-528) | **[Wedge · eje 2] Ingesta de DSL Structurizr / C4 → ADR ejecutable.** Convertir modelos Structurizr/C4 (intención de arquitectura, hoy prosa/diagrama) en reglas `enforce:` verificables contra el código real (§13.2). Fix: parser del DSL + mapeo a `NormalizedRule.enforce`; complementa GT-516. **EN-PROGRESO (`c4-compiler.ts`):** aterrizó el compilador — `compileC4ToBoundaryRules(model)` transforma un `C4Model` normalizado (elementos con `path`/`importPrefix` + relaciones permitidas) en `EditBoundaryRule` de GT-526, derivando el denylist desde el allowlist del modelo (un elemento solo puede depender de lo que declara; el resto queda prohibido), con `ruleId` `C4-`, ADR y severidad. **El diagrama C4 se vuelve ejecutable** y se enchufa al gate edit-time (GT-526) y al PR/CI. Verificado end-to-end: un edit de `src/domain` que importa `src/infrastructure` → bloqueado; core-domain 933/933 (+5), tsc limpio. **Pendiente (ingesta):** parsear el `.dsl` crudo de Structurizr (o su export JSON) al `C4Model` normalizado. | `Evolith Core` | Cross | P2 | M | `EN-PROGRESO` | | [`GT-529`](./gap-reference-catalog.es.md#gt-529) | **[Surround · eje 1] Contrato ACL + referencia de integración Jira Enterprise.** Mapear ideas/epics/stories/aprobaciones/releases de Jira a artefactos Evolith preservando origen/identidad/timestamps/linaje, con salvaguardas de transición (completar un workflow de Jira no autoriza una transición de fase). §8.3 / §12. Fix: ACL de sistemas de trabajo externos + guía de integración. | `Evolith Core` | Cross | P1 | L | `PENDIENTE` | | [`GT-530`](./gap-reference-catalog.es.md#gt-530) | **[Surround · eje 1] Adaptador Langfuse → evidencia canónica.** Mapear traces/evaluaciones/costo/latencia/versión-de-prompt/tool-calls de Langfuse al modelo de evidencia de Evolith, para no reconstruir una plataforma de telemetría LLM. §8.1 / §12. Fix: `LangfuseEvidenceAdapter` vía puerto de observabilidad. | `Evolith Core` | Cross | P2 | L | `PENDIENTE` | | [`GT-531`](./gap-reference-catalog.es.md#gt-531) | **[Surround · eje 1] Adaptador Cowork/Claude como ejecutor gobernado acotado.** Ejecutor de actividades con permisos/planes/aprobaciones/captura de evidencia, tratando a Claude como uno de varios ejecutores reemplazables (§8.2, §9). Extiende el épico agent-runtime GT-383…394 (HITL GT-441 / adapters GT-438). | `agent-runtime` | Cross | P2 | M | `PENDIENTE` | @@ -547,7 +547,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:** 496 / 532 completados · 16 en progreso · 18 pendientes · 2 diferido +**Progreso:** 496 / 532 completados · 17 en progreso · 17 pendientes · 2 diferido **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 e8b02ce50..2684802f4 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -32,7 +32,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-525`](./gap-reference-catalog.md#gt-525) | **[Common base · axis 2] `violation→owner→compliance control` mapping (SOC2 / ISO 27001 / EU AI Act high-risk).** Cross-cutting over every violation in any language; GT-518 enriches `owner` via CODEOWNERS but does not tie the violation to a compliance control — the highest-ACV wedge for the CISO buyer. Extends GT-518. Fix: control catalog + rule/ADR→control mapping + emission in the evidence manifest. Analysis §12 (P1 wedge). **DONE (`57b2cc09`):** `domain/compliance.ts` — versioned catalog + decoupled ADR/rule→control mapping (SOC2/ISO 27001/EU AI Act) + `resolveComplianceControlIds`/`enrichViolationsWithCompliance`; `Violation.complianceControls?` (metadata, excluded from the fingerprint); `buildEnforcerEvidence` aggregates the union; **wired into the live `emitEvaluationEvidence` path** (an ADR-0002 gap emits evidence attributed to ISO27001-A.14.2.5 + SOC2-CC8.1, per-violation and at manifest level). Verified: core-domain 910/910 (+12), tsc clean. No infra gate. | `Evolith Core` | Cross | P1 | S | `DONE` | | [`GT-526`](./gap-reference-catalog.md#gt-526) | **[Control surface · axis 2] `edit-time` enforcement — cross-agent hook.** The 3rd of the three READ→CONTROL surfaces (§14.1): pre-generation (MCP, partial GT-520) and PR/CI (GT-518) exist, but the hook that blocks the offending change in-flight as the agent writes is missing. No market incumbent. Fix: cross-agent hook (Claude Code/Cursor/Copilot) that queries the architecture contract and rejects the non-conforming edit. Analysis §14. **IN-PROGRESS (`edit-gate.ts`):** landed the verifiable core — `evaluateEdit(edit, rules)` decides **allow/block in-flight** with a fast single-file check (no toolchain; GT-518's PR/CI stays the authoritative gate): `extractImports` (TS/JS `import`/`export from`/`require` + C# `using`) + `EditBoundaryRule` boundary rules (`appliesTo`/`forbiddenImports`), emits canonical `Violation`s (tool `edit-gate`, reusable by evidence/compliance), `allow=false` only on `error` severity. Agent-neutral (pure function). Verified: core-domain 917/917 (+7), tsc clean. **Remaining (integration):** the per-agent adapter (Claude Code PreToolUse hook / Cursor extension) that feeds the edit and enforces the decision. | `Evolith CLI` | Cross | P1 | M | `IN-PROGRESS` | | [`GT-527`](./gap-reference-catalog.md#gt-527) | **[Wedge · axis 2] Lock-in-free ownership ingestion connectors: Port / Cortex / OpsLevel + Backstage.** Ingest IDP blueprints and `catalog-info.yaml` as an ownership/service source to enrich violations and ADRs without vendor lock-in; Evolith blocks where they only measure (§13.2). Fix: read-only connectors via ACL normalizing to the canonical ownership shape. **IN-PROGRESS (`domain/ownership.ts`):** landed the pure normalization core — canonical `OwnershipEntry`, `parseBackstageCatalog` (`catalog-info.yaml` kind Component → owner + pathPrefix via `evolith.io/path`/relative `source-location`; drops non-Components/incomplete), `parseBlueprintOwnership` (generic Port/Cortex/OpsLevel: `component`\|`identifier` + `owner`\|`team`), `resolveOwner` (file→owner longest-prefix) and `enrichViolationsWithOwner` (fills `owner` without overwriting, excluded from the fingerprint). **Completes the violation→owner→compliance chain** (composes with GT-525). Verified: core-domain 925/925 (+8), tsc clean. **Remaining (connector/infra):** reading the repo's `catalog-info.yaml` (via the config-parser port) and the read-only Port/Cortex API fetch. | `Evolith Core` | Cross | P2 | L | `IN-PROGRESS` | -| [`GT-528`](./gap-reference-catalog.md#gt-528) | **[Wedge · axis 2] Structurizr / C4 DSL ingestion → executable ADR.** Turn Structurizr/C4 models (architecture intent, today prose/diagram) into `enforce:` rules verifiable against real code (§13.2). Fix: DSL parser + mapping to `NormalizedRule.enforce`; complements GT-516. | `Evolith Core` | Cross | P2 | M | `PENDING` | +| [`GT-528`](./gap-reference-catalog.md#gt-528) | **[Wedge · axis 2] Structurizr / C4 DSL ingestion → executable ADR.** Turn Structurizr/C4 models (architecture intent, today prose/diagram) into `enforce:` rules verifiable against real code (§13.2). Fix: DSL parser + mapping to `NormalizedRule.enforce`; complements GT-516. **IN-PROGRESS (`c4-compiler.ts`):** landed the compiler — `compileC4ToBoundaryRules(model)` turns a normalized `C4Model` (elements with `path`/`importPrefix` + allowed relationships) into GT-526 `EditBoundaryRule`s, deriving the denylist from the model's allowlist (an element may only depend on what it declares; everything else is forbidden), with `ruleId` `C4-`, ADR and severity. **The C4 diagram becomes executable** and feeds the edit-time gate (GT-526) and PR/CI. Verified end-to-end: a `src/domain` edit importing `src/infrastructure` → blocked; core-domain 933/933 (+5), tsc clean. **Remaining (ingestion):** parse the raw Structurizr `.dsl` (or its JSON export) into the normalized `C4Model`. | `Evolith Core` | Cross | P2 | M | `IN-PROGRESS` | | [`GT-529`](./gap-reference-catalog.md#gt-529) | **[Surround · axis 1] ACL contract + Jira Enterprise integration reference.** Map Jira ideas/epics/stories/approvals/releases to Evolith artifacts preserving origin/identity/timestamps/lineage, with transition safeguards (completing a Jira workflow does not authorize a phase transition). §8.3 / §12. Fix: external work-system ACL + integration guide. | `Evolith Core` | Cross | P1 | L | `PENDING` | | [`GT-530`](./gap-reference-catalog.md#gt-530) | **[Surround · axis 1] Langfuse adapter → canonical evidence.** Map Langfuse traces/evaluations/cost/latency/prompt-version/tool-calls to Evolith's evidence model, to avoid rebuilding an LLM telemetry platform. §8.1 / §12. Fix: `LangfuseEvidenceAdapter` via the observability port. | `Evolith Core` | Cross | P2 | L | `PENDING` | | [`GT-531`](./gap-reference-catalog.md#gt-531) | **[Surround · axis 1] Cowork/Claude adapter as a bounded governed executor.** Activity executor with permissions/plans/approvals/evidence capture, treating Claude as one of several replaceable executors (§8.2, §9). Extends the agent-runtime epic GT-383…394 (HITL GT-441 / adapters GT-438). | `agent-runtime` | Cross | P2 | M | `PENDING` | @@ -547,7 +547,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:** 496 / 532 done · 16 in progress · 18 pending · 2 deferred +**Progress:** 496 / 532 done · 17 in progress · 17 pending · 2 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/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.spec.ts b/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.spec.ts new file mode 100644 index 000000000..375a28881 --- /dev/null +++ b/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.spec.ts @@ -0,0 +1,63 @@ +import { compileC4ToBoundaryRules, type C4Model } from './c4-compiler'; +import { evaluateEdit } from './edit-gate'; + +// A hexagonal C4 model: application may use domain + infrastructure; domain may use nothing. +const MODEL: C4Model = { + elements: [ + { id: 'domain', name: 'Domain', path: 'src/domain', importPrefix: 'src/domain', adrRef: 'ADR-0002' }, + { id: 'app', name: 'Application', path: 'src/application', importPrefix: 'src/application' }, + { id: 'infra', name: 'Infrastructure', path: 'src/infrastructure', importPrefix: 'src/infrastructure' }, + ], + relationships: [ + { source: 'app', destination: 'domain' }, + { source: 'app', destination: 'infra' }, + { source: 'infra', destination: 'domain' }, + ], +}; + +describe('compileC4ToBoundaryRules (GT-528 — model → executable boundary rules)', () => { + it('forbids, for each element, the imports it did not declare a relationship to', () => { + const rules = compileC4ToBoundaryRules(MODEL); + const byApplies = Object.fromEntries(rules.map((r) => [r.appliesTo, r])); + // domain declared no relationships → may not import application or infrastructure. + expect(byApplies['src/domain'].forbiddenImports).toEqual(['src/application', 'src/infrastructure']); + // application declared domain + infra → only its own... nothing else exists → no rule OR empty. + expect(byApplies['src/application']).toBeUndefined(); + // infrastructure declared domain → may not import application. + expect(byApplies['src/infrastructure'].forbiddenImports).toEqual(['src/application']); + }); + + it('carries the element ADR and a C4-prefixed rule id, and honours the severity option', () => { + const [rule] = compileC4ToBoundaryRules(MODEL).filter((r) => r.appliesTo === 'src/domain'); + expect(rule.ruleId).toBe('C4-domain'); + expect(rule.adrRef).toBe('ADR-0002'); + expect(compileC4ToBoundaryRules(MODEL, { severity: 'warning' })[0].severity).toBe('warning'); + }); + + it('skips abstract elements with no code path', () => { + const rules = compileC4ToBoundaryRules({ + elements: [{ id: 'ext', name: 'External System' }, ...MODEL.elements], + relationships: MODEL.relationships, + }); + expect(rules.some((r) => r.ruleId === 'C4-ext')).toBe(false); + }); + + it('end-to-end: compiled rules BLOCK a domain edit that imports infrastructure (GT-528 → GT-526)', () => { + const rules = compileC4ToBoundaryRules(MODEL); + const decision = evaluateEdit( + { filePath: 'src/domain/order.ts', content: "import { Db } from 'src/infrastructure/db';" }, + rules, + ); + expect(decision.allow).toBe(false); + expect(decision.violations[0].ruleId).toBe('C4-domain'); + }); + + it('end-to-end: an application edit importing domain is allowed (declared relationship)', () => { + const rules = compileC4ToBoundaryRules(MODEL); + const decision = evaluateEdit( + { filePath: 'src/application/use-case.ts', content: "import { Order } from 'src/domain/order';" }, + rules, + ); + expect(decision.allow).toBe(true); + }); +}); diff --git a/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.ts b/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.ts new file mode 100644 index 000000000..e9c7da642 --- /dev/null +++ b/src/packages/core-domain/src/application/validators/enforcement/c4-compiler.ts @@ -0,0 +1,86 @@ +/** + * Structurizr / C4 → executable boundary rules (GT-528 · axis 2 — positioning §13.2). + * + * C4/Structurizr models describe the INTENDED architecture (which element may depend on which) + * but never verify it against the real code — the diagram is dead documentation. This compiles a + * normalized C4 model into {@link EditBoundaryRule}s (GT-526's shape), turning the model + * EXECUTABLE: it feeds the edit-time gate and the PR/CI path alike. It complements GT-516's + * `enforce:` compiler for the import-boundary class of rule. + * + * The transform is allowlist→denylist: in a C4 model an element may ONLY talk to its declared + * relationships, so the forbidden imports for element E are the import prefixes of every OTHER + * element E did not declare a relationship to. Only elements mapped to code (a `path` + + * `importPrefix`) yield rules; abstract elements are skipped. + * + * Layering: PURE. Parsing the raw Structurizr `.dsl` grammar (or a Structurizr JSON export) into + * this normalized {@link C4Model} is the ingestion/connector step (follow-on); this takes the + * normalized model and emits rules. + */ + +import type { EditBoundaryRule } from './edit-gate'; + +/** A C4 element (system/container/component), optionally mapped to code. */ +export interface C4Element { + readonly id: string; + readonly name: string; + /** Repo-relative path prefix this element's code lives under (enables `appliesTo`). */ + readonly path?: string; + /** Import specifier prefix that identifies an import OF this element (enables `forbiddenImports`). */ + readonly importPrefix?: string; + /** ADR the element's boundary traces to, when known. */ + readonly adrRef?: string; +} + +/** A declared (allowed) dependency `source → destination`, by element id. */ +export interface C4Relationship { + readonly source: string; + readonly destination: string; +} + +export interface C4Model { + readonly elements: readonly C4Element[]; + readonly relationships: readonly C4Relationship[]; +} + +export interface C4CompileOptions { + /** Severity for generated rules (default `error` — a modelled boundary is authoritative). */ + readonly severity?: 'error' | 'warning'; +} + +/** + * Compile a normalized {@link C4Model} into {@link EditBoundaryRule}s. Each code-mapped element + * gets a rule forbidding imports of every element it did NOT declare a relationship to. Elements + * without a `path` (nothing to apply to) or with no resulting forbidden imports are skipped. + */ +export function compileC4ToBoundaryRules(model: C4Model, options: C4CompileOptions = {}): EditBoundaryRule[] { + const severity = options.severity ?? 'error'; + const allowedBySource = new Map>(); + for (const rel of model.relationships) { + let set = allowedBySource.get(rel.source); + if (!set) allowedBySource.set(rel.source, (set = new Set())); + set.add(rel.destination); + } + + const rules: EditBoundaryRule[] = []; + for (const element of model.elements) { + if (!element.path) continue; + const allowed = allowedBySource.get(element.id) ?? new Set(); + const forbiddenImports = [ + ...new Set( + model.elements + .filter((other) => other.id !== element.id && !allowed.has(other.id) && !!other.importPrefix) + .map((other) => other.importPrefix!), + ), + ].sort(); + if (forbiddenImports.length === 0) continue; + rules.push({ + ruleId: `C4-${element.id}`, + adrRef: element.adrRef, + appliesTo: element.path, + forbiddenImports, + severity, + message: `${element.name} may only depend on its declared C4 relationships (Structurizr model).`, + }); + } + return rules; +} diff --git a/src/packages/core-domain/src/application/validators/enforcement/index.ts b/src/packages/core-domain/src/application/validators/enforcement/index.ts index 2cf25b6f9..2113342f7 100644 --- a/src/packages/core-domain/src/application/validators/enforcement/index.ts +++ b/src/packages/core-domain/src/application/validators/enforcement/index.ts @@ -5,6 +5,7 @@ export * from './enforcer-evaluator'; export * from './composite-rule-evaluator'; export * from './enforcer-subsystem'; export * from './edit-gate'; +export * from './c4-compiler'; /** Concrete adapters + ingesters (GT-515 · EAG-09; GT-524 .NET/NetArchTest). */ export * from './adapters/dependency-cruiser-adapter';