diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1d32c4..e4af802 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/9668feb5b9ba3b816d470dde2cc87c70811a7f9a/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/697ce2f7d98eb3bfb249b33d3556efbd7e9365b1/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. diff --git a/UPSTREAM.json b/UPSTREAM.json index e3cf36b..b5e2806 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "d79729327266363a2fbc47cca778245c619d874a74c7c4ae7ff6ecee1f9ba6b2", + "CONTRIBUTING.md": "ca94e9856aab321b125ddeb9f9845c9a26f957585ba46e5547525875b34aa64b", "README.md": "d52c9be1a91165e3bb22318e3014278eb5326d10a45da6311f677d704e5c086d", "assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea", "assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4", @@ -32,21 +32,22 @@ "boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9", "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", "boatstack/cmd/boatstack-helper/main.go": "dbb8cb4bb403aa36a47bd9317e9a34845cabfa5b8e4152a7fb57ded1591d8957", - "boatstack/delivery.go": "bfdce7dd3bc1357a614bd458f2f6b4b8570015c117d1f7638c7a1bf3110e1a48", + "boatstack/delivery.go": "d092ac67116d651938ed1e2912bb845c3b273d650cf88049304888be9683c59c", "boatstack/delivery_test.go": "a8a5a7e6e8dcfee1538367d49c76c531e04211876c1685265884cff26ae04497", - "boatstack/export.go": "5a2002ba99b1b0c22f503f378072902393edb9d70dba06a090fe25e5ac734612", - "boatstack/export_test.go": "1cbed87e24f1b81d20fbdd64eaef0b5b713abda06518b144d5aed5726247d183", + "boatstack/export.go": "3f086d8615769f857ba704678fea78ec63bdf28c15acd4a7bf0c1414f132e285", + "boatstack/export_test.go": "87fc1aef65f3e437d3ca9660e31b9fa77bbb6d80f9b131adff1dbd422a71fdfe", "boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d", - "boatstack/hooks.go": "718161de45165e450369577e5ef1e9fdc76a484d61fa56a36eb6a117b2e63249", + "boatstack/hooks.go": "1d5d8c4bf7e6e867c8bf07e391d86158347269f856647a5d256f345bbb8d3c96", "boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3", - "boatstack/init.go": "a9740a401bf4b0b5a700b02fe8f15d80757ed2c697fa6ebce38d3c03f6c7ba75", - "boatstack/init_test.go": "ea5898df01d4fa5e039749df83f9190fd8fe95b2fe9b7e2526e0d0ecba8a7f03", + "boatstack/init.go": "3bc5b94f662c855f32ccf7db021d4783bad75e3c8c2ed7b384e3a9c60ccd4096", + "boatstack/init_test.go": "090cdde8541459330774e6b7ba17f6a0fa2c44e3a4dc7151dccb5d53b535a482", + "boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", - "boatstack/plan.go": "d83037a9f06f927427a59ea936bbee4041ea9276dd84992599c16ef6259b2d4f", + "boatstack/plan.go": "6a8a6bf3352749516b65748c413c5d14d3499897570902aff707ba3de05bfc42", "boatstack/plan_test.go": "006cdc6681f77e579c5a0f709e30ede759c337132d4f2f5193b7b79b29bd7149", "boatstack/planning.go": "3a26417a295e5dfc2b6dcac702287c04b6053e7b74215858a4ea11cf9f9dadfe", "boatstack/planning_test.go": "6b156a64182ed76d4c3d392b4c5a26abe5d8b81cea27ee12ac7c4627c827e186", - "boatstack/pr.go": "2af70c81108288cd9cd319f7100d1abd957e7e84f8db1474afa51a8b74e9d442", + "boatstack/pr.go": "2e0ea90a991fe4504e54972506cb306f48124c8b34be8577d96f6c97079bdf0c", "boatstack/pr_test.go": "74afb1a9be3c4a95a426515be415149ae24b077454b486d8e69b7118c42f2916", "boatstack/references/artifacts.md": "3a87b8f8d835329191bedb20c94e6056fe2251b3e9d510656909eb9d276069dc", "boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49", @@ -55,23 +56,23 @@ "boatstack/references/workflow.md": "6a7d146c2ad072c513324ad058d4b6c38ea967f2b9c199e055cfc528f4b826ad", "boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", - "boatstack/runtime.go": "b988d57ec14e15fc6a57949a995879fc0e0d6bfa9a7b62935e7754df0b85d87a", - "boatstack/runtime_cache.go": "ab0fbb7f8a2eb8d8428e928fbc3d8866c84104e7cb330a901c121dfec82cddb3", + "boatstack/runtime.go": "f393745950e8ba2da7e25d5539ad536a6239a10c17109224bad20cf48445c380", + "boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489", "boatstack/runtime_cache_test.go": "4cbca9dec7800d7df6e3ec0d74c7ecbe1508e5c5a288d863f35fc8d22986c308", - "boatstack/safety.go": "4b4ac600f0ef3c3800904e06834bd39f3887b822244b4a1fd5af7330ae91887c", + "boatstack/safety.go": "8bcce4c11094b4018093ac5fb5a5256cc5c63c825ddf7ec1a46b9d0098557d33", "boatstack/safety_test.go": "03885d5a93f42b6adba6eef6bbef0680d81c0e1442fb72626a105da4862aecdf", "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", "boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1", - "boatstack/update.go": "4538ae8138cd8d9e25261a849e7b4a4a543c22ef18c43e21d502597ebfc250b5", + "boatstack/update.go": "d538c3f44aa869a2c7c20b9552f51fadbe4ec82ebf40f7130bc90ed3b04690f3", "boatstack/update_test.go": "aa0c2ca97038aad661c46321600034e206e88639f89e0216b3c4cf597312cbae", "docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3", "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", - "docs/evidence-engineered-coding.md": "027e20dc9faad84dfa533f365514ad70195cad945bf46ca17a67e3d2dcbaedbb", + "docs/evidence-engineered-coding.md": "8c79f43e67c6fa9dbd4f5eb84595ef7846b14a35d21c6be1403182f20551ab4c", "docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c", "docs/getting-started.md": "4ef77719a01a2dbe98c5154d58fc03f51797ae0ef39a09d8804fe0708b8cc920", - "docs/public-claims.json": "915db0f39b14208ea083f9567dcedc5d19b973577c5c448136d6ca7baee94623", + "docs/public-claims.json": "0bcb69c56b2e6edd97d21b5b6d0f3ac3eb8554c46e9ea2324b46166c1814f180", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -85,7 +86,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": "d3b39300d7dba9b5b863826b7dc14b582963daa123fa8dda605a353043d2ad7b", + "labs/diagram-json/plan.lock.json": "de95c999eb36de930d3a0fcca0d5751c1087ae49bb26b8347ec90432291457d6", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -95,6 +96,7 @@ "release-notes/2026-07-17-delivery-harness-framing.md": "6fccbe7efdb288f5eca7e824ee5e44ad6b30a34f312eb997dca6aa44c55b26cb", "release-notes/2026-07-17-phase-scoped-delivery.md": "bfc8edd30a67daf5940ad4ceceebf81d01d62914ebeabb7073985c53f13df2ff", "release-notes/2026-07-17-visible-release-messages.md": "c93e8c812528a983502263e35d66c86a265d6ccba8203f393788c069b3fa6606", + "release-notes/2026-07-18-atomic-initialization.md": "86cbf490778f0e0b8e7b5088b47e8168de0782624e282ed05bd54d12d2eb19f0", "release-notes/2026-07-18-automated-releases.md": "6571eec442a27bd1a55667567f0659993c516171ef10ae71046cce80f7fd29fa", "release-notes/2026-07-18-base-aware-release-preflight.md": "cdface46ccd959a5299de4c363c9e4057e7257820dab77dcf0e587daef5d3d99", "release-notes/2026-07-18-claude-command-discovery.md": "bb42c23041916dee21e502449143620a8dcbd0dec4aad01162293499547f1114", @@ -108,7 +110,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "9668feb5b9ba3b816d470dde2cc87c70811a7f9a", + "commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/delivery.go b/boatstack/delivery.go index e3d9658..c344291 100644 --- a/boatstack/delivery.go +++ b/boatstack/delivery.go @@ -1,7 +1,6 @@ package boatstack import ( - "encoding/json" "fmt" "os" "path/filepath" @@ -238,8 +237,8 @@ func LoadDeliveryState(repo, feature string) (DeliveryState, error) { return DeliveryState{}, fmt.Errorf("managed delivery state is missing: %w", err) } var state DeliveryState - if err := json.Unmarshal(value, &state); err != nil { - return DeliveryState{}, fmt.Errorf("managed delivery state is invalid: %w", err) + if err := DecodeJSON("load managed delivery state", path, value, &state); err != nil { + return DeliveryState{}, err } if state.SchemaVersion != deliveryStateSchemaVersion || state.Feature != feature || len(state.Slices) == 0 || state.ActiveIndex < 0 || state.ActiveIndex > len(state.Slices) { return DeliveryState{}, fmt.Errorf("managed delivery state is invalid") @@ -385,7 +384,10 @@ func readDeliveryReceipt(repo, feature, sliceID, gate string) (DeliveryGateRecei return DeliveryGateReceipt{}, fmt.Errorf("%s gate receipt is missing for delivery slice %s", gate, sliceID) } var receipt DeliveryGateReceipt - if err := json.Unmarshal(value, &receipt); err != nil || receipt.SchemaVersion != deliveryStateSchemaVersion || receipt.Feature != feature || receipt.SliceID != sliceID || receipt.Gate != gate { + if err := DecodeJSON("load delivery gate receipt", path, value, &receipt); err != nil { + return DeliveryGateReceipt{}, err + } + if receipt.SchemaVersion != deliveryStateSchemaVersion || receipt.Feature != feature || receipt.SliceID != sliceID || receipt.Gate != gate { return DeliveryGateReceipt{}, fmt.Errorf("%s gate receipt is invalid for delivery slice %s", gate, sliceID) } return receipt, nil diff --git a/boatstack/export.go b/boatstack/export.go index 2498767..de909fb 100644 --- a/boatstack/export.go +++ b/boatstack/export.go @@ -19,6 +19,11 @@ var allowedAdapters = map[string]bool{ "github": true, } +var ( + readCanonical = ReadCanonical + readCanonicalDir = ReadCanonicalDir +) + type ExportBundle struct { Files map[string][]byte Config ProjectConfig @@ -68,7 +73,7 @@ func LoadConfig(path string) (ProjectConfig, []byte, error) { return ProjectConfig{}, nil, err } var config ProjectConfig - if err := json.Unmarshal(raw, &config); err != nil { + if err := DecodeJSON("load project configuration", path, raw, &config); err != nil { return ProjectConfig{}, nil, err } if err := ValidateConfig(config); err != nil { @@ -181,14 +186,14 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte } for _, name := range []string{"workflow.md", "artifacts.md", "failure-moves.md", "irreversible-operation-boundary.md"} { - value, err := ReadCanonical("references/" + name) + value, err := readCanonical("references/" + name) if err != nil { return ExportBundle{}, err } files[".product-loop/"+name] = GeneratedMarkdown(string(value)) } - entries, err := ReadCanonicalDir("assets/templates") + entries, err := readCanonicalDir("assets/templates") if err != nil { return ExportBundle{}, err } @@ -196,19 +201,20 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte if entry.IsDir() { continue } - value, err := ReadCanonical("assets/templates/" + entry.Name()) + value, err := readCanonical("assets/templates/" + entry.Name()) if err != nil { return ExportBundle{}, err } path := ".product-loop/templates/" + entry.Name() if strings.HasSuffix(entry.Name(), ".json") { var decoded any - if err := json.Unmarshal(value, &decoded); err != nil { + templateName := "assets/templates/" + entry.Name() + if err := DecodeJSON("build export bundle from JSON template", templateName, value, &decoded); err != nil { return ExportBundle{}, err } files[path], err = GeneratedJSON(decoded) if err != nil { - return ExportBundle{}, err + return ExportBundle{}, fmt.Errorf("generate JSON output %s from %s: %w", path, templateName, err) } } else { files[path] = GeneratedMarkdown(string(value)) @@ -389,6 +395,13 @@ List explicit gaps with impact and revisit trigger, or state that no material ga if err != nil { return ExportBundle{}, err } + for _, path := range sortedKeys(files) { + if strings.HasSuffix(path, ".json") { + if err := ValidateJSON("validate generated export bundle", path, files[path]); err != nil { + return ExportBundle{}, err + } + } + } return ExportBundle{Files: files, Config: config}, nil } diff --git a/boatstack/export_test.go b/boatstack/export_test.go index 5d6e4de..73764eb 100644 --- a/boatstack/export_test.go +++ b/boatstack/export_test.go @@ -1,12 +1,64 @@ package boatstack import ( + "io/fs" "os" "path/filepath" "strings" "testing" ) +type fixtureDirEntry struct{ name string } + +func (entry fixtureDirEntry) Name() string { return entry.name } +func (entry fixtureDirEntry) IsDir() bool { return false } +func (entry fixtureDirEntry) Type() fs.FileMode { return 0 } +func (entry fixtureDirEntry) Info() (fs.FileInfo, error) { return nil, nil } + +func TestBuildExportBundleNamesMalformedEmbeddedJSONTemplate(t *testing.T) { + oldRead := readCanonical + oldReadDir := readCanonicalDir + defer func() { readCanonical, readCanonicalDir = oldRead, oldReadDir }() + readCanonicalDir = func(path string) ([]fs.DirEntry, error) { + entries, err := oldReadDir(path) + return append(entries, fixtureDirEntry{name: "nul-fixture.json"}), err + } + readCanonical = func(path string) ([]byte, error) { + if path == "assets/templates/nul-fixture.json" { + return []byte{'{', 0, '}'}, nil + } + return oldRead(path) + } + config := testConfig() + raw, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + _, err = BuildExportBundle(".boatstack-project.json", config, raw, "boatstack") + if err == nil || !strings.Contains(err.Error(), "build export bundle from JSON template") || !strings.Contains(err.Error(), "assets/templates/nul-fixture.json") { + t.Fatalf("malformed template error lacks operation or exact asset: %v", err) + } +} + +func TestDecodeJSONAlwaysNamesOperationAndSource(t *testing.T) { + for _, test := range []struct { + operation string + source string + }{ + {"load project configuration", "/repo/.boatstack-project.json"}, + {"validate generated export bundle", ".product-loop/generated.lock.json"}, + {"look up latest release", "GitHub releases/latest response"}, + } { + t.Run(test.operation, func(t *testing.T) { + var decoded any + err := DecodeJSON(test.operation, test.source, []byte{'{', 0, '}'}, &decoded) + if err == nil || !strings.Contains(err.Error(), "operation "+test.operation) || !strings.Contains(err.Error(), "parse JSON "+test.source) { + t.Fatalf("JSON diagnostic lacks operation or source: %v", err) + } + }) + } +} + func testConfig() ProjectConfig { return ProjectConfig{ SchemaVersion: 1, diff --git a/boatstack/hooks.go b/boatstack/hooks.go index 2be19df..7b4b7af 100644 --- a/boatstack/hooks.go +++ b/boatstack/hooks.go @@ -230,8 +230,8 @@ func loadHookConfig(path string) (map[string]any, error) { return nil, err } config := map[string]any{} - if err := json.Unmarshal(value, &config); err != nil { - return nil, fmt.Errorf("invalid host hook config %s: %w", path, err) + if err := DecodeJSON("load host hook configuration", path, value, &config); err != nil { + return nil, err } return config, nil } @@ -275,6 +275,22 @@ func mergeHostHook(config map[string]any, host string) error { } func InstallHostHooks(repo string, adapters []string) error { + prepared, err := PrepareHostHooks(repo, adapters) + if err != nil { + return err + } + for _, path := range sortedKeys(prepared) { + if err := atomicWrite(path, prepared[path]); err != nil { + return err + } + } + return nil +} + +// PrepareHostHooks renders and validates every selected host document without +// writing, allowing initialization to fail before entering its commit phase. +func PrepareHostHooks(repo string, adapters []string) (map[string][]byte, error) { + prepared := map[string][]byte{} for _, host := range []string{"cursor", "claude", "codex"} { if !contains(adapters, host) { continue @@ -282,20 +298,21 @@ func InstallHostHooks(repo string, adapters []string) error { path := hostHookConfigPath(repo, host) config, err := loadHookConfig(path) if err != nil { - return err + return nil, err } if err := mergeHostHook(config, host); err != nil { - return err + return nil, fmt.Errorf("prepare %s host hooks in %s: %w", host, path, err) } value, err := MarshalJSON(config) if err != nil { - return err + return nil, fmt.Errorf("serialize merged host hook configuration %s: %w", path, err) } - if err := atomicWrite(path, value); err != nil { - return err + if err := ValidateJSON("validate merged host hook configuration", path, value); err != nil { + return nil, err } + prepared[path] = value } - return nil + return prepared, nil } func CheckHostHooks(repo string, adapters []string) error { @@ -318,8 +335,8 @@ func CheckInstalledHostHooks(repo string, adapters []string) error { if err != nil { return nil, fmt.Errorf("cannot read installed %s hook fragment: %w", host, err) } - if err := json.Unmarshal(value, &fragment); err != nil { - return nil, fmt.Errorf("invalid installed %s hook fragment: %w", host, err) + if err := DecodeJSON("load installed host hook fragment", path, value, &fragment); err != nil { + return nil, err } if fragment["schema_version"] != float64(1) || fragment["host"] != host { return nil, fmt.Errorf("invalid installed %s hook fragment identity", host) diff --git a/boatstack/init.go b/boatstack/init.go index 54efdd9..630a867 100644 --- a/boatstack/init.go +++ b/boatstack/init.go @@ -22,6 +22,11 @@ type InitOptions struct { Output io.Writer } +var ( + initDoctor = Doctor + initCheckpoint = func(string) error { return nil } +) + func gitOutput(repo string, arguments ...string) string { command := exec.Command("git", append([]string{"-C", repo}, arguments...)...) value, err := command.Output() @@ -194,9 +199,17 @@ func copyHelper(source, repo string) (string, string, error) { } func writeInstallLock(repo, binaryPath, binaryHash string, integrations map[string]IntegrationState) error { + value, err := buildInstallLock(repo, binaryPath, binaryHash, integrations) + if err != nil { + return err + } + return atomicWriteMode(filepath.Join(repo, ".product-loop", "bin", "install.lock.json"), value, 0o644) +} + +func buildInstallLock(repo, binaryPath, binaryHash string, integrations map[string]IntegrationState) ([]byte, error) { relativeBinaryPath, err := repositoryRelativePath(repo, binaryPath) if err != nil { - return fmt.Errorf("invalid Boatstack helper path: %w", err) + return nil, fmt.Errorf("invalid Boatstack helper path: %w", err) } lock := map[string]any{ "schema_version": 1, @@ -210,9 +223,13 @@ func writeInstallLock(repo, binaryPath, binaryHash string, integrations map[stri } value, err := MarshalJSON(lock) if err != nil { - return err + return nil, err } - return atomicWriteMode(filepath.Join(repo, ".product-loop", "bin", "install.lock.json"), value, 0o644) + lockPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json") + if err := ValidateJSON("validate generated install lock", lockPath, value); err != nil { + return nil, err + } + return value, nil } func readInstalledIntegrations(repo string, config ProjectConfig) (map[string]IntegrationState, error) { @@ -223,8 +240,9 @@ func readInstalledIntegrations(repo string, config ProjectConfig) (map[string]In var lock struct { Integrations map[string]IntegrationState `json:"integrations"` } - if err := json.Unmarshal(value, &lock); err != nil { - return nil, fmt.Errorf("invalid previous local install lock: %w", err) + lockPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json") + if err := DecodeJSON("load previous local install lock", lockPath, value, &lock); err != nil { + return nil, err } if len(lock.Integrations) > 0 { return lock.Integrations, nil @@ -275,7 +293,7 @@ func checkUpdateDiffScope(repo string, currentFiles map[string][]byte, previous return changed, nil } -func RunInit(options InitOptions) error { +func RunInit(options InitOptions) (returnErr error) { if options.Input == nil { options.Input = os.Stdin } @@ -373,6 +391,12 @@ func RunInit(options InitOptions) error { if err != nil { return err } + if err := ValidateJSON("validate project configuration before initialization", configPath, rawConfig); err != nil { + return err + } + if _, err := PrepareHostHooks(repo, config.Adapters); err != nil { + return err + } if problems := ExportCollisions(repo, bundle.Files); len(problems) > 0 { return fmt.Errorf("refusing to overwrite user-owned files: %s", strings.Join(problems, ", ")) } @@ -403,30 +427,61 @@ func RunInit(options InitOptions) error { return err } } + snapshot, err := beginRepositorySnapshot(repo) + if err != nil { + return err + } + defer func() { + if returnErr != nil { + if rollbackErr := snapshot.rollback(); rollbackErr != nil { + returnErr = fmt.Errorf("%v; initialization rollback failed: %w", returnErr, rollbackErr) + } + } + }() if _, err := installSharedRuntime(helperSource, repo, config.Integrations); err != nil { return fmt.Errorf("cannot install the repository-family Boatstack runtime: %w", err) } + var states map[string]IntegrationState + if options.Update { + states = preservedStates + } else { + states, err = InstallIntegrations(choice, repo, config.Adapters) + } + if err != nil { + return err + } + helperValue, err := os.ReadFile(helperSource) + if err != nil { + return fmt.Errorf("read Boatstack helper before initialization commit: %w", err) + } + prospectiveBinaryPath := filepath.Join(repo, ".product-loop", "bin", helperName()) + if _, err := buildInstallLock(repo, prospectiveBinaryPath, SHA256Bytes(helperValue), states); err != nil { + return err + } if err := os.WriteFile(configPath, rawConfig, 0o644); err != nil { return err } + if err := initCheckpoint("config-written"); err != nil { + return fmt.Errorf("initialization checkpoint config-written: %w", err) + } if err := WriteExport(repo, bundle.Files); err != nil { return err } + if err := initCheckpoint("export-written"); err != nil { + return fmt.Errorf("initialization checkpoint export-written: %w", err) + } if err := InstallHostHooks(repo, config.Adapters); err != nil { return err } + if err := initCheckpoint("hooks-written"); err != nil { + return fmt.Errorf("initialization checkpoint hooks-written: %w", err) + } binaryPath, binaryHash, err := copyHelper(helperSource, repo) if err != nil { return err } - var states map[string]IntegrationState - if options.Update { - states = preservedStates - } else { - states, err = InstallIntegrations(choice, repo, config.Adapters) - } - if err != nil { - return err + if err := initCheckpoint("helper-written"); err != nil { + return fmt.Errorf("initialization checkpoint helper-written: %w", err) } if _, err := installSharedRuntime(helperSource, repo, states); err != nil { return fmt.Errorf("cannot finalize the repository-family Boatstack runtime: %w", err) @@ -434,13 +489,16 @@ func RunInit(options InitOptions) error { if err := writeInstallLock(repo, binaryPath, binaryHash, states); err != nil { return err } + if err := initCheckpoint("install-lock-written"); err != nil { + return fmt.Errorf("initialization checkpoint install-lock-written: %w", err) + } if err := CheckExport(repo, bundle.Files); err != nil { return err } if err := CheckHostHooks(repo, config.Adapters); err != nil { return err } - if err := Doctor(repo); err != nil { + if err := initDoctor(repo); err != nil { return fmt.Errorf("post-install smoke check failed: %w", err) } if options.Update { @@ -504,6 +562,9 @@ func RunInit(options InitOptions) error { fmt.Fprintln(options.Output, " Cursor: /auto-plan") fmt.Fprintln(options.Output, " Codex: $boatstack auto-plan") fmt.Fprintln(options.Output, "If Boatstack created .claude/skills during an active Claude Code session, reload Claude Code before using its slash commands.") + if err := snapshot.commit(); err != nil { + return fmt.Errorf("remove initialization rollback snapshot: %w", err) + } return nil } diff --git a/boatstack/init_test.go b/boatstack/init_test.go index e1d523f..62a7fbc 100644 --- a/boatstack/init_test.go +++ b/boatstack/init_test.go @@ -83,6 +83,121 @@ func TestRuntimeFreeInit(t *testing.T) { } } +func TestInitFreshThirdPartyPythonRepositoryWithValidConfig(t *testing.T) { + repo := t.TempDir() + if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil { + t.Fatalf("git init: %v: %s", err, output) + } + if err := os.WriteFile(filepath.Join(repo, "pyproject.toml"), []byte("[project]\nname = \"hatch-fixture\"\n[tool.pytest.ini_options]\n"), 0o644); err != nil { + t.Fatal(err) + } + config := testConfig() + config.Project.Name = "hatch-fixture" + config.Project.Commands["test"] = "python -m pytest" + raw, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + configPath := filepath.Join(repo, ".boatstack-project.json") + if err := os.WriteFile(configPath, raw, 0o644); err != nil { + t.Fatal(err) + } + if err := RunInit(InitOptions{Repo: repo, IntegrationChoice: "core", Yes: true, Output: &bytes.Buffer{}}); err != nil { + t.Fatal(err) + } + for _, relative := range []string{ + ".product-loop/project.json", ".product-loop/generated.lock.json", + ".product-loop/bin/install.lock.json", ".cursor/commands/auto-plan.md", + } { + if !fileExists(filepath.Join(repo, filepath.FromSlash(relative))) { + t.Fatalf("fresh third-party init did not create %s", relative) + } + } + if err := Doctor(repo); err != nil { + t.Fatalf("fresh third-party installation is not controller-ready: %v", err) + } +} + +func TestInitRollsBackRepositoryWhenPostInstallVerificationFails(t *testing.T) { + repo := t.TempDir() + if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil { + t.Fatalf("git init: %v: %s", err, output) + } + packagePath := filepath.Join(repo, "package.json") + original := []byte(`{"scripts":{"test":"node --test"}}`) + if err := os.WriteFile(packagePath, original, 0o640); err != nil { + t.Fatal(err) + } + oldDoctor := initDoctor + initDoctor = func(string) error { return fmt.Errorf("injected verification failure") } + defer func() { initDoctor = oldDoctor }() + err := RunInit(InitOptions{Repo: repo, IntegrationChoice: "core", Yes: true, Output: &bytes.Buffer{}}) + if err == nil || !strings.Contains(err.Error(), "injected verification failure") { + t.Fatalf("expected injected initialization failure, got %v", err) + } + value, readErr := os.ReadFile(packagePath) + if readErr != nil || !bytes.Equal(value, original) { + t.Fatalf("rollback did not restore original repository file: %v", readErr) + } + info, statErr := os.Stat(packagePath) + if statErr != nil { + t.Fatalf("rollback did not restore original file metadata: %v", statErr) + } + if runtime.GOOS != "windows" && info.Mode().Perm() != 0o640 { + t.Fatalf("rollback did not restore original file mode: %v %#o", statErr, info.Mode().Perm()) + } + for _, relative := range []string{".boatstack-project.json", ".product-loop", ".cursor", ".claude", ".codex"} { + if _, statErr := os.Lstat(filepath.Join(repo, relative)); !os.IsNotExist(statErr) { + t.Fatalf("partial installation state remains at %s: %v", relative, statErr) + } + } +} + +func TestInitRollsBackAtEveryCommitStage(t *testing.T) { + stages := []string{"config-written", "export-written", "hooks-written", "helper-written", "install-lock-written"} + for _, stage := range stages { + t.Run(stage, func(t *testing.T) { + repo := t.TempDir() + if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil { + t.Fatalf("git init: %v: %s", err, output) + } + original := []byte(`{"scripts":{"test":"node --test"}}`) + packagePath := filepath.Join(repo, "package.json") + if err := os.WriteFile(packagePath, original, 0o640); err != nil { + t.Fatal(err) + } + oldCheckpoint := initCheckpoint + initCheckpoint = func(current string) error { + if current == stage { + return fmt.Errorf("injected %s failure", stage) + } + return nil + } + defer func() { initCheckpoint = oldCheckpoint }() + err := RunInit(InitOptions{Repo: repo, IntegrationChoice: "core", Yes: true, Output: &bytes.Buffer{}}) + if err == nil || !strings.Contains(err.Error(), "injected "+stage+" failure") { + t.Fatalf("expected injected %s failure, got %v", stage, err) + } + value, readErr := os.ReadFile(packagePath) + if readErr != nil || !bytes.Equal(value, original) { + t.Fatalf("rollback at %s did not restore original file: %v", stage, readErr) + } + info, statErr := os.Stat(packagePath) + if statErr != nil { + t.Fatalf("rollback at %s did not restore file metadata: %v", stage, statErr) + } + if runtime.GOOS != "windows" && info.Mode().Perm() != 0o640 { + t.Fatalf("rollback at %s did not restore file mode: %v", stage, statErr) + } + for _, relative := range []string{".boatstack-project.json", ".product-loop", ".cursor", ".claude", ".codex"} { + if _, statErr := os.Lstat(filepath.Join(repo, relative)); !os.IsNotExist(statErr) { + t.Fatalf("partial state remains after %s at %s: %v", stage, relative, statErr) + } + } + }) + } +} + func TestDetectTestCommandCoversCheckScriptAndPythonProjects(t *testing.T) { for name, setup := range map[string]struct { files map[string]string diff --git a/boatstack/init_transaction.go b/boatstack/init_transaction.go new file mode 100644 index 0000000..1ada128 --- /dev/null +++ b/boatstack/init_transaction.go @@ -0,0 +1,111 @@ +package boatstack + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" +) + +type repositorySnapshot struct { + repo string + backup string + active bool +} + +func beginRepositorySnapshot(repo string) (*repositorySnapshot, error) { + backup, err := os.MkdirTemp("", "boatstack-init-rollback-*") + if err != nil { + return nil, fmt.Errorf("create initialization rollback snapshot: %w", err) + } + snapshot := &repositorySnapshot{repo: repo, backup: backup, active: true} + if err := copyRepositoryTree(repo, backup); err != nil { + _ = os.RemoveAll(backup) + return nil, fmt.Errorf("snapshot repository before initialization: %w", err) + } + return snapshot, nil +} + +func copyRepositoryTree(source, destination string) error { + return filepath.WalkDir(source, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(source, path) + if err != nil { + return err + } + if relative == ".git" || (relative != "." && filepath.Dir(relative) == ".git") { + if entry.IsDir() { + return filepath.SkipDir + } + return nil + } + if relative == "." { + return nil + } + target := filepath.Join(destination, relative) + info, err := entry.Info() + if err != nil { + return err + } + if entry.Type()&os.ModeSymlink != 0 { + link, err := os.Readlink(path) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + return os.Symlink(link, target) + } + if entry.IsDir() { + return os.MkdirAll(target, info.Mode().Perm()) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("unsupported repository entry in initialization transaction: %s", path) + } + value, err := os.ReadFile(path) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + return os.WriteFile(target, value, info.Mode().Perm()) + }) +} + +func (snapshot *repositorySnapshot) rollback() error { + if !snapshot.active { + return nil + } + entries, err := os.ReadDir(snapshot.repo) + if err != nil { + return err + } + for _, entry := range entries { + if entry.Name() == ".git" { + continue + } + if err := os.RemoveAll(filepath.Join(snapshot.repo, entry.Name())); err != nil { + return fmt.Errorf("remove partial initialization path %s: %w", entry.Name(), err) + } + } + if err := copyRepositoryTree(snapshot.backup, snapshot.repo); err != nil { + return fmt.Errorf("restore repository after initialization failure: %w", err) + } + snapshot.active = false + return os.RemoveAll(snapshot.backup) +} + +func (snapshot *repositorySnapshot) commit() error { + if !snapshot.active { + return nil + } + if err := os.RemoveAll(snapshot.backup); err != nil { + return err + } + snapshot.active = false + return nil +} diff --git a/boatstack/plan.go b/boatstack/plan.go index 7467c93..19d62c1 100644 --- a/boatstack/plan.go +++ b/boatstack/plan.go @@ -1,7 +1,6 @@ package boatstack import ( - "encoding/json" "fmt" "os" "os/exec" @@ -155,7 +154,7 @@ func loadJSONObject(path, label, startMarker, endMarker string, allowLegacyMarkd } } var plan map[string]any - if err := json.Unmarshal(payload, &plan); err != nil { + if err := DecodeJSON("load "+label, path, payload, &plan); err != nil { return nil, fmt.Errorf("invalid %s json: %w", label, err) } return plan, nil @@ -859,8 +858,8 @@ func ActivatePlan(options ActivationOptions) error { return fmt.Errorf("existing plan lock cannot be verified: %w", readErr) } var existing map[string]any - if json.Unmarshal(value, &existing) != nil { - return fmt.Errorf("existing plan lock is unreadable; do not overwrite activation state") + if err := DecodeJSON("inspect existing plan lock", options.OutputPath, value, &existing); err != nil { + return fmt.Errorf("%w; do not overwrite activation state", err) } currentPlanHash, _ := SHA256File(options.PlanPath) currentSourceHash, _ := SHA256File(check.SourcePlanPath) @@ -951,8 +950,8 @@ func CheckApprovalLock(options ApprovalOptions) error { return fmt.Errorf("plan lock is missing or unreadable: %w", err) } lock := map[string]any{} - if err := json.Unmarshal(value, &lock); err != nil { - return fmt.Errorf("plan lock is unreadable: %w", err) + if err := DecodeJSON("check plan approval lock", options.OutputPath, value, &lock); err != nil { + return err } mismatches := []string{} paths := map[string]string{"source_plan": options.SourcePlanPath, "spec": options.SpecPath, "plan": options.PlanPath, "task_graph": options.TasksPath} diff --git a/boatstack/pr.go b/boatstack/pr.go index 7eb6dbe..29a7b9b 100644 --- a/boatstack/pr.go +++ b/boatstack/pr.go @@ -511,8 +511,8 @@ func parsePRFrontmatter(value string) (map[string]string, string, error) { continue } var decoded string - if err := json.Unmarshal([]byte(raw), &decoded); err != nil { - return nil, "", fmt.Errorf("PR frontmatter field %s must be a JSON-quoted string", key) + if err := DecodeJSON("parse PR frontmatter", "field "+key, []byte(raw), &decoded); err != nil { + return nil, "", fmt.Errorf("%w; value must be a JSON-quoted string", err) } fields[key] = decoded } diff --git a/boatstack/runtime.go b/boatstack/runtime.go index d6950d8..eaa23d6 100644 --- a/boatstack/runtime.go +++ b/boatstack/runtime.go @@ -114,13 +114,28 @@ func MarshalJSON(value any) ([]byte, error) { return append(data, '\n'), nil } +// DecodeJSON is the single diagnostic boundary for JSON consumed by Boatstack. +// source must name a filesystem path, embedded asset, generated destination, or +// remote response so malformed input is actionable without a debugger. +func DecodeJSON(operation, source string, raw []byte, destination any) error { + if err := json.Unmarshal(raw, destination); err != nil { + return fmt.Errorf("operation %s: parse JSON %s: %w", operation, source, err) + } + return nil +} + +func ValidateJSON(operation, source string, raw []byte) error { + var decoded any + return DecodeJSON(operation, source, raw, &decoded) +} + func GeneratedJSON(value any) ([]byte, error) { raw, err := json.Marshal(value) if err != nil { return nil, err } data := map[string]any{} - if err := json.Unmarshal(raw, &data); err != nil { + if err := DecodeJSON("generate JSON metadata", "marshaled generated value", raw, &data); err != nil { return nil, err } data["_generated_by"] = Generator diff --git a/boatstack/runtime_cache.go b/boatstack/runtime_cache.go index c5b97f7..54b83f0 100644 --- a/boatstack/runtime_cache.go +++ b/boatstack/runtime_cache.go @@ -1,7 +1,6 @@ package boatstack import ( - "encoding/json" "fmt" "io/fs" "os" @@ -181,8 +180,8 @@ func loadSharedRuntime(repo string) (runtimeManifest, string, error) { return runtimeManifest{}, "", fmt.Errorf("shared Boatstack runtime is missing; run the verified installer once from any checkout in this Git clone: %w", err) } var manifest runtimeManifest - if err := json.Unmarshal(value, &manifest); err != nil { - return runtimeManifest{}, "", fmt.Errorf("shared Boatstack runtime manifest is invalid: %w", err) + if err := DecodeJSON("load shared Boatstack runtime manifest", manifestPath, value, &manifest); err != nil { + return runtimeManifest{}, "", err } if manifest.SchemaVersion != 1 || manifest.BoatstackVersion != Version || manifest.SourceCommit != SourceCommit || manifest.Platform != platformKey() { @@ -207,8 +206,9 @@ func verifyGeneratedRuntime(repo string) error { return fmt.Errorf("missing generated Boatstack runtime provenance: %w", err) } var lock generatedRuntimeLock - if err := json.Unmarshal(value, &lock); err != nil { - return fmt.Errorf("invalid generated Boatstack runtime provenance: %w", err) + lockPath := filepath.Join(repo, ".product-loop", "generated.lock.json") + if err := DecodeJSON("verify generated Boatstack runtime provenance", lockPath, value, &lock); err != nil { + return err } if lock.BoatstackVersion != Version || lock.Runtime.SourceCommit != SourceCommit { return fmt.Errorf("this worktree expects Boatstack %s (%s), but the runtime is %s (%s); update or rebase its Boatstack infrastructure", @@ -310,8 +310,8 @@ func verifyLocalRuntime(repo string) error { return fmt.Errorf("missing local install lock: %w", err) } var lock installLock - if err := json.Unmarshal(value, &lock); err != nil { - return fmt.Errorf("invalid local install lock: %w", err) + if err := DecodeJSON("verify local Boatstack runtime", lockPath, value, &lock); err != nil { + return err } if lock.BoatstackVersion != Version || lock.SourceCommit != SourceCommit { return fmt.Errorf("helper version drift: installed %s (%s), expected %s (%s)", lock.BoatstackVersion, lock.SourceCommit, Version, SourceCommit) diff --git a/boatstack/safety.go b/boatstack/safety.go index 397b52d..ad82d16 100644 --- a/boatstack/safety.go +++ b/boatstack/safety.go @@ -278,8 +278,8 @@ func dedupeFindings(values []SafetyFinding) []SafetyFinding { func hookToolInput(host string, value []byte) (string, any, error) { var event map[string]any - if err := json.Unmarshal(value, &event); err != nil { - return "", nil, fmt.Errorf("invalid hook JSON") + if err := DecodeJSON("parse "+host+" hook event", "stdin", value, &event); err != nil { + return "", nil, err } if host == "cursor" { command := stringValue(event["command"]) diff --git a/boatstack/update.go b/boatstack/update.go index c599635..02def25 100644 --- a/boatstack/update.go +++ b/boatstack/update.go @@ -2,7 +2,6 @@ package boatstack import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -131,8 +130,12 @@ func defaultFetchLatestRelease() (ReleaseInfo, error) { Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` } - if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&payload); err != nil { - return ReleaseInfo{}, fmt.Errorf("invalid latest release response: %w", err) + responseJSON, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) + if err != nil { + return ReleaseInfo{}, fmt.Errorf("read latest GitHub release response: %w", err) + } + if err := DecodeJSON("look up latest release", "GitHub releases/latest response", responseJSON, &payload); err != nil { + return ReleaseInfo{}, err } if payload.Draft || payload.Prerelease { return ReleaseInfo{}, fmt.Errorf("latest release response is not a stable published release") @@ -160,7 +163,8 @@ func loadUpdateState(repo string) (UpdateState, error) { return UpdateState{}, err } var state UpdateState - if err := json.Unmarshal(value, &state); err != nil { + path := updateStatePath(repo) + if err := DecodeJSON("load Boatstack update state", path, value, &state); err != nil { return UpdateState{}, err } if state.SchemaVersion != 1 { @@ -280,7 +284,11 @@ func CheckPreviousGeneratedState(repo string) error { var lock struct { Files map[string]string `json:"files"` } - if err := json.Unmarshal(value, &lock); err != nil || len(lock.Files) == 0 { + path := filepath.Join(repo, ".product-loop", "generated.lock.json") + if err := DecodeJSON("check previous generated provenance", path, value, &lock); err != nil { + return err + } + if len(lock.Files) == 0 { return fmt.Errorf("invalid generated provenance") } problems := []string{} @@ -303,8 +311,9 @@ func CheckExistingInstallProvenance(repo string) error { return fmt.Errorf("missing previous local install lock: %w", err) } var lock installLock - if err := json.Unmarshal(value, &lock); err != nil { - return fmt.Errorf("invalid previous local install lock: %w", err) + path := filepath.Join(repo, ".product-loop", "bin", "install.lock.json") + if err := DecodeJSON("check existing install provenance", path, value, &lock); err != nil { + return err } if _, err := parseStableVersion(lock.BoatstackVersion); err != nil || strings.TrimSpace(lock.SourceCommit) == "" { return fmt.Errorf("previous local install lock has invalid release provenance") diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index dade05a..a7a64f7 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`9668feb5b9ba3b816d470dde2cc87c70811a7f9a`](https://github.com/operatorstack/intelligence-flow/tree/9668feb5b9ba3b816d470dde2cc87c70811a7f9a/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`697ce2f7d98eb3bfb249b33d3556efbd7e9365b1`](https://github.com/operatorstack/intelligence-flow/tree/697ce2f7d98eb3bfb249b33d3556efbd7e9365b1/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index 6f111f8..60b1177 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "9668feb5b9ba3b816d470dde2cc87c70811a7f9a", + "source_commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:9668feb5b9ba3b816d470dde2cc87c70811a7f9a" + "last_verified_version": "source:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 3312fff..316c9b9 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "9668feb5b9ba3b816d470dde2cc87c70811a7f9a", + "source_commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-18-atomic-initialization.md b/release-notes/2026-07-18-atomic-initialization.md new file mode 100644 index 0000000..b755faa --- /dev/null +++ b/release-notes/2026-07-18-atomic-initialization.md @@ -0,0 +1,3 @@ +### Boatstack initialization failures are actionable and atomic + +Boatstack now identifies the exact JSON source and parse operation when initialization encounters malformed configuration, embedded templates, generated assets, hooks, or install metadata. Initialization validates its JSON outputs before repository writes and restores the original repository state if any commit or verification stage fails, preventing partial installations while preserving fail-closed safety.