Skip to content

Business logic

This file indexes the domain rules atif-sql enforces: input validations, invariants the code holds across a boundary, derived-value calculations, and the policy gates that decide whether work runs at all.

Scope. Application-layer and domain-layer rules across the seven workspace members, plus the SQL surface atif-duck registers into DuckDB. There is no database server, no migration directory, and no HTTP surface in this repo, so there are no DDL constraints or request-validation middlewares to survey — the DuckDB views and macros are the closest thing to a “schema”, and the rules encoded in their DDL are in scope and captured under Calculations and Invariants. LLM-output schemas count as validations here, because the provider adapter re-validates every response with pydantic before it reaches a parquet row. Ruff/pyright/import-linter rules are toolchain policy, not domain logic, and are out of scope.

Test provenance. Where a rule is pinned by a test, the test is cited beside the implementation. A row with no test citation is a rule read out of the implementation and not covered by a named test — that difference is stated, never smoothed over.

Units. Every *_ns value is epoch nanoseconds from os.stat().st_mtime_ns; every *_chars value counts Python string characters, not bytes or tokens; pricing is USD per 1,000,000 tokens; backoff is minutes and tenacity waits are seconds. Scope (per session, per run, per pipeline) is stated per row.

Rule Domain Citation Failure mode
A session path must end in .jsonl AND be an existing file Conversion packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:163-165; test packages/atif-converter/tests/test_convert_and_audit.py:224 raise InvalidSessionInput; CLI exits 64 (packages/atif-cli/src/atif_cli/app.py:262-264)
harbor’s private _convert_events_to_trajectory must exist and be callable Conversion packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:112-139; tests packages/atif-converter/tests/test_snapshot_and_drift.py:296 and packages/atif-converter/tests/test_snapshot_and_drift.py:319 raise HarborPrivateApiMissing; CLI exits 127 (packages/atif-cli/src/atif_cli/errors.py:38)
harbor must return a trajectory for the session Conversion packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:187 raise EmptySessionError; CLI exits 2 (packages/atif-cli/src/atif_cli/app.py:266-267)
The trajectory must pass harbor’s TrajectoryValidator AFTER enrichment mutates extra Conversion packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:97-102, packages/atif-converter/src/atif_converter/application/convert_and_audit.py:150-154; test packages/atif-converter/tests/test_enrichment.py:472 errors collected onto ConversionResult.validation_errors; convert exits 65 when non-empty (packages/atif-cli/src/atif_cli/app.py:303-304)
No source file may change, vanish, or appear between the pre-read snapshot and the end of the raw parse Conversion packages/atif-converter/src/atif_converter/application/convert_and_audit.py:93-102, checked twice at packages/atif-converter/src/atif_converter/application/convert_and_audit.py:135 and packages/atif-converter/src/atif_converter/application/convert_and_audit.py:148; tests packages/atif-converter/tests/test_snapshot_and_drift.py:193, packages/atif-converter/tests/test_snapshot_and_drift.py:217, packages/atif-converter/tests/test_snapshot_and_drift.py:274 raise SourceMutatedDuringConversion; the materialize pass records the session as failed and continues
A JSONL line that is not valid JSON, or parses to a non-dict, is not a record Conversion packages/atif-converter/src/atif_converter/infrastructure/raw_records.py:136-142 silent drop, DEBUG log (same policy as harbor)
An agent step and an assistant-record group pair up only when their tool ids intersect, or when BOTH sides carry no tool ids Conversion packages/atif-converter/src/atif_converter/domain/enrichment.py:304-318; test packages/atif-converter/tests/test_enrichment.py:331 refuse: attribution stops at that step, enrichment_truncated_at_step recorded, WARNING logged
A user step and a user text record pair up only while the record’s harbor-derived text equals the step’s message Conversion packages/atif-converter/src/atif_converter/domain/enrichment.py:347-360; test packages/atif-converter/tests/test_enrichment.py:372 refuse: same truncation marker, WARNING logged
A session materializes only once its newest source mtime is at least quiesce_seconds old (default 300 seconds of source silence, per session) Corpus packages/atif-corpus/src/atif_corpus/domain/sessions.py:83-104 and packages/atif-corpus/src/atif_corpus/domain/sessions.py:201-202; tests packages/atif-corpus/tests/test_domain.py:40 (inclusive boundary) and packages/atif-corpus/tests/test_domain.py:98 deferred into skipped_live; revisited next pass
A source mtime in the FUTURE never satisfies the quiescence threshold Corpus packages/atif-corpus/src/atif_corpus/domain/sessions.py:96-104; test packages/atif-corpus/tests/test_domain.py:45 stays skipped, plus a WARNING every pass so the starvation is visible
watermark.json must parse as a mapping of path to int Corpus packages/atif-corpus/src/atif_corpus/application/materialize.py:160-176 degrade to empty (one full re-materialization pass), WARNING logged — never a refusal to sync
A scan finding zero sessions while the corpus holds materialized ones is not a deletion Corpus packages/atif-corpus/src/atif_corpus/application/materialize.py:97-104 and packages/atif-corpus/src/atif_corpus/application/materialize.py:553-559; test packages/atif-corpus/tests/test_materialize.py:292 raise SuspiciousEmptyScanError; nothing is removed
Only a genuine FileNotFoundError proves a source vanished; any other OSError means unreadable Corpus packages/atif-corpus/src/atif_corpus/infrastructure/scanner.py:83-96 and packages/atif-corpus/src/atif_corpus/infrastructure/scanner.py:176-186; test packages/atif-corpus/tests/test_materialize.py:819 session lands in SourceScan.unreadable, is never ghosted, and its watermark entries are retained
structural_only and llm_only are mutually exclusive Analytics packages/atif-analytics/src/atif_analytics/application/analyze.py:61-63 raise ValueError
A retry-queue or checkpoint pipeline must be one of the five contract names Analytics packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:96-98, packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/checkpointer.py:35-41; tests packages/atif-analytics/tests/test_state.py:145 and packages/atif-analytics/tests/test_state.py:150 raise ValueError
Every LLM-output confidence is bounded to the closed interval 0.0 to 1.0 (dimensionless, per emitted row) Analytics packages/atif-analytics/src/atif_analytics/domain/models.py:82-85, packages/atif-analytics/src/atif_analytics/domain/models.py:171-174, packages/atif-analytics/src/atif_analytics/domain/models.py:275-278, packages/atif-analytics/src/atif_analytics/domain/models.py:367-370, packages/atif-analytics/src/atif_analytics/domain/models.py:471-474 pydantic ValidationError at model_validate, translated to ProviderUnavailable (packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:307-311); the unit goes to the retry queue
Every categorical LLM-output field is a closed Literal union, and free text is length-capped (goal 280 chars, rationale 200 chars, evidence 280 chars, summary 280 chars, a turn uuid 64 chars) Analytics packages/atif-analytics/src/atif_analytics/domain/models.py:31, packages/atif-analytics/src/atif_analytics/domain/models.py:72-75, packages/atif-analytics/src/atif_analytics/domain/models.py:357-360, packages/atif-analytics/src/atif_analytics/domain/models.py:393-396, packages/atif-analytics/src/atif_analytics/domain/models.py:449-452, packages/atif-analytics/src/atif_analytics/domain/models.py:460-463 same pydantic rejection path
A returned turn_uuid must be in the session’s USER-role main-chain text-step header uuid set Analytics packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:123-134 and packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:303-312; tests packages/atif-analytics/tests/test_perceived.py:161, packages/atif-analytics/tests/test_resource_guards.py:617 row dropped with a WARNING; an EMPTY universe drops every row (fails closed)
A returned conflict pair’s two uuids must BOTH be real edges.jsonl uuids for that session Analytics packages/atif-analytics/src/atif_analytics/application/use_cases/conflicts.py:266-296; test packages/atif-analytics/tests/test_resource_guards.py:590 pair dropped; an unreadable universe drops every pair (fails closed)
An untrusted step body may not present itself as a turn header Analytics packages/atif-analytics/src/atif_analytics/domain/transcript.py:64-73; tests packages/atif-analytics/tests/test_resource_guards.py:561 and packages/atif-analytics/tests/test_resource_guards.py:576 coerce: [uuid= is rewritten to (uuid= case-insensitively, length preserved, before any header is built
A friction candidate is a user-role main-chain text step with a uuid, 1 to friction_max_chars characters (default 300, per message), CLI bookkeeping text excluded Analytics packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:154-175, packages/atif-analytics/src/atif_analytics/domain/transcript.py:81-86; test packages/atif-analytics/tests/test_friction_tiers.py:98 filtered out at the candidate boundary; no LLM call is ever made for it
ATIF_SQL_LLM_FAMILY must name a family with a wired provider adapter Models packages/atif-models/src/atif_models/infrastructure/settings.py:25 and packages/atif-models/src/atif_models/infrastructure/settings.py:41-54; tests packages/atif-models/tests/test_settings.py:59 and packages/atif-models/tests/test_settings.py:64 raise ValueError at settings load — a startup refusal instead of a Bedrock 400 on every call
size_for accepts only a pipeline with an llm_size_<pipeline> field Models packages/atif-models/src/atif_models/infrastructure/settings.py:67-77; test packages/atif-models/tests/test_settings.py:88 raise KeyError
A schema with a schema-valued additionalProperties (a dict[str, X] field) cannot be expressed in OpenAI strict mode Models packages/atif-models/src/atif_models/domain/schema.py:63-69; test packages/atif-models/tests/test_schema.py:71 raise ValueError at body-build time
finish_reason of content_filter or refusal, or a non-empty message.refusal, is a decline Models packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:77, packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:284-290; tests packages/atif-models/tests/test_openai_provider.py:182 and packages/atif-models/tests/test_openai_provider.py:190 raise RefusalError — terminal, never retried
finish_reason of length gets exactly ONE retry, at reasoning_effort degraded one step Models packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:85-90, packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:217-249, packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:291-297; tests packages/atif-models/tests/test_openai_provider.py:129, packages/atif-models/tests/test_openai_provider.py:144, packages/atif-models/tests/test_openai_provider.py:163 raise ProviderUnavailable when still truncated or already at the floor; the unit goes to the retry queue
Response content must be a non-empty string that parses as JSON and validates against the schema Models packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:298-311; tests packages/atif-models/tests/test_openai_provider.py:198, packages/atif-models/tests/test_openai_provider.py:206, packages/atif-models/tests/test_openai_provider.py:212 raise ProviderUnavailable
A store’s stamped (model, dim) must match the active embedder before any vector is read or appended Embeddings packages/atif-embed/src/atif_embed/domain/embedding_guard.py:38-80, packages/atif-embed/src/atif_embed/application/embed.py:166-174; tests packages/atif-embed/tests/test_embed_use_case.py:153, packages/atif-embed/tests/test_lance_store.py:321 and packages/atif-embed/tests/test_lance_store.py:330 raise EmbeddingProviderMismatch (terminal = True); query/search exit 65, embed exits 78
ensure_index accepts only cosine, l2, or dot Embeddings packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:414-419; test packages/atif-embed/tests/test_lance_store.py:110 raise ValueError
A text over 50,000 characters (per embeddable text) cannot be embedded whole Embeddings packages/atif-embed/src/atif_embed/domain/text_stamp.py:35, packages/atif-embed/src/atif_embed/domain/text_stamp.py:43-47, packages/atif-embed/src/atif_embed/domain/text_stamp.py:65-68; tests packages/atif-embed/tests/test_embed_use_case.py:369 and packages/atif-embed/tests/test_embed_use_case.py:386 coerce: sent head-only, and the row stamps truncated = true so the search miss is attributable
Every macro parameter name must have an ARG_EXEMPLARS entry SQL surface packages/atif-duck/src/atif_duck/domain/examples.py:58-75, packages/atif-duck/src/atif_duck/domain/examples.py:148-158; test packages/atif-duck/tests/test_examples.py:188 build_examples() raises; the drift test fails with it
Every catalog object must yield a runnable example or carry a documented EXCLUSIONS entry SQL surface packages/atif-duck/src/atif_duck/domain/examples.py:94-99; test packages/atif-duck/tests/test_examples.py:160 drift test fails
A materialized session dir with no meta.json is incomplete SQL surface packages/atif-duck/src/atif_duck/infrastructure/registry.py:160-177, packages/atif-duck/src/atif_duck/infrastructure/registry.py:188-192, packages/atif-duck/src/atif_duck/infrastructure/registry.py:213-221; test packages/atif-duck/tests/test_duck_views.py:630 excluded from every view, plus one WARNING per skipped dir so the exclusion is observable
A real embed run needs an explicit scope: --limit N or --all CLI packages/atif-cli/src/atif_cli/app.py:810-820 exit 64 with the hint naming both flags and --dry-run
search against an empty embeddings store is not a zero-result query CLI packages/atif-cli/src/atif_cli/app.py:930-941 exit 2 with the hint atif-sql embed --all --no-dry-run

Notes on two rows above. The strict-JSON transform in packages/atif-models/src/atif_models/domain/schema.py:6-11 is a rewriting validation rather than a rejecting one: it forces additionalProperties: false on every object level and moves every property into required, expressing optionality by making the field’s type nullable. Only the open-mapping shape has no representation and raises. Second: the provider’s structured-output contract and pydantic are two gates in series — the wire schema constrains shape, and model_validate re-applies the ge/le bounds the model may have ignored (packages/atif-models/src/atif_models/domain/schema.py:18-21).

Invariant Where enforced Citation
The seven FidelityGap members are the complete, typed statement of what harbor 0.22.0’s conversion loses; four are structural and observed on every session, three are session-conditional Application code packages/atif-converter/src/atif_converter/domain/fidelity.py:44-79; packages/atif-converter/src/atif_converter/application/convert_and_audit.py:52-59 (structural set) and packages/atif-converter/src/atif_converter/application/convert_and_audit.py:70-76 (conditional set); test packages/atif-converter/tests/test_convert_and_audit.py:163
Only RecordType.USER and RecordType.ASSISTANT are convertible; every other record type is dropped upstream Application code packages/atif-converter/src/atif_converter/domain/fidelity.py:41; test packages/atif-converter/tests/test_convert_and_audit.py:121
LossReport.records_total equals the edges.jsonl line count for any session, because the census and the edges emitter read one snapshot Application code packages/atif-converter/src/atif_converter/domain/fidelity.py:91-98, packages/atif-converter/src/atif_converter/infrastructure/census.py:57-67, packages/atif-converter/src/atif_converter/application/convert_and_audit.py:137-139; test packages/atif-converter/tests/test_snapshot_and_drift.py:122
records_converted counts raw user/assistant RECORDS, not ATIF steps — harbor bundles several assistant events sharing one message.id into one agent step, so the two numbers differ by design Application code packages/atif-converter/src/atif_converter/application/convert_and_audit.py:122-126, packages/atif-converter/src/atif_converter/application/convert_and_audit.py:62-68
subagent_files_convertible equals subagent_files_found: the staging layer flattens workflow-nested side-files into harbor’s visible directory, so WORKFLOW_SUBAGENTS_MISSED names the UPSTREAM gap rather than a local loss Application code packages/atif-converter/src/atif_converter/application/convert_and_audit.py:83-88, packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:71-92; tests packages/atif-converter/tests/test_convert_and_audit.py:127 and packages/atif-converter/tests/test_convert_and_audit.py:136
A truncation marker or a non-zero leftover count means enrichment attribution was REFUSED, never guessed Application code packages/atif-converter/src/atif_converter/domain/enrichment.py:45-62, packages/atif-converter/src/atif_converter/domain/enrichment.py:379-399; tests packages/atif-converter/tests/test_enrichment.py:272, packages/atif-converter/tests/test_enrichment.py:287, packages/atif-converter/tests/test_enrichment.py:302
Staged side-files are per-FILE symlinks under real directories, flattened with __ joins, because Python 3.13’s rglob does not descend a symlinked directory and a flat namespace can collide Application code packages/atif-converter/src/atif_converter/infrastructure/harbor_adapter.py:71-92
edges.jsonl line order is (ts, uuid) with missing values sorting first as empty strings, and the key order is contract-fixed Application code packages/atif-converter/src/atif_converter/domain/edges.py:22-32, packages/atif-converter/src/atif_converter/domain/edges.py:82-90; tests packages/atif-converter/tests/test_edges.py:43 and packages/atif-converter/tests/test_edges.py:47
A renamed harbor private method and a bad transcript must not look alike in a materialization log Application code packages/atif-converter/src/atif_converter/domain/errors.py:47-54; test packages/atif-converter/tests/test_snapshot_and_drift.py:331
Invariant Where enforced Citation
A reader never observes a torn artifact SET: all four artifacts are written into <corpus_root>/.staging/ and the whole directory is swapped in by rename Application code packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py:98-137, packages/atif-corpus/src/atif_corpus/application/materialize.py:190-244; tests packages/atif-corpus/tests/test_atomic.py:85, packages/atif-corpus/tests/test_materialize.py:346
meta.json is written LAST inside the staging directory, and it is the marker atif-duck gates every reader on Application code, both sides packages/atif-corpus/src/atif_corpus/application/materialize.py:207-212, packages/atif-duck/src/atif_duck/infrastructure/registry.py:188-192; test packages/atif-duck/tests/test_duck_views.py:630
Persistence order matches write order: each artifact is fsynced before the rename that publishes it, and the parent directory after Application code packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py:64-95, packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py:140-158; tests packages/atif-corpus/tests/test_atomic.py:131, packages/atif-corpus/tests/test_atomic.py:149, packages/atif-corpus/tests/test_atomic.py:194
A kill inside the swap window leaves the session dir MISSING rather than torn, and the next pass force-replans it Application code packages/atif-corpus/src/atif_corpus/infrastructure/atomic.py:113-123, packages/atif-corpus/src/atif_corpus/application/materialize.py:313-339; tests packages/atif-corpus/tests/test_materialize.py:791, packages/atif-corpus/tests/test_domain.py:216
Only sessions that materialized successfully advance their watermark entries, so a failed session stays stale and is retried Application code packages/atif-corpus/src/atif_corpus/application/materialize.py:428-473, packages/atif-corpus/src/atif_corpus/application/materialize.py:615-622; test packages/atif-corpus/tests/test_materialize.py:333
A VANISHED source path keeps its watermark entry unless the owning session succeeded this pass or left the scan entirely — the retained entry IS the staleness signal Application code packages/atif-corpus/src/atif_corpus/application/materialize.py:455-473; tests packages/atif-corpus/tests/test_materialize.py:191 and packages/atif-corpus/tests/test_materialize.py:227
owns_path is the single definition of a session’s watermark scope, so staleness and retention can never disagree about which entries belong to a session Application code packages/atif-corpus/src/atif_corpus/domain/sessions.py:129-140; tests packages/atif-corpus/tests/test_domain.py:191, packages/atif-corpus/tests/test_domain.py:202, packages/atif-corpus/tests/test_domain.py:207
Only an exactly-equal mtime_ns counts as unchanged; an mtime that moved BACKWARDS is modified Application code packages/atif-corpus/src/atif_corpus/domain/watermark.py:61-76; test packages/atif-corpus/tests/test_domain.py:86
A staging entry is removed only when EVERY pid its name carries is proven gone; only ProcessLookupError proves that Application code packages/atif-corpus/src/atif_corpus/application/materialize.py:256-310; tests packages/atif-corpus/tests/test_materialize.py:429, packages/atif-corpus/tests/test_materialize.py:457, packages/atif-corpus/tests/test_materialize.py:481
One broken transcript never aborts a corpus sync — the port may raise anything, and the pass records the failure and continues Application code packages/atif-corpus/src/atif_corpus/domain/ports.py:41-47, packages/atif-corpus/src/atif_corpus/application/materialize.py:597-616; test packages/atif-corpus/tests/test_materialize.py:316
The domain reads no clock: now_ns is always passed in, so identical inputs always yield an identical plan Application code packages/atif-corpus/src/atif_corpus/domain/sessions.py:18-21, packages/atif-corpus/src/atif_corpus/application/materialize.py:48-52, packages/atif-corpus/src/atif_corpus/application/materialize.py:528; test packages/atif-corpus/tests/test_domain.py:145
All three plan partitions are sorted by session id, so the same scan yields the same work order, log output, and failure ordering Application code packages/atif-corpus/src/atif_corpus/domain/sessions.py:110-121, packages/atif-corpus/src/atif_corpus/domain/sessions.py:195; test packages/atif-corpus/tests/test_domain.py:145
The corpus root ~/.claude maps to the reserved slug default; every other root is <sanitized-dirname>-<8 hex of sha256 of the resolved path> Application code packages/atif-corpus/src/atif_corpus/domain/slug.py:23, packages/atif-corpus/src/atif_corpus/domain/slug.py:45-50; tests packages/atif-corpus/tests/test_slug.py:19, packages/atif-corpus/tests/test_slug.py:24, packages/atif-corpus/tests/test_slug.py:32
The per-session artifact filenames and the watermark filename are fixed by the contract Application code packages/atif-corpus/src/atif_corpus/domain/layout.py:17-22; test packages/atif-corpus/tests/test_domain.py:245
Invariant Where enforced Citation
The retry queue is the SINGLE re-admission gate for a failed unit: a live entry blocks dispatch until drain says it is due, because a failed unit is never checkpointed and would otherwise be re-admitted fresh every run forever Application code packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:176-193, packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:167-173; tests packages/atif-analytics/tests/test_state.py:124, packages/atif-analytics/tests/test_perceived.py:231
A FAILED unit is not checkpointed; a REFUSED unit is (terminal), and its refusal is durable — an in-band sentinel row for session-keyed classify, a analytics/refusals sidecar row for the uuid-keyed pipelines Application code packages/atif-analytics/src/atif_analytics/application/use_cases/classify.py:222-246, packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:191-229, packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:277-289; test packages/atif-analytics/tests/test_perceived.py:252
Nothing is stamped — no checkpoint, no cache row, no retry entry — for a unit that was never dispatched Application code packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:155-183, packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:248-266; test packages/atif-analytics/tests/test_perceived.py:280
Budget overshoot is bounded by BUDGET_CHECK_BATCH units (8, per dispatch batch) on the priciest watched model, and does not scale with the session ceiling or the write chunk size Application code packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:100-112, packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:146-152; test packages/atif-analytics/tests/test_resource_guards.py:362 (over both dispatch shapes)
A checkpoint skip requires BOTH last_ts and last_mtime to be non-advancing; either bound moving forward re-admits the session Application code packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/checkpointer.py:136-176; tests packages/atif-analytics/tests/test_state.py:48 and packages/atif-analytics/tests/test_state.py:57
state.db is corpus-scoped at <corpus_root>/analytics/state.db, so one corpus’s completions can never skip another’s re-scoring Application code packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/checkpointer.py:22-24
Sidechain steps are excluded from every rendered transcript, because the prompts are calibrated on transcripts with no subagent content and harbor inlines sidechains Application code packages/atif-analytics/src/atif_analytics/domain/transcript.py:24-27, packages/atif-analytics/src/atif_analytics/domain/transcript.py:132-134, packages/atif-analytics/src/atif_analytics/domain/transcript.py:162; test packages/atif-analytics/tests/test_transcript.py:50
A compact-summary step is synthetic text: it never participates in a trajectory window and never counts toward a human-AI pair Application code packages/atif-analytics/src/atif_analytics/domain/transcript.py:199-217, packages/atif-analytics/src/atif_analytics/domain/transcript.py:253
Only render_session_text writes a real [uuid= header; every body is escaped first Application code packages/atif-analytics/src/atif_analytics/domain/transcript.py:33-38, packages/atif-analytics/src/atif_analytics/domain/transcript.py:160-161, packages/atif-analytics/src/atif_analytics/domain/transcript.py:173-183; test packages/atif-analytics/tests/test_resource_guards.py:561
CLI_BOOKKEEPING_TEXTS is defined once in the domain and shared by the friction candidate filter and the perceived-eligibility pair counter Application code packages/atif-analytics/src/atif_analytics/domain/transcript.py:78-86, packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:94-100; test packages/atif-analytics/tests/test_perceived.py:80
The source column vocabulary regex / sql / llm / refused names the row’s PROVENANCE TIER, not the engine that computed it, and downstream views bind to those literals Application code packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:29-33, packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:86-89
Two friction stamp rules matching one uuid resolve by higher confidence, ties keeping the first stamp, with rules applied in the fixed order 1 to 3 Application code packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:183-195
Structural hyperparameter defaults are pinned by contract and seed = 42 threads into every UMAP and Leiden call, so a same-seed rerun reproduces the clustering Application code packages/atif-analytics/src/atif_analytics/domain/config.py:10-12, packages/atif-analytics/src/atif_analytics/domain/config.py:20-40, packages/atif-analytics/src/atif_analytics/domain/config.py:43-60
A session’s community id is -1 when it is a singleton or unclusterable — an out-of-band sentinel that survives Int32 serialization Application code packages/atif-analytics/src/atif_analytics/domain/structure/community.py:84-86
A community-detection resolution γ must be strictly positive: CPM at γ = 0 has no null term and collapses every graph to one community Application code packages/atif-analytics/src/atif_analytics/domain/structure/community.py:100-102
Invariant Where enforced Citation
The registry is the ONLY place in the workspace where a model id is written down, and it is total over family times size (6 entries) Application code packages/atif-models/src/atif_models/domain/registry.py:5-8, packages/atif-models/src/atif_models/domain/registry.py:60-106; test packages/atif-models/tests/test_registry.py:61
estimate_cost returns None, never 0.0, when either rate is unknown — callers must render “pricing unavailable” Application code packages/atif-models/src/atif_models/domain/registry.py:125-137; tests packages/atif-models/tests/test_registry.py:85 and packages/atif-models/tests/test_registry.py:95
Usage is accumulated BEFORE any finish_reason gate, because a length-truncated call still billed tokens Application code packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:269-277; test packages/atif-models/tests/test_openai_provider.py:249
RETRY_CODES (8 Bedrock codes) is a deliberate TWIN of atif-embed’s set, pinned against the same literal in each package’s own suite because the two may not import each other Application code packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:59-74; tests packages/atif-models/tests/test_openai_provider.py:318 and packages/atif-models/tests/test_openai_provider.py:322
Every embeddings row stamps model, dim, text_hash, and truncated, so a re-conversion that changes a step’s text is detectable under the same uuid Application code packages/atif-embed/src/atif_embed/domain/text_stamp.py:6-18, packages/atif-embed/src/atif_embed/application/embed.py:216-235; test packages/atif-embed/tests/test_embed_use_case.py:89
A uuid never fans out to two vectors: a stale row under the same uuid is DELETED before the new vector is appended Application code packages/atif-embed/src/atif_embed/application/embed.py:203-210, packages/atif-embed/src/atif_embed/domain/text_stamp.py:50-63; tests packages/atif-embed/tests/test_embed_use_case.py:211, packages/atif-embed/tests/test_lance_store.py:129
_PRE_STAMP_SENTINEL (<pre-stamp>) can never equal a real blake2b hex digest, so every pre-stamp row mismatches its corpus hash and re-embeds through the ordinary staleness path Application code packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:69-73; test packages/atif-embed/tests/test_lance_store.py:192
ADDITIVE schema drift migrates ONLINE (metadata-only add_columns, no re-embedding, search stays up); a BREAKING change — provider or dimension switch — stays fail-loud Application code packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:16-32, packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:166-192; tests packages/atif-embed/tests/test_embed_use_case.py:293 and packages/atif-embed/tests/test_embed_use_case.py:314
The write chunk is a multiple of batch_size, so a checkpoint boundary never splits a Bedrock batch Application code packages/atif-embed/src/atif_embed/application/embed.py:145-147
The provider/dimension guard exists as two copies (atif-embed and atif-duck) because the independence contract forbids the import; a behavioral change in either must be ported Application code packages/atif-embed/src/atif_embed/domain/embedding_guard.py:15-18, packages/atif-duck/src/atif_duck/domain/embedding_guard.py:46; tests packages/atif-embed/tests/test_guard_twin_pin.py:76, packages/atif-embed/tests/test_guard_twin_pin.py:79, packages/atif-embed/tests/test_guard_twin_pin.py:85
The recovery hint names the real store locations (ATIF_SQL_LANCE_URI or <corpus-root>/embeddings_lance), never a fixed home-directory path Application code packages/atif-embed/src/atif_embed/domain/embedding_guard.py:25-35; tests packages/atif-embed/tests/test_guard_twin_pin.py:97 and packages/atif-embed/tests/test_guard_twin_pin.py:103
Invariant Where enforced Citation
Registration order is raw TEMP tables, then views, then VSS, then macros, then the v2 analytics views and macros — each layer binds against the previous at CREATE time Application code packages/atif-duck/src/atif_duck/infrastructure/registry.py:1228-1234; test packages/atif-duck/tests/test_duck_views.py:615
DESCRIPTIONS covers the catalog exactly (16 views plus 9 macros plus 12 analytics views plus 13 analytics macros equals 50) Application code packages/atif-duck/src/atif_duck/domain/catalog.py:326; test packages/atif-duck/tests/test_examples.py:178
cost_estimate’s est_cost_usd covers PRICED steps only and is meaningful only when unpriced_steps = 0; an inner join would return a partial number indistinguishable from a complete one Application code packages/atif-duck/src/atif_duck/infrastructure/registry.py:1053-1058, packages/atif-duck/src/atif_duck/infrastructure/registry.py:1066-1077; tests packages/atif-duck/tests/test_duck_views.py:407 and packages/atif-duck/tests/test_duck_views.py:482
Both cost_estimate counters filter on model_name IS NOT NULL, because user steps carry no model and would otherwise make every session look like a pricing gap Application code packages/atif-duck/src/atif_duck/infrastructure/registry.py:1060-1063; test packages/atif-duck/tests/test_duck_views.py:533
An absent or unattachable Lance store degrades to an empty message_embeddings TABLE with the right schema, so semantic_search always binds Application code packages/atif-duck/src/atif_duck/infrastructure/registry.py:884-905; tests packages/atif-duck/tests/test_vss.py:112 and packages/atif-duck/tests/test_vss.py:248
Exit codes are a stable wire contract shared with atif-converter’s taxonomy on every common number (64, 65, 70, 127) Application code packages/atif-cli/src/atif_cli/errors.py:5-11, packages/atif-cli/src/atif_cli/errors.py:23-39
terminal versus transient decides the embed exit code, so an unattended lane can stop retrying instead of burning identical ticks Application code packages/atif-embed/src/atif_embed/domain/errors.py:17-26, packages/atif-cli/src/atif_cli/app.py:833-847
Calculation Inputs Output Citation
Dry-run dollar projection for a planned batch measured input_tokens, output_tokens, and a (in_rate, out_rate) pair in USD per 1M tokens USD for the batch, flat linear, no minimums or tiers packages/atif-analytics/src/atif_analytics/domain/costs.py:33-47
Characters to tokens character count token estimate max(1, chars // 4), floor 1 when non-empty packages/atif-analytics/src/atif_analytics/domain/costs.py:23-30
Accumulated-usage dollar estimate for one ModelSpec ModelSpec.pricing_in, pricing_out (USD per 1M tokens), accumulated input_tokens and output_tokens USD, or None when a rate is unknown packages/atif-models/src/atif_models/domain/registry.py:125-137; test packages/atif-models/tests/test_registry.py:76
Running actual spend for a run every watched provider’s UsageAccumulator summary plus its ModelSpec total USD spent this run, across all LLM pipelines packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:123-139
cost_estimate(sid) SQL macro a session’s steps rows (prompt_tokens, cached_tokens, completion_tokens, model_name) LEFT JOINed against DEFAULT_PRICING (est_cost_usd, priced_steps, unpriced_steps) packages/atif-duck/src/atif_duck/infrastructure/registry.py:1066-1083; test packages/atif-duck/tests/test_duck_views.py:368
Retry backoff delay attempt counter min(2 ** attempts, 60) MINUTES, per (pipeline, unit_id): 2, 4, 8, 16, 32, capped at 60 packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:78-81; test packages/atif-analytics/tests/test_state.py:102
Bedrock call retry schedule the raised exception up to 10 attempts, exponential wait with multiplier 2, minimum 2 SECONDS and maximum 60 SECONDS, only for RETRY_CODES and network errors packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:251-257, packages/atif-models/src/atif_models/infrastructure/openai_bedrock.py:102-109; test packages/atif-models/tests/test_openai_provider.py:284
Parquet write chunk size batch_size (default 96 units) max(batch_size * 4, 256) ROWS per part — bounds crash loss, not spend packages/atif-analytics/src/atif_analytics/application/use_cases/_shared.py:242-250
Quiescence age test newest source mtime_ns, now_ns, quiesce_seconds boolean: now_ns - newest_mtime_ns >= quiesce_seconds * 1_000_000_000 packages/atif-corpus/src/atif_corpus/domain/sessions.py:96-104; test packages/atif-corpus/tests/test_domain.py:40
Source delta between two scans two path to mtime_ns maps added / modified / removed sorted tuples; touched is added then modified packages/atif-corpus/src/atif_corpus/domain/watermark.py:57-76; test packages/atif-corpus/tests/test_domain.py:76
Corpus slug a corpus root path default for ~/.claude, else <sanitized-dirname (<= 32 chars)>-<8 hex of sha256> packages/atif-corpus/src/atif_corpus/domain/slug.py:45-50; test packages/atif-corpus/tests/test_slug.py:24
Loss accounting for one session raw record counts by RecordType, side-file classification records_converted (user plus assistant records), records_dropped (total minus converted), gaps_observed packages/atif-converter/src/atif_converter/application/convert_and_audit.py:62-90, packages/atif-converter/src/atif_converter/domain/fidelity.py:109-112
Completed human-to-AI exchange count a session’s StepEvent list integer pair count, the perceived-error eligibility input packages/atif-analytics/src/atif_analytics/domain/transcript.py:236-262; test packages/atif-analytics/tests/test_perceived.py:63
Sentiment delta for a trajectory window prev_sentiment, curr_sentiment over the encoding negative equals -1, neutral equals 0, positive equals 1 curr - prev as a float in the closed interval -2.0 to 2.0, or None when there is no previous turn packages/atif-analytics/src/atif_analytics/domain/trajectory.py:31-32, packages/atif-analytics/src/atif_analytics/domain/trajectory.py:130-134, packages/atif-analytics/src/atif_analytics/domain/trajectory.py:171-183
Content stamp for an embeddable text the exact text sent to the embedder blake2b digest, 16 bytes hex-encoded (128 bits) packages/atif-embed/src/atif_embed/domain/text_stamp.py:26-40
c-TF-IDF term weights one pseudo-document per cluster plus a frozen TermsConfig (cluster_id, term, weight, rank) rows, ranks 1-based, non-positive weights dropped, top 10 per cluster packages/atif-analytics/src/atif_analytics/domain/structure/terms.py:44-74
CPM partition quality a weighted graph, a label vector, and γ scalar objective in the same units as the stored quality column packages/atif-analytics/src/atif_analytics/domain/structure/community.py:288-308
Resolution-profile call budget the configured γ range maximum distinct γ the bisection can evaluate, clamped by _PROFILE_MAX_CALLS (512 Leiden calls) packages/atif-analytics/src/atif_analytics/domain/structure/community.py:134-170
friction_rate(since_days) user_friction label counts per session; user-role main-chain non-empty steps as denominator per-session rate plus seven label counters packages/atif-duck/src/atif_duck/infrastructure/analytics.py:344-384
success_rate_by_work(since_days) session_classifications rows unknown_fraction over ALL sessions; success, failure and partial rates over KNOWN outcomes only packages/atif-duck/src/atif_duck/infrastructure/analytics.py:271-293
semantic_search(query_vec, k) a unit-norm query vector and k top-k (uuid, sim, distance) ordered by cosine distance packages/atif-duck/src/atif_duck/infrastructure/registry.py:1145-1152; test packages/atif-duck/tests/test_vss.py:195
todo_velocity(sid) todo_state_current rows for one session completed count divided by distinct subject count, NULL when there are no subjects packages/atif-duck/src/atif_duck/infrastructure/registry.py:1101-1107; test packages/atif-duck/tests/test_duck_views.py:566
subagent_fanout(sid) subagent_spawns rows for one session count of Task/Agent launch INTENTS, not side-transcript files packages/atif-duck/src/atif_duck/infrastructure/registry.py:1116-1124; test packages/atif-duck/tests/test_duck_views.py:572

Three of these need the formula spelled out.

cost_estimate(sid). Per step, uncharged-cache base input is prompt_tokens - cached_tokens, because ATIF’s prompt_tokens is the TOTAL (input plus cache-read plus cache-creation); the charge is (prompt_tokens - cached_tokens) * in_rate + completion_tokens * out_rate, summed over the session’s steps and divided by 1e6. Cache reads are uncharged, and the pricing join strips a dated model suffix (claude-haiku-4-5-20251001 matches claude-haiku-4-5) via regexp_replace(model_name, '-\d{8}$', ''). The pricing table itself is DEFAULT_PRICING, 11 entries of (in_rate, out_rate) in USD per 1,000,000 tokens, base rates only — the prompt-cache write and read multipliers (1.25x, 2x, 0.1x of base input) are deliberately not modeled, matching the macro (packages/atif-duck/src/atif_duck/domain/catalog.py:395-417, pinned by packages/atif-duck/tests/test_duck_views.py:459 against published list rates and by packages/atif-duck/tests/test_duck_views.py:470 for sanity bounds).

human_ai_pair_count. Walk the step list in materialized order. Skip any step that is sidechain, compact-summary, or has empty text. A user-role step arms a pending pair — unless its stripped text is one of the two CLI_BOOKKEEPING_TEXTS strings, which are Claude Code’s own user-role injections and are skipped outright. An assistant-role step completes the pending pair and disarms it. Consecutive user turns therefore collapse into one pending pair, because judging a perceived error requires the human RESPONDING to AI output (packages/atif-analytics/src/atif_analytics/domain/transcript.py:236-262). The perceived pipeline admits a session only at 2 or more pairs (packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:101-103, packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:118-120).

c-TF-IDF. CountVectorizer (lowercased, unicode-stripped accents, min_df = 2, max_df = 0.95, ngram range 1 to 2) produces a clusters-by-vocabulary count matrix. Term frequency is L1-normalized per cluster row. The IDF factor is log(1 + sum(avg) / max(col_sum, 1e-9)) where avg = col_sum / total, and the weight is the row-normalized TF times that IDF. Terms are ranked descending per cluster and the top 10 kept, with non-positive weights dropped (packages/atif-analytics/src/atif_analytics/domain/structure/terms.py:44-74).