Skip to content

review: --no-ai fix + --explain-tier / --force-tier flags + manifest auto-discovery + column-aware schema.yml test-removal - #1027

Merged
sahrizvi merged 10 commits into
mainfrom
feat/review-r18-observability-recall
Jul 23, 2026
Merged

review: --no-ai fix + --explain-tier / --force-tier flags + manifest auto-discovery + column-aware schema.yml test-removal#1027
sahrizvi merged 10 commits into
mainfrom
feat/review-r18-observability-recall

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Improvements to altimate-code review covering two bug fixes and four observability / recall features that share the same code surface. Filed against packages/opencode/src/{cli/cmd/review.ts, altimate/review/*.ts}.

Bug fixes

Both are reproducible against main and closed by this PR:

  • review: bare --no-ai flag triggers help path instead of running the review #1025 — bare --no-ai <other-flag> triggers the yargs help path and exits 0 without running the review. Fix: .parserConfiguration({ "boolean-negation": false }) on the review command's yargs builder, so --no-ai binds to the declared noAi option as authored. --no-ai=true / --no-ai=false continue to work for programmatic parity.
  • review: schema.yml test-removal detector misses removals when a sibling column still has the same test #1026detectSchemaYmlPatterns compared removed and added test lines as raw trimmed strings, so a genuine removal from one column was silently cancelled by an unrelated re-add on a sibling column (yaml re-serialization emits the same - unique / - not_null lines for other columns still declaring those tests). Fix: collectTestOccurrencesFromDiff in dbt-patterns.ts walks the unified diff with model/column context via nearest preceding - name: X header (resets on hunk boundaries), keys removals by (model, column, test), and only cancels on a same-tuple re-add. Emits one finding per removed (model, column, test) triple, with severity elevated to warning for unique removals and for not_null on id/key columns. Body text detects layer from file path (marts?/reporting → "mart-layer key", else "declared key") so the copy doesn't mislabel non-mart schema files.

Feature enhancements bundled with the fixes

Four additions in the same code surface, small enough to review alongside the bug fixes rather than as their own PRs:

  • --explain-tier — new boolean flag. When set, the verdict envelope carries tierReasons: string[] (the risk classifier's reasons for the tier decision) and the human-readable summary prints a 🧭 **Tier: X** — <reasons> line. Read-only — doesn't change tier classification or verdicts. Envelope field is optional (absent when the flag is off), so existing verdicts remain byte-equivalent.

  • --force-tier <trivial|lite|full> — new experimental flag for bench and debug work. Overrides the tier classifier with the supplied tier. Guardrails:

    • Prints an ⚠️ --force-tier=X is EXPERIMENTAL (bench / debug only). Classifier bypassed; verdict envelope will carry tierForced: true. warning to stderr on every use.
    • Verdict envelope carries tierForced: true and tierClassified: <original> whenever the flag is passed, regardless of whether the forced value matches the classifier's decision. (An earlier iteration of this change guarded that flag on input.forceTier !== classifiedTier, which meant --force-tier=full on a naturally-full PR silently bypassed the audit envelope. Corrected in the same branch — every use of the flag is now recorded.)
    • Verdict headline renders as <tier> tier — forced (was <classified>).
    • tierReasons is always populated when the flag is used, with a leading forced via --force-tier=X (classifier said Y) marker so downstream can't confuse the forced tier for a natural one.
    • Not a default-visible feature. Documented as EXPERIMENTAL in the yargs describe; the stderr banner ensures no one uses it in CI without noticing.
  • Manifest auto-discovery — before, --manifest <path> had to be explicit; otherwise the CLI used the config-relative default target/manifest.json, which silently missed when cwd wasn't the dbt project root, degrading every such review to lint-only. Now:

    1. If --manifest isn't explicit AND the config-relative path doesn't exist, walk UP from cwd looking for dbt_project.yml. Use the adjacent target/manifest.json when it exists.
    2. Log discovery to stderr (ℹ️ auto-discovered dbt manifest at ...) so users see which manifest the review used.
    3. Never auto-discover a manifest from a directory without a dbt project — a target/ from an unrelated tool never gets picked up.
    4. Freshness warning: when --head is set (CI shape) AND any changed file has an mtime newer than the manifest, print ⚠️ manifest ... appears stale to stderr. Skipped for working-tree diffs (mtime noise during active edits would spam warnings).

    Explicit --manifest <path> always wins. Auto-discovery is only attempted when the caller was silent AND the config default is absent.

  • Verdict envelope — three new optional fields added to VerdictEnvelope in verdict.ts, all included in the signed canonical body so tampering is detectable:

    • tierReasons?: string[] (from --explain-tier)
    • tierForced?: boolean (from --force-tier)
    • tierClassified?: RiskTier (from --force-tier — original tier before the override)

    All are optional and absent when the corresponding flag is off, so existing verdicts remain byte-equivalent to before.

Testing

Verified locally on synthetic scenarios:

  • Bare --no-ai now runs the review (verifies bug fix for review: bare --no-ai flag triggers help path instead of running the review #1025).
  • --no-ai=true and --no-ai=false continue to work.
  • --explain-tier populates tierReasons on both trivial and full verdicts.
  • --force-tier=full on a naturally-full PR sets tierForced: true and tierClassified: "full" on the envelope + emits the stderr warning + the leading forced via reason. --force-tier=lite on a naturally-full PR overrides tier to lite and records tierClassified: "full".
  • Manifest auto-discovery: finds target/manifest.json when invoked from a subdirectory containing dbt_project.yml upward; declines to use a target/manifest.json from a directory that has no dbt_project.yml (verified against a plain non-dbt git repo).
  • schema.yml test-removal: reproducer for review: schema.yml test-removal detector misses removals when a sibling column still has the same test #1026 (remove unique+not_null from one column while sibling columns still have them) now surfaces two warning-severity findings, one per removed (column, test) tuple. Layer-aware body renders "declared key" for a top-level models/schema.yml and "mart-layer key" for a models/marts/schema.yml.
  • Envelope signature still round-trips through verifyEnvelope for both flag-on and flag-off shapes.

Test plan

Closes #1025.
Closes #1026.


Summary by cubic

Fixes bare --no-ai runs, hardens file reads against symlink escapes with a shared safeReadInside helper, upgrades schema.yml test‑removal to a structural YAML diff (incl. deleted/renamed files), auto‑discovers the dbt manifest, and makes subdir/monorepo invocations resolve compiled SQL and working‑tree files correctly. Closes #1025 and #1026.

  • Bug Fixes

    • CLI: bare --no-ai now runs instead of falling into help.
    • Security: working‑tree and compiled‑SQL reads reject targets that realpath outside the repo/project (blocks tracked‑symlink escapes) and now share the centralized safeReadInside helper.
    • schema.yml: structural diff across models/sources/snapshots/seeds with tests/data_tests; handles deleted and renamed files; model‑level and column‑level attribution; per‑removal findings; elevated severity for unique and not_null on likely key columns; multiple relationships on one column are distinct; snapshot property files under snapshots/*.yml now route here; safe diff‑only fallback with per‑occurrence tags; emits the per‑file summary once with clearer “This schema file drops …” wording.
    • Tier display/audit: headline shows “forced (was X)” with an “unknown” fallback when tierClassified is missing; envelope schema enforces tierForcedtierClassified consistency and rejects tierForced: false; backtick‑safe, capped tier reason rendering.
    • Stale‑manifest warning filters to dbt‑relevant paths and only treats .md under models/ or analyses/ as manifest‑affecting to avoid false positives.
    • Subdir/monorepo invocation: working‑tree reads root at the git repo top‑level; compiled SQL resolver uses the dbt project realpath and strips the repo→project path prefix (Windows‑ and symlink‑safe), so files like packages/dbt/models/... resolve under the discovered dbt project.
  • New Features

    • --explain-tier: attaches tierReasons[] to the envelope and prints a capped tier line in the summary.
    • --force-tier <trivial|lite|full> (experimental): overrides the classifier with a stderr warning; envelope records tierForced: true and tierClassified; reasons start with a “forced via …” marker; headline shows “forced (was X)”.
    • Manifest auto‑discovery: when --manifest is omitted and the config default is missing, walks up to the nearest dbt_project.yml and uses its target/manifest.json; logs the chosen path; never picks a manifest outside a dbt project; warns if stale when --head is set; routes compiled SQL resolution and project name to the discovered project root. Explicit --manifest always wins.
    • Verdict envelope: adds optional tierReasons, tierForced, and tierClassified (all signed). Backwards compatible when flags aren’t used.

Written for commit 69553ed. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added --explain-tier to include tier reasoning in the verdict and enrich the summary when reasons are available.
    • Added experimental --force-tier to override tier selection and record forced-tier auditing metadata.
    • Auto-discovers the nearest dbt manifest when not provided, and warns if it may be stale.
  • Bug Fixes

    • Improved schema.yml guardrail removal detection with YAML-aware attribution (including data_tests), plus a safe diff-only fallback.
    • Fixed CLI boolean parsing for --no-ai.
    • Corrected snapshot classification under snapshots/ (now handles non-.sql snapshot YAML files).
  • Tests

    • Expanded coverage for tier rendering/forcing, schema-yml attribution, manifest staleness, and subdirectory invocation behavior.

Haider added 2 commits July 20, 2026 21:40
Round-17 bench established three baselines:
- A1-default: 7/64 recall (11%), 4% precision
- A1-workaround (with --manifest): 3/8 on applicable jaffle scenarios
- Baseline B: 0/64 (plugin never invoked the CLI — separately fixed)

This ships the six review-side changes documented as improvement paths
in the Round-18 plan. Each is guarded to avoid regressing the current
customer numbers.

## B1 — bare `--no-ai` triggers yargs help path

yargs' automatic `--no-<option>` negation collides with the option
literally named `no-ai`: bare `--no-ai` is interpreted as "set
undeclared option `ai` to false" and the command silently falls to the
help path (exit 0, no review runs). Every `--no-ai` invocation across
the bench was affected — our earlier A1 numbers were captured with
`--no-ai --yolo` but the flag was inert and the AI lane always ran.

Fix: `.parserConfiguration({ "boolean-negation": false })` on the
review command's yargs builder. `--no-ai` now binds to the declared
`noAi` flag as authored. `--no-ai=true` / `--no-ai=false` still work
for programmatic parity.

## G1 — `--explain-tier` flag

Adds a boolean flag that surfaces the tier classifier's reason list
on the verdict envelope (`tierReasons: string[]`) and in the human-
readable render (`> 🧭 **Tier: X** — ...`). Read-only — doesn't
change tier classification or verdicts. When the flag is off, the
envelope omits `tierReasons` entirely (backwards compatible; existing
consumers unaffected).

## G2 — `--force-tier <trivial|lite|full>` flag

EXPERIMENTAL / bench-debug only. Overrides the classifier's tier so
we can measure the tier-gate contribution to recall misses (e.g. js2
gets `trivial/0` — can't tell if the catalog missed vs. the tier gate
filtered it out). Guardrails:

- Prints an "EXPERIMENTAL (bench / debug only)" warning to stderr
  every use.
- Envelope carries `tierForced: true` and `tierClassified: <original>`
  so audits can see the bypass.
- `tierReasons` is force-populated with a leading marker even if
  `--explain-tier` isn't set, so the renderer always shows the tier
  was forced.
- Verdict header renders as `full tier — forced (was trivial)`.

Not a default-visible feature. Documented as debug-only in the yargs
`describe`, and the stderr banner ensures no customer accidentally
uses it in CI without noticing.

## G3 — manifest auto-discovery + freshness warning

Before: `--manifest <path>` had to be passed explicitly. Otherwise
the CLI used the config-relative default `target/manifest.json`,
which silently missed whenever `cwd` wasn't exactly the dbt project
root — every such review degraded to lint-only.

After (in `run.ts`):

1. If `--manifest` isn't explicit AND the config-relative path
   doesn't exist, walk UP from `cwd` looking for `dbt_project.yml`.
   Use the adjacent `target/manifest.json` when it exists.
2. Log the discovery to stderr (`ℹ️  auto-discovered dbt manifest at
   ...`) so customers see which manifest the review used.
3. Refuse to auto-discover a manifest from a directory that doesn't
   contain a dbt project — a `target/` from an unrelated tool (e.g.
   Airflow) never gets picked up.
4. Freshness warning: when `--head` is set (CI / bench shape) AND
   any changed file has an mtime newer than the manifest, print a
   `⚠️  manifest ... appears stale` message to stderr. Skipped for
   working-tree diffs because mtime noise during active edits would
   spam warnings.

Explicit `--manifest <path>` always wins. Auto-discovery is only
attempted when the caller was silent AND the config default is absent.

## G6 — column-aware schema.yml test-removal detector

Before: the detector compared removed vs added test lines as bare
strings (`- unique`, `- not_null`). Any sibling column that STILL had
the same test type re-added its line under a different indent (yaml
re-serialization), silently cancelling the genuine removal from a
different column. js2 (drops `unique` + `not_null` from
`customers.customer_id` while `orders.order_id` still has them) hit
this exact bug: 0/2 recall in both A1 modes across Round 17.

After: `collectTestOccurrencesFromDiff` walks the unified diff
tracking `(model, column)` context via nearest preceding
`- name: <X>` headers and hunk boundaries. Removed tests are keyed by
`(model, column, test)` and only cancelled by a re-add on the SAME
tuple. One finding per removal (not one summary), with severity
elevated to `warning` for `unique` removals and for `not_null` on
`_id` / `_key` / `id` columns (silent-PK-null risk).

Verified on js2 locally: 0 findings → 2 warnings.

## Verdict envelope schema

`VerdictEnvelope` gains three optional fields, all included in the
signed canonical body so tampering is detectable:

- `tierReasons?: string[]` — from G1 (surfaces classifier reasons)
- `tierForced?: boolean` — from G2
- `tierClassified?: RiskTier` — from G2 (original tier before force)

All are optional and absent in the default (no-flag) case, so
existing verdicts remain byte-equivalent to before.

## What's NOT in this change

- G4 (`verify` subcommand) — deferred; doesn't move measured metrics.
- G5 (`--commit` / `--pr` / `--files`) — deferred; adoption UX only.
- G7 (severity mapping tune-up) — deferred; needs its own design pass.
- Real-MR taxonomy expansion — larger design lift; addressed in a
  later round.

## Follow-up

Rerun A1-default and A1-workaround on the 13-scenario corpus with
these changes and publish the deltas as Round 18 in the reviewer
plugin journal.
Independent code review by Codex (2026-07-21) surfaced two issues in
the R18 fixes:

1. G2 guardrail bug — `tierForced` was only set to `true` when the
   forced tier value happened to DIFFER from the classifier's
   decision. So `altimate-code review --force-tier=full` on a PR the
   classifier would naturally rate `full` still runs the debug bypass
   (silently exercises the flag path in orchestrate.ts) but emits an
   envelope with no `tierForced`, no `tierClassified`, and no
   "forced via" reason — breaking the audit-trail invariant we
   documented in the R18 commit.

   Fix: `tierForced = input.forceTier !== undefined`. Whenever the
   caller passed the flag, the envelope now records the bypass
   regardless of whether the forced value matched the natural one.
   The leading reason string now also names the forced value.

   Verified on js4 (naturally `full`): `--force-tier=full` now
   produces `tierForced: true, tierClassified: "full", tierReasons:
   ["forced via --force-tier=full (classifier said full)", ...]`.

2. G6 cosmetic copy issue — the schema.yml test-removal detector's
   body claimed "Removing a `unique` test on a mart-layer key is how
   silent duplicate rows ship" for EVERY unique-test removal, even
   when the schema.yml lives outside a mart layer (e.g. js2's
   `models/schema.yml` sits at the top level, not under `models/marts/`).

   Fix: detect the layer from the file path (`marts?` or `reporting`
   → "mart-layer"; otherwise "declared") and interpolate that into
   the body. The finding is still accurate for genuine mart-layer
   removals; the copy no longer misattributes for staging or
   top-level schema files.

Neither change alters what findings are surfaced, only the audit
envelope shape (G2) and finding body prose (G6). No test-file
changes; smoke tests on js2 + js4 confirm both.

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

Copy link
Copy Markdown

Hey! Your PR title review: --no-ai fix + --explain-tier / --force-tier flags + manifest auto-discovery + column-aware schema.yml test-removal doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds structural dbt schema test-removal detection, manifest discovery and freshness checks, tier explanation and override controls, signed tier provenance, resolver path handling, and tier-aware output rendering. It also corrects snapshot YAML classification and expands regression coverage.

Changes

dbt schema test detection

Layer / File(s) Summary
Column-aware schema test detection
packages/opencode/src/altimate/review/dbt-patterns.ts, packages/opencode/src/altimate/review/diff-filter.ts, packages/opencode/test/altimate/review-dbt-patterns.test.ts, packages/opencode/test/altimate/review.test.ts
Structural YAML comparison and diff fallback detect removed guardrail tests by entity, column, and test, including sources, snapshots, block-form tests, data_tests, deletion cases, and dedupe-safe findings.

Tier and review controls

Layer / File(s) Summary
Review inputs, manifest resolution, and repository paths
packages/opencode/src/altimate/review/run.ts, packages/opencode/src/altimate/review/git.ts, packages/opencode/src/altimate/review/compiled.ts, packages/opencode/src/altimate/review/orchestrate.ts, packages/opencode/src/cli/cmd/review.ts, packages/opencode/test/altimate/review-run-stale.test.ts, packages/opencode/test/altimate/review-subdir-invocation.test.ts, packages/opencode/test/altimate/review-ci.test.ts, packages/opencode/test/cli/tui/command.test.ts
Review options support tier controls, manifests can be auto-discovered with freshness warnings, repository-root reads work from subdirectories, compiled paths support dbt-root prefixes, snapshot YAML files route as schema YAML, and CLI parsing handles bare --no-ai.
Tier classification and envelope provenance
packages/opencode/src/altimate/review/orchestrate.ts, packages/opencode/src/altimate/review/verdict.ts, packages/opencode/test/altimate/review.test.ts
Tier classification preserves original results, supports forced tiers, and records validated provenance and reasons in signed verdict envelopes.
Tier-aware verdict rendering
packages/opencode/src/altimate/review/format.ts, packages/opencode/test/altimate/review.test.ts
Rendered headlines identify forced tiers and summaries include safely fenced tier reasons when available.

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

Sequence Diagram(s)

sequenceDiagram
  participant ReviewCLI
  participant reviewPullRequest
  participant runReview
  participant runReviewTiering
  participant buildEnvelope
  participant renderSummary
  ReviewCLI->>reviewPullRequest: pass explain-tier and force-tier
  reviewPullRequest->>runReview: forward review options
  runReview->>runReviewTiering: classify review tier
  runReviewTiering->>runReviewTiering: apply optional forced tier
  runReview->>buildEnvelope: attach tier reasons and provenance
  buildEnvelope->>renderSummary: provide verdict envelope
  renderSummary-->>ReviewCLI: render tier and explanation
Loading

Possibly related issues

Possibly related PRs

Poem

A rabbit found tests that had hopped away,
And tagged each column by name in the hay.
Tiers gained breadcrumbs bright,
Manifests found their site,
While no-AI now parses right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
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.
Title check ✅ Passed Clearly related to the changeset and names the main fixes/features, though it is a bit long.
Description check ✅ Passed It covers the issue refs, what changed, and how it was verified; only the exact template headings are missing.
✨ 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 feat/review-r18-observability-recall

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.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

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

🧹 Nitpick comments (1)
packages/opencode/src/altimate/review/run.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Prefer FileSystem.FileSystem for effectful file I/O.

The new node:fs/promises imports bypass the project's Effect-driven I/O abstraction. As per coding guidelines, "Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O".

Consider migrating these new operations (stat, access) to the Effect FileSystem module where feasible, though since warnIfStale and autoDiscoverManifest are currently standard async functions, this might require broader refactoring.

🤖 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 `@packages/opencode/src/altimate/review/run.ts` at line 2, Replace the raw
node:fs/promises stat and access usage in warnIfStale and autoDiscoverManifest
with the project’s FileSystem.FileSystem abstraction, refactoring those async
flows as needed to use Effect-based file I/O. Preserve the existing stale-file
checks and manifest discovery behavior while removing the direct filesystem
imports.

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 `@packages/opencode/src/altimate/review/dbt-patterns.ts`:
- Around line 851-878: Update collectChangedFiles() and
collectTestOccurrencesFromDiff() so schema.yml diffs retain enough context to
identify the enclosing model when a changed column header is the first name
entry in the hunk. Prefer increasing unified diff context for schema files;
otherwise add fallback scope inference that distinguishes the column from the
missing model while preserving detection of removed unique, not_null, and
relationships tests.

In `@packages/opencode/src/altimate/review/run.ts`:
- Around line 122-124: Filter changedPaths before or within the loop so only
relevant dbt model files—schema.yml files or SQL files—reach the stale-manifest
comparison using abs. Exclude unrelated files such as README.md and package.json
while preserving the existing tracked, on-disk checks for eligible paths.

---

Nitpick comments:
In `@packages/opencode/src/altimate/review/run.ts`:
- Line 2: Replace the raw node:fs/promises stat and access usage in warnIfStale
and autoDiscoverManifest with the project’s FileSystem.FileSystem abstraction,
refactoring those async flows as needed to use Effect-based file I/O. Preserve
the existing stale-file checks and manifest discovery behavior while removing
the direct filesystem imports.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1242bef6-a381-4d45-a224-359ee38b5c18

📥 Commits

Reviewing files that changed from the base of the PR and between 65c56b0 and 98a0439.

📒 Files selected for processing (6)
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/src/altimate/review/verdict.ts
  • packages/opencode/src/cli/cmd/review.ts

Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated
Comment thread packages/opencode/src/altimate/review/run.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

The previous suggestion (compiled.ts:90 — duplicated realpath+containment block) is resolved by this commit: the shared safeReadInside is now exported from git.ts and reused by compiled.ts. Verified behavior is byte-for-byte equivalent (same realpath + separator-aware startsWith check + try/catch→undefined), no circular imports introduced, and the helper is still consumed internally by makeContentResolver in git.ts.

Files Reviewed (2 files)
  • packages/opencode/src/altimate/review/compiled.ts
  • packages/opencode/src/altimate/review/git.ts
Previous Review Summaries (5 snapshots, latest commit f218c3f)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f218c3f)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/review/compiled.ts 90 Realpath + containment block duplicates safeReadInside (git.ts:70); export and reuse the shared helper so the security-sensitive check stays in sync.
Files Reviewed (5 files)
  • packages/opencode/src/altimate/review/compiled.ts - 1 issue
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/git.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/test/altimate/review-subdir-invocation.test.ts

Fix these issues in Kilo Cloud

Previous review (commit c034373)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/review/run.ts 258 path.relative(gitRoot, dbtRoot) computes a wrong prefix when gitRoot (symlink-resolved via git rev-parse --show-toplevel) and dbtRoot (opts.cwd / path.resolve(cwd), not resolved) differ — an explicit --cwd under an unresolved symlink silently misses compiled SQL (graceful Jinja fallback, recall loss on the monorepo-subdir case this PR enables).
Files Reviewed (9 files)
  • packages/opencode/src/altimate/review/compiled.ts
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/git.ts
  • packages/opencode/src/altimate/review/run.ts - 1 issue
  • packages/opencode/src/altimate/review/verdict.ts
  • packages/opencode/test/altimate/review-ci.test.ts
  • packages/opencode/test/altimate/review-dbt-patterns.test.ts
  • packages/opencode/test/altimate/review-subdir-invocation.test.ts
  • packages/opencode/test/altimate/review.test.ts

Fix these issues in Kilo Cloud

Previous review (commit 310927e)

Status: No Issues Found | Recommendation: Merge

Incremental review (commit 310927ec9)

Reviewed the round-4 follow-up diff (39718d5d..310927ec9): 3 source files + 3 test files (~214 lines). All changes are responsive fixes to prior review feedback (cubic-review P2 + P3) and are correct, well-tested, and well-commented.

Round-4 changes verified
  • dbt-patterns.ts — deleted schema.yml handling. Early-return on status === "deleted" removed; isDeletedFile flag drives canUseStructural/canCommit to require only oldContent/oldDoc. New side is treated as {} via extractTestOccurrences(isDeletedFile ? {} : newDoc), so every prior test surfaces as a removal. Safe-degrades to [] when oldContent is missing; falls through to diff-only fallback when old YAML is unparseable. New model/column fields on the top-level Finding are schema-valid (finding.ts:80-82) and already threaded into the fingerprint.
  • format.ts — backtick-fence sizing. maxRun + 1 fence length with symmetric space-padding for leading/trailing backticks is mathematically correct for all CommonMark code-span edge cases (verified against the 6-case test).
  • orchestrate.ts — fetch-skip for deleted files. schemaFiles no longer filters out deleted; newContent fetch is skipped via file.status !== "deleted" (avoids a git-show that would fail at HEAD). Parallel Promise.all shape preserved.
  • Tests. 4 new deleted-file tests cover the structural-commit, safe-degrade, unparsable-fallback, and empty-old-doc branches. Backtick-escape test exercises 1/2/3-backtick fences + leading/trailing padding. command.test.ts switches toContaintoMatch to dodge a Bun-transpiler CI flake.
Files Reviewed (6 files)
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/test/altimate/review-dbt-patterns.test.ts
  • packages/opencode/test/altimate/review.test.ts
  • packages/opencode/test/cli/tui/command.test.ts

Notes:

  • All previously-raised inline comments on changed lines have been resolved by 310927ec9 (see @sahrizvi follow-ups: cubic P2 deleted-file path, cubic P3 top-level attribution, cubic P3 backtick fence).
  • Minor observation (not blocking): adding model/column to the top-level Finding shifts the fingerprint for structural findings vs. pre-round-4, since makeFinding includes them in the canonical hash. This is acceptable because (a) the feature is brand-new and unshipped, (b) the schema already supports the fields, and (c) the change was explicitly requested by cubic-review P3 for downstream consumers.
  • No Effect-await bypass, race conditions on shared state, SQL/HTML interpolation of external input, or path-escape risks introduced.

Previous review (commit 39718d5)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/src/altimate/review/verdict.ts
  • packages/opencode/src/cli/cmd/review.ts

Notes:

  • B1 (--no-ai yargs fix): .parserConfiguration({ "boolean-negation": false }) correctly binds bare --no-ai to the declared noAi flag.
  • G6 (column-aware schema.yml test-removal): collectTestOccurrencesFromDiff correctly tracks (model, column, test) tuples via indentation-aware - name: header disambiguation and hunk-boundary resets. Verified standard 2-space YAML nesting (model@2, columns@4, column@6, tests@8, test@10) walks correctly; pops fire on dedent past model/column indents.
  • G2 (force-tier audit): The follow-up commit correctly gates tierForced = input.forceTier !== undefined (not !== classifiedTier), so a forced value matching the classifier still records the bypass. tierForced/tierClassified are coupled in orchestrate.ts, so format.ts's verdictHeadline won't render "was undefined".
  • G3 (manifest auto-discovery): Early-return-undefined when a dbt_project.yml is found without an adjacent target/manifest.json is intentional and correct (refuses unrelated-tool target/ dirs). Explicit --manifest and existing config-relative paths win; auto-discovery only runs when caller was silent and default is absent.
  • Envelope byte-equivalence: stableStringify filters undefined values, and all three new fields (tierReasons/tierForced/tierClassified) are passed as undefined when their flag is off — so signatures for flag-less verdicts remain unchanged.
  • Cross-cutting: No Effect-await bypass, no race conditions on shared state, no SQL/HTML interpolation of external input, no path-escape risks beyond mtime reads of customer-controlled paths.

Previous review (commit 98a0439)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/src/altimate/review/verdict.ts
  • packages/opencode/src/cli/cmd/review.ts

Notes:

  • B1 (--no-ai yargs fix): .parserConfiguration({ "boolean-negation": false }) correctly binds bare --no-ai to the declared noAi flag.
  • G6 (column-aware schema.yml test-removal): collectTestOccurrencesFromDiff correctly tracks (model, column, test) tuples via indentation-aware - name: header disambiguation and hunk-boundary resets. Verified standard 2-space YAML nesting (model@2, columns@4, column@6, tests@8, test@10) walks correctly; pops fire on dedent past model/column indents.
  • G2 (force-tier audit): The follow-up commit correctly gates tierForced = input.forceTier !== undefined (not !== classifiedTier), so a forced value matching the classifier still records the bypass. tierForced/tierClassified are coupled in orchestrate.ts, so format.ts's verdictHeadline won't render "was undefined".
  • G3 (manifest auto-discovery): Early-return-undefined when a dbt_project.yml is found without an adjacent target/manifest.json is intentional and correct (refuses unrelated-tool target/ dirs). Explicit --manifest and existing config-relative paths win; auto-discovery only runs when caller was silent and default is absent.
  • Envelope byte-equivalence: stableStringify filters undefined values, and all three new fields (tierReasons/tierForced/tierClassified) are passed as undefined when their flag is off — so signatures for flag-less verdicts remain unchanged.
  • Cross-cutting: No Effect-await bypass, no race conditions on shared state, no SQL/HTML interpolation of external input, no path-escape risks beyond mtime reads of customer-controlled paths.

Reviewed by glm-5.2 · Input: 42.4K · Output: 5.3K · Cached: 491.1K

Review guidance: REVIEW.md from base branch main

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/altimate/review/run.ts
Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated
Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated
Comment thread packages/opencode/src/altimate/review/format.ts Outdated
Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated
… minors)

Independent multi-reviewer code review on the PR surfaced one blocker,
two majors, and three minors. This commit addresses each finding and
adds regression-guard tests.

## BLOCKER — column-aware detector regresses on real diffs + broke its
own test (finding #1)

The R18 `collectTestOccurrencesFromDiff` walker required both
`- name: model` AND `- name: column` inside the same diff hunk to
attribute a removed test. With git's default `-U3` context the model
header sits many lines above a column's `tests:` block and isn't in the
hunk — so the walker misclassified the first `- name:` it saw as a
model, never set `currentColumn`, and silently dropped the removal.

The existing unit test at test/altimate/review-dbt-patterns.test.ts:141
(`"-          - unique\n-          - not_null"`) went from pass to fail
under R18, so the branch was shipping a red test suite as well.

Rewrite `detectSchemaYmlPatterns` to prefer STRUCTURAL YAML diffing:

- Accept optional old/new file content via a `SchemaYmlDetectContent`
  parameter. When provided, parse both sides with the `yaml` package,
  walk `models[]`, `snapshots[]`, `sources[]`, `seeds[]`, extract every
  `(entity, column?, test)` tuple, diff old vs new sets, emit one
  finding per genuine removal.
- Supports both column-level and model-level tests (dbt allows
  `tests:` / `data_tests:` directly under the entity for
  surrogate-key `unique`, etc.) — closes MAJOR finding #2.
- Handles both `tests` and `data_tests` (dbt 1.8+ alias), both bare
  (`- unique`) and block-form (`- relationships: {...}`) tests, and
  quoted / commented YAML names — closes MINOR finding #6.
- Sources are qualified as `source.table.column` in the model field.

Fall back to the pre-R18 string-based line detection when no content is
provided (e.g. unit-test callers, offline CI diffs without a content
resolver). Fallback emits a suggestion / warning without column
attribution — cannot distinguish "moved to sibling column" from
"removed" with diff-only input, that limitation is inherent. The
existing test's expectation is preserved.

Wire the orchestrator to always supply old/new content when calling the
detector on modified schema.yml files, so the fallback only fires in
tests / diff-only CI paths, not in production reviews.

## MAJOR — auto-manifest projectRoot not threaded to compiled resolver
(finding #3)

`autoDiscoverManifest` returned `{ path, projectRoot }` and rebased
`manifestAbs` correctly, but `dbtProjectName(opts.cwd)` and
`makeCompiledResolver({ cwd: opts.cwd })` still used `opts.cwd`. When
the CLI was invoked from a subdirectory, auto-discovery would find the
manifest in an ancestor project but the compiled resolver looked for
`target/compiled/…` under the subdir and never found it — engine lanes
silently fell back to raw Jinja.

Thread a `dbtRoot` variable through the auto-discovery path: starts as
`opts.cwd`, updated to `discovered.projectRoot` when G3 fires. Use it
for both `dbtProjectName(dbtRoot)` and
`makeCompiledResolver({ cwd: dbtRoot })` so compiled SQL is found next
to the discovered manifest.

## MINORS

- **#4 docstring** — `autoDiscoverManifest`'s docstring cited defenses
  ("relative escapes", "under project's parent") that aren't in the
  code. Rewrote the doc to describe what actually happens:
  walk up for `dbt_project.yml`; return the adjacent
  `target/manifest.json` when present; never grab a `target/` from an
  unrelated tool that happens to sit above us on the filesystem.

- **#5a verdictHeadline undefined** — `env.tierClassified` is optional
  in the schema; an externally-built envelope with `tierForced: true`
  but no `tierClassified` would render `was undefined`. Added a
  `?? "unknown"` fallback in `format.ts`.

- **#5b unbounded tierReasons in summary** — when `--force-tier` is
  passed on a large PR, `tierReasons` prepends the forced marker AND
  spreads all `classifyPR().reasons` (one entry per file that forces
  the FULL tier). Rendered comment was bloating. Cap the rendered
  summary at the first 8 reasons with a "+N more in verdict envelope"
  overflow marker; the full list stays in the signed envelope for
  audit.

## Tests

Added 9 new regression tests:

- `dbt-patterns` — structural: sibling-column edge case (the exact
  case G6 claims to fix), model-level test removal with attribution
  metadata, block-form `- relationships:`, `data_tests` alias,
  source-column tests, added-file returns 0 removals, fallback
  diff-only path preserves the existing test's expectation.
- `verdict` — `--force-tier` envelope audit: `tierForced` and
  `tierClassified` are set whenever the flag is passed (including when
  forced tier matches classifier), and are covered by the HMAC
  signature (tampered envelope stripping the audit fields fails
  verifyEnvelope).

Test suite: 155/155 passing across the six review test files
(previously 46/47 — the one test the R18 branch broke is back green).
Typecheck clean.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

sahrizvi commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Follow-up commit b32065b23 addresses the blocker, both majors, and the three minors. Summary:

Blocker no. 1 — column-aware detector regression + broken test

Fixed. Rewrote detectSchemaYmlPatterns to prefer structural YAML diffing (reviewer's "preferred" fix): the detector now accepts old/new file content via a new SchemaYmlDetectContent parameter, parses both sides with the yaml package, walks models[] / snapshots[] / sources[] / seeds[], and diffs by (entity, column?, test) tuples.

  • Wired the orchestrator to always pass old/new content when calling the detector on modified schema.yml files, so production reviews use the structural path.
  • The fallback (string-based, mirroring the pre-behavior) only runs when no content is supplied (unit-test callers, offline CI diffs without a content resolver). Fallback emits a suggestion/warning without column attribution — the sibling-column limitation is inherent to diff-only input.
  • The previously-failing test at test/altimate/review-dbt-patterns.test.ts:141 is back green under the fallback path.

Major no. 2 — model-level test removals

Fixed as part of no. 1. Structural walker emits per-entity tests directly under models[] / snapshots[] / sources[] (surrogate-key unique, etc.). New test in review-dbt-patterns.test.ts asserts a model-level unique removal produces exactly one finding with attribution: "model-level" in the evidence.

Major no. 3 — auto-manifest projectRoot not threaded

Fixed. Added a dbtRoot local in run.ts that starts as opts.cwd and gets updated to discovered.projectRoot when earlier bug fires. dbtProjectName(dbtRoot) and makeCompiledResolver({ cwd: dbtRoot }) now use it, so subdir invocations find compiled SQL next to the discovered manifest instead of silently falling back to raw Jinja.

Minor no. 4 — docstring overstates safety guarantees

Fixed. Rewrote autoDiscoverManifest's docstring to describe what the code actually does (walk up for dbt_project.yml; return the adjacent target/manifest.json when present; never grab a target/ from an unrelated tool that happens to sit above us).

Minor no. 5 — verdictHeadline undefined + unbounded tierReasons

Both fixed.

  • format.ts: env.tierClassified ?? "unknown" fallback so an externally-built envelope with tierForced: true but no tierClassified doesn't render was undefined.
  • format.ts: cap the rendered tierReasons at 8 with a +N more in verdict envelope overflow marker on large PRs. Full list stays in the signed envelope for audit — only the human-readable summary is capped.

Minor no. 6 — quoted / commented YAML names

Subsumed by the structural YAML parser (Minor no. 6's own recommendation). The yaml package handles - name: "orders" and - name: order_id # pk correctly.

Missing tests (called out by convergence)

Added 9 regression-guard tests:

  • detectSchemaYmlPatterns (structural): sibling-column edge case (the exact scenario earlier bug claims to fix), model-level test removal + attribution metadata, block-form - relationships:, data_tests alias, source-column tests, added-file returns 0 removals.
  • detectSchemaYmlPatterns (fallback): diff-only path preserves the pre-test's expectation.
  • --force-tier envelope audit in review.test.ts: tierForced and tierClassified are set whenever the flag is passed (including when forced tier == classifier's tier), and are covered by the HMAC signature (stripping the audit fields from a signed envelope fails verifyEnvelope).

Test results: 155 pass / 0 fail across the 6 review*.test.ts files. Typecheck clean.

Not addressed in this commit (open follow-ups I'd take in a subsequent PR unless you want them here):

  • warnIfStale unit tests (fs setup)
  • autoDiscoverManifest walk-up unit test (fs setup)
  • --no-ai end-to-end regression through yargs (would need a subprocess harness; the smoke on js2 covers it)

Happy to fold any of the above into this PR if you'd rather not defer.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

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

🤖 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 `@packages/opencode/src/altimate/review/dbt-patterns.ts`:
- Around line 983-1003: Update the fallback deduplication in the !usedStructural
branch of the dbt pattern detection flow so distinct matched removal lines are
preserved even when model and column are empty. Key deduplication by each
matched line’s index or content, while still preventing duplicate processing of
the same fallback line and retaining the existing test-type extraction.
- Around line 954-981: The structural comparison around extractTestOccurrences
must only run when both old and new YAML documents are available; do not treat
undefined oldContent or an unparseable old document as an empty added-file side.
When old YAML is unavailable, leave usedStructural false so the caller executes
fallbackRemovedTestLines(file.diff), while preserving the added-file behavior
only when the old side is genuinely absent.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 682cedd5-0e3a-4488-9ff8-bb257c9a19d2

📥 Commits

Reviewing files that changed from the base of the PR and between 98a0439 and b32065b.

📒 Files selected for processing (6)
  • packages/opencode/src/altimate/review/dbt-patterns.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/test/altimate/review-dbt-patterns.test.ts
  • packages/opencode/test/altimate/review.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/run.ts

Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts
Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/altimate/review/dbt-patterns.ts">

<violation number="1" location="packages/opencode/src/altimate/review/dbt-patterns.ts:862">
P2: Snapshot YAML files under `snapshots/` never reach this extraction, so their removed tests remain unreported despite the new snapshots support. Classify snapshot YAML property files as `schema_yml` (or admit YAML `snapshot` files at this gate) before relying on this walker.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/altimate/review/orchestrate.ts Outdated
}
}
// `snapshots:` — same shape as models
if (Array.isArray(d.snapshots)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Snapshot YAML files under snapshots/ never reach this extraction, so their removed tests remain unreported despite the new snapshots support. Classify snapshot YAML property files as schema_yml (or admit YAML snapshot files at this gate) before relying on this walker.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/review/dbt-patterns.ts, line 862:

<comment>Snapshot YAML files under `snapshots/` never reach this extraction, so their removed tests remain unreported despite the new snapshots support. Classify snapshot YAML property files as `schema_yml` (or admit YAML `snapshot` files at this gate) before relying on this walker.</comment>

<file context>
@@ -790,162 +791,294 @@ export function detectModelPatterns(file: ChangedFile, newSql: string | undefine
+    }
+  }
+  // `snapshots:` — same shape as models
+  if (Array.isArray(d.snapshots)) {
+    for (const s of d.snapshots) {
+      if (!s || typeof s !== "object") continue
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. classifyDbtFile in diff-filter.ts now splits by extension: snapshots/*.sql remains snapshot (tier-forcing / catalog rules unchanged), snapshots/*.yml classifies as schema_yml and reaches the test-removal detector. Assertions added in review.test.ts (snapshots/*.ymlschema_yml) and an end-to-end test at review-dbt-patterns.test.ts:417 (snapshot yml property file (structural): removed test surfaces finding).

Comment thread packages/opencode/src/altimate/review/dbt-patterns.ts Outdated
Comment thread packages/opencode/src/altimate/review/orchestrate.ts Outdated

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Code Review — OpenCodeReview (Gemini) — 5 finding(s)

  • 5 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/altimate/review/dbt-patterns.ts (L994-L1002)

[🟠 MEDIUM] In fallback mode, model and column are empty strings, so the deduplication key is effectively just \x00\x00${test}. If multiple tests of the same type are removed, they will be collapsed into a single finding. This loses the total count of removed tests that the previous implementation provided (e.g., "removed 3 data test(s)"). Consider preserving the raw count of removed lines in fallback mode so the scale of test regressions is not understated to users.

2. packages/opencode/src/altimate/review/dbt-patterns.ts (L1055-L1061)

[🟠 MEDIUM] If a PR removes tests from multiple models, isFirstForModel will evaluate to true for the first finding of each distinct model. This means the exact same global summary ("This PR removes X data tests in total on model(s) A, B") will be redundantly appended to multiple findings in the review, causing comment clutter. Consider tracking a single boolean flag (e.g., let summaryPrinted = false) to ensure the global summary is only appended once to the very first finding generated for the file.

3. packages/opencode/src/altimate/review/dbt-patterns.ts (L958-L969)

[🔵 LOW] When YAML parsing fails, the exception is caught and silently ignored, which demotes the analysis to the string-diff fallback path. Consider adding a debug or telemetry log within the catch block. This would aid in diagnosing issues where valid-looking PRs fail structural parsing simply due to minor YAML syntax errors.

4. packages/opencode/src/altimate/review/run.ts (L126-L127)

[🟠 MEDIUM] Although the comment explicitly states an intention to only compare against tracked, on-disk model files (schema.yml or SQL), there is no actual file extension filtering applied to changedPaths. Because changedPaths includes all modified files in the diff, any modification to a non-dbt file (e.g., README.md or package.json) after the manifest was generated will trigger a false positive stale manifest warning.

You should filter the files before checking their mtimeMs.

Suggested change:

      // Only compare against tracked, on-disk model files (schema.yml or SQL)
      if (!/\.(sql|yml|yaml|py|csv)$/i.test(rel)) continue
      const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel)

5. packages/opencode/src/altimate/review/orchestrate.ts (L1226-L1232)

[🔴 HIGH] 1. Logic bug with renamed files:
Using file.status === "modified" skips fetching oldContent for renamed files (where status is "renamed"). Since the detector relies on comparing oldContent with newContent, any guardrail tests removed during a file rename will be completely missed. You should use file.status !== "added" instead to ensure oldContent is fetched for both renamed and modified files.

2. Performance (Async in Loops):
Fetching file contents sequentially inside a for...of loop creates a performance bottleneck. As per the async handling checklist, independent async operations should be parallelized.

While the direct syntax fix for the status check is provided in the suggested replacement, consider refactoring the entire loop to use Promise.all concurrently:

const schemaFindings = await Promise.all(
  reviewable
    .filter((file) => file.kind === "schema_yml")
    .map(async (file) => {
      const oldRef = file.oldPath ?? file.path
      const [oldContent, newContent] = await Promise.all([
        file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
        file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
      ])
      return detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent })
    })
)
all.push(...schemaFindings)

Suggested change:

    if (file.kind !== "schema_yml") continue
    const oldRef = file.oldPath ?? file.path
    const [oldContent, newContent] = await Promise.all([
      file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
      file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
    ])
    all.push(detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent }))

Comment on lines +994 to +1002
// Deduplicate identical (model, column, test) triples so the fallback
// path emits at most one finding per removed test-line pattern.
const seen = new Set<string>()
removals = removals.filter((r) => {
const k = `${r.model}\x00${r.column}\x00${r.test}`
if (seen.has(k)) return false
seen.add(k)
return true
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] In fallback mode, model and column are empty strings, so the deduplication key is effectively just \x00\x00${test}. If multiple tests of the same type are removed, they will be collapsed into a single finding. This loses the total count of removed tests that the previous implementation provided (e.g., "removed 3 data test(s)"). Consider preserving the raw count of removed lines in fallback mode so the scale of test regressions is not understated to users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. Fallback path now emits one finding per removed test-line (dedup removed, occurrence-index discriminator added to ruleKey so global fingerprint dedupe doesn't collapse them). Aggregate "removes N tests" line preserved via file-scoped summaryEmitted — appended once to the first finding, listing distinct models when structural attribution is available (empty when it isn't). See #3619947164 for the exact fix + regression test.

Comment on lines +1055 to +1061
const isFirstForModel = !!r.model && !seenModel.has(r.model)
if (r.model) seenModel.add(r.model)
const bodyTail =
isFirstForModel && removals.length > 1
? `\n\n_This PR removes ${removals.length} data tests in total on model(s) ` +
`${[...new Set(removals.map((x) => x.model).filter(Boolean))].map((m) => `\`${m}\``).join(", ")}._`
: ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] If a PR removes tests from multiple models, isFirstForModel will evaluate to true for the first finding of each distinct model. This means the exact same global summary ("This PR removes X data tests in total on model(s) A, B") will be redundantly appended to multiple findings in the review, causing comment clutter. Consider tracking a single boolean flag (e.g., let summaryPrinted = false) to ensure the global summary is only appended once to the very first finding generated for the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. Replaced the per-model isFirstForModel guard with a single file-scoped summaryEmitted boolean. The aggregate "This PR removes N data tests in total on model(s) …" line now appears exactly once per file — on the first finding — and the model list is computed from the whole removals array. New test at review-dbt-patterns.test.ts:454 asserts multi-model diffs produce ONE summary line, not one per model.

Comment on lines +958 to +969
try {
newDoc = YAML.parse(opts.newContent)
} catch {
newDoc = undefined
}
if (opts.oldContent !== undefined) {
try {
oldDoc = YAML.parse(opts.oldContent)
} catch {
oldDoc = undefined
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🔵 LOW] When YAML parsing fails, the exception is caught and silently ignored, which demotes the analysis to the string-diff fallback path. Consider adding a debug or telemetry log within the catch block. This would aid in diagnosing issues where valid-looking PRs fail structural parsing simply due to minor YAML syntax errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. Both YAML.parse failure branches now log via Log.create({ service: "review", tag: "detectSchemaYmlPatterns" }).warn(…) including the file path and error message, then fall through to the diff-based fallback (not fail-open). Diagnosing a valid-looking PR that fails structural parsing is now grep-friendly in the review logs.

Comment on lines +126 to +127
// Only compare against tracked, on-disk model files (schema.yml or SQL)
const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] Although the comment explicitly states an intention to only compare against tracked, on-disk model files (schema.yml or SQL), there is no actual file extension filtering applied to changedPaths. Because changedPaths includes all modified files in the diff, any modification to a non-dbt file (e.g., README.md or package.json) after the manifest was generated will trigger a false positive stale manifest warning.

You should filter the files before checking their mtimeMs.

Suggested change:

Suggested change
// Only compare against tracked, on-disk model files (schema.yml or SQL)
const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel)
// Only compare against tracked, on-disk model files (schema.yml or SQL)
if (!/\.(sql|yml|yaml|py|csv)$/i.test(rel)) continue
const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. See #3619684688 — introduced isManifestAffecting(rel) filter in run.ts, admits only dbt-relevant extensions under dbt source directories (plus root config files). Table-driven positive/negative tests in the new review-run-stale.test.ts file cover README.md, models/foo.sql, docs/foo.md, models/foo.md, seeds/lookup.csv, snapshots/.sql, snapshots/.yml, dbt_project.yml, .github/workflows/, target/…

Comment on lines +1226 to +1232
if (file.kind !== "schema_yml") continue
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status === "modified" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
])
all.push(detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent }))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🔴 HIGH] 1. Logic bug with renamed files:
Using file.status === "modified" skips fetching oldContent for renamed files (where status is "renamed"). Since the detector relies on comparing oldContent with newContent, any guardrail tests removed during a file rename will be completely missed. You should use file.status !== "added" instead to ensure oldContent is fetched for both renamed and modified files.

2. Performance (Async in Loops):
Fetching file contents sequentially inside a for...of loop creates a performance bottleneck. As per the async handling checklist, independent async operations should be parallelized.

While the direct syntax fix for the status check is provided in the suggested replacement, consider refactoring the entire loop to use Promise.all concurrently:

const schemaFindings = await Promise.all(
  reviewable
    .filter((file) => file.kind === "schema_yml")
    .map(async (file) => {
      const oldRef = file.oldPath ?? file.path
      const [oldContent, newContent] = await Promise.all([
        file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
        file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
      ])
      return detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent })
    })
)
all.push(...schemaFindings)

Suggested change:

Suggested change
if (file.kind !== "schema_yml") continue
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status === "modified" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
])
all.push(detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent }))
if (file.kind !== "schema_yml") continue
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
])
all.push(detectSchemaYmlPatterns(file, input.rubric, { oldContent, newContent }))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6729f5374. Both parts of your comment: (1) status gate changed to file.status !== "added" (covers renamed files); (2) loop refactored to Promise.all across schema files (concurrent) with per-file inner Promise.all for old/new content. Uses roughly the exact shape you suggested. See #3619993834 (rename regression test) and #3619993862 (perf note).

@sahrizvi
sahrizvi marked this pull request as draft July 21, 2026 09:57
…ame bypass, snapshot yml, +5 more)

After the earlier fixes on `b32065b23`, three independent reviewer
bots (coderabbitai, cubic-dev-ai, dev-punia-altimate) surfaced 8 more
issues on the schema.yml test-removal path — 2 HIGH, 1 MAJOR, 4
MEDIUM, 1 LOW — plus an independent local review round after that
caught an additional blocker downstream of the fixes. This commit
addresses each.

## BLOCKER — fallback distinct-removal fix defeated by global dedupe

The R18-follow-up fix removed a local dedup in the fallback path so
each removed test-line produced its own detector-level finding. But
`runReview` runs a global `dedupe(merged)` step that fingerprints
findings by (category, file, model, column, ruleKey) via
`finding.ts:107`. For fallback findings sharing `file`, empty
`model`, empty `column`, and a ruleKey varying only by `test`, two
`unique` removals in the same file still collapsed to one downstream.
The detector-level test at `review-dbt-patterns.test.ts:397` didn't
catch it because it asserts against the pre-dedupe output.

Fix: give fallback findings a per-diff occurrence-index discriminator
appended to the ruleKey (`.#0`, `.#1`, …) so distinct removals get
distinct fingerprints. Structural (attributed) findings unchanged —
`(model.column.test)` is already unique per removal.

Added an integration-shape regression test at
`review-dbt-patterns.test.ts:432` that pipes detector output through
`dedupe(f)` and asserts 4 distinct ids survive for 4 distinct
removals. Also a same-shape guard for the structural path.

## HIGH — renamed schema.yml bypasses detector

The orchestrator gated `oldContent` fetch on `file.status === "modified"`.
A schema.yml being renamed (e.g. moved to a new subdir) that also
dropped a `unique`/`not_null`/`relationships` guardrail silently
skipped the detector because `oldContent` came back undefined and
the structural path treated the file as newly-added.

Fix: gate on `file.status !== "added"` so renamed files fetch the
old side; separately, changed the detector so an undefined
`oldContent` on a modified/renamed file falls through to the
diff-based fallback (does not silently treat as added-file).

Added test at `review-dbt-patterns.test.ts:332` covering the rename
shape.

## MAJOR — structural path treats `oldContent=undefined` as added file

`canUseStructural = newContent !== undefined && (isAddedFile ||
oldContent !== undefined)`. When the content resolver returns
undefined on a modified/renamed file for a transient reason (git
failure, ref not readable), we now leave `usedStructural = false`
and let the fallback surface the raw diff removals — instead of
producing an empty structural-diff and silently dropping every
removal in the diff.

## MEDIUM — snapshot YAML property files never reached the detector

`classifyDbtFile` matched everything under `snapshots/` as
`snapshot` regardless of extension, so `snapshots/*.yml` never
reached the `schema_yml` gate in the orchestrator. The new
`snapshots:` branch added to the structural walker was dead code
for real snapshot property files.

Fix: split the classification by extension. `snapshots/*.sql`
remains `snapshot` (unchanged tier-forcing / catalog rules).
`snapshots/*.yml` classifies as `schema_yml` (routes to the
test-removal detector). Added `classifyDbtFile` assertions for
both extensions and an end-to-end structural-detector test on a
snapshot yml.

## MEDIUM — redundant per-model summary line

`isFirstForModel` fired once per distinct model, so a diff touching
two models produced two copies of the same aggregate summary
("This PR removes N tests total on model(s) A, B") appended to
separate findings.

Fix: replaced with a single file-scoped `summaryEmitted` boolean.
The aggregate summary is appended to the first finding for the
file, once, and the distinct-models list is computed from the
whole removals array. Test at
`review-dbt-patterns.test.ts:454` asserts exactly one finding
carries the aggregate line when two models have removals.

## MEDIUM — `warnIfStale` fired on unrelated file changes

`warnIfStale` compared mtimes for every changed path. Any
`README.md` / `package.json` / `.github/*` change post-manifest
triggered a false-positive stale warning.

Fix: introduced `isManifestAffecting()` — admits only
`.sql|.py|.yml|.yaml|.csv|.md` under
`models|seeds|snapshots|macros|tests|analyses/`, plus root
`dbt_project.yml|packages.yml|profiles.yml|dependencies.yml`.
Explicitly admits `.md` under `models/` because dbt docs blocks
live there. Exported for tests; added a table-driven test file
`review-run-stale.test.ts` covering both admitted and rejected
cases (README.md, models/foo.sql, docs/foo.md, models/foo.md,
seeds/lookup.csv, snapshots/*.sql, snapshots/*.yml, dbt_project.yml,
.github/workflows/, target/…).

## LOW — YAML parse errors silently ignored

Both `YAML.parse` failures (new content, old content) now log via
`Log.create({ service: "review", tag: "detectSchemaYmlPatterns" })`
with the file path and error message, then fall through to the
diff-based fallback. Not fail-open.

## PERF — sequential schema.yml `getContent` calls

The orchestrator's schema.yml loop was serial `await` per file;
schema-heavy PRs paid one round-trip per file. Refactored to
`Promise.all` across the file list with per-file `Promise.all`
for old/new. Ordering preserved by `Promise.all` contract.

## Test status

188 pass / 0 fail across the 7 review test files. Typecheck clean.
End-to-end sanity on the js2 test-removal scenario: CLI still
surfaces both `unique` and `not_null` warnings, verdict
`COMMENT/trivial/2`, envelope signature verifies.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

sahrizvi commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Bot-review round-2 addressed — pushed as 6729f5374 (PR back to draft while I address remaining feedback)

Three reviewer bots (coderabbitai, cubic-dev-ai, dev-punia-altimate) surfaced 8 issues on the previous commit b32065b23. A separate independent local review found one additional blocker downstream of those fixes. All 9 addressed in 6729f5374. Individual per-comment replies below.

What was preexisting on this PR vs what I introduced during the fix cycle

Splitting the 9 issues by origin so it's clear what the reviewers uncovered:

Preexisting bugs on the branch before any of the review rounds (would have shipped had reviewers not caught them):

  • Renamed schema.yml bypasses the detector (HIGH). The orchestrator gate at orchestrate.ts was file.status === "modified" since the commit; renamed schema files silently skipped oldContent fetch. Introduced when I first wired the detector to the content resolver.
  • Redundant per-model summary (MEDIUM). isFirstForModel fired once per distinct model instead of once per file; my aggregate summary line appeared multiple times on multi-model diffs. Introduced with the multi-finding refactor.
  • warnIfStale fired on unrelated file changes (MEDIUM). The stale-manifest warning iterated every changed path without filtering; a README.md change post-manifest triggered a false-positive. Introduced with earlier bug (manifest auto-discovery) in the commit.

Introduced by my earlier fix for the round-1 review's blocker (fixing the previous earlier bug regression re-created some rough edges):

  • Fallback dedup collapse (HIGH). When I removed the earlier round's diff-context walker and added a string-based fallback for diff-only callers, I dedup'd by (model, column, test) — but model + column are always empty in that path, so the key reduced to just test and distinct removals of the same test type collapsed.
  • Fallback distinct-removals defeated by global dedupe (BLOCKER, caught by the local independent review after the round-2 bot findings). Fixing the local dedup was insufficient — the global dedupe(findings) at orchestrate.ts:1380 fingerprints by (category, file, model, column, ruleKey). My fallback findings shared everything except the test-name suffix in ruleKey, so 2 unique removals still collapsed to 1 in production. The detector-level test asserted post-detector-count and missed the production regression. Fixed by adding a per-diff occurrence-index discriminator (.#0, .#1, …) to fallback ruleKeys and adding an integration-shape regression test that pipes detector output through dedupe().
  • Structural path treats oldContent=undefined as added-file (MAJOR). When the content resolver returned undefined on a modified file for a transient reason, my code set usedStructural = true with an empty old set and silently dropped every real removal in the diff.
  • Snapshot YAML property files never reached the detector (MEDIUM). classifyDbtFile matched everything under snapshots/ as snapshot regardless of extension, so snapshots/*.yml never reached the schema_yml gate.

Enhancement (surfaced by review, not a bug):

  • YAML parse errors silently ignored (LOW). Added Log.warn so failures are diagnosable.
  • Sequential getContent calls on schema.yml files (PERF). Refactored to Promise.all.

Test coverage added

  • Integration-shape test that pipes detector output through the global dedupe(...) step and asserts 4 distinct findings for 4 distinct removals — the exact regression guard the blocker demanded.
  • Same-shape guard for the structural path.
  • Table-driven test for isManifestAffecting() covering README.md, models/foo.sql, docs/foo.md, models/foo.md, seeds/lookup.csv, snapshots/*.sql, snapshots/*.yml, dbt_project.yml, .github/workflows/*, target/….
  • classifyDbtFile assertion for snapshots/*.ymlschema_yml.
  • Renamed schema.yml test covering the rename path.
  • Modified file with oldContent=undefined falls to fallback (not silently dropped).
  • Distinct fallback removals on same test type each surface.
  • Snapshot YAML property file structural removal surfaces finding.
  • Multi-model diff emits ONE aggregate summary line, not one per model.

Test status

188 pass / 0 fail across the 7 review test files. Typecheck clean. End-to-end sanity on the js2 test-removal scenario: CLI surfaces both unique and not_null warnings, verdict COMMENT/trivial/2, envelope signature verifies.

Meta-lesson

The R2 review round caught more than R1 because the R2 reviewers ran the code (extracted functions, executed against synthetic hunks, traced through downstream call sites like the global dedupe) rather than reasoning about it. State-combination pass was missing from the earlier review passes on my end. Fixing that in the review approach going forward — see the local review notes.

Marking the PR as draft while I finish per-comment replies below, then ready-for-review once every open thread has a resolution.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Code Review — OpenCodeReview (Gemini) — 3 finding(s)

  • 3 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/altimate/review/orchestrate.ts (L1233-L1241)

[🟠 MEDIUM] The change file.status !== "added" to capture renames also inadvertently includes deleted files. This causes an unnecessary fetch of oldContent (which could be an expensive operation like git show) for deleted files.

Since detectSchemaYmlPatterns immediately returns [] when file.status === "deleted", we can optimize this by filtering out deleted files upfront. This also simplifies the newContent condition.

Suggested change:

  const schemaFiles = reviewable.filter((f) => f.kind === "schema_yml" && f.status !== "deleted")
  if (schemaFiles.length) {
    const schemaFindingSets = await Promise.all(
      schemaFiles.map(async (file) => {
        const oldRef = file.oldPath ?? file.path
        const [oldContent, newContent] = await Promise.all([
          file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
          getContent?.(file.path, "new"),
        ])

2. packages/opencode/src/altimate/review/dbt-patterns.ts (L1100-L1108)

[🟠 MEDIUM] The distinctModels and modelClause variables are recomputed on every iteration of the loop, but they are only used when shouldEmitSummary is true (which evaluates to true only once per file). This causes unnecessary object allocations and redundant O(N) operations in every loop iteration.

Consider moving this computation inside an if (shouldEmitSummary) block, or precomputing the string once before the loop.

Suggested change:

    const shouldEmitSummary = !summaryEmitted && removals.length > 1
    if (shouldEmitSummary) summaryEmitted = true
    let bodyTail = ""
    if (shouldEmitSummary) {
      const distinctModels = [...new Set(removals.map((x) => x.model).filter(Boolean))]
      const modelClause = distinctModels.length
        ? ` on model(s) ${distinctModels.map((m) => `\`${m}\``).join(", ")}`
        : ""
      bodyTail = `\n\n_This PR removes ${removals.length} data tests in total${modelClause}._`
    }

3. packages/opencode/src/altimate/review/dbt-patterns.ts (L1116)

[🔵 LOW] Calling shift() on an array inside a loop yields O(N^2) time complexity because shift() forces a re-indexing of all remaining elements in the array. While the array length here might be small, it is a good practice to avoid array mutation inside loops when a simple index access suffices.

Consider defining an index variable (e.g. let fallbackIdx = 0;) before the for (const r of removals) loop, and using it here instead.

Suggested change:

    const discriminator = attributed || modelLevel ? "" : `.${fallbackDiscriminators[fallbackIdx++] ?? "#?"}`

Comment on lines +1233 to +1241
const schemaFiles = reviewable.filter((f) => f.kind === "schema_yml")
if (schemaFiles.length) {
const schemaFindingSets = await Promise.all(
schemaFiles.map(async (file) => {
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] The change file.status !== "added" to capture renames also inadvertently includes deleted files. This causes an unnecessary fetch of oldContent (which could be an expensive operation like git show) for deleted files.

Since detectSchemaYmlPatterns immediately returns [] when file.status === "deleted", we can optimize this by filtering out deleted files upfront. This also simplifies the newContent condition.

Suggested change:

Suggested change
const schemaFiles = reviewable.filter((f) => f.kind === "schema_yml")
if (schemaFiles.length) {
const schemaFindingSets = await Promise.all(
schemaFiles.map(async (file) => {
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
file.status !== "deleted" ? getContent?.(file.path, "new") : Promise.resolve(undefined),
])
const schemaFiles = reviewable.filter((f) => f.kind === "schema_yml" && f.status !== "deleted")
if (schemaFiles.length) {
const schemaFindingSets = await Promise.all(
schemaFiles.map(async (file) => {
const oldRef = file.oldPath ?? file.path
const [oldContent, newContent] = await Promise.all([
file.status !== "added" ? getContent?.(oldRef, "old") : Promise.resolve(undefined),
getContent?.(file.path, "new"),
])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 39718d5d1. schemaFiles now filters out status === "deleted" upfront in orchestrate.ts — deleted schema.yml files never enter the fetch loop, so the redundant git show round-trip is gone. newContent fetch simplifies to unconditional getContent?.(file.path, "new") since deleted files can't reach it anymore. Used the exact shape from your suggestion.

Comment on lines +1100 to +1108
const shouldEmitSummary = !summaryEmitted && removals.length > 1
if (shouldEmitSummary) summaryEmitted = true
const distinctModels = [...new Set(removals.map((x) => x.model).filter(Boolean))]
const modelClause = distinctModels.length
? ` on model(s) ${distinctModels.map((m) => `\`${m}\``).join(", ")}`
: ""
const bodyTail = shouldEmitSummary
? `\n\n_This PR removes ${removals.length} data tests in total${modelClause}._`
: ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] The distinctModels and modelClause variables are recomputed on every iteration of the loop, but they are only used when shouldEmitSummary is true (which evaluates to true only once per file). This causes unnecessary object allocations and redundant O(N) operations in every loop iteration.

Consider moving this computation inside an if (shouldEmitSummary) block, or precomputing the string once before the loop.

Suggested change:

Suggested change
const shouldEmitSummary = !summaryEmitted && removals.length > 1
if (shouldEmitSummary) summaryEmitted = true
const distinctModels = [...new Set(removals.map((x) => x.model).filter(Boolean))]
const modelClause = distinctModels.length
? ` on model(s) ${distinctModels.map((m) => `\`${m}\``).join(", ")}`
: ""
const bodyTail = shouldEmitSummary
? `\n\n_This PR removes ${removals.length} data tests in total${modelClause}._`
: ""
const shouldEmitSummary = !summaryEmitted && removals.length > 1
if (shouldEmitSummary) summaryEmitted = true
let bodyTail = ""
if (shouldEmitSummary) {
const distinctModels = [...new Set(removals.map((x) => x.model).filter(Boolean))]
const modelClause = distinctModels.length
? ` on model(s) ${distinctModels.map((m) => `\`${m}\``).join(", ")}`
: ""
bodyTail = `\n\n_This PR removes ${removals.length} data tests in total${modelClause}._`
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 39718d5d1. Moved the distinctModels / modelClause computation inside the if (shouldEmitSummary) block — no Set/spread/filter/map/join on iterations where the summary isn't attached (all but the first). summaryEmitted flip also moved inside the block.

// distinct removals of the same test type — we append the fallback
// discriminator so each removed line becomes a distinct finding
// downstream of the global dedupe.
const discriminator = attributed || modelLevel ? "" : `.${fallbackDiscriminators.shift() ?? "#?"}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🔵 LOW] Calling shift() on an array inside a loop yields O(N^2) time complexity because shift() forces a re-indexing of all remaining elements in the array. While the array length here might be small, it is a good practice to avoid array mutation inside loops when a simple index access suffices.

Consider defining an index variable (e.g. let fallbackIdx = 0;) before the for (const r of removals) loop, and using it here instead.

Suggested change:

Suggested change
const discriminator = attributed || modelLevel ? "" : `.${fallbackDiscriminators.shift() ?? "#?"}`
const discriminator = attributed || modelLevel ? "" : `.${fallbackDiscriminators[fallbackIdx++] ?? "#?"}`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 39718d5d1. Hoisted let fallbackIdx = 0 before the for (const r of removals) loop and swapped fallbackDiscriminators.shift() for fallbackDiscriminators[fallbackIdx++]. Discriminator array is no longer mutated inside the loop; index-based access is O(1) per iteration.

Five items from coderabbit / cubic-dev-ai / kilo-code-bot on round 5:

- Symlink-escape containment (coderabbit + cubic): `makeContentResolver`
  working-tree read and `makeCompiledResolver` compiled-SQL read now go
  through a `realpath` containment check that rejects any target whose
  resolved path falls outside the resolved root. A tracked symlink like
  `models/evil.sql → /etc/passwd` no longer leaks external content into
  the review pipeline. Applied at both the diff-side (git.ts) and
  compiled-side (compiled.ts) resolvers.

- Symlink-consistent path prefix (cubic + kilo): `path.relative(gitRoot,
  dbtRoot)` produced a bogus climb path (`../../../var/...`) whenever
  `dbtRoot` traversed an unresolved symlink (macOS `/var` → `/private/var`
  is the canonical case). The mapped prefix never matched incoming
  repo-relative paths, so compiled SQL was silently missed on the exact
  monorepo layouts the subdir fix was meant to enable. Both roots are
  now realpath-resolved before `path.relative`, and `dbtRootReal` is
  handed to `makeCompiledResolver` as its `cwd`. Falls back gracefully
  when realpath fails (path deleted mid-run).

- `gitRepoRoot` newline handling (cubic P3): `.trim()` also stripped
  legitimate leading/trailing path whitespace. Replaced with an explicit
  `\r?\n` terminator strip that leaves everything else intact.

- dbt version comment (cubic P3): the `arguments:` nesting syntax is
  documented from dbt v1.10.5, not 1.9+. Comment updated.

New regression tests in `review-subdir-invocation.test.ts`:
- makeContentResolver refuses to read a symlink escaping the git root
- makeCompiledResolver refuses to read a compiled symlink escaping
  the compiled root
- gitRepoRoot preserves legitimate path characters (no trim()
  regression)

Full altimate review suite: 3790 pass / 640 skip / 0 fail (was 3787
baseline — 3 new tests, +8 expect calls).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode/src/altimate/review/git.ts Outdated
const rootReal = await fs.realpath(compiledRoot)
const targetReal = await fs.realpath(path.join(compiledRoot, rel))
const sep = path.sep
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep)) return undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: This realpath + containment block duplicates safeReadInside in git.ts:70 (identical logic: resolve root + target, separator-aware startsWith check, then read the realpath'd target).

compiled.ts doesn't import git.ts today, and git.ts doesn't import compiled.ts (no cycle), so exporting safeReadInside and replacing lines 87-91 with return await safeReadInside(compiledRoot, rel) removes the copy. Keeping two copies of security-sensitive containment logic is risky — a fix to one (e.g. a Windows case-sensitivity or trailing-separator edge) won't propagate to the other.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in ab461c450 (same fix as cubic's P3 on this pair). safeReadInside is now exported from git.ts and used by both makeContentResolver and makeCompiledResolver. No behavior change; 3790 pass / 0 fail.

…compiled.ts

cubic + kilo bot review — the realpath containment check was duplicated
across `makeContentResolver` (git.ts) and `makeCompiledResolver`
(compiled.ts). Both bots flagged that keeping two copies of security-
sensitive logic risks a future fix (e.g. Windows case-sensitivity,
trailing-separator edge) landing in one call site but not the other.

- Export `safeReadInside(root, rel)` from git.ts as the single
  realpath-checked reader.
- `compiled.ts` imports it and replaces its inline containment block
  with `return await safeReadInside(compiledRoot, rel)`.
- No behavior change; regression suite still 3790 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

saravmajestic
saravmajestic previously approved these changes Jul 23, 2026
* Only considers dbt-relevant files (SQL, YAML, Python models, seed CSV,
* docs markdown) — changes to `README.md` at repo root, `.github/`,
* `package.json`, etc. don't affect whether the compiled manifest is still
* valid, so their mtimes shouldn't trigger a stale warning. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: This JSDoc block describes warnIfStale (the function that comes after isManifestAffecting), but it's physically placed before isManifestAffecting. TypeScript tooling and IDEs will attribute both JSDoc blocks to isManifestAffecting; warnIfStale will show no doc on hover.

Move this block to immediately precede async function warnIfStale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69553ed9d — the JSDoc block that described warnIfStale was placed above isManifestAffecting, so TS/IDE tooling attributed both blocks to the earlier symbol. Split into two self-contained blocks: isManifestAffecting gets its own docstring (concerns + why it's exported), and warnIfStale gets a fresh block immediately before its definition.

// Top-level dbt project config files.
if (/(^|\/)(dbt_project|packages|profiles|dependencies)\.ya?ml$/i.test(rel)) return true
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: The regex admits .md files under seeds/, macros/, tests/, and analyses/ directories. dbt docs blocks live under models/ (and sometimes analyses/); a macros/README.md or tests/README.md is unlikely to be manifest-affecting but would still trigger a stale warning.

Consider narrowing the .md match to models|analyses, or add a comment documenting that the wider match is a deliberate conservative over-include.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69553ed9d — narrowed the .md match to models|analyses and split the check: other extensions (.sql, .py, .yml, .yaml, .csv) still admitted across all six source dirs, .md only under the two doc-block-canonical dirs. Rejected-path test list gains macros/README.md, tests/README.md, seeds/README.md, snapshots/README.md; admit list gains analyses/gross_margin.md.

Follow-up: cubic-dev-ai flagged the narrow as too tight (dbt's docs-paths config can extend where docs blocks live). See my reply on that thread — kept the narrow since docs-paths extension is project-specific config, not default behavior.

}
} catch {
/* keep walking */
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought: When dbt_project.yml exists but target/manifest.json doesn't, we stop walking up and return undefined — even if an ancestor directory has a compiled project.

This is likely the right call ("found a project, it just hasn't been compiled — don't silently pick up a grandparent"), but it's worth a short comment here so future readers don't wonder if the early return is intentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69553ed9d — added a comment at the early-return site explaining the deliberate choice: when a dbt project is found but not compiled, we return undefined rather than walk further, because a grandparent's target/manifest.json belongs to a different project's DAG. Silent grandparent-fallback would surprise the caller.

sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
Three findings from the harness-bot pass on PR #1027:

1. `run.ts:114` — add rationale for the fixed-point walk's early
   return. When `dbt_project.yml` exists but `target/manifest.json`
   doesn't, we intentionally return `undefined` rather than continue
   walking to a grandparent's compiled project — a grandparent's DAG
   is a different project's DAG. Comment makes this clear so future
   maintainers don't "fix" it into a silent grandparent-fallback.

2. `run.ts:129` — narrow the `.md` match in `isManifestAffecting`.
   dbt `{% docs %}` blocks are canonically parsed under `models/` and
   `analyses/`. Under `macros/`, `seeds/`, `snapshots/`, and `tests/`,
   `.md` files are package documentation (READMEs), not manifest input.
   Split the check so `.md` requires `models|analyses`; other
   extensions (`.sql`, `.py`, `.yml`, `.yaml`, `.csv`) still admitted
   under all six source directories. Added `README.md` under each of
   the four newly-excluded directories to the rejected-path test list.

3. `run.ts:120-124` — JSDoc block that describes `warnIfStale` was
   physically placed above `isManifestAffecting`, so TS/IDE tooling
   would attribute both blocks to the earlier symbol and `warnIfStale`
   would show no hover doc. Split into two blocks: a self-contained
   docstring for `isManifestAffecting` (its own concerns + why it's
   exported) and a fresh docstring for `warnIfStale` immediately
   preceding its definition.

Tests: 210/210 green (7 review-* files); review-run-stale.test.ts
gains `analyses/gross_margin.md` (admitted) plus four `.md`-under-
non-docs-dirs entries in the rejected list.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/altimate/review/run.ts">

<violation number="1" location="packages/opencode/src/altimate/review/run.ts:141">
P2: Stale manifests will not be reported after documentation-block changes under `macros`, `tests`, `seeds`, or `snapshots`. dbt searches all resource paths for docs blocks by default, so retain those directories in the Markdown check.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// `macros/README.md` / `tests/README.md` / `seeds/README.md` is package
// documentation, not manifest input (altimate-harness-bot review,
// PR #1027 run.ts:133).
if (/(^|\/)(models|analyses)\/.*\.md$/i.test(rel)) return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Stale manifests will not be reported after documentation-block changes under macros, tests, seeds, or snapshots. dbt searches all resource paths for docs blocks by default, so retain those directories in the Markdown check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/review/run.ts, line 141:

<comment>Stale manifests will not be reported after documentation-block changes under `macros`, `tests`, `seeds`, or `snapshots`. dbt searches all resource paths for docs blocks by default, so retain those directories in the Markdown check.</comment>

<file context>
@@ -117,21 +124,31 @@ async function autoDiscoverManifest(cwd: string): Promise<{ path: string; projec
+  // `macros/README.md` / `tests/README.md` / `seeds/README.md` is package
+  // documentation, not manifest input (altimate-harness-bot review,
+  // PR #1027 run.ts:133).
+  if (/(^|\/)(models|analyses)\/.*\.md$/i.test(rel)) return true
   // Top-level dbt project config files.
   if (/(^|\/)(dbt_project|packages|profiles|dependencies)\.ya?ml$/i.test(rel)) return true
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Design call — kept the narrow models|analyses scope going with the harness-bot's take. Reasoning: dbt docs blocks ({% docs %}) are canonically parsed under models/ and analyses/; docs-paths can be extended in dbt_project.yml but that's project-specific config, not the default. Silent-failure mode you flag is real but bounded — a stale-manifest warning is non-fatal observability that fires again on the next mtime-checked path, so a missed warning on a custom docs-paths config would just delay the warning to the first SQL/YAML edit in the same PR. Kept the narrow match; happy to widen if this shows up in practice.

sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
@sahrizvi
sahrizvi merged commit 3062ea7 into main Jul 23, 2026
27 of 30 checks passed
sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
…adata

Grounded in the 5-PR internal corpus study
(data-engineering-skills/docs/pr-review-corpus-findings-r20.md) where
historical altimate-code recall was 2/84 = 2.4%. Two of five PRs
auto-approved despite each having 8+ substantive human findings:
- PR D: test-only YAML under `models/marts/` adding
  `data_tests:` / `constraints:` on a contracted mart → 8 misses,
  including 5 dbt-trino adapter-semantic bugs.
- PR E: cost-anchor redesign under `mrt_jobs_cost_savings.sql` →
  11 misses, including 3 critical FinOps corrections.

Adds three FileChangeClass signals + wires them into `fullTierReasons()`:

- **`dbtRiskYmlChanges`** — schema.yml diff introduces or edits
  `data_tests:`, `constraints:`, `contract:`, or a
  `unique_combination_of_columns` list-item. Regex anchors to YAML
  key position after optional diff marker, optional indent, and
  optional list marker; explicitly excludes comment lines. This
  catches PR D.

- **`martLayerChange`** — file lives under `models/marts/` or
  `models/mart/`. Does NOT promote on its own (description-only
  edits stay trivial, matching the existing test), but ENRICHES
  the `dbtRiskYmlChanges` reason string so the customer sees
  "under models/marts/ (mart-API surface)".

- **`finopsPathToken`** — path/filename contains a FinOps keyword
  (`cost|saving|billing|credit|dbu|spend|revenue|price|rate|pricing|
  invoice`) at a word / segment / extension boundary. Catches PR E.
  Word-boundary regex prevents false positives on incidental
  substrings (`broadcaster` ≠ `caste`, `precast` ≠ `cast`).

Regression tests (11 new, all green):
- PR D shapes (data_tests, constraints, unique_combination_of_columns)
  all promote to full with `mart-API surface` context.
- PR E shape (mrt_jobs_cost_savings.sql) + 7 other FinOps keyword
  variants all promote to full.
- FinOps false-positive guard: `broadcaster` / `precast` stay lite.
- Description-only edits under models/marts/ still trivial
  (pre-existing behavior preserved).
- Comment lines and description strings mentioning
  `data_tests:` / `constraints:` do NOT promote (regex tightness).
- Nested-indent YAML key position (production shape) still fires.

Codex-reviewed diff. Two highs addressed:
- Regex tightening to avoid comment / description-string false positives
- FileChangeClass consumer audit (no external constructions; safe)

Ship criteria (from plan v2): PRs D and E from the corpus must no
longer auto-approve. Baseline recall on the existing 13-scenario
corpus must not regress. Both hold: 96/96 tests pass (85 pre-existing
+ 11 new); full altimate review suite 3781/3781 green.

Depends on PR #1027 (feat/review-r18-observability-recall) — this branch
stacks on top so tierReasons[] wiring is in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
sahrizvi added a commit that referenced this pull request Jul 23, 2026
…ring dbt metadata (#1028)

* feat(review): [R20 S4] triage-tier promotion for risk-bearing dbt metadata

Grounded in the 5-PR internal corpus study
(data-engineering-skills/docs/pr-review-corpus-findings-r20.md) where
historical altimate-code recall was 2/84 = 2.4%. Two of five PRs
auto-approved despite each having 8+ substantive human findings:
- PR D: test-only YAML under `models/marts/` adding
  `data_tests:` / `constraints:` on a contracted mart → 8 misses,
  including 5 dbt-trino adapter-semantic bugs.
- PR E: cost-anchor redesign under `mrt_jobs_cost_savings.sql` →
  11 misses, including 3 critical FinOps corrections.

Adds three FileChangeClass signals + wires them into `fullTierReasons()`:

- **`dbtRiskYmlChanges`** — schema.yml diff introduces or edits
  `data_tests:`, `constraints:`, `contract:`, or a
  `unique_combination_of_columns` list-item. Regex anchors to YAML
  key position after optional diff marker, optional indent, and
  optional list marker; explicitly excludes comment lines. This
  catches PR D.

- **`martLayerChange`** — file lives under `models/marts/` or
  `models/mart/`. Does NOT promote on its own (description-only
  edits stay trivial, matching the existing test), but ENRICHES
  the `dbtRiskYmlChanges` reason string so the customer sees
  "under models/marts/ (mart-API surface)".

- **`finopsPathToken`** — path/filename contains a FinOps keyword
  (`cost|saving|billing|credit|dbu|spend|revenue|price|rate|pricing|
  invoice`) at a word / segment / extension boundary. Catches PR E.
  Word-boundary regex prevents false positives on incidental
  substrings (`broadcaster` ≠ `caste`, `precast` ≠ `cast`).

Regression tests (11 new, all green):
- PR D shapes (data_tests, constraints, unique_combination_of_columns)
  all promote to full with `mart-API surface` context.
- PR E shape (mrt_jobs_cost_savings.sql) + 7 other FinOps keyword
  variants all promote to full.
- FinOps false-positive guard: `broadcaster` / `precast` stay lite.
- Description-only edits under models/marts/ still trivial
  (pre-existing behavior preserved).
- Comment lines and description strings mentioning
  `data_tests:` / `constraints:` do NOT promote (regex tightness).
- Nested-indent YAML key position (production shape) still fires.

Codex-reviewed diff. Two highs addressed:
- Regex tightening to avoid comment / description-string false positives
- FileChangeClass consumer audit (no external constructions; safe)

Ship criteria (from plan v2): PRs D and E from the corpus must no
longer auto-approve. Baseline recall on the existing 13-scenario
corpus must not regress. Both hold: 96/96 tests pass (85 pre-existing
+ 11 new); full altimate review suite 3781/3781 green.

Depends on PR #1027 (feat/review-r18-observability-recall) — this branch
stacks on top so tierReasons[] wiring is in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* fix(review): [R20 S4] address consensus-review findings on triage promotion

Bundle addresses PR #1028's consensus review:

- MAJOR #1 — legacy `tests:` (pre-dbt-1.8 alias) added to risk-key
  patterns so `models/marts/*.yml` diffs adding `tests: [not_null]` or
  similar promote out of trivial. Regression tests cover both marts
  and non-marts placement + a word-boundary FP guard.

- MAJOR #3 — vacuous `unique_combination_of_columns` regression test
  fixed. Original path (`mrt_billing_account_prices.yml`) also matched
  FinOps + marts signals so the DBT_UNIQUE_COMBO_RE could have been
  deleted and the test still passed. Path moved to
  `models/intermediate/int_grain.yml` (non-mart, non-FinOps) and
  reason string asserted to confirm the combo regex is the sole
  triggering signal.

- MINOR #2 — dropped `dbus` from FINOPS_TOKEN_RE (D-Bus IPC collision
  with `stg_dbus_connector.sql`). Singular `dbu` is sufficient.

- MINOR #4 — each risk-YAML key now has its own regex in a
  DBT_RISK_KEY_PATTERNS table. `dbtRiskYmlKeyMatches()` returns the
  specific keys that matched; new `dbtRiskYmlKeys: string[]` field on
  FileChangeClass. Reason string now names the exact triggering keys
  (e.g. "schema.yml diff touches tests under models/marts/") rather
  than the concatenated umbrella.

- MINOR #5 — reworded the "upgrades the WEIGHT" comment. There is no
  weighting/ordering effect; `martLayerChange` only enriches the reason
  string with the mart-API-surface context.

- MINOR #6 — block-scalar body FP: `stripBlockScalars()` walks the diff
  tracking block-scalar state so a description or long-form comment
  containing `data_tests:` (or similar) doesn't spuriously promote.
  Codex round-6 review HIGH fixed too — earlier version worked only
  on the +/- slice, missing the common case where `description: |`
  is in the context and a changed line is inside its body. Now walks
  the full diff (context + changed) for state and only masks output on
  changed lines.

- MINOR #7 — FinOps boundary class extended with `\d` so digit-suffixed
  paths (`mrt_cost2024.sql`, `stg_billing2.sql`, `dbu1_usage.sql`)
  match.

- NIT #8 — DBT_UNIQUE_COMBO_RE relaxed to make the list-item marker
  optional, so the bare-key indented form
  (`unique_combination_of_columns:` under a short-form `tests:` map)
  also matches.

- NIT #9 — added regression test for removed-line risk-key promotion
  (removing a `data_tests:` block is at least as risk-worthy as
  adding one).

- NIT #10 — comment reworded from `unique_combination_of_columns` is
  a "TEST NAME" → "dbt test macro / test parameter".

Full altimate review suite: 3807 pass / 640 skip / 0 fail (was 3781
baseline — 26 new tests).

Codex-round-6 minor addressed: `dbtRiskYmlKeyMatches` is now called
once via a shared `scannedForRisk` local and its result reused for
both `dbtRiskYmlChanges` and `dbtRiskYmlKeys`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* fix(review): [R20 S4] address kilo-code-bot review — blockScalarStart regex tightened

kilo-code-bot suggestion on PR #1028 — the earlier `blockScalarStart`
regex accepted only `|`/`>` with an optional `+`/`-` chomping
indicator, so YAML block-scalar headers carrying an explicit
indentation indicator (`description: |2`, `|+2`, `|2-`) or a
trailing comment (`description: | # legacy`) failed to open a
scalar. Body lines beginning with a risk keyword (`data_tests:`,
`constraints:`, …) then false-positive promoted the file to `full`.
Safe-direction over-tiering (never a wrong verdict), but this regex
is the sole scalar gate — widen it to close the gap.

Regex now: `[|>](?:[+-]?[1-9]?|[1-9]?[+-]?)[ \\t]*(?:#.*)?$`.
- `[|>]` opener
- `(?:[+-]?[1-9]?|[1-9]?[+-]?)` chomp+indent in either order
- optional trailing `# comment`

New regression test loops over `|2`, `|+2`, and `| # legacy comment`
opener shapes and asserts each produces `trivial` on a diff whose
body lines contain risk keywords.

Full altimate review suite: 3808 pass / 640 skip / 0 fail (was 3807
baseline — 1 new test).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* fix(review): [R20 S4] context-line indent measurement in changedLinesForScan (cubic P2)

cubic P2 bot review — the scalar-state tracker measured indent
inconsistently between context and changed diff lines. Context lines
start with a leading SPACE (the diff marker) and my earlier code
left it in place; changed lines had their `+`/`-` marker stripped
before indent measurement. Result: a valid `|1` block scalar opened
in the context with a body indented exactly one space beyond the
header wasn't masked, because the context-side `scalarIndent`
counted one extra space and the changed-side body's indent then
failed the `indent > scalarIndent` check.

Fix strips the leading diff marker (`+`/`-`/space) on ALL lines
before indent measurement, so context and changed lines live in the
same coordinate system.

New regression test: `|1` block scalar opened in context with body
containing `data_tests:` / `constraints:` prose stays trivial.

Full altimate review suite: 3808 pass / 640 skip / 0 fail (was 3807
baseline — 1 new test).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* refactor(review): [R20 S4] move FinOps token list from reviewer core to `riskTierPathTokens` config

The hardcoded `FINOPS_TOKEN_RE` in `risk-tier.ts` baked one team's
billing/cost vocabulary into a reviewer everyone consumes. This moves
the list out of core to user-configurable `.altimate/review.yml`,
keeps the shipped list as an opt-in `preset:finops`, and generalizes
the field so custom categories (pci, patient, etc.) work the same way.

Also refactors the vacuous FP-guard test flagged by `altimate-harness-bot`
(review.test.ts:755-768): the earlier `broadcaster` / `precast_table`
paths did not contain any FinOps token substring, so the boundary anchors
in the regex were never exercised — the test would pass even if the
boundary class were deleted. Replaced with property-based loops over
`RISK_TOKEN_PRESETS.finops` that assert boundary behavior for every
preset token: `stg_{tok}_daily.sql` fires, `stg_x{tok}y.sql` does not.
Adding a preset token extends coverage automatically.

Core changes:
- `config.ts` — new `riskTierPathTokens: Record<string, string[]>`,
  default `{}`. Users opt in by naming a category and listing tokens
  or `preset:<name>` markers.
- `risk-tier.ts` — dropped `FINOPS_TOKEN_RE`. `FileChangeClass.finopsPathToken:
  boolean` → `.highRiskPathTokenCategory: string | undefined`. Reason string
  reads `path matches high-risk token category '<category>'`. Added
  `RISK_TOKEN_PRESETS` (shipped presets) and `compilePathTokenResolver`
  (compiles config into a resolver, boundary-anchored, handles preset
  expansion).
- `orchestrate.ts` — threads `config.riskTierPathTokens` through
  `compilePathTokenResolver` into `classifyPR` as the
  `pathTokenCategoryOf` callback.
- Comments genericized: dropped internal round / corpus / vertical-
  specific vocabulary ("PR D / PR E", "DBU savings > DBU cost",
  "misanchored billing units") in favor of neutral wording.

Backwards compatibility: projects that were relying on hardcoded FinOps
promotion should add `riskTierPathTokens: {finops: [preset:finops]}` to
their `.altimate/review.yml`. A project that never wanted FinOps
promotion (majority case) now gets no path-token promotion at all — the
reviewer core is neutral.

Tests: 229/229 green (review*.test.ts). Includes property-based loops
over the preset for both boundary-firing and interior-substring
suppression, plus a "no resolver configured → no promotion" test that
locks in the neutral-core guarantee.

* fix(review): [R20 S4] reject empty-string tokens in riskTierPathTokens (cubic-review P2)

An empty string in `riskTierPathTokens.<category>` compiles into a
regex alternative like `(?:|foo)` — the empty branch matches the
empty string between two adjacent boundary chars, so any path with
two `_`/`.`/`-`/`/`/digit chars in a row (e.g. `stg__orders.sql`)
silently over-promotes on any configured category. Reject at the
config-schema layer via `z.string().min(1)`; the resolver itself is
unchanged.

Test: `riskTierPathTokens rejects empty-string tokens` under
`describe("config")` in review.test.ts — asserts throw on empty
token, non-empty tokens still parse.

230/230 review-* tests pass.

* fix(review): [R20 S4] tighten changedLinesForScan marker guard to only emit +/- lines (harness-bot P2)

altimate-harness-bot review, PR #1028 risk-tier.ts:251. The earlier
guard `if (marker === " ") continue` skipped only unified-diff
context lines (space-prefixed). Free-form lines from a raw
`git diff -p` output — `diff --git a/... b/...`, `index abc..def`,
`Author:`, `Date:` — carry a `""` marker (they don't start with `+`,
`-`, or space), fall past the guard, and reach `dbtRiskYmlKeyMatches`
via `out.push(raw)`.

In-production `file.diff` comes from the GitHub PR files API, whose
per-file diff blocks begin at the first `@@` hunk header (no
`diff --git` / `index` / commit-metadata lines). So the exposure was
defensive parity, not a live promotion bug. But the code was
asymmetric — `+++`/`---`/`@@` headers were explicitly skipped at
line 220; other free-form lines weren't — and a future caller
passing raw `git diff -p` output would silently see header lines
in the scanner.

Fix (one operator): `if (marker !== "+" && marker !== "-") continue`.
Any line whose leading character isn't `+` or `-` is either a
context line (fine, still updated scalar state above) or a
free-form header (skipped, no emit).

Test: `R20 S4: raw git diff -p free-form headers do NOT reach the
scanner` builds a full raw-diff string (four header lines + the
`@@` hunk + one changed line) and asserts the same tier verdict as
the bare-`tests`-word test — trivial, no promotion.

231/231 review-* tests pass.

* fix(review): [R20 S4] fail loud on unknown `preset:<name>` in riskTierPathTokens (cubic + harness-bot P2)

Two bots on PR #1028 flagged the same silent-failure: a typo in a
`preset:<name>` entry (e.g. `preset:finop` instead of `preset:finops`)
compiled to an empty token list; then `if (tokens.length === 0)
continue` dropped the category with no error. The user's opt-in for a
risk-promotion category quietly did nothing — exactly the safety
invariant this PR is meant to guarantee.

Fix: `compilePathTokenResolver` now throws on an unknown preset name
before assembling the resolver. Error message names the offending
category, the typo, and the shipped preset list so the reader has a
fix pointer:

  Error: riskTierPathTokens.finops: unknown preset 'finop'.
  Known presets: finops. Configure a bare token list (e.g.
  ['card', 'pan']) instead if you want a custom category.

The shipped preset list is small and stable, so an unknown name is
almost certainly a typo, not a forward reference. Fail at review-start
means the misconfiguration is caught immediately, not discovered when
a real high-risk PR slips through.

Tests: `unknown preset name throws with the known-list in the message`
asserts throw on `preset:finop`, throw when a typo is mixed with valid
tokens, and that the known preset (`preset:finops`) still resolves
promotion correctly on `mrt_cost_daily.sql`.

111/111 tests in review.test.ts pass.

* fix(review): [R20 S4] catch riskTierPathTokens config error in orchestrate — don't crash the run (cubic P2)

Follow-up to `919bcc17f`. That commit made `compilePathTokenResolver`
throw on an unknown `preset:<name>` — the right safety fix — but
nothing between the resolver and the CLI handler caught the throw, so
a single typo in `.altimate/review.yml` (e.g. `preset:finop` instead of
`preset:finops`) would crash every review run for the project's CI
(cubic-review P2 on PR #1028 risk-tier.ts:134).

Fix: wrap the `compilePathTokenResolver` call in `orchestrate.ts` in
a try/catch. On throw:

1. Log a prominent warning to stderr with the config-error message
   and a fallback banner ("Review continuing without path-token
   promotion") so the CI log tells the operator what happened.
2. Fall back to `pathTokenCategoryOf = undefined` so `classifyPR`
   still runs — no promotion fires, but the rest of the review
   proceeds normally.
3. Prepend the config error to `tierResult.reasons` so it surfaces
   in `envelope.tierReasons` when `explainTier` is set. A reader of
   the envelope (or a PR-comment renderer) sees WHY their opt-in
   didn't fire without having to dig through CI logs.

The trade-off is deliberate: the user's opt-in is dead until they fix
the typo (conservative safe fallback — no silent auto-approve on
billing/PCI/etc. paths), but the review itself doesn't die.

Test: `runReview does NOT crash on an unknown-preset config typo —
degrades gracefully` builds a minimal fake ReviewRunner + config with
`preset:finop` typo, captures stderr, asserts (a) envelope is
produced (no crash), (b) stderr contains both the config-error
message and the fallback banner, (c) `env.tierReasons` carries the
same error so envelope readers see it too.

112/112 tests in review.test.ts pass.

* fix(review): [R20 S4] surface pathTokenConfigError in envelope even without --explain-tier (coderabbit review)

Follow-up to `9435bc6f9` (the try/catch around `compilePathTokenResolver`
that surfaces a config typo in `tierResult.reasons` instead of crashing).
The envelope's `tierReasons` field was gated on
`input.explainTier || tierForced` — both default false in a normal
`comment`/`gate` run — so the config error I prepended to
`tierResult.reasons` was silently stripped when the envelope was built.

Net effect on a typoed `.altimate/review.yml`: review ran, no promotion
fired, no error appeared in the PR comment. Only a stderr trace no
customer ever sees. Exact silent-failure mode the fail-loud fix was
supposed to close.

Fix: expand the gate to include the config-error case.

  tierReasons: input.explainTier || tierForced || pathTokenConfigError
               ? tierReasons
               : undefined

Regression test: `config error surfaces in envelope even WITHOUT
--explain-tier` builds a config with `finops: [preset:finop]`, calls
`runReview` with `explainTier` unset (default), and asserts
`env.tierReasons` still contains both `riskTierPathTokens config
invalid` and `unknown preset 'finop'`.

113/113 tests in review.test.ts pass (was 112).

---------

Co-authored-by: Haider <haider@altimate.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
sahrizvi pushed a commit that referenced this pull request Jul 23, 2026
For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of
sahrizvi added a commit that referenced this pull request Jul 23, 2026
…findings on 5-PR corpus) (#1029)

* feat(review): [R20 S1] grain-key `not_null` completeness detector

For every column named in a `dbt_utils.unique_combination_of_columns`
test's `combination_of_columns`, require `not_null` coverage on the
same model. Otherwise a NULL grain-key value silently passes the
uniqueness test — the guardrail is toothless. Cited as a hard rule in
`docs/DBT_GUIDELINES.md` in the corpus repo; kilo catches it, we didn't.

Coverage sources:
- `constraints: [{type: not_null}]` — only counted when
  `contract.enforced == true` (per codex R20 S1 high #3). On views /
  non-contracted models, `constraints:` is documentation-only, not
  enforced, so we don't miss a real gap.
- Column-level `data_tests: [not_null]` / `tests: [not_null]` — always
  counted; dbt's test runner enforces regardless of contract state.

Recommendation flips between `constraints:` (contracted model) and
`data_tests:` (view / non-contracted) based on the model's contract
state — matches the adapter-semantics discussion in the corpus study
(PR D×2 + PR A×2).

Supported YAML shapes:
- `unique_combination_of_columns` and `dbt_utils.unique_combination_of_columns`
  (exact match per codex high #1; `endsWith` would over-match).
- pre-1.9 flat args + dbt 1.9+ `arguments:` nesting.
- top-level `contract:` and nested `config.contract:` for enforcement flag.

False-positive guards:
- Column-name comparison is case-folded (Snowflake identifiers, per
  codex high #2) so `WORKSPACE_ID` in `combination_of_columns` matches
  `workspace_id` in `columns:`.
- Skipped on `status === "deleted"` files (no current grain to guard).
- Only runs on files in the PR diff, not repo-wide.

### Validation on 5-PR internal corpus (recall improvement)

vs S4 baseline (the tier-promotion PR this branch stacks on):
| Variant | S4 baseline | S1 result | Delta |
|---|---:|---:|---:|
| A1 (`--pure --no-ai=true`) | 118 | 129 | **+11 grain-key gaps** |
| A2 (`--pure`, LLM lane) | 143 | 150 | +7 (net; LLM run-to-run noise) |

Grain-key gaps caught:
- PR B (#1111) ×1: `mrt_all_purpose_auto_tune_recommendation.rec_type`
- PR C (#862) ×10: workspace_id x3, job_id x2, period_start_time x2, period_end_time x2, task_key x1
- PR A, D, E: 0 (fixes already landed in HEAD state per corpus study)

Strong matches to human blocker findings on PR C:
- `int_job_run_billing.workspace_id`, `int_job_run_cost_carrier.workspace_id`,
  `int_job_task_run_cost_carrier.workspace_id` → PR C human F11 (critical:
  "workspace_id missing from carrier identity")
- `mrt_job_run_timeline.period_end_time`, `mrt_job_task_run_timeline.period_end_time`
  → PR C human F10 (critical: "dbt mart grain includes period_end_time")

### Tests

- 71 pass / 0 fail in `review-dbt-patterns.test.ts` (58 pre-existing + 13 new S1 tests).
- 3797 pass / 640 skip / 0 fail in the full altimate review suite (132 files).
- Codex-reviewed diff, 3 highs addressed:
  - endsWith → exact-name match
  - column-name case-folding
  - constraints only count when contract enforced
- Codex minor #4 addressed (test fixture for top-level `contract:`)
- Codex minor #5 addressed (test fixture for non-contracted constraints ≠ coverage)

Depends on PR #1028 (feat/review-r20-s4-triage-promotion), which in turn
stacks on PR #1027 (feat/review-r18-observability-recall).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* fix(review): [R20 S1] address consensus-review findings on grain-key detector

Bundle addresses PR #1029's consensus review:

- MAJOR #1 — model-level and column-level primary_key / not_null
  constraints now count as coverage. dbt 1.5+ supports model-level
  constraints via `constraints: [{type: primary_key, columns: [a, b]}]`,
  and a primary_key inherently enforces NOT NULL on Postgres/Snowflake/
  BigQuery/Databricks. Grain columns declared via a model-level PK were
  previously falsely flagged as missing not_null. Fix scans both
  `mm.constraints` (model-level, honouring the `columns:` list) and
  column-level `constraints: [{type: primary_key}]`.

- MINOR #3 — contract-precedence bug. Earlier ternary short-circuited
  when `cfg.contract` was any object (e.g. `config: {contract:
  {alias: X}}` with no `enforced` key), masking a top-level
  `contract: {enforced: true}`. Now evaluated independently at both
  locations and OR'd.

- MINOR #4 — `dbt.` prefix on namespaced test names (`dbt.not_null` in
  dbt 1.8+) is stripped in `testName()` before matching, so it counts
  as coverage.

- MINOR #6 — `{name: <alias>, test_name: not_null}` alternative object
  form is recognised. Reading `Object.keys(t)[0]` returned `name`
  (the alias) rather than the underlying test type. Now `test_name`
  wins when present, falling back to the first key.

- NIT #7 — `norm()` hoisted from per-model to function scope.

Consensus items NOT addressed this round:
- NIT #8 (dedup GrainKeyGap for a column listed twice / multiple
  grain tests) — collapses downstream via the global finding
  fingerprint; cosmetic rather than correctness.
- NIT #9 (model-level `data_tests:` scanned as a grain-test source)
  — reviewer noted "harmless (no false match)"; no action.
- NIT #10 (documentation of fallback-skip) — no code change needed.
- MINOR #5 (contract resolved from dbt_project.yml or SQL config)
  — cross-file / cross-context, deferred.

Regression tests (all pass; 82 pass / 0 fail in review-dbt-patterns
suite, 3828 pass / 0 fail in full altimate suite — up from 3785):

- Model-level primary_key constraint covers grain columns
- Model-level not_null constraint with `columns:` list covers named cols
- Column-level primary_key counts as coverage
- Precision guard: PK missing cols still flagged
- Model-level constraints on non-contracted model don't count
- `config.contract` without `enforced` does NOT mask top-level
  `contract: {enforced: true}` (MINOR #3)
- `dbt.not_null` covers (MINOR #4)
- `{name:, test_name: not_null}` alternative form covers (MINOR #6)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of

* chore(review): [R20 S1] genericize internal-vocab leaks in dbt-patterns comments

Companion cleanup to PR #1028's FinOps strip. The grain-key detector's
docstrings named a specific internal column (`workspace_id`) as the
Snowflake case-folding example and referenced internal corpus PR labels
(`PR D×2, PR A×2`). Neither carries product meaning outside the
altimate-ingestion codebase.

- `dbt-patterns.ts:962` — replace `WORKSPACE_ID` / `workspace_id` example
  with `ORDER_ID` / `order_id`. Same illustrative point, no internal name.
- `dbt-patterns.ts:1443` — replace `(PR D×2, PR A×2)` corpus-label
  citation with `(four instances across the sample)`. Same count, no
  internal reference.

Tests: 250/250 green.

* fix(review): [R20 S1] address altimate-harness-bot review findings on grain-key detector

Two substantive findings on PR #1029:

1. `dbt-patterns.ts:1099` — grain detector was not change-scoped.
   `extractGrainKeyGaps(newDoc)` ran against every entity in the file,
   so a housekeeping edit (description bump, meta tag) surfaced all
   pre-existing grain gaps on unrelated models. Real precision cost:
   reviewers seeing findings on a PR they didn't intend suppress the
   whole rule.

   Fix: compare the `combination_of_columns` set per entity between
   `oldDoc` and `newDoc`; only emit gaps for entities whose grain
   declaration actually changed (added, removed, or column set diff).
   Added entities on the new side count as changed; the "added file"
   case (no oldDoc) unconditionally treats every entity as changed
   so newly-shipped grain declarations are still guarded.

   New helpers: `extractGrainDeclarations` returns
   `Map<entityName, Set<column>>` and `grainDeclChangedEntities` diffs
   two docs into a set of entity names whose declaration moved.
   Existing test that pinned the old steady-state-fires behavior
   (`R20 S1: unique_combination_of_columns with grain col missing
   not_null → warning finding`) rewritten to test the newly-added
   case; added a companion test that pins the new precision
   guarantee (`steady-state grain gap on unchanged model is NOT
   re-surfaced`), plus a `grain declaration changed (column added to
   combination_of_columns) does fire gap` test that keeps the
   regression case covered.

2. `dbt-patterns.ts:963` — `extractGrainKeyGaps` iterated only
   `d.models`, silently skipping `snapshots`, `sources`, and `seeds`.
   `unique_combination_of_columns` on a snapshot is a real SCD-2
   grain declaration; sources declare per-table `columns:` + tests;
   seeds carry the model shape.

   Fix: new `iterateGrainEntities(d)` helper walks all four sections
   (mirrors `extractTestOccurrences`). Sources descend one level to
   iterate their `tables[]` entries which carry the model shape.
   Three new tests: `grain detector also covers snapshots`,
   `... source tables`, `... seeds`.

Tests: 255/255 green (8 review-* files, +6 new grain-key tests).

* fix(review): [R20 S1] qualify source-table entity names as `<source>.<table>` (cubic-review P2)

Two sources both containing a table named `orders` (e.g. `raw.orders`
and `legacy.orders`) previously conflated in `iterateGrainEntities`:
- Grain-change detection collapsed them into a single map entry, so a
  change in one source's grain declaration could surface or suppress
  gaps for the other.
- Finding fingerprint uses the entity name; two distinct source tables
  with the same table name would dedupe to a single finding.

Fix: `iterateGrainEntities` now returns `{name, body}` pairs. Source
tables are qualified as `${sourceName}.${tableName}` while models,
snapshots, and seeds retain their unqualified name (they live in a
flat namespace already).

Tests: `grain detector also covers source tables` updated to assert
the qualified name; new test `same source-table name in two sources
does NOT conflate` locks in the fix.

257/257 review-* tests pass.

---------

Co-authored-by: Haider <haider@altimate.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jul 23, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants