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/76a1339c3bfb8c7a75cc3e3f7a408cf8736b5e75/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/666b0631c31ee9509a4b3cb0f1242fe865a97c24/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
24 changes: 13 additions & 11 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 73472,
"estimated_tokens": 18368,
"characters": 76152,
"estimated_tokens": 19038,
"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": "0a0005c09ebc1aa11103648517f7a14cdc4a19f4de07ec8fb121c1597477ff9d",
"CONTRIBUTING.md": "e937fe6f4cedacad98619d49538da76a0543f95990b2d51db271ffd03bcc1280",
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -47,7 +47,8 @@
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
"boatstack/delivery.go": "d9c8fdaa8cdc94a885e7d08c872dc2c4c851fa1e29b0db7cb2e8e0893cd36380",
"boatstack/delivery.go": "f7c780d9860e0d5456ecfad5f61242337b2ba1068fceec9eda7232f24374c03c",
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
"boatstack/export.go": "9d2b83b6075b3715599a3c19afb3a7ec8d2a006f8af624e3807f3b1fe7620065",
Expand All @@ -74,7 +75,7 @@
"boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173",
"boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467",
"boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a",
"boatstack/plan.go": "ddb5148113454502a57c45a7dc2e514b9d6c191b5035bbee10f15e2fe5e06ddf",
"boatstack/plan.go": "7209de3a97b134cd6e5224cf6e798b5af47b0a4163ae2f81a8ec0c1ff84031d6",
"boatstack/plan_test.go": "53477515165a910910b9175bfa33574548cf0d0f3be48e175ec3a775a12d30bb",
"boatstack/plan_validation.go": "412f06750832fe46f01190ea5e475fc6f6ea59c8ba78131f94ec031053a405d2",
"boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa",
Expand All @@ -93,11 +94,11 @@
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
"boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8",
"boatstack/references/failure-moves.md": "e1cdba05cb49817d8246eee45ab4a1ba4d691cd0ef3bd6325ee1d795ffa00b0e",
"boatstack/references/failure-moves.md": "fe4c867b06b0913cec202631a0a8beced52394d356bfd37b5fa43a9c977ebae2",
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
"boatstack/references/workflow.md": "e14a324f91b8c6012956168ff838cc6f64ae4cde61666d086174a2a4525dc683",
"boatstack/references/workflow.md": "fad616acd737818d0125655cd5d81d52dc39aff6242a6925c1a43dc38658b36e",
"boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76",
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739",
Expand Down Expand Up @@ -131,10 +132,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
"docs/evidence-engineered-coding.md": "473afd9bd7f52f901d3046844279694d5259bfeaf708d5cf9eb859a1448f98e1",
"docs/evidence-engineered-coding.md": "65056f8f744b7ef5693bad2c3fd8c7f3ebff3989098ee074ede8ebc250bc1373",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
"docs/public-claims.json": "e2758cfc1f3a089212a5fcdbb75db503c84833c7ea509f4c50a2a50c88939335",
"docs/public-claims.json": "a5923a6b53d922b3d515e64ce91d03ea4c251c7fcb00ef39be6ac0d056b11018",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -148,7 +149,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": "3fbb0fbd8cead40c6d48e57ed88552a2ce2ef82b85c5983a4acf1d04f577947e",
"labs/diagram-json/plan.lock.json": "bb709ce18133d8d49a8b96249bb2e2d534d37f004b01df9027a6c3dd070dcd6a",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -225,13 +226,14 @@
"release-notes/2026-07-24-ignored-deliveries-publication-authority.md": "a25f8469316276101490c79a57c1a236c18072dfbb23682bb1778871d247067d",
"release-notes/2026-07-24-provenance-verified-binary-install.md": "b64f3a8dbd750afb28bf6964489eb0f5f8519efa65fcee871db64b9e60c2fdb2",
"release-notes/2026-07-24-publication-nonblocking-control.md": "2b9d8ea817896783273a843ec183fbf00bb2f7ac420b4ce3409aeda7f59b7fb5",
"release-notes/2026-07-24-reactivation-preserves-published-progress.md": "77233df3d6955e8f7a076c301ce7cbff84ba138ab812f18ddd97785600fd3d22",
"release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0",
"release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "76a1339c3bfb8c7a75cc3e3f7a408cf8736b5e75",
"commit": "666b0631c31ee9509a4b3cb0f1242fe865a97c24",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
129 changes: 119 additions & 10 deletions boatstack/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,28 +334,137 @@ func initializeDeliveryState(repo, feature, planPath, lockPath string) error {
if err != nil {
return err
}
previousLocks := []string{}
repairAttempt := 0
if existing, loadErr := LoadDeliveryState(repo, feature); loadErr == nil {
// Re-activating the exact same lock is a no-op: never disturb progress.
if existing.PlanLockHash == lockHash {
return nil
}
if existing.ActiveIndex >= len(existing.Slices) {
return fmt.Errorf("published delivery %s is immutable; activate the correction under a new feature id with parent_delivery=%s", feature, feature)
// A plan amendment mid-delivery must preserve every already-published
// slice. deliveryDefinitions freshly recomputes ALL slices from the new
// plan, so a naive re-initialize would reset ActiveIndex to 0 and strand
// slices whose PR is already open or merged. Reconcile instead: keep the
// published prefix (and its BUILD pointer) and adopt the amended
// definitions only for the not-yet-published tail.
if err := validateAmendmentPreservesProgress(existing, slices); err != nil {
return err
}
previousLocks = append(previousLocks, existing.PreviousPlanLocks...)
if existing.PlanLockHash != "" {
previousLocks = append(previousLocks, existing.PlanLockHash)
}
repairAttempt = existing.RepairAttempt
return saveDeliveryState(repo, reconcileAmendedDeliveryState(existing, slices, lockHash))
}
return saveDeliveryState(repo, DeliveryState{
SchemaVersion: deliveryStateSchemaVersion, Feature: feature, PlanLockHash: lockHash,
PreviousPlanLocks: previousLocks, ActiveIndex: 0, Slices: slices, Mode: "NORMAL", RepairAttempt: repairAttempt,
ActiveIndex: 0, Slices: slices, Mode: "NORMAL",
ParentDelivery: strings.TrimSpace(stringValue(plan["parent_delivery"])),
})
}

// guardReactivationPreservesProgress lets ActivatePlan reject a
// progress-destroying amendment before it promotes any artifact. It is a no-op
// when no managed delivery exists yet (first activation) or when the amendment
// only touches the not-yet-published tail. An idempotent same-plan re-activation
// never reaches this guard: ActivatePlan short-circuits on the matching lock.
func guardReactivationPreservesProgress(repo, feature, planPath string) error {
existing, err := LoadDeliveryState(repo, feature)
if err != nil {
return nil
}
plan, err := LoadPlan(planPath)
if err != nil {
return err
}
newSlices, err := deliveryDefinitions(plan)
if err != nil {
return err
}
return validateAmendmentPreservesProgress(existing, newSlices)
}

// equalStrings reports slice equality treating nil and empty as the same, so a
// definition round-tripped through JSON (where an empty list may deserialize as
// nil) compares equal to a freshly recomputed one.
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

// deliveryDefinitionMatches reports whether two slices carry the same task
// composition and scope. Runtime fields (Status, PRURL, PRState, branches) are
// intentionally ignored: an amendment may not alter what an already-published
// slice built, but PR/branch bookkeeping is delivery state, not definition.
func deliveryDefinitionMatches(a, b DeliverySlice) bool {
return a.ID == b.ID &&
equalStrings(a.TaskIDs, b.TaskIDs) &&
equalStrings(a.AffectedPaths, b.AffectedPaths) &&
equalStrings(a.AcceptanceCriteria, b.AcceptanceCriteria)
}

// validateAmendmentPreservesProgress refuses a re-activation that would alter,
// drop, or reorder any already-published delivery slice. Slices in
// [0, ActiveIndex) have shipped — their branch, PR, and gate receipts are bound
// to the definition that shipped — so a change there must go through a corrective
// child delivery, never an in-place reset. The not-yet-published tail
// [ActiveIndex, len) is freely recomputable, so amending it (e.g. widening a
// building slice's affected_paths) is allowed.
func validateAmendmentPreservesProgress(existing DeliveryState, newSlices []DeliverySlice) error {
if existing.ActiveIndex >= len(existing.Slices) {
return fmt.Errorf("published delivery %s is immutable; activate the correction under a new feature id with parent_delivery=%s", existing.Feature, existing.Feature)
}
for i := 0; i < existing.ActiveIndex; i++ {
old := existing.Slices[i]
if i >= len(newSlices) {
return fmt.Errorf("amendment drops published delivery slice %s; draft a corrective child delivery instead of resetting delivery progress", old.ID)
}
if newSlices[i].ID != old.ID {
return fmt.Errorf("amendment reorders or renames published delivery slice %s (now %s at position %d); draft a corrective child delivery instead of resetting delivery progress", old.ID, newSlices[i].ID, i)
}
if !deliveryDefinitionMatches(old, newSlices[i]) {
return fmt.Errorf("amendment changes published delivery slice %s, whose pull request is bound to what shipped; draft a corrective child delivery instead of re-activating it in place", old.ID)
}
}
return nil
}

// reconcileAmendedDeliveryState preserves the published prefix and its BUILD
// pointer while adopting the amended plan's definitions for the not-yet-published
// tail. The pointer never moves backward and shipped slices keep their PR,
// branches, and status; only the active slice onward is recomputed (its prior
// gate receipts are already invalidated by the new plan lock, so it correctly
// restarts at BUILD). Callers MUST have passed validateAmendmentPreservesProgress
// first.
func reconcileAmendedDeliveryState(existing DeliveryState, newSlices []DeliverySlice, lockHash string) DeliveryState {
merged := make([]DeliverySlice, 0, len(newSlices))
merged = append(merged, existing.Slices[:existing.ActiveIndex]...)
for i := existing.ActiveIndex; i < len(newSlices); i++ {
slice := newSlices[i]
if i == existing.ActiveIndex {
slice.Status = "BUILD"
} else {
slice.Status = "PENDING"
}
merged = append(merged, slice)
}
previousLocks := append([]string{}, existing.PreviousPlanLocks...)
if existing.PlanLockHash != "" {
previousLocks = append(previousLocks, existing.PlanLockHash)
}
return DeliveryState{
SchemaVersion: deliveryStateSchemaVersion,
Feature: existing.Feature,
PlanLockHash: lockHash,
PreviousPlanLocks: previousLocks,
ActiveIndex: existing.ActiveIndex,
Slices: merged,
Mode: "NORMAL",
ParentDelivery: existing.ParentDelivery,
}
}

func archiveDeliveryReceipt(repo, feature, sliceID, gate, observationID string) (string, error) {
path, err := deliveryReceiptPath(repo, feature, sliceID, gate)
if err != nil {
Expand Down
Loading
Loading