Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a/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/7bcc7dcb692a3f4b34f6cbd84d46e648e055634e/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.

Expand Down
37 changes: 19 additions & 18 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "c6a8df3d94c8dd4228cbd6e1d85223ebd3f64613b31a534ee34f9d12c4ccee1f",
"CONTRIBUTING.md": "d384c4c6422684eddae5de40a837210dd0a40712caecff478a41d17563e77e7e",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand All @@ -36,15 +36,15 @@
"boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/attach.go": "6a855440fac9acc63be857efef9d76210a4619774728684832dfbb08caf42a30",
"boatstack/capability.go": "fc6bccb0ce4df579b81252ecffeb23c68d62f02ff041bfec980d844baf0885a3",
"boatstack/capability_test.go": "e8322903a843970d7f0317d2629466532cb05c3ffcb44b9423adf4219faa8021",
"boatstack/capture.go": "98a29fedcd7f6a95fadc9bac8c1bd601fee24c17cd64a6a76892e068f040ef66",
"boatstack/capture_test.go": "7072d0c5f9ab0ea9ef493f9e6dcb01cf0de544514c8201e30b8e4e6a4065505a",
"boatstack/capability.go": "270288270ac9689551232e772d1656acbb4c3b2e730c441beb7064b53dd21836",
"boatstack/capability_test.go": "4537db261b48311f6fd50f4bdf7c88b0795407dcb2ea627273d8fbd167c40892",
"boatstack/capture.go": "dc94692728f72626f2818fcb0f76383a19254592299b0afbec848bfbd7e1c734",
"boatstack/capture_test.go": "84a74efde7ae6f9f0900c764f0cea6d84b00c1f71efb1a34f62e6f81d5104f7f",
"boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf",
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
"boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "5c10219591d5b9998361ed83eac88debb4d4c338c835c7502b709ed81cf151b6",
"boatstack/cmd/boatstack-helper/flow.go": "0d41c7a86b49004e897f59551780220841d5941396202c81de97a4d52f593520",
"boatstack/cmd/boatstack-helper/main.go": "903a233fdbe706e9281b9840abda458e78035b8379a99c9c97dd922832430a64",
"boatstack/cmd/boatstack-helper/main.go": "538f56dc211e2332d6d49b1fe6885b8b1fecc3f6a5a86a6b555f01f096973318",
"boatstack/cmd/boatstack-helper/main_test.go": "b36c52d6d5c9dd2428730de10ff18194b7e32a98722e41341c301c6f7a04cad5",
"boatstack/cmd/boatstack-helper/retro.go": "68b83e33ade5b5fec126c70ec798fdcbed22fda755dc1cad2758143fead8e187",
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
Expand Down Expand Up @@ -159,19 +159,19 @@
"boatstack/paths.go": "f9a615f35e0f439d6f11c48e881f11db26c0029e1eb7dd5978941cf51c74e710",
"boatstack/plan.go": "a130c9e587f97a280b6cfa531bcaab46fc6e58772f27921031be03eb98f04439",
"boatstack/plan_test.go": "1b01e7d9d7794eb11c998e19a2f3532d3b8509d984eca934cf5f1662a0a7e573",
"boatstack/plan_validation.go": "99e40806fd579ff72de53f391cd6124acc9ff18707ecf9676c5e507b738d87d0",
"boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa",
"boatstack/plan_validation.go": "48c4fd061257f821ed54a0892b291cc04450e93e3db48f358f8c6599b21f9e41",
"boatstack/plan_validation_test.go": "406c672470e909cb8d54f42175952c581ea872fa21bdcf34675f23187688fe33",
"boatstack/planning.go": "63f2dcfcce85a3c7a39d183b5a557f7085f306ed57b130282e0678209b05c6c8",
"boatstack/planning_first_write_conformance_test.go": "873097aa9384b75bf01e74a475f3ec2ac7cca4a28f733e82f2c82959032c6a30",
"boatstack/planning_test.go": "06ec7022222d926040c3ae28b84ab50c3d2f804ae6473e61b303804dd992d884",
"boatstack/post_publish_prescribe_conformance_test.go": "3c20d359ff84648db7dedb227b4d64e6574d9f41d3cdca0adefec1c60bfbf4ae",
"boatstack/pr.go": "57a9f63fc622dc76bd2382cf2ccb9099b447d37eafadc457e3743217eba7a714",
"boatstack/pr.go": "4c6952a9481d0c5d39f4ef92fef0de026f3a5cf1e4c1cf23525626ede766917f",
"boatstack/pr_phase.go": "59f8cbb75b6b538a5345474acd6a725450979579bf8ecf9591956cbbe1cc4737",
"boatstack/pr_phase_conformance_test.go": "bc9c834e9c4ed43b35d81abafd7b1bf2a264ea2a8c4a4ec9758ee18d1d438968",
"boatstack/pr_test.go": "5c0ff03eb21e383026a4e9fbc5671b2e316040e4e4c55ab6583b930e7e117dda",
"boatstack/provenance.go": "d44dcd5421306269326f1202ba1d52df8c252490550270ef9d022e8ec2b65210",
"boatstack/provision.go": "eb7333a73331b011adc93f59a2d97415d850c2e588f0e2bbb5116984e2ef927d",
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
"boatstack/provision.go": "9531f975f09f61ba3eed120ca7518cdc53b1e0df9f45a4da2bd7f1f8d96984fe",
"boatstack/provision_test.go": "70199eac574cc8843ce12f2bad58b7fea0f86a4205a6d3be0a96594abd967b5d",
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
"boatstack/readiness.go": "30ddf87d650c92e66f4393c6d18b6728b0b38f0831f564159998be2dbe12b708",
Expand All @@ -182,7 +182,7 @@
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
"boatstack/references/artifacts.md": "e362b2663c904beb8daa777c997f9fcb2ed0e70442ed2e7bef3b169895d170f1",
"boatstack/references/config-schema.md": "01a3b7a9269a08a5cd2a86e967940c1e57f78e166db0b7289bff7a98732c8691",
"boatstack/references/config-schema.md": "d295a7ecc5545cffad056ada6b9681ab82545b68ed92a4a8270a775b444f5a18",
"boatstack/references/failure-moves.md": "b65ef72035afa6ad0dce589a0b38f84bc40cde3864c9ecf973f08fc687f001c3",
"boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
Expand Down Expand Up @@ -218,7 +218,7 @@
"boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091",
"boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a",
"boatstack/visual_attach_conformance_test.go": "a376f032cfc59615fe4d403bdb8d7c7d3972dc7de334c3f6763edf1531ff1f03",
"boatstack/visual_evidence.go": "f9fbde0e89f0ce8d9503939197d9b92cc8da377053007371d28360e29715a84a",
"boatstack/visual_evidence.go": "682debd135dea3835c5d5a2a8eb4bff1d3bb814c30990a9ea71cf00bbdae9adb",
"boatstack/visual_evidence_test.go": "b2e50093080d4c2381c8f441a4e1cfeca714fa3d034e7536835b7077fad0556a",
"boatstack/visual_publisher.go": "ec5e95228b48e4ec20731975606048f5e732c4e09a7fd175770d4b15e447cc88",
"boatstack/visual_publisher_test.go": "979f600edae00c77569ae753b80530e8cbf2e3b342995efcd992d351ff382eaf",
Expand All @@ -231,11 +231,11 @@
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "8c5b0a6a8394333165dd0b2b1a70ca74e9d53f784b478e2ef7f1b12d93bc03b0",
"docs/evidence-engineered-coding.md": "a0bc4ce88390774ea8ad188173264024505dc84e554820b3b219944e11cfe870",
"docs/configuration.md": "675cfe8fc5d97bea8d925d47a3c3b852cf6cebbb050d2b4d0266684d2c531d3b",
"docs/evidence-engineered-coding.md": "059d890ee788558964a5ffc2839674194434590ee8360e1b8352c9e94e5a1a8e",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "dd049cd3c8e23362bdd0bfcf2090015432f309e6298ab8b45b67741b780d170a",
"docs/public-claims.json": "6495ef482d4d92fa0f10d84e70f36c1e1c5f57dbd801224c6bc25a76d41138a0",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -249,7 +249,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": "eebacf19b2b399d8cb6bbd3aad96e9e1705e369b69561f8841aeeb06f5d68e03",
"labs/diagram-json/plan.lock.json": "bdf5a145a1d663b2157e1e8afc19ac4829c6476e10206e312e8d5e32695db37e",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -392,14 +392,15 @@
"release-notes/2026-07-28-retromine-recurrence-detector.md": "27790993a02e73f3a2700dce7340d044aeb0e52f785ded68271ff6673add9e25",
"release-notes/2026-07-29-readiness-and-journey-control.md": "2411db76974990ad0128fcd90e795c4c1c5c70d6d6f15fccd452f7e1a242310b",
"release-notes/2026-07-30-auto-capture-on-ship.md": "49aa077ac52005d380ba2cae9e406c7d0a9941157c40aa4ed44b9e59195b3837",
"release-notes/2026-07-30-per-surface-capture-harnesses.md": "6489002d0f95088be0d900b7699800d830ba8a1f8e299251405cefe664c91d5b",
"release-notes/2026-07-30-plan-approved-scenarios-escalate-to-require.md": "21c3f2be51b834fc2eb7662236db2549e80b1a1a0665a0721e773b7736e2c079",
"release-notes/2026-07-30-visual-attach-retry-prescription.md": "38b610685a7a62c0341ba21b273b42c24d82ca803de6d8f694754156eaa606a9",
"release-notes/2026-07-30-visual-evidence-survives-preview-commit.md": "71d19e9fabb40e8cb1e939f26bf288911d227979926f43f337046c6cf6b66551"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a",
"commit": "7bcc7dcb692a3f4b34f6cbd84d46e648e055634e",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
17 changes: 17 additions & 0 deletions boatstack/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,20 @@ func ResolveCapability(name string, config ProjectConfig) (CapabilityResolution,
}
return CapabilityResolution{Name: capability.Name, Kind: "unavailable"}, nil
}

// ResolveCapabilityForSurface resolves a capability command for one product
// surface: the surface-scoped key ("visual:web") outranks the global alias
// ladder, and an empty or unregistered surface falls back to it exactly — a
// repository with only a global command keeps serving every surface.
func ResolveCapabilityForSurface(name, surface string, config ProjectConfig) (CapabilityResolution, error) {
capability, ok := LookupCapability(name)
if !ok {
return CapabilityResolution{}, fmt.Errorf("unknown evidence capability %q", name)
}
if surface = strings.TrimSpace(surface); surface != "" {
if command := strings.TrimSpace(config.Project.Commands[capability.Name+":"+surface]); command != "" {
return CapabilityResolution{Name: capability.Name, Kind: "repository-command", Command: command}, nil
}
}
return ResolveCapability(name, config)
}
37 changes: 37 additions & 0 deletions boatstack/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,40 @@ func TestLookupCapabilityExposesRegisteredMetadata(t *testing.T) {
t.Fatalf("visual capability metadata is incomplete: %#v", capability)
}
}

// Invariant: a surface-scoped command outranks the global alias, and its
// absence falls back to the global ladder exactly — a repository with one
// global harness keeps serving every surface (zero-value behavior).
func TestResolveCapabilityForSurfacePrefersSurfaceScopedCommand(t *testing.T) {
config := testConfig()
delete(config.Project.Commands, "visual")
delete(config.Project.Commands, "screenshot")
delete(config.Project.Commands, "e2e")
config.Project.Commands["visual"] = "npm run capture:visual"
config.Project.Commands["visual:web"] = "npm run capture:web"

resolution, err := ResolveCapabilityForSurface("visual", "web", config)
if err != nil {
t.Fatal(err)
}
if resolution.Command != "npm run capture:web" {
t.Fatalf("surface key did not outrank the global alias: %#v", resolution)
}
for _, surface := range []string{"", "ops"} {
resolution, err = ResolveCapabilityForSurface("visual", surface, config)
if err != nil {
t.Fatal(err)
}
if resolution.Command != "npm run capture:visual" {
t.Fatalf("surface %q did not fall back to the global alias: %#v", surface, resolution)
}
}
delete(config.Project.Commands, "visual")
resolution, err = ResolveCapabilityForSurface("visual", "ops", config)
if err != nil {
t.Fatal(err)
}
if resolution.Kind != "unavailable" {
t.Fatalf("unregistered surface with no global command must be unavailable: %#v", resolution)
}
}
40 changes: 31 additions & 9 deletions boatstack/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (execCaptureRunner) Run(request CaptureRequest) error {
"BOATSTACK_CAPTURE_ENTRY="+request.Scenario.Entry,
"BOATSTACK_CAPTURE_STATE="+request.Scenario.State,
"BOATSTACK_CAPTURE_VIEWPORT="+request.Scenario.Viewport,
"BOATSTACK_CAPTURE_SURFACE="+request.Scenario.Surface,
"BOATSTACK_CAPTURE_OUTPUT="+request.OutputPath,
)
// The harness's authoritative output is the PNG on disk, not stdout; only
Expand Down Expand Up @@ -96,14 +97,6 @@ func CaptureEvidence(options CaptureEvidenceOptions) (PRVisualEvidenceManifest,
if err != nil {
return PRVisualEvidenceManifest{}, fmt.Errorf("capture requires a valid Boatstack project configuration: %w", err)
}
resolution, err := ResolveCapability(name, config)
if err != nil {
return PRVisualEvidenceManifest{}, err
}
if resolution.Kind != "repository-command" {
return PRVisualEvidenceManifest{}, fmt.Errorf("evidence capability %q is unavailable: register a repository command (project.commands) or provision it first", name)
}

relevance, source, scenarios, err := planVisualDecision(repo, feature)
if err != nil {
return PRVisualEvidenceManifest{}, err
Expand All @@ -114,6 +107,13 @@ func CaptureEvidence(options CaptureEvidenceOptions) (PRVisualEvidenceManifest,
if len(scenarios) == 0 {
return PRVisualEvidenceManifest{}, fmt.Errorf("no %s scenarios declared in the plan (pr_visual_evidence.scenarios)", name)
}
// Every scenario's command must resolve before any capture runs — a
// surface-scoped key outranks the global alias; a missing surface key
// with no global fallback is named exactly, never captured around.
commands, err := resolveScenarioCaptureCommands(name, scenarios, config)
if err != nil {
return PRVisualEvidenceManifest{}, err
}

head, err := gitCommand(repo, "rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
Expand Down Expand Up @@ -147,7 +147,7 @@ func CaptureEvidence(options CaptureEvidenceOptions) (PRVisualEvidenceManifest,
items := make([]PRVisualEvidenceItem, 0, len(scenarios))
for _, scenario := range scenarios {
outputPath := filepath.Join(stagingDir, scenario.ID+".png")
if err := captureScenario(repo, capability, resolution.Command, scenario, outputPath, feature, head, headCommit, diffHash, runner); err != nil {
if err := captureScenario(repo, capability, commands[scenario.ID], scenario, outputPath, feature, head, headCommit, diffHash, runner); err != nil {
return PRVisualEvidenceManifest{}, err
}
items = append(items, PRVisualEvidenceItem{
Expand Down Expand Up @@ -182,6 +182,28 @@ func CaptureEvidence(options CaptureEvidenceOptions) (PRVisualEvidenceManifest,
return saved, nil
}

// resolveScenarioCaptureCommands resolves the harness command for every
// scenario up front (surface key first, global alias fallback), so capture
// either runs with a complete command map or fails naming the exact missing
// registration before any harness executes.
func resolveScenarioCaptureCommands(name string, scenarios []PRVisualScenario, config ProjectConfig) (map[string]string, error) {
commands := make(map[string]string, len(scenarios))
for _, scenario := range scenarios {
resolution, err := ResolveCapabilityForSurface(name, scenario.Surface, config)
if err != nil {
return nil, err
}
if resolution.Kind != "repository-command" {
if surface := strings.TrimSpace(scenario.Surface); surface != "" {
return nil, fmt.Errorf("evidence capability %q is unavailable for surface %q: register project.commands[%q] (capability-register --capability %s --surface %s --command <command>) or a global command", name, surface, name+":"+surface, name, surface)
}
return nil, fmt.Errorf("evidence capability %q is unavailable: register a repository command (capability-register --capability %s --command <command>) or provision it first", name, name)
}
commands[scenario.ID] = resolution.Command
}
return commands, nil
}

// captureProductDiff reproduces the pr-context product-diff fingerprint so a
// captured manifest is trusted (PASS) by resolvePRVisualEvidence: same product
// diff. The head commit is recorded for provenance only — trust is keyed to
Expand Down
Loading
Loading