Skip to content
GitHub
Decisions

ADR-001: Three-context architecture (worlds, platform, core)

Context

Spectral 0.3.0 is a greenfield rewrite. The previous codebase (spectral-sef/) treated the whole system as a single bounded context organized into feature modules. That framing served the prior architecture but has aged out:

  • Spectral has grown two genuinely distinct domains with different ubiquitous languages. Scanning talks about scans, evaluations, candidate fixes, verdicts, tournaments; its primary abstractions are observations and statistical comparisons. World modeling talks about worlds, mechanics, entities, invariants, simulation rules; its primary abstractions are state spaces and transition systems.
  • When both domains live inside one context, the shared vocabulary quietly fractures: the same word gets two meanings (“entity” in worlds vs. “entity” as a generic DB record), and the same meaning gets two words (“Observation” in scanning, “Event” in worlds). That is the textbook signal that the team is carrying two bounded contexts dressed up as one.
  • We also want a durable discipline gate between the domains so one context cannot casually import internals from the other. The legacy validator could only enforce layer rules inside a single context; it had no inter-context rule to enforce because the project had no seam between contexts.

Without an explicit decision, the default would be to carry the single-context framing forward and watch the vocabulary problem repeat. The 0.3.0 scaffolding is the right moment to make the split explicit.

Decision

Adopt a three bounded-context topology for the Python monorepo:

  1. spectral.core. Owns substrate transport + cross-cutting plumbing shared across contexts. Admission discipline + the contract surface live in ADR-065.
  2. spectral.worlds. Owns the world-modeling bounded context — worlds, mechanics, entities, invariants, and the application + infrastructure layers that serve them.
  3. spectral.platform. The platform context’s responsibilities — decision-module hosting, routing, audit, auth, and billing — are governed by ADR-076. The three-context topology decision (platform as one of three bounded contexts with inter-context isolation) stands.

The package layout is the single-library form under src/spectral/ per ADR-031, with the context named platform (renamed from scanning per ADR-041 D11). The original ADR-001 used packages/<context>/ directories and spectral_<context> import-package names; the three-context topology (three contexts with isolation between them) carries forward unchanged under this layout and these names.

apps/test-agents/ (under apps/ per ADR-031) hosts reference-implementation subject agents that demonstrate how agents compose against the architecture; it is not a context and is excluded from default pytest runs.

Inter-context discipline. spectral.worlds and spectral.platform cannot import each other without an ADR explicitly authorising the exception (per ADR-065 D7). The architecture validator (tools/quality/validate_architecture.py) runs with STRICT=True and blocks CI on violations. A self-test in tools/quality/tests/test_validate_architecture.py runs the validator against a fixture tree containing a deliberate import between contexts; the test fails if the validator does not reject it.

Clean Architecture inside each context. The existing domain → application → infrastructure rule continues to apply within each context. The validator enforces both rules simultaneously: inter-context isolation at the package level and layer discipline inside each context.

This ADR explicitly departs from the prior single-context, feature-modules framing. That framing’s reasoning — “one ubiquitous language, so these are Evans-style modules, not contexts” — no longer holds for the 0.3.0 architecture: scanning/platform and world modeling have measurably different vocabularies and will diverge further. The split is made in full here.

Alternatives considered

Keep the single-context / feature-modules framing (rejected). Least disruptive option. Rejected because it defers the split rather than making it; the vocabulary problem gets worse as world-modeling grows, and the project pays for the failed abstraction in every cross-module import that really wanted to be an event between contexts.

Collapse everything into a single packages/spectral (rejected). Simplest possible layout. Rejected for the same vocabulary reason as above, plus it forecloses ever splitting the contexts later without a large rename.

Split into four or more contexts (rejected for now). Tempting to carve out the inference engine, the agent orchestration layer, or the operations tooling as separate contexts. Rejected because none of those currently have a distinct ubiquitous language from scanning — they are scanning-domain concerns. Revisit if one of them develops its own vocabulary.

Shared library (packages/shared or packages/common) instead of packages/core as the contract surface (rejected). Any package named “shared” or “common” becomes a dumping ground. Naming it core and giving it an explicit governance rule (contract-requirement tests + founder veto) is the discipline mechanism, not the name alone — but the name reinforces that it is the contract layer, not a grab-bag.

Consequences

  • The architecture validator is the primary enforcement mechanism for this decision. Every PR runs it; CI blocks on violations; a self-test fixture guarantees the validator itself stays honest.
  • spectral.core carries elevated review weight per ADR-065 admission discipline. Adding an item to the kernel is a doctrine event, not a convenience.
  • spectral.worlds and spectral.platform do not directly import each other. Coordination between them is mediated by events declared in producer-owned <context>.contracts.events.* per ADR-065 D2 + D4. The two contexts can ship at different cadences and be tested independently.
  • Apps (apps/api, apps/workers, apps/dashboard, apps/operations) may import from any context and from core; they cannot import each other. Deployment separation is preserved.
  • Future ADRs build on this one. Any decision that touches contracts between contexts, event shapes, or the core surface should link back here.