Skip to content

Commit a8675df

Browse files
Sync Boatstack from Intelligence Flow Labs @ fac1f242bb58 (#104)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent 449a686 commit a8675df

15 files changed

Lines changed: 816 additions & 20 deletions

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/49a684f6752d1f017281d7bbb8b17b856904df15/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/fac1f242bb58f88e94da3f7cbb50ba6159d02790/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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "3fea013e59482851b40d6c70734457600b421a021ef679c630bbee0cda19c17d",
15+
"CONTRIBUTING.md": "762db520d51872c6b6b6ec7c312f54c7e1ef5c14fdf61ed17673d65a525a7e01",
1616
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -56,6 +56,8 @@
5656
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
5757
"boatstack/export.go": "9cb23234e6cd79441ff6f39f88ed66d6d47ef7c27901404439a3572b03fdf881",
5858
"boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6",
59+
"boatstack/flow_trace.go": "95b5a99f5f557a27a3eca7152de9ec18459f2a88d9749924432463031536a96c",
60+
"boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b",
5961
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
6062
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
6163
"boatstack/hooks.go": "0639eff2ec5ce50dcbe77ace0f7c25de1e9a68784a6ed70d6acc9984d049ef1a",
@@ -69,9 +71,16 @@
6971
"boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14",
7072
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
7173
"boatstack/internal/deliverycontrol/cost.go": "a0a22292b8ed55cbfce9808599449d5128ae5b67ef6adc4881e604db0897f3f0",
74+
"boatstack/internal/deliverycontrol/graph.go": "13367b068d0004e0f2e857e7b6e9d19e758ef345070b3fbc008644be27438902",
75+
"boatstack/internal/deliverycontrol/oracle.go": "80765b1946d6c863f0e635a99b68d3ccafa7ff235360fba774811b5b0de791da",
76+
"boatstack/internal/deliverycontrol/oracle_test.go": "ce320a71f0c9440c5a7bc1b742d0f74c6a46759845e919ab36bde5ff8fabb311",
7277
"boatstack/internal/deliverycontrol/registry.go": "aa89cef9eec8d715c06d2f61a472df20bb9334c950a6d751a1e15d67b567c433",
7378
"boatstack/internal/deliverycontrol/registry_test.go": "473ab5e5d33f84d34c29a219db867abfc6eb3ad4489f3d5d0c7dc09b06d193f3",
7479
"boatstack/internal/deliverycontrol/state.go": "2551624bbcbd8f9dd897a1e2240cef2cc1895d117a4030525d88f1d62f6e395e",
80+
"boatstack/internal/deliverycontrol/trajectory.go": "e25eeb092fb2255bd82477141d5b094627c1d779bef3616b89752046ae08e7c3",
81+
"boatstack/internal/deliverycontrol/trajectory_test.go": "df5be9a8f55b09a94b0f6b94d2847180d39d357619eb4d1d11181015935c2f96",
82+
"boatstack/internal/deliverycontrol/trajectorylog.go": "a1da7e7252b33f63f232c101de683b4a515e80243801caf69fda53d24233e42f",
83+
"boatstack/internal/deliverycontrol/trajectorylog_test.go": "227dd6ed9ce181d517a37b67ef4d64dd93779a533eae804798ab54de35c7f13e",
7584
"boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775",
7685
"boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3",
7786
"boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e",
@@ -142,10 +151,10 @@
142151
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
143152
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
144153
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
145-
"docs/evidence-engineered-coding.md": "b8d35dc38d3e9385972fdd81595eb5245367b4e7e55a89363b5f6b53cce73977",
154+
"docs/evidence-engineered-coding.md": "bc8fcd561d1bc57f0cdbe79c537143e005b00995cbc8fe690982d00efdaf64d5",
146155
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
147156
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
148-
"docs/public-claims.json": "57e7d04dd7141b62e89ff111ced83674b6e9ad9094fa71a96da3dcb723ffc8f5",
157+
"docs/public-claims.json": "476511c6c2b2c4159f507b4e9c8f2c726b8a08a004db51a0967e73e92439050a",
149158
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
150159
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
151160
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -159,7 +168,7 @@
159168
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
160169
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
161170
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
162-
"labs/diagram-json/plan.lock.json": "1ddebf9603a1058b81ea2f02a7314bbba1a46a82a32d59bf3f3721cf74a85228",
171+
"labs/diagram-json/plan.lock.json": "2a120ef6977a3e028e66c534244d9cfcf5a347cde3738ad176038b6c57a1c370",
163172
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
164173
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
165174
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -243,6 +252,7 @@
243252
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
244253
"release-notes/2026-07-25-delivery-control-inventory.md": "1f359bcf4071dd47bd1011c877db573bd26309d28683abea8389c353f1c6c88d",
245254
"release-notes/2026-07-25-delivery-flow-navigation-model.md": "b2d805fae30100a7de4e76760341247237cc2476fdcc57d99074825bf47d6450",
255+
"release-notes/2026-07-25-deliverycontrol-flow-oracle.md": "04e64e27638b32a90a132f615f896bf20ee805f3c75bf5e3ab088f3c55e641a7",
246256
"release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273",
247257
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
248258
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
@@ -251,7 +261,7 @@
251261
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
252262
"schema_version": 1,
253263
"source": {
254-
"commit": "49a684f6752d1f017281d7bbb8b17b856904df15",
264+
"commit": "fac1f242bb58f88e94da3f7cbb50ba6159d02790",
255265
"path": "labs/12-product-engineering-loop",
256266
"repository": "operatorstack/intelligence-flow"
257267
}

boatstack/flow_trace.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package boatstack
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
8+
)
9+
10+
// flowTraceKillSwitch disables shadow trajectory recording when set to "0".
11+
// Recording is best-effort and off the critical path either way; the switch
12+
// exists so an operator can silence it entirely without a rebuild.
13+
const flowTraceKillSwitch = "BOATSTACK_FLOW_TRACE"
14+
15+
// flowLogDirectory is the append-only trajectory log location,
16+
// <gitdir>/boatstack/flow, a sibling of the delivery-state directory. It reuses
17+
// deliveryStateDirectory's Git-dir resolution so the two stay in lockstep.
18+
func flowLogDirectory(repo string) (string, error) {
19+
deliveries, err := deliveryStateDirectory(repo)
20+
if err != nil {
21+
return "", err
22+
}
23+
return filepath.Join(filepath.Dir(deliveries), "flow"), nil
24+
}
25+
26+
// RecordFlowTransition appends a best-effort shadow record of one delivery-flow
27+
// control attempt. It is deliberately inert with respect to command behavior:
28+
// it never returns an error, never panics into a caller, and writes nothing when
29+
// disabled or when anything goes wrong. Nothing consumes the log at runtime yet
30+
// — this is the measurement substrate for the flow-navigation meter, not a
31+
// control point.
32+
//
33+
// transition is the registry TransitionID being attempted; from is the
34+
// delivery-flow state the attempt started in ("" if unknown); ok reports whether
35+
// the underlying handler succeeded (a false ok on a mutation is billed as
36+
// friction, per the cmg model).
37+
func RecordFlowTransition(repo string, transition deliverycontrol.TransitionID, from deliverycontrol.StateID, ok bool) {
38+
// A trace must never take down a command. Swallow any panic from the
39+
// best-effort path.
40+
defer func() { _ = recover() }()
41+
42+
if os.Getenv(flowTraceKillSwitch) == "0" {
43+
return
44+
}
45+
descriptor, found := deliverycontrol.Transition(transition)
46+
if !found {
47+
return
48+
}
49+
50+
outcome := deliverycontrol.OutcomeAllowed
51+
if !ok {
52+
outcome = deliverycontrol.OutcomeDenied
53+
}
54+
directory, err := flowLogDirectory(repo)
55+
if err != nil {
56+
return
57+
}
58+
_ = deliverycontrol.AppendAttempt(directory, deliverycontrol.TransitionAttempt{
59+
From: from,
60+
Transition: transition,
61+
Outcome: outcome,
62+
CostClass: deliverycontrol.ChargedCostClass(descriptor.Kind, descriptor.CostClass, outcome),
63+
})
64+
}

boatstack/flow_trace_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package boatstack
2+
3+
import (
4+
"testing"
5+
6+
"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
7+
)
8+
9+
func readFlowTrajectory(t *testing.T, repo string) deliverycontrol.Trajectory {
10+
t.Helper()
11+
dir, err := flowLogDirectory(repo)
12+
if err != nil {
13+
t.Fatalf("flow log directory: %v", err)
14+
}
15+
traj, err := deliverycontrol.ReadTrajectory(dir)
16+
if err != nil {
17+
t.Fatalf("read trajectory: %v", err)
18+
}
19+
return traj
20+
}
21+
22+
// An allowed mutation and a denied one are both recorded, and the denial is
23+
// billed as friction — the recorder is a faithful, cheap witness to real
24+
// command outcomes.
25+
func TestRecordFlowTransitionCapturesOutcomes(t *testing.T) {
26+
repo := prTestRepo(t)
27+
28+
RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true)
29+
RecordFlowTransition(repo, "delivery.undo", deliverycontrol.StateBuild, false)
30+
31+
traj := readFlowTrajectory(t, repo)
32+
if len(traj) != 2 {
33+
t.Fatalf("recorded %d attempts, want 2", len(traj))
34+
}
35+
if traj[0].Outcome != deliverycontrol.OutcomeAllowed || traj[0].CostClass != deliverycontrol.CostMutation {
36+
t.Errorf("allowed gate: got %+v", traj[0])
37+
}
38+
if traj[1].Outcome != deliverycontrol.OutcomeDenied || traj[1].CostClass != deliverycontrol.CostFriction {
39+
t.Errorf("denied undo should be friction: got %+v", traj[1])
40+
}
41+
if cost := traj.WalkCost(deliverycontrol.DefaultFlowCostWeights()); cost != 4 {
42+
t.Errorf("walk cost = %d, want 4 (1 move + 3 friction)", cost)
43+
}
44+
}
45+
46+
// The recorder is best-effort: an unresolvable repo, an unknown transition, and
47+
// the kill switch each leave command behavior untouched and write nothing —
48+
// without erroring or panicking.
49+
func TestRecordFlowTransitionIsBestEffort(t *testing.T) {
50+
// A non-git directory cannot resolve a flow-log location; must be a silent no-op.
51+
RecordFlowTransition(t.TempDir(), "delivery.record_gate_test", deliverycontrol.StateBuild, true)
52+
53+
repo := prTestRepo(t)
54+
55+
// An unknown transition is ignored.
56+
RecordFlowTransition(repo, "delivery.does_not_exist", deliverycontrol.StateBuild, true)
57+
if traj := readFlowTrajectory(t, repo); len(traj) != 0 {
58+
t.Errorf("unknown transition should record nothing; got %d", len(traj))
59+
}
60+
61+
// The kill switch disables recording entirely.
62+
t.Setenv(flowTraceKillSwitch, "0")
63+
RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true)
64+
if traj := readFlowTrajectory(t, repo); len(traj) != 0 {
65+
t.Errorf("kill switch should suppress recording; got %d", len(traj))
66+
}
67+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package deliverycontrol
2+
3+
import "sort"
4+
5+
// Edge is a directed, costed control between two delivery-flow states: a single
6+
// move an agent can make, priced by its cost class.
7+
type Edge struct {
8+
Transition TransitionID
9+
From StateID
10+
To StateID
11+
CostClass TransitionCostClass
12+
Cost int
13+
}
14+
15+
// Graph is a weighted directed graph of delivery-flow controls. It is the
16+
// optimization projection of the single registry declaration: the same
17+
// transitions the conformance projection audits, arranged as a graph a
18+
// deterministic oracle can score. Only state-changing transitions (To != "")
19+
// become edges; read-only observes do not advance state and so are not moves on
20+
// this graph.
21+
type Graph struct {
22+
weights FlowCostWeights
23+
out map[StateID][]Edge
24+
nodes map[StateID]bool
25+
}
26+
27+
// NewGraph returns an empty graph priced by the given weights.
28+
func NewGraph(weights FlowCostWeights) *Graph {
29+
return &Graph{
30+
weights: weights,
31+
out: map[StateID][]Edge{},
32+
nodes: map[StateID]bool{},
33+
}
34+
}
35+
36+
// AddEdge adds a directed edge priced by the transition's cost class. An edge
37+
// whose cost class has no defined weight is skipped (the well-formedness
38+
// conformance test forbids that in the registry, so this only guards ad-hoc
39+
// graphs). Endpoints are registered as nodes even when the class is unknown, so
40+
// a state that only appears on a skipped edge is still a known node.
41+
func (g *Graph) AddEdge(from, to StateID, transition TransitionID, class TransitionCostClass) {
42+
g.nodes[from] = true
43+
g.nodes[to] = true
44+
cost, ok := g.weights.Cost(class)
45+
if !ok {
46+
return
47+
}
48+
g.out[from] = append(g.out[from], Edge{
49+
Transition: transition,
50+
From: from,
51+
To: to,
52+
CostClass: class,
53+
Cost: cost,
54+
})
55+
}
56+
57+
// Out returns the out-edges of a state in insertion order (deterministic).
58+
func (g *Graph) Out(state StateID) []Edge {
59+
return g.out[state]
60+
}
61+
62+
// Has reports whether a state is a known node (appears as an edge endpoint).
63+
// The oracle uses this to return Unresolved for a state it has never seen rather
64+
// than fabricate a path from nowhere.
65+
func (g *Graph) Has(state StateID) bool {
66+
return g.nodes[state]
67+
}
68+
69+
// Nodes returns every known state, sorted for deterministic iteration.
70+
func (g *Graph) Nodes() []StateID {
71+
out := make([]StateID, 0, len(g.nodes))
72+
for n := range g.nodes {
73+
out = append(out, n)
74+
}
75+
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
76+
return out
77+
}
78+
79+
// RegistryGraph projects the registry into a costed graph: one edge per
80+
// (From, To) pair of every transition that changes delivery state. A transition
81+
// with an empty To (a pure observation) advances nothing and contributes no
82+
// edge; a transition with an empty From (delivery.next, resolved from any state)
83+
// likewise contributes no edge because it changes no state. Iteration order
84+
// follows the registry declaration, so the projection is deterministic.
85+
func RegistryGraph(weights FlowCostWeights) *Graph {
86+
g := NewGraph(weights)
87+
for _, tr := range Transitions() {
88+
if tr.To == "" {
89+
continue
90+
}
91+
for _, from := range tr.From {
92+
g.AddEdge(from, tr.To, tr.ID, tr.CostClass)
93+
}
94+
}
95+
return g
96+
}

0 commit comments

Comments
 (0)