backend-skeleton 1.6.0 → 1.7.1
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 +4 -3
- package/bin/bskel.mjs +220 -12
- package/contracts/completeness.mjs +12 -1
- package/lib/cli.mjs +48 -2
- package/lib/decision-log.mjs +58 -0
- package/lib/gate-export.mjs +55 -4
- package/lib/impact.mjs +51 -3
- package/package.json +1 -1
- package/scanners/adapters/javascript-express.mjs +133 -83
- package/schemas/decision-event.schema.json +46 -0
- package/schemas/gate-export.schema.json +114 -38
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:sbf:decision-event:1",
|
|
4
|
+
"title": "backend-skeleton decision event",
|
|
5
|
+
"description": "D-decision-event-log: one line of a .sbf/<feature_id>.decisions.jsonl file, appended by lib/decision-log.mjs's appendDecisionEvent() every time one of the four spec-side decision files (contract-resolution, cross-feature-resolution, impact-resolution, patch-approvals) is written -- mirrors gate-event.schema.json's own shape and resilience contract exactly (append-only JSONL, a corrupt/invalid line is skipped with a warning at read time, never a hard failure). Deliberately NOT a unified record type across the four decision files themselves -- their on-disk shapes stay exactly as they were (different upsert semantics: contract waivers are append-if-absent, impact dispositions are replace-by-key, and forcing either into the other would be the over-unification this design explicitly rejected). `subject` is a discriminated union keyed on `kind`, carrying that decision kind's REAL key object verbatim -- never flattened to a string -- because a generic string key is exactly how the anti-wildcard property every one of the four already enforces (a decision covers ONE exact subject, never a class of future subjects) would eventually erode. This log is machine-local (.sbf/ is gitignored-by-convention) and is NOT tamper-evident on its own; its evidentiary value comes from being rolled up into a SIGNED gate-export attestation (see D-attestation-payload-completeness), exactly as gates.*.history already is.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema", "kind", "action", "at", "reason", "feature_id", "subject"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema": { "const": "sbf.decision-event/1" },
|
|
11
|
+
"kind": { "enum": ["contract_waiver", "cross_feature_waiver", "impact_disposition", "patch_approval"] },
|
|
12
|
+
"action": { "enum": ["record", "renew", "withdraw", "acknowledge"] },
|
|
13
|
+
"at": { "type": "string", "format": "date-time" },
|
|
14
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
15
|
+
"feature_id": { "type": "string" },
|
|
16
|
+
"subject": {
|
|
17
|
+
"description": "The exact key object for this decision, shaped per `kind` -- never a flattened string.",
|
|
18
|
+
"oneOf": [
|
|
19
|
+
{
|
|
20
|
+
"type": "object", "additionalProperties": false,
|
|
21
|
+
"required": ["code", "subject"],
|
|
22
|
+
"properties": { "code": { "type": "string" }, "subject": { "type": ["string", "null"] } }
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "object", "additionalProperties": false,
|
|
26
|
+
"required": ["signal", "identifier", "other_feature"],
|
|
27
|
+
"properties": { "signal": { "type": "string" }, "identifier": { "type": "string" }, "other_feature": { "type": "string" } }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "object", "additionalProperties": false,
|
|
31
|
+
"required": ["change_key", "downstream_feature"],
|
|
32
|
+
"properties": { "change_key": { "type": "string" }, "downstream_feature": { "type": "string" } }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "object", "additionalProperties": false,
|
|
36
|
+
"required": ["resource", "field"],
|
|
37
|
+
"properties": { "resource": { "type": "string" }, "field": { "type": "string" } }
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"mode": { "type": ["string", "null"] },
|
|
42
|
+
"expires_at": { "type": ["string", "null"] },
|
|
43
|
+
"tracked_by": { "type": ["string", "null"] },
|
|
44
|
+
"strategy": { "type": ["string", "null"] }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"properties": {
|
|
20
20
|
"schema": {
|
|
21
|
-
"const": "sbf.gate-export/
|
|
21
|
+
"const": "sbf.gate-export/4"
|
|
22
22
|
},
|
|
23
23
|
"feature_id": {
|
|
24
24
|
"type": "string",
|
|
@@ -151,7 +151,8 @@
|
|
|
151
151
|
"stack_record_hash",
|
|
152
152
|
"impact_baseline_hash",
|
|
153
153
|
"impact_report_hash",
|
|
154
|
-
"impact_resolution_hash"
|
|
154
|
+
"impact_resolution_hash",
|
|
155
|
+
"patch_approvals_hash"
|
|
155
156
|
],
|
|
156
157
|
"properties": {
|
|
157
158
|
"scan_report_hash": {
|
|
@@ -265,6 +266,13 @@
|
|
|
265
266
|
"null"
|
|
266
267
|
],
|
|
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}$"
|
|
268
276
|
}
|
|
269
277
|
}
|
|
270
278
|
},
|
|
@@ -411,9 +419,11 @@
|
|
|
411
419
|
"required": [
|
|
412
420
|
"forced",
|
|
413
421
|
"revoked",
|
|
414
|
-
"waiver_files"
|
|
422
|
+
"waiver_files",
|
|
423
|
+
"records",
|
|
424
|
+
"event_counts"
|
|
415
425
|
],
|
|
416
|
-
"description": "D-attestation-payload-completeness (K2): rolled up purely from the gate records already in `gates` above -- zero new file reads.",
|
|
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.",
|
|
417
427
|
"properties": {
|
|
418
428
|
"forced": {
|
|
419
429
|
"type": "array",
|
|
@@ -486,49 +496,104 @@
|
|
|
486
496
|
"additionalProperties": false,
|
|
487
497
|
"required": [
|
|
488
498
|
"contract_resolution",
|
|
489
|
-
"cross_feature_resolution"
|
|
499
|
+
"cross_feature_resolution",
|
|
500
|
+
"impact_resolution",
|
|
501
|
+
"patch_approvals"
|
|
490
502
|
],
|
|
491
|
-
"description": "presence + hash only -- the CONTENT is already bound via `artifacts` above (
|
|
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.",
|
|
492
504
|
"properties": {
|
|
493
|
-
"contract_resolution": {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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"] }
|
|
509
534
|
}
|
|
510
535
|
}
|
|
511
536
|
},
|
|
512
|
-
"
|
|
513
|
-
"type": "
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
"
|
|
517
|
-
"
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"type": "
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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" }
|
|
528
581
|
}
|
|
529
582
|
}
|
|
530
583
|
}
|
|
531
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 }
|
|
596
|
+
}
|
|
532
597
|
}
|
|
533
598
|
}
|
|
534
599
|
},
|
|
@@ -562,5 +627,16 @@
|
|
|
562
627
|
}
|
|
563
628
|
}
|
|
564
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
|
+
}
|
|
640
|
+
}
|
|
565
641
|
}
|
|
566
642
|
}
|