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/cli.py ADDED
@@ -0,0 +1,710 @@
1
+ """truequery: deterministic SQL checks for the agent edit loop. Offline, no LLM."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import re
9
+ import sys
10
+
11
+ from truequery import engine as E
12
+ from truequery import hook
13
+ from truequery import project as P
14
+ from truequery import report as RP
15
+ from truequery import schema
16
+ from truequery import settings as S
17
+ from truequery import std_registry
18
+ from truequery.engine import ENGINE_VERSION, active_overrides, run
19
+
20
+ EXIT_CLEAN = 0
21
+ EXIT_FINDINGS = 1
22
+ EXIT_ERROR = 2
23
+ EXIT_UNVERIFIED = 3
24
+
25
+ EXIT_HELP = ("Exit codes: 0 = everything checked, no findings at or above "
26
+ "--fail-on (default error); 1 = findings at or above it; "
27
+ "2 = usage or config error; 3 = no such findings, but some "
28
+ "files could not be checked (parse failures, undecodable "
29
+ "text, or files over the size limit, "
30
+ "listed on stderr). --allow-unverified turns 3 into 0.")
31
+
32
+ FAIL_LEVELS = ("error", "warning", "advisory")
33
+
34
+
35
+ def _dump(args, text: str, json_doc: str, sarif_doc: str) -> None:
36
+ if args.sarif:
37
+ print(sarif_doc)
38
+ elif args.json:
39
+ print(json_doc)
40
+ elif text:
41
+ print(text)
42
+
43
+
44
+ def _cli_layer(args) -> dict | None:
45
+ """CLI rule flags (the highest-precedence settings layer)."""
46
+ rules: dict[str, dict] = {}
47
+ for item in getattr(args, "rule_tier", None) or []:
48
+ slug, sep, tier = item.partition(":")
49
+ if not sep or tier not in ("immediate", "stop"):
50
+ return None
51
+ rules.setdefault(slug, {})["tier"] = tier
52
+ for slug in getattr(args, "enable_rule", None) or []:
53
+ rules.setdefault(slug, {})["enabled"] = True
54
+ for slug in getattr(args, "disable_rule", None) or []:
55
+ rules.setdefault(slug, {})["enabled"] = False
56
+ return {"rules": rules} if rules else None
57
+
58
+
59
+ def _add_rule_flags(parser) -> None:
60
+ parser.add_argument("--rule-tier", action="append",
61
+ metavar="SLUG:TIER",
62
+ help="override a rule's tier for this run "
63
+ "(highest precedence)")
64
+ parser.add_argument("--enable-rule", action="append", metavar="SLUG",
65
+ help="enable a rule for this run")
66
+ parser.add_argument("--disable-rule", action="append", metavar="SLUG",
67
+ help="disable a rule for this run")
68
+
69
+
70
+ def _effective_for(args, root):
71
+ """(Effective, settings map) for CLI verbs; None layer on bad flags."""
72
+ layer = _cli_layer(args)
73
+ if layer is None and (getattr(args, "rule_tier", None)
74
+ or getattr(args, "enable_rule", None)
75
+ or getattr(args, "disable_rule", None)):
76
+ raise ValueError("bad --rule-* flags (want SLUG[:TIER])")
77
+ eff = S.effective(root=root, cli=layer)
78
+ return eff, eff.check_map()
79
+
80
+
81
+ def _fail_level(args) -> int:
82
+ return RP.SEVERITY_RANK[args.fail_on]
83
+
84
+
85
+ def _exit_for(results, args) -> int:
86
+ threshold = _fail_level(args)
87
+ for finding in RP.all_findings(results):
88
+ if RP.SEVERITY_RANK.get(finding.severity, 0) >= threshold:
89
+ return EXIT_FINDINGS
90
+ if not args.allow_unverified and any(not r.verified for r in results):
91
+ return EXIT_UNVERIFIED # a CI gate must not pass on unchecked SQL
92
+ return EXIT_CLEAN
93
+
94
+
95
+ def _stderr_diagnostics(results, verbose: bool = False) -> None:
96
+ for diagnostic in RP.all_diagnostics(results):
97
+ print(f"{diagnostic.file}: {diagnostic.message}", file=sys.stderr)
98
+ if verbose and diagnostic.detail:
99
+ print(f" detail: {diagnostic.detail}", file=sys.stderr)
100
+
101
+
102
+ def _unverified_reason(results) -> str:
103
+ """Why files went unchecked: parse failures, the size limit, or both."""
104
+ diags = [d for r in results if not r.verified for d in r.diagnostics]
105
+ reasons = []
106
+ if any(d.message != E.TOO_LARGE for d in diags):
107
+ reasons.append("could not parse")
108
+ if any(d.message == E.TOO_LARGE for d in diags):
109
+ reasons.append("over the size limit")
110
+ return " / ".join(reasons) or "could not parse"
111
+
112
+
113
+ def _summary_line(results, findings) -> str:
114
+ """Never print 'clean' unless every file was verified."""
115
+ macros = sum(1 for r in results if r.kind == "macro")
116
+ results = [r for r in results if r.kind != "macro"]
117
+ verified = sum(1 for r in results if r.verified)
118
+ unverified = len(results) - verified
119
+ checked = RP._plural(verified, "file")
120
+ hidden = sum(len(r.suppressed) for r in results)
121
+ ignored = f" ({hidden} hidden by ignores)" if hidden else ""
122
+ reason = _unverified_reason(results)
123
+ skipped = (f" · {RP._plural(macros, 'dbt macro file')} not checked "
124
+ "(macro definitions)") if macros else ""
125
+ if not findings:
126
+ if unverified:
127
+ return (f"no findings in {checked} checked{ignored} · "
128
+ f"{unverified} not checked ({reason}){skipped}")
129
+ if skipped:
130
+ return f"no findings in {checked} checked offline{ignored}{skipped}"
131
+ if hidden:
132
+ return f"no findings in {checked} checked offline{ignored}"
133
+ return f"clean: {checked} checked offline, no findings"
134
+ counts = [RP._plural(sum(1 for f in findings if f.severity == sev), sev)
135
+ for sev in ("error", "warning")]
136
+ advisories = sum(1 for f in findings if f.severity == "advisory")
137
+ if advisories:
138
+ counts.append(RP._plural(advisories, "advisory"))
139
+ line = f"{', '.join(counts)} in {checked} checked offline{ignored}"
140
+ if unverified:
141
+ line += f" ({unverified} not checked: {reason})"
142
+ return line + skipped
143
+
144
+
145
+ def _dialect_arg(value: str) -> str | None:
146
+ try:
147
+ return P.normalize_dialect(value)
148
+ except ValueError as exc:
149
+ raise argparse.ArgumentTypeError(str(exc)) from None
150
+
151
+
152
+ def _add_output(parser) -> None:
153
+ group = parser.add_mutually_exclusive_group()
154
+ group.add_argument("--json", action="store_true",
155
+ help="machine-readable report")
156
+ group.add_argument("--sarif", action="store_true",
157
+ help="SARIF 2.1.0 report")
158
+ parser.add_argument("--dialect", default=None, type=_dialect_arg,
159
+ help="sqlglot dialect; else DATA.md tq-config; else auto")
160
+ parser.add_argument("--mode", default=None,
161
+ choices=["explore", "build", "change", "assure"],
162
+ help="override DATA.md mode globs")
163
+ parser.add_argument("--fail-on", default="error", choices=FAIL_LEVELS,
164
+ help="lowest severity that fails (default error)")
165
+ parser.add_argument("--verbose", action="store_true",
166
+ help="print full parse-error detail on stderr")
167
+ parser.add_argument("--allow-unverified", action="store_true",
168
+ help="exit 0 even when some files could not be "
169
+ "checked")
170
+
171
+
172
+ def cmd_detect(args: argparse.Namespace) -> int:
173
+ root = os.path.abspath(".")
174
+ try:
175
+ _, settings_map = _effective_for(args, root)
176
+ results = run(args.paths, root, dialect=args.dialect,
177
+ mode=args.mode, settings=settings_map)
178
+ except (P.ContextError, ValueError) as exc:
179
+ print(str(exc), file=sys.stderr)
180
+ return EXIT_ERROR
181
+ modes = [r.mode for r in results]
182
+ findings = RP.all_findings(results)
183
+ if not results:
184
+ note = "truequery detect: no SQL files found"
185
+ if args.sarif:
186
+ print(RP.render_sarif(results, ENGINE_VERSION))
187
+ elif args.json:
188
+ print(RP.render_json(results, ENGINE_VERSION, modes))
189
+ else:
190
+ print(note)
191
+ return EXIT_CLEAN
192
+ _dump(args,
193
+ RP.render_text(results, ENGINE_VERSION)
194
+ or _summary_line(results, findings),
195
+ RP.render_json(results, ENGINE_VERSION, modes),
196
+ RP.render_sarif(results, ENGINE_VERSION))
197
+ _stderr_diagnostics(results, verbose=args.verbose)
198
+ if not (args.json or args.sarif) and findings:
199
+ print(_summary_line(results, findings))
200
+ return _exit_for(results, args)
201
+
202
+
203
+ def _review_tables(results, dialect) -> list[str]:
204
+ # Only files the checker read: a skipped file (over the size limit,
205
+ # unparseable) is never opened again for the receipt.
206
+ return P.tables_in_files([r.path for r in results
207
+ if r.kind == "sql" and r.verified], dialect)
208
+
209
+
210
+ def cmd_review(args: argparse.Namespace) -> int:
211
+ root = os.path.abspath(".")
212
+ paths = args.paths or [root]
213
+ try:
214
+ eff, settings_map = _effective_for(args, root)
215
+ results = run(paths, root, dialect=args.dialect, mode=args.mode,
216
+ settings=settings_map)
217
+ except (P.ContextError, ValueError) as exc:
218
+ print(str(exc), file=sys.stderr)
219
+ return EXIT_ERROR
220
+ modes = [r.mode for r in results]
221
+ contracts, invalid = P.load_contracts_reporting(root)
222
+ tables = _review_tables(results, args.dialect)
223
+ receipt = RP.build_receipt(results, modes, contracts, tables,
224
+ tier_overrides=active_overrides(
225
+ P.load_config(root)),
226
+ rule_settings=eff.settings_line(),
227
+ invalid_contracts=invalid)
228
+ if args.sarif:
229
+ print(RP.render_sarif(results, ENGINE_VERSION))
230
+ elif args.json:
231
+ print(RP.render_json(results, ENGINE_VERSION, modes,
232
+ receipt=receipt))
233
+ else:
234
+ text = RP.render_text(results, ENGINE_VERSION)
235
+ if text:
236
+ print(text)
237
+ print()
238
+ else:
239
+ print(_summary_line(results, RP.all_findings(results)))
240
+ print()
241
+ print(RP.render_receipt(receipt))
242
+ _stderr_diagnostics(results, verbose=args.verbose)
243
+ return _exit_for(results, args)
244
+
245
+
246
+ def cmd_init(args: argparse.Namespace) -> int:
247
+ root = os.path.abspath(args.path or ".")
248
+ stack = P.detect_stack(root)
249
+ dialect = P.detect_dialect(root, stack)
250
+ config = P.default_config(stack, dialect)
251
+ wrote, kept = [], []
252
+ data_md = os.path.join(root, "DATA.md")
253
+ if not os.path.exists(data_md) or args.force:
254
+ with open(data_md, "w", encoding="utf-8") as handle:
255
+ handle.write(P.render_data_md(config))
256
+ wrote.append(data_md)
257
+ else:
258
+ kept.append(data_md)
259
+ contracts_dir = os.path.join(root, "contracts")
260
+ os.makedirs(contracts_dir, exist_ok=True)
261
+ # Contracts hold the team's confirmed answers: seeded when missing,
262
+ # never overwritten, not even with --force.
263
+ for path in P.seed_contracts(root):
264
+ wrote.append(path)
265
+ summary = {"stack": stack, "dialect": dialect, "wrote": wrote,
266
+ "kept": kept}
267
+ if args.sarif:
268
+ print(RP.render_sarif([], ENGINE_VERSION))
269
+ elif args.json:
270
+ print(json.dumps(summary, indent=2, sort_keys=True))
271
+ else:
272
+ for path in wrote:
273
+ print(f"wrote {path}")
274
+ for path in kept:
275
+ print(f"kept existing {path} (use --force to overwrite)")
276
+ print(f"init: stack={stack} dialect={dialect or 'auto'} "
277
+ f"({len(wrote)} wrote, {len(kept)} kept)")
278
+ return EXIT_CLEAN
279
+
280
+
281
+ def cmd_context(args: argparse.Namespace) -> int:
282
+ root = os.path.abspath(".")
283
+ config = P.load_config(root)
284
+ stale = config is None
285
+ if stale:
286
+ config = P.default_config(P.detect_stack(root), None)
287
+ dialect = (config or {}).get("dialect")
288
+ rel, target_text = None, ""
289
+ if args.target and os.path.isdir(args.target):
290
+ # A folder: every SQL file in it. One MODE when they agree, else
291
+ # "mixed" with the modes listed.
292
+ rel = P._rel(os.path.abspath(args.target), root)
293
+ files = P.discover([args.target])
294
+ modes = sorted({P.resolve_mode(P._rel(f, root), config, None)
295
+ for f in files}) or [P.resolve_mode("", config, None)]
296
+ mode = modes[0] if len(modes) == 1 else "mixed (" + ", ".join(modes) + ")"
297
+ tables = P.tables_in_files(files, dialect)
298
+ else:
299
+ if args.target:
300
+ rel = P._rel(os.path.abspath(args.target), root)
301
+ try:
302
+ with open(args.target, encoding="utf-8") as handle:
303
+ target_text = handle.read()
304
+ except OSError:
305
+ print(f"truequery: no such file or folder: {args.target}",
306
+ file=sys.stderr)
307
+ return EXIT_ERROR
308
+ mode = P.resolve_mode(rel or "", config, None)
309
+ tables = P.referenced_tables(target_text, dialect)
310
+ contracts = P.load_contracts(root)
311
+ directives = [f"MODE: {mode}", "STANDARD: 0.1",
312
+ "EVIDENCE_AVAILABLE: file"]
313
+ for table in tables:
314
+ if P.match_contract(table, contracts) is None:
315
+ directives.append(f"CONTRACT_MISSING: {table}")
316
+ if stale:
317
+ directives.append("CONTEXT_STALE")
318
+ doc = {"mode": mode, "standard": "0.1",
319
+ "evidence_available": ["file"],
320
+ "config": config, "target": rel,
321
+ "tables_referenced": tables,
322
+ "contracts": {t: contracts[P.match_contract(t, contracts)]
323
+ for t in tables
324
+ if P.match_contract(t, contracts) is not None},
325
+ "directives": directives,
326
+ "stale": stale}
327
+ if args.sarif:
328
+ print(RP.render_sarif([], ENGINE_VERSION))
329
+ elif args.json:
330
+ print(json.dumps(doc, indent=2, sort_keys=True))
331
+ else:
332
+ print(f"# truequery context (project root: {root})")
333
+ print(f"stack={config.get('stack')} "
334
+ f"dialect={config.get('dialect') or 'auto'}")
335
+ if rel:
336
+ print(f"target={rel}")
337
+ for table in tables:
338
+ key = P.match_contract(table, contracts)
339
+ contract = contracts.get(key) if key is not None else None
340
+ if contract is not None:
341
+ print(f"contract {table}: grain={contract['grain']} "
342
+ f"keys={P._format_keys(contract['keys']) or '—'}")
343
+ if not tables:
344
+ print("(target references no tables)")
345
+ for directive in directives:
346
+ print(directive)
347
+ return EXIT_CLEAN
348
+
349
+
350
+ def _rules_set(args: argparse.Namespace, root) -> int:
351
+ """`tq rules set` — a HUMAN command: writes rule settings to
352
+ the project (DATA.md tq-config) or user config. Agents never run
353
+ this; SKILL.md says so."""
354
+ slug = args.target
355
+ updates = {}
356
+ if args.enabled is not None:
357
+ updates["enabled"] = args.enabled == "on"
358
+ if args.tier is not None:
359
+ updates["tier"] = args.tier
360
+ if args.severity is not None:
361
+ updates["severity"] = args.severity
362
+ problem = S.guard(slug, updates, std_registry.RECORDS)
363
+ if problem:
364
+ print(problem, file=sys.stderr)
365
+ return EXIT_ERROR
366
+ try:
367
+ if args.scope == "user":
368
+ where = S.write_user_rule(slug, updates)
369
+ else:
370
+ data_md = os.path.join(root, "DATA.md")
371
+ try:
372
+ with open(data_md, encoding="utf-8") as handle:
373
+ text = handle.read()
374
+ except OSError:
375
+ print("truequery: no DATA.md here; run `truequery init` first "
376
+ "(or use --scope user)", file=sys.stderr)
377
+ return EXIT_ERROR
378
+ with open(data_md, "w", encoding="utf-8") as handle:
379
+ handle.write(S.data_md_with_rule(text, slug, updates))
380
+ where = data_md
381
+ except ValueError as exc:
382
+ print(f"truequery: {exc}", file=sys.stderr)
383
+ return EXIT_ERROR
384
+ eff = S.effective(root=root).rules[slug]
385
+ state = ("on" if eff["enabled"] else "off") + f" tier={eff['tier']}"
386
+ if eff["severity"]:
387
+ state += f" severity={eff['severity']}"
388
+ print(f"wrote {where}")
389
+ print(f"{slug} is now {state} (effective, from the {eff['source']} "
390
+ "settings)")
391
+ if args.scope == "user" and eff["source"] == "project":
392
+ print("note: DATA.md in this project sets this rule too, and a "
393
+ "project setting overrides your user setting here.")
394
+ return EXIT_CLEAN
395
+
396
+
397
+ def cmd_rules(args: argparse.Namespace) -> int:
398
+ records = std_registry.RECORDS
399
+ if getattr(args, "slug", None) == "set":
400
+ return _rules_set(args, os.path.abspath("."))
401
+ if args.slug:
402
+ slug = args.slug
403
+ if slug not in records:
404
+ print(f"truequery: unknown rule: {slug}", file=sys.stderr)
405
+ return EXIT_ERROR
406
+ try:
407
+ full = schema.load_all()[slug]
408
+ except (schema.SchemaError, KeyError, OSError) as exc:
409
+ print(f"truequery: cannot load {slug}: {exc}", file=sys.stderr)
410
+ return EXIT_ERROR
411
+ if args.sarif:
412
+ print(RP.render_sarif([], ENGINE_VERSION))
413
+ elif args.json:
414
+ print(json.dumps({
415
+ "rule": full.id, "title": full.title,
416
+ "domain": full.domain, "modes": list(full.modes),
417
+ "enforcement": full.enforcement,
418
+ "evidence": full.evidence,
419
+ "severity": full.severity, "tier": full.tier,
420
+ "implemented": full.implemented,
421
+ "enabled": full.enabled,
422
+ "statement": full.raw.get("statement"),
423
+ "rationale": full.raw.get("rationale"),
424
+ "fix": (full.raw.get("examples") or {}).get("good"),
425
+ "proof": ({
426
+ "setup": full.proof.setup, "bad": full.proof.bad,
427
+ "bad_result": full.proof.bad_result,
428
+ "good": full.proof.good,
429
+ "good_result": full.proof.good_result,
430
+ "note": full.proof.note} if full.proof else None),
431
+ }, indent=2, sort_keys=True))
432
+ else:
433
+ print(f"{full.id} [{full.tier}] "
434
+ f"(implemented={full.implemented} "
435
+ f"enabled={full.enabled})")
436
+ print(f"title: {full.title}")
437
+ print(f"statement: {full.raw.get('statement')}")
438
+ print(f"rationale: {full.raw.get('rationale')}")
439
+ good = (full.raw.get("examples") or {}).get("good")
440
+ if good:
441
+ print(f"fix:\n{good}")
442
+ if full.proof:
443
+ print(f"proof bad: {full.proof.bad} "
444
+ f"-> {full.proof.bad_result}")
445
+ print(f"proof good: {full.proof.good} "
446
+ f"-> {full.proof.good_result}")
447
+ return EXIT_CLEAN
448
+ rows = [(slug, rec) for slug, rec in sorted(records.items())
449
+ if args.all or rec["implemented"]]
450
+ if args.sarif:
451
+ print(RP.render_sarif([], ENGINE_VERSION))
452
+ return EXIT_CLEAN
453
+ try:
454
+ eff, _ = _effective_for(args, os.path.abspath("."))
455
+ except ValueError as exc:
456
+ print(f"truequery: {exc}", file=sys.stderr)
457
+ return EXIT_ERROR
458
+ if args.json:
459
+ print(json.dumps(
460
+ [{"rule": slug, "title": rec["title"],
461
+ "implemented": rec["implemented"],
462
+ "enabled": eff.rules[slug]["enabled"],
463
+ "tier": eff.rules[slug]["tier"],
464
+ "severity_override": eff.rules[slug]["severity"],
465
+ "source": eff.rules[slug]["source"],
466
+ "evidence": rec["evidence"],
467
+ "enforcement": rec["enforcement"],
468
+ "severity": rec["severity"]} for slug, rec in rows],
469
+ indent=2, sort_keys=True))
470
+ else:
471
+ for slug, rec in rows:
472
+ state = eff.rules[slug]
473
+ head = f"{slug} [{'on' if state['enabled'] else 'off'} " \
474
+ f"{state['tier']}]"
475
+ detail = (f"(implemented={rec['implemented']}, "
476
+ f"source: {state['source']})")
477
+ if state["severity"]:
478
+ detail = (f"(implemented={rec['implemented']}, "
479
+ f"severity: {state['severity']}, "
480
+ f"source: {state['source']})")
481
+ print(f"{head} {detail} {rec['title']}")
482
+ if not args.all:
483
+ hidden = sum(1 for rec in records.values()
484
+ if not rec["implemented"])
485
+ print(f"({hidden} unimplemented records hidden; "
486
+ f"use --all)")
487
+ return EXIT_CLEAN
488
+
489
+
490
+ def cmd_hook(args: argparse.Namespace) -> int:
491
+ return hook.main(args)
492
+
493
+
494
+ def cmd_install(args: argparse.Namespace) -> int:
495
+ return hook.install(args)
496
+
497
+
498
+ def cmd_uninstall(args: argparse.Namespace) -> int:
499
+ return hook.uninstall(args)
500
+
501
+
502
+ def cmd_status(args: argparse.Namespace) -> int:
503
+ return hook.status(args)
504
+
505
+
506
+ def cmd_ignore_add(args: argparse.Namespace) -> int:
507
+ """Narrowest-ignore with a reason. Line scope is required: the agent
508
+ may self-serve only one line; broader ignores are the user's call
509
+ (hand-edit .truequery/ignores.json)."""
510
+ if args.rule not in std_registry.RECORDS:
511
+ print(f"truequery: unknown rule: {args.rule}", file=sys.stderr)
512
+ return EXIT_ERROR
513
+ if not args.reason or not args.reason.strip():
514
+ print("truequery: --reason is required (who and what evidence)",
515
+ file=sys.stderr)
516
+ return EXIT_ERROR
517
+ match = re.match(r"^(.*):(\d+)$", args.target)
518
+ if not match:
519
+ print("truequery: target must be path:line — narrowest scope only; "
520
+ "file-level ignores need a human", file=sys.stderr)
521
+ return EXIT_ERROR
522
+ raw_path, line = match.group(1), int(match.group(2))
523
+ root = os.path.abspath(".")
524
+ # Store the path the engine reports: relative to the project, with
525
+ # forward slashes (`./models\\a.sql` -> `models/a.sql`).
526
+ path = P._rel(os.path.abspath(raw_path), root)
527
+ config = P.load_config(root)
528
+ try:
529
+ with open(raw_path if os.path.isabs(raw_path)
530
+ else os.path.join(root, raw_path),
531
+ encoding="utf-8") as handle:
532
+ text = handle.read()
533
+ except OSError:
534
+ print(f"truequery: no such file: {raw_path}", file=sys.stderr)
535
+ return EXIT_ERROR
536
+ spans = E._chunk_hashes(text, (config or {}).get("dialect"))
537
+ digest = E.stmt_hash_at(spans, line)
538
+ if digest is None:
539
+ print(f"truequery: line {line} is not inside a SQL statement",
540
+ file=sys.stderr)
541
+ return EXIT_ERROR
542
+ state_dir = os.path.join(root, ".truequery")
543
+ os.makedirs(state_dir, exist_ok=True)
544
+ ignores_path = os.path.join(state_dir, "ignores.json")
545
+ try:
546
+ with open(ignores_path, encoding="utf-8") as handle:
547
+ data = json.load(handle)
548
+ except (OSError, ValueError):
549
+ data = {"ignores": []}
550
+ if not isinstance(data, dict):
551
+ data = {"ignores": []}
552
+ entries = data.setdefault("ignores", [])
553
+ entry = {"rule": args.rule, "path": path, "line": line,
554
+ "stmt": digest, "offset": E.stmt_offset(spans, line),
555
+ "reason": args.reason.strip()}
556
+ if entry not in entries:
557
+ entries.append(entry)
558
+ with open(ignores_path, "w", encoding="utf-8") as handle:
559
+ json.dump(data, handle, indent=2)
560
+ print(f"ignore added: {args.rule} on {path}:{line} "
561
+ f"(reason recorded)")
562
+ return EXIT_CLEAN
563
+
564
+
565
+ def build_parser() -> argparse.ArgumentParser:
566
+ parser = argparse.ArgumentParser(
567
+ # Program name follows the invoked entry point (`truequery`
568
+ # or its `tq` alias), never a hardcoded default.
569
+ prog=os.path.basename(sys.argv[0] or "truequery"),
570
+ description="TrueQuery: catch SQL that silently returns the wrong "
571
+ "answer. Offline, deterministic, no LLM.")
572
+ # The plugin's hook shim uses this as a handshake: it runs a
573
+ # `truequery` on PATH only when it prints exactly "truequery <plugin
574
+ # version>", so a stale or foreign binary is never trusted silently.
575
+ parser.add_argument("--version", action="version",
576
+ version=f"truequery {ENGINE_VERSION}")
577
+ sub = parser.add_subparsers(dest="command", required=False)
578
+
579
+ # `check` is what agents tend to guess first; accept it.
580
+ p_detect = sub.add_parser(
581
+ "detect", aliases=["check"],
582
+ help="run enabled checks on files/dirs (zero config)",
583
+ epilog=EXIT_HELP)
584
+ p_detect.add_argument("paths", nargs="+", help=".sql files or dirs")
585
+ _add_output(p_detect)
586
+ _add_rule_flags(p_detect)
587
+ p_detect.set_defaults(func=cmd_detect)
588
+
589
+ p_review = sub.add_parser(
590
+ "review", help="checks plus the receipt (what was/wasn't checked)",
591
+ epilog=EXIT_HELP)
592
+ p_review.add_argument("paths", nargs="*",
593
+ help="files/dirs; default: the project")
594
+ _add_output(p_review)
595
+ _add_rule_flags(p_review)
596
+ p_review.set_defaults(func=cmd_review)
597
+
598
+ p_init = sub.add_parser(
599
+ "init", help="detect stack/dialect; write DATA.md; seed contracts/")
600
+ p_init.add_argument("path", nargs="?", default=".",
601
+ help="project root (default .)")
602
+ p_init.add_argument("--force", action="store_true",
603
+ help="rewrite DATA.md (contracts are never "
604
+ "overwritten)")
605
+ p_init.add_argument("--json", action="store_true")
606
+ p_init.add_argument("--sarif", action="store_true")
607
+ p_init.set_defaults(func=cmd_init)
608
+
609
+ p_context = sub.add_parser(
610
+ "context", help="DATA.md summary + referenced contracts + directives")
611
+ p_context.add_argument("--target", default=None,
612
+ help="only contracts this SQL file (or the SQL "
613
+ "files in this folder) references")
614
+ p_context.add_argument("--json", action="store_true")
615
+ p_context.add_argument("--sarif", action="store_true")
616
+ p_context.set_defaults(func=cmd_context)
617
+
618
+ p_rules = sub.add_parser(
619
+ "rules", help="list rules with effective settings; "
620
+ "'set' adjusts a rule (human only)",
621
+ epilog="truequery rules set <slug> --enabled on|off --tier T "
622
+ "--severity S [--scope project|user] # human-only "
623
+ "command; agents must never change rule settings.")
624
+ p_rules.add_argument("--all", action="store_true",
625
+ help="include unimplemented records")
626
+ p_rules.add_argument("slug", nargs="?",
627
+ help="show one record (statement, fix, proof); "
628
+ "'set' writes rule settings")
629
+ p_rules.add_argument("target", nargs="?",
630
+ help="slug for 'truequery rules set'")
631
+ p_rules.add_argument("--json", action="store_true")
632
+ p_rules.add_argument("--sarif", action="store_true")
633
+ p_rules.add_argument("--enabled", choices=["on", "off"],
634
+ help="(set) enable or disable the rule")
635
+ p_rules.add_argument("--tier", choices=["immediate", "stop"],
636
+ help="(set) per-edit or end-of-turn tier")
637
+ p_rules.add_argument("--severity", choices=list(
638
+ ("error", "warning", "advisory")),
639
+ help="(set) severity in every mode")
640
+ p_rules.add_argument("--scope", default="project",
641
+ choices=["project", "user"],
642
+ help="(set) write to DATA.md or user config")
643
+ _add_rule_flags(p_rules)
644
+ p_rules.set_defaults(func=cmd_rules)
645
+
646
+ p_hook = sub.add_parser(
647
+ "hook", help="agent-harness hook entrypoint (event JSON on stdin)")
648
+ p_hook.add_argument("--harness", default="auto",
649
+ choices=["auto", "claude", "codex", "muse"])
650
+ p_hook.add_argument("--event", default="auto",
651
+ choices=["auto", "post", "stop"],
652
+ help="override the payload event (tests)")
653
+ p_hook.add_argument("--log-file", default=None)
654
+ p_hook.set_defaults(func=cmd_hook)
655
+
656
+ p_install = sub.add_parser(
657
+ "install", help="write agent-harness hook configs")
658
+ p_install.add_argument("--harness", default="auto",
659
+ choices=["auto", "claude", "codex", "muse"])
660
+ p_install.add_argument("--scope", default="project",
661
+ choices=["project", "global"])
662
+ p_install.set_defaults(func=cmd_install)
663
+
664
+ p_uninstall = sub.add_parser(
665
+ "uninstall", help="remove tq-installed harness hook configs")
666
+ p_uninstall.add_argument("--harness", default="auto",
667
+ choices=["auto", "claude", "codex", "muse"])
668
+ p_uninstall.add_argument("--scope", default="project",
669
+ choices=["project", "global"])
670
+ p_uninstall.set_defaults(func=cmd_uninstall)
671
+
672
+ p_status = sub.add_parser(
673
+ "status", help="report hook state per harness (never writes)")
674
+ p_status.add_argument("--harness", default="auto",
675
+ choices=["auto", "claude", "codex", "muse"])
676
+ p_status.add_argument("--scope", default="project",
677
+ choices=["project", "global"])
678
+ p_status.set_defaults(func=cmd_status)
679
+
680
+ p_ignore = sub.add_parser(
681
+ "ignore", help="narrowest-ignore with a reason (line scope only)")
682
+ p_ignore_sub = p_ignore.add_subparsers(dest="ignore_command",
683
+ required=True)
684
+ p_add = p_ignore_sub.add_parser("add")
685
+ p_add.add_argument("rule", help="rule slug from tq rules")
686
+ p_add.add_argument("target", help="path:line (line required)")
687
+ p_add.add_argument("--reason", default=None,
688
+ help="who and what evidence (required)")
689
+ p_add.set_defaults(func=cmd_ignore_add)
690
+ return parser
691
+
692
+
693
+ def main(argv: list[str] | None = None) -> int:
694
+ parser = build_parser()
695
+ args = parser.parse_args(argv)
696
+ if args.command is None:
697
+ parser.print_help()
698
+ print("\nTry: tq detect <file.sql> (start here) | tq init (new project)")
699
+ return EXIT_ERROR
700
+ try:
701
+ return args.func(args)
702
+ except P.ContextError as exc:
703
+ # Unreadable DATA.md / user config: say so, never fall back to
704
+ # defaults silently.
705
+ print(f"truequery: {exc}", file=sys.stderr)
706
+ return EXIT_ERROR
707
+
708
+
709
+ if __name__ == "__main__":
710
+ sys.exit(main())