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
- A milestone with an adopted (canonical) milestone lifecycle and ≥2 planned verification classes.
- 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.
- The aggregate criterion row (
milestone-validation:aggregate, verdict inconclusive) inserts.
- 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.
Bug type
Auto-mode / milestone validation
Summary
gsd_validate_milestonecannot persist any non-passverdict when at least one planned verification class is fully green. The write aborts inside SQLite and rolls back the wholemilestone.validatedomain 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' }. Thatoutcomebecomesworkflow_attempt_results.outcome.evidenceVerdict(evidence)returns'pass'for any class whose observations are allpassed, 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 whoseoutcome='interrupted'.The trigger
trg_workflow_technical_verdict_scopeforbids exactly that pairing: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-
passverdicts that can be written are ones where every class carries at least one non-passedobservation.Steps to reproduce
gsd_validate_milestonewithverdict: "needs-attention", supplyingverificationEvidencewhere at least one class has all observationspassedand at least one class has aninconclusiveobservation.milestone-validation:aggregate, verdictinconclusive) inserts.milestone-validation:contract, verdictpass) aborts with:Criteria are sorted by
criterionKey, so:aggregateinserts before:contractaborts.Minimal SQL reproduction against a project DB, which isolates it from the tool layer entirely — seed a
milestone.validateoperation, a technical criterion, a settled attempt, and a result withoutcome='interrupted', then:Expected behavior
A milestone validation reporting
needs-attention(orneeds-remediation) should persist per-class verdicts that honestly reflect each class's evidence, includingpassfor classes that genuinely passed.Either:
operation_type = 'milestone.validate', where the writer already enforces the aggregate-level equivalent in JS; orvalidateEvidencerequires aninconclusivecriterion to carry aninconclusiveobservation, 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-retrywedge, whose sanctioned exit text ("Re-project the missing artifact with/gsd rebuild markdown") is the wrong class of remedy — nothing is missing andrebuild markdowncannot help.Diagnostic trap worth documenting: because
writeMilestoneValidationseeds 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
.backupcopy of a real project DB, with a negative control:milestone.validatecarve-outinconclusive,outcome='interrupted'pass,outcome='interrupted'failunderoutcome='failed', ordinary op (control)passunderoutcome='failed', ordinary op (must reject)The carve-out restores the mixed-evidence case without weakening the invariant on any other operation type.
Related
needs-attentionverdict as a technical failure. This is a distinct, deeper defect: there the verdict persisted and the retry loop misbehaved; here the verdict cannot be persisted at all, so the tool returns an error before the host ever sees a verdict. The observable end state (finalize-retry wedge with therebuild markdownexit text) is the same, which may make the two easy to conflate.Secondary issue found while diagnosing
V42schema application is gated oncurrentVersion < 42(db/engine.ts). A database that reacheduser_version48 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 andtrg_workflow_evidence_verdictabsent entirely, whilePRAGMA user_versionreported 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.