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/engine.py ADDED
@@ -0,0 +1,346 @@
1
+ """Deterministic offline engine facade.
2
+
3
+ One code path for the CLI and the hook: discover files, resolve
4
+ mode + dialect per file, run the registry-driven analyzer, return
5
+ results in a stable order. Zero network, zero LLM in this path.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import fnmatch
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ from dataclasses import dataclass, field
16
+
17
+ from truequery import __version__
18
+ from truequery import project as P
19
+ from truequery import settings as S
20
+ from truequery import std_registry
21
+ from truequery.analyzers import project_checks
22
+ from truequery.analyzers import sql as analyzer
23
+ from truequery.models import Diagnostic, Finding
24
+
25
+ ENGINE_VERSION = __version__
26
+
27
+ IGNORES_NAME = "ignores.json"
28
+
29
+ MAX_FILE_BYTES = 10 * 1024 * 1024 # ~10 MB; bigger files are not checked.
30
+
31
+
32
+ def active_overrides(config: dict | None) -> dict:
33
+ """tier_overrides from DATA.md, restricted to known slugs and tiers.
34
+
35
+ Dogfooding only: init never writes this, defaults never ship it.
36
+ Unknown slugs/tiers are dropped (typo safety)."""
37
+ raw = (config or {}).get("tier_overrides") or {}
38
+ if not isinstance(raw, dict):
39
+ return {}
40
+ return {slug: tier for slug, tier in raw.items()
41
+ if slug in std_registry.RECORDS
42
+ and tier in ("immediate", "stop")}
43
+
44
+
45
+ @dataclass
46
+ class FileResult:
47
+ path: str
48
+ rel: str
49
+ mode: str
50
+ findings: list = field(default_factory=list)
51
+ diagnostics: list = field(default_factory=list)
52
+ suppressed: list = field(default_factory=list)
53
+ key_lookups: list = field(default_factory=list)
54
+ # "sql"; "project" for .py/.yml/.yaml files; "macro" for a file of
55
+ # dbt macro definitions, which is listed as not checked.
56
+ kind: str = "sql"
57
+
58
+ @property
59
+ def verified(self) -> bool:
60
+ return not self.diagnostics
61
+
62
+
63
+ def load_ignores(root: str) -> list[dict]:
64
+ """Entries from .truequery/ignores.json: {rule, path, line?, reason?}.
65
+
66
+ Corrupt or absent files mean no ignores (the `ignore add` verb
67
+ validates on write; the hook never breaks on a bad file).
68
+ """
69
+ try:
70
+ with open(os.path.join(root, ".truequery", IGNORES_NAME),
71
+ encoding="utf-8") as handle:
72
+ data = json.load(handle)
73
+ except (OSError, ValueError):
74
+ return []
75
+ entries = data.get("ignores", []) if isinstance(data, dict) else []
76
+ out = []
77
+ for entry in entries:
78
+ if not isinstance(entry, dict):
79
+ continue
80
+ rule, path = entry.get("rule"), entry.get("path")
81
+ if not isinstance(rule, str) or not isinstance(path, str):
82
+ continue
83
+ line = entry.get("line")
84
+ stmt = entry.get("stmt")
85
+ offset = entry.get("offset")
86
+ out.append({"rule": rule, "path": path,
87
+ "line": line if isinstance(line, int) else None,
88
+ "stmt": stmt if isinstance(stmt, str) else None,
89
+ "offset": offset if isinstance(offset, int) else None,
90
+ "reason": entry.get("reason") or "",
91
+ "broad": not isinstance(line, int) and not isinstance(
92
+ stmt, str)})
93
+ return out
94
+
95
+
96
+ def _suppressed_by(finding, rel: str, ignores: list[dict],
97
+ spans=()) -> dict | None:
98
+ """Match by rule + file + normalized-statement hash + the line's
99
+ offset inside that statement: an ignore covers one line, and can't
100
+ drift onto another statement when lines shift. Entries without a
101
+ hash fall back to rule + file + line."""
102
+ for entry in ignores:
103
+ if not fnmatch.fnmatchcase(finding.rule, entry["rule"]):
104
+ continue
105
+ if not fnmatch.fnmatchcase(rel, entry["path"]):
106
+ continue
107
+ if entry["stmt"] is not None:
108
+ if getattr(finding, "_stmt", None) != entry["stmt"]:
109
+ continue
110
+ if (entry["offset"] is not None
111
+ and stmt_offset(spans, finding.line) != entry["offset"]):
112
+ continue
113
+ elif entry["line"] is not None and finding.line != entry["line"]:
114
+ continue
115
+ return entry
116
+ return None
117
+
118
+
119
+ def _chunk_hashes(text: str, dialect: str | None) -> list[tuple]:
120
+ """(start_line, end_line, sha1-12) per statement chunk, on the same
121
+ cleaning the analyzer uses, so ignore entries and git baselines
122
+ identify statements independent of line shifts."""
123
+ cleaned = analyzer.strip_comments(analyzer.strip_jinja(text),
124
+ dialect or "generic")
125
+ chunks = analyzer.split_statements(cleaned)
126
+ spans = []
127
+ for chunk, start in chunks:
128
+ # First and last content lines of the statement: the
129
+ # splitter's start trails the previous `;`, so blank/comment
130
+ # gap lines between statements belong to no statement.
131
+ rows = chunk.split("\n")
132
+ content = [i for i, row in enumerate(rows) if row.strip()]
133
+ if not content:
134
+ continue
135
+ first, last = start + content[0], start + content[-1]
136
+ norm = re.sub(r"\s+", " ", chunk).strip().rstrip(";")
137
+ digest = hashlib.sha1(norm.encode("utf-8")).hexdigest()[:12]
138
+ spans.append((first, last, digest))
139
+ return spans
140
+
141
+
142
+ def stmt_hash_at(spans: list[tuple], line: int) -> str | None:
143
+ for start, end, digest in spans:
144
+ if start <= line <= end:
145
+ return digest
146
+ return None
147
+
148
+
149
+ def stmt_offset(spans: list[tuple], line: int) -> int | None:
150
+ """line's offset from the first line of its statement."""
151
+ for start, end, _ in spans:
152
+ if start <= line <= end:
153
+ return line - start
154
+ return None
155
+
156
+
157
+ def discover_aux(paths: list[str]) -> list[str]:
158
+ """Non-SQL files the preview project checks read (.py, .yml/.yaml)."""
159
+ out: list[str] = []
160
+ for path in paths:
161
+ if os.path.isdir(path):
162
+ for dirpath, dirnames, filenames in os.walk(path):
163
+ dirnames[:] = sorted(d for d in dirnames
164
+ if d not in P.SKIP_DIRS
165
+ and not d.startswith("."))
166
+ for name in sorted(filenames):
167
+ if name.endswith((".py", ".yml", ".yaml")):
168
+ out.append(os.path.join(dirpath, name))
169
+ elif os.path.isfile(path) and path.endswith((".py", ".yml", ".yaml")):
170
+ out.append(os.path.abspath(path))
171
+ return sorted(set(out))
172
+
173
+
174
+ def _aux_findings(text, path, mode, tiers, settings):
175
+ """Preview non-SQL checks, honoring enabled/tier like SQL checks."""
176
+ raw = (project_checks.check_model_sql(text, path, mode, std_registry,
177
+ settings)
178
+ + project_checks.check_aux_file(text, path, mode, std_registry,
179
+ settings))
180
+ kept = []
181
+ for f in raw:
182
+ record = std_registry.RECORDS.get(f["rule"])
183
+ if record is None:
184
+ continue
185
+ setting = (settings or {}).get(f["rule"], {})
186
+ enabled = setting.get("enabled", record["enabled"])
187
+ tier = setting.get("tier", record["tier"])
188
+ if not enabled:
189
+ continue
190
+ if tiers is not None and tier not in tiers:
191
+ continue
192
+ kept.append(Finding(rule=f["rule"], severity=f["severity"],
193
+ mode=f["mode"], evidence=f["evidence"],
194
+ file=f["file"], line=f["line"], col=f["col"],
195
+ snippet=f["snippet"], why=f["why"],
196
+ fix=f["fix"]))
197
+ kept.sort(key=Finding.sort_key)
198
+ return kept
199
+
200
+
201
+ def check_file(path: str, dialect: str | None = None,
202
+ mode: str = "build", tiers=None,
203
+ settings=None, text: str | None = None
204
+ ) -> tuple[list[Finding], list[Diagnostic], list[str]]:
205
+ """Check one file from disk. The CLI and hook use run()."""
206
+ if text is None:
207
+ with open(path, encoding="utf-8") as handle:
208
+ text = handle.read()
209
+ res = analyzer.check_text(text, dialect=dialect, path=path, mode=mode,
210
+ tiers=tiers,
211
+ settings=settings)
212
+ findings = [Finding(rule=f["rule"], severity=f["severity"],
213
+ mode=f["mode"], evidence=f["evidence"],
214
+ file=f["file"], line=f["line"], col=f["col"],
215
+ snippet=f["snippet"], why=f["why"], fix=f["fix"])
216
+ for f in res["findings"]]
217
+ diagnostics = [Diagnostic(file=path, message=d["message"],
218
+ detail=d.get("detail", ""))
219
+ for d in res["diagnostics"]]
220
+ findings.sort(key=Finding.sort_key)
221
+ diagnostics.sort(key=Diagnostic.sort_key)
222
+ return findings, diagnostics, sorted(set(res.get("key_lookups", [])))
223
+
224
+
225
+ NOT_UTF8 = "not UTF-8 text (not checked)"
226
+
227
+ # dbt definition blocks: templates that render into other files' SQL.
228
+ _DEFINITION_BLOCK = re.compile(
229
+ r"{%-?\s*(macro|test|materialization|docs)\b.*?{%-?\s*end\1\s*-?%}",
230
+ re.DOTALL)
231
+ _JINJA_COMMENT = re.compile(r"{#.*?#}", re.DOTALL)
232
+
233
+
234
+ def is_macro_file(text: str) -> bool:
235
+ """True when a file holds only dbt macro/test/materialization/docs
236
+ definitions (plus comments): templates, not queries. A file that also
237
+ has a query outside those blocks is checked as usual."""
238
+ if "{%" not in text:
239
+ return False
240
+ rest, blocks = _DEFINITION_BLOCK.subn("", _JINJA_COMMENT.sub("", text))
241
+ if not blocks:
242
+ return False
243
+ return not analyzer.strip_comments(rest, "generic").strip()
244
+
245
+ TOO_LARGE = "file over the 10 MB size limit (not checked)"
246
+
247
+
248
+ def _read(path: str) -> str | None:
249
+ """File text; None when unreadable. NOT_UTF8 and TOO_LARGE are
250
+ sentinels handled by run(): the file is reported as not checked."""
251
+ try:
252
+ if os.path.getsize(path) > MAX_FILE_BYTES:
253
+ return TOO_LARGE
254
+ with open(path, encoding="utf-8") as handle:
255
+ return handle.read()
256
+ except UnicodeDecodeError:
257
+ return NOT_UTF8
258
+ except OSError:
259
+ return None
260
+
261
+
262
+ def _apply_ignores(result: FileResult, findings: list, spans, ignores) -> None:
263
+ """Tag each finding with its statement hash, then split kept from
264
+ suppressed by .truequery/ignores.json."""
265
+ for finding in findings:
266
+ finding.file = result.rel
267
+ finding._stmt = stmt_hash_at(spans, finding.line)
268
+ entry = _suppressed_by(finding, result.rel, ignores, spans)
269
+ if entry is None:
270
+ result.findings.append(finding)
271
+ else:
272
+ result.suppressed.append({"rule": finding.rule,
273
+ "line": finding.line,
274
+ "broad": entry["broad"],
275
+ "reason": entry["reason"]})
276
+ result.findings.sort(key=Finding.sort_key)
277
+
278
+
279
+ def run(paths: list[str], root: str, dialect: str | None = None,
280
+ mode: str | None = None, tiers=None, settings=None,
281
+ config: dict | None = None) -> list[FileResult]:
282
+ """Run enabled checks over discovered files. Deterministic order.
283
+
284
+ SQL files get the SQL checks plus the preview dbt-model check;
285
+ .py/.yml/.yaml files under the given paths get the preview project
286
+ checks. ``config`` is the parsed DATA.md tq-config (read from root
287
+ when None). ``settings`` is an Effective rules map; without
288
+ one it is resolved from that config plus the user's config.
289
+
290
+ Applies .truequery/ignores.json to every finding; suppressed ones
291
+ land on FileResult.suppressed (with a broad/narrow flag) so
292
+ reporters can say what an ignore hid.
293
+ """
294
+ sql_files = P.discover(paths)
295
+ if config is None:
296
+ config = P.load_config(root)
297
+ base_dialect = dialect or (config or {}).get("dialect")
298
+ if settings is None:
299
+ settings = S.effective(project=config).check_map()
300
+ ignores = load_ignores(root)
301
+ results: list[FileResult] = []
302
+ for path in sql_files:
303
+ text = _read(path)
304
+ if text is None:
305
+ continue
306
+ rel = P._rel(path, root)
307
+ file_mode = P.resolve_mode(rel, config, mode)
308
+ if text is NOT_UTF8 or text is TOO_LARGE:
309
+ # Never silent: an undecodable or oversize file is listed
310
+ # as not checked, with the reason.
311
+ results.append(FileResult(path=path, rel=rel, mode=file_mode,
312
+ diagnostics=[Diagnostic(rel, text)]))
313
+ continue
314
+ if is_macro_file(text):
315
+ # Macro definitions aren't queries: listed as not checked
316
+ # (kind "macro"), never as a parse failure, never as clean.
317
+ results.append(FileResult(path=path, rel=rel, mode=file_mode,
318
+ kind="macro"))
319
+ continue
320
+ findings, diagnostics, key_lookups = check_file(
321
+ path, dialect=base_dialect, mode=file_mode, tiers=tiers,
322
+ settings=settings, text=text)
323
+ # Preview dbt-model check (incremental without unique_key).
324
+ findings += _aux_findings(text, path, file_mode, tiers, settings)
325
+ for diagnostic in diagnostics:
326
+ diagnostic.file = rel
327
+ result = FileResult(path=path, rel=rel, mode=file_mode,
328
+ diagnostics=diagnostics, key_lookups=key_lookups)
329
+ _apply_ignores(result, findings, _chunk_hashes(text, base_dialect),
330
+ ignores)
331
+ results.append(result)
332
+ for path in discover_aux(paths):
333
+ text = _read(path)
334
+ if text is None or text is NOT_UTF8 or text is TOO_LARGE:
335
+ continue
336
+ rel = P._rel(path, root)
337
+ file_mode = P.resolve_mode(rel, config, mode)
338
+ result = FileResult(path=path, rel=rel, mode=file_mode,
339
+ kind="project")
340
+ # Project files have no statements: ignores match on rule + file
341
+ # + line.
342
+ _apply_ignores(result, _aux_findings(text, path, file_mode, tiers,
343
+ settings), [], ignores)
344
+ results.append(result)
345
+ results.sort(key=lambda r: r.rel)
346
+ return results