Skip to content

Contract map

What counts as a contract here. This workspace has almost no shared-type imports to trace, because pyproject.toml:514-523 forbids most of them: five of the seven members may never import each other, and atif-analytics may import only atif-models. What crosses a module boundary instead is a shape agreed by two packages that cannot reference one another’s symbols — a file layout, a JSON key order, a column projection, a typing.Protocol typed to a document rather than to an implementation. So a contract in this file is any of:

  1. a typing.Protocol declared in one package’s domain/ports.py and satisfied by an adapter elsewhere (there are exactly five, listed below);
  2. a shape declared twice or more in packages that cannot import each other, where the two declarations must agree or a query silently returns wrong rows;
  3. a build-enforced dependency rule (import-linter), which is a stronger fact than a convention;
  4. an upstream API this workspace depends on and pins;
  5. a version constraint that ships in a wheel and binds an external installer.

Every Protocol here is satisfied STRUCTURALLY, with no import in either direction. That is the single most important structural fact for reading this file. RealConverter (packages/atif-cli/src/atif_cli/converter_adapter.py:44) implements ConverterPort (packages/atif-corpus/src/atif_corpus/domain/ports.py:41) by shape alone; it never names the Protocol, and it cannot, because atif-corpus may never import atif-converter. The Protocol name appears at exactly two import sites in the whole workspace — packages/atif-corpus/src/atif_corpus/application/materialize.py:92 (the module that CALLS it) and packages/atif-cli/tests/test_converter_adapter.py:17 (the test that binds the two together in an annotation). So each Protocol row below has a producer of the shape and an implementer the type system never links to it, and the answer to “who finds out if the shape drifts” is a named test or nobody. Do not read a Protocol row as a dependency edge — pyproject.toml:514-517 forbids the edge it would imply.

Every contract below names its producer, its consumers, the verbatim shape, the assumptions consumers make beyond the shape, and the drift risk. Consumer counts are grep-derived and confirmed at each import, annotation, or call site — this repo has no code index (.gitignore:24 lists .codegraph/, and no index exists on disk), so no count here comes from a symbol graph. Two grep hazards shape every count: names collide across packages (DomainError in three, EmbeddingProviderMismatch in two, cached_tokens in two coordinate spaces), so every attribution here is by module path and never by bare name; and every cross-package import is indented inside a function body or a TYPE_CHECKING block, so a line-anchored grep finds nothing.

Contracts are ordered by confirmed consumer count, descending.

Producer: packages/atif-corpus/src/atif_corpus/domain/layout.py:18-22

Consumer(s):

Shape:

<corpus_root>/ # default: ~/.atif-sql/corpus/<corpus-slug>/
sessions/<session_id>/
trajectory.json # compact JSON (separators=(',',':')), ATIF-v1.7
loss_report.json # atif_converter LossReport.to_json()
edges.jsonl # one line per RAW record: {uuid, parent_uuid,
# message_id, type, ts, is_sidechain,
# is_compact_summary, source_file, tool_use_ids: [..]}
meta.json # {session_id, source_mtime_ns, source_files: [...],
# harbor_version, converter_version, materialized_at}
watermark.json # {path: mtime_ns} across source corpus

Assumptions consumers make:

Where the hand-written contract disagrees with the code, the code wins — and it does disagree in two places. docs/CONTRACT.md:61 heads its CLI section “atif-cli composes; only package importing the other three”, while the manifest declares five sibling dependencies (packages/atif-cli/pyproject.toml:32-36) and the CLI imports all five. docs/CONTRACT.md:16-17 lists VSS/semantic_search and the v2 LLM-analytics pipelines as out of scope and then reverses itself at docs/CONTRACT.md:17-19; both are shipped commands (packages/atif-cli/pyproject.toml:42 plus the embed / search / analyze commands). Read docs/CONTRACT.md:21-39 as authoritative for the layout — that is the part four packages actually implement — and the manifest as authoritative for who imports whom.

Drift risk: a fifth artifact, a renamed file, or a new meta.json key must be applied in four places that no test links, and three of the four are reader-side, so an addition silently reaches nobody. Mitigation: the writer-side constants at packages/atif-corpus/src/atif_corpus/domain/layout.py:18-22 are the single source of truth on the write side — any layout change starts there and then greps the three reader modules named above.

Producer: packages/atif-duck/src/atif_duck/domain/catalog.py:28 (VIEW_NAMES), with VIEW_SCHEMA:51, MACRO_NAMES:230, MACRO_SIGNATURES:247, ANALYTICS_VIEW_NAMES:269, ANALYTICS_MACRO_SIGNATURES:287, TABLE_MACRO_NAMES:308, DESCRIPTIONS:326

Consumer(s):

Shape:

VIEW_NAMES: tuple[str, ...] = (
"sessions",
"steps",
"messages",
...
)
VIEW_SCHEMA: dict[str, tuple[tuple[str, str], ...]] = {
"sessions": (
("session_id", "VARCHAR"),
("cwd", "VARCHAR"),
...
),
...
}
MACRO_SIGNATURES: dict[str, tuple[str, ...]] = {
"ago": ("interval_text",),
"model_used": ("sid",),
...
}

Assumptions consumers make:

Drift risk: adding a view or macro without a DESCRIPTIONS entry, an ARG_EXEMPLARS entry for each new parameter name, or TABLE_MACRO_NAMES membership fails CI loudly — the risk is inverted here, and the real exposure is a type that only the fixture corpus produces. Mitigation: VIEW_SCHEMA["message_embeddings"] at packages/atif-duck/src/atif_duck/domain/catalog.py:218-224 hardcodes FLOAT[1024], so run the drift test against a store built at a non-default output_dimension before changing that setting.

The seven import-linter architecture contracts

Section titled “The seven import-linter architecture contracts”

Producer: pyproject.toml:462-523

Consumer(s):

Shape:

[tool.importlinter]
root_packages = ["atif_converter", "atif_corpus", "atif_duck", "atif_models", "atif_analytics", "atif_embed", "atif_cli"]
[[tool.importlinter.contracts]]
name = "converter / corpus / duck / models / embed are mutually independent (only atif-cli composes; atif-analytics may import atif-models only)"
type = "independence"
modules = ["atif_converter", "atif_corpus", "atif_duck", "atif_models", "atif_embed"]
[[tool.importlinter.contracts]]
name = "atif-analytics imports only atif-models among workspace packages"
type = "forbidden"
source_modules = ["atif_analytics"]
forbidden_modules = ["atif_converter", "atif_corpus", "atif_duck", "atif_embed", "atif_cli"]

Assumptions consumers make:

Drift risk: a new workspace member that is not added to root_packages (pyproject.toml:463) is silently unchecked — import-linter reports “7 contracts, 7 kept” while the new package imports whatever it likes. Mitigation: adding a packages/* member means adding it to root_packages and giving it either a layers contract or a place in the independence list in the same commit.

LlmStructuredProvider — the structured-output port

Section titled “LlmStructuredProvider — the structured-output port”

Producer: packages/atif-models/src/atif_models/domain/ports.py:116

Consumer(s):

Shape:

@runtime_checkable
class LlmStructuredProvider(Protocol):
"""Port: one structured-output call. One adapter per backend.
The seam is deliberately narrow — everything provider-specific is
fixed at adapter construction.
"""
async def classify_structured(
self, *, system: str, prompt: str, schema: type[SchemaT]
) -> SchemaT: ...

Assumptions consumers make:

Drift risk: the port is @runtime_checkable, so an isinstance check passes on method names alone and would accept an adapter with the wrong parameter kinds (positional instead of keyword-only), and no isinstance guard against it exists in the workspace anyway (grep -rn 'isinstance(.*LlmStructuredProvider' packages/ returns nothing). Mitigation: the keyword-only signature at packages/atif-models/src/atif_models/domain/ports.py:123-125 is the contract, and the factory’s annotated return at packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:34 is where a mismatched adapter fails the typecheck gate.

Producer: packages/atif-converter/src/atif_converter/domain/edges.py:22-32

Consumer(s):

Shape:

#: Stable key order for one edges.jsonl line (contract-fixed shape).
EDGE_FIELDS: tuple[str, ...] = (
"uuid",
"parent_uuid",
"message_id",
"type",
"ts",
"is_sidechain",
"is_compact_summary",
"source_file",
"tool_use_ids",
)

Assumptions consumers make:

Drift risk: the nine keys are declared twice in packages that cannot import each other, and a tenth key added on the producer side is simply invisible to _EDGE_COLUMNS — a projection reader drops unknown keys without erroring. Mitigation: EDGE_FIELDS is a single tuple; changing it means editing packages/atif-duck/src/atif_duck/infrastructure/registry.py:107-117 in the same commit.

EmbeddingProvider, VectorStorePort, TextRowsPort

Section titled “EmbeddingProvider, VectorStorePort, TextRowsPort”

Producer: packages/atif-embed/src/atif_embed/domain/ports.py:31, packages/atif-embed/src/atif_embed/domain/ports.py:59, packages/atif-embed/src/atif_embed/domain/ports.py:87

Consumer(s):

Shape:

class EmbeddingProvider(Protocol):
@property
def model_id(self) -> str: ...
@property
def dimension(self) -> int: ...
async def embed_documents(self, texts: list[str]) -> list[list[float] | None]: ...
def embed_query(self, text: str) -> list[float]: ...
class VectorStorePort(Protocol):
def table_identity(self) -> tuple[str, int] | None: ...
def get_embedded_hashes(self) -> dict[str, str]: ...
def delete_uuids(self, uuids: Iterable[str]) -> int: ...
def add_chunk(self, df: pl.DataFrame) -> None: ...
def optimize(self) -> None: ...
def ensure_index(self, *, metric: str = "cosine") -> None: ...
class TextRowsPort(Protocol):
def iter_unembedded(
self,
corpus_root: Path,
*,
embedded: dict[str, str] | None = None,
limit: int | None = None,
) -> Iterator[PendingText]: ...

Assumptions consumers make:

Drift risk: dimension is read once per run and stamped on every row (packages/atif-embed/src/atif_embed/application/embed.py:161-162), so a provider whose width depends on the input rather than on configuration would write a store whose rows disagree with their own dim column. Mitigation: dimension is a property fixed at adapter construction from settings — packages/atif-embed/src/atif_embed/infrastructure/cohere_bedrock.py:305-307 reads it from packages/atif-embed/src/atif_embed/infrastructure/settings.py:31, never from a response.

Producer: packages/atif-corpus/src/atif_corpus/domain/ports.py:41 (ConverterPort) and packages/atif-corpus/src/atif_corpus/domain/ports.py:21 (ConversionOutput)

Consumer(s):

Shape:

@dataclass(frozen=True, slots=True)
class ConversionOutput:
trajectory_dict: dict[str, Any]
loss_report_dict: dict[str, Any]
edges_lines: list[str]
class ConverterPort(Protocol):
"""Anything that can turn one session JSONL into corpus artifacts.
Implementations may raise any exception: the materialize use case
records the failure against the session and continues — one broken
transcript must never abort a corpus sync.
"""
def convert(self, session_jsonl: Path) -> ConversionOutput:
"""Convert one session (main JSONL + its side-files) to artifacts."""
...

Assumptions consumers make:

Drift risk: the three dict[str, Any] fields mean a converter that renames a trajectory key type-checks perfectly and produces artifacts atif-duck’s explicit projections silently null out; and because the implementer never names the port, a convert signature change on either side is caught by exactly one assertion. Mitigation: packages/atif-cli/tests/test_converter_adapter.py:88-92 is that assertion — treat it as part of the contract, not as a redundant smoke test — and packages/atif-cli/tests/test_converter_adapter.py:17-60 pins the field mapping while atif-duck’s DESCRIBE drift test catches a projection that stops matching.

Producer: packages/atif-duck/src/atif_duck/infrastructure/registry.py:41-44 (the semantics) and packages/atif-duck/src/atif_duck/domain/catalog.py:405 (DEFAULT_PRICING)

Consumer(s):

Shape:

# Model pricing per 1M tokens (in_rate, out_rate) at public list rates from
# Anthropic's published pricing page.
DEFAULT_PRICING: dict[str, tuple[float, float]] = {
"claude-fable-5": (10.0, 50.0),
"claude-opus-5": (5.0, 25.0),
"claude-sonnet-5": (2.0, 10.0),
"claude-haiku-4-5": (1.0, 5.0),
...
}

Assumptions consumers make:

Drift risk: cached_tokens exists in two coordinate spaces under one name — atif-duck’s per-step cache-read count (packages/atif-duck/src/atif_duck/infrastructure/registry.py:380) and atif-models’ per-call CallUsage.cached_tokens (packages/atif-models/src/atif_models/domain/ports.py:75) — and neither name states its scope, so a cross-plane join or a copied formula silently mixes them. Mitigation: attribute the field by module path, and read the producer docstring (packages/atif-duck/src/atif_duck/infrastructure/registry.py:41-44 or packages/atif-models/src/atif_models/domain/ports.py:64-70) before using either in arithmetic.

The EmbeddingProviderMismatch guard, declared twice

Section titled “The EmbeddingProviderMismatch guard, declared twice”

Producer: two coordinate declarations, coupled by contract rather than by import — packages/atif-embed/src/atif_embed/domain/errors.py:29 with the rule at packages/atif-embed/src/atif_embed/domain/embedding_guard.py:38, and packages/atif-duck/src/atif_duck/domain/embedding_guard.py:33 with the rule at packages/atif-duck/src/atif_duck/domain/embedding_guard.py:46

Consumer(s):

Shape:

def ensure_store_matches(
*,
stored_model: str | None,
stored_dim: int | None,
expected_model: str,
expected_dim: int | None,
) -> None:
if stored_model is None or stored_dim is None:
return
model_ok = stored_model == expected_model
dim_ok = expected_dim is None or stored_dim == expected_dim
if model_ok and dim_ok:
return
raise EmbeddingProviderMismatch(...)

Assumptions consumers make:

Drift risk: the twin pin checks that the hint constant reaches the raise; it does not check that the two hint STRINGS are equal, so the copies could diverge in wording while both tests pass. Mitigation: treat the two RECOVERY_HINT literals as one value — change both in the same commit, which is what both docstrings instruct.

harbor’s private _convert_events_to_trajectory

Section titled “harbor’s private _convert_events_to_trajectory”

Producer: upstream, harbor.agents.installed.claude_code.ClaudeCode, pinned harbor>=0.22.0,<0.23 at packages/atif-converter/pyproject.toml:23

Consumer(s):

Shape:

#: The private harbor entry point this whole package is built on.
_CONVERT_METHOD = "_convert_events_to_trajectory"
def assert_harbor_private_api() -> None:
from harbor.agents.installed.claude_code import ClaudeCode
if not callable(getattr(ClaudeCode, _CONVERT_METHOD, None)):
...
raise HarborPrivateApiMissing(msg)

Assumptions consumers make:

Drift risk: the whole package rests on an API upstream owes nobody, and a rename inside 0.22.x would still satisfy the pin. Mitigation: the ceiling is <0.23 with the re-audit obligation written into the manifest (packages/atif-converter/pyproject.toml:19-23), and the probe converts a rename into one specific, actionable error before any session is attempted.

The atif-sql distribution’s ==0.1.0 sibling pins

Section titled “The atif-sql distribution’s ==0.1.0 sibling pins”

Producer: packages/atif-cli/pyproject.toml:32-36 and packages/atif-analytics/pyproject.toml:24

Consumer(s):

Shape:

dependencies = [
"atif-analytics==0.1.0",
"atif-converter==0.1.0",
"atif-corpus==0.1.0",
"atif-duck==0.1.0",
"atif-embed==0.1.0",
"cyclopts>=4.10.2",
"loguru>=0.7.3",
]
version_files = [
'packages/*/pyproject.toml:^version',
'packages/atif-cli/pyproject.toml:^\s*"atif-',
'packages/atif-analytics/pyproject.toml:^\s*"atif-models==',
]

Assumptions consumers make:

  • [tool.uv.sources] is invisible to an external installer. uv’s build backend does not translate a workspace source into a version constraint, so a bare name would ship as Requires-Dist: atif-duck and resolve from PyPI to whatever the newest release of that name is, owned by whoever owns it — the reasoning is inline at packages/atif-cli/pyproject.toml:24-31.
  • One version covers the whole repository. commitizen reads its own version key rather than the PEP 621 field (pyproject.toml:401-403), and version_files propagates it to the published [project] version and to all seven member manifests; seven numbers for one artifact is the shape being refused.
  • The version_files entries are per-file rather than globbed on purpose, because --check-consistency requires a hit in every matched file and five of the seven members carry no dev pin between members (pyproject.toml:436-439).
  • The lockfile must land in the same commit as the versions it resolves. pre_bump_hooks run uv lock and git add uv.lock after the rewrite and before the commit (pyproject.toml:422-433), because uv.lock records every member’s version and mise run lock:check would otherwise fail on the release commit itself.
  • A breaking change moves 0.1.0 to 0.2.0, not 1.0.0, because major_version_zero = true (pyproject.toml:416) — reaching 1.0.0 is a decision, not a side effect of a ! in a subject line.

Drift risk: a hand-edited pin, or a renamed member whose pin line stops matching its version_files regex, goes stale silently until a release. Mitigation: --check-consistency fails rather than skipping a file that no longer contains the current version — the guarantee is stated at pyproject.toml:434-436.