Skip to content

#444 — SourceSpan vocabulary + per-CTE byte-range retention - #452

Merged
cmbays merged 3 commits into
mainfrom
domain-444-sourcespan-retain
Jun 22, 2026
Merged

cmbays merged 3 commits into
mainfrom
domain-444-sourcespan-retain

Conversation

@cmbays

@cmbays cmbays commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

S1 of the source-map spine epic (#442). Introduces THE position/span vocabulary and retains the per-CTE byte ranges the CTE engine currently computes and throws away.

  • NEW src/domain/span.rs — SourcePos {line, col, byte: u32} + SourceSpan {start, end} with the guarded half-open primitives (contains / contains_byte / contains_range / overlaps / byte_range). Pure POD + serde derive only (gated by tests/domain_clean_arch.rs).
  • Retired the dead cte::Span (was used only in its own tests); the domain CteNode now carries the retained SourceSpan fact via source_span(). The AST work stays in the adapter and writes POD facts back to the domain (the cute-dbt#40 pattern).
  • slice_or_fallback returns (String, Option<SourceSpan>); slice_terminal returns Some((String, SourceSpan)). Attached to each node in build_nodes.
  • Blocker-1 (terminal trim): the terminal node's start.byte is advanced PAST the same leading-trim prefix ([',', '\n', '\r', ' ', '\t']) the text strips, so compiled[span.byte_range()] byte-equals the trimmed terminal text — not the raw closing-paren-end offset. Non-terminal CteBody spans are the untrimmed name AS ( … ) range, unadjusted.
  • Guarded usize→u32 via a single byte_u32 (checked u32::try_from + debug_assert! fallback) — the ONE construction boundary every span byte field flows through.
  • Empty-span guard (feedback refinement 4): contains_range / overlaps never underflow on the end.byte==0 fallback span; no end.byte - 1 arithmetic anywhere.

Gates run green

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --locked -- -D warnings
  • cargo nextest run (2380 passed)
  • cargo test --test bdd (242 scenarios passed)
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --locked
  • cargo deny check
  • headless E2E: headless_zero_egress (12 passed, -- --ignored) + headless_toggle (123 passed, -- --ignored)
  • crap4rs strict scorecard: span.rs joins the strict ≤15 set (worst function 14.0); 0 functions above threshold
  • cargo-mutants targeted on the span arithmetic + slice functions: 0 missed in new code (the 9 pre-existing survivors are untouched cte_engine.rs detectors, outside this slice's scope)
  • domain hexagonal-purity gate (tests/domain_clean_arch.rs) green

Goldens

Byte-identical (no movement). The domain CteNode is not serialized into the report payload yet, so no golden moves — verified via the headless zero-egress suite over the committed report AND explore examples.

Closes #444

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced source location tracking for CTE nodes with precise byte-level and line/column position metadata.
  • Improvements

    • CTE engine now captures and retains accurate source span information for each CTE body and query terminal, enabling improved error reporting and source-aware validation downstream.
  • Configuration

    • Updated code quality scoring configuration to apply stricter quality gates on specific modules.

…tion

Introduce THE position/span vocabulary (src/domain/span.rs: SourcePos +
SourceSpan with the guarded half-open primitives contains / contains_byte
/ contains_range / overlaps + byte_range). Pure POD + serde derive only
(gated by tests/domain_clean_arch.rs). Retire the dead cte::Span; attach
the byte ranges the CTE engine computed and threw away onto the domain
CteNode as a retained SourceSpan fact (the cute-dbt#40 pattern — the AST
work stays in the adapter, writes POD facts back to the domain).

slice_or_fallback now returns (String, Option<SourceSpan>); slice_terminal
returns Some((String, SourceSpan)) with start.byte advanced PAST the same
leading-trim prefix the text strips ([',', '\n', '\r', ' ', '\t'],
Blocker-1) so compiled[span.byte_range()] byte-equals the trimmed terminal
text, not the raw closing-paren-end offset. A non-terminal CteBody span is
the untrimmed name AS ( … ) range and needs no adjustment.

usize→u32 narrowing is guarded by a single checked u32::try_from +
debug_assert! fallback (byte_u32), the ONE construction boundary every
span byte field flows through. contains_range / overlaps never underflow
on the end.byte==0 fallback span (empty-span guard, feedback refinement 4)
— no end.byte-1 arithmetic anywhere.

span.rs joins the strict crap4rs set (≤15; worst 14.0). Span arithmetic +
slice functions verified by cargo-mutants (0 missed in new code).
Exhaustive structured enumeration over the bounded byte cube (house style,
no proptest). Goldens: NONE move (CteNode is not serialized into the
report payload yet); report + explore goldens verified byte-identical via
the headless zero-egress suite.

Closes #444

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199AmBCec5kyVEEF1Qd7TSF
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds src/domain/span.rs defining SourcePos and SourceSpan as the canonical domain span vocabulary, removes the old cte::Span type, updates CteNode to carry source_span: Option<SourceSpan>, rewires the CTE engine's slicing functions to return and attach accurate byte-range spans per node, and gates span.rs under a strict CRAP threshold of 15.

Changes

SourceSpan vocabulary and CTE span retention

Layer / File(s) Summary
SourcePos/SourceSpan domain vocabulary
src/domain/span.rs
Introduces SourcePos (1-based line/col, 0-based byte) and SourceSpan (half-open [start, end) byte range) with byte_range, contains, contains_byte, contains_range, and overlaps methods, plus a comprehensive unit-test suite covering serde round-trips, exhaustive arithmetic, and empty-span edge cases.
Domain module wiring and CteNode migration
src/domain/mod.rs, src/domain/cte.rs, src/domain/project_def.rs
Registers span as a public submodule and re-exports SourcePos/SourceSpan at the domain root; removes the old Span re-export from the cte module; updates project_def.rs docs to distinguish the coarse Span type; rewrites CteNode to store source_span: Option<SourceSpan> with updated constructor, accessor, serde attributes, and tests.
CTE engine span computation and node attachment
src/adapters/cte_engine.rs
Imports SourcePos/SourceSpan; updates build_nodes to call revised slice_or_fallback and slice_terminal and attach returned spans to each CteNode; reworks both slicing functions to return (text, Option<SourceSpan>) tuples with guarded validation; adds loc_to_pos, clamp_u32, and ByteIndex::pos_at helpers; validates all span-fidelity invariants via new unit tests.
CRAP score gate for span.rs
crap4rs.toml
Adds a per-path override for src/domain/span.rs setting threshold = 15, placing the span arithmetic module under the strict CRAP gate.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #444 – Introduce SourceSpan vocabulary; retain per-CTE byte ranges on CteNode: This PR directly implements every acceptance criterion from that issue: SourcePos/SourceSpan in src/domain/span.rs, retirement of cte::Span, slice_or_fallback returning a tuple, Blocker-1 terminal-span trim alignment, guarded usize→u32 conversion, empty-span guards, strict CRAP gate at ≤15, and all listed TDD tests.
  • Add the SourceMap on the spine; derive compiled_sql + CodeMapPayload from it #445: The SourceSpan type and domain re-exports established here are the direct prerequisite for the SourceMapEntry and SourceMap infrastructure described in that issue.

Possibly related PRs

  • breezy-bays-labs/cute-dbt#46: Both PRs modify src/adapters/cte_engine.rs to slice each CTE's raw_sql from compiled_sql using sqlparser span and ByteIndex-based byte-position mapping, making the earlier PR the direct ancestor of the span-retention wiring added here.

Poem

🐇 A span was once vague, a ghost in the night,
Now SourcePos marks every byte just right.
Half-open ranges, no underflow fear,
Each CTE node wears its location clear.
The rabbit hops proud — the offsets are tight! 🎉

🚥 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 directly references issue #444 and summarizes the two main changes: introducing SourceSpan vocabulary and retaining per-CTE byte ranges on CteNode, which align with the pull request's core objectives.
Linked Issues check ✅ Passed The pull request implements all acceptance criteria from #444: introduces SourcePos/SourceSpan vocabulary [#444], retires cte::Span [#444], retains byte ranges on CteNode [#444], adjusts terminal span for trim offset [#444], guards usize→u32 conversion [#444], implements empty-span guard [#444], and achieves strict CRAP compliance [#444].
Out of Scope Changes check ✅ Passed All changes scope to the linked #444 objectives: new span.rs module, updated CteNode, slice function modifications, trim-offset handling, and documentation clarifications directly support the SourceSpan vocabulary introduction and byte-range retention.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch domain-444-sourcespan-retain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

📄 Rendered report preview

All golden examples regenerated cleanly.

🟡 Golden examples

Committed to examples/ and byte-identity gated — the canonical reports contributors and consumers browse. Stable across PRs.

Report View Download
prdiff-minidag-report.html ▶ Open ↗ ⬇ Download
diff-showcase-report.html ▶ Open ↗ ⬇ Download
jaffle-shop-report.html ▶ Open ↗ ⬇ Download
seed-showcase-report.html ▶ Open ↗ ⬇ Download
macro-heavy-report.html ▶ Open ↗ ⬇ Download
playground-report.html ▶ Open ↗ ⬇ Download

🐶 Live dogfood preview

This PR doesn't touch dbt-project/, so there's no live dogfood preview.

🧭 Explore preview

The two-page cute-dbt explore explorer — dag.html (model lineage) + tests.html (unit-test viewer). Same golden/live split as the report.

🟡 Golden explore

The committed examples/explore/ playground golden (the full synthetic playground manifest). Byte-identity gated in Example report check. Stable across PRs.

Page View Download
explore/dag.html ▶ Open ↗ ⬇ Download
explore/tests.html ▶ Open ↗ ⬇ Download

🐶 Live explore

This PR doesn't touch dbt-project/, so there's no live explore preview.

▶ Open ↗ opens the report or explorer in your browser in one
click — published to this repo's GitHub Pages under
/pr-452/.
⬇ Download fetches the same self-contained HTML as a workflow
artifact (auth-gated; works fully offline). Either way the report
makes zero external resource requests.

The Pages preview may take ~1 min to update after this comment
posts. On PRs from forks the Open link is unavailable (read-only
token) — use Download.

Alternative: GitHub CLI
# gh CLI >= 2.63 extracts into ./report-preview-playground/.
gh run download 27930442031 -R breezy-bays-labs/cute-dbt -n report-preview-playground
open report-preview-playground/playground-report.html

Posted by report-preview.yml for 0f50f98c271077088a54199bd70380e3dbbeb8fa. Affordance only — never blocks merge.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new SourceSpan and SourcePos vocabulary in src/domain/span.rs to replace the old Span struct, enabling byte-faithful tracking of CTE bodies and terminal nodes. It updates the CTE engine to retain these source spans and includes comprehensive tests. The review feedback correctly identifies a bug in the overlaps method where empty spans can incorrectly report an overlap with non-empty spans, violating the documented behavior.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/domain/span.rs
cmbays and others added 2 commits June 22, 2026 00:39
…mutant, unify line/col narrowing, merge main

FIX 1 (never-a-false-claim): SourceSpan::overlaps now matches its doc —
an empty `[k,k)` span (zero bytes) overlaps nothing, even when it sits
arithmetically inside another span (e.g. (5,5) inside (0,10)). The
exhaustive [0,N]^4 oracle was vacuous for this case (its reference def
omitted the empty-span exclusion); corrected to `s_nonempty &&
o_nonempty && s0 < o1 && o0 < s1` plus explicit empty-span assertions in
both argument positions (empty-inside / at-boundary / outside).

FIX 2 (kill surviving mutant): pin a pos_at-derived col to its exact
1-based value in terminal_span_start_is_post_trim (end.col == 18, line
5) so the `+1 → *1` off-by-one on ByteIndex::pos_at's col is caught.
`cargo mutants -f src/adapters/cte_engine.rs --re pos_at` → 0 MISSED in
the pos_at function.

FIX 3: cross-check the loc_to_pos start.col against the byte-derived
char column in span_line_col_agree_with_bytes (so both col sources are
pinned under the §3.1 invariant), and route all three SourcePos axes
(byte/line/col) through one debug-asserted `clamp_u32` helper (renamed
from byte_u32) — no bare saturating unwrap_or(u32::MAX) left on line/col.

FIX 4: refresh the stale mod.rs comment that justified not re-exporting
project_def::Span via the now-retired cte::Span — the canonical
SourceSpan now owns the position vocabulary at the domain root.

Merged origin/main (S0 #443 lineage hoist) cleanly — mod.rs and
crap4rs.toml auto-merged additively (both S0 lineage + S1 span entries
preserved). Goldens unmoved (domain-only + adapter-internal; CteNode not
serialized into the report payload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199AmBCec5kyVEEF1Qd7TSF

@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: 1

🤖 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 `@crap4rs.toml`:
- Around line 41-42: The [[overrides]] header on line 41 is duplicated, creating
an unintended empty override entry that can interfere with proper override
parsing. Remove the duplicate [[overrides]] header, keeping only one instance of
this section header in the configuration file.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b30f0b8d-2b86-4682-91f2-3fca0d5b2696

📥 Commits

Reviewing files that changed from the base of the PR and between 8d2281a and 0f50f98.

📒 Files selected for processing (6)
  • crap4rs.toml
  • src/adapters/cte_engine.rs
  • src/domain/cte.rs
  • src/domain/mod.rs
  • src/domain/project_def.rs
  • src/domain/span.rs

Comment thread crap4rs.toml
@cmbays
cmbays merged commit ce105bd into main Jun 22, 2026
44 checks passed
@cmbays

cmbays commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

✅ Merged S1 (#444) — the SourceSpan vocabulary + per-CTE byte-range retention.

What landed: one position vocabulary (SourcePos/SourceSpan in src/domain/span.rs), the dead cte::Span retired, and the per-CTE byte ranges the engine used to discard now retained on the domain CteNode (the #40 POD write-back). This is the primitive S2's SourceMap rides on.

Quality: behavior-preserving (zero golden movement; 2391 nextest + 242 BDD; domain-pure). Council review (cao/ceng/cpo clean, cqo concerns) → merge_after_must_fix. The multi-layer review (workflow-verify + gemini + council + orchestrator probe) each caught something the others missed: gemini + my probe caught the overlaps empty-span doc-vs-code lie (the doc said empty spans never overlap, but the code returned true; fixed — plus the exhaustive test oracle, which had mirrored the bug, corrected); cqo + the synthesizer caught a surviving pos_at col mutant (the line/col-agree invariant was uncovered on that path; now pinned). line/col narrowing unified through a checked clamp.

Sequencing note: S1 ships ahead of S2 despite design D3's "collapse S1+S2" — this does not reintroduce the dead-code-slice smell D3 feared, because source_span is exposed via the public source_span() getter (a 1:1 replacement for the prior span()) and exercised by adapter tests, so no dead_code lint fires (clippy --all-targets --locked green) and the fact lands in its permanent home (CteNode).

Merged autonomously under the overnight build mandate. Next: S2 (#445) — the SourceMap on the spine — builds on this.

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.

Introduce SourceSpan vocabulary; retain per-CTE byte ranges on CteNode

1 participant