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/2da3d0b8f8ca7baec350a8b803e5409721517e52/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/5f4dda1585d8032019dd0ed2c3b8ea264abfcf4d/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
25 changes: 17 additions & 8 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "715cf4044f2af36764254efffd0425135e300ce8d57c03d19eb089a7041cb1fd",
"CONTRIBUTING.md": "ae6641051824b5bbc7588d7d70a2657f091e0579d5db4616a8aed5b4f18e1d69",
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand All @@ -34,9 +34,13 @@
"boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4",
"boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/capability.go": "9d9a75086e88bb1d9d4170821436c686002fe3a125e6ee7d23eea5779aac5cfe",
"boatstack/capability_test.go": "e8322903a843970d7f0317d2629466532cb05c3ffcb44b9423adf4219faa8021",
"boatstack/capture.go": "e32dd7096ccc4844d37c9ec0aea8284adee58bc84cd5b6264516b6661b348bfc",
"boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105",
"boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec",
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
"boatstack/cmd/boatstack-helper/main.go": "43f070dc71f924ea6466b4190c43811d7884cece7d9d44df433c9beeb6982053",
"boatstack/cmd/boatstack-helper/main.go": "5aff755d60b2f5c06dab3ed5d30df026d28c6fb90d891b3be38e955aa200621d",
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/command.go": "94d2117c6e390d5a644afc5cd90f7e712e3f8b1032f8c9e3b253cc134524c28a",
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
Expand All @@ -46,7 +50,7 @@
"boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48",
"boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f",
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
"boatstack/export.go": "abccdfc04b7b49a17e8531c0332f80262e693273f7974b646528fe00af1d6ae6",
"boatstack/export.go": "388c676e4fb25442a15e51e31bfc5931b6264d7a95a3422ecec0c1f78ab613d9",
"boatstack/export_test.go": "67eb890728d20630925d6e4e90d2a97ec025195ba5c54994c1b098ab72721dca",
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
Expand All @@ -72,6 +76,8 @@
"boatstack/planning_test.go": "c105a9c78c342be06614bf54d0bc1b661b0f7af64d63b79e43bd1fcc2769edd5",
"boatstack/pr.go": "bdb066acb329b6b772cb880db2e590b381cae909f270085cfacb8ef2fbfda651",
"boatstack/pr_test.go": "7f82954d94c1ceae848a581dda25e58af92251d78a5a94ed2d672bedf5a0349e",
"boatstack/provision.go": "4882d49681f99b11ba9d182ca13772131b7f9a11a6c2b560800654ca14f5111e",
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
"boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
Expand Down Expand Up @@ -99,7 +105,7 @@
"boatstack/update_publication.go": "c8b7bd38019b1cbf8c523652e9e20e8c971c7e48b2f3972a0ac638648326fe63",
"boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091",
"boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a",
"boatstack/visual_evidence.go": "90a68d554e10ff4fb7afa45000912cdedd4cdf93b3d279055b50844401924f01",
"boatstack/visual_evidence.go": "4d69f98adb6087d4830e6703273ae6e03b28ec8b3c496a6e432fd5e0e54d8a2f",
"boatstack/visual_evidence_test.go": "0fe8f5154ef4398dfeba5e7f7b387b2d279ed75635ea35d93ff392269f2cc6d0",
"boatstack/workspace.go": "91b343400b3506a6f516c28fabc3f1575f22024a5b19f934a020be660a20482e",
"boatstack/workspace_sync.go": "0cc2f03fd1aa57c66b3d603d5ef30aa820f14ab60771a795cf10c46f3c9a71b8",
Expand All @@ -109,10 +115,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1",
"docs/evidence-engineered-coding.md": "5326393482e5f97979f89c5b5714c9b8dda124cc16d398a962c9e5cf4fc8a528",
"docs/evidence-engineered-coding.md": "58205ac01839ab8c29d76090e2744afa827a623d7be6de6b6e468f7b3730c047",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
"docs/public-claims.json": "650290515c86557a4316261f2313a3ddf07121e12ad76eca9072a4d696c1acaf",
"docs/public-claims.json": "abbf9221262970d74e60e5cd54f69b1dad93ce453fa75e9516f86ee4ef12b2f8",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -126,7 +132,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": "e416845baca59bf23f91f8417d074b63c96dfe65293a481039e24428c8d61fb4",
"labs/diagram-json/plan.lock.json": "40ec1d86a0c3e7186546f6f26bc4023cbea0020b04553537515b3a85d5ae37fe",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -186,6 +192,9 @@
"release-notes/2026-07-22-value-translation-boundary.md": "9cf168ff7caaf3906b78533935bdfb2c86e753984ed1e5ec8204cb373d083390",
"release-notes/2026-07-23-bootstrap-safe-update-repair.md": "d8e66c46ae05e3d228dfea45879f4d1166d5e3a253ac24bababd4c3e396c214b",
"release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c",
"release-notes/2026-07-23-capability-provisioning.md": "86fefcc4212212b0522b2a9b732663ee98cc41cec96b9842f19bb4433277a6a4",
"release-notes/2026-07-23-capture-evidence-orchestration.md": "13e0dfa65898a4ba63a7866fc1db4d32df2f5610db0e8301734d47404f0e3da9",
"release-notes/2026-07-23-evidence-capability-substrate.md": "a3e6e364536f826f299243932494096c27458e2621a5ce55465c5db280b53758",
"release-notes/2026-07-23-explicit-source-plan.md": "ec1f97434f9263f6db4bc3b83ae83213053cd2bc6b7465e4fb2d67cbea5cf731",
"release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4",
"release-notes/2026-07-23-labkit-publishing-pipeline.md": "b3bfc5f28baf3961fb187380ff66e5383186c05a933bc6ca162416ce61149218",
Expand All @@ -197,7 +206,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "2da3d0b8f8ca7baec350a8b803e5409721517e52",
"commit": "5f4dda1585d8032019dd0ed2c3b8ea264abfcf4d",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
70 changes: 70 additions & 0 deletions boatstack/capability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package boatstack

import (
"fmt"
"strings"
)

// Capability describes a named producer of PR evidence. It is the generic spine
// that concrete evidence types register on: detection ("can this repository
// produce the evidence?"), capture orchestration ("what command runs it, and
// when?"), and provisioning ("if it is unavailable, how do we help?") all read
// this registry instead of hard-coding a single evidence type.
//
// Boatstack ships the contract in this registry, not the capture harness. The
// harness is authored in the user's repository and invoked through the resolved
// repository command; Boatstack only records what the command must satisfy.
type Capability struct {
// Name is the canonical capability identifier, e.g. "visual".
Name string
// CommandAliases are the project.commands keys that satisfy the capability,
// in priority order. The first non-empty command wins.
CommandAliases []string
// AdmittedStages are the delivery stages in which a capture attempt may run.
AdmittedStages []string
// RetryClass is the operation retry class recorded for a capture attempt.
RetryClass string
}

// CapabilityResolution is the portable capability cut for one capability: it
// reports whether the repository owns a command that produces this evidence.
type CapabilityResolution struct {
Name string `json:"name"`
Kind string `json:"kind"` // repository-command | unavailable
Command string `json:"command,omitempty"`
}

// capabilityRegistry holds every evidence capability Boatstack knows about.
// Adding a provider is a single entry here plus its tenant-specific manifest
// contract (see visual_evidence.go for the first tenant).
var capabilityRegistry = map[string]Capability{
"visual": {
Name: "visual",
CommandAliases: []string{"visual", "screenshot", "e2e"},
AdmittedStages: []string{"BUILD", "TEST_PASSED"},
RetryClass: "IDEMPOTENT_EXTERNAL",
},
}

// LookupCapability returns the registered capability metadata for a name.
func LookupCapability(name string) (Capability, bool) {
capability, ok := capabilityRegistry[strings.TrimSpace(name)]
return capability, ok
}

// ResolveCapability performs the repository-owned capability cut: it selects the
// first project command alias that is set. It is the generic detection primitive
// shared by capture orchestration and provisioning. Kind is "repository-command"
// when the repository owns a command, otherwise "unavailable".
func ResolveCapability(name string, config ProjectConfig) (CapabilityResolution, error) {
capability, ok := LookupCapability(name)
if !ok {
return CapabilityResolution{}, fmt.Errorf("unknown evidence capability %q", name)
}
for _, alias := range capability.CommandAliases {
if command := strings.TrimSpace(config.Project.Commands[alias]); command != "" {
return CapabilityResolution{Name: capability.Name, Kind: "repository-command", Command: command}, nil
}
}
return CapabilityResolution{Name: capability.Name, Kind: "unavailable"}, nil
}
61 changes: 61 additions & 0 deletions boatstack/capability_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package boatstack

import "testing"

func TestResolveCapabilitySelectsRepositoryCommandByAliasPriority(t *testing.T) {
config := testConfig()
delete(config.Project.Commands, "visual")
delete(config.Project.Commands, "screenshot")
delete(config.Project.Commands, "e2e")

// Lowest-priority alias still resolves the repository-owned cut.
config.Project.Commands["e2e"] = "npm run e2e"
resolution, err := ResolveCapability("visual", config)
if err != nil {
t.Fatalf("resolve visual: %v", err)
}
if resolution.Kind != "repository-command" || resolution.Command != "npm run e2e" || resolution.Name != "visual" {
t.Fatalf("alias fallback did not resolve: %#v", resolution)
}

// A higher-priority alias wins over a lower one.
config.Project.Commands["visual"] = "npm run capture:visual"
resolution, err = ResolveCapability("visual", config)
if err != nil {
t.Fatalf("resolve visual: %v", err)
}
if resolution.Command != "npm run capture:visual" {
t.Fatalf("higher-priority alias did not win: %#v", resolution)
}
}

func TestResolveCapabilityReportsUnavailableWithoutCommand(t *testing.T) {
config := testConfig()
delete(config.Project.Commands, "visual")
delete(config.Project.Commands, "screenshot")
delete(config.Project.Commands, "e2e")

resolution, err := ResolveCapability("visual", config)
if err != nil {
t.Fatalf("resolve visual: %v", err)
}
if resolution.Kind != "unavailable" || resolution.Command != "" {
t.Fatalf("expected unavailable resolution: %#v", resolution)
}
}

func TestResolveCapabilityRejectsUnknownCapability(t *testing.T) {
if _, err := ResolveCapability("does-not-exist", testConfig()); err == nil {
t.Fatal("expected error for unknown capability")
}
}

func TestLookupCapabilityExposesRegisteredMetadata(t *testing.T) {
capability, ok := LookupCapability("visual")
if !ok {
t.Fatal("visual capability is not registered")
}
if len(capability.CommandAliases) == 0 || capability.RetryClass == "" || len(capability.AdmittedStages) == 0 {
t.Fatalf("visual capability metadata is incomplete: %#v", capability)
}
}
Loading
Loading