@dogfood-lab/verify 1.3.1 → 1.4.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 +45 -25
- package/index.js +35 -2
- package/package.json +1 -1
- package/validators/policy.js +9 -1
- package/validators/schema-version.js +111 -0
- package/validators/steps.js +5 -1
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ if (!result.ok) {
|
|
|
48
48
|
|
|
49
49
|
| Validator | Purpose |
|
|
50
50
|
|---|---|
|
|
51
|
-
| `validators/schema.js` | JSON Schema check against `@dogfood-lab/schemas` |
|
|
51
|
+
| `validators/schema.js` | JSON Schema check against `@dogfood-lab/schemas` (SHAPE gate) |
|
|
52
|
+
| `validators/schema-version.js` | `schema_version` VALUE gate — refuses an incompatible MAJOR against `SUPPORTED_SCHEMA_VERSIONS` |
|
|
52
53
|
| `validators/policy.js` | Per-repo policy compliance (prototype-pollution-safe deep merge) |
|
|
53
54
|
| `validators/provenance.js` | GitHub Actions run-ID confirmation via API (with timeout guard) |
|
|
54
55
|
| `validators/steps.js` | Step-by-step contract checks (gate accumulation, ordering) |
|
|
@@ -84,48 +85,67 @@ Provenance fields (`github_run_id`, `github_workflow_ref`) are required when `pr
|
|
|
84
85
|
|
|
85
86
|
The verifier emits two prefix classes:
|
|
86
87
|
|
|
87
|
-
**
|
|
88
|
+
Discrimination happens by **class**, surfaced by `parseRejectionReason` (below). Every prefix maps to one of four classes: **submission-bad** (the submitter fixes the payload), **operational** (the verifier/tooling faulted), **ingest** (an ingest-side load fault), or **unknown** (unrecognized prefix).
|
|
89
|
+
|
|
90
|
+
**Submission-bad** — `class: 'submission-bad'` (the submitter's payload failed a validator gate; fix the submission and resubmit):
|
|
88
91
|
|
|
89
92
|
| Prefix | Source | Meaning |
|
|
90
93
|
|---|---|---|
|
|
91
94
|
| `schema:` | `validators/schema.js` | JSON Schema check on the submission/record envelope failed. The rest of the string carries the AJV path + message. |
|
|
92
|
-
| `policy:` | `validators/policy.js` | Per-repo policy gate failed (forbidden tags, missing required fields,
|
|
95
|
+
| `policy:` | `validators/policy.js` | Per-repo policy gate failed (forbidden tags, missing required fields, surface evidence/CI requirements, etc.). |
|
|
93
96
|
| `steps[<id>]:` | `validators/steps.js` | Step-level contract check failed on a specific step id (gate accumulation, ordering, evidence shape). |
|
|
94
97
|
| `provenance:` | `validators/provenance.js` | The GitHub run-id confirmation could not match the submitted commit/repo at the GitHub API. |
|
|
95
|
-
| `
|
|
98
|
+
| `repo:` | `index.js` cross-field guard | `submission.repo` does not match the owner/repo encoded in `source.run_url` (anti-forgery guard). Emitted as `repo:mismatch: …`. |
|
|
99
|
+
| `submission-contains-verifier-field:` | `index.js` | The submission carried a verifier-owned field (`policy_version`, `verification`, or an object `overall_verdict`) it must not author. |
|
|
100
|
+
| `CONTRACT_SCHEMA_TOO_NEW:` | `validators/schema-version.js` | The submission's `schema_version` declares a MAJOR **above** what this build supports (see `SUPPORTED_SCHEMA_VERSIONS` in `@dogfood-lab/schemas`). This build cannot understand a future contract — **the operator must upgrade testing-os**, but the routing class stays submission-bad (the payload as-shipped cannot be accepted by THIS build). |
|
|
101
|
+
| `CONTRACT_SCHEMA_TOO_OLD:` | `validators/schema-version.js` | The submission's `schema_version` declares a MAJOR **below** the supported floor. **The submitter must re-emit** against the current contract. A patch/minor delta inside the supported major range is NOT rejected. |
|
|
96
102
|
|
|
97
|
-
**
|
|
103
|
+
**Operational** — `class: 'operational'` (the validator itself threw an internal error; investigate the verifier, do NOT bounce to the submitter):
|
|
98
104
|
|
|
99
105
|
| Prefix | Source | Meaning |
|
|
100
106
|
|---|---|---|
|
|
101
107
|
| `VALIDATOR_FAULT_SCHEMA:` | `runValidator('schema', …)` catch | Internal exception inside the schema validator. The rest of the string carries the thrown `.message`. |
|
|
102
108
|
| `VALIDATOR_FAULT_POLICY:` | `runValidator('policy', …)` catch | Internal exception inside the policy validator. |
|
|
103
109
|
| `VALIDATOR_FAULT_STEPS:` | `runValidator('steps', …)` catch | Internal exception inside the steps validator. |
|
|
110
|
+
| `VALIDATOR_FAULT_CONTRACT_SCHEMA_VERSION:` | `runValidator('contract_schema_version', …)` catch | The version gate was called with an unknown contract key (a programmer error at the call site, not a submission fault). |
|
|
111
|
+
|
|
112
|
+
Any future `VALIDATOR_FAULT_<NEW>:` prefix is classified `operational` by family — `parseRejectionReason` matches the `VALIDATOR_FAULT_` head, so a new validator class needs no parser edit.
|
|
113
|
+
|
|
114
|
+
**Ingest** — `class: 'ingest'` (an ingest-side load fault, not a verifier gate):
|
|
115
|
+
|
|
116
|
+
| Prefix | Source | Meaning |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `scenario-load:` | `packages/ingest/run.js` | A scenario referenced by `scenario_results` could not be loaded from the source repo (typed-reason: `timeout` / `not_found` / `parse_error` / `invalid_id`). |
|
|
104
119
|
|
|
105
120
|
### Operator hygiene
|
|
106
121
|
|
|
122
|
+
Discriminate by **class**, not by hand-rolled `.startsWith()` chains. `parseRejectionReason(reason)` returns `{ class, prefix, detail }`:
|
|
123
|
+
|
|
107
124
|
```js
|
|
108
|
-
|
|
125
|
+
import { parseRejectionReason } from '@dogfood-lab/verify';
|
|
126
|
+
|
|
109
127
|
for (const r of result.rejection_reasons) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
const { class: cls, prefix, detail } =
|
|
129
|
+
parseRejectionReason(r);
|
|
130
|
+
switch (cls) {
|
|
131
|
+
case 'operational':
|
|
132
|
+
// Verifier-side fault. Page ops; do NOT bounce
|
|
133
|
+
// back to the submitter as "fix your payload".
|
|
134
|
+
notifyOps(prefix, detail);
|
|
135
|
+
break;
|
|
136
|
+
case 'submission-bad':
|
|
137
|
+
// The payload failed a gate — surface to the
|
|
138
|
+
// submitter so they fix it and resubmit.
|
|
139
|
+
surfaceToSubmitter(prefix, detail);
|
|
140
|
+
break;
|
|
141
|
+
case 'ingest':
|
|
142
|
+
// Ingest-side scenario fetch. The typed reason in
|
|
143
|
+
// `detail` (timeout vs not_found/…) decides triage.
|
|
144
|
+
triageScenarioLoad(detail);
|
|
145
|
+
break;
|
|
146
|
+
default: // 'unknown'
|
|
147
|
+
// Unrecognized prefix — log + surface raw text.
|
|
148
|
+
log.warn('unknown rejection_reason', r);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
```
|
package/index.js
CHANGED
|
@@ -9,7 +9,19 @@
|
|
|
9
9
|
import { validateSubmissionSchema as _defaultValidateSubmissionSchema } from './validators/schema.js';
|
|
10
10
|
import { validatePolicy as _defaultValidatePolicy } from './validators/policy.js';
|
|
11
11
|
import { validateStepResults as _defaultValidateStepResults } from './validators/steps.js';
|
|
12
|
+
import { validateSchemaVersion as _defaultValidateSchemaVersion } from './validators/schema-version.js';
|
|
12
13
|
import { computeVerdict } from './validators/verdict.js';
|
|
14
|
+
import { SUPPORTED_SCHEMA_VERSIONS } from '@dogfood-lab/schemas';
|
|
15
|
+
|
|
16
|
+
// F1-CONTRACTS-003: re-export the rejection-reason classifier from the package
|
|
17
|
+
// root so consumers `import { parseRejectionReason } from '@dogfood-lab/verify'`
|
|
18
|
+
// instead of hand-rolling .startsWith() chains over the prefix taxonomy.
|
|
19
|
+
export { parseRejectionReason } from './parse-rejection.js';
|
|
20
|
+
|
|
21
|
+
// F1-CONTRACTS-001: the persisted record's `schema_version` is the SINGLE
|
|
22
|
+
// source of truth from the contract package — not a hardcoded literal that
|
|
23
|
+
// can drift from `SUPPORTED_SCHEMA_VERSIONS.record.current`.
|
|
24
|
+
const RECORD_SCHEMA_VERSION = SUPPORTED_SCHEMA_VERSIONS.record.current;
|
|
13
25
|
|
|
14
26
|
/**
|
|
15
27
|
* D1B-003 (Stage C humanization): the SOLE catch wrapper for synchronous
|
|
@@ -75,7 +87,7 @@ export async function verify(submission, options) {
|
|
|
75
87
|
// timing.finished_at). Mark _skipPersist so the ingest layer surfaces the
|
|
76
88
|
// rejection without crashing the persist layer with `invalid repo format: undefined`.
|
|
77
89
|
return {
|
|
78
|
-
schema_version:
|
|
90
|
+
schema_version: RECORD_SCHEMA_VERSION,
|
|
79
91
|
_skipPersist: true,
|
|
80
92
|
verification: {
|
|
81
93
|
status: 'rejected',
|
|
@@ -95,6 +107,7 @@ export async function verify(submission, options) {
|
|
|
95
107
|
const validateSubmissionSchema = validatorOverrides.validateSubmissionSchema || _defaultValidateSubmissionSchema;
|
|
96
108
|
const validateStepResults = validatorOverrides.validateStepResults || _defaultValidateStepResults;
|
|
97
109
|
const validatePolicy = validatorOverrides.validatePolicy || _defaultValidatePolicy;
|
|
110
|
+
const validateSchemaVersion = validatorOverrides.validateSchemaVersion || _defaultValidateSchemaVersion;
|
|
98
111
|
const now = new Date().toISOString();
|
|
99
112
|
const reasons = [];
|
|
100
113
|
|
|
@@ -134,6 +147,26 @@ export async function verify(submission, options) {
|
|
|
134
147
|
reasons.push(schemaRun.faultReason);
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
// 1b. schema_version VALUE gate (F1-CONTRACTS-001)
|
|
151
|
+
// The schema check above gates `schema_version` by PATTERN only. This gate
|
|
152
|
+
// compares the declared MAJOR against `SUPPORTED_SCHEMA_VERSIONS` (the
|
|
153
|
+
// single source of truth in @dogfood-lab/schemas) and refuses an
|
|
154
|
+
// incompatible major instead of silently mis-validating a future contract
|
|
155
|
+
// against the live 1.x schema. It runs INDEPENDENT of `schemaResult.valid`:
|
|
156
|
+
// a future-major payload may also fail shape, but the version refusal is the
|
|
157
|
+
// operator-actionable signal and must land regardless. The validator emits a
|
|
158
|
+
// fully-prefixed `CONTRACT_SCHEMA_TOO_NEW:` / `CONTRACT_SCHEMA_TOO_OLD:`
|
|
159
|
+
// reason; an unknown-contract throw surfaces as
|
|
160
|
+
// `VALIDATOR_FAULT_CONTRACT_SCHEMA_VERSION` via the same runValidator seam.
|
|
161
|
+
const versionRun = runValidator('contract_schema_version', () => validateSchemaVersion(submission, 'recordSubmission'));
|
|
162
|
+
if (versionRun.ok) {
|
|
163
|
+
if (!versionRun.result.valid) {
|
|
164
|
+
reasons.push(...versionRun.result.errors);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
reasons.push(versionRun.faultReason);
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
// 2. Reject if submission includes verifier-owned fields
|
|
138
171
|
const verifierFields = ['policy_version', 'verification'];
|
|
139
172
|
for (const field of verifierFields) {
|
|
@@ -219,7 +252,7 @@ export async function verify(submission, options) {
|
|
|
219
252
|
|
|
220
253
|
// 7. Assemble persisted record
|
|
221
254
|
const persisted = {
|
|
222
|
-
schema_version:
|
|
255
|
+
schema_version: RECORD_SCHEMA_VERSION,
|
|
223
256
|
policy_version: policyVersion,
|
|
224
257
|
run_id: submission.run_id,
|
|
225
258
|
repo: submission.repo,
|
package/package.json
CHANGED
package/validators/policy.js
CHANGED
|
@@ -156,7 +156,15 @@ export function validatePolicy(submission, { globalPolicy, repoPolicy }) {
|
|
|
156
156
|
|
|
157
157
|
if (ciReqs.coverage_min != null) {
|
|
158
158
|
const coverageCheck = submission.ci_checks?.find(c => c.kind === 'coverage');
|
|
159
|
-
|
|
159
|
+
// ci_checks[].value is OPTIONAL in the submission schema (only id/kind/status
|
|
160
|
+
// are required). A value-less coverage check must be treated the SAME as a
|
|
161
|
+
// missing one: at the trust boundary the verifier rejects incomplete proof.
|
|
162
|
+
// Without this guard, `undefined < coverage_min` is false (and a coerced
|
|
163
|
+
// null/NaN is likewise not a real measurement), so the gate silently passed
|
|
164
|
+
// with no measured coverage.
|
|
165
|
+
const measured = coverageCheck && typeof coverageCheck.value === 'number'
|
|
166
|
+
&& Number.isFinite(coverageCheck.value);
|
|
167
|
+
if (!coverageCheck || !measured) {
|
|
160
168
|
errors.push(
|
|
161
169
|
`surface[${surface}]: coverage_min is ${ciReqs.coverage_min}% but no coverage data provided`
|
|
162
170
|
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema_version VALUE gate — validators/schema-version.js
|
|
3
|
+
*
|
|
4
|
+
* F1-CONTRACTS-001 (Wave 2, HIGH). The contract JSON schemas gate
|
|
5
|
+
* `schema_version` by PATTERN only (`^\d+\.\d+\.\d+$`). Nothing compared the
|
|
6
|
+
* declared value against the set this build actually supports, so a
|
|
7
|
+
* submission declaring `schema_version: '2.0.0'` (or `99.0.0`) validated
|
|
8
|
+
* clean against the live 1.x schema whenever its shape happened to fit — a
|
|
9
|
+
* genuinely-incompatible future major was silently mis-validated instead of
|
|
10
|
+
* cleanly refused.
|
|
11
|
+
*
|
|
12
|
+
* This validator compares a payload's MAJOR against
|
|
13
|
+
* SUPPORTED_SCHEMA_VERSIONS (the single source of truth imported from
|
|
14
|
+
* `@dogfood-lab/schemas` — NOT a hand-copied literal) for the named contract
|
|
15
|
+
* and returns a TYPED rejection-reason string:
|
|
16
|
+
*
|
|
17
|
+
* - major > maxMajor → `CONTRACT_SCHEMA_TOO_NEW: ...` (operator must upgrade
|
|
18
|
+
* testing-os — this build does not understand the future contract).
|
|
19
|
+
* - major < minMajor → `CONTRACT_SCHEMA_TOO_OLD: ...` (submitter must
|
|
20
|
+
* re-emit against the current contract).
|
|
21
|
+
* - in range → no reason (PASS; a patch/minor delta inside the range
|
|
22
|
+
* is accepted — do NOT reject on a non-major bump).
|
|
23
|
+
* - malformed / absent version → no reason (the JSON-schema PATTERN gate
|
|
24
|
+
* already owns shape; this validator only owns the VALUE comparison and
|
|
25
|
+
* stays silent rather than double-reporting a shape fault).
|
|
26
|
+
*
|
|
27
|
+
* Unlike the `schema`/`policy`/`steps` validators (whose caller prepends a
|
|
28
|
+
* single fixed prefix), the TOO_NEW vs TOO_OLD discriminant is known HERE, at
|
|
29
|
+
* the branch that fired — so this validator emits the FULL prefixed reason
|
|
30
|
+
* string. `index.js` pushes it verbatim into `verification.rejection_reasons`
|
|
31
|
+
* via the SAME `runValidator` seam (a thrown unknown-contract fault still
|
|
32
|
+
* surfaces as `VALIDATOR_FAULT_CONTRACT_SCHEMA_VERSION`). The
|
|
33
|
+
* `CONTRACT_SCHEMA_*` prefixes join the documented prefix taxonomy (see
|
|
34
|
+
* verify/README.md → "Error shape").
|
|
35
|
+
*
|
|
36
|
+
* Return contract: `{ valid: boolean, errors: string[] }` — `errors` holds
|
|
37
|
+
* the already-prefixed `CONTRACT_SCHEMA_*: ...` reason(s).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { SUPPORTED_SCHEMA_VERSIONS } from '@dogfood-lab/schemas';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse the MAJOR component of a semver-ish string. Returns null when the
|
|
44
|
+
* value is not a `\d+\.\d+\.\d+` triple — the shape gate owns that fault.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} version
|
|
47
|
+
* @returns {number | null}
|
|
48
|
+
*/
|
|
49
|
+
function parseMajor(version) {
|
|
50
|
+
if (typeof version !== 'string') return null;
|
|
51
|
+
const m = version.match(/^(\d+)\.\d+\.\d+$/);
|
|
52
|
+
if (!m) return null;
|
|
53
|
+
return Number(m[1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gate a payload's declared `schema_version` against the supported MAJOR
|
|
58
|
+
* range for a named contract.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} payload - Payload carrying a `schema_version` field.
|
|
61
|
+
* @param {string} [contract='recordSubmission'] - SUPPORTED_SCHEMA_VERSIONS key.
|
|
62
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
63
|
+
*/
|
|
64
|
+
export function validateSchemaVersion(payload, contract = 'recordSubmission') {
|
|
65
|
+
// Only gate payloads that actually declare a schema_version. A null/absent
|
|
66
|
+
// value is the shape gate's concern, not the value gate's.
|
|
67
|
+
if (!payload || typeof payload !== 'object') {
|
|
68
|
+
return { valid: true, errors: [] };
|
|
69
|
+
}
|
|
70
|
+
const declared = payload.schema_version;
|
|
71
|
+
if (declared === undefined || declared === null) {
|
|
72
|
+
return { valid: true, errors: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const supported = SUPPORTED_SCHEMA_VERSIONS[contract];
|
|
76
|
+
if (!supported) {
|
|
77
|
+
// Unknown contract key is a programmer error in the call site, not a
|
|
78
|
+
// submission fault — throw so the runValidator seam surfaces it as a
|
|
79
|
+
// VALIDATOR_FAULT_* operational incident rather than a submission reason.
|
|
80
|
+
throw new Error(`validateSchemaVersion: unknown contract "${contract}"`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const major = parseMajor(declared);
|
|
84
|
+
if (major === null) {
|
|
85
|
+
// Malformed shape — defer to the JSON-schema PATTERN gate; do not
|
|
86
|
+
// double-report.
|
|
87
|
+
return { valid: true, errors: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (major > supported.maxMajor) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
errors: [
|
|
94
|
+
`CONTRACT_SCHEMA_TOO_NEW: ${contract} schema v${declared} but this build supports ` +
|
|
95
|
+
`v${supported.current} (major ${supported.minMajor}–${supported.maxMajor}) — upgrade testing-os`,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (major < supported.minMajor) {
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
errors: [
|
|
104
|
+
`CONTRACT_SCHEMA_TOO_OLD: ${contract} schema v${declared} is below the supported floor ` +
|
|
105
|
+
`v${supported.current} (major ${supported.minMajor}–${supported.maxMajor}) — re-emit against the current contract`,
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true, errors: [] };
|
|
111
|
+
}
|
package/validators/steps.js
CHANGED
|
@@ -25,7 +25,11 @@ export function validateStepResults(scenarioResult) {
|
|
|
25
25
|
return errors;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Must match the step_results[].status enum in BOTH dogfood-record-submission.schema.json
|
|
29
|
+
// and dogfood-record.schema.json (["pass","fail","blocked","skip","partial"]). `partial`
|
|
30
|
+
// is a first-class, contract-blessed step status (verdict.js ranks it 2/3); omitting it
|
|
31
|
+
// here falsely rejected schema-valid submissions as "unknown status".
|
|
32
|
+
const VALID_STATUSES = new Set(['pass', 'fail', 'blocked', 'skip', 'partial']);
|
|
29
33
|
|
|
30
34
|
for (let i = 0; i < step_results.length; i++) {
|
|
31
35
|
const step = step_results[i];
|