@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.
- package/README.md +19 -16
- package/cli-lint.js +282 -67
- package/cli.js +16 -11
- package/index.js +110 -0
- package/package.json +1 -1
- package/parse-rejection.js +116 -28
- package/validators/f-2a5ddafa-provenance-retry-warn.test.js +226 -0
- package/validators/f-f50e779b-retry-warn-fallback-guard.test.js +153 -0
- package/validators/policy.js +40 -14
- package/validators/provenance-registry.test.js +35 -0
- package/validators/provenance.js +127 -8
- package/validators/repo-binding.js +17 -2
- package/validators/repo-binding.test.js +40 -0
- package/validators/steps.js +77 -0
- package/validators/verdict.js +19 -1
package/validators/steps.js
CHANGED
|
@@ -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
|
}
|
package/validators/verdict.js
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
* Verdict severity (highest to lowest): fail > blocked > partial > pass
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
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) {
|