Skip to content

feat: greenfield spec-test synthesis for dbt-pr-review (P0: propose-only)#990

Closed
anandgupta42 wants to merge 1 commit into
mainfrom
feat/greenfield-spec-test-synthesis
Closed

feat: greenfield spec-test synthesis for dbt-pr-review (P0: propose-only)#990
anandgupta42 wants to merge 1 commit into
mainfrom
feat/greenfield-spec-test-synthesis

Conversation

@anandgupta42

@anandgupta42 anandgupta42 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

PINEAPPLE

Summary

Adds greenfield spec-test synthesis to dbt-pr-review (P0: propose-only). Closes #989.

For a brand-new (git-added) dbt model — which has no base to diff against, so the equivalence/data-diff lanes can't verify it — an auxiliary lane generates candidate tests from the model's declared intent and surfaces them as a committable schema.yml patch. This raises the no-reference verification ceiling that missingGrainTestLane previously only flagged.

Two strictly separated tracks (design: packages/opencode/specs/greenfield-spec-test-synthesis.md):

  • Track A (deterministic): materialize dbt constraints the author declared but did not enforce (schema.yml not_null/unique/accepted_values/relationships, contract types) — no LLM in the (future) blocking path, so it can't be tricked into a back-labeled false positive.
  • Track B (LLM, advisory): propose new tests from soft intent (descriptions, ref() edges, PR text).

P0 is propose-only and cannot block a PR: every finding is emitted at severity: suggestion + confidence: unknown under the altimate.spec_test.proposed evidence tool, which is excluded from the verdict warning-count (mirrors the ai-review exclusion). Execution + the deterministic blocking path (Track A) land in P1.

Test Plan

  • 134 tests pass across review.test.ts, review-dbt-patterns.test.ts, and the new review-spec-test-gen.test.ts — covering added-only behavior, non-blocking verdict in both comment and gate mode, fabricated-ref dropping (filterToSpecDerived), and modified-only silence.
  • Honesty invariants verified directly: the lane emits only non-blocking findings, and verdict.ts excludes the proposed evidence tool from the warning-count so proposals can never reach REQUEST_CHANGES.
  • bun run script/upstream/analyze.ts --markers --base main --strict — clean.
  • Design reviewed adversarially by an independent model; findings folded into spec rev 2.

Checklist

  • Tests added/updated
  • Documentation updated (design specs under packages/opencode/specs/)
  • CHANGELOG updated (if user-facing) — N/A: P0 adds no user-facing behavior change (opt-in lane, advisory only)

Summary by cubic

Adds propose‑only greenfield spec‑test synthesis to dbt-pr-review to generate grounded candidate dbt tests for newly added models and output a committable schema.yml patch without affecting blocking verdicts. Also wires verdict guards and a new spec_tests lane into lite/full tiers.

  • New Features
    • New spec_tests lane runs only for added models; two tracks: deterministic from declared constraints and advisory from inferred context; emits suggestions only when AI is enabled.
    • Summary shows a “Proposed tests” section and aggregates a YAML patch; findings use evidence tool altimate.spec_test.proposed with confidence: unknown.
    • Verdict excludes altimate.spec_test.proposed from warning accumulation to ensure proposals never block.
    • Config adds specTests.execute (default false); execution comes later in P1.
    • Orchestrator injects generateSpecTests; guard filterToSpecDerived drops ungrounded/fabricated refs; deterministic test IDs prevent misattribution.
    • Risk tier enables spec_tests in lite and full.
    • Specs and tests added for the lane, verdict rules, and summary rendering.

Written for commit 850a9c5. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added optional spec-test synthesis for added dbt models, surfacing suggested tests in review output.
    • Review summaries now show a dedicated Proposed tests section, with combined YAML when available.
    • Added a config option to enable or disable spec-test execution.
  • Bug Fixes

    • Proposed test suggestions no longer affect blocking review decisions.
    • Invalid or unverified proposed tests are safely filtered out.
  • Tests

    • Added coverage for proposed-test filtering and review rendering behavior.

…nly)

Adds an auxiliary lane that generates candidate tests for greenfield
(git-added) dbt models from their declared intent, raising the no-reference
verification ceiling that `missingGrainTestLane` only flagged. Two strictly
separated tracks:

- Track A (deterministic): materialize declared-but-unenforced dbt constraints.
- Track B (LLM, advisory): propose new tests from soft intent.

P0 is propose-only: every finding is `suggestion` + `confidence: unknown` under
the `altimate.spec_test.proposed` evidence tool, which is excluded from the
verdict warning-count — it can never block a PR. Emits a committable
`schema.yml` patch as authoring help.

- `spec-test-gen.ts`: types, `filterToSpecDerived` anti-fabrication guard,
  `isBlockEligible`, `runSpecTestGen` transport (inline prompt for P0).
- `orchestrate.ts`: `specTestSynthesisLane` gated on added models + injected
  `generateSpecTests` (pure, like `aiReview`).
- `risk-tier.ts`: `spec_tests` in lite/full tiers.
- `verdict.ts`: exclude `altimate.spec_test.proposed` from warning accumulation.
- `config.ts`: `specTests.execute` shape (P1). `format.ts`: "Proposed tests".
- Design specs under `packages/opencode/specs/`.

Refs #989

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds greenfield spec-test synthesis for dbt PR review: a new spec-test-gen.ts module generates candidate dbt tests (declared-constraint and LLM-advisory tracks), an orchestrator lane surfaces non-blocking proposal findings, verdict logic excludes these from warning thresholds, config/risk-tier gate the lane, and output formatting renders proposed tests with an aggregated YAML patch. Two design spec documents accompany the implementation.

Changes

Greenfield spec-test synthesis

Layer / File(s) Summary
Design specs
packages/opencode/specs/corrective-app-memory.md, packages/opencode/specs/greenfield-spec-test-synthesis.md
New documents describe the app-memory data model/retrieval and the two-track (deterministic + LLM-advisory) spec-test synthesis design, gating, phasing, and non-goals.
Spec-test generation module
packages/opencode/src/altimate/review/spec-test-gen.ts
New module defines SpecSource/GeneratedTest types, filterToSpecDerived anti-fabrication guard, isBlockEligible, prompt/response handling, and runSpecTestGen which streams an LLM response and returns filtered proposals.
Orchestrator, config, risk-tier, and run wiring
packages/opencode/src/altimate/review/config.ts, .../index.ts, .../orchestrate.ts, .../risk-tier.ts, .../run.ts
Adds specTests.execute config flag, re-exports spec-test-gen, adds generateSpecTests callback and specTestSynthesisLane to the orchestrator, enables spec_tests lane in tier mappings, and wires runSpecTestGen into run.ts under AI-lane gating.
Verdict gating
packages/opencode/src/altimate/review/verdict.ts
Excludes findings with evidence tool altimate.spec_test.proposed from the warning-pattern blocking threshold.
Output formatting
packages/opencode/src/altimate/review/format.ts
Splits findings into proposed tests and regular findings, renders a "Proposed tests" section, and aggregates YAML snippets into a merged patch.
Tests
packages/opencode/test/altimate/review-spec-test-gen.test.ts
New tests cover the guard/eligibility helpers and end-to-end runReview behavior for proposed tests, mode matrix, fabricated refs, and modified-only PRs.

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

Possibly related PRs

  • AltimateAI/altimate-code#856: Extends the same orchestrate.ts lane wiring and verdict.ts warning-pattern gating for advisory/proposed findings.

Sequence Diagram(s)

sequenceDiagram
  participant runReview
  participant Orchestrator
  participant specTestSynthesisLane
  participant runSpecTestGen
  participant LLM
  participant Findings

  runReview->>Orchestrator: run(input with generateSpecTests)
  Orchestrator->>Orchestrator: lanes.has("spec_tests")?
  Orchestrator->>specTestSynthesisLane: run for added model
  specTestSynthesisLane->>specTestSynthesisLane: extract SpecSource from schema.yml
  specTestSynthesisLane->>runSpecTestGen: generateSpecTests(input)
  runSpecTestGen->>LLM: stream prompt
  LLM-->>runSpecTestGen: candidate tests JSON
  runSpecTestGen->>runSpecTestGen: filterToSpecDerived
  runSpecTestGen-->>specTestSynthesisLane: GeneratedTest[]
  specTestSynthesisLane->>Findings: emit suggestion-level findings
  Findings-->>Orchestrator: proposed test findings
Loading

Poem

A rabbit hops through schema.yml,
Sniffing out constraints with skill,
Proposing tests that never block,
Just gentle nudges, tick by tock. 🐇
Track A stays true, Track B just hints—
Hooray for safer dbt sprints!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: greenfield spec-test synthesis for dbt-pr-review in P0 propose-only mode.
Description check ✅ Passed The description matches the template with Summary, Test Plan, and Checklist sections, and it includes the required PINEAPPLE header.
Linked Issues check ✅ Passed The changes implement the linked issue's P0 propose-only greenfield spec-test synthesis, with advisory-only output and verdict exclusion.
Out of Scope Changes check ✅ Passed The added specs, lane wiring, config, verdict, formatting, and tests all appear directly related to the stated greenfield spec-test synthesis work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/greenfield-spec-test-synthesis

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

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

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

148-198: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type the YAML merge helpers instead of using any.

The proposed-test patch helpers (proposedTestsPatch, mergeTests, mergeColumns, pushUnique) are entirely any-typed, which eliminates compile-time safety if the YAML document shape drifts. Consider defining a lightweight interface for the parsed YAML model/column structure to catch mismatches early.

🤖 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/format.ts` around lines 148 - 198, The
YAML merge helpers in proposedTestsPatch, mergeTests, mergeColumns, and
pushUnique are using any throughout, which removes type safety for the parsed
document shape. Introduce lightweight local interfaces for the parsed model and
column structures, then update these helpers to use those types instead of any
so the YAML parsing and merge logic in format.ts is compile-time checked.
packages/opencode/test/altimate/review-spec-test-gen.test.ts (1)

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

Add an assertion verifying the aggregated YAML patch appears in the rendered summary.

The e2e test at lines 180-182 checks that the summary contains the "### Proposed tests" section header but doesn't verify that the aggregated yaml patch (the committable schema.yml output from proposedTestsPatch) is present. Adding an assertion like expect(summary).toContain("version: 2") or expect(summary).toContain("not_null") within a yaml fence would close the loop on the formatting layer's patch aggregation.

🤖 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/test/altimate/review-spec-test-gen.test.ts` around lines
153 - 253, The summary assertion in the spec test synthesis suite only checks
for the “### Proposed tests” section, but it does not verify that the aggregated
YAML patch from proposedTestsPatch is actually rendered. Update the relevant
test in review-spec-test-gen.test.ts, using renderSummary(env) and the existing
spec_tests flow, to assert the summary includes the committable schema.yml YAML
content (for example the version header or not_null content inside the fenced
YAML block) so the formatting and patch aggregation path is covered end to end.
packages/opencode/src/altimate/review/orchestrate.ts (3)

991-1007: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent failure on generateSpecTests errors.

Unlike runSpecTestGen's internal catches (which log.error), this catch swallows any error from the injected generateSpecTests call with no logging — a persistently failing LLM lane (bad model config, quota, etc.) would be invisible.

♻️ Proposed fix
   } catch {
-    return []
+    log.error("spec-test synthesis lane failed", { model, file: ctx.file.path })
+    return []
   }

(requires importing/creating a Log instance in this file, e.g. Log.create({ service: "orchestrate" }))

🤖 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/orchestrate.ts` around lines 991 -
1007, The generateSpecTests call in orchestrate.ts is swallowing all errors
silently, so failing LLM/spec generation is invisible. Update the try/catch
around input.generateSpecTests in the orchestration flow to log the caught error
before returning an empty array, using a Log instance (for example, a
module-level logger created for orchestrate) and keeping the existing control
flow intact. Use the generateSpecTests and runSpecTestGen paths as the reference
points for where the logging should be added.

818-887: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Model-level dbt constraints aren't extracted.

schemaModelSources only inspects column.constraints/column.tests/column.data_tests. dbt also supports model-level constraints (e.g., composite primary_key, table-level check), which are silently missed here, undercounting declared-but-unenforced constraints available as spec sources for Track B.

♻️ Proposed addition
   const contractEnforced = bool(record(node.contract)?.enforced)
+  for (const rawConstraint of array(node.constraints)) {
+    const constraint = record(rawConstraint)
+    const type = text(constraint?.type)
+    if (!type || !DECLARED_SPEC_SOURCE_KINDS.has(type)) continue
+    out.push({
+      origin: "declared_constraint",
+      kind: type as SpecSource["kind"],
+      ref: `schema.yml:${model}:${type}`,
+      text: type,
+      args: constraint,
+    })
+  }
   for (const rawColumn of array(node.columns)) {
🤖 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/orchestrate.ts` around lines 818 - 887,
schemaModelSources only extracts column-level tests and constraints, so
model-level dbt constraints are missed. Update schemaModelSources to also
inspect the model node itself for table-level/model-level constraints (for
example primary_key and check definitions) and add them to the returned
SpecSource[] using the same declared_constraint mapping approach. Use the
existing schema.yml:${model} ref pattern and keep the logic alongside the
current node.columns traversal so the function captures both column and model
constraints.

889-904: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Schema.yml is re-fetched and re-parsed per added model.

changedSchemaSources re-reads and re-parses every changed schema.yml via getContent+YAML.parse for each added model in the PR. For a PR adding several models across shared schema files, this repeats the same I/O and parse work redundantly.

♻️ Proposed caching
-async function changedSchemaSources(model: string, input: OrchestrateInput): Promise<SpecSource[]> {
+async function changedSchemaSources(
+  model: string,
+  input: OrchestrateInput,
+  parsedCache: Map<string, unknown>,
+): Promise<SpecSource[]> {
   if (!input.getContent) return []
   const reviewable = filterChangedFiles(input.changedFiles, input.rubric.exclusions.excludeGlobs)
   const schemaFiles = reviewable.filter((f) => f.kind === "schema_yml" && f.status !== "deleted")
   const sources: SpecSource[] = []
   for (const file of schemaFiles) {
     try {
-      const raw = await input.getContent(file.path, "new")
-      if (!raw) continue
-      sources.push(...schemaModelSources(model, YAML.parse(raw)))
+      if (!parsedCache.has(file.path)) {
+        const raw = await input.getContent(file.path, "new")
+        parsedCache.set(file.path, raw ? YAML.parse(raw) : undefined)
+      }
+      const doc = parsedCache.get(file.path)
+      if (doc !== undefined) sources.push(...schemaModelSources(model, doc))
     } catch {
       // Broken or unavailable schema.yml should not crash an advisory lane.
     }
   }
   return sources
 }

Pass a shared parsedCache from runReview into each specTestSynthesisLane(ctx, input, dialect) call.

Also applies to: 975-989

🤖 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/orchestrate.ts` around lines 889 - 904,
The schema parsing work in changedSchemaSources is being repeated for every
added model, causing the same schema.yml files to be re-fetched and re-parsed
multiple times. Update runReview to create and pass a shared parsedCache through
specTestSynthesisLane(ctx, input, dialect), then have changedSchemaSources reuse
that cache instead of calling input.getContent and YAML.parse repeatedly for the
same file. Keep the fix centered around changedSchemaSources and the
specTestSynthesisLane call path so shared schema files are parsed once per PR.
packages/opencode/src/altimate/review/spec-test-gen.ts (1)

264-295: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

dbtTest.test isn't validated against kind.

normalizeDbtTest only checks that test is a non-empty string, and filterToSpecDerived only validates kind against ALLOWED_TEST_KINDS — nothing checks that dbtTest.test actually corresponds to kind. An LLM proposal could carry kind: "not_null" (passes the guard) while dbtTest.test names an unrelated/arbitrary generic test, which then gets rendered verbatim into the committable schema.yml patch (proposedTestYaml in orchestrate.ts).

♻️ Proposed tightening
+const KIND_TO_DBT_TEST_NAME: Record<GeneratedTestKind, string> = {
+  not_null: "not_null",
+  unique: "unique",
+  accepted_values: "accepted_values",
+  relationships: "relationships",
+  range: "", // advisory-only; no canonical generic test name
+}
+
 export function filterToSpecDerived(
   tests: GeneratedTest[],
   providedSources: SpecSource[],
 ): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
   ...
+    const canonical = KIND_TO_DBT_TEST_NAME[t.kind]
+    if (canonical && t.dbtTest && t.dbtTest.test !== canonical) {
+      dropped.push({ test: t, reason: "kind_not_allowed" })
+      continue
+    }
🤖 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/spec-test-gen.ts` around lines 264 -
295, The generated dbt test payload currently accepts any non-empty dbtTest.test
string, so validate that it matches the selected kind before keeping the test.
Update normalizeDbtTest and/or parseGeneratedTests to cross-check dbtTest.test
against the allowed test name for the GeneratedTestKind used in
filterToSpecDerived, and drop or reject mismatches so arbitrary test names
cannot flow into proposedTestYaml. Keep the existing symbols normalizeDbtTest,
parseGeneratedTests, and GeneratedTestKind as the main integration points.
🤖 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.

Nitpick comments:
In `@packages/opencode/src/altimate/review/format.ts`:
- Around line 148-198: The YAML merge helpers in proposedTestsPatch, mergeTests,
mergeColumns, and pushUnique are using any throughout, which removes type safety
for the parsed document shape. Introduce lightweight local interfaces for the
parsed model and column structures, then update these helpers to use those types
instead of any so the YAML parsing and merge logic in format.ts is compile-time
checked.

In `@packages/opencode/src/altimate/review/orchestrate.ts`:
- Around line 991-1007: The generateSpecTests call in orchestrate.ts is
swallowing all errors silently, so failing LLM/spec generation is invisible.
Update the try/catch around input.generateSpecTests in the orchestration flow to
log the caught error before returning an empty array, using a Log instance (for
example, a module-level logger created for orchestrate) and keeping the existing
control flow intact. Use the generateSpecTests and runSpecTestGen paths as the
reference points for where the logging should be added.
- Around line 818-887: schemaModelSources only extracts column-level tests and
constraints, so model-level dbt constraints are missed. Update
schemaModelSources to also inspect the model node itself for
table-level/model-level constraints (for example primary_key and check
definitions) and add them to the returned SpecSource[] using the same
declared_constraint mapping approach. Use the existing schema.yml:${model} ref
pattern and keep the logic alongside the current node.columns traversal so the
function captures both column and model constraints.
- Around line 889-904: The schema parsing work in changedSchemaSources is being
repeated for every added model, causing the same schema.yml files to be
re-fetched and re-parsed multiple times. Update runReview to create and pass a
shared parsedCache through specTestSynthesisLane(ctx, input, dialect), then have
changedSchemaSources reuse that cache instead of calling input.getContent and
YAML.parse repeatedly for the same file. Keep the fix centered around
changedSchemaSources and the specTestSynthesisLane call path so shared schema
files are parsed once per PR.

In `@packages/opencode/src/altimate/review/spec-test-gen.ts`:
- Around line 264-295: The generated dbt test payload currently accepts any
non-empty dbtTest.test string, so validate that it matches the selected kind
before keeping the test. Update normalizeDbtTest and/or parseGeneratedTests to
cross-check dbtTest.test against the allowed test name for the GeneratedTestKind
used in filterToSpecDerived, and drop or reject mismatches so arbitrary test
names cannot flow into proposedTestYaml. Keep the existing symbols
normalizeDbtTest, parseGeneratedTests, and GeneratedTestKind as the main
integration points.

In `@packages/opencode/test/altimate/review-spec-test-gen.test.ts`:
- Around line 153-253: The summary assertion in the spec test synthesis suite
only checks for the “### Proposed tests” section, but it does not verify that
the aggregated YAML patch from proposedTestsPatch is actually rendered. Update
the relevant test in review-spec-test-gen.test.ts, using renderSummary(env) and
the existing spec_tests flow, to assert the summary includes the committable
schema.yml YAML content (for example the version header or not_null content
inside the fenced YAML block) so the formatting and patch aggregation path is
covered end to end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ca950d82-bd97-49e1-b057-066e663ba885

📥 Commits

Reviewing files that changed from the base of the PR and between f376fef and 850a9c5.

📒 Files selected for processing (11)
  • packages/opencode/specs/corrective-app-memory.md
  • packages/opencode/specs/greenfield-spec-test-synthesis.md
  • packages/opencode/src/altimate/review/config.ts
  • packages/opencode/src/altimate/review/format.ts
  • packages/opencode/src/altimate/review/index.ts
  • packages/opencode/src/altimate/review/orchestrate.ts
  • packages/opencode/src/altimate/review/risk-tier.ts
  • packages/opencode/src/altimate/review/run.ts
  • packages/opencode/src/altimate/review/spec-test-gen.ts
  • packages/opencode/src/altimate/review/verdict.ts
  • packages/opencode/test/altimate/review-spec-test-gen.test.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 850a9c5d56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

})
}

for (const rawTest of [...array(column.tests), ...array(column.data_tests)]) {

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 Badge Stop proposing tests that already exist

When an added model already includes schema tests in the same PR, this loop turns those existing tests/data_tests entries into spec sources for the proposal LLM, so the review can emit a patch suggesting the author add not_null/accepted_values tests that are already present in schema.yml. For greenfield models where the author correctly added tests alongside the SQL, this creates duplicate proposed-test noise instead of only suggesting missing coverage; filter out already-enforcing tests or keep them out of the proposal sources.

Useful? React with 👍 / 👎.

column,
confidence: "unknown",
evidence: { tool: "altimate.spec_test.proposed", result: { proposal: test, yaml } },
ruleKey: "spec_test:" + test.derivedFrom.ref,

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 Badge Include the generated test identity in the fingerprint

For multiple proposals derived from the same source ref and column, this rule key makes all of them share the same finding fingerprint because dedupe() keys on category/file/model/column/ruleKey. A single column description or PR-intent source can legitimately yield more than one candidate test, such as not_null plus range, but only one survives the final dedupe; include test.id, kind, or the dbt test spec in the rule key so distinct proposals are not silently dropped.

Useful? React with 👍 / 👎.

function proposedTestsPatch(findings: Finding[]): string {
const merged = new Map<string, any>()
for (const f of findings) {
for (const yaml of yamlFences(f.body)) {

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 Badge Build the patch from trusted YAML only

When the LLM-controlled rationale contains its own fenced yaml block, scanning every YAML fence in f.body lets that extra block be parsed and merged into the aggregate proposed-tests patch without passing filterToSpecDerived. The scenario only requires a generated rationale like yaml\nmodels: ...\n, because proposedTestBody appends the rationale after the trusted snippet; use evidence.result.yaml or otherwise select only the locally generated YAML instead of reparsing the whole markdown body.

Useful? React with 👍 / 👎.

Comment on lines +175 to +177
if (!ALLOWED_TEST_KINDS.has(t.kind)) {
dropped.push({ test: t, reason: "kind_not_allowed" })
continue

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 Badge Validate dbtTest.test, not just kind

When a generated item uses an allowed kind but sets dbtTest.test to a different or arbitrary generic test macro, this guard still keeps it because it only validates t.kind and the cited ref; proposedTestYaml later writes dbtTest.test directly into the suggested schema patch. That lets malformed or prompt-injected LLM output propose tests outside the advertised allowlist, so the guard should require dbtTest.test to match the allowed kind/test mapping before accepting the proposal.

Useful? React with 👍 / 👎.

tests: GeneratedTest[],
providedSources: SpecSource[],
): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
const providedRefs = new Set(providedSources.map((s) => s.ref))

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 Badge Preserve the trusted source metadata

When a proposal cites a real ref but changes the accompanying derivedFrom.origin or kind, this check still accepts the LLM-supplied object because providedRefs tracks only strings. The kept proposal then carries forged source metadata in evidence, and helpers like isBlockEligible() can misclassify an inferred-context proposal as a declared constraint; compare against the full provided source or replace t.derivedFrom with the trusted extracted source before returning it.

Useful? React with 👍 / 👎.

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

4 issues found across 11 files

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:137">
P2: The `generateSpecTests` lane is gated on the same `noAi || config.ai === false` check as the AI reviewer, but Track A of spec-test generation is purely deterministic (parsed dbt constraints → test proposals, no LLM). When AI is disabled, only Track B (LLM-based inferred_context proposals) should be skipped; Track A should still run so deterministic constraint-materialization tests are available.</violation>
</file>

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

<violation number="1" location="packages/opencode/src/altimate/review/format.ts:196">
P3: The aggregate schema.yml patch can include duplicate candidate tests when equivalent YAML mappings arrive with different key order. Using an order-stable key for parsed YAML values would keep the proposed patch idempotent.</violation>
</file>

<file name="packages/opencode/src/altimate/review/spec-test-gen.ts">

<violation number="1" location="packages/opencode/src/altimate/review/spec-test-gen.ts:351">
P2: Spec-test proposals can pass the anti-fabrication guard while citing a source the LLM was never given when `specSources` exceeds `MAX_SPEC_SOURCES`. The guard should validate against the same sliced source list used in `buildUserMessage` so omitted refs are not accepted as grounded.</violation>
</file>

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

<violation number="1" location="packages/opencode/src/altimate/review/orchestrate.ts:764">
P2: Greenfield models with declared `primary_key` or `foreign_key` contract constraints will not get spec-test proposals from those declarations because the source-kind allowlist omits dbt's contract constraint names. Consider accepting `primary_key`/`foreign_key` here and mapping them to the appropriate proposed `unique`/`relationships` tests.</violation>
</file>

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

Re-trigger cubic

modelVersion: opts.modelVersion,
coreVersion: opts.coreVersion,
aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
generateSpecTests: opts.noAi || config.ai === false ? undefined : runSpecTestGen,

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: The generateSpecTests lane is gated on the same noAi || config.ai === false check as the AI reviewer, but Track A of spec-test generation is purely deterministic (parsed dbt constraints → test proposals, no LLM). When AI is disabled, only Track B (LLM-based inferred_context proposals) should be skipped; Track A should still run so deterministic constraint-materialization tests are available.

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 137:

<comment>The `generateSpecTests` lane is gated on the same `noAi || config.ai === false` check as the AI reviewer, but Track A of spec-test generation is purely deterministic (parsed dbt constraints → test proposals, no LLM). When AI is disabled, only Track B (LLM-based inferred_context proposals) should be skipped; Track A should still run so deterministic constraint-materialization tests are available.</comment>

<file context>
@@ -133,6 +134,7 @@ export async function reviewPullRequest(opts: ReviewPullRequestOptions): Promise
     modelVersion: opts.modelVersion,
     coreVersion: opts.coreVersion,
     aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
+    generateSpecTests: opts.noAi || config.ai === false ? undefined : runSpecTestGen,
     prTitle: opts.prTitle,
     prBody: opts.prBody,
</file context>

if (!text) return []

const tests = parseGeneratedTests(text)
const { kept } = filterToSpecDerived(tests, input.specSources)

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: Spec-test proposals can pass the anti-fabrication guard while citing a source the LLM was never given when specSources exceeds MAX_SPEC_SOURCES. The guard should validate against the same sliced source list used in buildUserMessage so omitted refs are not accepted as grounded.

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/spec-test-gen.ts, line 351:

<comment>Spec-test proposals can pass the anti-fabrication guard while citing a source the LLM was never given when `specSources` exceeds `MAX_SPEC_SOURCES`. The guard should validate against the same sliced source list used in `buildUserMessage` so omitted refs are not accepted as grounded.</comment>

<file context>
@@ -0,0 +1,359 @@
+    if (!text) return []
+
+    const tests = parseGeneratedTests(text)
+    const { kept } = filterToSpecDerived(tests, input.specSources)
+    return kept
+  } catch (err) {
</file context>
Suggested change
const { kept } = filterToSpecDerived(tests, input.specSources)
const { kept } = filterToSpecDerived(tests, input.specSources.slice(0, MAX_SPEC_SOURCES))

]
}

const DECLARED_SPEC_SOURCE_KINDS = new Set(["not_null", "unique", "accepted_values", "relationships"])

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: Greenfield models with declared primary_key or foreign_key contract constraints will not get spec-test proposals from those declarations because the source-kind allowlist omits dbt's contract constraint names. Consider accepting primary_key/foreign_key here and mapping them to the appropriate proposed unique/relationships tests.

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/orchestrate.ts, line 764:

<comment>Greenfield models with declared `primary_key` or `foreign_key` contract constraints will not get spec-test proposals from those declarations because the source-kind allowlist omits dbt's contract constraint names. Consider accepting `primary_key`/`foreign_key` here and mapping them to the appropriate proposed `unique`/`relationships` tests.</comment>

<file context>
@@ -754,6 +761,270 @@ async function missingGrainTestLane(ctx: ModelContext, runner: ReviewRunner): Pr
   ]
 }
 
+const DECLARED_SPEC_SOURCE_KINDS = new Set(["not_null", "unique", "accepted_values", "relationships"])
+
+function record(value: unknown): Record<string, unknown> | undefined {
</file context>

}

function pushUnique(items: any[], value: any): void {
const key = JSON.stringify(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The aggregate schema.yml patch can include duplicate candidate tests when equivalent YAML mappings arrive with different key order. Using an order-stable key for parsed YAML values would keep the proposed patch idempotent.

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/format.ts, line 196:

<comment>The aggregate schema.yml patch can include duplicate candidate tests when equivalent YAML mappings arrive with different key order. Using an order-stable key for parsed YAML values would keep the proposed patch idempotent.</comment>

<file context>
@@ -111,3 +115,84 @@ function capitalize(s: string): string {
+}
+
+function pushUnique(items: any[], value: any): void {
+  const key = JSON.stringify(value)
+  if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
+}
</file context>

@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) — 8 finding(s)

  • 8 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/format.ts (L195-L198)

[🟠 MEDIUM] Using JSON.stringify to determine object uniqueness is fragile as it is sensitive to object key ordering (e.g., {"a": 1, "b": 2} vs {"b": 2, "a": 1}). Furthermore, repeatedly stringifying item inside the .some loop introduces unnecessary computational overhead.

Consider using a deep equality function (like isDeepEqual from remeda which seems available in the project) to ensure robust and efficient deduplication.

Suggested change:

function pushUnique(items: any[], value: any): void {
  // If `isDeepEqual` from "remeda" is available:
  // if (!items.some((item) => isDeepEqual(item, value))) items.push(value)
  const key = JSON.stringify(value)
  if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
}

2. packages/opencode/src/altimate/review/format.ts (L154-L161)

[🟠 MEDIUM] When merging parsed YAML content, the current logic strictly constructs new objects and copies only the name, tests, and columns properties, effectively ignoring other valid DBT properties (like description, data_type, or meta).

If the proposed test YAML snippets include other valid properties, they will be silently excluded from the final aggregated patch. Consider preserving the rest of the properties by spreading the original object, e.g., const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] } (and similar in mergeColumns).

Suggested change:

        for (const rawModel of Array.isArray(doc?.models) ? doc.models : []) {
          const modelName = typeof rawModel?.name === "string" ? rawModel.name : ""
          if (!modelName) continue
          const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] }
          mergeTests(target, rawModel)
          mergeColumns(target, rawModel)
          merged.set(modelName, target)
        }

3. packages/opencode/src/altimate/review/format.ts (L184-L188)

[🟠 MEDIUM] Similar to the models merging logic, constructing a new object with only the name property drops other valid column-level properties (e.g., description, data_type) present in the proposed tests.

Consider spreading the rawColumn properties when creating a new column entry.

Suggested change:

    let column = target.columns.find((c: any) => c.name === name)
    if (!column) {
      column = { ...rawColumn, tests: [] }
      target.columns.push(column)
    }

4. packages/opencode/src/altimate/review/format.ts (L172-L176)

[🔵 LOW] Widespread use of the any type (e.g., target: any, source: any, Map<string, any>) bypasses static type checking, violating TypeScript best practices.

This obscures the expected data shape for YAML parsing results and model structures, reducing maintainability and increasing the risk of runtime errors if schemas change. Consider defining and using explicit interfaces (e.g., for DBT models and columns) instead of relying on any.

5. packages/opencode/src/altimate/review/orchestrate.ts (L910-L913)

[🟠 MEDIUM] The regular expressions /\\bref\\s*\\(([^)]*)\\)/g and /\\bsource\\s*\\(([^)]*)\\)/g will match ref() and source() calls even if they are inside single-line (--) or block (/* ... */) SQL comments. This could result in hallucinated spec tests based on commented-out code. Consider stripping comments before running the regex matches.

You could define a local helper or import one if available:

function stripComments(s: string): string {
  return s.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ")
}

6. packages/opencode/src/altimate/review/orchestrate.ts (L896-L901)

[🔵 LOW] Swallowing the YAML.parse exception completely makes debugging difficult when users expect tests to be generated but they aren't (due to a malformed schema.yml).

Consider capturing the error and logging it with structured logging (e.g., log.warn, ensuring you don't use console.* to avoid TUI corruption).

    } catch (err) {
      log.warn(`Failed to parse schema.yml from ${file.path}`, { error: err })
      // Broken or unavailable schema.yml should not crash an advisory lane.
    }

7. packages/opencode/src/altimate/review/spec-test-gen.ts (L162-L190)

[🔴 HIGH] High Severity: LLM Hallucination bypasses block eligibility check

The parseGeneratedTests function directly casts and trusts the derivedFrom object from the LLM's JSON output. In filterToSpecDerived, you only check if the ref exists in providedSources, but you don't replace the LLM's object with the trusted source.

If the LLM hallucinates and sets derivedFrom.origin to "declared_constraint" for an inferred context source, isBlockEligible will evaluate to true. This bypasses the strict architectural separation between Track A and Track B, allowing an advisory-only LLM proposal to wrongly block a PR.

To fix this, overwrite the LLM's derivedFrom object with the trusted SpecSource from providedSources.

export function filterToSpecDerived(
  tests: GeneratedTest[],
  providedSources: SpecSource[],
): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
  const providedMap = new Map(providedSources.map((s) => [s.ref, s]))
  const kept: GeneratedTest[] = []
  const dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> = []
  for (const t of tests) {
    const df = t.derivedFrom
    if (!df) {
      dropped.push({ test: t, reason: "no_derived_from" })
      continue
    }
    if (!ALLOWED_TEST_KINDS.has(t.kind)) {
      dropped.push({ test: t, reason: "kind_not_allowed" })
      continue
    }
    if (!df.ref || !df.ref.trim()) {
      dropped.push({ test: t, reason: "empty_ref" })
      continue
    }
    const trustedSource = providedMap.get(df.ref)
    if (!trustedSource) {
      dropped.push({ test: t, reason: "ref_not_provided" })
      continue
    }
    t.derivedFrom = trustedSource
    kept.push(t)
  }
  return { kept, dropped }
}

8. packages/opencode/src/altimate/review/spec-test-gen.ts (L253-L257)

[🟠 MEDIUM] Medium Severity: Strict Regex for JSON Extraction

The regex /^```(?:json)?\s*([\s\S]*?)\s*```$/i uses strict start (^) and end ($) boundary anchors. If the LLM output contains any conversational text before or after the markdown JSON block (e.g., "Here are the tests:\n```json..."), the regex will fail to match.

This forces JSON.parse to evaluate the raw text, resulting in a SyntaxError that silently fails the entire generation process. Removing the anchors ensures the regex is resilient to LLM chatter.

Suggested change:

function stripJsonFence(text: string): string {
  const trimmed = text.trim()
  const fenced = /```(?:json)?\s*([\s\S]*?)\s*```/i.exec(trimmed)
  return fenced ? fenced[1].trim() : trimmed
}

Comment on lines +195 to +198
function pushUnique(items: any[], value: any): void {
const key = JSON.stringify(value)
if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
}

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] Using JSON.stringify to determine object uniqueness is fragile as it is sensitive to object key ordering (e.g., {"a": 1, "b": 2} vs {"b": 2, "a": 1}). Furthermore, repeatedly stringifying item inside the .some loop introduces unnecessary computational overhead.

Consider using a deep equality function (like isDeepEqual from remeda which seems available in the project) to ensure robust and efficient deduplication.

Suggested change:

Suggested change
function pushUnique(items: any[], value: any): void {
const key = JSON.stringify(value)
if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
}
function pushUnique(items: any[], value: any): void {
// If `isDeepEqual` from "remeda" is available:
// if (!items.some((item) => isDeepEqual(item, value))) items.push(value)
const key = JSON.stringify(value)
if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
}

Comment on lines +154 to +161
for (const rawModel of Array.isArray(doc?.models) ? doc.models : []) {
const modelName = typeof rawModel?.name === "string" ? rawModel.name : ""
if (!modelName) continue
const target = merged.get(modelName) ?? { name: modelName }
mergeTests(target, rawModel)
mergeColumns(target, rawModel)
merged.set(modelName, target)
}

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] When merging parsed YAML content, the current logic strictly constructs new objects and copies only the name, tests, and columns properties, effectively ignoring other valid DBT properties (like description, data_type, or meta).

If the proposed test YAML snippets include other valid properties, they will be silently excluded from the final aggregated patch. Consider preserving the rest of the properties by spreading the original object, e.g., const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] } (and similar in mergeColumns).

Suggested change:

Suggested change
for (const rawModel of Array.isArray(doc?.models) ? doc.models : []) {
const modelName = typeof rawModel?.name === "string" ? rawModel.name : ""
if (!modelName) continue
const target = merged.get(modelName) ?? { name: modelName }
mergeTests(target, rawModel)
mergeColumns(target, rawModel)
merged.set(modelName, target)
}
for (const rawModel of Array.isArray(doc?.models) ? doc.models : []) {
const modelName = typeof rawModel?.name === "string" ? rawModel.name : ""
if (!modelName) continue
const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] }
mergeTests(target, rawModel)
mergeColumns(target, rawModel)
merged.set(modelName, target)
}

Comment on lines +184 to +188
let column = target.columns.find((c: any) => c.name === name)
if (!column) {
column = { name }
target.columns.push(column)
}

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] Similar to the models merging logic, constructing a new object with only the name property drops other valid column-level properties (e.g., description, data_type) present in the proposed tests.

Consider spreading the rawColumn properties when creating a new column entry.

Suggested change:

Suggested change
let column = target.columns.find((c: any) => c.name === name)
if (!column) {
column = { name }
target.columns.push(column)
}
let column = target.columns.find((c: any) => c.name === name)
if (!column) {
column = { ...rawColumn, tests: [] }
target.columns.push(column)
}

Comment on lines +172 to +176
function mergeTests(target: any, source: any): void {
if (!Array.isArray(source?.tests)) return
target.tests = target.tests ?? []
for (const test of source.tests) pushUnique(target.tests, test)
}

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] Widespread use of the any type (e.g., target: any, source: any, Map<string, any>) bypasses static type checking, violating TypeScript best practices.

This obscures the expected data shape for YAML parsing results and model structures, reducing maintainability and increasing the risk of runtime errors if schemas change. Consider defining and using explicit interfaces (e.g., for DBT models and columns) instead of relying on any.

Comment on lines +910 to +913
function sqlIntentSources(sql: string | undefined): SpecSource[] {
if (!sql) return []
const out: SpecSource[] = []
for (const match of sql.matchAll(/\bref\s*\(([^)]*)\)/g)) {

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 regular expressions /\\bref\\s*\\(([^)]*)\\)/g and /\\bsource\\s*\\(([^)]*)\\)/g will match ref() and source() calls even if they are inside single-line (--) or block (/* ... */) SQL comments. This could result in hallucinated spec tests based on commented-out code. Consider stripping comments before running the regex matches.

You could define a local helper or import one if available:

function stripComments(s: string): string {
  return s.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ")
}

Comment on lines +896 to +901
const raw = await input.getContent(file.path, "new")
if (!raw) continue
sources.push(...schemaModelSources(model, YAML.parse(raw)))
} catch {
// Broken or unavailable schema.yml should not crash an advisory lane.
}

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] Swallowing the YAML.parse exception completely makes debugging difficult when users expect tests to be generated but they aren't (due to a malformed schema.yml).

Consider capturing the error and logging it with structured logging (e.g., log.warn, ensuring you don't use console.* to avoid TUI corruption).

    } catch (err) {
      log.warn(`Failed to parse schema.yml from ${file.path}`, { error: err })
      // Broken or unavailable schema.yml should not crash an advisory lane.
    }

Comment on lines +162 to +190
export function filterToSpecDerived(
tests: GeneratedTest[],
providedSources: SpecSource[],
): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
const providedRefs = new Set(providedSources.map((s) => s.ref))
const kept: GeneratedTest[] = []
const dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> = []
for (const t of tests) {
const df = t.derivedFrom
if (!df) {
dropped.push({ test: t, reason: "no_derived_from" })
continue
}
if (!ALLOWED_TEST_KINDS.has(t.kind)) {
dropped.push({ test: t, reason: "kind_not_allowed" })
continue
}
if (!df.ref || !df.ref.trim()) {
dropped.push({ test: t, reason: "empty_ref" })
continue
}
if (!providedRefs.has(df.ref)) {
dropped.push({ test: t, reason: "ref_not_provided" })
continue
}
kept.push(t)
}
return { kept, dropped }
}

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] High Severity: LLM Hallucination bypasses block eligibility check

The parseGeneratedTests function directly casts and trusts the derivedFrom object from the LLM's JSON output. In filterToSpecDerived, you only check if the ref exists in providedSources, but you don't replace the LLM's object with the trusted source.

If the LLM hallucinates and sets derivedFrom.origin to "declared_constraint" for an inferred context source, isBlockEligible will evaluate to true. This bypasses the strict architectural separation between Track A and Track B, allowing an advisory-only LLM proposal to wrongly block a PR.

To fix this, overwrite the LLM's derivedFrom object with the trusted SpecSource from providedSources.

export function filterToSpecDerived(
  tests: GeneratedTest[],
  providedSources: SpecSource[],
): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
  const providedMap = new Map(providedSources.map((s) => [s.ref, s]))
  const kept: GeneratedTest[] = []
  const dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> = []
  for (const t of tests) {
    const df = t.derivedFrom
    if (!df) {
      dropped.push({ test: t, reason: "no_derived_from" })
      continue
    }
    if (!ALLOWED_TEST_KINDS.has(t.kind)) {
      dropped.push({ test: t, reason: "kind_not_allowed" })
      continue
    }
    if (!df.ref || !df.ref.trim()) {
      dropped.push({ test: t, reason: "empty_ref" })
      continue
    }
    const trustedSource = providedMap.get(df.ref)
    if (!trustedSource) {
      dropped.push({ test: t, reason: "ref_not_provided" })
      continue
    }
    t.derivedFrom = trustedSource
    kept.push(t)
  }
  return { kept, dropped }
}

Comment on lines +253 to +257
function stripJsonFence(text: string): string {
const trimmed = text.trim()
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed)
return fenced ? fenced[1].trim() : trimmed
}

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] Medium Severity: Strict Regex for JSON Extraction

The regex /^```(?:json)?\s*([\s\S]*?)\s*```$/i uses strict start (^) and end ($) boundary anchors. If the LLM output contains any conversational text before or after the markdown JSON block (e.g., "Here are the tests:\n```json..."), the regex will fail to match.

This forces JSON.parse to evaluate the raw text, resulting in a SyntaxError that silently fails the entire generation process. Removing the anchors ensures the regex is resilient to LLM chatter.

Suggested change:

Suggested change
function stripJsonFence(text: string): string {
const trimmed = text.trim()
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed)
return fenced ? fenced[1].trim() : trimmed
}
function stripJsonFence(text: string): string {
const trimmed = text.trim()
const fenced = /```(?:json)?\s*([\s\S]*?)\s*```/i.exec(trimmed)
return fenced ? fenced[1].trim() : trimmed
}

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

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

  • 8 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/format.ts (L195-L198)

[🟠 MEDIUM] Using JSON.stringify to determine object uniqueness is fragile as it is sensitive to object key ordering (e.g., {"a": 1, "b": 2} vs {"b": 2, "a": 1}). Furthermore, repeatedly stringifying item inside the .some loop introduces unnecessary computational overhead.

Consider using a deep equality function (like isDeepEqual from remeda which seems available in the project) to ensure robust and efficient deduplication.

Suggested change:

function pushUnique(items: any[], value: any): void {
  // If `isDeepEqual` from "remeda" is available:
  // if (!items.some((item) => isDeepEqual(item, value))) items.push(value)
  const key = JSON.stringify(value)
  if (!items.some((item) => JSON.stringify(item) === key)) items.push(value)
}

2. packages/opencode/src/altimate/review/format.ts (L154-L161)

[🟠 MEDIUM] When merging parsed YAML content, the current logic strictly constructs new objects and copies only the name, tests, and columns properties, effectively ignoring other valid DBT properties (like description, data_type, or meta).

If the proposed test YAML snippets include other valid properties, they will be silently excluded from the final aggregated patch. Consider preserving the rest of the properties by spreading the original object, e.g., const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] } (and similar in mergeColumns).

Suggested change:

        for (const rawModel of Array.isArray(doc?.models) ? doc.models : []) {
          const modelName = typeof rawModel?.name === "string" ? rawModel.name : ""
          if (!modelName) continue
          const target = merged.get(modelName) ?? { ...rawModel, tests: [], columns: [] }
          mergeTests(target, rawModel)
          mergeColumns(target, rawModel)
          merged.set(modelName, target)
        }

3. packages/opencode/src/altimate/review/format.ts (L184-L188)

[🟠 MEDIUM] Similar to the models merging logic, constructing a new object with only the name property drops other valid column-level properties (e.g., description, data_type) present in the proposed tests.

Consider spreading the rawColumn properties when creating a new column entry.

Suggested change:

    let column = target.columns.find((c: any) => c.name === name)
    if (!column) {
      column = { ...rawColumn, tests: [] }
      target.columns.push(column)
    }

4. packages/opencode/src/altimate/review/format.ts (L172-L176)

[🔵 LOW] Widespread use of the any type (e.g., target: any, source: any, Map<string, any>) bypasses static type checking, violating TypeScript best practices.

This obscures the expected data shape for YAML parsing results and model structures, reducing maintainability and increasing the risk of runtime errors if schemas change. Consider defining and using explicit interfaces (e.g., for DBT models and columns) instead of relying on any.

5. packages/opencode/src/altimate/review/orchestrate.ts (L910-L913)

[🟠 MEDIUM] The regular expressions /\\bref\\s*\\(([^)]*)\\)/g and /\\bsource\\s*\\(([^)]*)\\)/g will match ref() and source() calls even if they are inside single-line (--) or block (/* ... */) SQL comments. This could result in hallucinated spec tests based on commented-out code. Consider stripping comments before running the regex matches.

You could define a local helper or import one if available:

function stripComments(s: string): string {
  return s.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ")
}

6. packages/opencode/src/altimate/review/orchestrate.ts (L896-L901)

[🔵 LOW] Swallowing the YAML.parse exception completely makes debugging difficult when users expect tests to be generated but they aren't (due to a malformed schema.yml).

Consider capturing the error and logging it with structured logging (e.g., log.warn, ensuring you don't use console.* to avoid TUI corruption).

    } catch (err) {
      log.warn(`Failed to parse schema.yml from ${file.path}`, { error: err })
      // Broken or unavailable schema.yml should not crash an advisory lane.
    }

7. packages/opencode/src/altimate/review/spec-test-gen.ts (L162-L190)

[🔴 HIGH] High Severity: LLM Hallucination bypasses block eligibility check

The parseGeneratedTests function directly casts and trusts the derivedFrom object from the LLM's JSON output. In filterToSpecDerived, you only check if the ref exists in providedSources, but you don't replace the LLM's object with the trusted source.

If the LLM hallucinates and sets derivedFrom.origin to "declared_constraint" for an inferred context source, isBlockEligible will evaluate to true. This bypasses the strict architectural separation between Track A and Track B, allowing an advisory-only LLM proposal to wrongly block a PR.

To fix this, overwrite the LLM's derivedFrom object with the trusted SpecSource from providedSources.

export function filterToSpecDerived(
  tests: GeneratedTest[],
  providedSources: SpecSource[],
): { kept: GeneratedTest[]; dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> } {
  const providedMap = new Map(providedSources.map((s) => [s.ref, s]))
  const kept: GeneratedTest[] = []
  const dropped: Array<{ test: GeneratedTest; reason: GuardDropReason }> = []
  for (const t of tests) {
    const df = t.derivedFrom
    if (!df) {
      dropped.push({ test: t, reason: "no_derived_from" })
      continue
    }
    if (!ALLOWED_TEST_KINDS.has(t.kind)) {
      dropped.push({ test: t, reason: "kind_not_allowed" })
      continue
    }
    if (!df.ref || !df.ref.trim()) {
      dropped.push({ test: t, reason: "empty_ref" })
      continue
    }
    const trustedSource = providedMap.get(df.ref)
    if (!trustedSource) {
      dropped.push({ test: t, reason: "ref_not_provided" })
      continue
    }
    t.derivedFrom = trustedSource
    kept.push(t)
  }
  return { kept, dropped }
}

8. packages/opencode/src/altimate/review/spec-test-gen.ts (L253-L257)

[🟠 MEDIUM] Medium Severity: Strict Regex for JSON Extraction

The regex /^```(?:json)?\s*([\s\S]*?)\s*```$/i uses strict start (^) and end ($) boundary anchors. If the LLM output contains any conversational text before or after the markdown JSON block (e.g., "Here are the tests:\n```json..."), the regex will fail to match.

This forces JSON.parse to evaluate the raw text, resulting in a SyntaxError that silently fails the entire generation process. Removing the anchors ensures the regex is resilient to LLM chatter.

Suggested change:

function stripJsonFence(text: string): string {
  const trimmed = text.trim()
  const fenced = /```(?:json)?\s*([\s\S]*?)\s*```/i.exec(trimmed)
  return fenced ? fenced[1].trim() : trimmed
}

@anandgupta42

Copy link
Copy Markdown
Contributor Author

Superseded by #991, which consolidates the propose-only P0 with track-A execution, the Track-B SQL sandbox, the compiled-core prompt move, P2 corrective app-memory, and all the bot-review fixes from this PR. Closing in favor of reviewing the full feature in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: greenfield spec-test synthesis for dbt-pr-review (raise the no-reference verification ceiling)

2 participants