Skip to content
GitHub
Decisions

ADR-097: Architecture-validator rule registry — canonical slugged rules + kernel dependency-direction discipline

Context

The architecture validator (tools/quality/validate_architecture.py) is the enforcement mechanism for the structural invariants set by ADR-031 (single library + per-app leaves), ADR-065 (contract-surface admission + inter-context import discipline), ADR-063 (no SQL grants; calls via DI; notifications via events), ADR-068 (pragmatic methods on kernel value types), ADR-070 (inter-context mechanism ladder), and ADR-034 (frontend data access via API proxy).

The validator cited a top-level END-STATE.md document as its authority (§6.1 rule list, §6.2 per-sub-directory shapes, §3.0 kernel intent). That document has been deleted. The dangling citation left the validator’s rules without a governing source of truth, and the rules themselves were unnamed — referenced positionally (“rule 4”, “rule 7”) in code comments and prose, which is brittle and unsearchable. There was no stable identifier to trace a given check back to the ADR that governs it.

Two substantive enforcement gaps surfaced alongside the dangling-citation problem:

  1. Kernel dependency direction was unchecked. The validator constrained the type-shape of items admitted to spectral.core (frozen Pydantic / Protocol / enum / Literal / Final-constant per ADR-065 D1) but did not constrain the dependency direction of imports within the kernel. The kernel is the innermost ring of the dependency rule — it may depend on nothing outward. Because only type-shape was checked, framework-coupled agent-runtime substrate (LangGraph orchestration) was placed under core/agents without tripping any rule, even though it imports outward into framework/infrastructure.

  2. The app→context-internal ban was asymmetric. The validator forbade apps from importing worlds context internals (domain.*, contracts.events.*) but did not apply the same prohibition to the other contexts. The worlds-only scope was an implementation gap, not a deliberate asymmetry — ADR-070 Tier 1 + ADR-065 D2/D4 define the legitimate app→context surface symmetrically for every context.

This ADR is the single canonical source of truth for the validator’s rules. It assigns every rule a stable slug, traces each slug to its governing ADR, decides the new kernel dependency-direction rule, names the consumer-ACL check as discrete, makes the app→context-internal ban symmetric, and records the kernel composition boundary that keeps agent-runtime substrate out of the kernel.

Decision

D1 — Canonical rule registry: one stable slug per rule, each traced to its governing ADR

The validator enforces exactly the eighteen rules below. Each rule has a stable slug ID. Code comments, prose, and validator output cite rules by slug and cite this ADR as authority. The slug is the durable identifier; positional references (“rule 4”, “rule 7”) are retired.

#SlugEnforcesAuthority
1core-no-context-importspectral.core may not import any context (worlds, platform). The kernel is context-agnostic substrate.ADR-031
2core-no-infra-importThe pure kernel zone (everything under core/ not beneath an infrastructure/ subdir) may import only stdlib + pydantic + spectral.core. Any framework/infrastructure import (langgraph, psycopg, httpx, litellm, sqlalchemy, etc.) is forbidden; the pure zone may not import the infra zone.ADR-097 (D2) + ADR-099 (D4, pure-zone scope)
3inter-context-importA context may not import another context, except a load-bearing case carrying a # noqa: AUTHORIZED-IMPORT ADR-XXX marker with a matching docs/decisions/XXX-*.md.ADR-065 D7
4consumer-acl-no-producer-payloadA context may not import another context’s contracts.events.*; consumers parse incoming payloads via a local ACL model. A discrete named check, separate from inter-context-import.ADR-065 D2/D4
5context-no-app-importA context may not import any app. The library never imports apps.ADR-031
6clean-arch-layersWithin a context: domain imports stdlib + pydantic only; application imports domain only; infrastructure may import domain + application.ADR-031
7inter-context-sqlContext code must not reference another context’s schema in SQL literals; the core schema is shared substrate.ADR-063
8core-subdir-shapeEach core/<area>/ admits only its functional-area type-shapes; the seven areas are auth, events, db, retention, llm, embeddings, tools.ADR-065 D1
9core-method-purityKernel value-type methods must be pure functions of self + value-typed args (no context/app-typed params, no EventPublisher/EventListener params).ADR-068
10core-placementEvery core item lives under a functional-area sub-directory; no top-level core/*.py except __init__.py.ADR-065 D1
11contracts-events-admission<context>.contracts.events.* admits only frozen Pydantic payloads + Literal/stdlib/same-package types; no logic.ADR-065 D2/D3
12contracts-protocols-admission<context>.contracts.protocols.* admits only Protocol/frozen-Pydantic/TypedDict/Literal; no logic.ADR-065 D2/D3
13tests-contracts-exempttests/contracts/ is exempt from inter-context import rules (bilateral contract tests).ADR-065 D6
14app-no-app-importAn app may not import another app.ADR-031
15app-context-surfaceApps may import a context’s application, infrastructure, and contracts.protocols (the framework-layer composition seam).ADR-070 Tier 1 + ADR-065
16app-no-context-internalApps may not import any context’s domain.* or contracts.events.* — symmetric across all contexts.ADR-070 + ADR-065 D2/D4
17frontend-no-library-importFrontend apps (apps/operations, apps/dashboard) may not import any spectral.* library code; they reach Postgres only through apps/api.ADR-034 D1
18core-infra-zone-no-contextThe core infrastructure zone (core/<area>/infrastructure/) may import infrastructure SDKs but may import no bounded context (worlds, platform) and no domain types. It admits only concrete impls of the seven functional-area contracts.ADR-099 D4

clean-arch-layers (rule 6) is ratified here as a registry member governed by ADR-031 — it is a first-class rule, not a check inherited informally from a prior validator. consumer-acl-no-producer-payload (rule 4) is named here as a discrete check (D4 below). core-no-infra-import (rule 2) is decided here (D2 below) and later scoped to the pure kernel zone by ADR-099. app-no-context-internal (rule 16) is made symmetric here (D3 below). core-infra-zone-no-context (rule 18) is added by ADR-099 D4, governing the core infrastructure zone that ADR-099 carves out alongside the pure kernel zone.

D2 — core-no-infra-import: the kernel may import only stdlib + pydantic

spectral.core is the innermost ring of the dependency rule. It may depend on nothing outward. Concretely, the kernel may import only the standard library and pydantic (the kernel’s expression substrate per ADR-065 D1’s type-shape constraint). Naming any framework or infrastructure dependency — langgraph, psycopg, httpx, litellm, sqlalchemy, or any peer — inverts the dependency rule and is forbidden.

This closes a gap in the prior validator, which checked the type-shape of kernel items but not the dependency-direction of their imports. A module can satisfy the type-shape constraint (frozen Pydantic, Protocol, enum) while still importing outward into a framework. That is exactly how framework-coupled agent-runtime substrate slipped into core/agents: the shapes passed, the direction was never inspected. core-no-infra-import makes the direction explicit and enforced.

The two kernel rules are complementary: core-subdir-shape (rule 8) + core-placement (rule 10) + core-method-purity (rule 9) constrain what the kernel contains and how it is expressed; core-no-context-import (rule 1) + core-no-infra-import (rule 2) constrain what the kernel may depend on. Both axes are now enforced.

D3 — app-no-context-internal is symmetric across all contexts

Apps may not import any context’s domain.* or contracts.events.*. This formalizes what ADR-070 (the app→context surface is application use cases at Tier 1, contracts.protocols at Tier 2) and ADR-065 D2/D4 (producer-owned event payloads are never imported across the boundary; consumers parse via local ACL models) already imply.

The prior validator scoped this prohibition to the worlds context only. That asymmetry was an implementation gap, not a deliberate boundary: there is no reason an app may reach into worlds.domain differently than platform.domain. The asymmetry is removed; the rule applies identically to every context. Apps reach a context only through its application, infrastructure, and contracts.protocols surfaces (rule 15, app-context-surface).

D4 — consumer-acl-no-producer-payload is a discrete named check

The prohibition on importing another context’s contracts.events.* is its own rule with its own slug, distinct from the general inter-context-import rule. Previously this check was folded silently into the inter-context import scan, which made violations report under a generic message that did not name the consumer-ACL discipline.

Naming it discretely matters because the remedy is specific: a consumer never imports the producer’s typed payload class — it declares its own local <EventName>Event ACL model and parses the envelope payload into that (ADR-065 D4). A violation of consumer-acl-no-producer-payload points the author directly at the ACL pattern, not at the general “don’t import another context” guidance.

D5 — Kernel composition boundary: agent-runtime substrate lives in apps/workers, not the kernel

The kernel’s functional areas are exactly seven: auth, events, db, retention, llm, embeddings, tools (ADR-065 D1). Agent-runtime substrate — the LangGraph supervisor, checkpointer, tool-factory, and interrupt() orchestration machinery — is not a kernel functional area. It lives in apps/workers per ADR-060 D1: “apps/workers hosts the LangGraph orchestrators for all three agents.” The workers tier is the framework-layer composition seam where inter-context tool dependencies wire via DI at startup.

core/agents had been placed in the kernel by implementation. That placement contradicts both ADR-060 D1 (orchestrators live in apps/workers) and ADR-065 D1 (the kernel admits only the seven functional areas; agent runtime is not among them), and it imports outward into LangGraph in violation of core-no-infra-import (D2). core/agents is dissolved into apps/workers. This ADR records that dissolution as compliance restoration: no ADR ever placed agent-runtime substrate in the kernel, so this supersedes no placement clause — it removes a placement that was never sanctioned.

Stands under ADR-099. The core infrastructure zone ADR-099 introduces (core/<area>/infrastructure/) does not reopen agent-runtime placement. That zone admits only concrete implementations of the seven functional-area contracts; LangGraph / agent-runtime substrate is not among them and remains in apps/workers. D5’s dissolution holds.

D6 — The validator and docs cite rules by slug and cite this ADR as authority

tools/quality/validate_architecture.py and any documentation referencing validator rules cite each rule by its slug (D1) and cite this ADR (097) as the governing authority for the registry. The deleted END-STATE.md citations are removed; the dangling reference is ended. Where a rule has a more specific governing ADR (the Authority column in D1), code and docs may cite both the slug’s source ADR and ADR-097 as the registry of record.

Consequences

  • The validator gains a discrete core-no-infra-import check (D2) and a discrete consumer-acl-no-producer-payload check (D4); the app-no-context-internal check is widened from worlds-only to all contexts (D3). These are enforcement additions, not relaxations — code that passed the prior validator may now report violations where the prior gaps masked them.
  • core/agents is dissolved into apps/workers (D5). The agent-runtime substrate continues to compose context use cases and Tier-2 OHS Protocols via DI at the workers composition seam per ADR-060 D1 + ADR-070 Tier 4; nothing about that runtime behavior changes, only its placement.
  • Validator output, code comments, and prose reference rules by slug rather than by position. Adding or retiring a rule is an amendment to this ADR’s D1 registry; the slug is the contract between the validator and the doctrine.
  • END-STATE.md is gone and is not reintroduced. The rule list (formerly its §6.1), the per-sub-directory shapes (formerly §6.2), and the kernel intent (formerly §3.0) are carried by this ADR’s D1 registry, ADR-065 D1, and D2/D5 respectively.
  • The two kernel-dependency rules (core-no-context-import, core-no-infra-import) and the kernel-shape rules (core-subdir-shape, core-placement, core-method-purity) together make the kernel’s innermost-ring position fully enforced on both the dependency-direction and type-shape axes.

References

  • ADR-031 — single-library package + per-app leaf namespaces (rules 1, 5, 6, 14)
  • ADR-034 — frontend data access via API proxy (rule 17)
  • ADR-060 — agent tool invocation + framework-layer composition; D1 places the LangGraph orchestrators in apps/workers (D5)
  • ADR-063 — inter-context access pattern; no cross-context SQL schema references (rule 7)
  • ADR-065 — contract-surface admission rules + payload pattern (rules 4, 8, 10, 11, 12, 13, 16)
  • ADR-068 — pragmatic methods on kernel value types (rule 9)
  • ADR-070 — inter-context mechanism ladder; Tier 1 app→context surface (rules 15, 16)
  • ADR-099 — core infrastructure zone; scopes core-no-infra-import (rule 2) to the pure kernel zone and adds core-infra-zone-no-context (rule 18)