atif-sql 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. atif_analytics/__init__.py +9 -0
  2. atif_analytics/application/__init__.py +3 -0
  3. atif_analytics/application/analyze.py +158 -0
  4. atif_analytics/application/prompts.py +750 -0
  5. atif_analytics/application/use_cases/__init__.py +3 -0
  6. atif_analytics/application/use_cases/_shared.py +264 -0
  7. atif_analytics/application/use_cases/classify.py +430 -0
  8. atif_analytics/application/use_cases/conflicts.py +448 -0
  9. atif_analytics/application/use_cases/friction.py +634 -0
  10. atif_analytics/application/use_cases/perceived.py +451 -0
  11. atif_analytics/domain/__init__.py +3 -0
  12. atif_analytics/domain/authorship.py +166 -0
  13. atif_analytics/domain/config.py +30 -0
  14. atif_analytics/domain/costs.py +50 -0
  15. atif_analytics/domain/friction.py +117 -0
  16. atif_analytics/domain/layout.py +94 -0
  17. atif_analytics/domain/models.py +385 -0
  18. atif_analytics/domain/ports.py +100 -0
  19. atif_analytics/domain/transcript.py +359 -0
  20. atif_analytics/infrastructure/__init__.py +3 -0
  21. atif_analytics/infrastructure/corpus_reader.py +546 -0
  22. atif_analytics/infrastructure/parquet_cache.py +186 -0
  23. atif_analytics/infrastructure/settings.py +114 -0
  24. atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
  25. atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
  26. atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
  27. atif_analytics/py.typed +0 -0
  28. atif_cli/__init__.py +3 -0
  29. atif_cli/__main__.py +8 -0
  30. atif_cli/app.py +2459 -0
  31. atif_cli/converter_adapter.py +151 -0
  32. atif_cli/corpus.py +409 -0
  33. atif_cli/cron.py +311 -0
  34. atif_cli/duck_errors.py +97 -0
  35. atif_cli/embed_lake.py +101 -0
  36. atif_cli/errors.py +89 -0
  37. atif_cli/lake.py +373 -0
  38. atif_cli/lake_sessions.py +270 -0
  39. atif_cli/output.py +297 -0
  40. atif_cli/py.typed +0 -0
  41. atif_converter/__init__.py +3 -0
  42. atif_converter/application/__init__.py +3 -0
  43. atif_converter/application/convert_and_audit.py +294 -0
  44. atif_converter/application/convert_codex.py +210 -0
  45. atif_converter/domain/__init__.py +9 -0
  46. atif_converter/domain/agents.py +40 -0
  47. atif_converter/domain/atif/__init__.py +55 -0
  48. atif_converter/domain/atif/agent.py +37 -0
  49. atif_converter/domain/atif/content.py +161 -0
  50. atif_converter/domain/atif/final_metrics.py +45 -0
  51. atif_converter/domain/atif/metrics.py +49 -0
  52. atif_converter/domain/atif/observation.py +21 -0
  53. atif_converter/domain/atif/observation_result.py +47 -0
  54. atif_converter/domain/atif/step.py +144 -0
  55. atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
  56. atif_converter/domain/atif/tool_call.py +36 -0
  57. atif_converter/domain/atif/trajectory.py +211 -0
  58. atif_converter/domain/atif/trajectory_validator.py +299 -0
  59. atif_converter/domain/blobs.py +510 -0
  60. atif_converter/domain/claude_code_conversion.py +1077 -0
  61. atif_converter/domain/codex_conversion.py +1036 -0
  62. atif_converter/domain/codex_edges.py +192 -0
  63. atif_converter/domain/codex_enrichment.py +419 -0
  64. atif_converter/domain/codex_fidelity.py +124 -0
  65. atif_converter/domain/codex_nested_calls.py +165 -0
  66. atif_converter/domain/edges.py +113 -0
  67. atif_converter/domain/enrichment.py +412 -0
  68. atif_converter/domain/errors.py +60 -0
  69. atif_converter/domain/fidelity.py +171 -0
  70. atif_converter/domain/model_prices.json +8538 -0
  71. atif_converter/domain/pricing.py +790 -0
  72. atif_converter/domain/result_signals.py +774 -0
  73. atif_converter/domain/schema_version.py +90 -0
  74. atif_converter/domain/session_events.py +408 -0
  75. atif_converter/infrastructure/__init__.py +3 -0
  76. atif_converter/infrastructure/census.py +89 -0
  77. atif_converter/infrastructure/claude_code_converter.py +127 -0
  78. atif_converter/infrastructure/codex_adapter.py +132 -0
  79. atif_converter/infrastructure/codex_census.py +100 -0
  80. atif_converter/infrastructure/codex_converter.py +82 -0
  81. atif_converter/infrastructure/harbor_adapter.py +187 -0
  82. atif_converter/infrastructure/raw_records.py +426 -0
  83. atif_converter/infrastructure/source_archive.py +154 -0
  84. atif_converter/py.typed +0 -0
  85. atif_corpus/__init__.py +13 -0
  86. atif_corpus/application/__init__.py +7 -0
  87. atif_corpus/application/materialize.py +1826 -0
  88. atif_corpus/domain/__init__.py +9 -0
  89. atif_corpus/domain/agents.py +33 -0
  90. atif_corpus/domain/generation.py +34 -0
  91. atif_corpus/domain/layout.py +168 -0
  92. atif_corpus/domain/ports.py +175 -0
  93. atif_corpus/domain/session_id.py +68 -0
  94. atif_corpus/domain/sessions.py +228 -0
  95. atif_corpus/domain/slug.py +72 -0
  96. atif_corpus/domain/source_layout.py +143 -0
  97. atif_corpus/domain/watermark.py +76 -0
  98. atif_corpus/infrastructure/__init__.py +7 -0
  99. atif_corpus/infrastructure/atomic.py +266 -0
  100. atif_corpus/infrastructure/compress_artifacts.py +161 -0
  101. atif_corpus/infrastructure/fake_converter.py +174 -0
  102. atif_corpus/infrastructure/scanner.py +316 -0
  103. atif_corpus/infrastructure/settings.py +130 -0
  104. atif_corpus/infrastructure/source_archive.py +202 -0
  105. atif_corpus/py.typed +0 -0
  106. atif_duck/__init__.py +43 -0
  107. atif_duck/domain/__init__.py +32 -0
  108. atif_duck/domain/artifacts.py +66 -0
  109. atif_duck/domain/authorship.py +165 -0
  110. atif_duck/domain/catalog.py +626 -0
  111. atif_duck/domain/columnar.py +149 -0
  112. atif_duck/domain/embedding_guard.py +88 -0
  113. atif_duck/domain/examples.py +265 -0
  114. atif_duck/domain/lake.py +427 -0
  115. atif_duck/domain/lake_sessions.py +97 -0
  116. atif_duck/domain/raw_readers.py +140 -0
  117. atif_duck/domain/session_id.py +69 -0
  118. atif_duck/domain/sql_literal.py +50 -0
  119. atif_duck/infrastructure/__init__.py +34 -0
  120. atif_duck/infrastructure/analytics.py +469 -0
  121. atif_duck/infrastructure/authorship.py +139 -0
  122. atif_duck/infrastructure/columnar.py +641 -0
  123. atif_duck/infrastructure/lake.py +1383 -0
  124. atif_duck/infrastructure/lake_sessions.py +194 -0
  125. atif_duck/infrastructure/lake_settings.py +63 -0
  126. atif_duck/infrastructure/lake_steps.py +225 -0
  127. atif_duck/infrastructure/projections.py +221 -0
  128. atif_duck/infrastructure/registry.py +2194 -0
  129. atif_duck/infrastructure/stored_artifacts.py +115 -0
  130. atif_duck/py.typed +0 -0
  131. atif_embed/__init__.py +12 -0
  132. atif_embed/application/__init__.py +3 -0
  133. atif_embed/application/embed.py +293 -0
  134. atif_embed/application/prune.py +80 -0
  135. atif_embed/domain/__init__.py +3 -0
  136. atif_embed/domain/discovery.py +70 -0
  137. atif_embed/domain/embedding_guard.py +83 -0
  138. atif_embed/domain/errors.py +94 -0
  139. atif_embed/domain/ports.py +223 -0
  140. atif_embed/domain/sql_literal.py +49 -0
  141. atif_embed/domain/text_stamp.py +71 -0
  142. atif_embed/infrastructure/__init__.py +3 -0
  143. atif_embed/infrastructure/cohere_bedrock.py +433 -0
  144. atif_embed/infrastructure/corpus_text_rows.py +273 -0
  145. atif_embed/infrastructure/gemma_local.py +355 -0
  146. atif_embed/infrastructure/lake_text_rows.py +203 -0
  147. atif_embed/infrastructure/lance_store.py +440 -0
  148. atif_embed/infrastructure/providers.py +30 -0
  149. atif_embed/infrastructure/settings.py +154 -0
  150. atif_embed/py.typed +0 -0
  151. atif_models/__init__.py +14 -0
  152. atif_models/domain/__init__.py +9 -0
  153. atif_models/domain/ports.py +143 -0
  154. atif_models/domain/registry.py +151 -0
  155. atif_models/domain/schema.py +109 -0
  156. atif_models/infrastructure/__init__.py +8 -0
  157. atif_models/infrastructure/openai_bedrock.py +314 -0
  158. atif_models/infrastructure/settings.py +85 -0
  159. atif_models/py.typed +0 -0
  160. atif_sql-0.2.0.dist-info/METADATA +272 -0
  161. atif_sql-0.2.0.dist-info/RECORD +164 -0
  162. atif_sql-0.2.0.dist-info/WHEEL +4 -0
  163. atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
  164. atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,427 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """The DuckLake the corpora are queried through: tables, partitions, schema identity.
4
+
5
+ One lake holds every corpus (both Claude Code corpora and the Codex one). Each
6
+ lake table is the registry's raw reader of the same artifact kind
7
+ (:mod:`atif_duck.domain.raw_readers`) with two identity columns in front,
8
+ ``corpus`` (the corpus directory's name) and ``agent`` (the agent the corpus
9
+ holds). The per-session artifacts stay the source of truth: the lake is
10
+ loaded FROM the registry's raw relations over them, so a lake row is by
11
+ construction the row the per-session path would have produced, and the views
12
+ bind over either with no change.
13
+
14
+ Nothing here is declared twice. Every payload column comes from the raw
15
+ reader shapes, which come from the catalog and the columnar contract; the
16
+ only lake-local decisions are the identity columns, the rename rule for a
17
+ source column that collides with one, the derived ``started_at`` the sessions
18
+ table is partitioned by, and the partition specs.
19
+
20
+ Partitioning: every table is partitioned by ``agent`` and ``corpus`` first,
21
+ then by month where a row has a time. ``corpus`` is there because a query is
22
+ scoped to one corpus by default, and a corpus-pure file is what lets that scope
23
+ skip the other corpora's files. It also keeps compaction from merging two
24
+ corpora's rows into one file. ``agent`` is implied by ``corpus`` (one corpus
25
+ holds one agent), so it adds no files, only a readable directory level. The
26
+ two one-row-per-session tables (``loss_reports``, ``session_meta``) have no
27
+ time column and stay at ``agent``/``corpus``: splitting a few thousand small
28
+ rows by month would multiply files without pruning anything a query asks for.
29
+
30
+ Schema identity: the lake records :data:`LAKE_SCHEMA_VERSION`, a digest of
31
+ the rendered table definitions (:func:`lake_schema_digest`), and the columnar
32
+ schema version it was loaded under. Any of the three differing from the
33
+ running code makes the lake STALE: the writer rebuilds it and a reader falls
34
+ back to the per-session path. ``tests/test_lake.py`` pins the digest
35
+ beside the version, so a change to any shape the lake derives from fails until
36
+ someone decides whether it also needs a version bump.
37
+
38
+ Pure constants and string building: no duckdb import, no filesystem access.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import hashlib
44
+ import json
45
+ from dataclasses import dataclass
46
+
47
+ from atif_duck.domain.catalog import VIEW_SCHEMA
48
+ from atif_duck.domain.columnar import COLUMNAR_SCHEMA_VERSION
49
+ from atif_duck.domain.raw_readers import (
50
+ CORPUS_COLUMN,
51
+ EDGE_COLUMNS,
52
+ LOSS_REPORT_COLUMNS,
53
+ LOSS_REPORT_PATH_COLUMN,
54
+ META_COLUMNS,
55
+ RAW_EDGES,
56
+ RAW_LOSS_REPORTS,
57
+ RAW_META,
58
+ RAW_SESSION_EVENTS,
59
+ RAW_STEPS,
60
+ RAW_TOOL_CALLS,
61
+ RAW_TOOL_RESULTS,
62
+ RAW_TRAJECTORIES,
63
+ SESSION_ID_PATH,
64
+ TRAJECTORY_RELATION_COLUMNS,
65
+ )
66
+ from atif_duck.domain.sql_literal import SqlFragment
67
+
68
+ #: Bump when the lake's meaning changes in a way the table definitions do not
69
+ #: show (what a row is loaded from, how a key is derived). A change the
70
+ #: definitions DO show moves :func:`lake_schema_digest` on its own; the pin test
71
+ #: makes the author decide whether it also warrants a bump.
72
+ LAKE_SCHEMA_VERSION: int = 1
73
+
74
+ #: The lake tables whose rows keep their source order. A step's tool calls
75
+ #: and results carry no ordinal, so their order within a step is the order the
76
+ #: writer inserted them in, which DuckLake keeps as ``rowid``. A parallel
77
+ #: partitioned insert interleaves rows from its threads (measured: results
78
+ #: of one step out of order in a few sessions per hundred), so the writer
79
+ #: inserts these tables on one thread. Part of :func:`lake_schema_digest`: a
80
+ #: lake written without the guarantee reads back misordered, so it is stale.
81
+ SOURCE_ORDERED_TABLES: tuple[str, ...] = ("steps", "tool_calls", "tool_results")
82
+
83
+ #: Parquet row group size for every lake file. DuckDB's default (122,880 rows)
84
+ #: puts a whole month of a corpus's tool results in one row group, so reading
85
+ #: one session's results decompressed every other session's too (measured on
86
+ #: the copied corpora: 0.7 s per session). The step-level tables' rows land
87
+ #: in session order, so a small group holds a few sessions and a session read
88
+ #: touches one or two. Part of :func:`lake_schema_digest`, so a lake written in
89
+ #: another layout is rebuilt rather than left slow.
90
+ LAKE_ROW_GROUP_SIZE: int = 4096
91
+
92
+ #: The name the lake is attached under on every connection that reads or
93
+ #: writes it. Not part of the queryable surface: the views are.
94
+ LAKE_ALIAS: str = "atif_lake"
95
+
96
+ #: The metadata database DuckLake attaches beside :data:`LAKE_ALIAS`.
97
+ LAKE_METADATA_ALIAS: str = f"__ducklake_metadata_{LAKE_ALIAS}"
98
+
99
+ #: The identity every lake table starts with.
100
+ AGENT_COLUMN: str = "agent"
101
+ SESSION_KEY: str = "session_id"
102
+ IDENTITY_COLUMNS: tuple[tuple[str, str], ...] = (
103
+ (CORPUS_COLUMN, "VARCHAR"),
104
+ (AGENT_COLUMN, "VARCHAR"),
105
+ (SESSION_KEY, "VARCHAR"),
106
+ )
107
+
108
+ #: A source column whose name is one of the identity names is stored under
109
+ #: this prefix (the trajectory's own ``agent`` struct becomes ``src_agent``).
110
+ COLLISION_PREFIX: str = "src_"
111
+
112
+ #: ``lake_info`` keys (the lake's recorded schema identity).
113
+ INFO_SCHEMA_VERSION: str = "lake_schema_version"
114
+ INFO_SCHEMA_DIGEST: str = "lake_schema_digest"
115
+ INFO_COLUMNAR_SCHEMA: str = "columnar_schema_version"
116
+
117
+ #: The two bookkeeping tables. Their shape never changes, so a stale lake can
118
+ #: still say which corpora it holds and a rebuild can find them again.
119
+ LAKE_INFO_TABLE: str = "lake_info"
120
+ CORPORA_TABLE: str = "corpora"
121
+ LAKE_INFO_COLUMNS: tuple[tuple[str, str], ...] = (("key", "VARCHAR"), ("value", "VARCHAR"))
122
+ CORPORA_COLUMNS: tuple[tuple[str, str], ...] = (
123
+ (CORPUS_COLUMN, "VARCHAR"),
124
+ (AGENT_COLUMN, "VARCHAR"),
125
+ ("corpus_root", "VARCHAR"),
126
+ ("registered_at", "VARCHAR"),
127
+ )
128
+
129
+ #: The source for the sessions table: the trajectory rows plus the first
130
+ #: step's time, which the table is partitioned by.
131
+ _SESSIONS_SOURCE: str = (
132
+ f"(SELECT t.*, s.started_at FROM {RAW_TRAJECTORIES} t LEFT JOIN " # noqa: S608 # nosec B608 - module constants only
133
+ f"(SELECT session_id, min(ts) AS started_at FROM {RAW_STEPS} GROUP BY session_id) s "
134
+ f"ON s.session_id = t.{SESSION_ID_PATH})"
135
+ )
136
+
137
+ _MONTHLY: tuple[str, ...] = ("year({ts})", "month({ts})")
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class LakeColumn:
142
+ """One payload column: its lake name, type, and the source column it copies."""
143
+
144
+ name: str
145
+ sql_type: str
146
+ source: str
147
+ #: Computed at load time rather than copied from the relation, so the
148
+ #: reader view does not select it back (``sessions.started_at``).
149
+ derived: bool = False
150
+
151
+
152
+ @dataclass(frozen=True, slots=True)
153
+ class LakeTable:
154
+ """One lake table and the raw relation it is loaded from and read back as."""
155
+
156
+ #: Table name inside the lake.
157
+ name: str
158
+ #: The raw reader (a registry relation name) whose shape the table stores.
159
+ relation: str
160
+ #: What the load selects from: the relation itself, or a constant subquery.
161
+ load_source: str
162
+ #: The source column holding the session key (``session_id`` or ``session_id_path``).
163
+ key_source: str
164
+ #: Payload columns after the identity columns, in order.
165
+ payload: tuple[LakeColumn, ...]
166
+ #: Partition expressions, after the identity-derived ``agent, corpus``.
167
+ partition_by: tuple[str, ...]
168
+ #: Source columns the relation exposes that the reader view must rebuild
169
+ #: from the identity (``corpus`` on the trajectories relation).
170
+ relation_extras: tuple[str, ...] = ()
171
+
172
+ @property
173
+ def columns(self) -> tuple[tuple[str, str], ...]:
174
+ """Every lake column, identity first."""
175
+ return (*IDENTITY_COLUMNS, *((c.name, c.sql_type) for c in self.payload))
176
+
177
+
178
+ def _payload(
179
+ columns: tuple[tuple[str, str], ...], *, key_source: str, skip: tuple[str, ...] = ()
180
+ ) -> tuple[LakeColumn, ...]:
181
+ """Map a relation's columns onto lake payload columns (key and identity renamed)."""
182
+ reserved = {name for name, _ in IDENTITY_COLUMNS}
183
+ out: list[LakeColumn] = []
184
+ for name, sql_type in columns:
185
+ if name == key_source or name in skip:
186
+ continue
187
+ lake_name = f"{COLLISION_PREFIX}{name}" if name in reserved else name
188
+ out.append(LakeColumn(name=lake_name, sql_type=sql_type, source=name))
189
+ return tuple(out)
190
+
191
+
192
+ def _monthly(ts: str) -> tuple[str, ...]:
193
+ return tuple(part.format(ts=ts) for part in _MONTHLY)
194
+
195
+
196
+ def _view_table(name: str, relation: str, view: str) -> LakeTable:
197
+ return LakeTable(
198
+ name=name,
199
+ relation=relation,
200
+ load_source=relation,
201
+ key_source=SESSION_KEY,
202
+ payload=_payload(VIEW_SCHEMA[view], key_source=SESSION_KEY),
203
+ partition_by=_monthly("ts"),
204
+ )
205
+
206
+
207
+ #: Every lake table, in load order. The sessions table reads the steps
208
+ #: relation for ``started_at``, which the registry binds before the load runs.
209
+ LAKE_TABLES: tuple[LakeTable, ...] = (
210
+ LakeTable(
211
+ name="sessions",
212
+ relation=RAW_TRAJECTORIES,
213
+ load_source=_SESSIONS_SOURCE,
214
+ key_source=SESSION_ID_PATH,
215
+ payload=(
216
+ *_payload(TRAJECTORY_RELATION_COLUMNS, key_source=SESSION_ID_PATH),
217
+ LakeColumn(name="started_at", sql_type="TIMESTAMP", source="started_at", derived=True),
218
+ ),
219
+ partition_by=_monthly("started_at"),
220
+ relation_extras=(CORPUS_COLUMN,),
221
+ ),
222
+ _view_table("steps", RAW_STEPS, "steps"),
223
+ _view_table("tool_calls", RAW_TOOL_CALLS, "tool_calls"),
224
+ _view_table("tool_results", RAW_TOOL_RESULTS, "tool_results"),
225
+ _view_table("session_events", RAW_SESSION_EVENTS, "session_events"),
226
+ LakeTable(
227
+ name="edges",
228
+ relation=RAW_EDGES,
229
+ load_source=RAW_EDGES,
230
+ key_source=SESSION_ID_PATH,
231
+ payload=_payload(tuple(EDGE_COLUMNS.items()), key_source=SESSION_ID_PATH),
232
+ partition_by=_monthly("ts"),
233
+ ),
234
+ LakeTable(
235
+ name="loss_reports",
236
+ relation=RAW_LOSS_REPORTS,
237
+ load_source=RAW_LOSS_REPORTS,
238
+ key_source=SESSION_ID_PATH,
239
+ payload=_payload(
240
+ (*LOSS_REPORT_COLUMNS.items(), LOSS_REPORT_PATH_COLUMN), key_source=SESSION_ID_PATH
241
+ ),
242
+ partition_by=(),
243
+ ),
244
+ LakeTable(
245
+ name="session_meta",
246
+ relation=RAW_META,
247
+ load_source=RAW_META,
248
+ key_source=SESSION_ID_PATH,
249
+ payload=_payload(tuple(META_COLUMNS.items()), key_source=SESSION_ID_PATH),
250
+ partition_by=(),
251
+ ),
252
+ )
253
+
254
+ LAKE_TABLE_NAMES: tuple[str, ...] = tuple(table.name for table in LAKE_TABLES)
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Statements. Every one is a module constant built from LAKE_TABLES, so the
259
+ # AST audit in tests/test_sql_text_boundaries.py can prove each placeholder
260
+ # is catalog text; the only values that ever vary (corpus, agent, session
261
+ # ids) are bound parameters.
262
+ # ---------------------------------------------------------------------------
263
+
264
+ #: ``CREATE TABLE`` for the two bookkeeping tables.
265
+ BOOTSTRAP_CREATE_SQL: tuple[SqlFragment, ...] = (
266
+ SqlFragment(
267
+ f"CREATE TABLE {LAKE_ALIAS}.{LAKE_INFO_TABLE} "
268
+ f"({', '.join(f'{name} {sql_type}' for name, sql_type in LAKE_INFO_COLUMNS)})"
269
+ ),
270
+ SqlFragment(
271
+ f"CREATE TABLE {LAKE_ALIAS}.{CORPORA_TABLE} "
272
+ f"({', '.join(f'{name} {sql_type}' for name, sql_type in CORPORA_COLUMNS)})"
273
+ ),
274
+ )
275
+
276
+ #: ``CREATE TABLE`` per lake table.
277
+ CREATE_TABLE_SQL: dict[str, SqlFragment] = {
278
+ t.name: SqlFragment(
279
+ f"CREATE TABLE {LAKE_ALIAS}.{t.name} "
280
+ f"({', '.join(f'{name} {sql_type}' for name, sql_type in t.columns)})"
281
+ )
282
+ for t in LAKE_TABLES
283
+ }
284
+
285
+ #: ``ALTER TABLE ... SET PARTITIONED BY`` per lake table: agent and corpus
286
+ #: first, then the table's own time partition.
287
+ PARTITION_SQL: dict[str, SqlFragment] = {
288
+ t.name: SqlFragment(
289
+ f"ALTER TABLE {LAKE_ALIAS}.{t.name} SET PARTITIONED BY "
290
+ f"({', '.join((AGENT_COLUMN, CORPUS_COLUMN, *t.partition_by))})"
291
+ )
292
+ for t in LAKE_TABLES
293
+ }
294
+
295
+ #: Delete one corpus's rows for a bound list of session ids (params: corpus, ids).
296
+ DELETE_SESSIONS_SQL: dict[str, SqlFragment] = {
297
+ t.name: SqlFragment(
298
+ f"DELETE FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ? " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
299
+ f"AND {SESSION_KEY} IN (SELECT unnest(?::VARCHAR[]))"
300
+ )
301
+ for t in LAKE_TABLES
302
+ }
303
+
304
+ #: Delete every row of one corpus (param: corpus).
305
+ DELETE_CORPUS_SQL: dict[str, SqlFragment] = {
306
+ t.name: SqlFragment(f"DELETE FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ?") # noqa: S608 # nosec B608 - constants only; corpus is bound
307
+ for t in LAKE_TABLES
308
+ }
309
+
310
+ #: Copy the bound raw relation's rows into the lake (params: corpus, agent).
311
+ #: Every column is cast to its declared type, so a relation that infers a
312
+ #: looser type (an all-NULL column) still lands in the table's shape.
313
+ INSERT_SQL: dict[str, SqlFragment] = {
314
+ t.name: SqlFragment(
315
+ f"INSERT INTO {LAKE_ALIAS}.{t.name} ({', '.join(name for name, _ in t.columns)}) " # noqa: S608 # nosec B608 - constants only; corpus and agent are bound
316
+ f"SELECT CAST(? AS VARCHAR), CAST(? AS VARCHAR), CAST({t.key_source} AS VARCHAR), "
317
+ f"{', '.join(f'CAST({c.source} AS {c.sql_type})' for c in t.payload)} "
318
+ f"FROM {t.load_source}"
319
+ )
320
+ for t in LAKE_TABLES
321
+ }
322
+
323
+ #: Each lake table read back in its raw relation's shape, every corpus. A
324
+ #: reader appends ``WHERE corpus = <literal>`` to scope it.
325
+ READER_SELECT_SQL: dict[str, SqlFragment] = {
326
+ t.name: SqlFragment(
327
+ "SELECT " # noqa: S608 # nosec B608 - constants only
328
+ + ", ".join(
329
+ (
330
+ *(f"{c.name} AS {c.source}" for c in t.payload if not c.derived),
331
+ f"{SESSION_KEY} AS {t.key_source}",
332
+ *t.relation_extras,
333
+ )
334
+ )
335
+ + f" FROM {LAKE_ALIAS}.{t.name}"
336
+ )
337
+ for t in LAKE_TABLES
338
+ }
339
+
340
+ #: Per-session ``(session_id, rows, content hash)`` over the registry's raw
341
+ #: relation, columns cast as the load casts them. ``sum(hash(...))`` rather
342
+ #: than ``bit_xor``: order-free like an XOR, but two identical rows do not
343
+ #: cancel out.
344
+ ARTIFACT_HASH_SQL: dict[str, SqlFragment] = {
345
+ t.name: SqlFragment(
346
+ f"SELECT CAST({t.key_source} AS VARCHAR) AS session_id, count(*) AS n, " # noqa: S608 # nosec B608 - constants only
347
+ f"sum(CAST(hash({', '.join(f'CAST({c.source} AS {c.sql_type})' for c in t.payload)}) "
348
+ f"AS HUGEINT)) AS h FROM {t.load_source} GROUP BY 1"
349
+ )
350
+ for t in LAKE_TABLES
351
+ }
352
+
353
+ #: The same over the lake table for one corpus (param: corpus).
354
+ LAKE_HASH_SQL: dict[str, SqlFragment] = {
355
+ t.name: SqlFragment(
356
+ f"SELECT {SESSION_KEY} AS session_id, count(*) AS n, " # noqa: S608 # nosec B608 - constants only; corpus is bound
357
+ f"sum(CAST(hash({', '.join(c.name for c in t.payload)}) AS HUGEINT)) AS h "
358
+ f"FROM {LAKE_ALIAS}.{t.name} WHERE {CORPUS_COLUMN} = ? GROUP BY 1"
359
+ )
360
+ for t in LAKE_TABLES
361
+ }
362
+
363
+
364
+ def lake_schema_digest() -> str:
365
+ """sha256 over every table's name, columns and partition spec, bookkeeping and file layout included."""
366
+ spec = {
367
+ "row_group_size": LAKE_ROW_GROUP_SIZE,
368
+ "source_ordered_tables": SOURCE_ORDERED_TABLES,
369
+ "tables": [
370
+ {"name": t.name, "columns": t.columns, "partition_by": t.partition_by}
371
+ for t in LAKE_TABLES
372
+ ],
373
+ LAKE_INFO_TABLE: LAKE_INFO_COLUMNS,
374
+ CORPORA_TABLE: CORPORA_COLUMNS,
375
+ }
376
+ return hashlib.sha256(json.dumps(spec, sort_keys=True).encode("utf-8")).hexdigest()
377
+
378
+
379
+ def expected_lake_info() -> dict[str, str]:
380
+ """The ``lake_info`` rows a lake written by this code carries."""
381
+ return {
382
+ INFO_SCHEMA_VERSION: str(LAKE_SCHEMA_VERSION),
383
+ INFO_SCHEMA_DIGEST: lake_schema_digest(),
384
+ INFO_COLUMNAR_SCHEMA: str(COLUMNAR_SCHEMA_VERSION),
385
+ }
386
+
387
+
388
+ def lake_info_mismatches(recorded: dict[str, str]) -> tuple[str, ...]:
389
+ """The ``lake_info`` keys whose recorded value differs from this code's (sorted)."""
390
+ expected = expected_lake_info()
391
+ return tuple(sorted(key for key, value in expected.items() if recorded.get(key) != value))
392
+
393
+
394
+ __all__ = [
395
+ "AGENT_COLUMN",
396
+ "ARTIFACT_HASH_SQL",
397
+ "BOOTSTRAP_CREATE_SQL",
398
+ "COLLISION_PREFIX",
399
+ "CORPORA_COLUMNS",
400
+ "CORPORA_TABLE",
401
+ "CREATE_TABLE_SQL",
402
+ "DELETE_CORPUS_SQL",
403
+ "DELETE_SESSIONS_SQL",
404
+ "IDENTITY_COLUMNS",
405
+ "INFO_COLUMNAR_SCHEMA",
406
+ "INFO_SCHEMA_DIGEST",
407
+ "INFO_SCHEMA_VERSION",
408
+ "INSERT_SQL",
409
+ "LAKE_ALIAS",
410
+ "LAKE_HASH_SQL",
411
+ "LAKE_INFO_COLUMNS",
412
+ "LAKE_INFO_TABLE",
413
+ "LAKE_METADATA_ALIAS",
414
+ "LAKE_ROW_GROUP_SIZE",
415
+ "LAKE_SCHEMA_VERSION",
416
+ "LAKE_TABLES",
417
+ "LAKE_TABLE_NAMES",
418
+ "PARTITION_SQL",
419
+ "READER_SELECT_SQL",
420
+ "SESSION_KEY",
421
+ "SOURCE_ORDERED_TABLES",
422
+ "LakeColumn",
423
+ "LakeTable",
424
+ "expected_lake_info",
425
+ "lake_info_mismatches",
426
+ "lake_schema_digest",
427
+ ]
@@ -0,0 +1,97 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """The statements that read one corpus's sessions back out of the lake, a batch at a time.
4
+
5
+ The LLM pipelines (atif-analytics) walk a corpus session by session: bounds
6
+ for every session, then the steps of the ones a gate or the renderer needs.
7
+ :class:`atif_duck.infrastructure.lake_sessions.LakeSessionReader` runs these
8
+ statements for them (atif-cli adapts its rows to atif-analytics' session
9
+ port, since the two packages may not import each other).
10
+
11
+ Every statement is a module constant over the lake tables of
12
+ :mod:`atif_duck.domain.lake`, so the AST audit in
13
+ ``tests/test_sql_text_boundaries.py`` reads them like any other constant. The
14
+ corpus and the session ids are bound parameters; a batch of ids is one
15
+ ``VARCHAR[]`` parameter.
16
+
17
+ Row order. A step's tool calls and results have no ordinal column; their
18
+ order is the order the writer inserted them in, which the lake keeps as
19
+ ``rowid`` (DuckLake numbers a data file's rows from its ``row_id_start``, and
20
+ merges keep the numbers). The writer inserts the step-level tables
21
+ (:data:`atif_duck.domain.lake.SOURCE_ORDERED_TABLES`) on one thread so that order is the source
22
+ order; the statements below sort by ``step_id`` and then ``rowid``.
23
+
24
+ Pure constants: no duckdb import, no filesystem access.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from atif_duck.domain.lake import LAKE_ALIAS, SESSION_KEY
30
+ from atif_duck.domain.raw_readers import CORPUS_COLUMN
31
+ from atif_duck.domain.sql_literal import SqlFragment
32
+
33
+ _SCOPE: str = f"{CORPUS_COLUMN} = ? AND {SESSION_KEY} IN (SELECT unnest(?::VARCHAR[]))"
34
+ _ORDER: str = f"ORDER BY {SESSION_KEY}, step_id, rowid"
35
+
36
+ #: ``(session_id, materialized_at, source_mtime_ns)`` for every session of a
37
+ #: corpus (param: corpus). What a session's ``meta.json`` said when the lake
38
+ #: loaded it, so a reader can tell a current session from one the lake holds an
39
+ #: older write of.
40
+ SESSION_FRESHNESS_SQL: SqlFragment = SqlFragment(
41
+ f"SELECT {SESSION_KEY}, materialized_at, source_mtime_ns " # noqa: S608 # nosec B608 - constants only; corpus is bound
42
+ f"FROM {LAKE_ALIAS}.session_meta WHERE {CORPUS_COLUMN} = ?"
43
+ )
44
+
45
+ #: ``(session_id, newest record ts)`` for every session of a corpus (param:
46
+ #: corpus): the checkpoint's first bound, from ``edges`` like the file path.
47
+ LAST_EDGE_TS_SQL: SqlFragment = SqlFragment(
48
+ f"SELECT {SESSION_KEY}, max(ts) FROM {LAKE_ALIAS}.edges " # noqa: S608 # nosec B608 - constants only; corpus is bound
49
+ f"WHERE {CORPUS_COLUMN} = ? GROUP BY 1"
50
+ )
51
+
52
+ #: Every step of a batch of sessions without its tool payloads (params:
53
+ #: corpus, ids): ``(session_id, step_id, ts, source, message, is_sidechain,
54
+ #: is_compact_summary, source_uuids)``.
55
+ TURNS_SQL: SqlFragment = SqlFragment(
56
+ "SELECT " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
57
+ f"{SESSION_KEY}, step_id, ts, source, message, is_sidechain, is_compact_summary, "
58
+ f"CAST(source_uuids AS VARCHAR) FROM {LAKE_ALIAS}.steps WHERE {_SCOPE} {_ORDER}"
59
+ )
60
+
61
+ #: ``(session_id, step_id)`` of every step with an error result in a batch
62
+ #: (params: corpus, ids). The gates need the flag, not the result bodies.
63
+ ERROR_STEPS_SQL: SqlFragment = SqlFragment(
64
+ f"SELECT DISTINCT {SESSION_KEY}, step_id FROM {LAKE_ALIAS}.tool_results " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
65
+ f"WHERE {_SCOPE} AND is_error"
66
+ )
67
+
68
+ #: Every tool call of a batch, in source order (params: corpus, ids):
69
+ #: ``(session_id, step_id, tool_name, tool_input)`` with the input as JSON text.
70
+ TOOL_CALLS_SQL: SqlFragment = SqlFragment(
71
+ f"SELECT {SESSION_KEY}, step_id, tool_name, CAST(tool_input AS VARCHAR) " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
72
+ f"FROM {LAKE_ALIAS}.tool_calls WHERE {_SCOPE} {_ORDER}"
73
+ )
74
+
75
+ #: Every tool result of a batch, in source order (params: corpus, ids):
76
+ #: ``(session_id, step_id, tool_use_id, content, is_error)`` with the content
77
+ #: as JSON text.
78
+ TOOL_RESULTS_SQL: SqlFragment = SqlFragment(
79
+ f"SELECT {SESSION_KEY}, step_id, tool_use_id, CAST(content AS VARCHAR), is_error " # noqa: S608 # nosec B608 - constants only; corpus and ids are bound
80
+ f"FROM {LAKE_ALIAS}.tool_results WHERE {_SCOPE} {_ORDER}"
81
+ )
82
+
83
+ #: The non-empty raw-record uuids of one session (params: corpus, session id).
84
+ EDGE_UUIDS_SQL: SqlFragment = SqlFragment(
85
+ f"SELECT DISTINCT uuid FROM {LAKE_ALIAS}.edges " # noqa: S608 # nosec B608 - constants only; corpus and the id are bound
86
+ f"WHERE {CORPUS_COLUMN} = ? AND {SESSION_KEY} = ? AND uuid IS NOT NULL AND uuid <> ''"
87
+ )
88
+
89
+ __all__ = [
90
+ "EDGE_UUIDS_SQL",
91
+ "ERROR_STEPS_SQL",
92
+ "LAST_EDGE_TS_SQL",
93
+ "SESSION_FRESHNESS_SQL",
94
+ "TOOL_CALLS_SQL",
95
+ "TOOL_RESULTS_SQL",
96
+ "TURNS_SQL",
97
+ ]
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """The raw readers' names and column shapes, shared by the registry and the lake.
4
+
5
+ The registry (:mod:`atif_duck.infrastructure.registry`) binds one relation per
6
+ corpus artifact kind under these names, and every business view reads them
7
+ and nothing else. The lake (:mod:`atif_duck.domain.lake`) derives its tables
8
+ from the same shapes, so a column declared here is declared once for both the
9
+ per-session path and the lake path.
10
+
11
+ The names are deliberately absent from :data:`atif_duck.domain.catalog.VIEW_NAMES`:
12
+ they describe corpus files rather than being queryable business surface.
13
+
14
+ Pure constants: no duckdb import, no filesystem access.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from atif_duck.domain.columnar import META_COLUMNAR_KEY, SESSION_COLUMNS
20
+
21
+ #: The trajectory's top-level members minus ``steps``, plus the two
22
+ #: path-derived keys. ``v_raw_trajectories`` carries these, then ``corpus``.
23
+ RAW_TRAJECTORIES: str = "v_raw_trajectories"
24
+ #: One row per ATIF step, in the ``steps`` view's column shape.
25
+ RAW_STEPS: str = "v_raw_steps"
26
+ #: One row per tool call, in the ``tool_calls`` view's column shape.
27
+ RAW_TOOL_CALLS: str = "v_raw_tool_calls"
28
+ #: One row per observation result, in the ``tool_results`` view's column shape.
29
+ RAW_TOOL_RESULTS: str = "v_raw_tool_results"
30
+ #: One row per kept non-message record, in the ``session_events`` view's shape.
31
+ RAW_SESSION_EVENTS: str = "v_raw_session_events"
32
+ #: One row per RAW transcript record, from ``edges.jsonl``.
33
+ RAW_EDGES: str = "v_raw_edges"
34
+ #: One row per session, from ``loss_report.json``.
35
+ RAW_LOSS_REPORTS: str = "v_raw_loss_reports"
36
+ #: One row per complete session, from ``meta.json``.
37
+ RAW_META: str = "v_raw_meta"
38
+
39
+ #: The column every raw reader except the step-level four keys its session
40
+ #: on: the ``sessions/<id>/`` directory name.
41
+ SESSION_ID_PATH: str = "session_id_path"
42
+
43
+ #: The ``corpus`` column ``v_raw_trajectories`` ends with: the corpus
44
+ #: directory's name, which is how the lake and the ``sessions`` view name a
45
+ #: corpus.
46
+ CORPUS_COLUMN: str = "corpus"
47
+
48
+ #: ``v_raw_trajectories`` columns before ``corpus``, in order: the
49
+ #: trajectory's top-level members minus ``steps``, then the two path-derived
50
+ #: keys.
51
+ TRAJECTORY_RELATION_COLUMNS: tuple[tuple[str, str], ...] = (
52
+ *(column for column in SESSION_COLUMNS if column[0] != SESSION_ID_PATH),
53
+ ("trajectory_path", "VARCHAR"),
54
+ (SESSION_ID_PATH, "VARCHAR"),
55
+ )
56
+
57
+ # Explicit projection for ``v_raw_edges``: one line per RAW transcript record
58
+ # per docs/CONTRACT.md. ``parent_uuid`` is declared VARCHAR outright: a root
59
+ # record leaves it null, so inferred typing resolves it as a NULL-vs-string
60
+ # JSON union and every downstream view then needs its own CAST. Declaring the
61
+ # type in the one explicit-columns reader pays that cost once.
62
+ EDGE_COLUMNS: dict[str, str] = {
63
+ "uuid": "VARCHAR",
64
+ "parent_uuid": "VARCHAR",
65
+ "message_id": "VARCHAR",
66
+ "type": "VARCHAR",
67
+ "ts": "TIMESTAMP",
68
+ "is_sidechain": "BOOLEAN",
69
+ "is_compact_summary": "BOOLEAN",
70
+ "source_file": "VARCHAR",
71
+ "tool_use_ids": "JSON",
72
+ }
73
+
74
+ # Explicit projection for ``v_raw_loss_reports``: atif_converter
75
+ # ``LossReport.to_json()`` shape (record_counts / gaps_observed stay JSON —
76
+ # enum-keyed dict and list respectively).
77
+ LOSS_REPORT_COLUMNS: dict[str, str] = {
78
+ "record_counts": "JSON",
79
+ "records_total": "BIGINT",
80
+ "records_converted": "BIGINT",
81
+ # Added with session_events (converter schema 2). A report written before
82
+ # then has no such key and reads as NULL, which the view keeps as NULL:
83
+ # "not measured" rather than "none captured".
84
+ "records_captured": "BIGINT",
85
+ "records_dropped": "BIGINT",
86
+ "gaps_observed": "JSON",
87
+ "subagent_files_found": "BIGINT",
88
+ "subagent_files_convertible": "BIGINT",
89
+ "workflow_subagent_files_found": "BIGINT",
90
+ }
91
+
92
+ #: The path column ``v_raw_loss_reports`` adds (the ``loss_reports`` view
93
+ #: returns it).
94
+ LOSS_REPORT_PATH_COLUMN: tuple[str, str] = ("report_path", "VARCHAR")
95
+
96
+ # Explicit projection for ``v_raw_meta`` per docs/CONTRACT.md. An explicit
97
+ # ``columns=`` projection nulls a missing key rather than failing, which is
98
+ # what lets a corpus written before a key existed still register.
99
+ META_COLUMNS: dict[str, str] = {
100
+ "session_id": "VARCHAR",
101
+ "source_mtime_ns": "BIGINT",
102
+ "source_files": "JSON",
103
+ "harbor_version": "VARCHAR",
104
+ "converter_version": "VARCHAR",
105
+ "materialized_at": "VARCHAR",
106
+ # Which agent wrote the transcript. Added 2026-09-11 with Codex support, so
107
+ # a session materialized before then has no such key and reads as NULL.
108
+ # Queries read ``sessions.agent`` (from the trajectory) instead; this
109
+ # column is provenance for an operator reading meta.json directly.
110
+ "agent": "VARCHAR",
111
+ # Which columnar schema the session's parquet artifacts were written
112
+ # against. Absent (NULL) on a session materialized before the artifacts
113
+ # existed; that session is read from trajectory.json, which is always
114
+ # correct.
115
+ META_COLUMNAR_KEY: "BIGINT",
116
+ # The converter's output version, and whether the source transcript still
117
+ # exists. NULL on a session written before the key existed.
118
+ "converter_schema": "BIGINT",
119
+ "source_present": "BOOLEAN",
120
+ "source_removed_at": "VARCHAR",
121
+ }
122
+
123
+
124
+ __all__ = [
125
+ "CORPUS_COLUMN",
126
+ "EDGE_COLUMNS",
127
+ "LOSS_REPORT_COLUMNS",
128
+ "LOSS_REPORT_PATH_COLUMN",
129
+ "META_COLUMNS",
130
+ "RAW_EDGES",
131
+ "RAW_LOSS_REPORTS",
132
+ "RAW_META",
133
+ "RAW_SESSION_EVENTS",
134
+ "RAW_STEPS",
135
+ "RAW_TOOL_CALLS",
136
+ "RAW_TOOL_RESULTS",
137
+ "RAW_TRAJECTORIES",
138
+ "SESSION_ID_PATH",
139
+ "TRAJECTORY_RELATION_COLUMNS",
140
+ ]