Skip to content

Commit b2e8bf2

Browse files
Sync Boatstack from Intelligence Flow Labs @ 49a684f6752d
1 parent 8112519 commit b2e8bf2

12 files changed

Lines changed: 450 additions & 20 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Contributing
44

5-
Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/c25c282a5d9b5f952caf16e99e22749b70a92d78/labs/12-product-engineering-loop).
5+
Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/49a684f6752d1f017281d7bbb8b17b856904df15/labs/12-product-engineering-loop).
66

77
The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR.
88

UPSTREAM.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "0a0dcec357890e5d6b853c1c15f7fb40f5f3902024c445a62f23ef0c48f02a00",
15+
"CONTRIBUTING.md": "3fea013e59482851b40d6c70734457600b421a021ef679c630bbee0cda19c17d",
1616
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -52,6 +52,7 @@
5252
"boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3",
5353
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
5454
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
55+
"boatstack/deliverycontrol_parity_test.go": "027c04471c6037fc585a8af8646171addcc548e43147c5c6dbbe76bad9f5a10c",
5556
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
5657
"boatstack/export.go": "9cb23234e6cd79441ff6f39f88ed66d6d47ef7c27901404439a3572b03fdf881",
5758
"boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6",
@@ -67,6 +68,11 @@
6768
"boatstack/installation_repair.go": "6574f7133a9644843c9260b9b9daede641a14438f7357bae42fb8ec188890446",
6869
"boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14",
6970
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
71+
"boatstack/internal/deliverycontrol/cost.go": "a0a22292b8ed55cbfce9808599449d5128ae5b67ef6adc4881e604db0897f3f0",
72+
"boatstack/internal/deliverycontrol/registry.go": "aa89cef9eec8d715c06d2f61a472df20bb9334c950a6d751a1e15d67b567c433",
73+
"boatstack/internal/deliverycontrol/registry_test.go": "473ab5e5d33f84d34c29a219db867abfc6eb3ad4489f3d5d0c7dc09b06d193f3",
74+
"boatstack/internal/deliverycontrol/state.go": "2551624bbcbd8f9dd897a1e2240cef2cc1895d117a4030525d88f1d62f6e395e",
75+
"boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775",
7076
"boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3",
7177
"boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e",
7278
"boatstack/mutation.go": "59fc9e92105d8ec20f854af9898cde037ab0e3e46c453794838dbfc65fecdd6d",
@@ -136,10 +142,10 @@
136142
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
137143
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
138144
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
139-
"docs/evidence-engineered-coding.md": "2e21eeb6f2cc73fc667ded5163c3e6005f30993f72a181bcad5d205564686e0d",
145+
"docs/evidence-engineered-coding.md": "b8d35dc38d3e9385972fdd81595eb5245367b4e7e55a89363b5f6b53cce73977",
140146
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
141147
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
142-
"docs/public-claims.json": "04c1a4898228049b9e586f8db3af7c2ad13a01846c20c1b736e17005edeba12f",
148+
"docs/public-claims.json": "57e7d04dd7141b62e89ff111ced83674b6e9ad9094fa71a96da3dcb723ffc8f5",
143149
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
144150
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
145151
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -153,7 +159,7 @@
153159
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
154160
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
155161
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
156-
"labs/diagram-json/plan.lock.json": "5b2f60c8a1c7921f7513f25591cca82f42c5b1ca4e2380964f2dc989b969cafa",
162+
"labs/diagram-json/plan.lock.json": "1ddebf9603a1058b81ea2f02a7314bbba1a46a82a32d59bf3f3721cf74a85228",
157163
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
158164
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
159165
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -237,14 +243,15 @@
237243
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
238244
"release-notes/2026-07-25-delivery-control-inventory.md": "1f359bcf4071dd47bd1011c877db573bd26309d28683abea8389c353f1c6c88d",
239245
"release-notes/2026-07-25-delivery-flow-navigation-model.md": "b2d805fae30100a7de4e76760341247237cc2476fdcc57d99074825bf47d6450",
246+
"release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273",
240247
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
241248
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
242249
"release-notes/2026-07-25-root-cause-operation.md": "5bf1f082e9123c5a7bcc8bc01b12e97b24b5ae15958577b4ff2a358994fca891"
243250
},
244251
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
245252
"schema_version": 1,
246253
"source": {
247-
"commit": "c25c282a5d9b5f952caf16e99e22749b70a92d78",
254+
"commit": "49a684f6752d1f017281d7bbb8b17b856904df15",
248255
"path": "labs/12-product-engineering-loop",
249256
"repository": "operatorstack/intelligence-flow"
250257
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package boatstack
2+
3+
import (
4+
"testing"
5+
6+
"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
7+
)
8+
9+
// control-law: registry-mirrors-real-transitions
10+
// The deliverycontrol registry is a second projection of the real delivery
11+
// state machine in this package. These tests are the boundary that keeps the two
12+
// from drifting: every HandlerRef must name a real exported function here, and
13+
// the registry's slice-status states must equal the DeliverySlice.Status
14+
// literals the real machine uses.
15+
16+
// realDeliveryHandlers maps the exported functions the registry may reference to
17+
// their values. Referencing the values makes this fail to COMPILE if any handler
18+
// is renamed or removed, so a registry HandlerRef can never point at a function
19+
// that no longer exists.
20+
var realDeliveryHandlers = map[string]any{
21+
"ActivatePlan": ActivatePlan,
22+
"RecordDeliveryGate": RecordDeliveryGate,
23+
"RecordChangeObservation": RecordChangeObservation,
24+
"PublishPR": PublishPR,
25+
"UndoManagedMutation": UndoManagedMutation,
26+
"DiscardDelivery": DiscardDelivery,
27+
"RepairState": RepairState,
28+
"IgnoreDelivery": IgnoreDelivery,
29+
"CurrentDeliveryState": CurrentDeliveryState,
30+
"CheckDeliveryReadyForShip": CheckDeliveryReadyForShip,
31+
"ResolveNext": ResolveNext,
32+
"ResolveRecovery": ResolveRecovery,
33+
}
34+
35+
func TestRegistryHandlerRefsAreRealFunctions(t *testing.T) {
36+
for _, tr := range deliverycontrol.Transitions() {
37+
if _, ok := realDeliveryHandlers[tr.HandlerRef]; !ok {
38+
t.Errorf("transition %s references handler %q which is not a known real delivery function", tr.ID, tr.HandlerRef)
39+
}
40+
}
41+
}
42+
43+
func TestRegistrySliceStatusMatchesRealLiterals(t *testing.T) {
44+
// The real slice lifecycle literals, from DeliverySlice.Status assignments in
45+
// delivery.go and the nextForDelivery switch in next.go. If the real machine
46+
// gains or renames a slice status, update both the machine and the registry.
47+
realLiterals := map[string]bool{
48+
"PENDING": true, "BUILD": true, "TEST_PASSED": true,
49+
"REVIEW_PASSED": true, "PUBLISHED": true,
50+
}
51+
registryStatus := map[string]bool{}
52+
for _, s := range deliverycontrol.SliceStatusStates() {
53+
registryStatus[string(s)] = true
54+
}
55+
for lit := range realLiterals {
56+
if !registryStatus[lit] {
57+
t.Errorf("registry SliceStatusStates is missing real literal %q", lit)
58+
}
59+
}
60+
for s := range registryStatus {
61+
if !realLiterals[s] {
62+
t.Errorf("registry declares slice-status %q that the real machine does not use", s)
63+
}
64+
}
65+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package deliverycontrol
2+
3+
// FlowCostWeights maps a TransitionCostClass to its J_flow cost. Per the cmg
4+
// model (../../notes/delivery-flow-navigation-model.md): a normal
5+
// move/observe/inspect costs 1; a denied/blocked committed mutation is friction
6+
// and costs 3 (it burns a turn and returns nothing).
7+
type FlowCostWeights map[TransitionCostClass]int
8+
9+
// DefaultFlowCostWeights is the cost model the cmg prototype pins
10+
// (composable-model-graph:python/examples/12-agent-trajectory/main.py).
11+
func DefaultFlowCostWeights() FlowCostWeights {
12+
return FlowCostWeights{
13+
CostObserve: 1,
14+
CostInspect: 1,
15+
CostQuery: 1,
16+
CostMutation: 1,
17+
CostRecovery: 1,
18+
CostFriction: 3,
19+
}
20+
}
21+
22+
// Cost returns the weight for a cost class and whether it is defined.
23+
func (w FlowCostWeights) Cost(class TransitionCostClass) (int, bool) {
24+
v, ok := w[class]
25+
return v, ok
26+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package deliverycontrol
2+
3+
// registry is the single declaration of Boatstack's delivery transitions,
4+
// mirroring ../../notes/delivery-control-inventory.md. Each HandlerRef names a
5+
// real exported function in package boatstack; the parity conformance test keeps
6+
// this faithful. This is a shadow catalog — nothing consumes it at runtime yet.
7+
var registry = []TransitionDescriptor{
8+
{
9+
ID: "delivery.activate", From: []StateID{StateUninitialized}, To: StateBuild,
10+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: true,
11+
HandlerRef: "ActivatePlan", CLIVerb: "activate-plan",
12+
Note: "Requires CheckPlan + repository-safety PASS and a human/policy approval receipt; writes the plan lock via the mutation boundary. Reversible via delivery.undo while no gate receipt exists.",
13+
},
14+
{
15+
ID: "delivery.record_gate_test", From: []StateID{StateBuild}, To: StateTestPassed,
16+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: true,
17+
HandlerRef: "RecordDeliveryGate", CLIVerb: "record-delivery-gate",
18+
Note: "Records the test gate (PASS/PASS_WITH_GAPS) against a validated plan lock and a matching evidence ledger. Reversible via record-change re-gate.",
19+
},
20+
{
21+
ID: "delivery.record_gate_review", From: []StateID{StateTestPassed}, To: StateReviewPassed,
22+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: true,
23+
HandlerRef: "RecordDeliveryGate", CLIVerb: "record-delivery-gate",
24+
Note: "Records the review gate; requires prior TEST_PASSED with a matching diff and reviewer identity/method on high-risk paths. Clears rework mode.",
25+
},
26+
{
27+
ID: "delivery.record_change", From: []StateID{StateTestPassed, StateReviewPassed, StatePublished}, To: StateBuild,
28+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: true,
29+
HandlerRef: "RecordChangeObservation", CLIVerb: "record-change",
30+
Note: "Rework resets the addressable slice to BUILD (bounded by RepairAttempt<3); amendment/plan-invalid set Mode; a fully-published delivery emits a corrective child with no state mutation.",
31+
},
32+
{
33+
ID: "delivery.publish", From: []StateID{StateReviewPassed, StatePublished}, To: StatePublished,
34+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: false,
35+
HandlerRef: "PublishPR", CLIVerb: "publish-pr",
36+
Note: "Publishes the reviewed slice behind a human-confirmed preview fingerprint and advances ActiveIndex to the next slice. Re-publishing a PUBLISHED-open slice is idempotent and does not advance the pointer.",
37+
},
38+
{
39+
ID: "delivery.undo", From: []StateID{StateBuild}, To: StateUninitialized,
40+
Kind: KindReversibleMutation, CostClass: CostMutation, Reversible: true,
41+
HandlerRef: "UndoManagedMutation", CLIVerb: "undo",
42+
Note: "Reverses a plan-activation/compiled-plan mutation through the boundary (closed under inversion, so redo is undo of the returned receipt). Refused once any gate receipt exists (would strand delivery state).",
43+
},
44+
{
45+
ID: "delivery.discard_delivery", From: []StateID{StatePending, StateBuild, StateTestPassed, StateReviewPassed, StatePublished}, To: StateDiscarded,
46+
Kind: KindReversibleMutation, CostClass: CostMutation, Reversible: true,
47+
HandlerRef: "DiscardDelivery", CLIVerb: "discard-delivery",
48+
Note: "Archives (never deletes) the delivery state directory. Refuses a slice with a set PRState unless --force; preserves published authority and git/lock/merged history.",
49+
},
50+
{
51+
ID: "delivery.repair_state", From: []StateID{StateInvalid}, To: StateUninitialized,
52+
Kind: KindRecovery, CostClass: CostRecovery, Reversible: true,
53+
HandlerRef: "RepairState", CLIVerb: "repair-state",
54+
Note: "Quarantines a malformed unregistered feature draft by moving it aside; refuses when a plan lock, pr.md, managed state, tracked files, or an active/published delivery is present. Typed and bounded — never a generic bypass.",
55+
},
56+
{
57+
ID: "delivery.ignore_delivery", From: []StateID{StatePending, StateBuild, StateTestPassed, StateReviewPassed, StatePublished}, To: "",
58+
Kind: KindCommittedMutation, CostClass: CostMutation, Reversible: true,
59+
HandlerRef: "IgnoreDelivery", CLIVerb: "ignore-delivery",
60+
Note: "Appends the feature to project.json workflow.ignored_deliveries, filtering it from ResolveNext and publication authority. Changes no slice status; reversible by removing the entry.",
61+
},
62+
{
63+
ID: "delivery.status", From: []StateID{StateBuild, StateTestPassed, StateReviewPassed, StatePublished}, To: "",
64+
Kind: KindObserve, CostClass: CostObserve, Reversible: false,
65+
HandlerRef: "CurrentDeliveryState", CLIVerb: "delivery-status",
66+
Note: "Reads delivery state and validates the plan lock. No state change.",
67+
},
68+
{
69+
ID: "delivery.check_ship", From: []StateID{StateReviewPassed, StatePublished}, To: "",
70+
Kind: KindQuery, CostClass: CostQuery, Reversible: false,
71+
HandlerRef: "CheckDeliveryReadyForShip", CLIVerb: "ship-gate",
72+
Note: "Re-checks receipt freshness and gate policy for the addressable slice and returns its PR sources. No state change.",
73+
},
74+
{
75+
ID: "delivery.next", From: nil, To: "",
76+
Kind: KindObserve, CostClass: CostObserve, Reversible: false,
77+
HandlerRef: "ResolveNext", CLIVerb: "next-status",
78+
Note: "Derives the recommended next move. Read-only, except that the published branch caches an observed terminal PRState as a best-effort side effect (a known bypass, modeled not fixed).",
79+
},
80+
{
81+
ID: "delivery.recovery_status", From: []StateID{StateBuild, StateTestPassed, StateReviewPassed, StatePublished}, To: "",
82+
Kind: KindObserve, CostClass: CostObserve, Reversible: false,
83+
HandlerRef: "ResolveRecovery", CLIVerb: "recovery-status",
84+
Note: "Derives a correction decision; carries no edit/approve/publish authority. Read-only, except the same best-effort terminal-PRState cache.",
85+
},
86+
}
87+
88+
// Transitions returns a copy of the declared transition set.
89+
func Transitions() []TransitionDescriptor {
90+
out := make([]TransitionDescriptor, len(registry))
91+
copy(out, registry)
92+
return out
93+
}
94+
95+
// Transition returns the descriptor with the given ID.
96+
func Transition(id TransitionID) (TransitionDescriptor, bool) {
97+
for _, t := range registry {
98+
if t.ID == id {
99+
return t, true
100+
}
101+
}
102+
return TransitionDescriptor{}, false
103+
}
104+
105+
// HandlerRefs returns the distinct real function names the registry declares.
106+
func HandlerRefs() []string {
107+
seen := map[string]bool{}
108+
var out []string
109+
for _, t := range registry {
110+
if !seen[t.HandlerRef] {
111+
seen[t.HandlerRef] = true
112+
out = append(out, t.HandlerRef)
113+
}
114+
}
115+
return out
116+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package deliverycontrol
2+
3+
import "testing"
4+
5+
// control-law: deliverycontrol-registry-well-formed
6+
// The single declaration must be internally consistent: unique ids, declared
7+
// states on every edge, valid kinds/cost classes, and a defined cost weight for
8+
// every class a transition uses.
9+
func TestRegistryWellFormed(t *testing.T) {
10+
states := map[StateID]bool{}
11+
for _, s := range States() {
12+
states[s] = true
13+
}
14+
kinds := map[TransitionKind]bool{}
15+
for _, k := range AllKinds() {
16+
kinds[k] = true
17+
}
18+
classes := map[TransitionCostClass]bool{}
19+
for _, c := range AllCostClasses() {
20+
classes[c] = true
21+
}
22+
weights := DefaultFlowCostWeights()
23+
24+
seen := map[TransitionID]bool{}
25+
for _, tr := range Transitions() {
26+
if tr.ID == "" {
27+
t.Errorf("transition with empty ID: %+v", tr)
28+
}
29+
if seen[tr.ID] {
30+
t.Errorf("duplicate transition ID %q", tr.ID)
31+
}
32+
seen[tr.ID] = true
33+
if !kinds[tr.Kind] {
34+
t.Errorf("%s: undeclared kind %q", tr.ID, tr.Kind)
35+
}
36+
if !classes[tr.CostClass] {
37+
t.Errorf("%s: undeclared cost class %q", tr.ID, tr.CostClass)
38+
}
39+
if _, ok := weights.Cost(tr.CostClass); !ok {
40+
t.Errorf("%s: cost class %q has no weight", tr.ID, tr.CostClass)
41+
}
42+
for _, from := range tr.From {
43+
if !states[from] {
44+
t.Errorf("%s: undeclared From state %q", tr.ID, from)
45+
}
46+
}
47+
if tr.To != "" && !states[tr.To] {
48+
t.Errorf("%s: undeclared To state %q", tr.ID, tr.To)
49+
}
50+
if tr.HandlerRef == "" {
51+
t.Errorf("%s: empty HandlerRef", tr.ID)
52+
}
53+
}
54+
if len(seen) == 0 {
55+
t.Fatal("registry is empty")
56+
}
57+
}
58+
59+
// The cmg model only makes friction expensive; if friction ever costs no more
60+
// than a move, regret vanishes and the whole model is meaningless.
61+
func TestFrictionCostsMoreThanAMove(t *testing.T) {
62+
w := DefaultFlowCostWeights()
63+
move, ok := w.Cost(CostObserve)
64+
if !ok {
65+
t.Fatal("observe cost undefined")
66+
}
67+
friction, ok := w.Cost(CostFriction)
68+
if !ok {
69+
t.Fatal("friction cost undefined")
70+
}
71+
if friction <= move {
72+
t.Errorf("friction (%d) must cost more than a move (%d)", friction, move)
73+
}
74+
}
75+
76+
func TestSliceStatusStatesAreDeclared(t *testing.T) {
77+
declared := map[StateID]bool{}
78+
for _, s := range States() {
79+
declared[s] = true
80+
}
81+
for _, s := range SliceStatusStates() {
82+
if !declared[s] {
83+
t.Errorf("slice-status state %q is not in States()", s)
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)