Contracts

integration-boundary-rule.md

Contract: Integration/Core Boundary Rule

Contract ID: integration-boundary-rule Version: 1.0.0 Mission: integration-boundary-01KW0PBE Status: Active


One-Directional Rule

CORE must NOT import INTEGRATION.
INTEGRATION may import CORE facades.

CORE set (src/specify_cli/)

  • core/
  • status/
  • readiness/
  • invocation/

INTEGRATION set (src/specify_cli/)

  • orchestrator_api/
  • sync/
  • tracker/
  • saas/
  • saas_client/

Enforcement

tests/architectural/test_integration_boundary.py enforces this contract on every CI run. The test: 1. Uses stdlib ast.walk to scan ALL import forms (module-level, if TYPE_CHECKING: blocks, lazy function-body imports). 2. Scans every .py file in all four CORE-set directories recursively. 3. Fails with a message identifying: the violating source file, the offending import path, and the corrective action (NFR-002: ≥ 3 diagnostic fields). 4. Carries pytest.mark.architectural. 5. 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). 6. Includes a sanity sub-test that proves the allowlist cannot be bypassed silently.


Allowlist

Controlled exceptions. Each entry must include source module, imported module, and written rationale. Changes require editing test_integration_boundary.py directly.

SourceImportedRationalePlanned resolution
readiness/coordinator.pyspecify_cli.saas.rolloutis_saas_sync_enabled is a shared-config v1 pure feature-flag read; relocation to a core/kernel config module is planned. Exempted until relocation lands.Follow-up mission (no issue number yet)

Corrective Action for Violations

When the test fails: 1. Do NOT add an allowlist entry unless the crossing is a deliberate, time-bounded exception with a written follow-up plan. 2. Route the dependency through the adapter/observer registry instead:

status/adapters.py or core/adapters.py; call the fire function.

3. If physical extraction is the right long-term fix, file a follow-up mission targeting src/orchestrator/ per ADR architecture/adrs/2026-05-11-1-defer-391-structural-extraction-from-3-2-x.md.

  • Core → Sync/Tracker/SaaS fan-out: register an observer with
  • Invocation → Sync: register via invocation/adapters.py.

Out-of-Scope (Deferred)

  • Bidirectional enforcement (INTEGRATION importing CORE) — C-003.
  • coordination/, lanes/, runtime/ — C-004.
  • Physical extraction to src/orchestrator/ — C-001, ADR 2026-05-11-1.

invocation-adapters-registry-contract.md

Contract: invocation/adapters.py Registry

Contract ID: invocation-adapters-registry Version: 1.0.0 Mission: integration-boundary-01KW0PBE Reference spec constraint: FR-008, C-007


Module: src/specify_cli/invocation/adapters.py

Mirrors src/specify_cli/status/adapters.py exactly. No deviation from the pattern.

Public API

from collections.abc import Callable
from pathlib import Path
from typing import Any

# Resolver: given repo_root, returns True/False/None (None = no preference)
SyncRoutingResolver = Callable[[Path], bool | None]

# Factory: given repo_root, returns a connected client or None
SaasClientFactory = Callable[[Path], Any | None]

def register_sync_routing_resolver(fn: SyncRoutingResolver) -> None: ...
def register_saas_client_factory(fn: SaasClientFactory) -> None: ...
def resolve_sync_routing(repo_root: Path) -> bool | None: ...
def get_saas_client(repo_root: Path) -> Any | None: ...
def reset_adapters() -> None: ...  # test-only

Invariants

InvariantSpecification
Non-raisingresolve_sync_routing and get_saas_client catch all exceptions; return None on any error
Idempotent registrationRe-registering a handler with the same __module__.__qualname__ replaces the existing entry; no duplicates
Empty registry is no-opresolve_sync_routing returns None when no resolver registered; get_saas_client returns None when no factory registered
No third-party importsThe module imports only stdlib and specify_cli.invocation.* (C-007)
No INTEGRATION importsThe module MUST NOT import from specify_cli.sync., specify_cli.tracker., etc.

Registration Site

Concrete implementations MUST be registered in src/specify_cli/sync/__init__.py::register_default_handlers() using the same contextlib.suppress(ImportError) guard pattern.

# Resolver lambda — reads CheckoutSyncRouting, returns just effective_sync_enabled
# The lambda is defined in sync/__init__.py where sync.routing is a legal import.
def _sync_routing_resolver(repo_root: Path) -> bool | None:
    from specify_cli.sync.routing import resolve_checkout_sync_routing
    routing = resolve_checkout_sync_routing(repo_root)
    if routing is None:
        return None
    return routing.effective_sync_enabled

# Factory — returns connected WebSocketClient or None
def _saas_client_factory(repo_root: Path) -> Any | None:
    # ... mirrors _get_saas_client logic from propagator.py, moved here
    ...

Degradation Contract

Stateresolve_sync_routing returnsget_saas_client returnsPropagator behaviour
No resolver registeredNoneN/ASync-enabled check skipped; propagator continues — same as resolve_checkout_sync_routing returning None
No factory registeredN/ANone_get_saas_client returns None; propagator returns early — existing fast-path
Factory registered, not authenticatedN/ANoneSame as above
Import error on resolve callNone (caught)N/ASafe fallback

This contract ensures that if invocation/ is imported before sync/ has run register_default_handlers(), the propagator degrades safely — no crash, no data loss.