ADR-071: Application-layer use case handlers placed flat under `<context>.application.<domain>`
Status: Accepted (2026-05-01)
Context
ADR-031 establishes the single-library + per-context namespace convention (spectral.<context>.*) but leaves the internal organization of each context’s application layer unspecified. During the M10 wave-5 re-refinement (2026-04-30), four epics — SPEC-238 (World Agent + evolution loop), SPEC-254 (operator world-model authoring), SPEC-255 (source-material distillation), SPEC-256 (version publication + release notes) — converged on a flat per-domain layout for application-layer use case handlers:
worlds.application.world_models.publish— notworlds.application.world_models.use_cases.publishworlds.application.publication.atomic_mint— notworlds.application.publication.commands.atomic_mintworlds.application.distillation.run_llm_pass— notworlds.application.distillation.handlers.run_llm_pass
The convergence was implicit — each epic chose flat independently based on Tier 1 use case handler simplicity per ADR-070 — but never pinned as doctrine. SPEC-237 and SPEC-239 (refined pre-M10) carry implicit nesting in their AC text. Propagating the convention to those epics needs a written-down decision rather than continued implicit convergence.
This ADR pins the convention.
Decision
D1 — Flat module per domain
Application-layer use case handlers live in flat modules under <context>.application.<domain>. Each domain has one module path — not a nested package with use-case sub-modules.
Concrete shape:
spectral/└── worlds/ └── application/ ├── world_models/ │ ├── __init__.py │ ├── publish.py # PublishWorldModel use case handler │ ├── archive.py # ArchiveWorldModel use case handler │ └── read_health.py # ReadWorldModelHealth use case handler ├── rules/ │ ├── __init__.py │ ├── enshrine.py │ ├── revise.py │ └── cluster_link.py └── publication/ ├── __init__.py ├── atomic_mint.py └── release_notes.pyEach handler module exports its handler class or function as a top-level symbol. Tests and adapters import directly: from spectral.worlds.application.world_models.publish import PublishWorldModelHandler.
D2 — Nesting permitted when a domain has internal substructure
The flat default is not a ban. If a domain’s handler set genuinely benefits from internal grouping — typically when the count exceeds ~6–8 handlers and natural sub-categories emerge — nested sub-packages are permitted. Example: if rules later splits into authoring vs lifecycle vs querying with 4–5 handlers each, a nested layout (worlds.application.rules.authoring.*, worlds.application.rules.lifecycle.*) is fine.
The default is flat because most domains hold 2–5 handlers, and flat reduces directory noise + import-path length. Nesting is the escape hatch, not the convention.
D3 — Independent of contracts placement
Inter-context contract surfaces follow ADR-065 D2 / D3 (producer-typed payloads at <context>.contracts.events.*, callee-owned Protocols at <context>.contracts.protocols.*). Those locations are independent of the application-layer organization pinned here. ADR-065 covers the contracts boundary; this ADR covers the application-layer internal layout.
Alternatives considered
Nested under use_cases / commands / handlers sub-modules. Rejected. Adds one directory level + one __init__.py per domain with no semantic gain — every module under <context>.application.<domain> IS a use case handler by definition; the sub-package label is redundant. Imports become worlds.application.world_models.use_cases.publish instead of worlds.application.world_models.publish — three name parts where two carry the same information.
Per-handler sub-package (e.g., worlds.application.world_models.publish_world_model.handler). Rejected. Useful when a handler ships with adjacent test fixtures, mocks, or DTOs that warrant package-scoped colocation. In practice, handlers in this codebase are single-file with tests in tests/ — the per-handler package is empty overhead.
Consequences
- SPEC-237 + SPEC-239 patch direction. Pre-M10 AC text using nested module paths aligns to flat per this ADR. Lands in the patch-sweep workstream alongside the other DIRTY-WITH-PATCHES sweeps.
- Future epics default flat. Application-layer module paths in epic ACs use flat layouts unless a domain-internal-substructure case is made explicitly.
- Validator rule available. The architecture validator can add a rule rejecting
*/use_cases/*.py,*/commands/*.py, and*/handlers/*.pypaths under<context>/application/to prevent drift. Add to the validator’s next-touch (SPEC-235 / kernel-audit cadence). - Independent of contracts placement. ADR-065 D2/D3 placements are unaffected; this ADR addresses application-layer internals only.
References
- ADR-031 — single library + per-context namespace; this ADR fills in the application-layer internal convention.
- ADR-065 — contracts boundary; orthogonal to this ADR’s scope.
- ADR-070 — Tier 1 use case handler default; the simplest-fit choice that drove M10 wave-5 epics toward flat layouts.
- SPEC-238, SPEC-254, SPEC-255, SPEC-256 — M10 wave-5 epics that converged on the flat layout.
- SPEC-237, SPEC-239 — patch targets for alignment per the audit-A disposition.