Skip to content

Module map

The uv workspace members live under packages/*, declared at pyproject.toml:110. The root is both the workspace and the one published distribution: its [project] table builds a single atif-sql wheel carrying all seven module trees, its dependency list is the union of the members’ third-party requirements, and it also holds the shared dev dependency-group and the shared tool config (pyproject.toml:1, :16, :34). The internal import graph is a star: atif-cli declares five of its six siblings as ==-pinned dependencies (packages/atif-cli/pyproject.toml:32-36) and reaches atif-models only through atif-analytics, plus one further edge from atif-analytics to atif-models that a forbidden import-linter contract leaves open while it closes every other atif-analytics edge (pyproject.toml:611), with every pair among the other five members closed by an independence contract (pyproject.toml:606). Outside atif-cli’s adapter modules, which atif_cli.app imports only inside command bodies, 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, boto3, or polars, which is why PLC0415 is ignored workspace-wide (pyproject.toml:225).

run_analyze composes the LLM pipelines in a fixed stage order: classify, conflicts, friction, perceived (packages/atif-analytics/src/atif_analytics/application/analyze.py:37). Every stage calls a model and honors dry_run, which defaults to True as a cost guard so the stages return plan dicts instead of spending (:13); each plan turns a measured prompt length into tokens at the resolved model’s own chars_per_token (packages/atif-analytics/src/atif_analytics/domain/costs.py:28). Every classifier system prompt lives in one module, each assembled by joining a task body to shared sections (packages/atif-analytics/src/atif_analytics/application/prompts.py:714-723), and the same text goes to both model families. The user message comes from the same module: the transcript, or friction’s one message, fenced in a tag pair whose id is a digest of the text, then the one-line request (:742, :753). 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). Who wrote a user step comes from atif_analytics.domain.authorship, a pinned twin of atif-duck’s rule table (packages/atif-analytics/src/atif_analytics/domain/authorship.py). Apart from atif-cli, this is the one member permitted to import a sibling: atif-models and nothing else (pyproject.toml:611).

register(con, corpus_root) binds a DuckDB connection to a contract-shaped corpus tree and exposes the core views and macros (packages/atif-duck/src/atif_duck/domain/catalog.py:30, packages/atif-duck/src/atif_duck/infrastructure/registry.py:2185). 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 analytics views and 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:119). It has domain/ and infrastructure/ but no application/, and unlike atif-models, the other member with that shape, it carries no layers contract (pyproject.toml:553).

  • packages/atif-duck/src/atif_duck/infrastructure/registry.py: the raw readers (packages/atif-duck/src/atif_duck/infrastructure/registry.py:637), most of the core views (:1024) and macros (:1946), and the Lance attach path (:1642).
  • packages/atif-duck/src/atif_duck/domain/raw_readers.py: the raw relation names and column shapes the registry binds and the lake stores, so neither declares them twice.
  • packages/atif-duck/src/atif_duck/domain/lake.py: the lake’s tables, partition specs and every statement the lake runs, built as module constants from the raw reader shapes, plus the schema version and digest that decide when a lake is stale.
  • packages/atif-duck/src/atif_duck/infrastructure/lake.py: DuckLakeSessionSink (the SessionSink implementation materialize writes through), the published read-only catalog copy, the query-side attach and fallback, and rebuild_lake, verify_lake, lake_status and compact_lake.
  • packages/atif-duck/src/atif_duck/domain/artifacts.py and packages/atif-duck/src/atif_duck/infrastructure/stored_artifacts.py: which artifacts may be stored as <name>.zst, and how a reader finds a session’s stored file (compressed first), reads it, and sizes it from its zstd frame header.
  • packages/atif-duck/src/atif_duck/infrastructure/lake_steps.py: one corpus’s step texts and primary uuids read from the published lake for the embedding store: the full read, the read of the uuids ducklake_table_changes names after a snapshot, and the corpus’s lineage and snapshot range.
  • packages/atif-duck/src/atif_duck/infrastructure/lake_settings.py: LakeSettings, the ATIF_SQL_-prefixed lake root, corpus base, batch sizes, stage worker count, lock timeout and snapshot expiry.
  • packages/atif-duck/src/atif_duck/domain/catalog.py: VIEW_NAMES (packages/atif-duck/src/atif_duck/domain/catalog.py:30), MACRO_SIGNATURES (:343), the analytics catalogs (:370-462), and one DESCRIPTIONS entry per object (:487).
  • packages/atif-duck/src/atif_duck/infrastructure/analytics.py: the v2 views and macros over the analytics parquet outputs, gated on file presence (packages/atif-duck/src/atif_duck/infrastructure/analytics.py:119).
  • packages/atif-duck/src/atif_duck/domain/examples.py: build_examples derives one runnable query per catalog object from ARG_EXEMPLARS rather than hardcoding strings (packages/atif-duck/src/atif_duck/domain/examples.py:191).
  • packages/atif-duck/src/atif_duck/domain/authorship.py: the step_author rule table: prefix matches after stripping leading whitespace, first rule wins, twinned in atif-analytics (packages/atif-duck/src/atif_duck/domain/authorship.py:51).
  • packages/atif-duck/src/atif_duck/infrastructure/authorship.py: the step_author macro and the user_steps, human_turns, and session_outcomes views, registered after the core macros and before the analytics surface (packages/atif-duck/src/atif_duck/infrastructure/authorship.py:38).
  • packages/atif-duck/src/atif_duck/domain/embedding_guard.py: the read-side provider and dimension guard, a deliberate twin of atif-embed’s copy because the independence contract forbids sharing it (packages/atif-duck/src/atif_duck/domain/embedding_guard.py:5).
  • packages/atif-duck/src/atif_duck/__init__.py: re-exports the catalog constants and the register* entry points (packages/atif-duck/src/atif_duck/__init__.py:17-30).
  • packages/atif-duck/src/atif_duck/domain/__init__.py: the pure catalog layer, no duckdb import (packages/atif-duck/src/atif_duck/domain/__init__.py:5).
  • packages/atif-duck/src/atif_duck/infrastructure/__init__.py: the infrastructure layer’s re-export surface, the registry entry points plus the columnar producer (packages/atif-duck/src/atif_duck/infrastructure/__init__.py:10-22).

The composition root: it declares five of its six siblings as ==-pinned dependencies (packages/atif-cli/pyproject.toml:32-36) and wires every cross-package seam: the ConverterPort adapter, the clock, version pins, the DuckDB connection (packages/atif-cli/src/atif_cli/app.py:7-9). The commands hang off one cyclopts App: @app.command functions from convert (packages/atif-cli/src/atif_cli/app.py:690) to schema (:2464), plus the cron, lake, and corpus sub-apps registered at :104-109, with main (:2538) exposed as the single console script named atif-sql (pyproject.toml:74). Heavy imports (duckdb, the ATIF models 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:50-53). 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:30) and a driver-dependent classifier (packages/atif-cli/src/atif_cli/duck_errors.py:35) so the lean path never imports duckdb.

  • packages/atif-cli/src/atif_cli/app.py: the commands and every wiring decision between them (packages/atif-cli/src/atif_cli/app.py:3).
  • packages/atif-cli/src/atif_cli/lake.py: the lake sub-app: rebuild, verify, status and compact, each deferring atif-duck’s imports into the command body.
  • packages/atif-cli/src/atif_cli/corpus.py: the corpus sub-app: slim converts an old-layout corpus to the compressed layout, and storage_layout is the scan status reports.
  • packages/atif-cli/src/atif_cli/embed_lake.py: DuckLakeSteps, atif-embed’s LakeStepsPort implemented over atif-duck’s lake_steps, the one place the two meet.
  • packages/atif-cli/src/atif_cli/cron.py: cron install prints a crontab block and never writes one; cron status reports per-lane lock and last-run state from injected probes (packages/atif-cli/src/atif_cli/cron.py:7).
  • packages/atif-cli/src/atif_cli/output.py: --format auto resolves to a table on a TTY and JSON on a pipe (packages/atif-cli/src/atif_cli/output.py:84); emit_cursor streams fetchmany batches so the client holds one batch (:158).
  • packages/atif-cli/src/atif_cli/duck_errors.py: the duckdb exception classifier, plus a widened caught set for the registration path since EmbeddingProviderMismatch is not a duckdb.Error (packages/atif-cli/src/atif_cli/duck_errors.py:32).
  • packages/atif-cli/src/atif_cli/converter_adapter.py: RealConverter adapts atif-converter’s two use cases, one per agent, to atif-corpus’s port, the one place allowed to import both (packages/atif-cli/src/atif_cli/converter_adapter.py:80).
  • packages/atif-cli/src/atif_cli/errors.py: EXIT_CODES and the ClassifiedError shape, pure and free of any atif_* import (packages/atif-cli/src/atif_cli/errors.py:30, :70).
  • packages/atif-cli/src/atif_cli/__main__.py: python -m atif_cli reaching the same main (packages/atif-cli/src/atif_cli/__main__.py:5).
  • packages/atif-cli/src/atif_cli/__init__.py: package docstring only (packages/atif-cli/src/atif_cli/__init__.py:3).

The vector-search write path: an embedder behind EmbeddingProvider (Cohere Embed v4 on Bedrock by default, or EmbeddingGemma 2 on this machine), a local Lance store written through DuckDB’s lance extension behind VectorStorePort, and a corpus reader behind TextRowsPort (packages/atif-embed/src/atif_embed/domain/ports.py:37). run_backfill anti-joins step text against the store’s uuid-to-text-hash map, embeds the misses, and bounds loss in layers: chunked discovery, mid-run checkpoints, and per-batch isolation inside a chunk (packages/atif-embed/src/atif_embed/application/embed.py:63). 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 ALTER TABLE ... ADD COLUMN ... DEFAULT when a column is missing (packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:365), and the store records its SCHEMA_VERSION in a sidecar file (:89). Discovery reads the lake’s steps through LakeStepsPort when a lake holds the corpus, only the rows changed since a watermark kept in the store directory (packages/atif-embed/src/atif_embed/infrastructure/lake_text_rows.py), and falls back to the per-session reader otherwise.

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:1497). 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:169). 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:101). The SessionSink Protocol in the same module is the other way out: after the swaps, the pass hands it every session it published or marked, and records the ones it hasn’t taken yet in sink_pending.json so a failed or killed hand-over is retried by the next pass. 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:111-124), so the scanner walks either layout without branching on the agent, and layout_for refuses an agent that has none (:141-143). 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:220). The conversion is ours: convert_claude_code_records, a port of harbor 0.24.0’s Claude Code converter built on the public ATIF data classes (packages/atif-converter/src/atif_converter/domain/claude_code_conversion.py:981), reached through convert_loaded_claude_code_session, which takes the records the use case already read (packages/atif-converter/src/atif_converter/infrastructure/claude_code_converter.py:72), and validated with the vendored copy of harbor’s TrajectoryValidator (packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:76). The Codex path is the same shape one module over: convert_codex_and_audit (packages/atif-converter/src/atif_converter/application/convert_codex.py:144) over convert_codex_records (packages/atif-converter/src/atif_converter/domain/codex_conversion.py:924), 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:118, :135), frozen to goldens and diffed against the live corpus. harbor is a dev dependency only: the ATIF data classes and validator the converters build on are vendored from harbor 0.24.0 in packages/atif-converter/src/atif_converter/domain/atif/ and held to upstream by packages/atif-converter/tests/test_vendored_atif.py. The known conversion gaps are types rather than prose: FidelityGap enumerates them (packages/atif-converter/src/atif_converter/domain/fidelity.py:55) and a pure enrichment pass repairs some of them 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 set (packages/atif-converter/src/atif_converter/domain/codex_fidelity.py:66), 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.priced_cost_per_token on the Claude Code path and pricing.cost_per_token on the Codex path (packages/atif-converter/src/atif_converter/domain/pricing.py), which read the vendored model_prices.json (a filtered copy of litellm’s public price data, regenerated by scripts/update_prices.py) and reproduce litellm.cost_per_token’s floats exactly; a model the table doesn’t price is NULL. litellm is a dev dependency, for the identity test only.

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:131). Its only in-repo consumer is atif-analytics, the single edge the forbidden contract leaves open (pyproject.toml:611), 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, :162). Small, medium, and large resolve to the GPT-6 models (gpt-6-luna, gpt-6.1-sol, gpt-6-astra) in the openai family, the default, and to Claude Haiku 5.5, Sonnet 5.5, and Opus 5.5 in the anthropic family (:95). Both families run: build_structured_provider picks the adapter by the spec’s family and imports only that adapter’s module (packages/atif-models/src/atif_models/infrastructure/providers.py:20). The openai adapter posts an OpenAI chat-completions body to invoke_model in strict json_schema mode (packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:81). The anthropic adapter posts a Messages body whose output_config.format carries the schema, after the Anthropic SDK’s transform_schema moves the constraints Claude rejects into the field descriptions (packages/atif-models/src/atif_models/infrastructure/anthropic_bedrock.py:172). Both dispatch the blocking call through anyio.to_thread under a capacity limiter, with tenacity owning the retry loop and botocore’s own retries disabled in the client they build the same way (packages/atif-models/src/atif_models/infrastructure/bedrock_common.py:88). OpenAI 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).