Skip to content

Commit f2faf38

Browse files
Sync Boatstack from Intelligence Flow Labs @ 6877a03c0381 (#159)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent 986a60e commit f2faf38

29 files changed

Lines changed: 2048 additions & 73 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/7bcc7dcb692a3f4b34f6cbd84d46e648e055634e/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/6877a03c0381c7dec94a0d8a52c9f8c6a0954016/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: 32 additions & 28 deletions
Large diffs are not rendered by default.

boatstack/cmd/boatstack-helper/coverage_conformance_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ var nonDeliveryVerbs = map[string]bool{
8383
"workspace-sync": true,
8484
// Flow layer itself is read-only navigation over the machine, not a transition.
8585
"flow": true,
86+
// Insight capture is a detached control-plane tenant. Its append-only events
87+
// observe delivery evidence but never transition the delivery machine.
88+
"insight": true,
8689
// Retro derivation reads operator-supplied transcripts and proposes typed
8790
// promotions; it mutates nothing, so it registers no delivery transition.
8891
// control-law: retro-proposes-never-enforces
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strings"
9+
10+
boatstack "github.com/operatorstack/boatstack/boatstack"
11+
)
12+
13+
func readInsightInput(path string) ([]byte, error) {
14+
path = strings.TrimSpace(path)
15+
if path == "" || path == "-" {
16+
return io.ReadAll(os.Stdin)
17+
}
18+
return os.ReadFile(path)
19+
}
20+
21+
func printInsightView(view boatstack.InsightView, jsonOutput bool) int {
22+
if jsonOutput {
23+
return emitJSON(view)
24+
}
25+
fmt.Printf("Insight %s: %s\n", view.Capture.ID, view.Evaluation.State)
26+
fmt.Println(view.Evaluation.Reason)
27+
fmt.Printf("Repository diff: %s\n", view.RepositoryPath)
28+
return 0
29+
}
30+
31+
func insightCommand(arguments []string) int {
32+
if len(arguments) == 0 {
33+
fmt.Fprintln(os.Stderr, "usage: boatstack-helper insight <check|save|list|show|associate|bind|evaluate|frontier|disposition>")
34+
return 2
35+
}
36+
switch arguments[0] {
37+
case "check":
38+
flags := flag.NewFlagSet("insight check", flag.ContinueOnError)
39+
repo := flags.String("repo", ".", "repository whose insight inbox should be checked")
40+
input := flags.String("input", "-", "capture JSON file, or - for stdin")
41+
if err := flags.Parse(arguments[1:]); err != nil {
42+
return 2
43+
}
44+
value, err := readInsightInput(*input)
45+
if err != nil {
46+
return fail(err)
47+
}
48+
result, err := boatstack.CheckInsightCapture(*repo, value)
49+
if err != nil {
50+
return fail(err)
51+
}
52+
return emitJSON(result)
53+
case "save":
54+
flags := flag.NewFlagSet("insight save", flag.ContinueOnError)
55+
repo := flags.String("repo", ".", "repository whose tracked insight inbox should receive the capture")
56+
input := flags.String("input", "-", "capture JSON file, or - for stdin")
57+
nonce := flags.String("preview-nonce", "", "nonce returned by insight check")
58+
fingerprint := flags.String("preview-fingerprint", "", "fingerprint returned by insight check")
59+
jsonOutput := flags.Bool("json", false, "print the structured capture")
60+
if err := flags.Parse(arguments[1:]); err != nil {
61+
return 2
62+
}
63+
value, err := readInsightInput(*input)
64+
if err != nil {
65+
return fail(err)
66+
}
67+
view, err := boatstack.SaveInsightCapture(*repo, value, *nonce, *fingerprint)
68+
if err != nil {
69+
return fail(err)
70+
}
71+
return printInsightView(view, *jsonOutput)
72+
case "list":
73+
flags := flag.NewFlagSet("insight list", flag.ContinueOnError)
74+
repo := flags.String("repo", ".", "repository whose captures should be listed")
75+
if err := flags.Parse(arguments[1:]); err != nil {
76+
return 2
77+
}
78+
views, err := boatstack.ListInsights(*repo)
79+
if err != nil {
80+
return fail(err)
81+
}
82+
return emitJSON(views)
83+
case "show":
84+
flags := flag.NewFlagSet("insight show", flag.ContinueOnError)
85+
repo := flags.String("repo", ".", "repository whose capture should be shown")
86+
id := flags.String("id", "", "insight capture id")
87+
if err := flags.Parse(arguments[1:]); err != nil {
88+
return 2
89+
}
90+
view, err := boatstack.ShowInsight(*repo, *id)
91+
if err != nil {
92+
return fail(err)
93+
}
94+
return emitJSON(view)
95+
case "associate":
96+
flags := flag.NewFlagSet("insight associate", flag.ContinueOnError)
97+
repo := flags.String("repo", ".", "repository whose capture should be associated")
98+
id := flags.String("id", "", "insight capture id")
99+
primary := flags.String("primary-topic", "", "human-confirmed primary feature topic")
100+
var related stringList
101+
flags.Var(&related, "related-topic", "related feature topic (repeatable)")
102+
if err := flags.Parse(arguments[1:]); err != nil {
103+
return 2
104+
}
105+
view, err := boatstack.AssociateInsight(*repo, *id, *primary, related)
106+
if err != nil {
107+
return fail(err)
108+
}
109+
return emitJSON(view)
110+
case "bind":
111+
flags := flag.NewFlagSet("insight bind", flag.ContinueOnError)
112+
repo := flags.String("repo", ".", "repository whose capture should be bound")
113+
id := flags.String("id", "", "insight capture id")
114+
feature := flags.String("feature", "", "managed feature id")
115+
var criteria stringList
116+
flags.Var(&criteria, "criterion", "mapped acceptance criterion id (repeatable)")
117+
if err := flags.Parse(arguments[1:]); err != nil {
118+
return 2
119+
}
120+
view, err := boatstack.BindInsight(*repo, *id, *feature, criteria)
121+
if err != nil {
122+
return fail(err)
123+
}
124+
return emitJSON(view)
125+
case "evaluate":
126+
flags := flag.NewFlagSet("insight evaluate", flag.ContinueOnError)
127+
repo := flags.String("repo", ".", "repository whose capture should be evaluated")
128+
id := flags.String("id", "", "insight capture id")
129+
if err := flags.Parse(arguments[1:]); err != nil {
130+
return 2
131+
}
132+
result, err := boatstack.EvaluateInsight(*repo, *id)
133+
if err != nil {
134+
return fail(err)
135+
}
136+
return emitJSON(result)
137+
case "frontier":
138+
flags := flag.NewFlagSet("insight frontier", flag.ContinueOnError)
139+
repo := flags.String("repo", ".", "repository whose pending insight frontier should be shown")
140+
jsonOutput := flags.Bool("json", false, "print the structured frontier")
141+
if err := flags.Parse(arguments[1:]); err != nil {
142+
return 2
143+
}
144+
report, err := boatstack.InsightFrontier(*repo)
145+
if err != nil {
146+
return fail(err)
147+
}
148+
if *jsonOutput {
149+
return emitJSON(report)
150+
}
151+
fmt.Print(boatstack.FormatInsightFrontier(report))
152+
return 0
153+
case "disposition":
154+
flags := flag.NewFlagSet("insight disposition", flag.ContinueOnError)
155+
repo := flags.String("repo", ".", "repository whose capture should be dispositioned")
156+
id := flags.String("id", "", "insight capture id")
157+
outcome := flags.String("outcome", "", "completed, deferred, rejected, or duplicate")
158+
reason := flags.String("reason", "", "human reason, required for non-ready completion and non-complete outcomes")
159+
duplicateOf := flags.String("duplicate-of", "", "original capture id for duplicate outcomes")
160+
if err := flags.Parse(arguments[1:]); err != nil {
161+
return 2
162+
}
163+
view, err := boatstack.DisposeInsight(*repo, *id, *outcome, *reason, *duplicateOf)
164+
if err != nil {
165+
return fail(err)
166+
}
167+
return emitJSON(view)
168+
default:
169+
fmt.Fprintln(os.Stderr, "unknown insight subcommand:", arguments[0])
170+
return 2
171+
}
172+
}

boatstack/cmd/boatstack-helper/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1510,7 +1510,7 @@ func workspaceSyncCommand(arguments []string) int {
15101510

15111511
func run() int {
15121512
if len(os.Args) < 2 {
1513-
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <attach|detach|detached-status|context|activate|deactivate|init|update|check-update|repair-status|operation-status|prepare-update-pr|publish-update-pr|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|repair-state|mutation-status|undo|run-preflight|record-change|record-journey-results|ignore-delivery|record-delivery-gate|record-pr-visual-evidence|capture-evidence|provision-capability|capability-register|record-pr-visual-publication|attach-evidence|check-safety|migrate-config|safety-hook|ambient-safety-hook|diagnose-hook|render-denial|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-reap|workspace-status|workspace-sync|flow|retro|doctor|version>")
1513+
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <attach|detach|detached-status|context|activate|deactivate|init|update|check-update|repair-status|operation-status|prepare-update-pr|publish-update-pr|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|repair-state|mutation-status|undo|run-preflight|record-change|record-journey-results|ignore-delivery|record-delivery-gate|record-pr-visual-evidence|capture-evidence|provision-capability|capability-register|record-pr-visual-publication|attach-evidence|check-safety|migrate-config|safety-hook|ambient-safety-hook|diagnose-hook|render-denial|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-reap|workspace-status|workspace-sync|flow|retro|insight|doctor|version>")
15141514
return 2
15151515
}
15161516
switch os.Args[1] {
@@ -1630,6 +1630,8 @@ func run() int {
16301630
return flowCommand(os.Args[2:])
16311631
case "retro":
16321632
return retroCommand(os.Args[2:])
1633+
case "insight":
1634+
return insightCommand(os.Args[2:])
16331635
case "version":
16341636
fmt.Printf("Boatstack %s (%s)\n", boatstack.Version, boatstack.SourceCommit)
16351637
return 0

boatstack/config_documentation_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ func TestPublicConfigurationGuideContainsOnlySupportedUserControls(t *testing.T)
114114
want := []string{
115115
"adapters",
116116
"delivery.terminal",
117+
"insights.capture_mode",
118+
"insights.completion_mode",
119+
"insights.enabled",
120+
"insights.evaluate_on_pr",
121+
"insights.pending_frontier",
122+
"insights.suggest_features",
123+
"insights.value_map",
117124
"project.commands",
118125
"project.context",
119126
"project.default_branch",

boatstack/delivery.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,11 @@ func MarkDeliveryPublished(repo, feature, sliceID, url string) error {
12411241
if strings.TrimSpace(state.Slices[sliceIndex].PRState) == "" {
12421242
state.Slices[sliceIndex].PRState = "OPEN"
12431243
}
1244-
return saveDeliveryState(repo, state)
1244+
if err := saveDeliveryState(repo, state); err != nil {
1245+
return err
1246+
}
1247+
reconcileInsightsForFeature(repo, feature)
1248+
return nil
12451249
}
12461250
if slice.Status != StatusReviewPassed {
12471251
return fmt.Errorf("delivery slice %s is not ready to publish", sliceID)
@@ -1262,7 +1266,11 @@ func MarkDeliveryPublished(repo, feature, sliceID, url string) error {
12621266
state.Mode = "NORMAL"
12631267
}
12641268
}
1265-
return saveDeliveryState(repo, state)
1269+
if err := saveDeliveryState(repo, state); err != nil {
1270+
return err
1271+
}
1272+
reconcileInsightsForFeature(repo, feature)
1273+
return nil
12661274
}
12671275

12681276
// scanManagedDeliveries partitions the delivery-state store into deliveries

boatstack/denial_solutions.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package boatstack
22

33
import (
4+
"path/filepath"
45
"strings"
56

67
"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
@@ -174,6 +175,15 @@ func tamperOwnerVerbs(repo, attempted string) []string {
174175
if err != nil {
175176
continue
176177
}
178+
if entry.Class == ClassCommittedInsight {
179+
root, rootErr := w.InsightDir()
180+
if rootErr == nil {
181+
relative, relErr := filepath.Rel(w.RepoRoot, root)
182+
if relErr == nil && strings.Contains(normalized, filepath_ToSlashLower(relative)) {
183+
return entry.OwnerVerbs
184+
}
185+
}
186+
}
177187
key := boatstackSubtreeKey(filepath_ToSlashLower(sample))
178188
if key != "" && strings.Contains(normalized, "boatstack/"+key) {
179189
return entry.OwnerVerbs

boatstack/denial_solutions_conformance_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func TestTamperDenialNamesDeclaredOwnerVerbs(t *testing.T) {
125125
"state-root/boatstack/registry.json": {"attach", "detach"},
126126
".git/boatstack/visual-evidence/x/manifest.json": {"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication", "attach-evidence"},
127127
"boatstack/repositories/sample/binding.json": {"attach", "detach", "activate"},
128+
"docs/insights/ins-sample/capture.json": {"insight"},
128129
}
129130
for attempted, want := range cases {
130131
got := tamperOwnerVerbs(repo, attempted)

boatstack/detached_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ func TestAttachPopulatesExternalRuntimeSlot(t *testing.T) {
387387
t.Fatalf("runtime slot must be external, got %s", p)
388388
}
389389
}
390+
manifest, loadedPath, err := loadSharedRuntime(repo)
391+
if err != nil {
392+
t.Fatalf("detached runtime must load through its external ownership boundary: %v", err)
393+
}
394+
if loadedPath != binaryPath || manifest.BoatstackVersion != Version {
395+
t.Fatalf("loaded detached runtime drifted: path=%s manifest=%+v", loadedPath, manifest)
396+
}
390397
}
391398

392399
// control-law: activation-preserves-existing-host-config

0 commit comments

Comments
 (0)