backend-skeleton 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.
Files changed (40) hide show
  1. package/README.md +113 -8
  2. package/bin/bskel.mjs +549 -63
  3. package/contracts/completeness.mjs +12 -1
  4. package/contracts/openapi.mjs +125 -18
  5. package/handles/providers/java-spring/ast-bridge.mjs +85 -1
  6. package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +407 -0
  7. package/handles/providers/java-spring/emit.mjs +126 -6
  8. package/handles/providers/java-spring/plan.mjs +220 -74
  9. package/handles/providers/java-spring/source-splice.mjs +477 -0
  10. package/handles/providers/java-spring/templates/AuthorizationPolicyStub.java.tmpl +30 -0
  11. package/handles/providers/java-spring/templates/HandleController.java.tmpl +21 -3
  12. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +26 -0
  13. package/handles/providers/java-spring/templates/ResourceResolverPolicyStub.java.tmpl +9 -0
  14. package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +3 -3
  15. package/lib/attest.mjs +59 -1
  16. package/lib/cli.mjs +125 -7
  17. package/lib/decision-log.mjs +58 -0
  18. package/lib/doctor.mjs +23 -0
  19. package/lib/exit-codes.mjs +17 -0
  20. package/lib/gate-definitions.mjs +65 -2
  21. package/lib/gate-export.mjs +250 -0
  22. package/lib/impact-export-graphify.mjs +145 -0
  23. package/lib/impact-graph.mjs +194 -0
  24. package/lib/impact-surface.mjs +158 -0
  25. package/lib/impact.mjs +334 -0
  26. package/lib/patch-kinds.mjs +24 -0
  27. package/lib/repo.mjs +46 -0
  28. package/lib/workflow.mjs +16 -0
  29. package/package.json +1 -1
  30. package/scanners/adapters/_java-spring-analyzer.mjs +6 -0
  31. package/schemas/decision-event.schema.json +46 -0
  32. package/schemas/gate-attestation.schema.json +6 -1
  33. package/schemas/gate-export.schema.json +606 -22
  34. package/schemas/handles-plan.schema.json +32 -0
  35. package/schemas/impact-baseline.schema.json +59 -0
  36. package/schemas/impact-graph.schema.json +53 -0
  37. package/schemas/impact-report.schema.json +86 -0
  38. package/schemas/impact-resolution.schema.json +33 -0
  39. package/schemas/java-source-splice.schema.json +84 -0
  40. package/schemas/patch-transaction.schema.json +87 -2
@@ -1,23 +1,279 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:sbf:gate-export:1",
3
+ "$id": "urn:sbf:gate-export:2",
4
4
  "title": "backend-skeleton gate export report (bskel gate export)",
5
- "description": "D-gate-attestation-signing: this schema was written FROM cmdGateExport's real, already-shipped report shape (bin/bskel.mjs) -- it did not exist before this item and the report itself is unchanged by adding it. `current` mirrors schemas/state.schema.json's own per-gate record shape exactly (the same raw, stored record getGate() returns -- status is constrained to the 3 values ever WRITTEN to disk, never the read-time-derived not_run/stale/pass (forced)). `additionalProperties` is intentionally open on `gates` itself (keyed by gate name) so this schema never needs editing when a new gate is added -- GATE_NAMES in lib/gate-definitions.mjs is the one place that list lives.",
5
+ "description": "D-attestation-payload-completeness: additive bump from sbf.gate-export/1 (D-gate-attestation-signing) -- every /1 field keeps its exact name, position, and meaning; new content (tool, artifacts, decisions, verdict, gates.*.live, and 5 new git.* fields) is added alongside. `current` mirrors schemas/state.schema.json's own per-gate record shape exactly (the raw, stored record getGate() returns -- status constrained to the 3 values ever WRITTEN to disk). `gates.*.live` is RECOMPUTED at export time (lib/verify.mjs's collectGateStatuses(), the same function `bskel verify` uses) so `current` (what was last written) and `live` (what is honestly true right now) can be compared side by side -- a gate whose stored record says pass but whose inputs have since changed shows current.status:\"pass\" and live.status:\"stale\" together. `additionalProperties` stays open on `gates` itself (keyed by gate name) so this schema never needs editing when a new gate is added -- GATE_NAMES in lib/gate-definitions.mjs is the one place that list lives.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["schema", "feature_id", "generated_at", "git", "gates"],
8
+ "required": [
9
+ "schema",
10
+ "feature_id",
11
+ "generated_at",
12
+ "tool",
13
+ "git",
14
+ "artifacts",
15
+ "gates",
16
+ "decisions",
17
+ "verdict"
18
+ ],
9
19
  "properties": {
10
- "schema": { "const": "sbf.gate-export/1" },
11
- "feature_id": { "type": "string", "pattern": "^[0-9]{3}-[a-z0-9]+(-[a-z0-9]+)*$" },
12
- "generated_at": { "type": "string", "format": "date-time" },
20
+ "schema": {
21
+ "const": "sbf.gate-export/4"
22
+ },
23
+ "feature_id": {
24
+ "type": "string",
25
+ "pattern": "^[0-9]{3}-[a-z0-9]+(-[a-z0-9]+)*$"
26
+ },
27
+ "generated_at": {
28
+ "type": "string",
29
+ "format": "date-time"
30
+ },
31
+ "tool": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": [
35
+ "name",
36
+ "version",
37
+ "gate_names",
38
+ "canonicalization"
39
+ ],
40
+ "properties": {
41
+ "name": {
42
+ "const": "bskel"
43
+ },
44
+ "version": {
45
+ "type": "string",
46
+ "description": "backend-skeleton's own package.json version -- lib/gate-export.mjs's toolVersion(), never the TARGET repo's version."
47
+ },
48
+ "gate_names": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ },
53
+ "description": "GATE_NAMES at export time -- lets a verifier on a different bskel build see that the signer's gate set differs from its own."
54
+ },
55
+ "canonicalization": {
56
+ "const": "sortkeysdeep-json",
57
+ "description": "lib/attest.mjs's CANONICALIZATION_ID -- the named algorithm this payload was canonicalized with before signing."
58
+ }
59
+ }
60
+ },
13
61
  "git": {
14
62
  "type": "object",
15
63
  "additionalProperties": false,
16
- "required": ["branch", "head_sha", "dirty"],
64
+ "required": [
65
+ "branch",
66
+ "head_sha",
67
+ "dirty",
68
+ "head_tree_sha",
69
+ "dirty_acknowledged",
70
+ "dirty_file_count",
71
+ "dirty_files_truncated",
72
+ "dirty_files"
73
+ ],
17
74
  "properties": {
18
- "branch": { "type": ["string", "null"], "description": "lib/repo.mjs's currentBranch() -- null on failure, or the literal string \"HEAD\" for a detached HEAD (not a real branch name)." },
19
- "head_sha": { "type": "string" },
20
- "dirty": { "type": ["boolean", "null"], "description": "lib/repo.mjs's isDirty() -- null on failure. true covers BOTH modified tracked files and untracked files (git status --porcelain, no -uno)." }
75
+ "branch": {
76
+ "type": [
77
+ "string",
78
+ "null"
79
+ ],
80
+ "description": "lib/repo.mjs's currentBranch() -- null on failure, or the literal string \"HEAD\" for a detached HEAD (not a real branch name)."
81
+ },
82
+ "head_sha": {
83
+ "type": "string"
84
+ },
85
+ "dirty": {
86
+ "type": [
87
+ "boolean",
88
+ "null"
89
+ ],
90
+ "description": "lib/repo.mjs's isDirty()-equivalent (derived from worktreeStatus()) -- null on failure. true covers BOTH modified tracked files and untracked files (git status --porcelain, no -uno)."
91
+ },
92
+ "head_tree_sha": {
93
+ "type": [
94
+ "string",
95
+ "null"
96
+ ],
97
+ "description": "git rev-parse HEAD^{tree} -- the CONTENT identity of HEAD, distinct from head_sha (its commit identity). null on git failure."
98
+ },
99
+ "dirty_acknowledged": {
100
+ "type": "boolean",
101
+ "description": "true iff `--allow-dirty` was passed to `gate export --sign` for THIS export -- see D-attestation-payload-completeness K4. Always false for an unsigned export."
102
+ },
103
+ "dirty_file_count": {
104
+ "type": "integer",
105
+ "minimum": 0
106
+ },
107
+ "dirty_files_truncated": {
108
+ "type": "boolean",
109
+ "description": "true iff dirty_file_count exceeds the cap and dirty_files below was truncated."
110
+ },
111
+ "dirty_files": {
112
+ "type": "array",
113
+ "description": "sorted by path, capped (see dirty_files_truncated). Deliberately NO file content hashes -- see D-attestation-payload-completeness's EXIT list.",
114
+ "items": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": [
118
+ "status",
119
+ "path"
120
+ ],
121
+ "properties": {
122
+ "status": {
123
+ "type": "string",
124
+ "description": "the raw 2-character `git status --porcelain` status code (e.g. \" M\", \"??\", \"R \")."
125
+ },
126
+ "path": {
127
+ "type": "string"
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ },
134
+ "artifacts": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "description": "D-attestation-payload-completeness (K2/K8): every key here comes from lib/gate-export.mjs's ARTIFACT_SOURCES table, unconditionally -- hashed FRESH at export time, independent of whether any gate ever passed. sha256 hex digest, or null when the file does not exist.",
138
+ "required": [
139
+ "scan_report_hash",
140
+ "spec_hash",
141
+ "contract_hash",
142
+ "contract_resolution_hash",
143
+ "openapi_snapshot_hash",
144
+ "cross_feature_report_hash",
145
+ "cross_feature_resolution_hash",
146
+ "dependencies_hash",
147
+ "rules_source_hash",
148
+ "rules_compiled_hash",
149
+ "conformance_report_hash",
150
+ "handles_manifest_hash",
151
+ "stack_record_hash",
152
+ "impact_baseline_hash",
153
+ "impact_report_hash",
154
+ "impact_resolution_hash",
155
+ "patch_approvals_hash"
156
+ ],
157
+ "properties": {
158
+ "scan_report_hash": {
159
+ "type": [
160
+ "string",
161
+ "null"
162
+ ],
163
+ "pattern": "^[0-9a-f]{64}$"
164
+ },
165
+ "spec_hash": {
166
+ "type": [
167
+ "string",
168
+ "null"
169
+ ],
170
+ "pattern": "^[0-9a-f]{64}$"
171
+ },
172
+ "contract_hash": {
173
+ "type": [
174
+ "string",
175
+ "null"
176
+ ],
177
+ "pattern": "^[0-9a-f]{64}$"
178
+ },
179
+ "contract_resolution_hash": {
180
+ "type": [
181
+ "string",
182
+ "null"
183
+ ],
184
+ "pattern": "^[0-9a-f]{64}$"
185
+ },
186
+ "openapi_snapshot_hash": {
187
+ "type": [
188
+ "string",
189
+ "null"
190
+ ],
191
+ "pattern": "^[0-9a-f]{64}$"
192
+ },
193
+ "cross_feature_report_hash": {
194
+ "type": [
195
+ "string",
196
+ "null"
197
+ ],
198
+ "pattern": "^[0-9a-f]{64}$"
199
+ },
200
+ "cross_feature_resolution_hash": {
201
+ "type": [
202
+ "string",
203
+ "null"
204
+ ],
205
+ "pattern": "^[0-9a-f]{64}$"
206
+ },
207
+ "dependencies_hash": {
208
+ "type": [
209
+ "string",
210
+ "null"
211
+ ],
212
+ "pattern": "^[0-9a-f]{64}$"
213
+ },
214
+ "rules_source_hash": {
215
+ "type": [
216
+ "string",
217
+ "null"
218
+ ],
219
+ "pattern": "^[0-9a-f]{64}$"
220
+ },
221
+ "rules_compiled_hash": {
222
+ "type": [
223
+ "string",
224
+ "null"
225
+ ],
226
+ "pattern": "^[0-9a-f]{64}$"
227
+ },
228
+ "conformance_report_hash": {
229
+ "type": [
230
+ "string",
231
+ "null"
232
+ ],
233
+ "pattern": "^[0-9a-f]{64}$"
234
+ },
235
+ "handles_manifest_hash": {
236
+ "type": [
237
+ "string",
238
+ "null"
239
+ ],
240
+ "pattern": "^[0-9a-f]{64}$"
241
+ },
242
+ "stack_record_hash": {
243
+ "type": [
244
+ "string",
245
+ "null"
246
+ ],
247
+ "pattern": "^[0-9a-f]{64}$"
248
+ },
249
+ "impact_baseline_hash": {
250
+ "type": [
251
+ "string",
252
+ "null"
253
+ ],
254
+ "pattern": "^[0-9a-f]{64}$"
255
+ },
256
+ "impact_report_hash": {
257
+ "type": [
258
+ "string",
259
+ "null"
260
+ ],
261
+ "pattern": "^[0-9a-f]{64}$"
262
+ },
263
+ "impact_resolution_hash": {
264
+ "type": [
265
+ "string",
266
+ "null"
267
+ ],
268
+ "pattern": "^[0-9a-f]{64}$"
269
+ },
270
+ "patch_approvals_hash": {
271
+ "type": [
272
+ "string",
273
+ "null"
274
+ ],
275
+ "pattern": "^[0-9a-f]{64}$"
276
+ }
21
277
  }
22
278
  },
23
279
  "gates": {
@@ -25,34 +281,362 @@
25
281
  "additionalProperties": {
26
282
  "type": "object",
27
283
  "additionalProperties": false,
28
- "required": ["scope", "current", "history"],
284
+ "required": [
285
+ "scope",
286
+ "current",
287
+ "live",
288
+ "history"
289
+ ],
29
290
  "properties": {
30
- "scope": { "type": "string", "description": "the resolved scope id this gate was read from -- REPO_GATE_ID (\"_repo\") for a repo-scoped gate, or the feature_id itself." },
291
+ "scope": {
292
+ "type": "string",
293
+ "description": "the resolved scope id this gate was read from -- REPO_GATE_ID (\"_repo\") for a repo-scoped gate, or the feature_id itself."
294
+ },
31
295
  "current": {
32
296
  "anyOf": [
33
- { "type": "null" },
297
+ {
298
+ "type": "null"
299
+ },
34
300
  {
35
301
  "type": "object",
36
302
  "additionalProperties": false,
37
- "required": ["status", "token", "at"],
303
+ "required": [
304
+ "status",
305
+ "token",
306
+ "at"
307
+ ],
38
308
  "properties": {
39
- "status": { "enum": ["pass", "awaiting_disposition", "revoked"] },
40
- "token": { "type": "string" },
41
- "at": { "type": "string" },
42
- "forced": { "type": "boolean" },
43
- "reason": { "type": "string" },
44
- "evidence": { "type": "object" },
45
- "inputs": { "type": "object" }
309
+ "status": {
310
+ "enum": [
311
+ "pass",
312
+ "awaiting_disposition",
313
+ "revoked"
314
+ ]
315
+ },
316
+ "token": {
317
+ "type": "string"
318
+ },
319
+ "at": {
320
+ "type": "string"
321
+ },
322
+ "forced": {
323
+ "type": "boolean"
324
+ },
325
+ "reason": {
326
+ "type": "string"
327
+ },
328
+ "evidence": {
329
+ "type": "object"
330
+ },
331
+ "inputs": {
332
+ "type": "object"
333
+ }
46
334
  }
47
335
  }
48
336
  ]
49
337
  },
338
+ "live": {
339
+ "type": "object",
340
+ "additionalProperties": false,
341
+ "required": [
342
+ "policy",
343
+ "status",
344
+ "blocking",
345
+ "ran",
346
+ "current_token",
347
+ "stale_reason",
348
+ "changed_inputs"
349
+ ],
350
+ "description": "RECOMPUTED at export time via the same lib/verify.mjs#collectGateStatuses() `bskel verify` uses -- the honest, read-time-derived verdict, as opposed to `current` (the raw stored record).",
351
+ "properties": {
352
+ "policy": {
353
+ "enum": [
354
+ "required",
355
+ "required-when-present"
356
+ ]
357
+ },
358
+ "status": {
359
+ "enum": [
360
+ "pass",
361
+ "pass (forced)",
362
+ "stale",
363
+ "not_run",
364
+ "awaiting_disposition",
365
+ "revoked"
366
+ ]
367
+ },
368
+ "blocking": {
369
+ "type": "boolean",
370
+ "description": "whether THIS gate's current live status blocks bskel verify's overall verdict (lib/verify.mjs#isBlockingGateResult)."
371
+ },
372
+ "ran": {
373
+ "type": "boolean"
374
+ },
375
+ "current_token": {
376
+ "type": [
377
+ "string",
378
+ "null"
379
+ ]
380
+ },
381
+ "stale_reason": {
382
+ "type": [
383
+ "string",
384
+ "null"
385
+ ],
386
+ "enum": [
387
+ "inputs_changed",
388
+ "no_recorded_inputs",
389
+ "recorded_inputs_mismatch",
390
+ "unknown",
391
+ "ttl_expired",
392
+ "invalid_timestamp",
393
+ null
394
+ ]
395
+ },
396
+ "changed_inputs": {
397
+ "type": [
398
+ "array",
399
+ "null"
400
+ ],
401
+ "items": {
402
+ "type": "string"
403
+ }
404
+ }
405
+ }
406
+ },
50
407
  "history": {
51
408
  "type": "array",
52
- "items": { "type": "object" }
409
+ "items": {
410
+ "type": "object"
411
+ }
412
+ }
413
+ }
414
+ }
415
+ },
416
+ "decisions": {
417
+ "type": "object",
418
+ "additionalProperties": false,
419
+ "required": [
420
+ "forced",
421
+ "revoked",
422
+ "waiver_files",
423
+ "records",
424
+ "event_counts"
425
+ ],
426
+ "description": "D-attestation-payload-completeness (K2): forced/revoked rolled up purely from the gate records already in `gates` above -- zero new file reads. D-decision-event-log (D7) additive, sbf.gate-export/3 -> /4: `records`/`event_counts` cover the four spec-side decision files (contract waivers, cross-feature waivers, impact dispositions, patch approvals) -- a /3 attestation still verifies (K7's own promise), it just won't have these two keys.",
427
+ "properties": {
428
+ "forced": {
429
+ "type": "array",
430
+ "items": {
431
+ "type": "object",
432
+ "additionalProperties": false,
433
+ "required": [
434
+ "gate",
435
+ "scope",
436
+ "reason",
437
+ "at"
438
+ ],
439
+ "properties": {
440
+ "gate": {
441
+ "type": "string"
442
+ },
443
+ "scope": {
444
+ "type": "string"
445
+ },
446
+ "reason": {
447
+ "type": [
448
+ "string",
449
+ "null"
450
+ ]
451
+ },
452
+ "at": {
453
+ "type": [
454
+ "string",
455
+ "null"
456
+ ]
457
+ }
458
+ }
459
+ }
460
+ },
461
+ "revoked": {
462
+ "type": "array",
463
+ "items": {
464
+ "type": "object",
465
+ "additionalProperties": false,
466
+ "required": [
467
+ "gate",
468
+ "scope",
469
+ "reason",
470
+ "at"
471
+ ],
472
+ "properties": {
473
+ "gate": {
474
+ "type": "string"
475
+ },
476
+ "scope": {
477
+ "type": "string"
478
+ },
479
+ "reason": {
480
+ "type": [
481
+ "string",
482
+ "null"
483
+ ]
484
+ },
485
+ "at": {
486
+ "type": [
487
+ "string",
488
+ "null"
489
+ ]
490
+ }
491
+ }
492
+ }
493
+ },
494
+ "waiver_files": {
495
+ "type": "object",
496
+ "additionalProperties": false,
497
+ "required": [
498
+ "contract_resolution",
499
+ "cross_feature_resolution",
500
+ "impact_resolution",
501
+ "patch_approvals"
502
+ ],
503
+ "description": "presence + hash only -- the CONTENT is already bound via `artifacts` above, and again (live-evaluated) via `records` below, never embedded here a second time.",
504
+ "properties": {
505
+ "contract_resolution": { "$ref": "#/$defs/waiverFileRef" },
506
+ "cross_feature_resolution": { "$ref": "#/$defs/waiverFileRef" },
507
+ "impact_resolution": { "$ref": "#/$defs/waiverFileRef" },
508
+ "patch_approvals": { "$ref": "#/$defs/waiverFileRef" }
509
+ }
510
+ },
511
+ "records": {
512
+ "type": "object",
513
+ "additionalProperties": false,
514
+ "required": [
515
+ "contract_waivers",
516
+ "cross_feature_waivers",
517
+ "impact_dispositions",
518
+ "patch_approvals"
519
+ ],
520
+ "description": "D-decision-event-log (D7): LIVE-evaluated entries only (expired contract/impact entries excluded, mirroring gates.*.live's current-vs-live split) -- never the raw stored array, so an expired-but-not-yet-renewed waiver never reads as still covering its warning inside a signed attestation.",
521
+ "properties": {
522
+ "contract_waivers": {
523
+ "type": "array",
524
+ "items": {
525
+ "type": "object",
526
+ "additionalProperties": false,
527
+ "required": ["code", "subject", "reason", "at", "expires_at"],
528
+ "properties": {
529
+ "code": { "type": "string" },
530
+ "subject": { "type": ["string", "null"] },
531
+ "reason": { "type": "string" },
532
+ "at": { "type": "string" },
533
+ "expires_at": { "type": ["string", "null"] }
534
+ }
535
+ }
536
+ },
537
+ "cross_feature_waivers": {
538
+ "type": "array",
539
+ "items": {
540
+ "type": "object",
541
+ "additionalProperties": false,
542
+ "required": ["signal", "identifier", "other_feature", "reason", "at"],
543
+ "properties": {
544
+ "signal": { "type": "string" },
545
+ "identifier": { "type": "string" },
546
+ "other_feature": { "type": "string" },
547
+ "reason": { "type": "string" },
548
+ "at": { "type": "string" }
549
+ }
550
+ }
551
+ },
552
+ "impact_dispositions": {
553
+ "type": "array",
554
+ "items": {
555
+ "type": "object",
556
+ "additionalProperties": false,
557
+ "required": ["change_key", "downstream_feature", "mode", "reason", "tracked_by", "expires_at", "acknowledged"],
558
+ "properties": {
559
+ "change_key": { "type": "string" },
560
+ "downstream_feature": { "type": "string" },
561
+ "mode": { "type": "string" },
562
+ "reason": { "type": "string" },
563
+ "tracked_by": { "type": ["string", "null"] },
564
+ "expires_at": { "type": ["string", "null"] },
565
+ "acknowledged": { "type": "boolean" }
566
+ }
567
+ }
568
+ },
569
+ "patch_approvals": {
570
+ "type": "array",
571
+ "items": {
572
+ "type": "object",
573
+ "additionalProperties": false,
574
+ "required": ["resource", "field", "strategy", "reason", "at"],
575
+ "properties": {
576
+ "resource": { "type": "string" },
577
+ "field": { "type": "string" },
578
+ "strategy": { "type": "string" },
579
+ "reason": { "type": "string" },
580
+ "at": { "type": "string" }
581
+ }
582
+ }
583
+ }
584
+ }
585
+ },
586
+ "event_counts": {
587
+ "type": "object",
588
+ "additionalProperties": false,
589
+ "required": ["contract_waiver", "cross_feature_waiver", "impact_disposition", "patch_approval"],
590
+ "description": "total decision-log events per kind (all actions combined) -- an all-zero count is informative (no log file, or no events of this kind), distinct from the file not existing at all.",
591
+ "properties": {
592
+ "contract_waiver": { "type": "integer", "minimum": 0 },
593
+ "cross_feature_waiver": { "type": "integer", "minimum": 0 },
594
+ "impact_disposition": { "type": "integer", "minimum": 0 },
595
+ "patch_approval": { "type": "integer", "minimum": 0 }
53
596
  }
54
597
  }
55
598
  }
599
+ },
600
+ "verdict": {
601
+ "type": "object",
602
+ "additionalProperties": false,
603
+ "required": [
604
+ "blocking_gates",
605
+ "passing",
606
+ "total",
607
+ "ok"
608
+ ],
609
+ "description": "D-attestation-payload-completeness (K2/K3): pure derivation from gates[*].live -- what a human or CI actually greps for.",
610
+ "properties": {
611
+ "blocking_gates": {
612
+ "type": "array",
613
+ "items": {
614
+ "type": "string"
615
+ }
616
+ },
617
+ "passing": {
618
+ "type": "integer",
619
+ "minimum": 0
620
+ },
621
+ "total": {
622
+ "type": "integer",
623
+ "minimum": 0
624
+ },
625
+ "ok": {
626
+ "type": "boolean"
627
+ }
628
+ }
629
+ }
630
+ },
631
+ "$defs": {
632
+ "waiverFileRef": {
633
+ "type": "object",
634
+ "additionalProperties": false,
635
+ "required": ["present", "hash"],
636
+ "properties": {
637
+ "present": { "type": "boolean" },
638
+ "hash": { "type": ["string", "null"] }
639
+ }
56
640
  }
57
641
  }
58
642
  }