Skip to content

[Bug]: gsd_validate_milestone cannot persist a non-pass verdict when any verification class is fully green — trg_workflow_technical_verdict_scope aborts the domain operation #2025

Description

@bbasketballer75

Bug type

Auto-mode / milestone validation

Summary

gsd_validate_milestone cannot persist any non-pass verdict when at least one planned verification class is fully green. The write aborts inside SQLite and rolls back the whole milestone.validate domain operation, so the tool returns an error rather than a verdict.

Per-class technical verdicts are derived independently of the aggregate attempt outcome:

  • canonicalOutcome("needs-attention"){ verdict: 'inconclusive', observation: 'inconclusive', outcome: 'interrupted' }. That outcome becomes workflow_attempt_results.outcome.
  • evidenceVerdict(evidence) returns 'pass' for any class whose observations are all passed, and it is applied per class.

So a mixed report — Contract/Integration/Operational all green, UAT inconclusive — produces criterion rows with verdict='pass' under a result whose outcome='interrupted'.

The trigger trg_workflow_technical_verdict_scope forbids exactly that pairing:

AND (NEW.verdict != 'pass' OR result.outcome = 'succeeded')

The writer's only outcome guard is at the aggregate level and fires solely on input.verdict === "pass". There is no per-criterion equivalent, so the illegal rows are never caught in JS and die in SQLite.

Net effect: a mixed-evidence milestone report is unrepresentable. The only non-pass verdicts that can be written are ones where every class carries at least one non-passed observation.

Steps to reproduce

  1. A milestone with an adopted (canonical) milestone lifecycle and ≥2 planned verification classes.
  2. Call gsd_validate_milestone with verdict: "needs-attention", supplying verificationEvidence where at least one class has all observations passed and at least one class has an inconclusive observation.
  3. The aggregate criterion row (milestone-validation:aggregate, verdict inconclusive) inserts.
  4. The first fully-green class row (e.g. milestone-validation:contract, verdict pass) aborts with:
Error validating milestone: technical verdict requires the current criterion and matching settled attempt

Criteria are sorted by criterionKey, so :aggregate inserts before :contract aborts.

Minimal SQL reproduction against a project DB, which isolates it from the tool layer entirely — seed a milestone.validate operation, a technical criterion, a settled attempt, and a result with outcome='interrupted', then:

-- inserts fine
INSERT INTO workflow_technical_verdicts (... verdict ...) VALUES (... 'inconclusive' ...);
-- aborts
INSERT INTO workflow_technical_verdicts (... verdict ...) VALUES (... 'pass' ...);

Expected behavior

A milestone validation reporting needs-attention (or needs-remediation) should persist per-class verdicts that honestly reflect each class's evidence, including pass for classes that genuinely passed.

Either:

  • (a) scope the pass/outcome invariant so it does not apply to operation_type = 'milestone.validate', where the writer already enforces the aggregate-level equivalent in JS; or
  • (b) clamp per-class criterion verdicts in the writer when the aggregate is non-pass — but note this cascades, because validateEvidence requires an inconclusive criterion to carry an inconclusive observation, so the synthesized observations would have to be fabricated. (a) looks correct; (b) would put false evidence in the audit record.

Actual behavior

The whole domain operation rolls back. The agent retries with identical inputs until the liveness backstop trips a finalize-retry wedge, whose sanctioned exit text ("Re-project the missing artifact with /gsd rebuild markdown") is the wrong class of remedy — nothing is missing and rebuild markdown cannot help.

Diagnostic trap worth documenting: because writeMilestoneValidation seeds criterion → attempt → result → verdicts in one transaction, a repeatedly-failing milestone lifecycle ends up showing 0 acceptance criteria and 0 execution attempts. That is rollback residue, not a precondition — it is very easy to misread as "the milestone lifecycle was never seeded" and go looking for the wrong bug.

GSD version

1.16.1 (defect present in the shipped v42 schema, and in the v34 foundation schema)

Node.js version

24.x

Operating system

Windows 11

Install method

npm global (@opengsd/gsd-pi)

Affected area

validate-milestone, workflow kernel schema, auto-mode liveness backstop

Logs, screenshots, and evidence

Reproduced end-to-end on a .backup copy of a real project DB, with a negative control:

insert shipped trigger with milestone.validate carve-out
aggregate, inconclusive, outcome='interrupted' inserts inserts
contract, pass, outcome='interrupted' ABORT inserts
fail under outcome='failed', ordinary op (control) inserts inserts
pass under outcome='failed', ordinary op (must reject) ABORT ABORT (still rejected)

The carve-out restores the mixed-evidence case without weakening the invariant on any other operation type.

Related

Secondary issue found while diagnosing

V42 schema application is gated on currentVersion < 42 (db/engine.ts). A database that reached user_version 48 by another route never receives V42's trigger definitions and can never self-heal, because the gate is permanently false. On the affected project this left the older v34 foundation trigger bodies in place and trg_workflow_evidence_verdict absent entirely, while PRAGMA user_version reported 48 — i.e. the database looked fully migrated and was not. Worth considering a trigger-definition reconciliation pass that is not version-gated, since triggers are idempotent to recreate.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions