-
Notifications
You must be signed in to change notification settings - Fork 134
feat(validators): deterministic completion gates — zero-write, build-green, literal deliverables, config/dialect lints #1175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anandgupta42
wants to merge
21
commits into
main
Choose a base branch
from
feat/deterministic-validators
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
57ea234
feat(validators): nothing-built inverse completion gate
anandgupta42 5a71009
feat(validators): build-green completion gate
anandgupta42 8b15d9a
feat(validators): literal-deliverable (spec-name) completion gate
anandgupta42 830e444
feat(validators): incremental-config consistency lint
anandgupta42 13b21d9
feat(validators): dialect-guard lint
anandgupta42 a659dad
docs: engine-split assessment for the two parse-level completion checks
anandgupta42 b6ecfaa
fix(validators): scope build-green failure counts correctly
anandgupta42 39781d8
test(validators): pin the lane's registration list
anandgupta42 14747ac
docs: placement assessment for the five shipped completion-gate valid…
anandgupta42 6626c46
docs: end-to-end evidence for the five completion-gate validators
anandgupta42 006bc8f
fix(validators): close the false positives the completion gates fire …
anandgupta42 8f0f9d8
fix(validators): close the review backlog on the deterministic comple…
anandgupta42 08abaae
fix(validators): keep the two run-results exemption axes independent
anandgupta42 b5c5d63
fix(validators): second review wave on the completion gates
anandgupta42 e8502ab
docs(validators): record the dialect-guard branch-semantics gap as a …
anandgupta42 7aa9087
fix(validators): inspect elif arms and bound the is_incremental() match
anandgupta42 28fb92d
fix(validators): close the build-provenance holes the consensus revie…
anandgupta42 7c09cc1
fix(validators): a delivered-but-untouched deliverable must satisfy t…
anandgupta42 af6f241
fix(validators): address the bot review wave on the consensus fixes
anandgupta42 3b944fe
fix(validators): make the unknown-command fallback permissive all the…
anandgupta42 dd858ef
fix(validators): second review sweep — under-firing gates and blockin…
anandgupta42 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
436 changes: 436 additions & 0 deletions
436
packages/opencode/src/altimate/validators/dbt-build-green.ts
Large diffs are not rendered by default.
Oops, something went wrong.
175 changes: 175 additions & 0 deletions
175
packages/opencode/src/altimate/validators/dbt-deliverable-names.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // altimate_change start — literal deliverable / spec-name completion gate | ||
| /** | ||
| * Literal-deliverable (spec-name) gate. | ||
| * | ||
| * A recurring, fully deterministic loss mode in evaluation traces: the work is | ||
| * functionally reasonable but shipped under self-chosen names — a prefix | ||
| * added, a plural dropped, a "v2" suffix, or an entirely different noun — and | ||
| * the agent then self-verifies against its own renamed output and reports | ||
| * success. The literal contract in the task document is never re-read. | ||
| * | ||
| * This gate re-reads it. It compares the deliverable names the task states | ||
| * **literally** against the names the project actually defines, and refuses | ||
| * to terminate when a required name is absent. | ||
| * | ||
| * Conservatism is the whole design: | ||
| * - Required names come only from `extractRequiredDeliverables`, which | ||
| * accepts a name solely from an explicit declaration marker, a | ||
| * deliverables section, or a requirement line — and only when it sits in | ||
| * an inline code span and is identifier- or path-shaped. There is no | ||
| * fuzzy matching and no inference. | ||
| * - When no required-names source is discoverable, `appliesTo` returns | ||
| * false and the session is never inspected. Silence, never a guess. | ||
| * - Produced names are the union of the filesystem inventory and every | ||
| * `manifest.json` name/alias, so an aliased relation cannot read as | ||
| * missing. | ||
| * - Comparison is exact (case-insensitive only). A near-miss name is | ||
| * reported as a possible substitute in the hint, never accepted as the | ||
| * deliverable. | ||
| * | ||
| * Deliberately out of scope: required *column* names. Asserting a column | ||
| * exists means resolving `select *`, CTEs and upstream schemas — real SQL | ||
| * analysis, not a filesystem inventory — so it is not attempted here rather | ||
| * than attempted badly. | ||
| */ | ||
|
|
||
| import { promises as fs } from "fs" | ||
| import { join } from "path" | ||
| import type { Validator, ValidatorContext, ValidatorResult } from "../../session/validators/types" | ||
| import { | ||
| findDbtProjectRoot, | ||
| findTaskInstructionFiles, | ||
| extractRequiredDeliverables, | ||
| collectProducedNodeNames, | ||
| modelsModifiedSince, | ||
| modelNameFromPath, | ||
| type RequiredDeliverables, | ||
| } from "./validator-utils" | ||
|
|
||
| /** The task contract for this workspace, when one is discoverable. */ | ||
| interface Contract { | ||
| taskFile: string | ||
| required: RequiredDeliverables | ||
| } | ||
|
|
||
| /** Read the workspace's literal deliverable contract, or null if there is none. */ | ||
| async function readContract(cwd: string, dbtRoot: string): Promise<Contract | null> { | ||
| // Every candidate, not just the first readable one: an informational | ||
| // `TASK.md` that states no deliverables must not mask a `REQUIREMENTS.md` | ||
| // that does, or this gate silently skips a session it was meant to check. | ||
| for (const task of await findTaskInstructionFiles(cwd, dbtRoot)) { | ||
| const required = extractRequiredDeliverables(task.content) | ||
| if (required) return { taskFile: task.path, required } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| /** True when `relative` exists under either the dbt project or the workspace. */ | ||
| async function fileExists(dbtRoot: string, cwd: string, relative: string): Promise<boolean> { | ||
| for (const root of new Set([dbtRoot, cwd])) { | ||
| try { | ||
| const stat = await fs.stat(join(root, relative)) | ||
| if (stat.isFile()) return true | ||
| } catch { | ||
| // keep looking | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| export const DbtDeliverableNamesValidator: Validator = { | ||
| name: "dbt-deliverable-names", | ||
| description: | ||
| "After the agent declares done, compares the deliverable names the task document states literally against the model, seed and snapshot names the project actually defines, and refuses to terminate when a required name is absent — catching renames and self-chosen substitutes.", | ||
|
|
||
| async appliesTo(ctx: ValidatorContext): Promise<boolean> { | ||
| const dbtRoot = await findDbtProjectRoot(ctx.workingDirectory) | ||
| if (!dbtRoot) return false | ||
| return (await readContract(ctx.workingDirectory, dbtRoot)) !== null | ||
| }, | ||
|
|
||
| async check(ctx: ValidatorContext): Promise<ValidatorResult> { | ||
| const startedAt = Date.now() | ||
| const dbtRoot = await findDbtProjectRoot(ctx.workingDirectory) | ||
| if (!dbtRoot) { | ||
| return { ok: true, details: { skipped: "no dbt project", session_id: ctx.sessionID } } | ||
| } | ||
| const contract = await readContract(ctx.workingDirectory, dbtRoot) | ||
| if (!contract) { | ||
| return { ok: true, details: { skipped: "no literal contract", session_id: ctx.sessionID } } | ||
| } | ||
|
|
||
| const produced = await collectProducedNodeNames(dbtRoot) | ||
|
anandgupta42 marked this conversation as resolved.
|
||
| const missingModels = contract.required.models.filter((name) => !produced.has(name)) | ||
| const missingFiles: string[] = [] | ||
| for (const relative of contract.required.files) { | ||
| if (!(await fileExists(dbtRoot, ctx.workingDirectory, relative))) missingFiles.push(relative) | ||
| } | ||
|
|
||
| const details = { | ||
| task_file: contract.taskFile, | ||
| required_source: contract.required.source, | ||
| required_models: contract.required.models, | ||
| required_files: contract.required.files, | ||
| produced_count: produced.size, | ||
| missing_models: missingModels, | ||
| missing_files: missingFiles, | ||
| dbt_root: dbtRoot, | ||
| session_id: ctx.sessionID, | ||
| elapsed_ms: Date.now() - startedAt, | ||
| } | ||
|
|
||
| if (missingModels.length === 0 && missingFiles.length === 0) { | ||
| return { ok: true, details } | ||
| } | ||
|
|
||
| // Names this session authored that the task did not ask for. These are the | ||
| // likely substitutes behind a missing required name; reported as context, | ||
| // never asserted as equivalent. | ||
| const requiredSet = new Set(contract.required.models) | ||
| const authored = await modelsModifiedSince(dbtRoot, ctx.sessionStartMs) | ||
| const unrequested = Array.from( | ||
| new Set( | ||
| authored | ||
| .map((p) => modelNameFromPath(p).toLowerCase()) | ||
| .filter((name) => name.length > 0 && !requiredSet.has(name)), | ||
| ), | ||
| ) | ||
|
|
||
| const reasonParts: string[] = [] | ||
| if (missingModels.length > 0) { | ||
| reasonParts.push( | ||
| `the task names ${missingModels.length} deliverable(s) this project does not define: ${missingModels.join(", ")}`, | ||
| ) | ||
| } | ||
| if (missingFiles.length > 0) { | ||
| reasonParts.push(`required file(s) missing: ${missingFiles.join(", ")}`) | ||
| } | ||
|
|
||
| const hintLines: string[] = [ | ||
| `The task document (${contract.taskFile}) states these names literally. A model that does the right thing under a different name does not satisfy the task, and self-verification against the renamed output will not detect it.`, | ||
| ] | ||
| if (missingModels.length > 0) { | ||
| hintLines.push(` • Create or rename to exactly: ${missingModels.join(", ")}`) | ||
| } | ||
| if (missingFiles.length > 0) { | ||
| hintLines.push(` • Create at exactly these paths: ${missingFiles.join(", ")}`) | ||
| } | ||
| if (unrequested.length > 0) { | ||
| hintLines.push( | ||
| ` • Models you created this session that the task did not name: ${unrequested.join(", ")}. If one of them is a renamed version of a required deliverable, rename the file (and any \`ref()\` to it) back to the required name.`, | ||
| ) | ||
| } | ||
| hintLines.push( | ||
| " • If a required deliverable is produced under an alias, set `alias` in its config so the required name is the relation name.", | ||
| ) | ||
|
|
||
| return { | ||
| ok: false, | ||
| reason: `Deliverable-name mismatch: ${reasonParts.join("; ")}.`, | ||
| fixHint: hintLines.join("\n"), | ||
| details: { ...details, unrequested_models: unrequested }, | ||
| } | ||
| }, | ||
| } | ||
| // altimate_change end | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the session deletes, renames, or edits the discovered task document so it no longer yields a contract, this completion-time lookup makes
appliesToreturn false;dbt-nothing-builtperforms the same live lookup, so both contract gates disappear. A session can therefore removeTASK.md, produce no requested model or build artifact, and terminate successfully because every remaining gate sees zero touched models. Capture the original contract at session start or read it from harness state that the agent cannot mutate.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Real, and deliberately left open rather than half-fixed. Recorded as item 19 in
.github/meta/harness-review-followups.mdindd858ef59fso this reasoning is not re-derived later.The finding is right:
readContractandartifactExpectationboth run at completion time against the live filesystem, so a session that deletes or rewritesTASK.mdmakes both contract gates returnappliesTo === falseand can terminate having produced nothing.The fix you name is the right one — capture the contract at session start, or read it from harness state the agent cannot write — and neither exists in this lane.
ValidatorContextcarriessessionStartMsand nothing else; there is no session-scoped artifact store to put a snapshot in. That is the same missing infrastructure follow-up items 4 and 8 need.The available half-measure is worse than the gap: blocking when a task-document candidate path was written during the session cannot distinguish a deletion from a file that never existed, and fires on the entirely normal case of a task that asks for the document itself to be updated. That converts a false negative into a blocking false positive on correct sessions, which is the wrong direction for a gate that terminates work.
Leaving this open rather than resolving it, since it is not addressed.