Skip to content

Module map

Seven uv workspace members live under packages/*, declared at pyproject.toml:100; the root carries no [project] table because it is a virtual workspace holding only the member list, the shared dev dependency-group, and the shared tool config (pyproject.toml:1). The internal import graph is a star: atif-cli declares five siblings as ==-pinned dependencies (packages/atif-cli/pyproject.toml:32), plus one further edge from atif-analytics to atif-models that a forbidden import-linter contract leaves open (pyproject.toml:520), with every other pair closed by an independence contract (pyproject.toml:515). Every cross-package import sits inside a function body or a TYPE_CHECKING block so the CLI’s fast path pulls in no duckdb, harbor, lancedb, boto3, or polars, which is why PLC0415 is ignored workspace-wide (pyproject.toml:164). Modules below are ordered by total source LOC, descending; LOC figures are wc -l over the file.

run_analyze composes eight pipelines in a fixed stage order — cluster, terms, community, classify, trajectory, conflicts, friction, perceived (packages/atif-analytics/src/atif_analytics/application/analyze.py:36). The first three are structural math at zero LLM cost (:8); the remaining five call a model and honor dry_run, which defaults to True as a cost guard so those stages return plan dicts instead of spending (:19). Every classifier system prompt lives in one module, four of them assembled by concatenating a shared appendix (packages/atif-analytics/src/atif_analytics/application/prompts.py:1006), and the pydantic v2 response schemas they bind against are pure domain models whose field descriptions are themselves part of the prompt surface (packages/atif-analytics/src/atif_analytics/domain/models.py:3). This is the one member permitted to import a sibling — atif-models and nothing else (pyproject.toml:520).

register(con, corpus_root) binds a DuckDB connection to a contract-shaped corpus tree and exposes 16 views plus 9 macros (packages/atif-duck/src/atif_duck/__init__.py:9, packages/atif-duck/src/atif_duck/infrastructure/registry.py:1212). Those names are not introspected at runtime: a static catalog answers atif-sql schema in under 50 ms with no DuckDB bind, and two drift tests assert it column-for-column against DESCRIBE and signature-for-signature against the DDL (packages/atif-duck/src/atif_duck/domain/catalog.py:3). The 12 analytics views and 13 analytics macros register separately, each only when its backing parquet is populated, because a corpus with no atif-sql analyze run is the default state (packages/atif-duck/src/atif_duck/infrastructure/analytics.py:124). It is the one member with domain/ and infrastructure/ but no application/, a deliberate shape that is why it carries no layers contract among the seven (pyproject.toml:462).

The composition root: it declares five siblings as ==-pinned dependencies (packages/atif-cli/pyproject.toml:32) and wires every cross-package seam — the ConverterPort adapter, the clock, version pins, the DuckDB connection (packages/atif-cli/src/atif_cli/app.py:5). Ten commands hang off one cyclopts App: nine @app.command functions from convert (packages/atif-cli/src/atif_cli/app.py:226) to schema (:1055), plus the cron sub-app registered at :65, with main (:1093) exposed as the single console script named atif-sql (packages/atif-cli/pyproject.toml:42). Heavy imports — duckdb, harbor through atif-converter, pydantic through atif-corpus — are deferred into the command bodies that use them so schema, --help, and --version stay on a lean import graph that a fresh-interpreter test pins (packages/atif-cli/src/atif_cli/app.py:22). Failures resolve to stable exit codes, 64 for parse, 65 for catalog, 70 for runtime, split between a pure taxonomy module (packages/atif-cli/src/atif_cli/errors.py:25) and a driver-dependent classifier (packages/atif-cli/src/atif_cli/duck_errors.py:34) so the lean path never imports duckdb.

The vector-search write path: Cohere Embed v4 on Bedrock behind EmbeddingProvider, a local LanceDB store behind VectorStorePort, and a corpus reader behind TextRowsPort (packages/atif-embed/src/atif_embed/domain/ports.py:31). run_backfill anti-joins step text against the store’s uuid-to-text-hash map, embeds the misses, and bounds loss three ways — chunked discovery, mid-run checkpoints, and per-batch isolation inside a chunk (packages/atif-embed/src/atif_embed/application/embed.py:61). Two stamps keep the vector space honest: model_id and dimension on every row, checked on both the write and the read path so a provider switch fails loud instead of corrupting kNN (packages/atif-embed/src/atif_embed/domain/embedding_guard.py:3), and text_hash of the exact text a row was built from, so a re-conversion under a stable uuid reads as stale (packages/atif-embed/src/atif_embed/domain/text_stamp.py:3). Additive schema columns migrate online through a metadata-only add_columns, keyed on a SCHEMA_VERSION sidecar file (packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:61).

One materialization pass is sweep, scan, plan, convert, write, advance watermark, and materialize is that pass (packages/atif-corpus/src/atif_corpus/application/materialize.py:476). The decision half is pure — the domain never stats a file and never reads a clock, so build_plan partitions scanned sessions into to-materialize, up-to-date, and skipped-live deterministically from mtimes in epoch nanoseconds plus an injected now (packages/atif-corpus/src/atif_corpus/domain/sessions.py:159). The write half is atomic by construction: artifacts land in a temp session directory that is renamed into place, and every writer fsyncs the tmp file before the rename and the directory after, because atif-duck reads the corpus with no locks and no journal (packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py:5). Conversion arrives through the ConverterPort Protocol, typed to the contract’s artifact shapes rather than converter internals since this member may never import atif-converter (packages/atif-corpus/src/atif_corpus/domain/ports.py:41). What counts as a transcript, and how deep under the source root it sits, is one value object per agent — depth 1 for Claude Code’s <project>/<session>.jsonl, depth 3 for Codex’s <YYYY>/<MM>/<DD> nesting (packages/atif-corpus/src/atif_corpus/domain/source_layout.py:119) — so the scanner walks either layout without branching on the agent, and layout_for refuses an agent that has none (:131). The agent enum itself is an AST-pinned twin of the converter’s, since the independence contract forbids the import (packages/atif-corpus/src/atif_corpus/domain/agents.py:25).

convert_and_audit returns a conversion result paired with a loss report — the trajectory plus an accounting of what upstream dropped (packages/atif-converter/src/atif_converter/application/convert_and_audit.py:105). The conversion is ours: convert_claude_code_records, a port of harbor 0.22.0’s Claude Code converter built on the public ATIF data classes (packages/atif-converter/src/atif_converter/domain/claude_code_conversion.py:75), reached through the file-reading seam at packages/atif-converter/src/atif_converter/infrastructure/claude_code_converter.py:73 and validated with harbor’s public TrajectoryValidator (packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:68). The Codex path is the same shape one module over: convert_codex_and_audit (packages/atif-converter/src/atif_converter/application/convert_codex.py:128) over convert_codex_records (packages/atif-converter/src/atif_converter/domain/codex_conversion.py:781), one rollout in and one trajectory out by construction. harbor’s own private converters are the parity ORACLE, reached from the tests only (packages/atif-converter/tests/harbor_oracle.py:94, :111), frozen to goldens and diffed against the live corpus; that is what lets the dependency widen to harbor>=0.22.0,<1 (packages/atif-converter/pyproject.toml:26). The known conversion gaps are types rather than prose — FidelityGap enumerates them (packages/atif-converter/src/atif_converter/domain/fidelity.py:44) and a pure enrichment pass repairs three by re-running harbor’s deterministic normalization order over the raw records (packages/atif-converter/src/atif_converter/domain/enrichment.py:198). Codex has its own seven (packages/atif-converter/src/atif_converter/domain/codex_fidelity.py:64), all values namespaced codex_* so one gaps_observed array can carry either agent’s, and its own enrichment pass, which attributes agent steps by a re-derived api_call_id rather than by message text because harbor drops empty text parts and an empty assistant message can never be placed by matching (packages/atif-converter/src/atif_converter/domain/codex_enrichment.py). Each source file is read once, fingerprinted and parsed in the same pass, and the converter and the audit consume that one list of records; the fingerprints are re-checked once the artifacts are built, so a session that resumes writing mid-conversion fails instead of publishing artifacts for bytes it no longer holds (packages/atif-converter/src/atif_converter/infrastructure/raw_records.py, load_session and mutated_files). Per-step cost estimates come from pricing.cost_per_token (packages/atif-converter/src/atif_converter/domain/pricing.py:628), which reads litellm’s bundled price table without importing litellm and reproduces litellm.cost_per_token’s floats exactly, falling back to litellm for any shape it does not cover; import litellm used to be four of the five seconds a large session took.

A deliberately narrow seam: a system prompt, a user prompt, and a pydantic schema in; a validated instance of that schema out (packages/atif-models/src/atif_models/domain/ports.py:116). Its only in-repo consumer is atif-analytics — the single edge the forbidden contract leaves open (pyproject.toml:520) — and the registry is the only place in the workspace where a Bedrock model id is written down, so a pipeline names a family and a size alias and lets resolve pick the id (packages/atif-models/src/atif_models/domain/registry.py:6, :109). The default adapter posts an OpenAI chat-completions body to invoke_model in strict json_schema mode, dispatching the blocking call through anyio.to_thread under a capacity limiter with tenacity owning the retry loop and botocore’s own retries disabled (packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:142). Strict mode demands additionalProperties: false at every object level and every property listed in required, which one pure transform enforces before the schema reaches the wire (packages/atif-models/src/atif_models/domain/schema.py:40).