CORE / INTEGRATION Boundary Model
Filename: 2026-06-26-1-core-integration-boundary.md
Status: Accepted
Date: 2026-06-26
Deciders: Jeroen Nouws (owner), Spec Kitty planning system (recommendation)
Technical Story:
- Mission
integration-boundary-01KW0PBE— fix and enforce the CORE/INTEGRATION boundary - ADR
docs/adr/3.x/2026-05-11-1-defer-391-structural-extraction-from-3-2-x.md— context for deferred physical extraction docs/architecture/05_ownership_manifest.yaml— package ownership map
Context and Problem Statement
src/specify_cli/ contains two conceptually distinct layers: a CORE layer that
implements the canonical mission lifecycle and governance logic, and an INTEGRATION
layer that connects spec-kitty to external systems (SaaS, issue trackers, orchestrator).
Before this mission, three import leaks allowed CORE modules to depend directly on INTEGRATION modules, violating the intended separation. These violations:
- Made the governance engine dependent on external-connector availability and startup ordering.
- Made isolated testing of CORE logic harder (import side effects from INTEGRATION).
- Prevented future physical extraction of INTEGRATION to
src/orchestrator/.
This ADR records the boundary model, the one-directional rule, all allowlist exemptions, and deferred scope items so that contributors can understand the model without reading the enforcement test.
Decision Drivers
- CORE governance logic (state machine, mission creation, readiness, invocation lifecycle) must remain fast and independently testable without importing outbound connectors.
- INTEGRATION modules (sync, tracker, SaaS, orchestrator client) can legitimately read CORE state; the reverse dependency is an architectural inversion.
- The observer/adapter registry pattern (already in use for
status/adapters.py) is the proven mechanism for decoupling CORE events from INTEGRATION consumers. - Physical extraction to
src/orchestrator/is a planned future step; the boundary must be enforced in-place first so that extraction can proceed safely.
Considered Options
- (A) Enforce in-place boundary — add adapter registries in CORE; move INTEGRATION logic into INTEGRATION-owned consumers; enforce with an AST-based test.
- (B) Defer until physical extraction — allow the leaks to remain until
src/orchestrator/is ready. - (C) Silently allowlist all three leaks — add three blanket exemptions to the test.
Decision Outcome
Chosen option: (A) Enforce in-place boundary, because CORE governance logic must be independently testable and the adapter-registry pattern is already proven in the codebase. Options (B) and (C) preserve structural coupling that makes the governance engine brittle.
Set Definitions
CORE Set
Modules that implement the canonical mission lifecycle and governance logic. These modules MUST NOT import from the INTEGRATION set.
| Package | Root path | Description |
|---|---|---|
core |
src/specify_cli/core/ |
Mission creation, contract gate, dependency graph |
status |
src/specify_cli/status/ |
State machine, event log, adapter registry |
readiness |
src/specify_cli/readiness/ |
Readiness checks and coordinator |
invocation |
src/specify_cli/invocation/ |
Op lifecycle propagation and registry |
INTEGRATION Set
Modules that connect spec-kitty to external systems. These modules MAY import CORE facades (allowed direction).
| Package | Root path | Description |
|---|---|---|
orchestrator_api |
src/specify_cli/orchestrator_api/ |
Orchestrator HTTP client |
sync |
src/specify_cli/sync/ |
Real-time SaaS sync, WebSocket, OfflineQueue |
tracker |
src/specify_cli/tracker/ |
Issue-tracker origin binding |
saas |
src/specify_cli/saas/ |
SaaS-specific feature flags and rollout |
saas_client |
src/specify_cli/saas_client/ |
SaaS REST client |
Out-of-Scope Modules (C-004)
The following modules are not classified in either set for this mission.
Their import patterns are not checked by test_integration_boundary.py.
| Package | Root path | Reason for exclusion |
|---|---|---|
coordination |
src/specify_cli/coordination/ |
Deferred (C-004) |
lanes |
src/specify_cli/lanes/ |
Deferred (C-004) |
runtime |
src/specify_cli/runtime/ and src/runtime/ |
Deferred (C-004) |
The One-Directional Rule and Its Rationale
CORE must NOT import INTEGRATION.
INTEGRATION may import CORE facades.
Rationale: CORE modules implement the canonical mission lifecycle and governance logic; coupling them to outbound adapters would make the governance engine depend on the availability and startup ordering of external connectors. Inversion (CORE fires events; INTEGRATION registers handlers) keeps the governance path fast, testable, and independent of connectivity.
The adapter/observer registry pattern is the sanctioned mechanism for this inversion:
- Core → Sync/Tracker/SaaS fan-out: register an observer with
status/adapters.pyorcore/adapters.py; call the fire function. - Invocation → Sync: register via
invocation/adapters.py.
Enforcement
tests/architectural/test_integration_boundary.py enforces this rule on every CI run.
The test:
- Uses stdlib
ast.walkto scan all import forms — module-level,if TYPE_CHECKING:blocks, and lazy function-body imports. - Scans every
.pyfile in all four CORE-set directories by consuming the session-scopedsrc_source_treefixture (tests/architectural/conftest.py) — the shared, read-once/parse-once source cache the other boundary gates use — filtered to the CORE set, rather than independently re-walkingsrc/. - Fails with a message identifying: the violating source file, the offending import path, and the corrective action (NFR-002: at least 3 diagnostic fields).
- Carries
pytest.mark.architectural. - Includes a path-existence sanity check for every CORE-set directory so that a directory rename causes a loud failure rather than a vacuous pass (C-008).
- Includes a sanity sub-test that drives a real on-disk non-allowlisted violation through the same enforcement scanner the gate uses, proving the allowlist cannot be bypassed silently (and a regression in the enforcement loop itself — not just a re-implemented copy — would be caught).
- Pins
len(ALLOWLIST) <= 1with a count-ratchet so the exemption set can only shrink (when issue #2252 lands) and never silently grow.
Any new .py file added to a CORE-set directory is automatically covered without
any test change (FR-001, C-008).
Relationship to #2173 (infra/logic separation via ports)
This boundary and the infra/logic-separation work in #2173 (tracked under #1619) operate on orthogonal axes and land independently:
- This ADR governs package import direction — CORE must not import the
INTEGRATION set, enforced by
test_integration_boundary.py. - #2173 injects infra ports into pure core (FS / Clock / Git / Resolver) — a dependency-shape concern, not a cross-package import-direction concern.
The two reinforce each other: the core/adapters.py and invocation/adapters.py
registries introduced here reuse the established status/adapters.py seam, and
#2173 explicitly drops its SaaS port ("already seamed via fire_saas_fanout"),
i.e. it leans on the very inversion seam this boundary hardens. Neither blocks the
other; there is no file-level collision.
Allowlist Exemptions
Each entry permits one specific (source_module, imported_module) pair with a written
rationale. Exactly one exemption existed at the time this mission merged.
| Source | Imported | Rationale | Planned resolution |
|---|---|---|---|
readiness/coordinator.py |
specify_cli.saas.rollout |
is_saas_sync_enabled is a pure feature-flag read (shared-config v1) with no side effects. Not a structural SaaS dependency. Exempted until is_saas_sync_enabled is relocated to a core/kernel config module. |
Follow-up issue #2252 — relocate is_saas_sync_enabled and remove this entry. A len(ALLOWLIST) <= 1 count-ratchet in the enforcement test then holds the exemption set at zero. |
Adding new exemptions: Do NOT add an allowlist entry unless the crossing is a
deliberate, time-bounded exception with a written follow-up plan. Edit
test_integration_boundary.py directly with a written rationale. Broadening an
existing entry is not permitted.
Adapter Registries Introduced by This Mission
Three adapter registries were introduced or used to eliminate the leaks:
| Registry | Module | Registration site |
|---|---|---|
| Pending-origin consumer | core/adapters.py |
tracker/__init__.py startup hook |
| Sync-routing resolver | invocation/adapters.py |
sync/__init__.py startup hook |
| SaaS-client factory | invocation/adapters.py |
sync/__init__.py startup hook |
All registries follow the idempotent, non-raising contract established by
status/adapters.py.
Deferred Scope
The following items are out of scope for this mission and are explicitly deferred:
- Physical extraction to
src/orchestrator/— deferred per ADRdocs/adr/3.x/2026-05-11-1-defer-391-structural-extraction-from-3-2-x.mdand the ownership manifestdocs/architecture/05_ownership_manifest.yaml. The boundary is enforced in-place first; the physical move to a separate top-level package is a follow-up mission. - Bidirectional enforcement (preventing INTEGRATION from importing CORE in ways that violate contract) — out of scope for this mission (C-003); deferred to a follow-up.
coordination/,lanes/,runtime/— not classified in either set; their import patterns are not checked bytest_integration_boundary.py(C-004). Their boundary classification is deferred.
Consequences
Positive
- CORE governance logic is independently testable without importing external connectors.
- A new
.pyfile in any CORE-set directory is automatically covered by the enforcement test — no test change required. - The adapter-registry pattern is consistent across the codebase
(
status/adapters.py,core/adapters.py,invocation/adapters.py). - Physical extraction to
src/orchestrator/can proceed safely on top of this enforced boundary.
Negative
- The single remaining allowlist entry (
readiness/coordinator.py→specify_cli.saas.rollout) leaves one structural coupling in CORE untilis_saas_sync_enabledis relocated.
Neutral
coordination/,lanes/, andruntime/remain unclassified; their import patterns accumulate unchecked until a follow-up mission addresses C-004.
Confirmation
This decision is correct when:
- All three original leaks are fixed (WP01–WP03 merged and approved).
pytest tests/architectural/test_integration_boundary.pypasses with exactly one allowlist entry.- Follow-up issue #2252
successfully relocates
is_saas_sync_enabledand removes the last allowlist entry.
More Information
- Mission spec:
kitty-specs/integration-boundary-01KW0PBE/spec.md - Data model (set definitions and allowlist):
kitty-specs/integration-boundary-01KW0PBE/data-model.md - Contract:
kitty-specs/integration-boundary-01KW0PBE/contracts/integration-boundary-rule.md - Enforcement test:
tests/architectural/test_integration_boundary.py - Physical extraction deferral:
docs/adr/3.x/2026-05-11-1-defer-391-structural-extraction-from-3-2-x.md - Ownership manifest:
docs/architecture/05_ownership_manifest.yaml - Shared-package-boundary precedent:
architecture/3.x/adr/2026-04-25-1-shared-package-boundary.md