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/60f76062f72185efddad1c22f6c1fc088aff0005/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/550a1c87fa41edfc233c546105d5e4ee921290e7/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
22 changes: 12 additions & 10 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 78586,
"estimated_tokens": 19647,
"characters": 80016,
"estimated_tokens": 20004,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
Expand All @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "2e8bb6baa538f3ea22f5d31d03511eac4ffea83842733ec2f2de731f66baae76",
"CONTRIBUTING.md": "119288a73b70fd9ebfcdd0e15d1089e501cbd292a7349a8a1f8c7bb24642f20f",
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -44,10 +44,11 @@
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
"boatstack/compiled_artifact_resolution_test.go": "0748d67643263e698211eb04d46464e1dd3db15d94537f5fd5092b5aa689745b",
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
"boatstack/delivery.go": "4b4e870335b4b45c1fbfdc5a2daeb65b14c41671bc489f7e75189b9fc2ae6cb9",
"boatstack/delivery.go": "3d1580512c1922ae4ce349a790acfb3356e3c10105af7871457e37fffda39416",
"boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3",
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
Expand Down Expand Up @@ -83,7 +84,7 @@
"boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa",
"boatstack/planning.go": "ef4507a9fecc900f0691372c50883f328c9232c3dfd6986fbde26fb7ef436ae3",
"boatstack/planning_test.go": "c105a9c78c342be06614bf54d0bc1b661b0f7af64d63b79e43bd1fcc2769edd5",
"boatstack/pr.go": "981a59288a0a90534f51a2378656b78bfdc4899810bce5fdeefaa6cde63eeffe",
"boatstack/pr.go": "eae93f7a6e423f67336545d37181ec2ea350991a2d75985f1cf2cf4b7bc8382d",
"boatstack/pr_test.go": "7f82954d94c1ceae848a581dda25e58af92251d78a5a94ed2d672bedf5a0349e",
"boatstack/provenance.go": "d44dcd5421306269326f1202ba1d52df8c252490550270ef9d022e8ec2b65210",
"boatstack/provision.go": "4882d49681f99b11ba9d182ca13772131b7f9a11a6c2b560800654ca14f5111e",
Expand All @@ -97,7 +98,7 @@
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
"boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8",
"boatstack/references/failure-moves.md": "35ac99fdf19eac823313b684b4a8a92990fb42392dc1a94447621d538c637fc7",
"boatstack/references/failure-moves.md": "b65ef72035afa6ad0dce589a0b38f84bc40cde3864c9ecf973f08fc687f001c3",
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
Expand Down Expand Up @@ -135,10 +136,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
"docs/evidence-engineered-coding.md": "8da580e5b910db5255944ba15aa23bea9356b9545e52430962abc265810f871c",
"docs/evidence-engineered-coding.md": "2a6cee818ce3c1eaebca53384237e164aaea0da82d003749565bc022243f06f2",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
"docs/public-claims.json": "8fd1c004d69856c2426263f96ce5c9c25cda37ac3d43f8f39bffec0f0913908c",
"docs/public-claims.json": "2f221186fbb1c6694ac11a247d589e69e3193663b6e0cb98887ab9cfd5dd7093",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -152,7 +153,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": "617523321b82841a59652e680bd163416507c84ddb727fc226ff5727175cef16",
"labs/diagram-json/plan.lock.json": "77d311f5fc945b1e24de638d25f64cf1a5167109f76229207b0a29b46a57a849",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -234,13 +235,14 @@
"release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0",
"release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c",
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
"release-notes/2026-07-25-root-cause-operation.md": "5bf1f082e9123c5a7bcc8bc01b12e97b24b5ae15958577b4ff2a358994fca891"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "60f76062f72185efddad1c22f6c1fc088aff0005",
"commit": "550a1c87fa41edfc233c546105d5e4ee921290e7",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
125 changes: 125 additions & 0 deletions boatstack/compiled_artifact_resolution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package boatstack

import (
"os"
"path/filepath"
"strings"
"testing"
)

// Regression for the evidence-path split-brain: activate-plan writes the evidence
// ledger under compiled/, and pr-context resolves it through the shared dual-layout
// rule (feature-root canonical, compiled/ fallback). But the delivery-gate recorder
// used to hand-join the feature-root path with no fallback, so once a real project
// kept its ledger only at compiled/evidence.md the recorder could not find it and
// the gate failed with "delivery gate requires current evidence". These tests pin
// the recorder to the same resolver every other layer uses.

// TestFeatureEvidencePathResolvesBothLayouts locks the shared resolver's ordering:
// feature-root (legacy canonical) first, compiled/ as the current-layout fallback,
// and the canonical path returned even when neither exists so the caller reports a
// clear error location.
func TestFeatureEvidencePathResolvesBothLayouts(t *testing.T) {
dir := t.TempDir()
root := filepath.Join(dir, "evidence.md")
compiled := filepath.Join(dir, "compiled", "evidence.md")

// With neither present the resolver returns its last candidate (the compiled
// copy) so a missing-evidence error names the canonical write location.
if got := featureEvidencePath(dir); got != compiled {
t.Fatalf("with neither present, want the compiled error path %q, got %q", compiled, got)
}

if err := os.MkdirAll(filepath.Dir(compiled), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(compiled, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if got := featureEvidencePath(dir); got != compiled {
t.Fatalf("with only compiled present, want %q, got %q", compiled, got)
}

if err := os.WriteFile(root, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if got := featureEvidencePath(dir); got != root {
t.Fatalf("with both present, legacy root must win to match pr-context, got %q", got)
}
}

// TestRecordGateResolvesCompiledEvidenceLedger is the proof of fix: a delivery whose
// ledger lives ONLY at compiled/evidence.md (the taxweave state) must gate cleanly
// with no explicit --evidence. This fails on the pre-fix recorder, which resolved
// only the feature root.
func TestRecordGateResolvesCompiledEvidenceLedger(t *testing.T) {
repo, feature := activateTwoSliceDelivery(t)
dir := filepath.Join(repo, ".product-loop", "features", feature)

// Move the edited gate ledger to the compiled layout and drop the feature-root
// copy, so only compiled/evidence.md carries the gate outcomes.
ledger := "# Evidence ledger\n\n- Test gate (phase-one): `PASS`\n- Review gate (phase-one): `PASS`\n- Test gate (phase-two): `BLOCKED`\n- Review gate (phase-two): `BLOCKED`\n"
if err := os.WriteFile(filepath.Join(dir, "compiled", "evidence.md"), []byte(ledger), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(dir, "evidence.md")); err != nil {
t.Fatal(err)
}
runGit(t, repo, "add", "-A")
runGit(t, repo, "commit", "-m", "keep the evidence ledger only in the compiled layout")

receipt, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: "test", Status: "PASS"})
if err != nil {
t.Fatalf("recorder could not resolve compiled-only evidence ledger: %v", err)
}
if !strings.HasSuffix(filepath.ToSlash(receipt.EvidencePath), "compiled/evidence.md") {
t.Fatalf("recorder bound the wrong evidence path: %q", receipt.EvidencePath)
}
}

// TestRecordGateStillResolvesFeatureRootEvidence guards the legacy layout: a ledger
// at the feature root (no compiled copy) must still gate, so the added fallback is
// strictly additive.
func TestRecordGateStillResolvesFeatureRootEvidence(t *testing.T) {
repo, feature := activateTwoSliceDelivery(t)
dir := filepath.Join(repo, ".product-loop", "features", feature)

// activateTwoSliceDelivery already writes the ledger at the feature root; remove
// the compiled copy so only the legacy location remains.
if err := os.Remove(filepath.Join(dir, "compiled", "evidence.md")); err != nil {
t.Fatal(err)
}
runGit(t, repo, "add", "-A")
runGit(t, repo, "commit", "-m", "keep the evidence ledger only at the feature root")

receipt, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: "test", Status: "PASS"})
if err != nil {
t.Fatalf("recorder could not resolve feature-root evidence ledger: %v", err)
}
if strings.Contains(filepath.ToSlash(receipt.EvidencePath), "compiled/") {
t.Fatalf("recorder ignored the legacy feature-root ledger: %q", receipt.EvidencePath)
}
}

// TestRecorderAndPRContextResolveSameEvidence is the anti-drift invariant: the
// recorder's default evidence resolution and pr-context both route through
// featureEvidencePath, so for any given feature they can never resolve different
// evidence files. This is the structural guarantee the layers cannot re-diverge —
// the compiled-artifact analogue of the published-slice addressability invariant.
func TestRecorderAndPRContextResolveSameEvidence(t *testing.T) {
dir := t.TempDir()
compiled := filepath.Join(dir, "compiled", "evidence.md")
if err := os.MkdirAll(filepath.Dir(compiled), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(compiled, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}

// The recorder's default (empty --evidence) and pr-context's managedPRSources
// both call featureEvidencePath(dir); assert that single resolver returns the
// file that actually exists rather than a hand-joined feature-root guess.
if got := featureEvidencePath(dir); got != compiled {
t.Fatalf("shared resolver must return the existing ledger both consumers read, got %q", got)
}
}
2 changes: 1 addition & 1 deletion boatstack/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error
}
evidencePath := strings.TrimSpace(options.EvidencePath)
if evidencePath == "" {
evidencePath = filepath.Join(repo, ".product-loop", "features", options.Feature, "evidence.md")
evidencePath = featureEvidencePath(filepath.Join(repo, ".product-loop", "features", options.Feature))
} else if !filepath.IsAbs(evidencePath) {
evidencePath = filepath.Join(repo, evidencePath)
}
Expand Down
13 changes: 12 additions & 1 deletion boatstack/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,17 @@ func featureArtifactPath(directory string, candidates ...string) string {
return last
}

// featureEvidencePath resolves a feature's evidence ledger through the one shared
// dual-layout rule: the feature-root copy (canonical for legacy features) first,
// the compiled/ copy as the current-layout fallback. The delivery-gate recorder
// and pr-context MUST both call this so they can never resolve different evidence
// files for the same feature — the compiled-artifact analogue of the per-slice
// addressability resolver that closed the published-slice split-brain. Hand-joining
// a single fixed path (as the recorder once did) is exactly the drift this prevents.
func featureEvidencePath(featureDir string) string {
return featureArtifactPath(featureDir, "evidence.md", filepath.Join("compiled", "evidence.md"))
}

func managedPRSources(repo, feature string) ([]PRSource, map[string]string, error) {
directory := filepath.Join(repo, ".product-loop", "features", feature)
planPath := filepath.Join(directory, "plan.md")
Expand Down Expand Up @@ -441,7 +452,7 @@ func managedPRSources(repo, feature string) ([]PRSource, map[string]string, erro
}); err != nil {
return nil, nil, fmt.Errorf("managed PR requires a current build lock: %w", err)
}
evidencePath := featureArtifactPath(directory, "evidence.md", filepath.Join("compiled", "evidence.md"))
evidencePath := featureEvidencePath(directory)
if err := checkNonEmptyFile(evidencePath, "feature evidence"); err != nil {
return nil, nil, err
}
Expand Down
Loading
Loading