atif-sql · Module map
Section titled “atif-sql · 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).
atif-analytics
Section titled “atif-analytics”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).
packages/atif-analytics/src/atif_analytics/application/prompts.py: the task-framing system prompts, public constants assembled atpackages/atif-analytics/src/atif_analytics/application/prompts.py:714-723, the request lines (:726-729), and the two user-message builders,transcript_prompt(:742) andmessage_prompt(:753).packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py: the friction tiers behind a human-turn and message-length pre-filter: regex fast path, deterministic stamp rules, then the LLM (packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:5).packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py: the LangSmith Perceived Error definition on the conflicts chassis; clean sessions produce zero rows (packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:16).packages/atif-analytics/src/atif_analytics/application/use_cases/conflicts.py: one row per detected stance-conflict pair, keyed on two turn uuids, with refusals routed to a sidecar; non-interactive sessions are skipped (packages/atif-analytics/src/atif_analytics/application/use_cases/conflicts.py:11).packages/atif-analytics/src/atif_analytics/application/use_cases/classify.py: one row per interactive session carryingwork_categoryandgoal, anti-joined against the parquet cache and written in crash-resilient chunks (packages/atif-analytics/src/atif_analytics/application/use_cases/classify.py:3).packages/atif-analytics/src/atif_analytics/domain/transcript.py:render_session_textrenders the transcript a stage sends, each tool result as a one-line stub with the content’s length, so what a tool returned stays on disk and never reaches Bedrock (packages/atif-analytics/src/atif_analytics/domain/transcript.py:247).ATIF_SQL_SESSION_TEXT_TOOL_RESULT_BODIES=trueputs the bodies back, each clipped at 50,000 characters (packages/atif-analytics/src/atif_analytics/infrastructure/settings.py:94,:90).packages/atif-analytics/src/atif_analytics/domain/models.py: the response schemas, fromSessionClassification(packages/atif-analytics/src/atif_analytics/domain/models.py:22) toPerceivedErrorsResult(:355).
atif-duck
Section titled “atif-duck”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(theSessionSinkimplementation materialize writes through), the published read-only catalog copy, the query-side attach and fallback, andrebuild_lake,verify_lake,lake_statusandcompact_lake.packages/atif-duck/src/atif_duck/domain/artifacts.pyandpackages/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 uuidsducklake_table_changesnames after a snapshot, and the corpus’s lineage and snapshot range.packages/atif-duck/src/atif_duck/infrastructure/lake_settings.py:LakeSettings, theATIF_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 oneDESCRIPTIONSentry 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_examplesderives one runnable query per catalog object fromARG_EXEMPLARSrather than hardcoding strings (packages/atif-duck/src/atif_duck/domain/examples.py:191).packages/atif-duck/src/atif_duck/domain/authorship.py: thestep_authorrule 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: thestep_authormacro and theuser_steps,human_turns, andsession_outcomesviews, 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 theregister*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).
atif-cli
Section titled “atif-cli”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: thelakesub-app:rebuild,verify,statusandcompact, each deferring atif-duck’s imports into the command body.packages/atif-cli/src/atif_cli/corpus.py: thecorpussub-app:slimconverts an old-layout corpus to the compressed layout, andstorage_layoutis the scanstatusreports.packages/atif-cli/src/atif_cli/embed_lake.py:DuckLakeSteps, atif-embed’sLakeStepsPortimplemented over atif-duck’slake_steps, the one place the two meet.packages/atif-cli/src/atif_cli/cron.py:cron installprints a crontab block and never writes one;cron statusreports 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 autoresolves to a table on a TTY and JSON on a pipe (packages/atif-cli/src/atif_cli/output.py:84);emit_cursorstreamsfetchmanybatches 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 sinceEmbeddingProviderMismatchis not aduckdb.Error(packages/atif-cli/src/atif_cli/duck_errors.py:32).packages/atif-cli/src/atif_cli/converter_adapter.py:RealConverteradapts 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_CODESand theClassifiedErrorshape, pure and free of anyatif_*import (packages/atif-cli/src/atif_cli/errors.py:30,:70).packages/atif-cli/src/atif_cli/__main__.py:python -m atif_clireaching the samemain(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).
atif-embed
Section titled “atif-embed”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.
packages/atif-embed/src/atif_embed/infrastructure/lance_store.py: a bounded DuckDB connection with the lance extension loaded and the store directory attached (packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:240), then open or create, online-migrate, delete by uuid, append, and compact plus vacuum, all as SQL (packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:597).packages/atif-embed/src/atif_embed/infrastructure/cohere_bedrock.py: theinvoke_modeladapter under a tenacity retry with botocore retries off, and the document-int8/ query-floatasymmetry (packages/atif-embed/src/atif_embed/infrastructure/cohere_bedrock.py:285).packages/atif-embed/src/atif_embed/application/embed.py:discover_unembedded(packages/atif-embed/src/atif_embed/application/embed.py:45),run_backfill(:63), and the syncembed_querythe search command calls (:286).packages/atif-embed/src/atif_embed/infrastructure/corpus_text_rows.py: reads the contract corpus layout with its own DuckDB connection, since importing atif-duck is forbidden (packages/atif-embed/src/atif_embed/infrastructure/corpus_text_rows.py:229).packages/atif-embed/src/atif_embed/domain/ports.py: the port Protocols:EmbeddingProvider(packages/atif-embed/src/atif_embed/domain/ports.py:37),VectorStorePort(:65),TextRowsPort(:89), andLakeStepsPort(:198).packages/atif-embed/src/atif_embed/domain/embedding_guard.py:ensure_store_matches, the fail-loud provider and dimension rule (packages/atif-embed/src/atif_embed/domain/embedding_guard.py:38).packages/atif-embed/src/atif_embed/domain/errors.py: theDomainErrortaxonomy the CLI catches, withterminalseparating operator-needed states from retryable ones (packages/atif-embed/src/atif_embed/domain/errors.py:14).packages/atif-embed/src/atif_embed/domain/text_stamp.py:text_hash(packages/atif-embed/src/atif_embed/domain/text_stamp.py:38) and theMAX_EMBEDDABLE_CHARShead-only cap (:35).
atif-corpus
Section titled “atif-corpus”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).
packages/atif-corpus/src/atif_corpus/application/materialize.py: the pass itself (packages/atif-corpus/src/atif_corpus/application/materialize.py:1497), the report value object (:220), and the publicread_watermarkits consumers call (:424).packages/atif-corpus/src/atif_corpus/infrastructure/scanner.py: the only place this member stats the source corpus (packages/atif-corpus/src/atif_corpus/infrastructure/scanner.py:5);SourceScankeeps a genuinely absent transcript separate from a failedstat, so a transient error never marks a live session as source-removed (:67).packages/atif-corpus/src/atif_corpus/domain/sessions.py:SessionSource(packages/atif-corpus/src/atif_corpus/domain/sessions.py:42),QuiescencePolicy(:71),MaterializationPlan(:118), andbuild_plan(:169).packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py: tmp-sibling write, fsync, rename, for files (plain and zstd-compressed) and whole directories.packages/atif-corpus/src/atif_corpus/infrastructure/compress_artifacts.py: compresses an old-layout session’s plain JSON artifacts in place forcorpus slim, reading each copy back before the plain file is removed and keeping its mtime.packages/atif-corpus/src/atif_corpus/domain/watermark.py:diff_source_mtimespartitions two mtime maps into added, modified, and removed (packages/atif-corpus/src/atif_corpus/domain/watermark.py:57).packages/atif-corpus/src/atif_corpus/domain/layout.py:CorpusLayoutis the single writer-side computation of every contract path (packages/atif-corpus/src/atif_corpus/domain/layout.py:65).packages/atif-corpus/src/atif_corpus/infrastructure/fake_converter.py: a scriptableConverterPortfake shipped ininfrastructureso the type checker holds it to the port on every run (packages/atif-corpus/src/atif_corpus/infrastructure/fake_converter.py:3).packages/atif-corpus/src/atif_corpus/infrastructure/settings.py:ATIF_SQL_-prefixed settings whose default factories read env at call time, not import time (packages/atif-corpus/src/atif_corpus/infrastructure/settings.py:7-8).
atif-converter
Section titled “atif-converter”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.
packages/atif-converter/src/atif_converter/domain/pricing.py:fast_cost_per_tokenprices the covered model shapes from the vendoredmodel_prices.jsonbeside it;cost_per_tokenraisesUnpriceableModelErrorfor anything else,priced_cost_per_tokenturns that intoNone, andhas_pricing_entryis Codex’s table lookup.packages/atif-converter/src/atif_converter/domain/enrichment.py:enrich_trajectoryrestores source uuids, the compact-summary flag, and the cache-creation total (packages/atif-converter/src/atif_converter/domain/enrichment.py:198).packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py: the Claude Code conversion seam, which keeps its historical name:read_sessionreads a session once,convert_loaded_sessionconverts those records, andvalidate_trajectoryruns the vendoredTrajectoryValidator; harbor itself isn’t a runtime dependency, and no module undersrc/imports it (packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:5-7).packages/atif-converter/src/atif_converter/infrastructure/raw_records.py: the one raw reader the census, edges emitter, and enrichment share, withmutated_filesas the race check (packages/atif-converter/src/atif_converter/infrastructure/raw_records.py:320,:382).packages/atif-converter/src/atif_converter/application/convert_and_audit.py: the use case (packages/atif-converter/src/atif_converter/application/convert_and_audit.py:220) and its refusal on any mid-window mutation (:199).packages/atif-converter/src/atif_converter/domain/fidelity.py:RecordType(packages/atif-converter/src/atif_converter/domain/fidelity.py:26),FidelityGap(:55), and theLossReportvalue object (:114).packages/atif-converter/src/atif_converter/domain/edges.py: oneedges.jsonlline per raw record, key order fixed by the contract (packages/atif-converter/src/atif_converter/domain/edges.py:22).packages/atif-converter/src/atif_converter/infrastructure/census.py: the raw-side counts that form the input half of a loss report, derived from an already-parsed snapshot (packages/atif-converter/src/atif_converter/infrastructure/census.py:57).packages/atif-converter/src/atif_converter/domain/errors.py: the typed exceptions under oneDomainErrorbase, with exit-code mapping left to the CLI (packages/atif-converter/src/atif_converter/domain/errors.py:14).
atif-models
Section titled “atif-models”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).
packages/atif-models/src/atif_models/infrastructure/anthropic_bedrock.py:AnthropicBedrockProvider(packages/atif-models/src/atif_models/infrastructure/anthropic_bedrock.py:130), which joins thetextblocks that follow anythinkingblock, folds a string enum value that differs from the schema only in letter case onto the declared one (fold_enum_case,:102), raisesRefusalErroronstop_reason: refusal(:253-257), and retries amax_tokensstop once at one effort step lower (:200-219); usage extraction (:81).packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:OpenAiBedrockProvider(packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:81), which retries alengthfinish once at one effort step lower (:165-177), and usage extraction (:63).packages/atif-models/src/atif_models/domain/registry.py:ModelSpec(packages/atif-models/src/atif_models/domain/registry.py:50), whose long-context threshold and rates price a request over the threshold at the long rates and whosechars_per_tokenis the family’s measured characters per token on rendered transcripts;resolve(:162),is_long_context(:175), andestimate_costover per-1M-token rates, which bills the long-context share of the tokens at the long rates (:180).packages/atif-models/src/atif_models/domain/ports.py: theLlmStructuredProviderProtocol (packages/atif-models/src/atif_models/domain/ports.py:131), the terminal-versus-retryable error split (:43-58), and lock-guarded usage accumulation that keeps the long-context share apart (:81).packages/atif-models/src/atif_models/domain/schema.py:to_openai_strictkeeps$defsand$ref, dropsdefaultandtitle(packages/atif-models/src/atif_models/domain/schema.py:40).packages/atif-models/src/atif_models/infrastructure/bedrock_common.py: what both adapters share: the retryable-code set (packages/atif-models/src/atif_models/infrastructure/bedrock_common.py:31) and its predicateis_retryable(:66), the one-step effort ladder for a truncated response (:50), theLengthTruncationmarker (:58), the backoff log line (:76), andbuild_runtime_client(:88).packages/atif-models/src/atif_models/infrastructure/settings.py:LlmSettingscarries the family, region, concurrency, and per-pipeline size overrides (packages/atif-models/src/atif_models/infrastructure/settings.py:28), and settings load refuses a family outsideRUNNABLE_FAMILIES, openai and anthropic (:25).packages/atif-models/src/atif_models/infrastructure/providers.py:build_structured_provider, the factory atif-analytics calls: anthropic getsAnthropicBedrockProviderand openai getsOpenAiBedrockProvider, each module imported only when its family is asked for (packages/atif-models/src/atif_models/infrastructure/providers.py:20).packages/atif-models/src/atif_models/__init__.py: states the ownership rule: no other member hardcodes a model id (packages/atif-models/src/atif_models/__init__.py:9).packages/atif-models/src/atif_models/domain/__init__.py: the pure layer: no boto3, no env, no clock (packages/atif-models/src/atif_models/domain/__init__.py:7).packages/atif-models/src/atif_models/infrastructure/__init__.py: the adapters, the factory, and env settings, importing downward intodomainonly (packages/atif-models/src/atif_models/infrastructure/__init__.py:5-8).