frontier-runner 0.2.0__tar.gz → 0.2.2__tar.gz

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 (75) hide show
  1. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/CHANGELOG.md +16 -0
  2. {frontier_runner-0.2.0/frontier_runner.egg-info → frontier_runner-0.2.2}/PKG-INFO +4 -4
  3. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/README.md +3 -3
  4. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/__init__.py +1 -1
  5. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/base.py +19 -0
  6. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/bigquery.py +17 -0
  7. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/snowflake.py +162 -3
  8. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/api.py +9 -0
  9. frontier_runner-0.2.2/frontier/certification.py +263 -0
  10. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cli.py +905 -89
  11. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/comment.py +94 -12
  12. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/compare.py +233 -15
  13. frontier_runner-0.2.2/frontier/environment.py +86 -0
  14. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/execute.py +493 -11
  15. frontier_runner-0.2.2/frontier/filter_v1.py +2423 -0
  16. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/frontier.py +27 -8
  17. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/impact.py +4 -1
  18. frontier_runner-0.2.2/frontier/mutation.py +373 -0
  19. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/permissions.py +7 -0
  20. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/saas.py +1 -1
  21. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/progress.py +37 -4
  22. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/proof.py +209 -46
  23. frontier_runner-0.2.2/frontier/repair.py +394 -0
  24. frontier_runner-0.2.2/frontier/snapshot.py +643 -0
  25. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/sql_fingerprint.py +29 -0
  26. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/validation.py +8 -3
  27. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/warehouse.py +185 -0
  28. {frontier_runner-0.2.0 → frontier_runner-0.2.2/frontier_runner.egg-info}/PKG-INFO +4 -4
  29. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/SOURCES.txt +6 -0
  30. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/pyproject.toml +1 -1
  31. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/LICENSE +0 -0
  32. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/MANIFEST.in +0 -0
  33. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/__main__.py +0 -0
  34. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/__init__.py +0 -0
  35. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/databricks.py +0 -0
  36. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/postgres.py +0 -0
  37. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/adapters/redshift.py +0 -0
  38. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/artifacts.py +0 -0
  39. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/__init__.py +0 -0
  40. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/config.py +0 -0
  41. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/consume.py +0 -0
  42. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/normalize.py +0 -0
  43. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/prove.py +0 -0
  44. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/route.py +0 -0
  45. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/store.py +0 -0
  46. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/streams.py +0 -0
  47. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/cdc/upload.py +0 -0
  48. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/config.py +0 -0
  49. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/credentials.py +0 -0
  50. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/dbt_artifacts.py +0 -0
  51. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/errors.py +0 -0
  52. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/github.py +0 -0
  53. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/hashing.py +0 -0
  54. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/local_config.py +0 -0
  55. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/__init__.py +0 -0
  56. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/commands.py +0 -0
  57. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/constants.py +0 -0
  58. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/demo.py +0 -0
  59. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/detect.py +0 -0
  60. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/discover.py +0 -0
  61. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/doctor.py +0 -0
  62. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/github.py +0 -0
  63. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/gitignore.py +0 -0
  64. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/hashkey.py +0 -0
  65. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/prompt.py +0 -0
  66. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/routes.py +0 -0
  67. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/onboard/versions.py +0 -0
  68. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/semantic.py +0 -0
  69. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/snowflake.py +0 -0
  70. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier/snowflake_sql.py +0 -0
  71. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/dependency_links.txt +0 -0
  72. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/entry_points.txt +0 -0
  73. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/requires.txt +0 -0
  74. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/frontier_runner.egg-info/top_level.txt +0 -0
  75. {frontier_runner-0.2.0 → frontier_runner-0.2.2}/setup.cfg +0 -0
@@ -5,6 +5,20 @@ All notable changes to frontier-runner are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] — 2026-09-22
9
+
10
+ - Disposable repair validation is the authoritative repaired-vs-head check. The unchanged materialized mart versus head comparison is an expected pre-repair difference and no longer fails the run.
11
+ - `assert_repaired_equals_reference`, mismatch counts, targeted-repair safety, GitHub comments, and run detail now follow `repairValidation`. Production apply remains not requested unless explicitly applied.
12
+ - Mart baseline, candidate certification, disposable repair, production apply, economics, and execution stay independent dimensions. `NOT_EVALUATED` economics does not fail a correct assessment.
13
+
14
+ ## [0.2.1] — 2026-09-22
15
+
16
+ - Made filter-v1 eligibility and the RowCover candidate compiler authoritative for `compare` and `prove`. Ineligible plans stay `UNCERTIFIED` and are not reported as safe for targeted repair.
17
+ - Failed `prove` writes a fresh uniquely identified run artifact. `upload` refuses a stale `frontier-run.json`.
18
+ - Targeted SQL generation failures are classified as execution failures instead of being swallowed.
19
+ - Mixed compiled dbt environments fail before warehouse execution with `ENVIRONMENT_MISMATCH`.
20
+ - Economics stay independent of `SQL_CERTIFIED` and use measured bytes rather than a demonstrated cost saving.
21
+
8
22
  ## [0.2.0] — 2026-09-21
9
23
 
10
24
  - Added browser-based CLI authentication.
@@ -86,6 +100,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
86
100
  - SaaS commands resolve stored keychain credentials through one shared
87
101
  resolver instead of requiring `FRONTIER_API_KEY` in the environment.
88
102
 
103
+ [0.2.2]: https://github.com/jadsamara/frontier-runner/compare/v0.2.1...v0.2.2
104
+ [0.2.1]: https://github.com/jadsamara/frontier-runner/compare/v0.2.0...v0.2.1
89
105
  [0.2.0]: https://github.com/jadsamara/frontier-runner/compare/v0.1.3...v0.2.0
90
106
  [0.1.3]: https://github.com/jadsamara/frontier-runner/compare/v0.1.2...v0.1.3
91
107
  [0.1.2]: https://github.com/jadsamara/frontier-runner/compare/v0.1.1...v0.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontier-runner
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Customer-side Frontier CLI for dbt + Snowflake or BigQuery + GitHub impact assessments.
5
5
  Author: Frontier
6
6
  License: MIT License
@@ -92,7 +92,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
92
92
  tag. You do not need a commit SHA:
93
93
 
94
94
  ```bash
95
- pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.0/frontier_runner-0.2.0-py3-none-any.whl"
95
+ pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.2/frontier_runner-0.2.2-py3-none-any.whl"
96
96
  pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
97
97
  ```
98
98
 
@@ -222,9 +222,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
222
222
  Pin an immutable released version:
223
223
 
224
224
  ```bash
225
- pip install "frontier-runner[snowflake]==0.2.0"
225
+ pip install "frontier-runner[snowflake]==0.2.2"
226
226
  # or
227
- pip install "frontier-runner[bigquery]==0.2.0"
227
+ pip install "frontier-runner[bigquery]==0.2.2"
228
228
  ```
229
229
 
230
230
  Until PyPI trusted publishing is reviewed and live, install the GitHub Release
@@ -34,7 +34,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
34
34
  tag. You do not need a commit SHA:
35
35
 
36
36
  ```bash
37
- pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.0/frontier_runner-0.2.0-py3-none-any.whl"
37
+ pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.2/frontier_runner-0.2.2-py3-none-any.whl"
38
38
  pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
39
39
  ```
40
40
 
@@ -164,9 +164,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
164
164
  Pin an immutable released version:
165
165
 
166
166
  ```bash
167
- pip install "frontier-runner[snowflake]==0.2.0"
167
+ pip install "frontier-runner[snowflake]==0.2.2"
168
168
  # or
169
- pip install "frontier-runner[bigquery]==0.2.0"
169
+ pip install "frontier-runner[bigquery]==0.2.2"
170
170
  ```
171
171
 
172
172
  Until PyPI trusted publishing is reviewed and live, install the GitHub Release
@@ -1,3 +1,3 @@
1
1
  """Customer-side Frontier runner."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -75,3 +75,22 @@ class CursorAdapter:
75
75
 
76
76
  def get_query_history(self, run_id: str) -> list[dict[str, Any]]:
77
77
  return []
78
+
79
+ def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
80
+ from frontier.snapshot import SOURCE_SNAPSHOT_NOT_PINNED, unpinned_snapshot
81
+
82
+ del relations, kwargs
83
+ return unpinned_snapshot(
84
+ failure_code=SOURCE_SNAPSHOT_NOT_PINNED,
85
+ failure_reason=f"{self.warehouse_type} does not pin source snapshots",
86
+ )
87
+
88
+ def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
89
+ del snapshot
90
+ return sql
91
+
92
+ def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
93
+ from frontier.snapshot import ASSURANCE_ADAPTER
94
+
95
+ del sql
96
+ return getattr(snapshot, "assurance", None) == ASSURANCE_ADAPTER and False
@@ -183,6 +183,23 @@ class BigQueryAdapter:
183
183
  "location": self.location,
184
184
  }
185
185
 
186
+ def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
187
+ from frontier.snapshot import SOURCE_SNAPSHOT_NOT_PINNED, unpinned_snapshot
188
+
189
+ del relations, kwargs
190
+ return unpinned_snapshot(
191
+ failure_code=SOURCE_SNAPSHOT_NOT_PINNED,
192
+ failure_reason="BigQuery source snapshots are not implemented",
193
+ )
194
+
195
+ def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
196
+ del snapshot
197
+ return sql
198
+
199
+ def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
200
+ del sql, snapshot
201
+ return False
202
+
186
203
  def close(self) -> None:
187
204
  client = self._client
188
205
  if client is not None and hasattr(client, "close"):
@@ -124,7 +124,8 @@ class SnowflakeAdapter(CursorAdapter):
124
124
  return {}
125
125
  try:
126
126
  rows = self.execute(
127
- "select query_id, bytes_scanned, partitions_scanned, rows_produced "
127
+ "select query_id, bytes_scanned, rows_produced, total_elapsed_time, "
128
+ "credits_used_cloud_services "
128
129
  "from table(information_schema.query_history()) "
129
130
  f"where query_id = {sql_string(token)} "
130
131
  "order by start_time desc limit 1"
@@ -134,13 +135,171 @@ class SnowflakeAdapter(CursorAdapter):
134
135
  if not rows:
135
136
  return {}
136
137
  row = rows[0]
138
+ elapsed = row[3] if len(row) > 3 else None
137
139
  return {
138
140
  "query_id": row[0],
139
141
  "bytes_scanned": row[1] if len(row) > 1 else None,
140
- "partitions_scanned": row[2] if len(row) > 2 else None,
141
- "rows_produced": row[3] if len(row) > 3 else None,
142
+ "rows_produced": row[2] if len(row) > 2 else None,
143
+ "elapsed_ms": elapsed,
144
+ "total_elapsed_ms": elapsed,
145
+ "cloud_services_credits": float(row[4]) if len(row) > 4 and row[4] is not None else None,
142
146
  }
143
147
 
148
+ def capture_snapshot(self, relations: list[str] | tuple[str, ...], **kwargs: Any) -> Any:
149
+ from frontier.snapshot import capture_from_catalog, utc_now_iso
150
+
151
+ identifier = self._current_timestamp_literal()
152
+ catalog = self._inventory_relations(relations)
153
+ return capture_from_catalog(
154
+ relations,
155
+ catalog=catalog,
156
+ identifier=identifier,
157
+ captured_at=utc_now_iso(),
158
+ attestation_source=kwargs.get("attestation_source"),
159
+ )
160
+
161
+ def bind_query_to_snapshot(self, sql: str, snapshot: Any) -> str:
162
+ from frontier.snapshot import bind_sql_to_snapshot
163
+
164
+ return bind_sql_to_snapshot(sql, snapshot, dialect=self.dialect)
165
+
166
+ def verify_snapshot_binding(self, sql: str, snapshot: Any) -> bool:
167
+ from frontier.snapshot import verify_snapshot_binding as verify
168
+
169
+ return verify(sql, snapshot, dialect=self.dialect)
170
+
171
+ def _current_timestamp_literal(self) -> str:
172
+ rows = self.execute(
173
+ "select to_varchar(current_timestamp(), 'YYYY-MM-DD HH24:MI:SS.FF3 TZHTZM')"
174
+ )
175
+ if not rows or rows[0][0] is None:
176
+ from frontier.snapshot import SnapshotError, SOURCE_SNAPSHOT_NOT_PINNED
177
+
178
+ raise SnapshotError(SOURCE_SNAPSHOT_NOT_PINNED, "Snowflake did not return a snapshot timestamp")
179
+ return str(rows[0][0]).strip()
180
+
181
+ def _inventory_relations(self, relations: list[str] | tuple[str, ...]) -> dict[str, dict[str, Any]]:
182
+ from frontier.snapshot import DYNAMIC_TABLE, EXTERNAL_TABLE, MATERIALIZED_VIEW, VIEW
183
+ from frontier.warehouse import split_relation_parts
184
+
185
+ catalog: dict[str, dict[str, Any]] = {}
186
+ pending = [str(item).strip() for item in relations if str(item).strip()]
187
+ seen: set[str] = set()
188
+ while pending:
189
+ name = pending.pop(0)
190
+ key = name.lower()
191
+ if key in seen:
192
+ continue
193
+ seen.add(key)
194
+ database, schema, table = split_relation_parts(name)
195
+ if not table:
196
+ continue
197
+ entry = self._lookup_table(database, schema, table)
198
+ if entry is None:
199
+ continue
200
+ kind = str(entry.get("kind") or "")
201
+ if kind == VIEW:
202
+ definition = self._view_definition(database, schema, table)
203
+ if definition:
204
+ entry["view_sql"] = definition
205
+ from frontier.snapshot import collect_source_relations
206
+
207
+ pending.extend(collect_source_relations(definition, dialect="snowflake"))
208
+ elif kind in {MATERIALIZED_VIEW, EXTERNAL_TABLE, DYNAMIC_TABLE}:
209
+ entry["kind"] = kind
210
+ catalog[name] = entry
211
+ dynamic = self._is_dynamic_table(database, schema, table)
212
+ if dynamic:
213
+ catalog[name]["kind"] = DYNAMIC_TABLE
214
+ return catalog
215
+
216
+ def _lookup_table(
217
+ self,
218
+ database: str | None,
219
+ schema: str | None,
220
+ table: str,
221
+ ) -> dict[str, Any] | None:
222
+ sql = (
223
+ "select table_catalog, table_schema, table_name, table_type, "
224
+ "is_transient, retention_time "
225
+ "from information_schema.tables "
226
+ f"where lower(table_name) = lower({sql_string(table)})"
227
+ )
228
+ if schema:
229
+ sql += f" and lower(table_schema) = lower({sql_string(schema)})"
230
+ if database:
231
+ sql += f" and lower(table_catalog) = lower({sql_string(database)})"
232
+ sql += " limit 1"
233
+ try:
234
+ rows = self.execute(sql)
235
+ except Exception:
236
+ return None
237
+ if not rows:
238
+ return None
239
+ row = rows[0]
240
+ table_type = str(row[3] or "").strip().upper()
241
+ is_transient = str(row[4] or "").strip().upper() == "YES"
242
+ retention = row[5] if len(row) > 5 else None
243
+ kind = table_type.lower()
244
+ if table_type == "BASE TABLE" and is_transient:
245
+ kind = "transient"
246
+ elif table_type == "BASE TABLE":
247
+ kind = "permanent_table"
248
+ return {
249
+ "kind": kind,
250
+ "retention_days": int(retention) if retention is not None else None,
251
+ }
252
+
253
+ def _view_definition(
254
+ self,
255
+ database: str | None,
256
+ schema: str | None,
257
+ table: str,
258
+ ) -> str | None:
259
+ sql = (
260
+ "select view_definition from information_schema.views "
261
+ f"where lower(table_name) = lower({sql_string(table)})"
262
+ )
263
+ if schema:
264
+ sql += f" and lower(table_schema) = lower({sql_string(schema)})"
265
+ if database:
266
+ sql += f" and lower(table_catalog) = lower({sql_string(database)})"
267
+ sql += " limit 1"
268
+ try:
269
+ rows = self.execute(sql)
270
+ except Exception:
271
+ return None
272
+ if not rows or rows[0][0] is None:
273
+ return None
274
+ text = str(rows[0][0]).strip().rstrip(";")
275
+ if text.lower().startswith("create "):
276
+ lowered = text.lower()
277
+ marker = " as "
278
+ index = lowered.find(marker)
279
+ if index != -1:
280
+ text = text[index + len(marker) :].strip()
281
+ return text or None
282
+
283
+ def _is_dynamic_table(
284
+ self,
285
+ database: str | None,
286
+ schema: str | None,
287
+ table: str,
288
+ ) -> bool:
289
+ sql = (
290
+ "select 1 from information_schema.dynamic_tables "
291
+ f"where lower(table_name) = lower({sql_string(table)})"
292
+ )
293
+ if schema:
294
+ sql += f" and lower(table_schema) = lower({sql_string(schema)})"
295
+ if database:
296
+ sql += f" and lower(table_catalog) = lower({sql_string(database)})"
297
+ sql += " limit 1"
298
+ try:
299
+ return bool(self.execute(sql))
300
+ except Exception:
301
+ return False
302
+
144
303
  def describe(self) -> dict[str, Any]:
145
304
  if self._config is None:
146
305
  return {"warehouse_type": self.warehouse_type}
@@ -73,6 +73,9 @@ def build_ingest_payload(
73
73
  semantic_manifest_version: int | None = None,
74
74
  semantic_manifest_fingerprint: str | None = None,
75
75
  manifest_source: str | None = None,
76
+ runner_version: str | None = None,
77
+ dbt_target: str | None = None,
78
+ assessment_identity: dict[str, Any] | None = None,
76
79
  ) -> dict[str, Any]:
77
80
  payload = {
78
81
  "externalRunId": external_run_id,
@@ -121,6 +124,12 @@ def build_ingest_payload(
121
124
  payload["semanticManifestFingerprint"] = semantic_manifest_fingerprint
122
125
  if manifest_source:
123
126
  payload["manifestSource"] = manifest_source
127
+ if runner_version:
128
+ payload["runnerVersion"] = runner_version
129
+ if dbt_target:
130
+ payload["dbtTarget"] = dbt_target
131
+ if assessment_identity:
132
+ payload["assessmentIdentity"] = assessment_identity
124
133
  assert_payload_has_no_secrets(payload)
125
134
  assert_no_raw_rows(payload)
126
135
  return payload
@@ -0,0 +1,263 @@
1
+ """Separated assessment result dimensions for filter-v1.
2
+
3
+ Certification, validation, economics, and execution are independent
4
+ claims. Warehouse execution failure, economic fallback, and
5
+ full-reference validation must not overwrite certification status.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from frontier.snapshot import (
13
+ ASSURANCE_ADAPTER,
14
+ MODE_NONE,
15
+ RULE_SET_VERSION,
16
+ SOURCE_SNAPSHOT_NOT_PINNED,
17
+ SourceSnapshot,
18
+ unpinned_snapshot,
19
+ )
20
+
21
+ SQL_CERTIFIED = "SQL_CERTIFIED"
22
+ CONTRACT_CERTIFIED = "CONTRACT_CERTIFIED"
23
+ UNCERTIFIED = "UNCERTIFIED"
24
+ _LEGACY_UNCIFIED = "UNCIFIED"
25
+
26
+
27
+ def normalize_certification_status(value: str | None) -> str:
28
+ """Accept the 21A UNCIFIED misspelling; emit only UNCERTIFIED."""
29
+ if value == _LEGACY_UNCIFIED:
30
+ return UNCERTIFIED
31
+ if value in {SQL_CERTIFIED, CONTRACT_CERTIFIED, UNCERTIFIED}:
32
+ return value
33
+ return UNCERTIFIED
34
+
35
+
36
+ VALIDATION_NOT_RUN = "NOT_RUN"
37
+ CANDIDATES_CONFIRMED = "CANDIDATES_CONFIRMED"
38
+ FULL_REFERENCE_VALIDATED = "FULL_REFERENCE_VALIDATED"
39
+ VALIDATION_FAILED = "FAILED"
40
+
41
+ TARGETED_REPAIR_RECOMMENDED = "TARGETED_REPAIR_RECOMMENDED"
42
+ FULL_REBUILD_RECOMMENDED = "FULL_REBUILD_RECOMMENDED"
43
+ ECONOMICS_NOT_EVALUATED = "NOT_EVALUATED"
44
+
45
+ EXECUTION_PENDING = "PENDING"
46
+ EXECUTION_SUCCEEDED = "SUCCEEDED"
47
+ EXECUTION_FAILED = "FAILED"
48
+
49
+
50
+ def certification_status(
51
+ snapshot: SourceSnapshot | None,
52
+ *,
53
+ static_certified: bool = False,
54
+ contract_certified: bool = False,
55
+ ) -> tuple[str, str | None]:
56
+ """Return (status, failure_code). Never inspects execution or validation."""
57
+ pinned = snapshot if snapshot is not None else unpinned_snapshot()
58
+ if not pinned.allows_sql_certified():
59
+ code = pinned.failure_code or SOURCE_SNAPSHOT_NOT_PINNED
60
+ return UNCERTIFIED, code
61
+ if static_certified:
62
+ return SQL_CERTIFIED, None
63
+ if contract_certified:
64
+ return CONTRACT_CERTIFIED, None
65
+ return UNCERTIFIED, None
66
+
67
+
68
+ def validation_status(
69
+ *,
70
+ full_reference_validated: bool = False,
71
+ full_reference_failed: bool = False,
72
+ candidates_confirmed: bool = False,
73
+ confirmation_failed: bool = False,
74
+ ) -> str:
75
+ if full_reference_validated:
76
+ return FULL_REFERENCE_VALIDATED
77
+ if full_reference_failed or confirmation_failed:
78
+ return VALIDATION_FAILED
79
+ if candidates_confirmed:
80
+ return CANDIDATES_CONFIRMED
81
+ return VALIDATION_NOT_RUN
82
+
83
+
84
+ def economics_decision(
85
+ *,
86
+ full_rebuild_recommended: bool = False,
87
+ targeted_ran: bool = False,
88
+ frontier_bytes: int | None = None,
89
+ full_comparison_bytes: int | None = None,
90
+ warehouse_credits: float | None = None,
91
+ ) -> str:
92
+ """Bytes and wall time are not billed credits. Do not recommend targeted repair from running alone."""
93
+ del targeted_ran
94
+ if full_rebuild_recommended:
95
+ return FULL_REBUILD_RECOMMENDED
96
+ if frontier_bytes is not None and full_comparison_bytes is not None:
97
+ if frontier_bytes >= full_comparison_bytes:
98
+ return FULL_REBUILD_RECOMMENDED
99
+ if warehouse_credits is None:
100
+ return ECONOMICS_NOT_EVALUATED
101
+ return TARGETED_REPAIR_RECOMMENDED
102
+ return ECONOMICS_NOT_EVALUATED
103
+
104
+
105
+ def execution_status(
106
+ *,
107
+ execution_failed: bool = False,
108
+ execution_ran: bool = False,
109
+ ) -> str:
110
+ if execution_failed:
111
+ return EXECUTION_FAILED
112
+ if execution_ran:
113
+ return EXECUTION_SUCCEEDED
114
+ return EXECUTION_PENDING
115
+
116
+
117
+ def build_assessment_dimensions(
118
+ *,
119
+ snapshot: SourceSnapshot | None,
120
+ static_certified: bool = False,
121
+ contract_certified: bool = False,
122
+ execution_failed: bool = False,
123
+ execution_ran: bool = False,
124
+ full_rebuild_recommended: bool = False,
125
+ targeted_ran: bool = False,
126
+ frontier_bytes: int | None = None,
127
+ full_comparison_bytes: int | None = None,
128
+ warehouse_credits: float | None = None,
129
+ candidates_confirmed: bool = False,
130
+ confirmation_failed: bool = False,
131
+ full_reference_validated: bool = False,
132
+ full_reference_failed: bool = False,
133
+ failure_phase: str | None = None,
134
+ failure_code: str | None = None,
135
+ failure_reason: str | None = None,
136
+ ) -> dict[str, Any]:
137
+ cert_status, cert_code = certification_status(
138
+ snapshot,
139
+ static_certified=static_certified,
140
+ contract_certified=contract_certified,
141
+ )
142
+ if cert_status == SQL_CERTIFIED and (snapshot is None or not snapshot.allows_sql_certified()):
143
+ cert_status = UNCERTIFIED
144
+ cert_code = SOURCE_SNAPSHOT_NOT_PINNED
145
+ certification: dict[str, Any] = {
146
+ "status": cert_status,
147
+ "ruleSetVersion": RULE_SET_VERSION,
148
+ }
149
+ if cert_code:
150
+ certification["failureCode"] = cert_code
151
+ source = snapshot or unpinned_snapshot()
152
+ upload = source.to_upload_payload()
153
+ economics = {
154
+ "decision": economics_decision(
155
+ full_rebuild_recommended=full_rebuild_recommended,
156
+ targeted_ran=targeted_ran,
157
+ frontier_bytes=frontier_bytes,
158
+ full_comparison_bytes=full_comparison_bytes,
159
+ warehouse_credits=warehouse_credits,
160
+ )
161
+ }
162
+ if frontier_bytes is not None:
163
+ economics["frontierBytesScanned"] = int(frontier_bytes)
164
+ if full_comparison_bytes is not None:
165
+ economics["fullComparisonBytesScanned"] = int(full_comparison_bytes)
166
+ execution: dict[str, Any] = {
167
+ "status": execution_status(
168
+ execution_failed=execution_failed,
169
+ execution_ran=execution_ran,
170
+ )
171
+ }
172
+ if failure_phase:
173
+ execution["failurePhase"] = failure_phase[:64]
174
+ if failure_code:
175
+ execution["failureCode"] = failure_code[:64]
176
+ if failure_reason:
177
+ execution["failureReason"] = failure_reason[:512]
178
+ return {
179
+ "certification": certification,
180
+ "validation": {"status": validation_status(
181
+ full_reference_validated=full_reference_validated,
182
+ full_reference_failed=full_reference_failed,
183
+ candidates_confirmed=candidates_confirmed,
184
+ confirmation_failed=confirmation_failed,
185
+ )},
186
+ "economics": economics,
187
+ "execution": execution,
188
+ "sourceSnapshot": upload,
189
+ "baselineBoundary": dict(BASELINE_BOUNDARY),
190
+ }
191
+
192
+
193
+ BASELINE_BOUNDARY = {
194
+ "comparesSqlVersionsAtPinnedSnapshot": True,
195
+ "existingMaterializedMartRepresentsSnapshot": False,
196
+ "safeInPlaceProductionRepair": False,
197
+ }
198
+
199
+
200
+ def filter_v1_sql_certified(
201
+ *,
202
+ eligible: bool,
203
+ compiled: bool,
204
+ confirmed: bool,
205
+ execution_failed: bool,
206
+ ) -> bool:
207
+ """All four 21C gates except snapshot, which certification_status still checks."""
208
+ return bool(eligible and compiled and confirmed and not execution_failed)
209
+
210
+
211
+ def enrich_certification_record(
212
+ dimensions: dict[str, Any],
213
+ *,
214
+ eligibility: dict[str, Any] | None,
215
+ snapshot: SourceSnapshot | None,
216
+ candidate_fingerprint: str | None = None,
217
+ execution_failed: bool = False,
218
+ ) -> dict[str, Any]:
219
+ """Attach plan, taint, assumption, and snapshot identity to certification."""
220
+ certification = dict(dimensions.get("certification") or {})
221
+ eligibility = eligibility or {}
222
+ for src, dest in (
223
+ ("oldPlanFingerprint", "oldPlanFingerprint"),
224
+ ("newPlanFingerprint", "newPlanFingerprint"),
225
+ ("changedFilterNodeId", "changedFilterNodeId"),
226
+ ("coveredNodeIds", "coveredNodeIds"),
227
+ ("operators", "operators"),
228
+ ("joinTaint", "joinTaint"),
229
+ ("checkedAssumptions", "checkedAssumptions"),
230
+ ("manifestDependencies", "manifestDependencies"),
231
+ ("candidateFingerprint", "candidateQueryFingerprint"),
232
+ ):
233
+ value = eligibility.get(src)
234
+ if value:
235
+ certification[dest] = value
236
+ if candidate_fingerprint:
237
+ certification["candidateQueryFingerprint"] = candidate_fingerprint
238
+ source = snapshot or unpinned_snapshot()
239
+ if source.identifier:
240
+ certification["snapshotIdentifier"] = source.identifier
241
+ if eligibility.get("eligible") is False:
242
+ certification["status"] = UNCERTIFIED
243
+ certification["failureCode"] = eligibility.get("reasonCode") or UNCERTIFIED
244
+ elif execution_failed:
245
+ certification["status"] = UNCERTIFIED
246
+ if snapshot is not None and (
247
+ snapshot.assurance == ASSURANCE_ADAPTER or snapshot.allows_sql_certified()
248
+ ):
249
+ certification["failureCode"] = EXECUTION_FAILED
250
+ dimensions["certification"] = certification
251
+ dimensions["baselineBoundary"] = dict(BASELINE_BOUNDARY)
252
+ return dimensions
253
+
254
+
255
+ def display_evidence_level(evidence_level: str, validation: dict[str, Any] | None) -> str:
256
+ """Never label empirically validated unless full-reference actually ran."""
257
+ if (
258
+ validation
259
+ and validation.get("status") != FULL_REFERENCE_VALIDATED
260
+ and evidence_level == "empirically_validated"
261
+ ):
262
+ return "aggregates"
263
+ return evidence_level