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,149 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """The typed columnar artifact contract: names, schema version, column shapes.
4
+
5
+ Beside the JSON artifacts in ``<corpus_root>/sessions/<id>/``, a session
6
+ may carry five Parquet files that hold what the transcript-derived views need,
7
+ already typed, so a query never parses ``trajectory.json``:
8
+
9
+ * ``session.parquet`` one row: the trajectory's top-level members minus
10
+ ``steps`` (what the ``sessions`` view reads), keyed by ``session_id_path``.
11
+ * ``steps.parquet`` one row per ATIF step, exactly the ``steps`` view's
12
+ columns.
13
+ * ``tool_calls.parquet`` one row per tool call, exactly the ``tool_calls``
14
+ view's columns.
15
+ * ``tool_results.parquet`` one row per observation result, exactly the
16
+ ``tool_results`` view's columns.
17
+ * ``session_events.parquet`` one row per kept non-message record, exactly the
18
+ ``session_events`` view's columns (schema 2 onward). Built from the staged
19
+ ``session_events.jsonl``, not from the trajectory.
20
+
21
+ atif-duck owns this shape (the views define what the columns mean) and writes
22
+ it through :class:`atif_duck.infrastructure.columnar.ColumnarArtifactProducer`;
23
+ atif-corpus calls that producer through its ``ArtifactProducer`` port without
24
+ knowing these names. The registry reads a session from these files only when
25
+ ``meta.json`` carries ``columnar_schema`` equal to
26
+ :data:`COLUMNAR_SCHEMA_VERSION` and all five files are present; otherwise it
27
+ reads the session's ``trajectory.json`` as before, so a corpus materialized
28
+ before this contract existed (or by a build with a different schema version)
29
+ still queries correctly.
30
+
31
+ Pure constants and path arithmetic: no duckdb import, no filesystem access.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import TYPE_CHECKING
37
+
38
+ from atif_duck.domain import artifacts
39
+ from atif_duck.domain.catalog import VIEW_SCHEMA
40
+
41
+ if TYPE_CHECKING:
42
+ from pathlib import Path
43
+
44
+ #: Bump when any column, type, or filename below changes. A session whose
45
+ #: ``meta.json`` names another version is read from ``trajectory.json``.
46
+ #:
47
+ #: 2: ``session_events.parquet`` added; ``steps`` gained ``agent_id`` /
48
+ #: ``images``; ``tool_results`` gained ``is_error`` / ``exit_code`` /
49
+ #: ``interrupted`` / ``images``. One bump for both changes: they ship together,
50
+ #: and no corpus was ever stamped with only one of them, so a single version
51
+ #: separates every schema-1 session from every schema-2 one.
52
+ COLUMNAR_SCHEMA_VERSION: int = 2
53
+
54
+ #: The ``meta.json`` key that records which columnar schema a session's
55
+ #: parquet files were written against. Absent on sessions materialized
56
+ #: before the columnar artifacts existed, which is a legal, JSON-read state.
57
+ META_COLUMNAR_KEY: str = "columnar_schema"
58
+
59
+ SESSION_PARQUET: str = "session.parquet"
60
+ STEPS_PARQUET: str = "steps.parquet"
61
+ TOOL_CALLS_PARQUET: str = "tool_calls.parquet"
62
+ TOOL_RESULTS_PARQUET: str = "tool_results.parquet"
63
+ #: Added in schema 2: the ``session_events`` view's rows, built from the staged
64
+ #: ``session_events.jsonl`` the corpus writer puts beside ``edges.jsonl``.
65
+ SESSION_EVENTS_PARQUET: str = "session_events.parquet"
66
+
67
+ #: Every columnar filename, in the order the producer writes them.
68
+ COLUMNAR_FILENAMES: tuple[str, ...] = (
69
+ SESSION_PARQUET,
70
+ STEPS_PARQUET,
71
+ TOOL_CALLS_PARQUET,
72
+ TOOL_RESULTS_PARQUET,
73
+ SESSION_EVENTS_PARQUET,
74
+ )
75
+
76
+ #: The JSON artifact the ``session_events`` rows come from (atif-corpus writes
77
+ #: it, compressed on a current corpus: see :mod:`atif_duck.domain.artifacts`).
78
+ SESSION_EVENTS_JSONL: str = artifacts.SESSION_EVENTS_JSONL
79
+
80
+ #: ``session.parquet`` columns. ``session_id_path`` is the session directory
81
+ #: name, the canonical key every view uses as ``session_id``; the rest are the
82
+ #: trajectory's top-level members with the types the JSON reader projects them
83
+ #: to (``atif_duck.infrastructure.registry._TRAJECTORY_COLUMNS``), minus
84
+ #: ``steps``.
85
+ SESSION_COLUMNS: tuple[tuple[str, str], ...] = (
86
+ ("session_id_path", "VARCHAR"),
87
+ ("schema_version", "VARCHAR"),
88
+ ("session_id", "VARCHAR"),
89
+ ("trajectory_id", "VARCHAR"),
90
+ ("agent", "STRUCT(name VARCHAR, version VARCHAR, model_name VARCHAR, extra JSON)"),
91
+ (
92
+ "final_metrics",
93
+ (
94
+ "STRUCT(total_prompt_tokens BIGINT, total_completion_tokens BIGINT, "
95
+ "total_cached_tokens BIGINT, total_cost_usd DOUBLE, total_steps BIGINT, extra JSON)"
96
+ ),
97
+ ),
98
+ ("extra", "JSON"),
99
+ )
100
+
101
+ #: Column shape per columnar file. The three step-derived files carry exactly
102
+ #: their view's catalog columns, so the catalog stays the one place a column
103
+ #: is declared and the schema drift test covers these files too.
104
+ COLUMNAR_SCHEMAS: dict[str, tuple[tuple[str, str], ...]] = {
105
+ SESSION_PARQUET: SESSION_COLUMNS,
106
+ STEPS_PARQUET: VIEW_SCHEMA["steps"],
107
+ TOOL_CALLS_PARQUET: VIEW_SCHEMA["tool_calls"],
108
+ TOOL_RESULTS_PARQUET: VIEW_SCHEMA["tool_results"],
109
+ SESSION_EVENTS_PARQUET: VIEW_SCHEMA["session_events"],
110
+ }
111
+
112
+ #: Smallest byte count a readable parquet file can have (``PAR1`` + footer +
113
+ #: footer length + ``PAR1``). A shorter file is what a crashed writer leaves
114
+ #: behind; the registry treats it as absent and reads the session from JSON.
115
+ MIN_PARQUET_BYTES: int = 16
116
+
117
+
118
+ def columnar_paths(session_dir: Path) -> tuple[Path, ...]:
119
+ """The five columnar artifact paths inside one session directory."""
120
+ return tuple(session_dir / name for name in COLUMNAR_FILENAMES)
121
+
122
+
123
+ def is_current_columnar_schema(value: object) -> bool:
124
+ """True when a ``meta.json`` ``columnar_schema`` value names this build's schema.
125
+
126
+ Anything but the exact integer (absent key, ``None``, a string, another
127
+ version) answers ``False`` and sends the session down the JSON path.
128
+ """
129
+ return (
130
+ isinstance(value, int) and not isinstance(value, bool) and value == COLUMNAR_SCHEMA_VERSION
131
+ )
132
+
133
+
134
+ __all__ = [
135
+ "COLUMNAR_FILENAMES",
136
+ "COLUMNAR_SCHEMAS",
137
+ "COLUMNAR_SCHEMA_VERSION",
138
+ "META_COLUMNAR_KEY",
139
+ "MIN_PARQUET_BYTES",
140
+ "SESSION_COLUMNS",
141
+ "SESSION_EVENTS_JSONL",
142
+ "SESSION_EVENTS_PARQUET",
143
+ "SESSION_PARQUET",
144
+ "STEPS_PARQUET",
145
+ "TOOL_CALLS_PARQUET",
146
+ "TOOL_RESULTS_PARQUET",
147
+ "columnar_paths",
148
+ "is_current_columnar_schema",
149
+ ]
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Fail-loud embedding provider/dimension guard for the VSS bind path.
4
+
5
+ DELIBERATE TWIN of ``atif_embed.domain.embedding_guard``: atif-duck may never
6
+ import atif-embed (import-linter independence contract — only atif-cli
7
+ composes), and this is a tiny pure rule (string/int comparison, no I/O), so
8
+ each package carries its own copy rather than growing a shared-kernel package
9
+ for one function. ``test_guard_twin_pin.py`` is what keeps the copies honest.
10
+ The two copies are coupled through the CONTRACT's store-stamp semantics; a
11
+ behavioral change in one must be ported to the other.
12
+
13
+ The read/bind path (``register_vss``) reads the Lance store's stamped
14
+ ``(model, dim)`` back and calls :func:`ensure_store_matches` BEFORE binding
15
+ the ``message_embeddings`` view — a store written by a different provider
16
+ raises rather than serving numerically valid but garbage cosine scores.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ #: Recovery instruction appended to the mismatch message. The store lives at
22
+ #: ``<corpus_root>/embeddings_lance`` by default, NOT under a fixed home
23
+ #: directory — naming a path the store isn't at makes the operator delete
24
+ #: nothing and stay broken. Kept identical to atif-embed's twin copy.
25
+ RECOVERY_HINT = (
26
+ "Re-embed under the new provider: rm -rf the Lance store directory "
27
+ "(ATIF_SQL_LANCE_URI, or <corpus-root>/embeddings_lance by default) "
28
+ "and re-run `atif-sql embed --all --no-dry-run` (a bare `atif-sql embed` "
29
+ "exits 64: a real run needs an explicit scope)."
30
+ )
31
+
32
+
33
+ class EmbeddingProviderMismatch(Exception): # noqa: N818 — names a store state, not an "*Error"
34
+ """Raised on a stamped-vs-active ``(model, dim)`` mismatch.
35
+
36
+ The Lance store records the identity of the embedder that wrote it, and it
37
+ differs from the identity of the embedder now asking to read or extend it.
38
+
39
+ The name carries no ``Error`` suffix because it names a STATE OF THE STORE
40
+ that is true before anything is attempted, not a fault that occurred while
41
+ doing something. Nothing has gone wrong at runtime: two consistent stores
42
+ simply cannot be the same store.
43
+
44
+ Different embedding models produce vectors in incompatible spaces, so
45
+ querying across a provider switch yields numerically valid but
46
+ semantically garbage cosine scores. This error is terminal: the store
47
+ must be dropped and re-embedded under the new provider.
48
+ """
49
+
50
+
51
+ def ensure_store_matches(
52
+ *,
53
+ stored_model: str | None,
54
+ stored_dim: int | None,
55
+ expected_model: str,
56
+ expected_dim: int | None,
57
+ ) -> None:
58
+ """Fail loud if a store's stamped identity differs from the active embedder.
59
+
60
+ ``None`` for either stored value means the store is empty (fresh
61
+ install) and any provider may claim it, so the check is a no-op.
62
+ ``model_id`` is the primary identity; ``expected_dim`` is also checked
63
+ when supplied (Cohere's single model id can emit different Matryoshka
64
+ widths). ``expected_dim=None`` trusts ``model_id`` alone.
65
+ """
66
+ if stored_model is None or stored_dim is None:
67
+ return
68
+ model_ok = stored_model == expected_model
69
+ dim_ok = expected_dim is None or stored_dim == expected_dim
70
+ if model_ok and dim_ok:
71
+ return
72
+ # The message stays INSIDE the raise: `test_guard_twin_pin.py` reads both
73
+ # twin modules as source text and requires `{RECOVERY_HINT}` to appear after
74
+ # the `raise EmbeddingProviderMismatch` keyword, which is what proves the
75
+ # shared constant reaches the operator rather than only sitting in the
76
+ # module. Hoisting the message to a local satisfies EM102 / TRY003 and
77
+ # defeats that pin, so both are suppressed here instead.
78
+ raise EmbeddingProviderMismatch( # noqa: TRY003
79
+ "Embedding store was written by a different provider/model. " # noqa: EM102
80
+ f"stored=(model={stored_model!r}, dim={stored_dim}) "
81
+ f"active=(model={expected_model!r}, dim={expected_dim}). "
82
+ "Vectors from different models live in incompatible spaces (even at "
83
+ "matching dimensions), so mixing them silently corrupts kNN search. "
84
+ f"{RECOVERY_HINT}"
85
+ )
86
+
87
+
88
+ __all__ = ["RECOVERY_HINT", "EmbeddingProviderMismatch", "ensure_store_matches"]
@@ -0,0 +1,265 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Derived, test-executed example queries for the atif-duck SQL surface.
4
+
5
+ Operator's principle: DON'T hardcode example strings — they rot the moment a
6
+ macro signature or view changes. Every example here is DERIVED from the
7
+ static catalogs (:data:`~atif_duck.domain.catalog.VIEW_NAMES`,
8
+ :data:`~atif_duck.domain.catalog.ANALYTICS_VIEW_NAMES`,
9
+ :data:`~atif_duck.domain.catalog.MACRO_SIGNATURES`,
10
+ :data:`~atif_duck.domain.catalog.ANALYTICS_MACRO_SIGNATURES`) plus one small
11
+ exemplar table mapping parameter NAMES to literal SQL argument text
12
+ (:data:`ARG_EXEMPLARS`). The catalogs are already drift-pinned against the
13
+ real DDL by the tests in ``tests/test_duck_views.py`` /
14
+ ``tests/test_analytics_views.py``, so the examples inherit that freshness.
15
+
16
+ THE TEST IS THE CONTRACT: ``tests/test_examples.py`` EXECUTES every example
17
+ :func:`build_examples` emits against the fixture corpus (analytics parquets
18
+ registered, tiny Lance store attached). An example that stops running stops
19
+ shipping — the CLI surface (``atif-sql examples``) can honestly claim
20
+ "test-executed against this version".
21
+
22
+ The only hand-maintained prose is
23
+ :data:`~atif_duck.domain.catalog.DESCRIPTIONS` in ``catalog.py``; a coverage
24
+ drift test forces one description per catalog object.
25
+
26
+ Pure domain module: no duckdb import, no I/O — safe on the CLI's lean
27
+ import path (``schema`` / ``examples`` / ``--help``).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass
33
+ from typing import Literal, cast, get_args
34
+
35
+ from atif_duck.domain.catalog import (
36
+ ANALYTICS_MACRO_SIGNATURES,
37
+ ANALYTICS_VIEW_NAMES,
38
+ ANALYTICS_VIEW_SCHEMA,
39
+ DESCRIPTIONS,
40
+ MACRO_SIGNATURES,
41
+ TABLE_MACRO_NAMES,
42
+ VIEW_NAMES,
43
+ VIEW_SCHEMA,
44
+ )
45
+
46
+ #: What an example needs registered before it runs: the base corpus views
47
+ #: (``core``), the analytics parquet views (``analytics``), or the
48
+ #: Lance-backed embeddings surface (``vss``).
49
+ Requires = Literal["core", "analytics", "vss"]
50
+
51
+ #: Structural kind, derived from which catalog the object lives in and
52
+ #: whether its DDL is ``AS TABLE`` (see
53
+ #: :data:`~atif_duck.domain.catalog.TABLE_MACRO_NAMES`).
54
+ Category = Literal["view", "table-macro", "scalar-macro"]
55
+
56
+ REQUIRES_VALUES: tuple[str, ...] = cast("tuple[str, ...]", get_args(Requires))
57
+ CATEGORY_VALUES: tuple[str, ...] = cast("tuple[str, ...]", get_args(Category))
58
+
59
+ #: Exemplar SQL argument text per macro parameter NAME. The single seam
60
+ #: between the derived examples and real macro signatures: a new parameter
61
+ #: name lands here (one line) or ``build_examples`` fails loud — and the
62
+ #: executing test in ``tests/test_examples.py`` fails with it.
63
+ #:
64
+ #: * ``sid`` — a real session id, picked from the corpus itself so the
65
+ #: example runs verbatim on ANY materialized corpus.
66
+ #: * ``query_vec`` — a stored embedding L2-NORMALIZED into a unit-norm probe,
67
+ #: matching what ``embed_query`` returns, so the example runs on any corpus
68
+ #: without fabricating a dim-wide literal. The normalization is not
69
+ #: cosmetic: stored vectors are un-normalized, and a RAW stored vector is
70
+ #: its own nearest neighbor under every metric, so an un-normalized probe
71
+ #: would make this example score sim == 1.0 no matter which metric the macro
72
+ #: ranks by — it could not tell a correct macro from a broken one. For TEXT
73
+ #: queries the flow is two-step: embed the query text first (``atif-sql
74
+ #: search`` wraps both steps); see
75
+ #: :data:`~atif_duck.domain.catalog.DESCRIPTIONS`.
76
+ ARG_EXEMPLARS: dict[str, str] = {
77
+ "sid": "(SELECT session_id FROM sessions LIMIT 1)",
78
+ "since_days": "30",
79
+ "last_n_days": "30",
80
+ "k": "10",
81
+ "n": "10",
82
+ "label_name": "'correction'",
83
+ "signal_name": "'correction'",
84
+ "src": "'user'",
85
+ "msg": "'Stop hook feedback: the claim has no evidence'",
86
+ "interval_text": "'7 days'",
87
+ "query_vec": (
88
+ "(SELECT list_transform("
89
+ "me.embedding, v -> (v / sqrt(list_sum("
90
+ "list_transform(me.embedding, x -> x::DOUBLE * x))))::FLOAT"
91
+ ") FROM message_embeddings me LIMIT 1)"
92
+ ),
93
+ }
94
+
95
+ #: Catalog objects DELIBERATELY without an example, mapped to the reason.
96
+ #: The coverage drift test treats these as documented exclusions; everything
97
+ #: else in the catalogs MUST yield an example. Currently empty — every view
98
+ #: and macro is exampled — but the seam stays so a future exclusion is a
99
+ #: reviewed one-liner instead of a silent coverage hole.
100
+ EXCLUSIONS: dict[str, str] = {}
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class ExampleQuery:
105
+ """One runnable example query, derived from the catalog.
106
+
107
+ Attributes
108
+ ----------
109
+ name
110
+ The catalog object (view or macro) the example demonstrates.
111
+ sql
112
+ A complete, runnable statement for ``atif-sql query``.
113
+ description
114
+ Agent-facing prose from :data:`~atif_duck.domain.catalog.DESCRIPTIONS`.
115
+ requires
116
+ ``core`` | ``analytics`` | ``vss`` — what must be registered/populated.
117
+ category
118
+ ``view`` | ``table-macro`` | ``scalar-macro``.
119
+ """
120
+
121
+ name: str
122
+ sql: str
123
+ description: str
124
+ requires: Requires
125
+ category: Category
126
+
127
+
128
+ #: Catalog objects that need the embeddings store (``atif-sql embed``).
129
+ _VSS_OBJECTS: frozenset[str] = frozenset({"message_embeddings", "semantic_search"})
130
+
131
+
132
+ def object_requires(name: str) -> Requires:
133
+ """What must be registered/populated before catalog object ``name`` binds.
134
+
135
+ ``analytics`` for the parquet-gated analytics views and macros, ``vss``
136
+ for the embeddings surface, ``core`` for everything the corpus alone
137
+ registers (the authorship views included). ``atif-sql schema`` prints
138
+ this beside every view and macro, and each derived example carries it.
139
+ """
140
+ if name in ANALYTICS_VIEW_SCHEMA or name in ANALYTICS_MACRO_SIGNATURES:
141
+ return "analytics"
142
+ return "vss" if name in _VSS_OBJECTS else "core"
143
+
144
+
145
+ def _view_sql(name: str) -> str:
146
+ """Canonical top-N SELECT for one view.
147
+
148
+ When the static :data:`~atif_duck.domain.catalog.VIEW_SCHEMA` (or
149
+ :data:`~atif_duck.domain.catalog.ANALYTICS_VIEW_SCHEMA`) declares a
150
+ timestamp column, the example orders by the FIRST one descending
151
+ (newest-first is the agent's default question); otherwise a plain LIMIT.
152
+ """
153
+ schema = VIEW_SCHEMA.get(name) or ANALYTICS_VIEW_SCHEMA.get(name, ())
154
+ ts_col = next((col for col, typ in schema if typ.startswith("TIMESTAMP")), None)
155
+ if ts_col is not None:
156
+ # The view name and its timestamp column both come from the static catalog.
157
+ return f"SELECT * FROM {name} ORDER BY {ts_col} DESC LIMIT 10" # noqa: S608 # nosec B608 - view name and column are VIEW_SCHEMA constants
158
+ return f"SELECT * FROM {name} LIMIT 10" # noqa: S608 # nosec B608 - view name is a VIEW_SCHEMA constant
159
+
160
+
161
+ def _macro_sql(name: str, params: tuple[str, ...]) -> str:
162
+ """Runnable invocation for one macro, args filled from the exemplars."""
163
+ try:
164
+ args = ", ".join(ARG_EXEMPLARS[param] for param in params)
165
+ except KeyError as exc:
166
+ msg = (
167
+ f"macro {name!r} has parameter {exc.args[0]!r} with no entry in "
168
+ "ARG_EXEMPLARS — add an exemplar literal so build_examples() can "
169
+ "derive a runnable example"
170
+ )
171
+ raise KeyError(msg) from exc
172
+ if name in TABLE_MACRO_NAMES:
173
+ # The macro name comes from MACRO_SIGNATURES and args from ARG_EXEMPLARS.
174
+ return f"SELECT * FROM {name}({args}) LIMIT 10" # noqa: S608 # nosec B608 - macro name from MACRO_SIGNATURES, args from ARG_EXEMPLARS
175
+ return f"SELECT {name}({args}) AS {name}"
176
+
177
+
178
+ def _description(name: str) -> str:
179
+ """DESCRIPTIONS lookup that fails loud on a missing catalog object."""
180
+ try:
181
+ return DESCRIPTIONS[name]
182
+ except KeyError as exc:
183
+ msg = (
184
+ f"catalog object {name!r} has no entry in "
185
+ "atif_duck.domain.catalog.DESCRIPTIONS — add one (the drift test "
186
+ "in tests/test_examples.py pins this coverage)"
187
+ )
188
+ raise KeyError(msg) from exc
189
+
190
+
191
+ def build_examples() -> tuple[ExampleQuery, ...]:
192
+ """Derive the full example inventory from the catalogs.
193
+
194
+ Order: core views, VSS view, analytics views, then core macros, VSS
195
+ macro, analytics macros — stable, so the CLI listing and the JSON array
196
+ are deterministic across runs.
197
+ """
198
+ examples: list[ExampleQuery] = []
199
+
200
+ for view in VIEW_NAMES:
201
+ if view in EXCLUSIONS:
202
+ continue
203
+ examples.append(
204
+ ExampleQuery(
205
+ name=view,
206
+ sql=_view_sql(view),
207
+ description=_description(view),
208
+ requires=object_requires(view),
209
+ category="view",
210
+ )
211
+ )
212
+
213
+ for view in ANALYTICS_VIEW_NAMES:
214
+ if view in EXCLUSIONS:
215
+ continue
216
+ examples.append(
217
+ ExampleQuery(
218
+ name=view,
219
+ sql=_view_sql(view),
220
+ description=_description(view),
221
+ requires=object_requires(view),
222
+ category="view",
223
+ )
224
+ )
225
+
226
+ for macro, params in MACRO_SIGNATURES.items():
227
+ if macro in EXCLUSIONS:
228
+ continue
229
+ examples.append(
230
+ ExampleQuery(
231
+ name=macro,
232
+ sql=_macro_sql(macro, params),
233
+ description=_description(macro),
234
+ requires=object_requires(macro),
235
+ category="table-macro" if macro in TABLE_MACRO_NAMES else "scalar-macro",
236
+ )
237
+ )
238
+
239
+ for macro, params in ANALYTICS_MACRO_SIGNATURES.items():
240
+ if macro in EXCLUSIONS:
241
+ continue
242
+ examples.append(
243
+ ExampleQuery(
244
+ name=macro,
245
+ sql=_macro_sql(macro, params),
246
+ description=_description(macro),
247
+ requires=object_requires(macro),
248
+ category="table-macro" if macro in TABLE_MACRO_NAMES else "scalar-macro",
249
+ )
250
+ )
251
+
252
+ return tuple(examples)
253
+
254
+
255
+ __all__ = [
256
+ "ARG_EXEMPLARS",
257
+ "CATEGORY_VALUES",
258
+ "EXCLUSIONS",
259
+ "REQUIRES_VALUES",
260
+ "Category",
261
+ "ExampleQuery",
262
+ "Requires",
263
+ "build_examples",
264
+ "object_requires",
265
+ ]