@dogfood-lab/schemas 1.5.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogfood-lab/schemas",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "JSON schemas for the testing-os contract spine — record, finding, pattern, recommendation, doctrine, policy, scenario, submission.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "devDependencies": {
36
36
  "@dogfood-lab/dogfood-swarm": "*",
37
37
  "@types/node": "^25.3.5",
38
- "vitest": "^4.1.6"
38
+ "vitest": "^4.1.9"
39
39
  },
40
40
  "author": "mcp-tool-shop",
41
41
  "license": "MIT",
@@ -100,7 +100,8 @@
100
100
 
101
101
  "ci_checks": {
102
102
  "type": "array",
103
- "description": "Machine-evaluated CI results. Optional — not all dogfood runs include CI.",
103
+ "maxItems": 200,
104
+ "description": "Machine-evaluated CI results. Optional — not all dogfood runs include CI. Capped at 200 (resource guard; a real run reports a handful of checks).",
104
105
  "items": {
105
106
  "type": "object",
106
107
  "required": ["id", "kind", "status"],
@@ -130,7 +131,8 @@
130
131
  "scenario_results": {
131
132
  "type": "array",
132
133
  "minItems": 1,
133
- "description": "Dogfood evidence. At least one scenario required.",
134
+ "maxItems": 1000,
135
+ "description": "Dogfood evidence. At least one scenario required. Capped at 1000 (resource guard for the policy engine, which evaluates per-scenario rules synchronously; a real run reports far fewer).",
134
136
  "items": {
135
137
  "type": "object",
136
138
  "required": ["scenario_id", "product_surface", "execution_mode", "verdict", "step_results"],
@@ -165,10 +167,17 @@
165
167
  "type": "string",
166
168
  "description": "Required when verdict is blocked. Verifier enforces."
167
169
  },
170
+ "tags": {
171
+ "type": "array",
172
+ "maxItems": 100,
173
+ "items": { "type": "string" },
174
+ "description": "Free-form scenario tags (e.g. 'smoke', 'release', 'wip', 'flaky', 'skip-ci'). Optional. Capped at 100 (resource guard). Policy may gate on these via a surface's evidence_requirements.forbidden_tags / required_tags."
175
+ },
168
176
  "step_results": {
169
177
  "type": "array",
170
178
  "minItems": 1,
171
- "description": "Per-step execution results. Bridges scenario steps to record evidence.",
179
+ "maxItems": 500,
180
+ "description": "Per-step execution results. Bridges scenario steps to record evidence. Capped at 500 (resource guard).",
172
181
  "items": {
173
182
  "type": "object",
174
183
  "required": ["step_id", "status"],
@@ -189,6 +198,7 @@
189
198
  },
190
199
  "evidence": {
191
200
  "type": "array",
201
+ "maxItems": 100,
192
202
  "items": {
193
203
  "type": "object",
194
204
  "required": ["kind", "url"],
@@ -173,6 +173,11 @@
173
173
  "type": "string",
174
174
  "description": "Required when verdict is blocked. Verifier enforces."
175
175
  },
176
+ "tags": {
177
+ "type": "array",
178
+ "items": { "type": "string" },
179
+ "description": "Free-form scenario tags (e.g. 'smoke', 'release', 'wip', 'flaky', 'skip-ci'). Carried verbatim from the submission. Policy gates on these via a surface's evidence_requirements.forbidden_tags / required_tags."
180
+ },
176
181
  "step_results": {
177
182
  "type": "array",
178
183
  "minItems": 1,
@@ -282,6 +287,11 @@
282
287
  "items": { "type": "string" },
283
288
  "description": "Machine-readable reasons for rejection. Empty if accepted."
284
289
  },
290
+ "warnings": {
291
+ "type": "array",
292
+ "items": { "type": "string" },
293
+ "description": "Accepted-with-warning notes from warn-severity policy rules. A populated warnings array does NOT imply rejection — the record was accepted. Optional and backward-compatible: records written before this channel existed have no warnings block. Mirrors the rejection_reasons shape (one machine-readable string per matched warn-rule)."
294
+ },
285
295
  "provenance_remediation": {
286
296
  "type": "object",
287
297
  "properties": {
@@ -54,7 +54,8 @@
54
54
 
55
55
  "defaults": {
56
56
  "$ref": "#/$defs/surface_policy",
57
- "description": "Default surface policy. Used when a surface has no explicit policy."
57
+ "description": "Default surface policy. Used when a surface has no explicit policy. VERIFY-F1: `custom_rules` is NOT permitted here — it is a repo-surface concern only. Global, non-overridable declarative rules belong in `global_rules` (which fail operationally, not submission-bad). Allowing custom_rules under defaults would make a global-origin predicate fault classify as submission-bad.",
58
+ "properties": { "custom_rules": false }
58
59
  },
59
60
 
60
61
  "global_rules": {
@@ -74,6 +75,19 @@
74
75
  "type": "string",
75
76
  "enum": ["reject", "warn", "info"],
76
77
  "description": "reject: fails verification. warn: accepted with warning. info: logged only."
78
+ },
79
+ "scope": {
80
+ "type": "string",
81
+ "enum": ["submission", "scenario_result"],
82
+ "description": "VERIFY-F1: evaluation scope of the `when` predicate. submission (default) evaluates against the whole submission and emits one reason if matched. scenario_result evaluates each scenario_result independently and emits one reason per offending element, in array order. Ignored when `when` is absent (the rule falls to the built-in id switch). See docs/policy-dsl.md."
83
+ },
84
+ "when": {
85
+ "$ref": "#/$defs/predicate",
86
+ "description": "VERIFY-F1: optional declarative VIOLATION predicate. The rule fires (per its severity) when this evaluates true. Absent => the rule is enforced by the built-in id switch instead. See docs/policy-dsl.md."
87
+ },
88
+ "reason_template": {
89
+ "type": "string",
90
+ "description": "VERIFY-F1: optional per-rule reason body. {slot} placeholders interpolate fields of the matched element (scenario_result scope) or submission (submission scope), raw and unescaped. The engine prepends `[<id>] `. Absent => the body is the rule's `description`."
77
91
  }
78
92
  }
79
93
  }
@@ -225,11 +239,153 @@
225
239
  "min_evidence_count": {
226
240
  "type": "integer",
227
241
  "minimum": 0,
228
- "description": "Minimum number of evidence items across all scenarios."
242
+ "description": "Minimum number of evidence items required PER scenario. The verifier enforces this against each scenario_result independently, not as a sum across all scenarios."
243
+ },
244
+ "forbidden_tags": {
245
+ "type": "array",
246
+ "items": { "type": "string" },
247
+ "description": "Scenario tags that REJECT a scenario_result on this surface. A scenario_result whose tags include any of these (e.g. 'wip', 'flaky', 'skip-ci') fails policy. Enforced per scenario_result independently."
248
+ },
249
+ "required_tags": {
250
+ "type": "array",
251
+ "items": { "type": "string" },
252
+ "description": "Scenario tags that EVERY scenario_result on this surface must carry. A scenario_result missing any of these (or carrying no tags at all) fails policy. Enforced per scenario_result independently."
253
+ }
254
+ }
255
+ },
256
+ "custom_rules": {
257
+ "type": "array",
258
+ "description": "VERIFY-F1: declarative per-scenario_result rules for this surface. Each rule's `when` predicate is evaluated against every scenario_result whose product_surface matches; a match fires the rule per its severity. Custom rules can only ADD constraints (reject/warn/info) — there is no accept/except verb, so a repo policy can never weaken a non-overridable global gate. See docs/policy-dsl.md.",
259
+ "items": {
260
+ "type": "object",
261
+ "required": ["id", "severity", "when"],
262
+ "additionalProperties": false,
263
+ "properties": {
264
+ "id": {
265
+ "type": "string",
266
+ "description": "Stable rule ID for reporting and diagnostics."
267
+ },
268
+ "description": { "type": "string" },
269
+ "severity": {
270
+ "type": "string",
271
+ "enum": ["reject", "warn", "info"],
272
+ "description": "reject: fails verification. warn: accepted with warning. info: logged only. There is intentionally no 'accept' value — custom rules are additive-only (non-weakening)."
273
+ },
274
+ "when": {
275
+ "$ref": "#/$defs/predicate",
276
+ "description": "The declarative VIOLATION predicate, evaluated per scenario_result on this surface. The rule fires when this evaluates true."
277
+ },
278
+ "reason_template": {
279
+ "type": "string",
280
+ "description": "Optional per-rule reason body. {slot} placeholders interpolate the matched scenario_result's fields, raw and unescaped. The engine prepends `[<id>] `. Absent => the body is the rule's `description`."
281
+ }
229
282
  }
230
283
  }
231
284
  }
232
285
  }
286
+ },
287
+
288
+ "predicate": {
289
+ "description": "VERIFY-F1: a bounded, no-eval predicate node. Exactly one form per node: a leaf {field, op, value?} or a combinator (all / any / not / implies). Combinator depth is additionally capped at 5 by the evaluator (the DoS floor; not expressible in JSON Schema). See docs/policy-dsl.md.",
290
+ "oneOf": [
291
+ { "$ref": "#/$defs/predicate_leaf" },
292
+ { "$ref": "#/$defs/predicate_all" },
293
+ { "$ref": "#/$defs/predicate_any" },
294
+ { "$ref": "#/$defs/predicate_not" },
295
+ { "$ref": "#/$defs/predicate_implies" }
296
+ ]
297
+ },
298
+
299
+ "predicate_leaf": {
300
+ "type": "object",
301
+ "additionalProperties": false,
302
+ "required": ["field", "op"],
303
+ "properties": {
304
+ "field": {
305
+ "type": "string",
306
+ "description": "Dotted field path into the evaluated data; a '[]' segment means 'any element'. The segments __proto__, constructor, and prototype are forbidden (prototype-pollution guard).",
307
+ "pattern": "^(?!.*(?:^|\\.)(?:__proto__|constructor|prototype)(?:$|\\.|\\[))[A-Za-z_][A-Za-z0-9_]*(?:\\[\\])?(?:\\.[A-Za-z_][A-Za-z0-9_]*(?:\\[\\])?)*$"
308
+ },
309
+ "op": {
310
+ "type": "string",
311
+ "enum": ["equals", "not_equals", "in", "not_in", "contains", "not_contains", "exists", "not_exists", "gt", "gte", "lt", "lte"],
312
+ "description": "Closed operator set. exists/not_exists are truthiness (not strict key-presence) and take no value."
313
+ },
314
+ "value": {
315
+ "description": "Comparand. Omitted for exists/not_exists. An array for in/not_in. A scalar otherwise. Numeric for gt/gte/lt/lte (a non-numeric resolved field is an eval-time policy-config: fault)."
316
+ }
317
+ },
318
+ "allOf": [
319
+ {
320
+ "if": { "required": ["op"], "properties": { "op": { "enum": ["exists", "not_exists"] } } },
321
+ "then": { "not": { "required": ["value"] } }
322
+ },
323
+ {
324
+ "if": { "required": ["op"], "properties": { "op": { "enum": ["equals", "not_equals", "in", "not_in", "contains", "not_contains", "gt", "gte", "lt", "lte"] } } },
325
+ "then": { "required": ["value"] }
326
+ },
327
+ {
328
+ "if": { "required": ["op"], "properties": { "op": { "enum": ["in", "not_in"] } } },
329
+ "then": { "properties": { "value": { "type": "array" } } }
330
+ }
331
+ ]
332
+ },
333
+
334
+ "predicate_all": {
335
+ "type": "object",
336
+ "additionalProperties": false,
337
+ "required": ["all"],
338
+ "properties": {
339
+ "all": {
340
+ "type": "array",
341
+ "minItems": 1,
342
+ "maxItems": 64,
343
+ "description": "Logical AND. True when every child predicate is true. Width capped at 64 (DoS guard; the evaluator additionally enforces a node-visit budget).",
344
+ "items": { "$ref": "#/$defs/predicate" }
345
+ }
346
+ }
347
+ },
348
+
349
+ "predicate_any": {
350
+ "type": "object",
351
+ "additionalProperties": false,
352
+ "required": ["any"],
353
+ "properties": {
354
+ "any": {
355
+ "type": "array",
356
+ "minItems": 1,
357
+ "maxItems": 64,
358
+ "description": "Logical OR. True when at least one child predicate is true. Width capped at 64 (DoS guard; the evaluator additionally enforces a node-visit budget).",
359
+ "items": { "$ref": "#/$defs/predicate" }
360
+ }
361
+ }
362
+ },
363
+
364
+ "predicate_not": {
365
+ "type": "object",
366
+ "additionalProperties": false,
367
+ "required": ["not"],
368
+ "properties": {
369
+ "not": {
370
+ "description": "Logical NOT.",
371
+ "$ref": "#/$defs/predicate"
372
+ }
373
+ }
374
+ },
375
+
376
+ "predicate_implies": {
377
+ "type": "object",
378
+ "additionalProperties": false,
379
+ "required": ["implies"],
380
+ "properties": {
381
+ "implies": {
382
+ "type": "array",
383
+ "minItems": 2,
384
+ "maxItems": 2,
385
+ "description": "Sugar for [antecedent, consequent]: 'antecedent requires consequent'. As a VIOLATION predicate it desugars to all(antecedent, not(consequent)) — it matches the inputs that break the implication (antecedent holds but consequent does not).",
386
+ "items": { "$ref": "#/$defs/predicate" }
387
+ }
388
+ }
233
389
  }
234
390
  }
235
391
  }