@dogfood-lab/verify 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,6 +72,51 @@ export function validateStepResults(scenarioResult) {
72
72
  }
73
73
  }
74
74
 
75
+ // F-88fb37ff: mirror of the pass-direction check above. A scenario cannot
76
+ // claim "fail"/"blocked" while every reported step says otherwise either —
77
+ // computeVerdict() (validators/verdict.js) trusts scenario_results[].verdict
78
+ // verbatim and never re-derives it from step_results, so without this check
79
+ // a self-reported "blocked" verdict backed by zero failing/blocked steps
80
+ // sailed through with no rejection reason at all.
81
+ //
82
+ // F-e42e8f80 (wave 20, amends F-88fb37ff): the original gate required at
83
+ // least one step to ACTIVELY report fail/blocked, which wrongly rejected
84
+ // the common honest shape "the scenario was blocked before any step could
85
+ // run, so every step reports 'skip'" — 'skip' and 'partial' are NEUTRAL
86
+ // (no evidence either way), not "evidence of no failure." Only 'pass' is an
87
+ // ACTIVE CONTRADICTION of a fail/blocked verdict (the step ran and claimed
88
+ // success). The gate now fires only when EVERY reported step actively says
89
+ // "pass" — a single skip/partial/fail/blocked step is enough to keep a
90
+ // fail/blocked verdict internally consistent.
91
+ //
92
+ // F-cc198701 (wave 22, confirming audit of F-e42e8f80): this condition and
93
+ // its validateRequiredSteps mirror below enumerated only 2 of the 4 legal
94
+ // values in scenario_results[].verdict's own schema enum (dogfood-record-
95
+ // submission.schema.json: ["pass","fail","blocked","partial"]) — 'partial'
96
+ // was never checked in either direction. A submitter could self-report
97
+ // verdict:'partial' with EVERY step actively 'fail' (strictly worse,
98
+ // more self-contradictory evidence than the fail/blocked direction already
99
+ // rejects) and sail through with zero rejection reason, because
100
+ // computeVerdict() never re-derives the verdict from step_results either —
101
+ // the ONLY guard against a dishonest self-report was this exact check, and
102
+ // it had a verdict-enum-shaped hole. Widened to also fire for 'partial',
103
+ // reusing the identical "all present steps actively pass" bar: 'partial'
104
+ // backed by zero non-pass evidence is exactly as self-contradictory as
105
+ // 'blocked'/'fail' backed by zero non-pass evidence. Deliberately does NOT
106
+ // touch the pass-direction check above (line ~59) — 'partial' backed by
107
+ // SOME failing steps is not inherently contradictory the way 'partial'
108
+ // backed by all-pass steps is.
109
+ if (verdict === 'fail' || verdict === 'blocked' || verdict === 'partial') {
110
+ const allStepsActivelyPass = step_results.every(
111
+ s => s != null && s.status === 'pass'
112
+ );
113
+ if (allStepsActivelyPass) {
114
+ errors.push(
115
+ `scenario verdict is "${verdict}" but no step reports status fail/blocked`
116
+ );
117
+ }
118
+ }
119
+
75
120
  return errors;
76
121
  }
77
122
 
@@ -136,5 +181,37 @@ export function validateRequiredSteps(scenarioResult, requiredSteps) {
136
181
  }
137
182
  }
138
183
 
184
+ // F-88fb37ff: mirror of the pass-direction block above, scoped to REQUIRED
185
+ // steps (the sibling check in validateStepResults enforces the same rule
186
+ // over ALL reported steps regardless of which are required).
187
+ //
188
+ // F-e42e8f80 (wave 20, amends F-88fb37ff): "no required step reports
189
+ // fail/blocked" was too narrow a bar — a required step honestly reporting
190
+ // 'skip' (never ran because the scenario was blocked upstream) or 'partial'
191
+ // is NEUTRAL, not an active contradiction, and must not force a rejection.
192
+ // The gate now fires only when every PRESENT required step actively says
193
+ // "pass" — mirroring validateStepResults' all-pass bar. A required step
194
+ // that is simply MISSING is "not an active pass" exactly like skip/partial
195
+ // would be, so it never by itself forces this check to fire (`result !=
196
+ // null && result.status === 'pass'` is false for a missing step too); its
197
+ // absence is already rejected unconditionally by the [step-results-present]
198
+ // loop above, regardless of verdict, so this check deliberately does not
199
+ // pile a second, verdict-specific error onto the exact same gap.
200
+ //
201
+ // F-cc198701 (wave 22): mirrors the identical widening in
202
+ // validateStepResults above — 'partial' was the one legal scenario-verdict
203
+ // enum value this scoped-to-required-steps check never enumerated either.
204
+ if ((verdict === 'fail' || verdict === 'blocked' || verdict === 'partial') && requiredSteps.length > 0) {
205
+ const allPresentRequiredStepsActivelyPass = requiredSteps.every(stepId => {
206
+ const result = resultMap.get(stepId);
207
+ return result != null && result.status === 'pass';
208
+ });
209
+ if (allPresentRequiredStepsActivelyPass) {
210
+ errors.push(
211
+ `[step-verdict-consistent] scenario verdict is "${verdict}" but no required step reports status fail/blocked`
212
+ );
213
+ }
214
+ }
215
+
139
216
  return errors;
140
217
  }
@@ -6,7 +6,15 @@
6
6
  * Verdict severity (highest to lowest): fail > blocked > partial > pass
7
7
  */
8
8
 
9
- const VERDICT_RANK = { fail: 0, blocked: 1, partial: 2, pass: 3 };
9
+ // F-937733ee: family sibling of F-2965699b/F-7ce07baa. Object.create(null)
10
+ // removes the prototype chain entirely — `VERDICT_RANK['constructor']` is
11
+ // `undefined`, not `Object.prototype.constructor` — so the `== null` guards
12
+ // below fire correctly for every Object.prototype key, not just for a
13
+ // literal typo. Currently unreachable in production (index.js:326 only ever
14
+ // passes schema-enum-valid verdicts into `scenarioResults`), but sealing it
15
+ // here keeps a future caller that skips the schema gate from silently
16
+ // reopening the class.
17
+ const VERDICT_RANK = Object.assign(Object.create(null), { fail: 0, blocked: 1, partial: 2, pass: 3 });
10
18
 
11
19
  /**
12
20
  * Compute the verified verdict.
@@ -60,6 +68,16 @@ export function computeVerdict(proposed, context) {
60
68
  downgrade_reasons.push('policy validation failed');
61
69
  }
62
70
 
71
+ // F-a0a4d806: non-empty rejection reasons are a fail floor. verify() sets
72
+ // status from reasons.length > 0, but overall_verdict.verified previously
73
+ // ignored the already-plumbed `reasons` argument — a steps[...] or
74
+ // repo:mismatch rejection could leave verified:'pass' next to
75
+ // status:'rejected'. Treat reasons as evidence, not a dual signal.
76
+ if (Array.isArray(reasons) && reasons.length > 0) {
77
+ floorVerdict = 'fail';
78
+ downgrade_reasons.push('non-empty rejection reasons force fail');
79
+ }
80
+
63
81
  // The verified verdict is the worse of proposed and floor
64
82
  // (we never upgrade, so if proposed is worse than floor, keep proposed)
65
83
  if (proposed && VERDICT_RANK[proposed] == null) {