From 23bdf214abb8e3d60ed85c62ecbce47d433064fa Mon Sep 17 00:00:00 2001 From: Haider Date: Wed, 22 Jul 2026 03:27:24 +0530 Subject: [PATCH 1/5] feat(review): [R20 S1] grain-key `not_null` completeness detector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of --- .../src/altimate/review/dbt-patterns.ts | 213 ++++++++++- .../test/altimate/review-dbt-patterns.test.ts | 330 ++++++++++++++++++ 2 files changed, 542 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/altimate/review/dbt-patterns.ts b/packages/opencode/src/altimate/review/dbt-patterns.ts index 1af4fd588b..4bfbe8d842 100644 --- a/packages/opencode/src/altimate/review/dbt-patterns.ts +++ b/packages/opencode/src/altimate/review/dbt-patterns.ts @@ -922,6 +922,162 @@ function extractTestOccurrences(doc: unknown): Set { return out } +/** + * R20 S1 — grain-key `not_null` completeness. + * + * A `dbt_utils.unique_combination_of_columns` test names N columns as the + * declared grain. If any grain column lacks `not_null` coverage, a NULL + * grain-key silently passes the uniqueness test — the guardrail is toothless. + * `DBT_GUIDELINES.md` in the corpus repo states this as a hard rule; kilo + * catches it, we didn't. + * + * Coverage sources (either counts): + * - `constraints: [{type: not_null}]` on the column — enforced by the + * database when the model has `contract: {enforced: true}` on Trino / + * Databricks-with-contracts / Postgres / Snowflake. + * - `data_tests: [not_null]` / `tests: [not_null]` on the column — + * enforced by dbt test runner regardless of contract. + * + * Returns one gap per uncovered column per grain declaration. + */ +export interface GrainKeyGap { + /** Model / entity name. */ + model: string + /** Column name in the uncovered `combination_of_columns`. */ + column: string + /** True when `config.contract.enforced == true` — coverage should be a + * `constraints:` entry rather than a `data_tests:` entry, since the + * constraint enforces at write-time and the test enforces at CI-time. */ + contractEnforced: boolean +} + +function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { + const gaps: GrainKeyGap[] = [] + if (!doc || typeof doc !== "object") return gaps + const d = doc as Record + if (!Array.isArray(d.models)) return gaps + + // Small local helper: pull the string test-name from a YAML test entry. + // Bare form: `- not_null`, block form: `- not_null: {config: {severity: warn}}`. + const testName = (t: unknown): string | undefined => { + if (typeof t === "string") return t.toLowerCase() + if (t && typeof t === "object") { + const k = Object.keys(t as Record)[0] + return k ? k.toLowerCase() : undefined + } + return undefined + } + + for (const m of d.models) { + if (!m || typeof m !== "object") continue + const mm = m as Record + const mname = typeof mm.name === "string" ? mm.name : undefined + if (!mname) continue + + // Contract enforcement: either at model-level `config.contract.enforced` + // OR at top-level `contract:` (dbt supports both shapes). + const cfg = mm.config && typeof mm.config === "object" ? (mm.config as Record) : {} + const contractCfg = + cfg.contract && typeof cfg.contract === "object" + ? (cfg.contract as Record) + : mm.contract && typeof mm.contract === "object" + ? (mm.contract as Record) + : {} + const contractEnforced = contractCfg.enforced === true + + // Normalize column names for coverage comparison. dbt YAML often uses + // adapter-cased column names (Snowflake folds unquoted identifiers to + // uppercase; other adapters differ). Lowercase both sides so + // `WORKSPACE_ID` in `combination_of_columns` matches `workspace_id` + // in `columns:`. Original spelling is preserved for display in findings + // (populated below from the grain-combo entry). + const norm = (s: string): string => s.toLowerCase() + + // Coverage per column, split by mechanism (per codex R20 S1 review): + // - constraint coverage counts ONLY when `contract.enforced == true`. + // On non-contracted models, `constraints: [{type: not_null}]` is + // documentation-only and not enforced by the database. Requiring + // contract-enforced means we don't miss a real gap on views. + // - test coverage (`tests:` / `data_tests: [not_null]`) always counts, + // since dbt's test runner enforces it independent of contract state. + const coveredByConstraint = new Set() + const coveredByTest = new Set() + if (Array.isArray(mm.columns)) { + for (const col of mm.columns) { + if (!col || typeof col !== "object") continue + const c = col as Record + const cname = typeof c.name === "string" ? c.name : undefined + if (!cname) continue + const key = norm(cname) + if (Array.isArray(c.constraints)) { + for (const cn of c.constraints) { + if (cn && typeof cn === "object") { + const type = (cn as Record).type + if (typeof type === "string" && type.toLowerCase() === "not_null") { + coveredByConstraint.add(key) + } + } + } + } + for (const testsKey of ["tests", "data_tests"] as const) { + const tests = c[testsKey] + if (!Array.isArray(tests)) continue + for (const t of tests) { + if (testName(t) === "not_null") coveredByTest.add(key) + } + } + } + } + const hasCoverage = (col: string): boolean => { + const k = norm(col) + return coveredByTest.has(k) || (contractEnforced && coveredByConstraint.has(k)) + } + + // Find grain declarations in this model's model-level tests / data_tests. + // Restrict to `unique_combination_of_columns` and + // `dbt_utils.unique_combination_of_columns` exactly (per codex R20 S1 + // review) — `endsWith` would over-match `not_unique_combination_of_columns` + // and third-party macros that share the suffix. + // Supports both dbt shapes: + // pre-1.9: `- dbt_utils.unique_combination_of_columns: {combination_of_columns: [...]}` + // 1.9+: `- dbt_utils.unique_combination_of_columns: {arguments: {combination_of_columns: [...]}}` + const isGrainTestName = (name: string): boolean => { + const n = name.toLowerCase() + return n === "unique_combination_of_columns" || n === "dbt_utils.unique_combination_of_columns" + } + for (const testsKey of ["tests", "data_tests"] as const) { + const tests = mm[testsKey] + if (!Array.isArray(tests)) continue + for (const t of tests) { + if (!t || typeof t !== "object") continue + const entry = t as Record + for (const k of Object.keys(entry)) { + if (!isGrainTestName(k)) continue + const args = entry[k] + if (!args || typeof args !== "object") continue + const argsObj = args as Record + const nested = + argsObj.arguments && typeof argsObj.arguments === "object" + ? (argsObj.arguments as Record) + : undefined + const combo = + (nested?.combination_of_columns as unknown) ?? (argsObj.combination_of_columns as unknown) + if (!Array.isArray(combo)) continue + for (const col of combo) { + if (typeof col !== "string") continue + if (!hasCoverage(col)) { + // Preserve original grain-column spelling in the finding. + gaps.push({ model: mname, column: col, contractEnforced }) + } + } + } + } + } + } + + return gaps +} + /** * Fallback removed-test detector for callers that supply only the diff * (no old/new content). Kept intentionally conservative: matches removed @@ -985,6 +1141,8 @@ export function detectSchemaYmlPatterns( testTag?: string }> = [] let usedStructural = false + // R20 S1 — populated from the structural NEW-side parse below. + let grainGaps: GrainKeyGap[] = [] // Deleting a whole schema.yml removes every test declared in it — arguably // a bigger removal than dropping a single test. Treat the new side as empty @@ -1069,6 +1227,16 @@ export function detectSchemaYmlPatterns( removals.push({ model, column, test, testTag }) } usedStructural = true + // R20 S1 — grain-key `not_null` completeness. Only fire on + // non-deleted files: a deleted schema.yml has no current grain to + // guard. Extract from the NEW side so we flag the current state of + // the file, whether the grain declaration was newly added in this + // PR or pre-existing (a broken grain-guard is a real risk either + // way, and reviewers can suppress if the pre-existing case is by + // design). Uses the `not_null-missing` gap emitted below. + if (!isDeletedFile && newDoc !== undefined) { + grainGaps = extractGrainKeyGaps(newDoc) + } } } @@ -1100,7 +1268,7 @@ export function detectSchemaYmlPatterns( } } - if (!removals.length) return [] + if (!removals.length && !grainGaps.length) return [] const findings: Finding[] = [] const isMartLayer = /(^|\/)(marts?|reporting)\//.test(file.path) @@ -1220,5 +1388,48 @@ export function detectSchemaYmlPatterns( }), ) } + + // R20 S1 — grain-key `not_null` completeness. For every column in a + // `unique_combination_of_columns` test's `combination_of_columns` that + // lacks `not_null` coverage on the same model, emit one finding. + // Recommendation flips between `constraints:` (contracted model) and + // `data_tests:` (view / non-contracted model) based on the model's + // contract state — matches the adapter-semantics discussion in the + // corpus study (PR D×2, PR A×2). + for (const g of grainGaps) { + const filename = file.path.split("/").pop() + const recommendation = g.contractEnforced + ? `Add \`constraints: [{type: not_null}]\` to \`${g.column}\` on \`${g.model}\` (contract is \`enforced: true\` so the constraint is enforced at write-time).` + : `Add \`not_null\` to \`${g.column}\`'s \`data_tests:\` on \`${g.model}\` (contract is not enforced, so a \`constraints:\` entry would be inert — use a data_test).` + findings.push( + makeFinding({ + severity: clampSeverity("test_coverage", "warning", "high"), + category: "test_coverage", + title: `${filename}: grain column \`${g.column}\` in unique_combination_of_columns lacks \`not_null\` on \`${g.model}\``, + body: + `The \`unique_combination_of_columns\` test on \`${g.model}\` names \`${g.column}\` as a grain key, but no \`not_null\` ` + + `coverage is declared for it (either as a \`constraints:\` entry on a contracted model, or as a \`data_tests: [not_null]\` ` + + `on a view). A NULL grain-key value silently passes the uniqueness test, so a fan-out or duplicate bug can ship without ` + + `any test catching it. ${recommendation}`, + file: file.path, + model: g.model, + column: g.column, + confidence: "high", + evidence: { + tool: "dbt-patterns", + result: { + rule: "grain_key_not_null_missing", + model: g.model, + column: g.column, + contractEnforced: g.contractEnforced, + }, + }, + // Per-column ruleKey so the global fingerprint dedupe keeps distinct + // grain-column gaps on the same model as separate findings. + ruleKey: `test_coverage:grain-key-not-null:${g.model}.${g.column}`, + }), + ) + } + return findings.filter((x) => !exclusionReason(x, rubric)) } diff --git a/packages/opencode/test/altimate/review-dbt-patterns.test.ts b/packages/opencode/test/altimate/review-dbt-patterns.test.ts index 5095863092..f05cf40616 100644 --- a/packages/opencode/test/altimate/review-dbt-patterns.test.ts +++ b/packages/opencode/test/altimate/review-dbt-patterns.test.ts @@ -783,6 +783,336 @@ models: [] expect(f.length).toBe(0) }) + // R20 S1 — grain-key `not_null` completeness. Every column named in a + // `unique_combination_of_columns` test's `combination_of_columns` must have + // `not_null` coverage on the same model (constraint if contracted, data_test + // if view). Directly targets PR D×2 + PR A×2 human findings in the R20 + // corpus study; explicit rule in DBT_GUIDELINES.md. + test("R20 S1: unique_combination_of_columns with grain col missing not_null → warning finding", () => { + // `price_start_time` is grain but has no not_null coverage → gap. + // `metastore_id` / `sku_name` have not_null via constraints on the + // contracted model → covered. + const newContent = `version: 2 +models: + - name: mrt_billing_account_prices + config: + contract: + enforced: true + columns: + - name: metastore_id + constraints: + - type: not_null + - name: sku_name + constraints: + - type: not_null + - name: price_start_time + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - metastore_id + - sku_name + - price_start_time +` + const oldContent = newContent // steady-state (grain already present) — still fires + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_billing_account_prices.yml", status: "modified", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent, newContent }, + ) + const gap = f.find((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gap).toBeDefined() + expect(gap!.severity).toBe("warning") + expect(gap!.model).toBe("mrt_billing_account_prices") + expect(gap!.column).toBe("price_start_time") + // Contract is enforced → recommendation should point at `constraints:`. + expect(gap!.body).toContain("constraints: [{type: not_null}]") + // Non-gap columns must not appear as findings. + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(1) + }) + + test("R20 S1: non-contracted (view) model recommends data_tests: not_null", () => { + const newContent = `version: 2 +models: + - name: stg_billing + columns: + - name: metastore_id + data_tests: + - not_null + - name: sku_name + data_tests: + - not_null + - name: price_start_time # ← no not_null on this grain col + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - metastore_id + - sku_name + - price_start_time +` + const f = detectSchemaYmlPatterns( + { path: "models/staging/stg_billing.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gap = f.find((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gap).toBeDefined() + // Non-contracted model → recommendation should point at `data_tests:`. + expect(gap!.body).toContain("data_tests:") + expect(gap!.body).not.toContain("constraints: [{type: not_null}]") + }) + + test("R20 S1: grain col covered by column-level tests: [not_null] (dbt <1.8 alias) is not a gap", () => { + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + tests: + - not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: SCD2-style grain (change_time) missing not_null flagged (PR D F4 shape)", () => { + const newContent = `version: 2 +models: + - name: mrt_job_tasks_inventory + config: + contract: + enforced: true + columns: + - name: metastore_id + constraints: [{type: not_null}] + - name: job_id + constraints: [{type: not_null}] + - name: task_key + constraints: [{type: not_null}] + - name: change_time # ← temporal grain, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - metastore_id + - job_id + - task_key + - change_time +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_job_tasks_inventory.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("change_time") + }) + + test("R20 S1: no false positive when every grain col has not_null coverage", () => { + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: a + data_tests: [not_null] + - name: b + data_tests: [not_null] + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [a, b] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: bare unique_combination_of_columns (no `dbt_utils.` prefix) also matches", () => { + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + data_tests: + - unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(1) + }) + + test("R20 S1: non-contracted model — `constraints: [not_null]` does NOT count as coverage", () => { + // On a view / non-contracted model, `constraints:` is documentation + // only (not enforced by the DB). Only column-level `not_null` data_tests + // should count as coverage. Codex R20 S1 high #3. + const newContent = `version: 2 +models: + - name: stg_x # ← no config.contract.enforced + columns: + - name: id + constraints: + - type: not_null # ← doesn't count on non-contracted model + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/staging/stg_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].body).toContain("data_tests:") + }) + + test("R20 S1: top-level `contract: {enforced: true}` (not nested under config:) is recognised", () => { + // dbt supports declaring contract enforcement either at model.config.contract + // or at model.contract directly. Both must count as contract-enforced so + // the recommendation correctly suggests `constraints:`. + const newContent = `version: 2 +models: + - name: mrt_x + contract: + enforced: true + columns: + - name: id + constraints: [{type: not_null}] + - name: change_time # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id, change_time] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("change_time") + // Contract IS enforced (via top-level `contract:`) → recommendation should + // point at `constraints:`, not data_tests. + expect(gaps[0].body).toContain("constraints: [{type: not_null}]") + }) + + test("R20 S1: test-name match is exact, not endsWith (false-positive guard)", () => { + // `not_unique_combination_of_columns` (fictional but plausible) or a + // third-party macro ending in the same suffix must NOT trigger the rule. + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + data_tests: + - some_package.not_unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: adapter case-folding — SNOWFLAKE_ID grain col matches snowflake_id column coverage", () => { + // Snowflake folds unquoted identifiers to uppercase. If someone writes + // `combination_of_columns: [WORKSPACE_ID]` while the column is declared + // as `- name: workspace_id`, the coverage should still match. + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: workspace_id + data_tests: [not_null] + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [WORKSPACE_ID] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: dbt 1.9+ `arguments:` nesting is recognised (real corpus shape)", () => { + // The real internal corpus PRs use the dbt 1.9+ shape: + // `- dbt_utils.unique_combination_of_columns: {arguments: {combination_of_columns: [...]}}` + // Detector must recognise both nested (`arguments:`) and pre-1.9 flat forms. + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + columns: + - name: a + constraints: [{type: not_null}] + - name: b # ← missing not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + arguments: + combination_of_columns: [a, b] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("b") + }) + + test("R20 S1: does not fire when there's no unique_combination_of_columns test", () => { + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: a + - name: b +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: does not fire on deleted schema.yml (no current grain to guard)", () => { + const oldContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "deleted", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent, newContent: undefined }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + test("benign additive column produces NO dbt-pattern finding (precision)", () => { const sql = `select id, upper(status) as status_upper from {{ ref('x') }}` const f = detectModelPatterns( From 591f374c435050ceec82782fa58c07b2f832158e Mon Sep 17 00:00:00 2001 From: Haider Date: Wed, 22 Jul 2026 19:20:33 +0530 Subject: [PATCH 2/5] fix(review): [R20 S1] address consensus-review findings on grain-key detector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: , 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 Claude-Session: https://claude.ai/code/session_017zXDXMiNFh4qDPxPCfa2of --- .../src/altimate/review/dbt-patterns.ts | 89 +++++-- .../test/altimate/review-dbt-patterns.test.ts | 223 ++++++++++++++++++ 2 files changed, 290 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/altimate/review/dbt-patterns.ts b/packages/opencode/src/altimate/review/dbt-patterns.ts index 4bfbe8d842..404cdb7af6 100644 --- a/packages/opencode/src/altimate/review/dbt-patterns.ts +++ b/packages/opencode/src/altimate/review/dbt-patterns.ts @@ -957,13 +957,30 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { const d = doc as Record if (!Array.isArray(d.models)) return gaps - // Small local helper: pull the string test-name from a YAML test entry. - // Bare form: `- not_null`, block form: `- not_null: {config: {severity: warn}}`. + // Normalise column names for coverage comparison. dbt YAML often uses + // adapter-cased column names (Snowflake folds unquoted identifiers to + // uppercase; other adapters differ). Lowercase both sides so `WORKSPACE_ID` + // in `combination_of_columns` matches `workspace_id` in `columns:`. Hoisted + // to function scope per consensus NIT #7 (was redeclared per-model). + const norm = (s: string): string => s.toLowerCase() + + // Pull the string test-name from a YAML test entry. Consensus fixes: + // - MINOR #6: dbt's documented alternative object form is + // `{name: , test_name: not_null, ...}`. `Object.keys(t)[0]` + // would return `name`, missing the underlying test type. Prefer + // `test_name` when present. + // - MINOR #4: dbt 1.8+ allows namespaced names like `dbt.not_null`; + // strip a leading `dbt.` prefix so `dbt.not_null` matches `not_null`. + // Bare form: `- not_null`. Block form: `- not_null: {config: ...}`. const testName = (t: unknown): string | undefined => { - if (typeof t === "string") return t.toLowerCase() + if (typeof t === "string") return t.toLowerCase().replace(/^dbt\./, "") if (t && typeof t === "object") { - const k = Object.keys(t as Record)[0] - return k ? k.toLowerCase() : undefined + const obj = t as Record + // Alternative form: `{name: , test_name: , ...}`. + // If both `name` and `test_name` are present, `test_name` wins. + if (typeof obj.test_name === "string") return obj.test_name.toLowerCase().replace(/^dbt\./, "") + const k = Object.keys(obj)[0] + return k ? k.toLowerCase().replace(/^dbt\./, "") : undefined } return undefined } @@ -975,23 +992,18 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { if (!mname) continue // Contract enforcement: either at model-level `config.contract.enforced` - // OR at top-level `contract:` (dbt supports both shapes). + // OR at top-level `contract:` (dbt supports both shapes). Consensus + // MINOR #3 — earlier ternary short-circuited on `cfg.contract` being + // an object (e.g. `config: {contract: {alias: ...}}` with no + // `enforced` key), masking a top-level `contract: {enforced: true}`. + // Evaluate `enforced === true` at both locations independently and OR + // them so either declaration counts. const cfg = mm.config && typeof mm.config === "object" ? (mm.config as Record) : {} - const contractCfg = - cfg.contract && typeof cfg.contract === "object" - ? (cfg.contract as Record) - : mm.contract && typeof mm.contract === "object" - ? (mm.contract as Record) - : {} - const contractEnforced = contractCfg.enforced === true - - // Normalize column names for coverage comparison. dbt YAML often uses - // adapter-cased column names (Snowflake folds unquoted identifiers to - // uppercase; other adapters differ). Lowercase both sides so - // `WORKSPACE_ID` in `combination_of_columns` matches `workspace_id` - // in `columns:`. Original spelling is preserved for display in findings - // (populated below from the grain-combo entry). - const norm = (s: string): string => s.toLowerCase() + const cfgContract = + cfg.contract && typeof cfg.contract === "object" ? (cfg.contract as Record) : undefined + const topContract = + mm.contract && typeof mm.contract === "object" ? (mm.contract as Record) : undefined + const contractEnforced = cfgContract?.enforced === true || topContract?.enforced === true // Coverage per column, split by mechanism (per codex R20 S1 review): // - constraint coverage counts ONLY when `contract.enforced == true`. @@ -1000,8 +1012,38 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { // contract-enforced means we don't miss a real gap on views. // - test coverage (`tests:` / `data_tests: [not_null]`) always counts, // since dbt's test runner enforces it independent of contract state. + // + // Consensus MAJOR #1 — also count MODEL-LEVEL constraints: + // - `constraints: [{type: primary_key, columns: [a, b]}]` (dbt 1.5+) + // inherently enforces NOT NULL on every named column on + // Postgres/Snowflake/BigQuery/Databricks, so grain columns declared + // via a model-level PK are already covered. + // - `constraints: [{type: not_null, columns: [...]}]` (dbt's model- + // level form) — same coverage, just spelled out. + // - Column-level `constraints: [{type: primary_key}]` — same rationale + // at column granularity. const coveredByConstraint = new Set() const coveredByTest = new Set() + + // Model-level constraints — dbt allows a `constraints:` list under + // the model itself (not per-column) that names one or more columns. + if (Array.isArray(mm.constraints)) { + for (const cn of mm.constraints) { + if (!cn || typeof cn !== "object") continue + const cnRec = cn as Record + const type = typeof cnRec.type === "string" ? cnRec.type.toLowerCase() : "" + // `primary_key` implies NOT NULL on every listed column across the + // adapters dbt supports for enforced contracts; `not_null` at + // model level is the explicit multi-column variant of the column + // form. + if (type !== "not_null" && type !== "primary_key") continue + const cols = Array.isArray(cnRec.columns) ? (cnRec.columns as unknown[]) : [] + for (const c of cols) { + if (typeof c === "string") coveredByConstraint.add(norm(c)) + } + } + } + if (Array.isArray(mm.columns)) { for (const col of mm.columns) { if (!col || typeof col !== "object") continue @@ -1013,7 +1055,10 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { for (const cn of c.constraints) { if (cn && typeof cn === "object") { const type = (cn as Record).type - if (typeof type === "string" && type.toLowerCase() === "not_null") { + const t = typeof type === "string" ? type.toLowerCase() : "" + // Column-level `not_null` OR `primary_key` — the latter + // inherently enforces NOT NULL (consensus MAJOR #1). + if (t === "not_null" || t === "primary_key") { coveredByConstraint.add(key) } } diff --git a/packages/opencode/test/altimate/review-dbt-patterns.test.ts b/packages/opencode/test/altimate/review-dbt-patterns.test.ts index f05cf40616..9b77397613 100644 --- a/packages/opencode/test/altimate/review-dbt-patterns.test.ts +++ b/packages/opencode/test/altimate/review-dbt-patterns.test.ts @@ -1050,6 +1050,229 @@ models: expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) }) + test("R20 S1: MODEL-level primary_key constraint covers every listed grain column (consensus MAJOR #1)", () => { + // Consensus MAJOR #1 — dbt 1.5+ supports model-level constraints via + // `constraints: [{type: primary_key, columns: [a, b]}]`. A primary key + // inherently enforces NOT NULL on Postgres/Snowflake/BigQuery/ + // Databricks. Grain columns declared via a model-level PK constraint + // must not be flagged as missing not_null. + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + constraints: + - type: primary_key + columns: [metastore_id, sku_name, price_start_time] + columns: + - name: metastore_id + - name: sku_name + - name: price_start_time + - name: currency + data_tests: + - dbt_utils.unique_combination_of_columns: + arguments: + combination_of_columns: [metastore_id, sku_name, price_start_time] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: MODEL-level not_null constraint with `columns:` list covers each named column", () => { + // Explicit multi-column form of the model-level constraint. Same + // coverage effect as the primary_key case. + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + constraints: + - type: not_null + columns: [a, b] + columns: + - name: a + - name: b + - name: c + data_tests: + - dbt_utils.unique_combination_of_columns: + arguments: + combination_of_columns: [a, b] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: COLUMN-level primary_key constraint also counts as not_null coverage", () => { + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + columns: + - name: id + constraints: + - type: primary_key + data_tests: + - dbt_utils.unique_combination_of_columns: + arguments: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: model-level primary_key still misses columns NOT in its list", () => { + // Precision guard — a PK that names only some grain cols must still + // leave the OTHER grain cols flagged. + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + constraints: + - type: primary_key + columns: [a] # only covers a + columns: + - name: a + - name: b + data_tests: + - dbt_utils.unique_combination_of_columns: + arguments: + combination_of_columns: [a, b] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("b") + }) + + test("R20 S1: model-level constraints on NON-CONTRACTED model do NOT count as coverage (documentation-only)", () => { + // Consistent with column-level rule — model-level constraints without + // contract enforcement are documentation, not enforcement, on most + // adapters. + const newContent = `version: 2 +models: + - name: stg_x + constraints: + - type: primary_key + columns: [a, b] + columns: + - name: a + - name: b + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [a, b] +` + const f = detectSchemaYmlPatterns( + { path: "models/staging/stg_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + // Both columns flagged (no test-level not_null, no contract). + expect(gaps.length).toBe(2) + }) + + test("R20 S1: `config.contract` object without `enforced` does NOT mask a top-level `contract: {enforced: true}` (MINOR #3)", () => { + // Consensus MINOR #3 — an earlier ternary short-circuited when + // `cfg.contract` was any object. `config: {contract: {alias: X}}` + // with no `enforced` key hid a top-level `contract: {enforced: true}`. + // Now both locations are OR'd; either declaration counts. + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + alias: SOMETHING_ELSE # no enforced key here + contract: + enforced: true # ← must still count as contracted + columns: + - name: id + constraints: + - type: not_null + - name: change_time # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id, change_time] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("change_time") + // Contract IS enforced — recommendation must point at constraints. + expect(gaps[0].body).toContain("constraints: [{type: not_null}]") + }) + + test("R20 S1: `dbt.not_null` namespaced test alias counts as coverage (MINOR #4)", () => { + // Consensus MINOR #4 — dbt 1.8+ allows namespaced test names. Column + // covered by `data_tests: [dbt.not_null]` must not be flagged. + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + data_tests: [dbt.not_null] + data_tests: + - unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: `{name:, test_name: not_null}` alternative test form counts as coverage (MINOR #6)", () => { + // Consensus MINOR #6 — dbt's documented alternative object form is + // `{name: my_test, test_name: not_null, ...}`. Reading only the first + // key returns `name` (an alias), missing the underlying test type. The + // `test_name` field is authoritative when present. + const newContent = `version: 2 +models: + - name: mrt_x + columns: + - name: id + data_tests: + - name: id_never_null + test_name: not_null + data_tests: + - unique_combination_of_columns: + combination_of_columns: [id] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + test("R20 S1: dbt 1.9+ `arguments:` nesting is recognised (real corpus shape)", () => { // The real internal corpus PRs use the dbt 1.9+ shape: // `- dbt_utils.unique_combination_of_columns: {arguments: {combination_of_columns: [...]}}` From 2f661474667c428ad83f93b7a382fc9a6da27ead Mon Sep 17 00:00:00 2001 From: Haider Date: Thu, 23 Jul 2026 12:30:14 +0530 Subject: [PATCH 3/5] chore(review): [R20 S1] genericize internal-vocab leaks in dbt-patterns comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/opencode/src/altimate/review/dbt-patterns.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/altimate/review/dbt-patterns.ts b/packages/opencode/src/altimate/review/dbt-patterns.ts index 404cdb7af6..076ef17fee 100644 --- a/packages/opencode/src/altimate/review/dbt-patterns.ts +++ b/packages/opencode/src/altimate/review/dbt-patterns.ts @@ -959,8 +959,8 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { // Normalise column names for coverage comparison. dbt YAML often uses // adapter-cased column names (Snowflake folds unquoted identifiers to - // uppercase; other adapters differ). Lowercase both sides so `WORKSPACE_ID` - // in `combination_of_columns` matches `workspace_id` in `columns:`. Hoisted + // uppercase; other adapters differ). Lowercase both sides so `ORDER_ID` + // in `combination_of_columns` matches `order_id` in `columns:`. Hoisted // to function scope per consensus NIT #7 (was redeclared per-model). const norm = (s: string): string => s.toLowerCase() @@ -1440,7 +1440,7 @@ export function detectSchemaYmlPatterns( // Recommendation flips between `constraints:` (contracted model) and // `data_tests:` (view / non-contracted model) based on the model's // contract state — matches the adapter-semantics discussion in the - // corpus study (PR D×2, PR A×2). + // corpus study (four instances across the sample). for (const g of grainGaps) { const filename = file.path.split("/").pop() const recommendation = g.contractEnforced From 78657cdbc79bf8095dfb20b8b1ab1b13ba86684f Mon Sep 17 00:00:00 2001 From: Haider Date: Thu, 23 Jul 2026 12:38:59 +0530 Subject: [PATCH 4/5] fix(review): [R20 S1] address altimate-harness-bot review findings on grain-key detector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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>` 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). --- .../src/altimate/review/dbt-patterns.ts | 125 +++++++++++-- .../test/altimate/review-dbt-patterns.test.ts | 166 +++++++++++++++++- 2 files changed, 276 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/altimate/review/dbt-patterns.ts b/packages/opencode/src/altimate/review/dbt-patterns.ts index 076ef17fee..6e4e34fece 100644 --- a/packages/opencode/src/altimate/review/dbt-patterns.ts +++ b/packages/opencode/src/altimate/review/dbt-patterns.ts @@ -951,11 +951,113 @@ export interface GrainKeyGap { contractEnforced: boolean } +/** + * Iterate every dbt entity in a schema.yml that can carry + * `unique_combination_of_columns` + `columns:` + `constraints:` — the shape + * checked for grain-key not_null coverage. Models are the primary case; + * snapshots also declare grain (SCD-2 unique_key semantics) and are a real + * miss vector when omitted. Sources declare table-level `columns:` + tests + * on their `tables[]` entries, so we descend one level. Seeds are covered + * for symmetry with `extractTestOccurrences` — grain-key tests on a seed + * are rare but legal (altimate-harness-bot review, PR #1029 + * dbt-patterns.ts:963). + */ +function iterateGrainEntities(d: Record): Array> { + const out: Array> = [] + for (const key of ["models", "snapshots", "seeds"] as const) { + const arr = d[key] + if (!Array.isArray(arr)) continue + for (const e of arr) { + if (e && typeof e === "object") out.push(e as Record) + } + } + // sources nest per-table entities under `.tables[]`; each table has its own + // `name` + `columns` + `tests`/`data_tests`, matching the model shape. + const sources = d.sources + if (Array.isArray(sources)) { + for (const s of sources) { + if (!s || typeof s !== "object") continue + const tables = (s as Record).tables + if (!Array.isArray(tables)) continue + for (const t of tables) { + if (t && typeof t === "object") out.push(t as Record) + } + } + } + return out +} + +/** + * Extract the `combination_of_columns` set for each entity in a doc. Used to + * scope grain-gap findings to entities whose grain declaration actually + * changed between the base and head of the PR — otherwise a housekeeping + * edit (adding a description, bumping a meta tag) surfaces every + * pre-existing gap in the file and trains reviewers to suppress the rule + * (altimate-harness-bot review, PR #1029 dbt-patterns.ts:1099). + */ +function extractGrainDeclarations(doc: unknown): Map> { + const out = new Map>() + if (!doc || typeof doc !== "object") return out + const d = doc as Record + for (const mm of iterateGrainEntities(d)) { + const name = typeof mm.name === "string" ? mm.name : undefined + if (!name) continue + const cols = new Set() + for (const testsKey of ["tests", "data_tests"] as const) { + const tests = mm[testsKey] + if (!Array.isArray(tests)) continue + for (const t of tests) { + if (!t || typeof t !== "object") continue + for (const [k, args] of Object.entries(t as Record)) { + const bare = k.toLowerCase().replace(/^dbt_utils\./, "") + if (bare !== "unique_combination_of_columns") continue + if (!args || typeof args !== "object") continue + const argsObj = args as Record + const nested = + argsObj.arguments && typeof argsObj.arguments === "object" + ? (argsObj.arguments as Record) + : undefined + const combo = + (nested?.combination_of_columns as unknown) ?? (argsObj.combination_of_columns as unknown) + if (!Array.isArray(combo)) continue + for (const c of combo) if (typeof c === "string") cols.add(c.toLowerCase()) + } + } + } + if (cols.size) out.set(name, cols) + } + return out +} + +/** + * Entities whose grain declaration differs from old → new (added, removed, + * or column set changed). Emit gap findings only for these entities so a + * PR that doesn't touch grain declarations doesn't surface every + * pre-existing gap. + */ +function grainDeclChangedEntities(oldDoc: unknown, newDoc: unknown): Set { + const oldMap = extractGrainDeclarations(oldDoc) + const newMap = extractGrainDeclarations(newDoc) + const changed = new Set() + for (const [name, cols] of newMap) { + const prior = oldMap.get(name) + if (!prior || prior.size !== cols.size || [...cols].some((c) => !prior.has(c))) changed.add(name) + } + return changed +} + +/** + * Grain-key not_null completeness gaps for every entity (model, snapshot, + * source table, seed) in a schema.yml document. Callers filter by + * `grainDeclChangedEntities` before emitting findings, so this returns the + * full population and change-scoping happens above. + */ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { const gaps: GrainKeyGap[] = [] if (!doc || typeof doc !== "object") return gaps const d = doc as Record - if (!Array.isArray(d.models)) return gaps + const entities = iterateGrainEntities(d) + if (!entities.length) return gaps // Normalise column names for coverage comparison. dbt YAML often uses // adapter-cased column names (Snowflake folds unquoted identifiers to @@ -985,9 +1087,7 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { return undefined } - for (const m of d.models) { - if (!m || typeof m !== "object") continue - const mm = m as Record + for (const mm of entities) { const mname = typeof mm.name === "string" ? mm.name : undefined if (!mname) continue @@ -1274,13 +1374,18 @@ export function detectSchemaYmlPatterns( usedStructural = true // R20 S1 — grain-key `not_null` completeness. Only fire on // non-deleted files: a deleted schema.yml has no current grain to - // guard. Extract from the NEW side so we flag the current state of - // the file, whether the grain declaration was newly added in this - // PR or pre-existing (a broken grain-guard is a real risk either - // way, and reviewers can suppress if the pre-existing case is by - // design). Uses the `not_null-missing` gap emitted below. + // guard. Change-scoped: only emit gaps for entities (models, + // snapshots, source tables, seeds) whose grain declaration + // (`combination_of_columns` set) actually changed between the base + // and head of this PR. A housekeeping edit — adding a description, + // bumping a meta tag — must not surface pre-existing gaps on + // unrelated entities; that trains reviewers to suppress the rule + // (altimate-harness-bot review, PR #1029 dbt-patterns.ts:1099). + // When there's no old side (added file), every entity counts as + // changed and every gap surfaces. if (!isDeletedFile && newDoc !== undefined) { - grainGaps = extractGrainKeyGaps(newDoc) + const changedEntities = grainDeclChangedEntities(oldDoc, newDoc) + grainGaps = extractGrainKeyGaps(newDoc).filter((g) => changedEntities.has(g.model)) } } } diff --git a/packages/opencode/test/altimate/review-dbt-patterns.test.ts b/packages/opencode/test/altimate/review-dbt-patterns.test.ts index 9b77397613..fa8fd8b9bb 100644 --- a/packages/opencode/test/altimate/review-dbt-patterns.test.ts +++ b/packages/opencode/test/altimate/review-dbt-patterns.test.ts @@ -791,10 +791,12 @@ models: [] test("R20 S1: unique_combination_of_columns with grain col missing not_null → warning finding", () => { // `price_start_time` is grain but has no not_null coverage → gap. // `metastore_id` / `sku_name` have not_null via constraints on the - // contracted model → covered. + // contracted model → covered. Test scoped as a newly-added model to + // trigger the change-scoped gate (see the steady-state test below for + // the no-change precision guarantee). const newContent = `version: 2 models: - - name: mrt_billing_account_prices + - name: mrt_x config: contract: enforced: true @@ -813,16 +815,17 @@ models: - sku_name - price_start_time ` - const oldContent = newContent // steady-state (grain already present) — still fires + // Grain declaration didn't exist on the old side — the model itself is new. + const oldContent = "version: 2\nmodels: []\n" const f = detectSchemaYmlPatterns( - { path: "models/marts/mrt_billing_account_prices.yml", status: "modified", diff: undefined }, + { path: "models/marts/mrt_x.yml", status: "modified", diff: undefined }, DEFAULT_RUBRIC, { oldContent, newContent }, ) const gap = f.find((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") expect(gap).toBeDefined() expect(gap!.severity).toBe("warning") - expect(gap!.model).toBe("mrt_billing_account_prices") + expect(gap!.model).toBe("mrt_x") expect(gap!.column).toBe("price_start_time") // Contract is enforced → recommendation should point at `constraints:`. expect(gap!.body).toContain("constraints: [{type: not_null}]") @@ -830,6 +833,159 @@ models: expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(1) }) + test("R20 S1: steady-state grain gap on unchanged model is NOT re-surfaced (change-scoped precision)", () => { + // altimate-harness-bot review, PR #1029 dbt-patterns.ts:1099. A + // housekeeping edit (description bump, meta tag) on a file whose grain + // declarations are identical old→new must not surface pre-existing + // gaps on unrelated models. Otherwise reviewers suppress the rule. + const yml = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + columns: + - name: id + constraints: + - type: not_null + - name: change_time # ← pre-existing gap on a grain column + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id, change_time] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "modified", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: yml, newContent: yml }, + ) + // Same grain declaration old→new → grain-key gap should NOT fire even + // though the not_null coverage is incomplete. Removals detector still + // covers the case where coverage was dropped in this diff. + expect(f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing").length).toBe(0) + }) + + test("R20 S1: grain declaration changed (column added to combination_of_columns) does fire gap", () => { + // Same file, but the PR ADDS `change_time` to `combination_of_columns` + // without adding not_null coverage. This is a real regression the + // reviewer must catch even though the model existed before this PR. + const oldContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + columns: + - name: id + constraints: + - type: not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id] +` + const newContent = `version: 2 +models: + - name: mrt_x + config: + contract: + enforced: true + columns: + - name: id + constraints: + - type: not_null + - name: change_time # ← added, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [id, change_time] +` + const f = detectSchemaYmlPatterns( + { path: "models/marts/mrt_x.yml", status: "modified", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].column).toBe("change_time") + }) + + test("R20 S1: grain detector also covers snapshots (SCD-2 grain declaration)", () => { + // altimate-harness-bot review, PR #1029 dbt-patterns.ts:963. + // Snapshots carry `unique_combination_of_columns` for SCD-2 unique_key + // semantics — a legitimate grain-declaration site the previous + // models-only iteration silently skipped. + const newContent = `version: 2 +snapshots: + - name: dim_customer_snapshot + columns: + - name: customer_id + data_tests: [not_null] + - name: valid_from # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [customer_id, valid_from] +` + const f = detectSchemaYmlPatterns( + { path: "snapshots/dim_customer_snapshot.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].model).toBe("dim_customer_snapshot") + expect(gaps[0].column).toBe("valid_from") + }) + + test("R20 S1: grain detector also covers source tables (per-table `columns:` + tests)", () => { + // Source tables declare `columns:` + `tests:` at the table level; + // grain declarations there are legitimate and were previously missed. + const newContent = `version: 2 +sources: + - name: raw + tables: + - name: orders + columns: + - name: order_id + data_tests: [not_null] + - name: event_ts # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [order_id, event_ts] +` + const f = detectSchemaYmlPatterns( + { path: "models/sources.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].model).toBe("orders") + expect(gaps[0].column).toBe("event_ts") + }) + + test("R20 S1: grain detector also covers seeds", () => { + // Grain-key tests on seeds are rare but legal — coverage for symmetry + // with `extractTestOccurrences` which iterates all four entity types. + const newContent = `version: 2 +seeds: + - name: lookup + columns: + - name: region_id + data_tests: [not_null] + - name: effective_from # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [region_id, effective_from] +` + const f = detectSchemaYmlPatterns( + { path: "seeds/lookup.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + expect(gaps.length).toBe(1) + expect(gaps[0].model).toBe("lookup") + expect(gaps[0].column).toBe("effective_from") + }) + test("R20 S1: non-contracted (view) model recommends data_tests: not_null", () => { const newContent = `version: 2 models: From 0c48d4e9e3ef2de471bf61d9aa19f61043554023 Mon Sep 17 00:00:00 2001 From: Haider Date: Thu, 23 Jul 2026 13:17:00 +0530 Subject: [PATCH 5/5] fix(review): [R20 S1] qualify source-table entity names as `.` (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. --- .../src/altimate/review/dbt-patterns.ts | 30 ++++++++----- .../test/altimate/review-dbt-patterns.test.ts | 43 ++++++++++++++++++- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/altimate/review/dbt-patterns.ts b/packages/opencode/src/altimate/review/dbt-patterns.ts index 6e4e34fece..1ed240b083 100644 --- a/packages/opencode/src/altimate/review/dbt-patterns.ts +++ b/packages/opencode/src/altimate/review/dbt-patterns.ts @@ -962,25 +962,37 @@ export interface GrainKeyGap { * are rare but legal (altimate-harness-bot review, PR #1029 * dbt-patterns.ts:963). */ -function iterateGrainEntities(d: Record): Array> { - const out: Array> = [] +function iterateGrainEntities(d: Record): Array<{ name: string; body: Record }> { + const out: Array<{ name: string; body: Record }> = [] for (const key of ["models", "snapshots", "seeds"] as const) { const arr = d[key] if (!Array.isArray(arr)) continue for (const e of arr) { - if (e && typeof e === "object") out.push(e as Record) + if (!e || typeof e !== "object") continue + const body = e as Record + const name = typeof body.name === "string" ? body.name : undefined + if (name) out.push({ name, body }) } } // sources nest per-table entities under `.tables[]`; each table has its own // `name` + `columns` + `tests`/`data_tests`, matching the model shape. + // Qualify with the enclosing source name (`.
`) so two + // sources with a table of the same name don't conflate in change-scoping + // OR finding fingerprint (cubic-review P2 on PR #1029). const sources = d.sources if (Array.isArray(sources)) { for (const s of sources) { if (!s || typeof s !== "object") continue - const tables = (s as Record).tables + const src = s as Record + const srcName = typeof src.name === "string" ? src.name : undefined + if (!srcName) continue + const tables = src.tables if (!Array.isArray(tables)) continue for (const t of tables) { - if (t && typeof t === "object") out.push(t as Record) + if (!t || typeof t !== "object") continue + const body = t as Record + const tblName = typeof body.name === "string" ? body.name : undefined + if (tblName) out.push({ name: `${srcName}.${tblName}`, body }) } } } @@ -999,9 +1011,7 @@ function extractGrainDeclarations(doc: unknown): Map> { const out = new Map>() if (!doc || typeof doc !== "object") return out const d = doc as Record - for (const mm of iterateGrainEntities(d)) { - const name = typeof mm.name === "string" ? mm.name : undefined - if (!name) continue + for (const { name, body: mm } of iterateGrainEntities(d)) { const cols = new Set() for (const testsKey of ["tests", "data_tests"] as const) { const tests = mm[testsKey] @@ -1087,9 +1097,7 @@ function extractGrainKeyGaps(doc: unknown): GrainKeyGap[] { return undefined } - for (const mm of entities) { - const mname = typeof mm.name === "string" ? mm.name : undefined - if (!mname) continue + for (const { name: mname, body: mm } of entities) { // Contract enforcement: either at model-level `config.contract.enforced` // OR at top-level `contract:` (dbt supports both shapes). Consensus diff --git a/packages/opencode/test/altimate/review-dbt-patterns.test.ts b/packages/opencode/test/altimate/review-dbt-patterns.test.ts index fa8fd8b9bb..5c5099dfab 100644 --- a/packages/opencode/test/altimate/review-dbt-patterns.test.ts +++ b/packages/opencode/test/altimate/review-dbt-patterns.test.ts @@ -957,10 +957,51 @@ sources: ) const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") expect(gaps.length).toBe(1) - expect(gaps[0].model).toBe("orders") + // Source-table entity is qualified as `.
` so two + // sources with same-named tables don't conflate (cubic-review P2). + expect(gaps[0].model).toBe("raw.orders") expect(gaps[0].column).toBe("event_ts") }) + test("R20 S1: same source-table name in two sources does NOT conflate (cubic-review P2)", () => { + // Two sources both containing a table named `orders`. A grain gap on + // one must not surface / suppress gaps on the other, and the finding + // fingerprint must distinguish them. + const newContent = `version: 2 +sources: + - name: raw + tables: + - name: orders + columns: + - name: order_id + data_tests: [not_null] + - name: event_ts # ← grain col, no not_null + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [order_id, event_ts] + - name: legacy + tables: + - name: orders + columns: + - name: order_id + data_tests: [not_null] + - name: event_ts # ← same shape, same grain gap in the OTHER source + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: [order_id, event_ts] +` + const f = detectSchemaYmlPatterns( + { path: "models/sources.yml", status: "added", diff: undefined }, + DEFAULT_RUBRIC, + { oldContent: undefined, newContent }, + ) + const gaps = f.filter((x) => (x.evidence?.result as any)?.rule === "grain_key_not_null_missing") + // Two distinct entities → two distinct gaps. + expect(gaps.length).toBe(2) + const models = new Set(gaps.map((g) => g.model)) + expect(models).toEqual(new Set(["raw.orders", "legacy.orders"])) + }) + test("R20 S1: grain detector also covers seeds", () => { // Grain-key tests on seeds are rare but legal — coverage for symmetry // with `extractTestOccurrences` which iterates all four entity types.