Skip to content

#154: dbt-expectations prune+grade adapter via manifest compiled_code - #266

Merged
wjduenow merged 22 commits into
devfrom
feature/154-dbt-expectations-prune
Jul 6, 2026
Merged

#154: dbt-expectations prune+grade adapter via manifest compiled_code#266
wjduenow merged 22 commits into
devfrom
feature/154-dbt-expectations-prune

Conversation

@wjduenow

@wjduenow wjduenow commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Super plan for #154 — a prune+grade adapter that consumes dbt-expectations (and
other generic/namespaced) tests' already-compiled SQL from manifest.json and runs
them through the existing custom_sql pipeline, so they get the same
kept/kept-uncertain/dropped/flagged treatment as the built-ins. Closes the
un-graded half of the prune gate for teams that aren't greenfield.

Phase: detailing (awaiting approval)
Structure: single cohesive plan (not an epic) — reuses custom_sql, one design.
Decisions: 18 (DEC-001…018)
Stories: 8 implementation + Quality Gate + Patterns & Memory

Key shape decisions

  • Reuse CandidateTestCustomSQL — no 7th test type (DEC-001).
  • Built on sqlglot AST — determinism detection, aggregate-shape gate, and
    comment-tolerant validation all need AST parsing because compiled_code is
    dbt's foreign-rendered SQL; the #116 string-substitution machinery is safe
    but wrong-tool (DEC-006).
  • scope=sample deferred → full-scope for ingested tests in pass 1 (dbt's
    quoted relation can't be string-substituted; sampling silently degrades to
    kept-without-evidence) (DEC-007).
  • Row-returning only; aggregate/scalar bodies skip-recorded — a correctness
    gate (wrapping a scalar in COUNT(*) AS failures yields failures=1 always →
    silent wrong "kept") (DEC-004).
  • Opt-in --from-manifest + --grade on prune-existing; --grade requires
    --from-manifest, off by default; existing runs byte-unchanged (DEC-002/005/018).
  • Real dbt compile fixture (dbt-duckdb + dbt-expectations), per-node
    hand-patch fallback (DEC-017).

Architecture review

Four focused reviews (SQL-handling/determinism · manifest read-back · grade wiring ·
fixture feasibility). Two blockers, both resolvable (compiled fixture; scope=sample
substitution) — no design dead-ends. Full pass/concern/blocker table in the plan doc.

Deferred (filed as follow-up issues)

  • Aggregate/scalar macro support via source-table routing (Direction-2 precedent).
  • scope=sample support for ingested tests via sqlglot AST relation-rewriting.

Plan document

See plans/super/154-dbt-expectations-prune.md.

Next steps

  • Review the plan in this PR.
  • Approve in Claude Code to proceed to devolve (beads creation).

Summary by CodeRabbit

  • New Features

    • Added support for pruning dbt “compiled test” nodes from manifest.json, surfaced as custom_sql candidates.
    • prune-existing now supports --from-manifest and optional --grade, adding an extra grading stage and producing grade artifacts.
    • Diff now includes ingested tests in the results table while keeping read-only behavior (no proposed test files).
  • Bug Fixes

    • Improved handling of ingested compiled_code: comment-tolerant safety checks, determinism validation, and safer fallback routing.
    • Manifest-ingested tests now evaluate full-scope even when sample scope is requested, and diff “why” threads macro identity for ingested drops/uncertains.
  • Tests

    • Added coverage for the end-to-end --from-manifest/--grade workflow and “why” construction plus read-only diff output.

…ifest compiled_code

Single cohesive plan (not an epic): reuse the custom_sql pipeline to prune+grade
dbt-expectations (and other generic/namespaced) tests read from manifest.json
compiled_code. 18 decisions, 8 implementation stories + Quality Gate + Patterns.

Key shape: built on sqlglot AST (determinism / aggregate-shape / comment-tolerant
validation); scope=sample deferred to full-scope in pass 1; opt-in --from-manifest
+ --grade on prune-existing.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65a4fabb-e64f-4011-ba31-7c1f50a0ca0d

📥 Commits

Reviewing files that changed from the base of the PR and between 0fc5c67 and 31c606a.

📒 Files selected for processing (9)
  • docs/ingest-ops.md
  • plans/super/154-dbt-expectations-prune.md
  • src/signalforge/cli/prune_existing.py
  • src/signalforge/diff/engine.py
  • src/signalforge/manifest/__init__.py
  • src/signalforge/manifest/loader.py
  • src/signalforge/prune/compiler.py
  • tests/diff/test_engine.py
  • tests/fixtures/regenerate.sh
✅ Files skipped from review due to trivial changes (1)
  • plans/super/154-dbt-expectations-prune.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/signalforge/manifest/init.py
  • src/signalforge/prune/compiler.py
  • src/signalforge/manifest/loader.py
  • tests/fixtures/regenerate.sh
  • tests/diff/test_engine.py
  • src/signalforge/diff/engine.py
  • docs/ingest-ops.md
  • src/signalforge/cli/prune_existing.py

📝 Walkthrough

Walkthrough

This PR adds manifest-ingested custom_sql support for dbt compiled test nodes, including new manifest models, SQL classification helpers, prune/compiler/diff routing, CLI flags, fixture support, and tests. It also documents the flow across rules and ops guides.

Changes

Manifest-ingested custom_sql feature

Layer / File(s) Summary
Provenance flag and manifest test models
src/signalforge/draft/models.py, src/signalforge/manifest/models.py, src/signalforge/manifest/loader.py, src/signalforge/manifest/__init__.py, tests/manifest/*, tests/draft/*
Adds from_manifest, GenericTest/TestMetadata, Manifest.tests, and test association/serialization coverage.
Compiled SQL analysis helpers
src/signalforge/ingest/_compiled_sql.py, tests/ingest/test_compiled_sql.py
Adds AST-based determinism, row-returning, and safety checks for compiled dbt SQL.
Manifest test ingestion bridge
src/signalforge/ingest/reader.py, src/signalforge/ingest/__init__.py, tests/ingest/test_manifest_tests.py
Reads manifest tests into candidates or skips and synthesizes macro-based rationale.
Prune compiler and source-table routing
src/signalforge/prune/engine.py, src/signalforge/prune/compiler.py, tests/prune/*
Forces ingested custom_sql through full-scope source evaluation and returns compiled SQL verbatim after checks.
Diff why and proposed file emission
src/signalforge/diff/engine.py, tests/diff/test_engine.py
Threads manifest macro identity into why and suppresses proposed test files when requested.
CLI ingestion and grading orchestration
src/signalforge/cli/prune_existing.py, tests/cli/test_prune_existing.py
Adds --from-manifest/--grade, staged orchestration, and progress renumbering.
dbt-expectations fixture and regeneration
tests/fixtures/dbt_project_expectations/*, tests/fixtures/regenerate.sh, tests/fixtures/manifest/generic_test_nodes.json, tests/fixtures/README.md, tests/manifest/test_expectations_fixture_loads.py
Adds the compiled manifest fixture and regeneration/scrubbing support.
Rules and docs for manifest-ingested prune flow
.claude/rules/*, docs/*, plans/super/154-dbt-expectations-prune.md, src/signalforge/skills/signalforge/SKILL.md
Documents the new reader, prune, diff, CLI, and grading behavior end to end.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit hopped through manifest trees,
With compiled tests and careful ease.
From source to diff, the paths now sing,
Macro names in why can ring. 🐇
Safe SQL, full scope, the hop is true,
And from_manifest guides us through. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: adding prune+grade support for dbt-expectations via manifest compiled_code.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

wjduenow added 20 commits July 4, 2026 10:30
…st.tests sibling filter

Add a frozen GenericTest Pydantic model (+ nested TestMetadata) carrying a
test node's compiled_code / test_metadata / column_name / depends_on /
attached_node / file_key_name, and surface resource_type=="test" nodes into a
new Manifest.tests dict via a sibling filter in _load (parallel to
filtered_sources). nodes stays model-only.

Add associate_test_model(): a feature-detect precedence ladder (attached_node
for v10+ -> depends_on.nodes disambiguated by file_key_name / kwargs.model ref
for v9) so association never version-branches (the loader discards the detected
version).

Silent-degrade on absent/null compiled_code (mirrors Column.data_type=None,
#159); the "not a silent skip" surfacing is a downstream ingest concern.

Tests: StrictGenericTest(extra="forbid") drift detector over a hand-authored
generic_test_nodes.json fixture (both v10-attached and v9-no-attached shapes) +
poisoned-key rejection; association-ladder coverage for both version shapes,
single/multi/no-dep and ambiguous cases; load() surfaces test nodes into
Manifest.tests while keeping nodes model-only; empty-default construction; and
confirmation that the frozen model_copy catalog overlay passes Manifest.tests
through untouched.

Traces to DEC-008, DEC-009 (plans/super/154-dbt-expectations-prune.md).
… test SQL

Add src/signalforge/ingest/_compiled_sql.py — three pure, string-in/verdict-out
functions over a dbt-compiled ``compiled_code`` body, all sqlglot-AST-based
(#154 US-002, DEC-006/DEC-004/DEC-012/DEC-013):

- is_deterministic_sql: flags TABLESAMPLE + non-deterministic funcs (RAND/RANDOM/
  RND, CURRENT_TIMESTAMP/CURRENT_DATE/NOW/GETDATE, UUID/GENERATE_UUID) via AST
  function-node inspection, so a column named random_id / current_timestamp_col
  and those tokens inside a string literal or comment never false-positive.
- is_row_returning: rejects scalar/aggregate bodies (all top-level projections
  collapsing aggregates + no GROUP BY) while treating windowed aggregates and
  subquery aggregates as row-returning — the row_count_between silent-wrong-kept
  guard (AR row 10).
- validate_ingested_sql: comment-tolerant sibling of validate_test_sql; strips
  --/ /* */ comments (string-literal-aware) before the ;/unbalanced-paren
  injection scan, but still fails loud on real injection (AR row 11). NOT a reuse
  of the #116 validator.

Placement documented in the module docstring: lives under ingest (the primary
consumer via the manifest-test bridge, US-003) as a pure str->verdict seam
carrying no ingest domain types, so the prune compiler (US-004) can import it
without cross-stage coupling. Extends the sqlglot confinement (#159 DEC-008)
from draft/parser to this one module.

tests/ingest/test_compiled_sql.py: 42 tests incl. a planted-violation negative
per helper (column-name / string-literal / windowed-aggregate / subquery-
aggregate / comment-hidden-injection cases). Full validation green.
…ipt (#154 US-006)

Add tests/fixtures/dbt_project_expectations/ — the first committed manifest
built via real `dbt deps && dbt compile` (dbt-duckdb + dbt-expectations 0.10.4)
rather than `dbt parse`, so its five `resource_type == "test"` nodes carry
populated `compiled_code`. #154's manifest-test bridge reads that Jinja-resolved
SQL off test nodes; every prior fixture is `dbt parse` output (compiled_code
null, zero test nodes).

The five dbt-expectations tests engineer the four #154 prune outcomes through
the macro args (dbt resolves them statically, per the row_count_between e2e
precedent):

- expect_column_values_to_be_between, impossible bounds [1000,2000] on
  amount (100/200) -> returns failing rows -> KEPT
- expect_column_values_to_be_between, vacuous bounds [0,1e6] -> ALWAYS-PASSES
- expect_column_values_to_not_be_null on natural-NOT-NULL order_id -> ALWAYS-PASSES
- expect_table_row_count_to_be_between -> aggregate COUNT(*) -> SKIP
- expect_row_values_to_have_recent_data -> compiled SQL has now() -> non-deterministic SKIP

Extends tests/fixtures/regenerate.sh with a `dbt deps && dbt compile` run and a
broader jq scrub that also nulls every per-node `created_at` epoch (verified:
two independent compiles produce byte-identical scrubbed output). The compiled
manifest is committed at target/manifest.json via a fixture-local .gitignore
negation so `signalforge.manifest.load(fixture_dir)` finds it at the default
path.

tests/manifest/test_expectations_fixture_loads.py is the network-free loads
guard: it asserts the manifest loads via signalforge.manifest.load and (reading
the JSON directly) that all five compiled test nodes survive. Tolerant of the
sibling Manifest.tests surface (bead .1) not yet being merged — the
Manifest.tests assertions run only when that attribute exists.

Traces to DEC-017, DEC-008 of plans/super/154-dbt-expectations-prune.md.
…TestCustomSQL (#154 US-003)

Add read_manifest_tests(manifest, model, *, project_dir=None) -> IngestResult:
walk Manifest.tests, select nodes associated to the target model via
associate_test_model, and route each by the US-002 sqlglot helpers —

- absent/blank compiled_code -> SkippedTest(custom-or-generic-test), detail
  naming dbt compile (DEC-010); one prepended summary skip when ALL associated
  nodes lack compiled_code (not a silent skip).
- NOT is_row_returning (aggregate/scalar) -> SkippedTest(malformed-supported-test)
  (DEC-004; aggregate support deferred to #267).
- NOT is_deterministic_sql -> SkippedTest(malformed-supported-test) (DEC-012).
- validate_ingested_sql (comment-tolerant) raises -> SkippedTest(malformed).
- else -> model-level CandidateTestCustomSQL(sql=compiled_code, column=None)
  with a synthesized envelope-safe rationale (DEC-001, DEC-011).

Rationale = '<namespace-label> <macro>(column=..., <args>)' built at frozen
construction; the </ARTIFACT> close tag (literal + whitespace-split) is stripped
so a hostile macro arg cannot fail-close the grade run. Macro identity rides on
the rationale so the diff why cascade names it (DEC-015). SkipReason stays the
closed 3-value literal; stage-0 (no logging). Re-exported from signalforge.ingest.

Tests cover all five dispositions, the </ARTIFACT> strip, the all-missing
summary, association filtering, and the real committed dbt-compiled fixture.
…e, not proposed files (#154 US-005)

DEC-015 of #154 — two behaviours for INGESTED (read-only) manifest/external
tests flowing through the custom_sql prune pipeline:

1. Macro name into the diff `why`. The ingest bridge (US-003) carries the
   source-macro identity on the custom_sql test's `rationale`
   ("dbt-expectations expect_...(args)"). The KEPT tier already surfaces it via
   the rationale -> evidence -> fallback cascade; the DROPPED and kept-uncertain
   tiers bypassed that cascade and showed only decision.why, leaving the
   operator no way to locate the test to remove. New `_macro_why` threads the
   macro identity (front, so it survives max_why_chars truncation) ahead of the
   prune verdict/cause. Scoped to `custom_sql` so a built-in/drafted test with a
   drafter rationale keeps the issue-#50 carve-out (decision.why only) — the
   drop CATEGORY still rides the separate drop_reason column; the #50 cause
   stays load-bearing on kept-uncertain.

2. Ingested tests on the kept/dropped/flagged table, NEVER proposed_test_files.
   New `emit_test_files: bool = True` kwarg on render_diff gates
   emit_proposed_test_files. prune-existing (read-only; never drafts, so every
   custom_sql it prunes is external) passes emit_test_files=False, so an ingested
   test appears only as a table row, never re-surfaced as an authored .sql file.
   generate keeps the default True (#116 behaviour unchanged).

tier stays the 4-value literal; no audit_schema_version bump (no shape change);
no origin marker added to CandidateTestCustomSQL. tests/diff/test_engine.py adds
kept/dropped/kept-uncertain macro-why coverage, the emit_test_files gate, and a
built-in-unchanged regression pin.
…routing + comment-tolerant compile (#154 US-004)

Route manifest-ingested custom_sql candidates (dbt compiled_code, already
Jinja-resolved) to full-scope-against-source evaluation regardless of the
configured --scope, and validate them comment-tolerantly. dbt renders the
model relation with its own quoting, which the {{ this }} string-substitution
cannot match — so under scope=sample every ingested test silently degraded to
kept-without-evidence (AR row 8). Now they run full-scope against the source
(never a _SESSION._sf_sample_* temp table), bounded by maximum_bytes_billed.

Marker (DEC-007): CandidateTestCustomSQL gains a runtime-only
from_manifest: bool = Field(default=False, exclude=True) provenance flag — the
"typed category at the source" discriminator (grade-layer.md), set by
read_manifest_tests, read by the engine + compiler. exclude=True keeps it out
of model_dump / model_dump_json, so the diff candidate_hash, proposed YAML, and
every committed custom_sql fixture stay byte-identical; drafted custom_sql
(from_manifest=False) keeps its existing sample behaviour unchanged (DEC-016).

- engine: _test_requires_source_table returns True for ingested custom_sql
  under either sample strategy (joins the metadata-aggregate bypass set); one
  INFO fires when scope=sample was requested for an ingested batch.
- compiler: _compile_custom_sql takes an early full-scope branch for ingested
  bodies — skips the {{ this }}/sample substitution, uses the comment-tolerant
  validate_ingested_sql (DEC-013), and keeps the belt-and-braces
  is_deterministic_sql -> _InvalidIdentifier -> kept-without-evidence fallback
  (DEC-012). DropReason stays the locked 5-value Literal; no PruneEvent field,
  no _PRUNE_AUDIT_SCHEMA_VERSION bump.

Tests: 11 new (engineered-determinism via FakeBigQueryClient) covering ingested
tautology drop / real-failure kept / non-deterministic kept-without-evidence /
scope=sample -> full-scope with one INFO and source-not-temp dispatched SQL /
comment-bearing not false-rejected / drafted custom_sql byte-unchanged / a
mixed drafted+ingested batch pinning the per-test override arm.
#154 US-007)

Add two opt-in flags to `signalforge prune-existing`:

- --from-manifest (DEC-005): ALSO ingest the model's dbt-compiled manifest
  test nodes via ingest.read_manifest_tests and merge the row-returning +
  deterministic compiled_code bodies (as from_manifest=True custom_sql
  candidates) into the prune set alongside the --schema / tests/*.sql
  candidates. Manifest skip records fold into the existing skipped-test
  report. Off by default: byte-identical to the pre-#154 behaviour.

- --grade (DEC-002 / DEC-018): opt-in LLM-as-judge grade stage on the
  ingested manifest tests. Requires --from-manifest (schema.yml / singular
  tests carry rationale=None, so grading them is noise) — --grade alone is
  an input-validation failure (CliInputError, exit 2) raised at handler
  entry. Wires load_grade_config + grade_artifacts between prune and diff,
  feeds grading_report into render_diff (enabling the flagged tier), writes
  grade.json / grade.jsonl, and renumbers progress to [N/4]. Credential
  gate is implicit (missing key -> LLMAuthError tier 3 at call time).

6-surface parity: argparse help, handler/module docstrings, docs/cli-ops.md
(flag reference + prose sections), test names + docstrings, and SKILL.md.
Tests cover: --grade-without---from-manifest -> exit 2, --from-manifest OFF
(no manifest ingestion), --from-manifest ON (manifest custom_sql on the diff
table via the real dbt_project_expectations fixture), skip folding, grade
stage runs (faked grade_artifacts), and [N/4] progress renumber.
…s prune+grade adapter (#154 US-008)

Document the manifest-compiled-SQL prune path (issue #154) across the five
ops docs, verified against the shipped code and the committed
tests/fixtures/dbt_project_expectations/ fixture:

- ingest-ops.md: read_manifest_tests recognition path — the four gates
  (presence / row-returning / deterministic / comment-tolerant safety),
  the closed-SkipReason routing, macro-identity rationale, and the "run
  dbt compile" remediation surface. Documents the KEY FINDING that
  dbt-expectations wraps every macro (incl. expect_table_row_count) in a
  row-returning validation_errors shell, so aggregate-SKIP fires only on
  bare SELECT COUNT(*) bodies.
- prune-ops.md: how ingested compiled tests route through the custom_sql
  pipeline; the DEC-007 full-scope decision (scope=full always; --scope=
  sample emits an INFO and still runs full); comment-tolerant validation;
  the from_manifest source-vs-drafted split.
- diff-ops.md: macro identity into the diff why; ingested tests appear on
  the kept/dropped/flagged table, never as proposed .sql files (read-only).
- cli-ops.md: verified the US-007 --from-manifest/--grade rows; filled the
  exit-code list (CliInputError tier 2, LLM/grade errors tier 3) and added
  a worked example pruning a real dbt-expectations schema.yml.
- grade-ops.md: --grade behavior on prune-existing (opt-in LLM grade of
  ingested tests via the synthesized rationale; requires --from-manifest;
  off by default; credential/cost implications).

mkdocs build (non-strict) emits no new anchor-breakage vs baseline.
Fixes from 4 diverse-lens review passes (correctness / rules / test-signal / integration):
- diff macro-why: scope to from_manifest ingested custom_sql only — was gated on
  type==custom_sql, leaking the drafter rationale into generate's dropped/kept-uncertain
  why and re-breaking the issue-#50 carve-out (confirmed regression, 2 reviewers).
- prune-existing --grade: grade ONLY the manifest-ingested tests, not the merged
  candidate — grading the operator's schema.yml built-ins (empty rationale) wasted
  LLM calls and could flip a kept built-in to the flagged tier (DEC-002 intent).
- ingest _compiled_sql: backslash-escape-aware literal blanking; drop the cross-layer
  warehouse _strip_string_literals private import (self-contained _blank_sql_literals).
- tests: pin from_manifest exclude=True byte-identity (unguarded DEC-016 invariant);
  grade->LLMAuthError exit-3 path; drafted-custom_sql why regression guard;
  prune-existing never emits proposed_test_files (read-only).
- fixture comment + cli-ops caveat: row-count macro is pruned (validation_errors wrapper),
  not aggregate-skipped; proposed-files suppression scoped in the byte-identical claim.
- manifest-readers.md: GenericTest + Manifest.tests sibling filter + feature-detect association
- ingest-layer.md: read_manifest_tests source + four-gate sqlglot classification + SkipReason reuse
- prune-engine.md: from_manifest full-scope routing + comment-tolerant validation + DropReason lock
- business-rule-tests.md: custom_sql third source (manifest); gate on from_manifest not type
- llm-drafter.md: sqlglot confinement 2nd/3rd importer (foreign-SQL = AST, not string-sub)
@wjduenow wjduenow changed the title #154: dbt-expectations prune+grade adapter via manifest compiled_code (plan) #154: dbt-expectations prune+grade adapter via manifest compiled_code Jul 6, 2026
@wjduenow
wjduenow requested a review from Copilot July 6, 2026 00:12
@wjduenow
wjduenow marked this pull request as ready for review July 6, 2026 00:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements issue #154’s core “manifest compiled SQL” adapter so dbt-compiled generic/namespaced tests (e.g., dbt-expectations) can be pruned and optionally graded by reusing the existing custom_sql pipeline, with conservative-bias routing preserved.

Changes:

  • Extend the manifest loader to surface resource_type == "test" nodes as Manifest.tests (GenericTest) and add association logic (associate_test_model).
  • Add an ingest bridge (read_manifest_tests) plus sqlglot-based compiled-SQL gates (determinism, scalar/aggregate-shape detection, comment-tolerant safety scan) and wire from_manifest=True routing into prune + diff.
  • Add prune-existing --from-manifest and --grade wiring, plus a real dbt compile fixture project and comprehensive tests/docs.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/prune/test_engine.py Adds prune-engine routing tests for manifest-ingested custom_sql (full-scope bypass under sample).
tests/prune/test_compiler.py Adds compiler unit tests for from_manifest=True behavior (no sampling, comment-tolerant validation, determinism fallback).
tests/manifest/test_models.py Adds GenericTest/Manifest.tests model tests + drift sentinel fixture usage.
tests/manifest/test_loader.py Tests loader filtering of test nodes into Manifest.tests and association ladder behavior.
tests/manifest/test_expectations_fixture_loads.py Smoke test ensuring the committed dbt-expectations compiled manifest fixture loads and contains compiled test SQL.
tests/ingest/test_manifest_tests.py End-to-end unit tests for read_manifest_tests classification, rationale synthesis, and skip recording.
tests/ingest/test_compiled_sql.py Tests sqlglot-based determinism/row-shape analysis and comment-tolerant safety validation.
tests/fixtures/regenerate.sh Adds regen logic for the dbt-expectations compiled fixture (dbt deps + dbt compile + scrubbing).
tests/fixtures/README.md Documents the new expectations fixture and how to regenerate it.
tests/fixtures/manifest/generic_test_nodes.json Hand-authored generic test node fragments for loader/model drift + association tests.
tests/fixtures/dbt_project_expectations/profiles.yml DuckDB profile committed for offline fixture regeneration.
tests/fixtures/dbt_project_expectations/packages.yml Pins dbt-expectations dependency for the compiled fixture.
tests/fixtures/dbt_project_expectations/package-lock.yml Commits a reproducible dbt deps resolution for the fixture.
tests/fixtures/dbt_project_expectations/models/schema.yml Declares dbt-expectations tests used to exercise prune outcomes.
tests/fixtures/dbt_project_expectations/models/orders.sql Provides a deterministic 2-row model for compiled test outcomes.
tests/fixtures/dbt_project_expectations/dbt_project.yml Fixture project config + vars needed for deterministic compilation.
tests/fixtures/dbt_project_expectations/.gitignore Ensures only target/manifest.json is committed from compiled artifacts.
tests/draft/test_models.py Guards that from_manifest stays excluded from serialization for hash/byte-identity invariants.
tests/draft/test_drift_detector.py Updates strict drift model mirror to include runtime-only from_manifest.
tests/diff/test_engine.py Ensures diff why includes macro identity for ingested tests and suppresses proposed test files when requested.
tests/cli/test_prune_existing.py Adds CLI tests for --from-manifest, --grade dependency, progress renumbering, and proposed-file suppression.
src/signalforge/skills/signalforge/SKILL.md Documents new prune-existing flags and behavior.
src/signalforge/prune/engine.py Routes from_manifest custom_sql to source-table/full-scope under sampling and emits one INFO.
src/signalforge/prune/compiler.py Adds manifest-ingested custom_sql compile branch with determinism + comment-tolerant validation fallback.
src/signalforge/manifest/models.py Introduces GenericTest, TestMetadata, and Manifest.tests.
src/signalforge/manifest/loader.py Filters test nodes into Manifest.tests and adds associate_test_model ladder.
src/signalforge/manifest/init.py Re-exports GenericTest and associate_test_model.
src/signalforge/ingest/reader.py Implements read_manifest_tests bridge and rationale synthesis + skip recording.
src/signalforge/ingest/_compiled_sql.py New sqlglot-based helpers for determinism/row-shape analysis and comment-tolerant safety scanning.
src/signalforge/ingest/init.py Exposes read_manifest_tests as a public ingest entry point.
src/signalforge/draft/models.py Adds runtime-only from_manifest flag to CandidateTestCustomSQL (excluded from serialization).
src/signalforge/diff/engine.py Threads macro identity into dropped/kept-uncertain why for ingested tests; adds emit_test_files option.
src/signalforge/cli/prune_existing.py Adds --from-manifest and --grade, grade-stage wiring, and always suppresses proposed test files.
docs/prune-ops.md Documents routing and validation differences for manifest-ingested vs drafted custom_sql.
docs/ingest-ops.md Documents read_manifest_tests gates, skip reasons, and macro identity behavior.
docs/grade-ops.md Documents prune-existing --grade behavior for ingested manifest tests.
docs/diff-ops.md Documents macro identity threading into diff why for ingested tests and read-only behavior.
docs/cli-ops.md Documents new CLI flags, behaviors, and exit-code implications.
.claude/rules/prune-engine.md Updates project rules with #154 routing/validation/determinism notes.
.claude/rules/manifest-readers.md Documents the new Manifest.tests surface and association ladder.
.claude/rules/llm-drafter.md Updates sqlglot confinement notes for the new ingest compiled-SQL module.
.claude/rules/ingest-layer.md Documents read_manifest_tests classification gates and skip surfaces.
.claude/rules/business-rule-tests.md Notes the new custom_sql source and from_manifest scoping requirements across layers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/fixtures/regenerate.sh
Comment thread src/signalforge/cli/prune_existing.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/signalforge/prune/compiler.py (1)

70-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose compiled-SQL helpers through a public seam.

prune.compiler now reaches into signalforge.ingest._compiled_sql, which makes prune depend on an ingest-private module. Please re-export these helpers through signalforge.ingest.__all__ or move the shared SQL validation seam to a common/warehouse module before importing it here. As per coding guidelines, "Ensure the public API of each subpackage is defined via __all__; contract details are in matching docs/*-ops.md files. Prefix internal implementation details with _."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/signalforge/prune/compiler.py` at line 70, `prune.compiler` is importing
compiled-SQL helpers from an ingest-private module instead of a public seam.
Move `is_deterministic_sql` and `validate_ingested_sql` behind a public API by
re-exporting them from `signalforge.ingest.__all__` or relocating the shared SQL
validation helpers to a common/warehouse module, then update the import in
`prune.compiler` to use that public symbol path.

Source: Coding guidelines

src/signalforge/ingest/_compiled_sql.py (1)

214-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the two hand-rolled quote-scanners.

_strip_sql_comments and _blank_sql_literals each independently re-implement the same quote/backslash/doubled-quote tracking state machine (~40 lines apiece), differing only in what happens to comment/literal content. Since validate_ingested_sql composes both, a future fix to quote-handling in one (e.g. a new escape convention) risks not being mirrored in the other, silently reintroducing a bypass. Consider extracting a shared low-level scanner (e.g., a generator yielding (char, in_quote, in_comment) or span classification) that both functions consume.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/signalforge/ingest/_compiled_sql.py` around lines 214 - 357, The
quote/backslash/doubled-quote handling is duplicated in _strip_sql_comments and
_blank_sql_literals, so they can drift and create inconsistent SQL scanning
behavior. Extract the shared quote-state machine into a common helper used by
both functions, and have validate_ingested_sql rely on that shared logic for
comment stripping and literal blanking so any future escape-rule change is
applied consistently.
tests/fixtures/regenerate.sh (1)

140-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use mv -f for the scrub temp-file swap.

Line 152's mv "$out.tmp" "$out" should use -f to avoid any interactive overwrite prompt, consistent with every other file operation in this script and the repo's shell guideline.

🛠️ Proposed fix
   ' "$in" >"$out.tmp"
-  mv "$out.tmp" "$out"
+  mv -f "$out.tmp" "$out"
 }

As per coding guidelines, "Use mv -f instead of mv to force overwrite without prompting."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fixtures/regenerate.sh` around lines 140 - 153, The temp-file swap in
scrub_compiled_manifest should force overwrite instead of relying on plain mv.
Update the mv call that replaces "$out" so it uses the force flag, matching the
script’s non-interactive file operation pattern and the repo shell guideline.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ingest-ops.md`:
- Around line 419-423: The heading under the prunable vs. skip-recorded section
has a gate-count mismatch: it says “Three gates” while the described flow in the
same paragraph lists four checks. Update the heading in the markdown section so
it matches the actual four-step sequence described around the bridge checks,
keeping the wording consistent with the surrounding explanation.

In `@plans/super/154-dbt-expectations-prune.md`:
- Around line 16-22: Remove the accidental diff artifact in the prune-gate note
by updating the text around the `custom_sql` mention so it reads as a normal
sentence. The issue is the leading `+` before `custom_sql`, so edit the markdown
content in this plan document to eliminate that marker and keep the phrase
listing the built-ins and `custom_sql` grammatically consistent.

In `@src/signalforge/diff/engine.py`:
- Around line 471-472: The concatenation in the preserve-why path is letting a
long macro rationale overwrite the kept-uncertain base cause, so update the
logic in the relevant `engine.py` helper(s) around the `base_why`/`macro`
combination to always retain `decision.why`. Reserve part of `max_why_chars` for
the base cause before calling `_truncate_why`, or shorten the macro text first,
and apply the same fix anywhere the same pattern appears (including the later
duplicate block).

In `@src/signalforge/manifest/loader.py`:
- Around line 506-510: _UPDATE_REF_CALL_RE in loader.py so the ref() extractor
matches both single-quoted and double-quoted arguments, including the one-arg
and two-arg forms. Keep the existing purpose of resolving the v9
no-attached_node path in the generic test metadata parsing, but broaden the
regex used by the bounded ref() disambiguator so ref("my_model") and ref("pkg",
"name") are handled the same as the single-quoted variants.

In `@src/signalforge/manifest/models.py`:
- Around line 128-189: The package surface is missing TestMetadata even though
GenericTest.test_metadata exposes it publicly. Update signalforge.manifest’s
__init__ exports to import TestMetadata from models.py and include it in
__all__, alongside GenericTest and the other manifest model re-exports, so
callers can access it directly from signalforge.manifest.

In `@src/signalforge/prune/compiler.py`:
- Line 704: The determinism check in compiler.py is still using the helper
default dialect instead of the active one, so compiled SQL for non-BigQuery
dialects can be misclassified; update the `is_deterministic_sql` call inside the
pruning logic to pass `dialect.name` from the surrounding compiler flow so it
uses the current `Dialect` consistently with sqlglot.

---

Nitpick comments:
In `@src/signalforge/ingest/_compiled_sql.py`:
- Around line 214-357: The quote/backslash/doubled-quote handling is duplicated
in _strip_sql_comments and _blank_sql_literals, so they can drift and create
inconsistent SQL scanning behavior. Extract the shared quote-state machine into
a common helper used by both functions, and have validate_ingested_sql rely on
that shared logic for comment stripping and literal blanking so any future
escape-rule change is applied consistently.

In `@src/signalforge/prune/compiler.py`:
- Line 70: `prune.compiler` is importing compiled-SQL helpers from an
ingest-private module instead of a public seam. Move `is_deterministic_sql` and
`validate_ingested_sql` behind a public API by re-exporting them from
`signalforge.ingest.__all__` or relocating the shared SQL validation helpers to
a common/warehouse module, then update the import in `prune.compiler` to use
that public symbol path.

In `@tests/fixtures/regenerate.sh`:
- Around line 140-153: The temp-file swap in scrub_compiled_manifest should
force overwrite instead of relying on plain mv. Update the mv call that replaces
"$out" so it uses the force flag, matching the script’s non-interactive file
operation pattern and the repo shell guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4058c05b-a42e-4b01-8ad4-319f19df6104

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe853a and 0fc5c67.

📒 Files selected for processing (45)
  • .claude/rules/business-rule-tests.md
  • .claude/rules/ingest-layer.md
  • .claude/rules/llm-drafter.md
  • .claude/rules/manifest-readers.md
  • .claude/rules/prune-engine.md
  • docs/cli-ops.md
  • docs/diff-ops.md
  • docs/grade-ops.md
  • docs/ingest-ops.md
  • docs/prune-ops.md
  • plans/super/154-dbt-expectations-prune.md
  • src/signalforge/cli/prune_existing.py
  • src/signalforge/diff/engine.py
  • src/signalforge/draft/models.py
  • src/signalforge/ingest/__init__.py
  • src/signalforge/ingest/_compiled_sql.py
  • src/signalforge/ingest/reader.py
  • src/signalforge/manifest/__init__.py
  • src/signalforge/manifest/loader.py
  • src/signalforge/manifest/models.py
  • src/signalforge/prune/compiler.py
  • src/signalforge/prune/engine.py
  • src/signalforge/skills/signalforge/SKILL.md
  • tests/cli/test_prune_existing.py
  • tests/diff/test_engine.py
  • tests/draft/test_drift_detector.py
  • tests/draft/test_models.py
  • tests/fixtures/README.md
  • tests/fixtures/dbt_project_expectations/.gitignore
  • tests/fixtures/dbt_project_expectations/dbt_project.yml
  • tests/fixtures/dbt_project_expectations/models/orders.sql
  • tests/fixtures/dbt_project_expectations/models/schema.yml
  • tests/fixtures/dbt_project_expectations/package-lock.yml
  • tests/fixtures/dbt_project_expectations/packages.yml
  • tests/fixtures/dbt_project_expectations/profiles.yml
  • tests/fixtures/dbt_project_expectations/target/manifest.json
  • tests/fixtures/manifest/generic_test_nodes.json
  • tests/fixtures/regenerate.sh
  • tests/ingest/test_compiled_sql.py
  • tests/ingest/test_manifest_tests.py
  • tests/manifest/test_expectations_fixture_loads.py
  • tests/manifest/test_loader.py
  • tests/manifest/test_models.py
  • tests/prune/test_compiler.py
  • tests/prune/test_engine.py

Comment thread docs/ingest-ops.md Outdated
Comment thread plans/super/154-dbt-expectations-prune.md
Comment thread src/signalforge/diff/engine.py Outdated
Comment thread src/signalforge/manifest/loader.py Outdated
Comment thread src/signalforge/manifest/models.py
Comment thread src/signalforge/prune/compiler.py Outdated
- diff/engine.py (MAJOR): _macro_why is tier-aware — kept-uncertain preserves the
  load-bearing kept-without-evidence cause under truncation (cause_priority), dropped
  keeps macro-locator-first (drop_reason column carries the category). +test.
- prune/compiler.py: pass dialect.name to is_deterministic_sql so Snowflake/Databricks
  compiled SQL parses under the right dialect (a parse failure returned a false pass).
- manifest/loader.py: _REF_CALL_RE accepts double-quoted ref("name") (dbt/Jinja allow it).
- manifest/__init__.py: export TestMetadata (GenericTest.test_metadata exposes it).
- cli/prune_existing.py: reword the grade comment — grade_artifacts also scores the
  model-level doc artifacts; only manifest TESTS can surface as flagged.
- regenerate.sh: mv -f for the scrub temp-swap (non-interactive-safe).
- docs/ingest-ops.md: Three -> Four gates heading; plan doc: drop stray + marker.
@wjduenow

wjduenow commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

PR Review Summary

All 8 review threads addressed in 31c606a — 8 fixed, 0 false positives.

Fixed (8 items)

File Issue Fix
src/signalforge/diff/engine.py (Major) _macro_why head-truncation could drop the kept-uncertain cause Tier-aware: kept-uncertain now preserves the load-bearing kept-without-evidence cause (cause_priority, reserving a minimal head for the macro locator); dropped keeps macro-first (the drop_reason column separately carries the category). + unit test.
src/signalforge/prune/compiler.py is_deterministic_sql used the BigQuery default → false pass on Snowflake/Databricks Pass dialect.name (the Dialect names already match sqlglot).
src/signalforge/manifest/loader.py _REF_CALL_RE only matched single-quoted ref('…') Accept double-quoted ref("…") too (dbt/Jinja allow both).
src/signalforge/manifest/__init__.py TestMetadata not re-exported though GenericTest.test_metadata exposes it Added to imports + __all__.
src/signalforge/cli/prune_existing.py Comment claimed grade "scores ONLY" ingested tests, but grade_artifacts also scores model-level doc artifacts Reworded: only manifest tests are scored among tests / can surface as flagged; the model-level description/rationale docs are still scored (not a test, can't flag one).
tests/fixtures/regenerate.sh Plain mv can prompt under mv -i alias mv -f for the scrub temp-swap.
docs/ingest-ops.md Heading said "Three gates" but lists four "Four gates".
plans/super/154-dbt-expectations-prune.md Stray leading + read as a diff artifact Reworded to "… and custom_sql".

False Positives (0)

None — every comment was a real, actionable issue.

Validation after fixes: ruff / ruff format --check / pyright (0 errors) / pytest (4612 passed) all green.

@wjduenow
wjduenow merged commit 89e2914 into dev Jul 6, 2026
7 checks passed
@wjduenow
wjduenow deleted the feature/154-dbt-expectations-prune branch July 6, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants