feat(workflows): verification-criteria module — parser, scale, mean+veto gate (V1, #2487) - #2504
Conversation
Shared Criterion/Criteria types, named NoCriteria/EmptyCriterion
errors, HTML-comment stripping, {#id} slugging with 40-char
truncation and _2/_3 dedup, and the anchored 1–20 scale.
Assistant-model: Grok 4.6
Accept record, string[], and CriterionInput[] shapes with the same slug/dedup rules as parse_rubric. Unknown ids throw instead of dropping silently. Assistant-model: Grok 4.6
Accept only when quorum holds, mean meets the threshold, and no veto finding exists. A veto forces Repair even at mean 20. Below-quorum rounds are Indeterminate. invalidCount is metadata and cannot shift the mean. Assistant-model: Grok 4.6
…veto
Table tests for section layout, {#id} slugging/dedup, 40-char
truncation, comment stripping, empty/no-criteria errors, all
normalizer shapes, unknown-id select, threshold/veto/quorum
boundaries, and invalidCount never shifting the mean.
Assistant-model: Grok 4.6
Remove comment openers to a fixed point and drop trailing unterminated comments so sanitized rubric text cannot retain hidden headings or content. Assistant-model: GPT-5.6 Sol
3aa02ca to
3a2aca4
Compare
# Conflicts: # packages/workflows/CHANGELOG.md
| let n = 1; | ||
| while (seen.has(out)) { | ||
| n += 1; | ||
| out = `${cid}_${n}`; |
There was a problem hiding this comment.
Deduplication suffix exceeds the canonical ID limit
When two criteria generate the same 40-character slug, dedup_id appends _2 to the already-truncated base ID. The second identifier is therefore 42 characters long, contradicting the documented canonical maximum of 40 characters and making these criteria incompatible with consumers that enforce that format. Reserve room for the suffix when constructing deduplicated IDs.
Artifacts
Focused 40-character criterion-ID collision validation source
- The exact TypeScript script imports parse_rubric, creates two criteria with the same generated 40-character slug, and asserts the suffixed ID exceeds 40 characters; the takeaway is that this is the precise runtime case tested.
Observed output from the existing criterion-ID collision validation
- The captured existing Bun execution exited 0 and reported criterion ID lengths of 40 and 42 after collision suffixing; the takeaway is that the precise maximum-length violation occurred at runtime.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/workflows/builtin/verification-criteria.ts
Line: 85
Comment:
**Deduplication suffix exceeds the canonical ID limit**
When two criteria generate the same 40-character slug, `dedup_id` appends `_2` to the already-truncated base ID. The second identifier is therefore 42 characters long, contradicting the documented canonical maximum of 40 characters and making these criteria incompatible with consumers that enforce that format. Reserve room for the suffix when constructing deduplicated IDs.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| } | ||
|
|
||
| const mean = mean_score(scores); | ||
| const findings = scores.flatMap((item) => item.findings); |
There was a problem hiding this comment.
Non-finite scores bypass the verification threshold
decide_verification trusts the runtime score value instead of enforcing the declared 1–20 integer scale. A NaN score makes mean < acceptMean evaluate to false, so a full-quorum round with no veto returns accept with a NaN mean. Validate scores as finite schema-valid integers before aggregation, or treat invalid values as unavailable rather than allowing them to approve verification.
Artifacts
NaN verification decision reproduction source
- Exact TypeScript runtime reproduction that invokes decide_verification with a NaN score, full quorum, and no veto.
NaN verification decision execution output
- Captured Bun execution output showing the NaN mean produces an accept decision with exit code 0.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/workflows/builtin/verification-criteria.ts
Line: 325
Comment:
**Non-finite scores bypass the verification threshold**
`decide_verification` trusts the runtime `score` value instead of enforcing the declared 1–20 integer scale. A `NaN` score makes `mean < acceptMean` evaluate to `false`, so a full-quorum round with no veto returns `accept` with a `NaN` mean. Validate scores as finite schema-valid integers before aggregation, or treat invalid values as unavailable rather than allowing them to approve verification.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
V1 of #2487: a shared
verification-criteriamodule that every later verification slice can import. Rubrics parse once, scores are schema-valid integers on a shared 1–20 scale, anddecide_verificationis the only accept door. An unparseable report cannot become aCriterionScoreand cannot shift the mean.Does not rewire
adversarial-verification(V2) and does not add prompt builders or warm-first fan-out (V3).Changes
parse_rubric:criteria.mdparser with optional first-wins## Ground Truth Note,## Criteria/### Name {#id}headings, HTML-comment stripping, lowercase-alnum-underscore slugs ≤40 chars,_2/_3dedup, and namedNoCriteria/EmptyCriterionerrorsnormalize_criteria: record,string[], andCriterionInput[]shapes with the same slug/dedup rulesselect_criteria: subset + order by ids; unknown id throwsVERIFICATION_SCALE: anchored 1–20Type.Integerschemadecide_verification: Accept iff quorum ∧ mean ≥ acceptMean ∧ noseverity: "veto"finding; veto → Repair even at mean 20; belowceil(expected × quorumFraction)→ Indeterminate;invalidCountis metadata onlyEvidence
Acceptance criteria (from #2487, this slice's subset)
{#id}anchors, id dedup, comment stripping, empty-description rejection — proven bytest/unit/verification-criteria.test.ts(npx vitest --run --project unit -t "verification-criteria", 33 passed)packages/workflows/builtin/verification-criteria.ts: format parser, normalizer, aggregation helpers (parse_rubric,normalize_criteria,select_criteria,decide_verification,VERIFICATION_SCALE)decide_verificationtable tests (threshold boundary, veto overrides mean=20, quorum atceil(expected × fraction),invalidCountnever shifts the mean)CriterionScorehas no invalid/parse-failure constructor;invalidCountis unread metadatapackages/workflows(raw.ts,.jsimport specifiers)npm run checkgreenpackages/workflows/CHANGELOG.md[Unreleased]### Addedupdatedpackages/coding-agent/docsprimitive docs — V11, out of this sliceCommands
Size
Source only (
verification-criteria.ts): 320 insertions (cap: 500 source lines). Tests uncapped (392). Changelog docs-exempt (4).Spec
specs/2026-08-17-verification-criteria-module.md§5.1; research:research/docs/2026-08-17-llm-verifier-adoption-scan.md§3–§4.Related: #2487
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Greptile Summary
This change adds shared helpers for parsing verification rubrics, normalizing and selecting criteria, and aggregating verifier decisions with quorum, mean, and veto handling. Focused runtime checks confirmed that deduplicated generated IDs can exceed the documented 40-character canonical format and that a non-finite score can approve a verification round.
Merge safety: not safe to merge until score validation prevents invalid numeric values from producing acceptance. The identifier truncation issue should also be corrected to preserve the documented ID contract.
Confidence Score: 4/5
The new decision helper can return acceptance for an invalid score, so the change is not safe to merge as written.
There is one independent P1 non-security finding and one P2 finding. Under the required scoring table, one non-security P1 produces a score of 4; the P2 finding does not further change the score.
Files Needing Attention: packages/workflows/builtin/verification-criteria.ts needs score validation in decide_verification and suffix-aware truncation in dedup_id.
What T-Rex did
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile