From da0b47687900e82b3e838388dcf0a8b3c243356c Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Sat, 25 Jul 2026 13:20:16 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ fac1f242bb58 --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 20 ++- boatstack/flow_trace.go | 64 ++++++++ boatstack/flow_trace_test.go | 67 +++++++++ boatstack/internal/deliverycontrol/graph.go | 96 ++++++++++++ boatstack/internal/deliverycontrol/oracle.go | 106 +++++++++++++ .../internal/deliverycontrol/oracle_test.go | 84 +++++++++++ .../internal/deliverycontrol/trajectory.go | 94 ++++++++++++ .../deliverycontrol/trajectory_test.go | 139 ++++++++++++++++++ .../internal/deliverycontrol/trajectorylog.go | 74 ++++++++++ .../deliverycontrol/trajectorylog_test.go | 49 ++++++ docs/evidence-engineered-coding.md | 2 +- docs/public-claims.json | 24 +-- labs/diagram-json/plan.lock.json | 2 +- .../2026-07-25-deliverycontrol-flow-oracle.md | 13 ++ 15 files changed, 816 insertions(+), 20 deletions(-) create mode 100644 boatstack/flow_trace.go create mode 100644 boatstack/flow_trace_test.go create mode 100644 boatstack/internal/deliverycontrol/graph.go create mode 100644 boatstack/internal/deliverycontrol/oracle.go create mode 100644 boatstack/internal/deliverycontrol/oracle_test.go create mode 100644 boatstack/internal/deliverycontrol/trajectory.go create mode 100644 boatstack/internal/deliverycontrol/trajectory_test.go create mode 100644 boatstack/internal/deliverycontrol/trajectorylog.go create mode 100644 boatstack/internal/deliverycontrol/trajectorylog_test.go create mode 100644 release-notes/2026-07-25-deliverycontrol-flow-oracle.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ad29db..bb99b68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ # Contributing -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). +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). 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. diff --git a/UPSTREAM.json b/UPSTREAM.json index 3b3611a..a6ea8c0 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "3fea013e59482851b40d6c70734457600b421a021ef679c630bbee0cda19c17d", + "CONTRIBUTING.md": "762db520d51872c6b6b6ec7c312f54c7e1ef5c14fdf61ed17673d65a525a7e01", "README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -56,6 +56,8 @@ "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", "boatstack/export.go": "9cb23234e6cd79441ff6f39f88ed66d6d47ef7c27901404439a3572b03fdf881", "boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6", + "boatstack/flow_trace.go": "95b5a99f5f557a27a3eca7152de9ec18459f2a88d9749924432463031536a96c", + "boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b", "boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641", "boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017", "boatstack/hooks.go": "0639eff2ec5ce50dcbe77ace0f7c25de1e9a68784a6ed70d6acc9984d049ef1a", @@ -69,9 +71,16 @@ "boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/internal/deliverycontrol/cost.go": "a0a22292b8ed55cbfce9808599449d5128ae5b67ef6adc4881e604db0897f3f0", + "boatstack/internal/deliverycontrol/graph.go": "13367b068d0004e0f2e857e7b6e9d19e758ef345070b3fbc008644be27438902", + "boatstack/internal/deliverycontrol/oracle.go": "80765b1946d6c863f0e635a99b68d3ccafa7ff235360fba774811b5b0de791da", + "boatstack/internal/deliverycontrol/oracle_test.go": "ce320a71f0c9440c5a7bc1b742d0f74c6a46759845e919ab36bde5ff8fabb311", "boatstack/internal/deliverycontrol/registry.go": "aa89cef9eec8d715c06d2f61a472df20bb9334c950a6d751a1e15d67b567c433", "boatstack/internal/deliverycontrol/registry_test.go": "473ab5e5d33f84d34c29a219db867abfc6eb3ad4489f3d5d0c7dc09b06d193f3", "boatstack/internal/deliverycontrol/state.go": "2551624bbcbd8f9dd897a1e2240cef2cc1895d117a4030525d88f1d62f6e395e", + "boatstack/internal/deliverycontrol/trajectory.go": "e25eeb092fb2255bd82477141d5b094627c1d779bef3616b89752046ae08e7c3", + "boatstack/internal/deliverycontrol/trajectory_test.go": "df5be9a8f55b09a94b0f6b94d2847180d39d357619eb4d1d11181015935c2f96", + "boatstack/internal/deliverycontrol/trajectorylog.go": "a1da7e7252b33f63f232c101de683b4a515e80243801caf69fda53d24233e42f", + "boatstack/internal/deliverycontrol/trajectorylog_test.go": "227dd6ed9ce181d517a37b67ef4d64dd93779a533eae804798ab54de35c7f13e", "boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", @@ -142,10 +151,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a", - "docs/evidence-engineered-coding.md": "b8d35dc38d3e9385972fdd81595eb5245367b4e7e55a89363b5f6b53cce73977", + "docs/evidence-engineered-coding.md": "bc8fcd561d1bc57f0cdbe79c537143e005b00995cbc8fe690982d00efdaf64d5", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd", - "docs/public-claims.json": "57e7d04dd7141b62e89ff111ced83674b6e9ad9094fa71a96da3dcb723ffc8f5", + "docs/public-claims.json": "476511c6c2b2c4159f507b4e9c8f2c726b8a08a004db51a0967e73e92439050a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -159,7 +168,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "1ddebf9603a1058b81ea2f02a7314bbba1a46a82a32d59bf3f3721cf74a85228", + "labs/diagram-json/plan.lock.json": "2a120ef6977a3e028e66c534244d9cfcf5a347cde3738ad176038b6c57a1c370", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -243,6 +252,7 @@ "release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b", "release-notes/2026-07-25-delivery-control-inventory.md": "1f359bcf4071dd47bd1011c877db573bd26309d28683abea8389c353f1c6c88d", "release-notes/2026-07-25-delivery-flow-navigation-model.md": "b2d805fae30100a7de4e76760341247237cc2476fdcc57d99074825bf47d6450", + "release-notes/2026-07-25-deliverycontrol-flow-oracle.md": "04e64e27638b32a90a132f615f896bf20ee805f3c75bf5e3ab088f3c55e641a7", "release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273", "release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11", "release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082", @@ -251,7 +261,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "49a684f6752d1f017281d7bbb8b17b856904df15", + "commit": "fac1f242bb58f88e94da3f7cbb50ba6159d02790", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/flow_trace.go b/boatstack/flow_trace.go new file mode 100644 index 0000000..a28e932 --- /dev/null +++ b/boatstack/flow_trace.go @@ -0,0 +1,64 @@ +package boatstack + +import ( + "os" + "path/filepath" + + "github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol" +) + +// flowTraceKillSwitch disables shadow trajectory recording when set to "0". +// Recording is best-effort and off the critical path either way; the switch +// exists so an operator can silence it entirely without a rebuild. +const flowTraceKillSwitch = "BOATSTACK_FLOW_TRACE" + +// flowLogDirectory is the append-only trajectory log location, +// /boatstack/flow, a sibling of the delivery-state directory. It reuses +// deliveryStateDirectory's Git-dir resolution so the two stay in lockstep. +func flowLogDirectory(repo string) (string, error) { + deliveries, err := deliveryStateDirectory(repo) + if err != nil { + return "", err + } + return filepath.Join(filepath.Dir(deliveries), "flow"), nil +} + +// RecordFlowTransition appends a best-effort shadow record of one delivery-flow +// control attempt. It is deliberately inert with respect to command behavior: +// it never returns an error, never panics into a caller, and writes nothing when +// disabled or when anything goes wrong. Nothing consumes the log at runtime yet +// — this is the measurement substrate for the flow-navigation meter, not a +// control point. +// +// transition is the registry TransitionID being attempted; from is the +// delivery-flow state the attempt started in ("" if unknown); ok reports whether +// the underlying handler succeeded (a false ok on a mutation is billed as +// friction, per the cmg model). +func RecordFlowTransition(repo string, transition deliverycontrol.TransitionID, from deliverycontrol.StateID, ok bool) { + // A trace must never take down a command. Swallow any panic from the + // best-effort path. + defer func() { _ = recover() }() + + if os.Getenv(flowTraceKillSwitch) == "0" { + return + } + descriptor, found := deliverycontrol.Transition(transition) + if !found { + return + } + + outcome := deliverycontrol.OutcomeAllowed + if !ok { + outcome = deliverycontrol.OutcomeDenied + } + directory, err := flowLogDirectory(repo) + if err != nil { + return + } + _ = deliverycontrol.AppendAttempt(directory, deliverycontrol.TransitionAttempt{ + From: from, + Transition: transition, + Outcome: outcome, + CostClass: deliverycontrol.ChargedCostClass(descriptor.Kind, descriptor.CostClass, outcome), + }) +} diff --git a/boatstack/flow_trace_test.go b/boatstack/flow_trace_test.go new file mode 100644 index 0000000..8bcb2d4 --- /dev/null +++ b/boatstack/flow_trace_test.go @@ -0,0 +1,67 @@ +package boatstack + +import ( + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol" +) + +func readFlowTrajectory(t *testing.T, repo string) deliverycontrol.Trajectory { + t.Helper() + dir, err := flowLogDirectory(repo) + if err != nil { + t.Fatalf("flow log directory: %v", err) + } + traj, err := deliverycontrol.ReadTrajectory(dir) + if err != nil { + t.Fatalf("read trajectory: %v", err) + } + return traj +} + +// An allowed mutation and a denied one are both recorded, and the denial is +// billed as friction — the recorder is a faithful, cheap witness to real +// command outcomes. +func TestRecordFlowTransitionCapturesOutcomes(t *testing.T) { + repo := prTestRepo(t) + + RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true) + RecordFlowTransition(repo, "delivery.undo", deliverycontrol.StateBuild, false) + + traj := readFlowTrajectory(t, repo) + if len(traj) != 2 { + t.Fatalf("recorded %d attempts, want 2", len(traj)) + } + if traj[0].Outcome != deliverycontrol.OutcomeAllowed || traj[0].CostClass != deliverycontrol.CostMutation { + t.Errorf("allowed gate: got %+v", traj[0]) + } + if traj[1].Outcome != deliverycontrol.OutcomeDenied || traj[1].CostClass != deliverycontrol.CostFriction { + t.Errorf("denied undo should be friction: got %+v", traj[1]) + } + if cost := traj.WalkCost(deliverycontrol.DefaultFlowCostWeights()); cost != 4 { + t.Errorf("walk cost = %d, want 4 (1 move + 3 friction)", cost) + } +} + +// The recorder is best-effort: an unresolvable repo, an unknown transition, and +// the kill switch each leave command behavior untouched and write nothing — +// without erroring or panicking. +func TestRecordFlowTransitionIsBestEffort(t *testing.T) { + // A non-git directory cannot resolve a flow-log location; must be a silent no-op. + RecordFlowTransition(t.TempDir(), "delivery.record_gate_test", deliverycontrol.StateBuild, true) + + repo := prTestRepo(t) + + // An unknown transition is ignored. + RecordFlowTransition(repo, "delivery.does_not_exist", deliverycontrol.StateBuild, true) + if traj := readFlowTrajectory(t, repo); len(traj) != 0 { + t.Errorf("unknown transition should record nothing; got %d", len(traj)) + } + + // The kill switch disables recording entirely. + t.Setenv(flowTraceKillSwitch, "0") + RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true) + if traj := readFlowTrajectory(t, repo); len(traj) != 0 { + t.Errorf("kill switch should suppress recording; got %d", len(traj)) + } +} diff --git a/boatstack/internal/deliverycontrol/graph.go b/boatstack/internal/deliverycontrol/graph.go new file mode 100644 index 0000000..0c169d1 --- /dev/null +++ b/boatstack/internal/deliverycontrol/graph.go @@ -0,0 +1,96 @@ +package deliverycontrol + +import "sort" + +// Edge is a directed, costed control between two delivery-flow states: a single +// move an agent can make, priced by its cost class. +type Edge struct { + Transition TransitionID + From StateID + To StateID + CostClass TransitionCostClass + Cost int +} + +// Graph is a weighted directed graph of delivery-flow controls. It is the +// optimization projection of the single registry declaration: the same +// transitions the conformance projection audits, arranged as a graph a +// deterministic oracle can score. Only state-changing transitions (To != "") +// become edges; read-only observes do not advance state and so are not moves on +// this graph. +type Graph struct { + weights FlowCostWeights + out map[StateID][]Edge + nodes map[StateID]bool +} + +// NewGraph returns an empty graph priced by the given weights. +func NewGraph(weights FlowCostWeights) *Graph { + return &Graph{ + weights: weights, + out: map[StateID][]Edge{}, + nodes: map[StateID]bool{}, + } +} + +// AddEdge adds a directed edge priced by the transition's cost class. An edge +// whose cost class has no defined weight is skipped (the well-formedness +// conformance test forbids that in the registry, so this only guards ad-hoc +// graphs). Endpoints are registered as nodes even when the class is unknown, so +// a state that only appears on a skipped edge is still a known node. +func (g *Graph) AddEdge(from, to StateID, transition TransitionID, class TransitionCostClass) { + g.nodes[from] = true + g.nodes[to] = true + cost, ok := g.weights.Cost(class) + if !ok { + return + } + g.out[from] = append(g.out[from], Edge{ + Transition: transition, + From: from, + To: to, + CostClass: class, + Cost: cost, + }) +} + +// Out returns the out-edges of a state in insertion order (deterministic). +func (g *Graph) Out(state StateID) []Edge { + return g.out[state] +} + +// Has reports whether a state is a known node (appears as an edge endpoint). +// The oracle uses this to return Unresolved for a state it has never seen rather +// than fabricate a path from nowhere. +func (g *Graph) Has(state StateID) bool { + return g.nodes[state] +} + +// Nodes returns every known state, sorted for deterministic iteration. +func (g *Graph) Nodes() []StateID { + out := make([]StateID, 0, len(g.nodes)) + for n := range g.nodes { + out = append(out, n) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + +// RegistryGraph projects the registry into a costed graph: one edge per +// (From, To) pair of every transition that changes delivery state. A transition +// with an empty To (a pure observation) advances nothing and contributes no +// edge; a transition with an empty From (delivery.next, resolved from any state) +// likewise contributes no edge because it changes no state. Iteration order +// follows the registry declaration, so the projection is deterministic. +func RegistryGraph(weights FlowCostWeights) *Graph { + g := NewGraph(weights) + for _, tr := range Transitions() { + if tr.To == "" { + continue + } + for _, from := range tr.From { + g.AddEdge(from, tr.To, tr.ID, tr.CostClass) + } + } + return g +} diff --git a/boatstack/internal/deliverycontrol/oracle.go b/boatstack/internal/deliverycontrol/oracle.go new file mode 100644 index 0000000..a968b6a --- /dev/null +++ b/boatstack/internal/deliverycontrol/oracle.go @@ -0,0 +1,106 @@ +package deliverycontrol + +// Resolution reports whether the oracle found a path. Unknown or unreachable +// states are Unresolved — the oracle never fabricates a route it cannot prove. +type Resolution string + +const ( + Resolved Resolution = "resolved" + Unresolved Resolution = "unresolved" +) + +// FlowPath is the oracle's answer: the lowest-cost sequence of controls from a +// start state to a goal state, or Unresolved when none exists. +type FlowPath struct { + From StateID + Goal StateID + Edges []Edge + Cost int + Resolution Resolution +} + +// ShortestFlow is the free oracle: Dijkstra over the owned graph for the +// lowest-cost walk from a state to a goal. It is deterministic — ties are broken +// by the frontier's insertion order, which follows the registry declaration — so +// the same graph and endpoints always yield the same path. +// +// It returns Unresolved, never a guess, when: +// - the start state is not a known node (unknown/invalid state), or +// - the goal is not a known node, or +// - no path from start to goal exists. +// +// A start that already equals the goal resolves to a zero-cost, empty path. +func (g *Graph) ShortestFlow(from, goal StateID) FlowPath { + result := FlowPath{From: from, Goal: goal, Resolution: Unresolved} + + if !g.Has(from) || !g.Has(goal) { + return result + } + if from == goal { + result.Cost = 0 + result.Resolution = Resolved + return result + } + + const unreached = -1 + dist := map[StateID]int{from: 0} + prev := map[StateID]Edge{} + visited := map[StateID]bool{} + + for { + // Select the unvisited node with the smallest known distance. Nodes() is + // sorted, so among equal distances the lexicographically-first node wins: + // a stable, reproducible choice. + current := StateID("") + best := unreached + for _, node := range g.Nodes() { + if visited[node] { + continue + } + d, seen := dist[node] + if !seen { + continue + } + if best == unreached || d < best { + best = d + current = node + } + } + if best == unreached { + // Frontier exhausted without reaching the goal. + return result + } + if current == goal { + break + } + visited[current] = true + + for _, edge := range g.Out(current) { + candidate := dist[current] + edge.Cost + if existing, seen := dist[edge.To]; !seen || candidate < existing { + dist[edge.To] = candidate + prev[edge.To] = edge + } + } + } + + // Reconstruct the path from goal back to start. + var reversed []Edge + for at := goal; at != from; { + edge, ok := prev[at] + if !ok { + return FlowPath{From: from, Goal: goal, Resolution: Unresolved} + } + reversed = append(reversed, edge) + at = edge.From + } + edges := make([]Edge, len(reversed)) + for i, edge := range reversed { + edges[len(reversed)-1-i] = edge + } + + result.Edges = edges + result.Cost = dist[goal] + result.Resolution = Resolved + return result +} diff --git a/boatstack/internal/deliverycontrol/oracle_test.go b/boatstack/internal/deliverycontrol/oracle_test.go new file mode 100644 index 0000000..c0ebf63 --- /dev/null +++ b/boatstack/internal/deliverycontrol/oracle_test.go @@ -0,0 +1,84 @@ +package deliverycontrol + +import "testing" + +// control-law: oracle-resolves-real-paths-or-unresolved +// The oracle must return the true shortest cost over the registry graph for +// reachable goals and Unresolved — never a fabricated path — for unknown or +// unreachable states. +func TestShortestFlowOverRegistry(t *testing.T) { + g := RegistryGraph(DefaultFlowCostWeights()) + + cases := []struct { + name string + from StateID + goal StateID + cost int + }{ + {"activate through publish", StateUninitialized, StatePublished, 4}, + {"build to published", StateBuild, StatePublished, 3}, + {"review to published", StateReviewPassed, StatePublished, 1}, + {"repair out of invalid", StateInvalid, StateUninitialized, 1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + path := g.ShortestFlow(tc.from, tc.goal) + if path.Resolution != Resolved { + t.Fatalf("%s→%s: expected Resolved, got %s", tc.from, tc.goal, path.Resolution) + } + if path.Cost != tc.cost { + t.Errorf("%s→%s: cost = %d, want %d (edges: %v)", tc.from, tc.goal, path.Cost, tc.cost, edgeIDs(path.Edges)) + } + // The reconstructed path's costs must sum to the reported cost, and + // each edge must chain from the previous To. + sum, at := 0, tc.from + for _, e := range path.Edges { + if e.From != at { + t.Errorf("%s→%s: broken chain at edge %s (from %s, expected %s)", tc.from, tc.goal, e.Transition, e.From, at) + } + sum += e.Cost + at = e.To + } + if sum != path.Cost { + t.Errorf("%s→%s: edge cost sum %d != reported %d", tc.from, tc.goal, sum, path.Cost) + } + if at != tc.goal { + t.Errorf("%s→%s: path ends at %s", tc.from, tc.goal, at) + } + }) + } +} + +func TestShortestFlowSameStateIsFree(t *testing.T) { + g := RegistryGraph(DefaultFlowCostWeights()) + path := g.ShortestFlow(StateBuild, StateBuild) + if path.Resolution != Resolved || path.Cost != 0 || len(path.Edges) != 0 { + t.Fatalf("same-state path: got %+v, want resolved zero-cost empty", path) + } +} + +func TestShortestFlowUnresolved(t *testing.T) { + g := RegistryGraph(DefaultFlowCostWeights()) + + // Unknown start state — the oracle has never seen it, so it must not guess. + if path := g.ShortestFlow(StateID("MADE_UP"), StatePublished); path.Resolution != Unresolved { + t.Errorf("unknown start: got %s, want Unresolved", path.Resolution) + } + // Unknown goal state. + if path := g.ShortestFlow(StateBuild, StateID("NOWHERE")); path.Resolution != Unresolved { + t.Errorf("unknown goal: got %s, want Unresolved", path.Resolution) + } + // Known states with no connecting path: PENDING can only be discarded, so it + // cannot reach PUBLISHED. + if path := g.ShortestFlow(StatePending, StatePublished); path.Resolution != Unresolved { + t.Errorf("unreachable goal: got %s, want Unresolved", path.Resolution) + } +} + +func edgeIDs(edges []Edge) []TransitionID { + ids := make([]TransitionID, len(edges)) + for i, e := range edges { + ids[i] = e.Transition + } + return ids +} diff --git a/boatstack/internal/deliverycontrol/trajectory.go b/boatstack/internal/deliverycontrol/trajectory.go new file mode 100644 index 0000000..45aa5dc --- /dev/null +++ b/boatstack/internal/deliverycontrol/trajectory.go @@ -0,0 +1,94 @@ +package deliverycontrol + +// Outcome is what happened when an agent attempted a control. +type Outcome string + +const ( + // OutcomeAllowed: the control was accepted and advanced (or observed) state. + OutcomeAllowed Outcome = "allowed" + // OutcomeDenied: the control was refused — a blocked committed mutation is the + // friction the model prices at 3 (a burned turn that returns nothing). + OutcomeDenied Outcome = "denied" +) + +// TransitionAttempt is one recorded move on the delivery-flow graph: the control +// an agent tried, from which state, and how it landed. Trajectories are built +// from these, append-only. +type TransitionAttempt struct { + Sequence int `json:"sequence"` + From StateID `json:"from"` + Transition TransitionID `json:"transition"` + Goal StateID `json:"goal,omitempty"` + Outcome Outcome `json:"outcome"` + CostClass TransitionCostClass `json:"cost_class"` + Note string `json:"note,omitempty"` +} + +// Trajectory is an ordered walk of attempts — one real (or replayed) session. +type Trajectory []TransitionAttempt + +// ChargedCostClass returns the cost class an attempt is billed at. A denied +// mutation (committed or reversible) is friction; every other outcome keeps the +// transition's declared class. Read-only controls are never denied in the model, +// so they always bill their own class. Centralizing the rule here keeps the +// recorder and the report in agreement. +func ChargedCostClass(kind TransitionKind, declared TransitionCostClass, outcome Outcome) TransitionCostClass { + if outcome == OutcomeDenied { + switch kind { + case KindCommittedMutation, KindReversibleMutation: + return CostFriction + } + } + return declared +} + +// FlowTrajectoryReport is the meter: the observed navigation cost of a walk, +// the oracle's cost for the same start→goal, and the regret between them. When +// the oracle cannot resolve the endpoints, Resolution is Unresolved and Regret +// is left at zero (there is no baseline to regret against — never a fabricated +// one). +type FlowTrajectoryReport struct { + Start StateID + Goal StateID + JFlow int + JFlowStar int + Regret int + Steps int + Resolution Resolution +} + +// WalkCost sums a trajectory's observed J_flow: each attempt billed at its +// charged cost class. Attempts whose class has no weight are skipped rather than +// silently counted as zero-defined. +func (t Trajectory) WalkCost(weights FlowCostWeights) int { + total := 0 + for _, attempt := range t { + if cost, ok := weights.Cost(attempt.CostClass); ok { + total += cost + } + } + return total +} + +// ComputeReport measures a trajectory against the oracle. Start is the walk's +// first From; the caller supplies the goal (the accepted end state B). J_flow is +// the observed walk cost; J_flow* is the oracle's shortest cost start→goal over +// the graph; Regret = J_flow − J_flow* when the oracle resolves. +func ComputeReport(t Trajectory, g *Graph, weights FlowCostWeights, goal StateID) FlowTrajectoryReport { + report := FlowTrajectoryReport{ + Goal: goal, + JFlow: t.WalkCost(weights), + Steps: len(t), + Resolution: Unresolved, + } + if len(t) > 0 { + report.Start = t[0].From + } + oracle := g.ShortestFlow(report.Start, goal) + report.Resolution = oracle.Resolution + if oracle.Resolution == Resolved { + report.JFlowStar = oracle.Cost + report.Regret = report.JFlow - oracle.Cost + } + return report +} diff --git a/boatstack/internal/deliverycontrol/trajectory_test.go b/boatstack/internal/deliverycontrol/trajectory_test.go new file mode 100644 index 0000000..da2b35c --- /dev/null +++ b/boatstack/internal/deliverycontrol/trajectory_test.go @@ -0,0 +1,139 @@ +package deliverycontrol + +import "testing" + +// Incident-graph node names, from the cmg design note +// (../../notes/delivery-flow-navigation-model.md). The published-slice incident +// is modeled there as a small costed graph; this test rebuilds that graph and +// walk and runs the real Go oracle over it, pinning the note's SELF-CHECK +// numbers (J_flow=15, J_flow*=5, regret=10) to this implementation. +const ( + sNeedsFix StateID = "NEEDS_FIX" + sObserved StateID = "OBSERVED" + sPushDenied StateID = "PUSH_DENIED" + sUndoBlocked StateID = "UNDO_BLOCKED" + sRepair StateID = "REPAIR" + sAsk StateID = "ASK" + sDiagnosed StateID = "DIAGNOSED" + sUpgraded StateID = "UPGRADED" + sRegated StateID = "REGATED" + sLanded StateID = "LANDED" +) + +func incidentGraph() *Graph { + g := NewGraph(DefaultFlowCostWeights()) + // The unique low-cost exit of the start state: observe before acting. + g.AddEdge(sNeedsFix, sObserved, "observe-state", CostObserve) + // The friction region — reachable only by acting before observing. + g.AddEdge(sNeedsFix, sPushDenied, "git-push", CostFriction) + g.AddEdge(sPushDenied, sUndoBlocked, "undo", CostFriction) + g.AddEdge(sUndoBlocked, sRepair, "undo-mutation", CostFriction) + g.AddEdge(sRepair, sAsk, "repair-state", CostRecovery) + g.AddEdge(sAsk, sObserved, "ask-then-observe", CostObserve) + // The shared tail every path takes once oriented. + g.AddEdge(sObserved, sDiagnosed, "diagnose", CostInspect) + g.AddEdge(sDiagnosed, sUpgraded, "upgrade-helper", CostInspect) + g.AddEdge(sUpgraded, sRegated, "re-gate", CostMutation) + g.AddEdge(sRegated, sLanded, "publish-update", CostMutation) + return g +} + +// observedIncidentWalk is the real session from the note: the agent acts before +// it observes, hits friction on three committed/reversible mutations, then +// orients. Friction is derived by ChargedCostClass from the denied outcome — not +// hardcoded — so the model's "denied mutation = friction" rule is what produces +// the cost. +func observedIncidentWalk() Trajectory { + deny := func(from StateID, id TransitionID, kind TransitionKind) TransitionAttempt { + return TransitionAttempt{ + From: from, Transition: id, Goal: sLanded, Outcome: OutcomeDenied, + CostClass: ChargedCostClass(kind, CostMutation, OutcomeDenied), + } + } + allow := func(from StateID, id TransitionID, class TransitionCostClass) TransitionAttempt { + return TransitionAttempt{ + From: from, Transition: id, Goal: sLanded, Outcome: OutcomeAllowed, + CostClass: ChargedCostClass(KindObserve, class, OutcomeAllowed), + } + } + return Trajectory{ + deny(sNeedsFix, "git-push", KindCommittedMutation), + deny(sPushDenied, "undo", KindReversibleMutation), + deny(sUndoBlocked, "undo-mutation", KindCommittedMutation), + allow(sRepair, "repair-state", CostRecovery), + allow(sAsk, "ask-then-observe", CostObserve), + allow(sObserved, "diagnose", CostInspect), + allow(sDiagnosed, "upgrade-helper", CostInspect), + allow(sUpgraded, "re-gate", CostMutation), + allow(sRegated, "publish-update", CostMutation), + } +} + +// control-law: flow-regret-matches-cmg-note +func TestPublishedSliceIncidentRegret(t *testing.T) { + g := incidentGraph() + weights := DefaultFlowCostWeights() + + // The oracle path is Dijkstra over the owned graph, not learned: observe first. + oracle := g.ShortestFlow(sNeedsFix, sLanded) + if oracle.Resolution != Resolved { + t.Fatalf("oracle could not resolve the incident: %s", oracle.Resolution) + } + if oracle.Cost != 5 { + t.Errorf("J_flow* = %d, want 5 (oracle path: %v)", oracle.Cost, edgeIDs(oracle.Edges)) + } + if got := edgeIDs(oracle.Edges); len(got) != 5 || got[0] != "observe-state" { + t.Errorf("oracle should exit via observe-state; got %v", got) + } + + report := ComputeReport(observedIncidentWalk(), g, weights, sLanded) + if report.Resolution != Resolved { + t.Fatalf("report unresolved") + } + if report.JFlow != 15 { + t.Errorf("J_flow = %d, want 15", report.JFlow) + } + if report.JFlowStar != 5 { + t.Errorf("J_flow* = %d, want 5", report.JFlowStar) + } + if report.Regret != 10 { + t.Errorf("regret = %d, want 10 (all of it in J_flow)", report.Regret) + } +} + +func TestChargedCostClass(t *testing.T) { + cases := []struct { + kind TransitionKind + declared TransitionCostClass + outcome Outcome + want TransitionCostClass + }{ + {KindCommittedMutation, CostMutation, OutcomeDenied, CostFriction}, + {KindReversibleMutation, CostMutation, OutcomeDenied, CostFriction}, + {KindCommittedMutation, CostMutation, OutcomeAllowed, CostMutation}, + {KindObserve, CostObserve, OutcomeDenied, CostObserve}, // reads are never friction + {KindRecovery, CostRecovery, OutcomeDenied, CostRecovery}, // recovery denial is not modeled as friction + } + for _, tc := range cases { + if got := ChargedCostClass(tc.kind, tc.declared, tc.outcome); got != tc.want { + t.Errorf("ChargedCostClass(%s,%s,%s) = %s, want %s", tc.kind, tc.declared, tc.outcome, got, tc.want) + } + } +} + +// A trajectory whose goal the oracle cannot resolve reports Unresolved and no +// regret — never a fabricated baseline. +func TestComputeReportUnresolvedGoal(t *testing.T) { + g := RegistryGraph(DefaultFlowCostWeights()) + traj := Trajectory{{From: StatePending, Transition: "delivery.discard_delivery", Outcome: OutcomeAllowed, CostClass: CostMutation}} + report := ComputeReport(traj, g, DefaultFlowCostWeights(), StatePublished) + if report.Resolution != Unresolved { + t.Errorf("resolution = %s, want Unresolved", report.Resolution) + } + if report.Regret != 0 || report.JFlowStar != 0 { + t.Errorf("unresolved report must not fabricate a baseline: %+v", report) + } + if report.JFlow != 1 { + t.Errorf("observed J_flow should still be measured: got %d, want 1", report.JFlow) + } +} diff --git a/boatstack/internal/deliverycontrol/trajectorylog.go b/boatstack/internal/deliverycontrol/trajectorylog.go new file mode 100644 index 0000000..918030d --- /dev/null +++ b/boatstack/internal/deliverycontrol/trajectorylog.go @@ -0,0 +1,74 @@ +package deliverycontrol + +import ( + "bufio" + "encoding/json" + "errors" + "io/fs" + "os" + "path/filepath" +) + +// trajectoryLogFile is the append-only record of attempts within a flow-log +// directory. One JSON object per line (JSONL), oldest first. +const trajectoryLogFile = "trajectory.jsonl" + +// AppendAttempt appends one attempt to the trajectory log under dir, creating +// the directory and file as needed. It is the write half of the shadow trace; +// it returns an error so tests can assert round-trips, but the live recorder +// treats every error as best-effort and swallows it — a trace must never change +// command behavior. +func AppendAttempt(dir string, attempt TransitionAttempt) error { + if dir == "" { + return errors.New("trajectory log directory is empty") + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + line, err := json.Marshal(attempt) + if err != nil { + return err + } + file, err := os.OpenFile(filepath.Join(dir, trajectoryLogFile), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return err + } + defer file.Close() + if _, err := file.Write(append(line, '\n')); err != nil { + return err + } + return nil +} + +// ReadTrajectory reads the append-only log under dir in write order. A missing +// log is an empty trajectory, not an error, so a first read before any write is +// well-defined. +func ReadTrajectory(dir string) (Trajectory, error) { + file, err := os.Open(filepath.Join(dir, trajectoryLogFile)) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return Trajectory{}, nil + } + return nil, err + } + defer file.Close() + + var trajectory Trajectory + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + var attempt TransitionAttempt + if err := json.Unmarshal(line, &attempt); err != nil { + return nil, err + } + trajectory = append(trajectory, attempt) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return trajectory, nil +} diff --git a/boatstack/internal/deliverycontrol/trajectorylog_test.go b/boatstack/internal/deliverycontrol/trajectorylog_test.go new file mode 100644 index 0000000..512ab10 --- /dev/null +++ b/boatstack/internal/deliverycontrol/trajectorylog_test.go @@ -0,0 +1,49 @@ +package deliverycontrol + +import ( + "path/filepath" + "testing" +) + +func TestTrajectoryLogRoundTrip(t *testing.T) { + dir := filepath.Join(t.TempDir(), "flow") + + // A first read before any write is an empty trajectory, not an error. + if traj, err := ReadTrajectory(dir); err != nil || len(traj) != 0 { + t.Fatalf("empty read: got %v (%d), err %v", traj, len(traj), err) + } + + want := Trajectory{ + {Sequence: 0, From: StateBuild, Transition: "delivery.record_gate_test", Outcome: OutcomeAllowed, CostClass: CostMutation}, + {Sequence: 1, From: StateBuild, Transition: "delivery.undo", Outcome: OutcomeDenied, CostClass: CostFriction, Note: "gate receipt exists"}, + } + for _, attempt := range want { + if err := AppendAttempt(dir, attempt); err != nil { + t.Fatalf("append %s: %v", attempt.Transition, err) + } + } + + got, err := ReadTrajectory(dir) + if err != nil { + t.Fatalf("read: %v", err) + } + if len(got) != len(want) { + t.Fatalf("read %d attempts, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("attempt %d: got %+v, want %+v", i, got[i], want[i]) + } + } + + // Order is preserved and the walk cost reflects the friction on the denial. + if cost := got.WalkCost(DefaultFlowCostWeights()); cost != 4 { + t.Errorf("walk cost = %d, want 4 (1 move + 3 friction)", cost) + } +} + +func TestAppendAttemptRejectsEmptyDir(t *testing.T) { + if err := AppendAttempt("", TransitionAttempt{Transition: "x"}); err == nil { + t.Error("expected an error for an empty directory") + } +} diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 5f7bbc6..908f0d6 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`49a684f6752d1f017281d7bbb8b17b856904df15`](https://github.com/operatorstack/intelligence-flow/tree/49a684f6752d1f017281d7bbb8b17b856904df15/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`fac1f242bb58f88e94da3f7cbb50ba6159d02790`](https://github.com/operatorstack/intelligence-flow/tree/fac1f242bb58f88e94da3f7cbb50ba6159d02790/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index 46d8780..a7ce558 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "49a684f6752d1f017281d7bbb8b17b856904df15", + "source_commit": "fac1f242bb58f88e94da3f7cbb50ba6159d02790", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:49a684f6752d1f017281d7bbb8b17b856904df15" + "last_verified_version": "source:fac1f242bb58f88e94da3f7cbb50ba6159d02790" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 8a601fa..7d0745e 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "49a684f6752d1f017281d7bbb8b17b856904df15", + "source_commit": "fac1f242bb58f88e94da3f7cbb50ba6159d02790", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-25-deliverycontrol-flow-oracle.md b/release-notes/2026-07-25-deliverycontrol-flow-oracle.md new file mode 100644 index 0000000..c6871cd --- /dev/null +++ b/release-notes/2026-07-25-deliverycontrol-flow-oracle.md @@ -0,0 +1,13 @@ +### Flow-navigation oracle and regret meter over the delivery graph (shadow, read-only) + +The delivery transition registry now projects into a costed graph, and a deterministic +shortest-path oracle scores it: given a start and a goal delivery state it returns the lowest-cost +sequence of moves, or reports the pair as unresolved rather than guess a route it cannot prove. A +session's actual moves can be recorded as an append-only trajectory and measured against that oracle, +yielding the navigation cost of the walk, the oracle's cost for the same endpoints, and the regret +between them — with a blocked mutation priced as friction, matching the costed-graph design note. + +This is shadow-only and best-effort: the trajectory recorder never changes a command's behavior or +exit code, is disabled by a kill switch, and nothing consumes the meter at runtime. It is the +measurement substrate for a future flow-navigation advisor, and a replay test pins the meter to the +worked incident in the design note.