truequery 0.1.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 (254) hide show
  1. truequery/__init__.py +3 -0
  2. truequery/analyzers/__init__.py +0 -0
  3. truequery/analyzers/project_checks.py +220 -0
  4. truequery/analyzers/sql.py +529 -0
  5. truequery/checks/__init__.py +0 -0
  6. truequery/checks/sql.py +1278 -0
  7. truequery/cli.py +710 -0
  8. truequery/engine.py +346 -0
  9. truequery/hook.py +860 -0
  10. truequery/keylookup.py +135 -0
  11. truequery/models.py +32 -0
  12. truequery/project.py +561 -0
  13. truequery/registry_impl.json +127 -0
  14. truequery/report.py +343 -0
  15. truequery/schema.py +206 -0
  16. truequery/settings.py +239 -0
  17. truequery/standard/consume/README.md +111 -0
  18. truequery/standard/consume/adhoc-limit-required.yaml +50 -0
  19. truequery/standard/consume/aggregate-before-export.yaml +39 -0
  20. truequery/standard/consume/consumer-cost-guard.yaml +44 -0
  21. truequery/standard/consume/consumer-reads-marts.yaml +38 -0
  22. truequery/standard/consume/dashboard-query-budget.yaml +40 -0
  23. truequery/standard/consume/dashboard-refresh-schedule.yaml +47 -0
  24. truequery/standard/consume/deterministic-export-order.yaml +44 -0
  25. truequery/standard/consume/explicit-timezone-consumers.yaml +44 -0
  26. truequery/standard/consume/explore-answer-carries-reproducible-query.yaml +56 -0
  27. truequery/standard/consume/explore-caveats-and-unchecked-stated.yaml +52 -0
  28. truequery/standard/consume/explore-day-boundaries-and-partial-periods.yaml +51 -0
  29. truequery/standard/consume/explore-experiment-readout-follows-plan.yaml +63 -0
  30. truequery/standard/consume/explore-freshness-and-coverage-checked.yaml +54 -0
  31. truequery/standard/consume/explore-inspect-distributions-and-examples.yaml +47 -0
  32. truequery/standard/consume/explore-join-row-count-checks.yaml +53 -0
  33. truequery/standard/consume/explore-null-aware-negative-filters.yaml +50 -0
  34. truequery/standard/consume/explore-question-metric-grain-stated.yaml +55 -0
  35. truequery/standard/consume/explore-rates-not-counts-across-segments.yaml +51 -0
  36. truequery/standard/consume/explore-reconcile-to-known-total.yaml +51 -0
  37. truequery/standard/consume/explore-reuse-governed-metric.yaml +52 -0
  38. truequery/standard/consume/export-row-cap.yaml +45 -0
  39. truequery/standard/consume/freshness-displayed.yaml +44 -0
  40. truequery/standard/consume/no-deep-offset.yaml +38 -0
  41. truequery/standard/consume/parameterized-dashboard-filters.yaml +38 -0
  42. truequery/standard/consume/reverse-etl-idempotent-keys.yaml +45 -0
  43. truequery/standard/consume/reverse-etl-pii-consent.yaml +50 -0
  44. truequery/standard/consume/reverse-etl-schema-contract.yaml +47 -0
  45. truequery/standard/dq/README.md +117 -0
  46. truequery/standard/dq/accuracy-proxy-or-unmeasurable.yaml +45 -0
  47. truequery/standard/dq/audits-blocking-every-run.yaml +47 -0
  48. truequery/standard/dq/checks-versioned-with-code.yaml +52 -0
  49. truequery/standard/dq/completeness-declare-missing-sentinels.yaml +44 -0
  50. truequery/standard/dq/completeness-fill-rate-thresholds.yaml +45 -0
  51. truequery/standard/dq/completeness-not-null-required-columns.yaml +60 -0
  52. truequery/standard/dq/completeness-table-not-empty.yaml +48 -0
  53. truequery/standard/dq/consistency-cross-column-row-rules.yaml +49 -0
  54. truequery/standard/dq/consistency-no-orphan-keys.yaml +49 -0
  55. truequery/standard/dq/consistency-paired-aggregate-reconciliation.yaml +48 -0
  56. truequery/standard/dq/schema-conformance-checked-on-read.yaml +48 -0
  57. truequery/standard/dq/unenforced-constraints-are-tested.yaml +51 -0
  58. truequery/standard/dq/uniqueness-composite-grain.yaml +52 -0
  59. truequery/standard/dq/uniqueness-key-no-duplicates.yaml +53 -0
  60. truequery/standard/dq/unit-tests-with-fixtures.yaml +52 -0
  61. truequery/standard/dq/validity-allowed-value-sets.yaml +46 -0
  62. truequery/standard/dq/validity-enforce-model-contracts.yaml +51 -0
  63. truequery/standard/dq/validity-range-format-type-rules.yaml +50 -0
  64. truequery/standard/gov/README.md +94 -0
  65. truequery/standard/gov/audit-columns-or-snapshots.yaml +45 -0
  66. truequery/standard/gov/column-grants-for-pii.yaml +41 -0
  67. truequery/standard/gov/declared-model-owner.yaml +43 -0
  68. truequery/standard/gov/description-coverage-for-shared-models.yaml +40 -0
  69. truequery/standard/gov/erasure-path-documented.yaml +43 -0
  70. truequery/standard/gov/exposures-registered.yaml +73 -0
  71. truequery/standard/gov/group-private-access.yaml +41 -0
  72. truequery/standard/gov/least-privilege-grants.yaml +80 -0
  73. truequery/standard/gov/lineage-from-declared-sources.yaml +41 -0
  74. truequery/standard/gov/mesh-public-contracts-only.yaml +41 -0
  75. truequery/standard/gov/no-bypassrls-roles.yaml +39 -0
  76. truequery/standard/gov/no-public-privileges.yaml +39 -0
  77. truequery/standard/gov/no-raw-pii-in-seeds.yaml +41 -0
  78. truequery/standard/gov/pii-classification-declared.yaml +42 -0
  79. truequery/standard/gov/production-pii-not-in-dev.yaml +43 -0
  80. truequery/standard/gov/redacting-views-for-pii.yaml +41 -0
  81. truequery/standard/gov/retention-declared.yaml +42 -0
  82. truequery/standard/gov/row-policy-for-sensitive-tables.yaml +38 -0
  83. truequery/standard/ingest/README.md +100 -0
  84. truequery/standard/ingest/cap-intake-cost-and-volume.yaml +46 -0
  85. truequery/standard/ingest/cdc-replica-identity-required.yaml +47 -0
  86. truequery/standard/ingest/checkpoint-streaming-intake.yaml +42 -0
  87. truequery/standard/ingest/choose-batch-vs-streaming-deliberately.yaml +45 -0
  88. truequery/standard/ingest/declare-source-freshness.yaml +80 -0
  89. truequery/standard/ingest/dedupe-on-natural-key-before-append.yaml +45 -0
  90. truequery/standard/ingest/handle-schema-drift-explicitly.yaml +45 -0
  91. truequery/standard/ingest/idempotent-batch-loads.yaml +49 -0
  92. truequery/standard/ingest/isolate-source-failures.yaml +50 -0
  93. truequery/standard/ingest/landing-rebuildable-from-source.yaml +43 -0
  94. truequery/standard/ingest/monitor-cdc-lag-and-slots.yaml +47 -0
  95. truequery/standard/ingest/pin-connector-and-schema-versions.yaml +42 -0
  96. truequery/standard/ingest/pin-file-format-options.yaml +42 -0
  97. truequery/standard/ingest/propagate-deletes-explicitly.yaml +48 -0
  98. truequery/standard/ingest/quarantine-invalid-rows-at-intake.yaml +56 -0
  99. truequery/standard/ingest/raw-landing-append-only.yaml +48 -0
  100. truequery/standard/ingest/reconcile-row-counts-per-load.yaml +43 -0
  101. truequery/standard/ingest/record-file-lineage-at-intake.yaml +48 -0
  102. truequery/standard/ingest/reference-sources-not-raw-tables.yaml +42 -0
  103. truequery/standard/ingest/require-completeness-marker.yaml +50 -0
  104. truequery/standard/ingest/scan-pii-at-landing.yaml +45 -0
  105. truequery/standard/ingest/seeds-small-static-only.yaml +43 -0
  106. truequery/standard/ingest/test-intake-against-fixtures.yaml +46 -0
  107. truequery/standard/ingest/watermark-and-late-data-policy.yaml +49 -0
  108. truequery/standard/model/README.md +91 -0
  109. truequery/standard/model/dbt-contract-enforced.yaml +50 -0
  110. truequery/standard/model/delete-handling-incremental.yaml +51 -0
  111. truequery/standard/model/explicit-column-lists.yaml +83 -0
  112. truequery/standard/model/full-refresh-parity.yaml +49 -0
  113. truequery/standard/model/grain-preserved-joins.yaml +52 -0
  114. truequery/standard/model/idempotent-reexec.yaml +48 -0
  115. truequery/standard/model/incremental-lookback-window.yaml +53 -0
  116. truequery/standard/model/incremental-watermark-filter.yaml +48 -0
  117. truequery/standard/model/jinja-macro-determinism.yaml +46 -0
  118. truequery/standard/model/jinja-no-nested-curlies.yaml +43 -0
  119. truequery/standard/model/materialization-choice.yaml +51 -0
  120. truequery/standard/model/merge-source-deduped.yaml +57 -0
  121. truequery/standard/model/microbatch-event-time.yaml +39 -0
  122. truequery/standard/model/null-join-keys-excluded.yaml +45 -0
  123. truequery/standard/model/snapshot-scd2-validity.yaml +45 -0
  124. truequery/standard/model/snapshot-strategy-choice.yaml +41 -0
  125. truequery/standard/model/staging-no-business-logic.yaml +44 -0
  126. truequery/standard/model/surrogate-keys-stable.yaml +37 -0
  127. truequery/standard/model/unique-key-merge-key.yaml +55 -0
  128. truequery/standard/ops/README.md +98 -0
  129. truequery/standard/ops/breaking-change-categorization.yaml +35 -0
  130. truequery/standard/ops/ci-pr-checks-required.yaml +44 -0
  131. truequery/standard/ops/ci-slim-builds-only-changed.yaml +42 -0
  132. truequery/standard/ops/cost-estimate-before-run.yaml +41 -0
  133. truequery/standard/ops/cost-guardrail-confidence-levels.yaml +39 -0
  134. truequery/standard/ops/dev-environment-expiry.yaml +42 -0
  135. truequery/standard/ops/disposable-dev-environments.yaml +42 -0
  136. truequery/standard/ops/drift-detection-history-first.yaml +42 -0
  137. truequery/standard/ops/full-refresh-approval.yaml +76 -0
  138. truequery/standard/ops/model-versioning-breaking-changes.yaml +83 -0
  139. truequery/standard/ops/plan-preview-before-apply.yaml +39 -0
  140. truequery/standard/ops/post-deploy-data-validation.yaml +42 -0
  141. truequery/standard/ops/pre-destructive-snapshot.yaml +35 -0
  142. truequery/standard/ops/prod-credentials-isolation.yaml +96 -0
  143. truequery/standard/ops/run-artifact-retention.yaml +41 -0
  144. truequery/standard/ops/staged-upgrades-with-backup.yaml +39 -0
  145. truequery/standard/orch/README.md +74 -0
  146. truequery/standard/orch/concurrency-pools-and-limits.yaml +63 -0
  147. truequery/standard/orch/dag-ci-parse-and-unit-tests.yaml +70 -0
  148. truequery/standard/orch/data-aware-scheduling-over-polling.yaml +62 -0
  149. truequery/standard/orch/declared-asset-lineage.yaml +66 -0
  150. truequery/standard/orch/deferrable-sensors-with-timeout.yaml +75 -0
  151. truequery/standard/orch/explicit-catchup-policy.yaml +53 -0
  152. truequery/standard/orch/explicit-schedule-timezone.yaml +68 -0
  153. truequery/standard/orch/failure-alert-routing.yaml +100 -0
  154. truequery/standard/orch/idempotent-backfill-windowing.yaml +68 -0
  155. truequery/standard/orch/idempotent-retries-with-limits.yaml +79 -0
  156. truequery/standard/orch/light-dag-parse-no-heavy-top-level.yaml +75 -0
  157. truequery/standard/orch/no-circular-dependencies.yaml +67 -0
  158. truequery/standard/orch/partitioned-assets-windowing.yaml +66 -0
  159. truequery/standard/orch/sla-deadline-alerts.yaml +76 -0
  160. truequery/standard/orch/task-atomicity-no-partial-writes.yaml +62 -0
  161. truequery/standard/orch/task-timeout-execution-limit.yaml +70 -0
  162. truequery/standard/py/README.md +63 -0
  163. truequery/standard/py/airflow-isolate-task-dependencies.yaml +40 -0
  164. truequery/standard/py/dagster-asset-declared-dependencies.yaml +47 -0
  165. truequery/standard/py/dagster-io-manager-not-manual-io.yaml +47 -0
  166. truequery/standard/py/dagster-resources-for-external-clients.yaml +53 -0
  167. truequery/standard/py/dbt-python-model-contract.yaml +48 -0
  168. truequery/standard/py/deterministic-model-outputs.yaml +83 -0
  169. truequery/standard/py/hook-adapter-connection-lifecycle.yaml +59 -0
  170. truequery/standard/py/idempotent-task-logic.yaml +55 -0
  171. truequery/standard/py/no-n-plus-one-queries-in-loops.yaml +46 -0
  172. truequery/standard/py/pandas-filter-before-collect.yaml +44 -0
  173. truequery/standard/py/pandas-no-chained-assignment.yaml +41 -0
  174. truequery/standard/py/pandas-no-row-loop-mutations.yaml +44 -0
  175. truequery/standard/py/pin-pipeline-dependencies.yaml +82 -0
  176. truequery/standard/py/sqlmesh-audit-python-outputs.yaml +49 -0
  177. truequery/standard/py/sqlmesh-python-model-execution-context.yaml +52 -0
  178. truequery/standard/sem/README.md +97 -0
  179. truequery/standard/sem/aggregation-type-matches-additivity.yaml +61 -0
  180. truequery/standard/sem/conversion-metrics-window-bounds.yaml +54 -0
  181. truequery/standard/sem/count-distinct-uses-entity-key.yaml +52 -0
  182. truequery/standard/sem/cumulative-metrics-time-spine.yaml +53 -0
  183. truequery/standard/sem/derived-metrics-compose-from-governed.yaml +52 -0
  184. truequery/standard/sem/description-and-ownership-per-metric.yaml +50 -0
  185. truequery/standard/sem/dimension-naming-conventions.yaml +52 -0
  186. truequery/standard/sem/entity-primary-key-declared.yaml +54 -0
  187. truequery/standard/sem/join-paths-declared-via-entities.yaml +47 -0
  188. truequery/standard/sem/metric-filters-declared-not-hidden.yaml +57 -0
  189. truequery/standard/sem/metric-grain-declaration.yaml +52 -0
  190. truequery/standard/sem/metric-smoke-test-on-change.yaml +43 -0
  191. truequery/standard/sem/no-fanout-double-counting.yaml +52 -0
  192. truequery/standard/sem/no-metric-defined-off-staging.yaml +47 -0
  193. truequery/standard/sem/null-handling-in-aggregations.yaml +57 -0
  194. truequery/standard/sem/percentile-metrics-use-approximate.yaml +51 -0
  195. truequery/standard/sem/ratio-metrics-divide-after-aggregation.yaml +49 -0
  196. truequery/standard/sem/saved-query-canonical-slices.yaml +56 -0
  197. truequery/standard/sem/semantic-model-validates.yaml +50 -0
  198. truequery/standard/sem/single-governed-metric-definition.yaml +68 -0
  199. truequery/standard/sem/time-dimension-standardized.yaml +51 -0
  200. truequery/standard/sem/unit-consistency.yaml +54 -0
  201. truequery/standard/sql/README.md +126 -0
  202. truequery/standard/sql/accidental-cross-join.yaml +58 -0
  203. truequery/standard/sql/average-of-ratios.yaml +63 -0
  204. truequery/standard/sql/between-date-on-timestamp.yaml +74 -0
  205. truequery/standard/sql/case-without-else-in-aggregate.yaml +67 -0
  206. truequery/standard/sql/count-star-after-left-join.yaml +67 -0
  207. truequery/standard/sql/distinct-dedup-bandaid.yaml +59 -0
  208. truequery/standard/sql/explore-limit-is-not-a-sample.yaml +56 -0
  209. truequery/standard/sql/explore-profile-filter-columns.yaml +48 -0
  210. truequery/standard/sql/fanout-join-pre-aggregation.yaml +64 -0
  211. truequery/standard/sql/group-by-bare-column.yaml +75 -0
  212. truequery/standard/sql/insert-explicit-column-list.yaml +60 -0
  213. truequery/standard/sql/integer-division-truncation.yaml +94 -0
  214. truequery/standard/sql/keyset-pagination.yaml +70 -0
  215. truequery/standard/sql/left-join-null-rejecting-where.yaml +58 -0
  216. truequery/standard/sql/limit-requires-order-by.yaml +67 -0
  217. truequery/standard/sql/natural-join-explicit-keys.yaml +56 -0
  218. truequery/standard/sql/negative-filter-drops-nulls.yaml +70 -0
  219. truequery/standard/sql/nondeterministic-merge-keys.yaml +69 -0
  220. truequery/standard/sql/not-in-nullable-subquery.yaml +57 -0
  221. truequery/standard/sql/null-equality-comparison.yaml +65 -0
  222. truequery/standard/sql/random-sampling-no-sort.yaml +48 -0
  223. truequery/standard/sql/row-number-dedupe-tiebreaker.yaml +67 -0
  224. truequery/standard/sql/sargable-filter-bare-column.yaml +53 -0
  225. truequery/standard/sql/set-operation-column-position.yaml +59 -0
  226. truequery/standard/sql/unguarded-update-delete.yaml +63 -0
  227. truequery/standard/sql/union-all-default.yaml +70 -0
  228. truequery/standard/store/README.md +88 -0
  229. truequery/standard/store/cluster-on-frequent-filters-joins.yaml +44 -0
  230. truequery/standard/store/compact-small-files-optimize.yaml +44 -0
  231. truequery/standard/store/declare-not-null-check-constraints.yaml +43 -0
  232. truequery/standard/store/declare-sort-order-for-range-queries.yaml +44 -0
  233. truequery/standard/store/enable-change-feed-for-incremental-consumers.yaml +44 -0
  234. truequery/standard/store/evolve-partitions-by-metadata-not-rewrite.yaml +45 -0
  235. truequery/standard/store/explicit-physical-types-no-implicit-casts.yaml +43 -0
  236. truequery/standard/store/full-refresh-by-replace-not-delete-insert.yaml +44 -0
  237. truequery/standard/store/incremental-watermark-partition-alignment.yaml +40 -0
  238. truequery/standard/store/no-high-cardinality-partition-column.yaml +47 -0
  239. truequery/standard/store/partition-expiration-retention-policy.yaml +43 -0
  240. truequery/standard/store/partition-on-filter-column.yaml +43 -0
  241. truequery/standard/store/partition-predicate-required.yaml +41 -0
  242. truequery/standard/store/prefer-auto-liquid-clustering-over-manual-tune.yaml +37 -0
  243. truequery/standard/store/rewrite-manifests-and-stats-periodically.yaml +37 -0
  244. truequery/standard/store/single-open-format-per-medallion-layer.yaml +37 -0
  245. truequery/standard/store/time-travel-retention-explicit.yaml +46 -0
  246. truequery/standard/store/use-transient-temporary-for-ephemeral.yaml +35 -0
  247. truequery/standard/store/vacuum-expire-snapshots-with-retention-floor.yaml +42 -0
  248. truequery/std_registry.py +3310 -0
  249. truequery-0.1.0.dist-info/METADATA +52 -0
  250. truequery-0.1.0.dist-info/RECORD +254 -0
  251. truequery-0.1.0.dist-info/WHEEL +4 -0
  252. truequery-0.1.0.dist-info/entry_points.txt +3 -0
  253. truequery-0.1.0.dist-info/licenses/LICENSE +201 -0
  254. truequery-0.1.0.dist-info/licenses/NOTICE +4 -0
truequery/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """TrueQuery: one standard for data work, enforced on every edit."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,220 @@
1
+ """Preview non-SQL checks (v1, Stop tier, never immediate).
2
+
3
+ Each check is backed by an existing standard practice (cited in its
4
+ ``why``); practices with no detector-shaped record yet (Airflow dynamic
5
+ ``start_date``, pandas ``merge(validate=)``) are NOT implemented here —
6
+ they are listed as gaps in docs. Deterministic, offline, file/project
7
+ evidence only.
8
+ """
9
+ import ast
10
+ import os
11
+ import re
12
+
13
+ import yaml
14
+
15
+ INCREMENTAL = re.compile(r"materialized\s*=\s*['\"]incremental['\"]")
16
+ UNIQUE_KEY = re.compile(r"unique_key\s*=")
17
+
18
+
19
+ def _finding(rule, registry, settings, mode, path, line, why, fix,
20
+ snippet=""):
21
+ record = registry.RECORDS.get(rule) if registry else None
22
+ sev = None
23
+ if settings:
24
+ sev = settings.get(rule, {}).get("severity")
25
+ if not sev and record:
26
+ sev = record["severity"].get(mode, "warning")
27
+ return {"rule": rule, "severity": sev or "warning", "mode": mode,
28
+ "evidence": "project", "file": path, "line": line, "col": 1,
29
+ "snippet": snippet[:120], "why": why, "fix": fix}
30
+
31
+
32
+ STRATEGY = re.compile(r"incremental_strategy\s*=\s*['\"]([\w+]+)['\"]")
33
+
34
+
35
+ def _yaml_unique_key(path):
36
+ """True when a properties YAML next to the model sets unique_key in
37
+ the model's config block (dbt allows config there as well as in
38
+ the SQL)."""
39
+ model = os.path.splitext(os.path.basename(path))[0]
40
+ directory = os.path.dirname(os.path.abspath(path))
41
+ try:
42
+ names = sorted(os.listdir(directory))
43
+ except OSError:
44
+ return False
45
+ for name in names:
46
+ if not name.endswith((".yml", ".yaml")):
47
+ continue
48
+ try:
49
+ with open(os.path.join(directory, name), encoding="utf-8") as handle:
50
+ doc = yaml.safe_load(handle)
51
+ except (OSError, ValueError, yaml.YAMLError):
52
+ continue
53
+ specs = doc.get("models") if isinstance(doc, dict) else None
54
+ for spec in specs or []:
55
+ if (isinstance(spec, dict) and spec.get("name") == model
56
+ and isinstance(spec.get("config"), dict)
57
+ and spec["config"].get("unique_key")):
58
+ return True
59
+ return False
60
+
61
+
62
+ def check_dbt_model(text, path, mode="build", registry=None,
63
+ settings=None):
64
+ """model/unique-key-merge-key: merge/delete+insert incremental
65
+ model with no unique_key (in the SQL config or the model's YAML).
66
+ Microbatch/insert_overwrite strategies key on event_time/partitions
67
+ instead — no unique_key expected."""
68
+ out = []
69
+ m = INCREMENTAL.search(text)
70
+ strat = STRATEGY.search(text)
71
+ merge_like = strat is None or strat.group(1) in (
72
+ "merge", "delete+insert", "delete_insert")
73
+ if (m and merge_like and not UNIQUE_KEY.search(text)
74
+ and not _yaml_unique_key(path)):
75
+ line = text[:m.start()].count("\n") + 1
76
+ out.append(_finding(
77
+ "model/unique-key-merge-key", registry, settings, mode, path,
78
+ line,
79
+ "incremental model declares no unique_key; a merge degrades "
80
+ "to append and duplicates rows on every re-run "
81
+ "(model/unique-key-merge-key)",
82
+ "add unique_key='<key>' to the model config and a "
83
+ "unique + not_null test on the key"))
84
+ return out
85
+
86
+
87
+ def _tests_of(entry):
88
+ tests = set()
89
+ for t in entry.get("tests") or []:
90
+ tests.add(t if isinstance(t, str) else next(iter(t), ""))
91
+ for t in entry.get("data_tests") or []:
92
+ tests.add(t if isinstance(t, str) else next(iter(t), ""))
93
+ return tests
94
+
95
+
96
+ def check_schema_yaml(text, path, mode="build", registry=None,
97
+ settings=None):
98
+ """dq/uniqueness-key-no-duplicates + ingest/declare-source-freshness
99
+ over a dbt schema YAML (models:/sources:)."""
100
+ out = []
101
+ try:
102
+ doc = yaml.safe_load(text)
103
+ except Exception:
104
+ return out
105
+ if not isinstance(doc, dict):
106
+ return out
107
+ lines = text.splitlines()
108
+
109
+ def line_of(needle, after=0):
110
+ for i, row in enumerate(lines[after:], start=after + 1):
111
+ if needle in row:
112
+ return i
113
+ return 1
114
+
115
+ for model in doc.get("models") or []:
116
+ if not isinstance(model, dict):
117
+ continue
118
+ name = model.get("name", "")
119
+ cols = model.get("columns") or []
120
+ # A key candidate needs a declared signal: a MetricFlow primary
121
+ # entity, or a unique test (someone claimed uniqueness). A bare
122
+ # *_id column is as often a foreign key, and a column with an
123
+ # accepted_values test is not a key — stay silent there.
124
+ key_cols = []
125
+ for c in cols:
126
+ if not isinstance(c, dict):
127
+ continue
128
+ entity = c.get("entity") or {}
129
+ if entity.get("type") == "primary" or "unique" in _tests_of(c):
130
+ key_cols.append(c)
131
+ if not key_cols:
132
+ continue # no declared key candidate: cannot infer, stay silent
133
+ col = key_cols[0]
134
+ tests = _tests_of(col)
135
+ combo = any("unique_combination_of_columns" in t
136
+ for t in _tests_of(model))
137
+ if combo or {"unique", "not_null"} <= tests:
138
+ continue
139
+ out.append(_finding(
140
+ "dq/uniqueness-key-no-duplicates", registry, settings, mode,
141
+ path, line_of(str(col.get("name"))),
142
+ f"model {name}: key column {col.get('name')} lacks a "
143
+ "unique + not_null test pair, so duplicate or null keys "
144
+ "pass silently (dq/uniqueness-key-no-duplicates)",
145
+ f"add unique and not_null tests on {col.get('name')}, or a "
146
+ "unique_combination_of_columns test on the model"))
147
+ for source in doc.get("sources") or []:
148
+ if not isinstance(source, dict):
149
+ continue
150
+ for table in source.get("tables") or []:
151
+ if not isinstance(table, dict):
152
+ continue
153
+ freshness = source.get("freshness") or table.get("freshness")
154
+ loaded = (source.get("loaded_at_field")
155
+ or table.get("loaded_at_field"))
156
+ # Fire only on intent: the source tracks its load time but
157
+ # never checks it. A source with no loaded_at_field at all
158
+ # has not opted into freshness — stay silent.
159
+ if freshness or not loaded:
160
+ continue
161
+ out.append(_finding(
162
+ "ingest/declare-source-freshness", registry, settings,
163
+ mode, path, line_of(str(table.get("name", ""))),
164
+ f"source table {table.get('name')} tracks loaded_at_field "
165
+ "but declares no freshness check, so a stalled source is "
166
+ "invisible (ingest/declare-source-freshness)",
167
+ "add freshness: {warn_after/error_after} to the source"))
168
+ return out
169
+
170
+
171
+ def check_python(text, path, mode="build", registry=None, settings=None):
172
+ """orch/explicit-catchup-policy: Airflow DAG without catchup."""
173
+ out = []
174
+ try:
175
+ tree = ast.parse(text)
176
+ except SyntaxError:
177
+ return out
178
+ for node in ast.walk(tree):
179
+ if not isinstance(node, ast.Call):
180
+ continue
181
+ func = node.func
182
+ name = (func.id if isinstance(func, ast.Name)
183
+ else func.attr if isinstance(func, ast.Attribute) else "")
184
+ if name not in ("DAG", "dag"): # DAG(...), models.DAG, @dag(...)
185
+ continue
186
+ kws = {k.arg for k in node.keywords if k.arg}
187
+ if "catchup" in kws:
188
+ continue
189
+ out.append(_finding(
190
+ "orch/explicit-catchup-policy", registry, settings, mode,
191
+ path, node.lineno,
192
+ "Airflow DAG declares no explicit catchup; the default "
193
+ "backfills every missed interval on first deploy "
194
+ "(orch/explicit-catchup-policy)",
195
+ "set catchup=False (or True deliberately) on the DAG"))
196
+ return out
197
+
198
+
199
+ def check_aux_file(text, path, mode="build", registry=None, settings=None):
200
+ """Dispatch by file kind. Returns finding dicts (engine shape)."""
201
+ if path.endswith(".py"):
202
+ return check_python(text, path, mode, registry, settings)
203
+ if path.endswith((".yml", ".yaml")):
204
+ return check_schema_yaml(text, path, mode, registry, settings)
205
+ return []
206
+
207
+
208
+ def check_model_sql(text, path, mode="build", registry=None, settings=None):
209
+ if path.endswith(".sql"):
210
+ return check_dbt_model(text, path, mode, registry, settings)
211
+ return []
212
+
213
+
214
+ # slug -> implementing function, for the standard<->checks CI test.
215
+ PREVIEW_CHECKS = {
216
+ "model/unique-key-merge-key": check_dbt_model,
217
+ "dq/uniqueness-key-no-duplicates": check_schema_yaml,
218
+ "ingest/declare-source-freshness": check_schema_yaml,
219
+ "orch/explicit-catchup-policy": check_python,
220
+ }