diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ca716a..d4816f1 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/fbb9ecffc548440fadb5d32339115bd20166a29f/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/2364eaedaefbe73d8996108dfac261c38b0ce2b3/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 224d86f..befd862 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "e016bb241bf61fdc8e96cac4d1cac7a674c545a2e788abeac663ce4235316c3f", + "CONTRIBUTING.md": "ea4d692766d22427d515ab944c9d692e862d63584c255fdecdb67e063cb32e32", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -115,6 +115,8 @@ "boatstack/internal/deliverycontrol/trajectorylog_test.go": "227dd6ed9ce181d517a37b67ef4d64dd93779a533eae804798ab54de35c7f13e", "boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", + "boatstack/migrate_effect_grade.go": "bccb58e770001aa9554d8e7f151663d907f152508a61118f56fd90465ba6f32e", + "boatstack/migrate_effect_grade_test.go": "fea1d1057bc6d8eaf015e377864a3adab29ef5731f597fe0a38b96fa80355d14", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", "boatstack/mutation.go": "59fc9e92105d8ec20f854af9898cde037ab0e3e46c453794838dbfc65fecdd6d", "boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6", @@ -145,7 +147,7 @@ "boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e", "boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742", "boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59", - "boatstack/references/config-schema.md": "fa5e09d008101957cf5577e9031de256ab682711c3fa21fa9494cd600ddbd2f0", + "boatstack/references/config-schema.md": "eff8586850eca9941df1cea29ac7edb779277ed9bd6d938a1980db5028d28cf4", "boatstack/references/failure-moves.md": "b65ef72035afa6ad0dce589a0b38f84bc40cde3864c9ecf973f08fc687f001c3", "boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", @@ -156,7 +158,7 @@ "boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739", "boatstack/run.go": "3127e8c054b80e41413011e423ba9463fe06a7e6c9e1e71750ab98587871d89d", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", - "boatstack/runtime.go": "11f741d45994c336e927121ede2481c94d5c33ed9c3beecf89f760450b304da3", + "boatstack/runtime.go": "7dc5d033ec11bbcf9a4561da66d4d6692a071bc79ac2fe4eb66feaced358d3c3", "boatstack/runtime_cache.go": "e026ffc1906f7e1e98b768bae63e6658164d2826c07169c9121ce0f23c73faf8", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", "boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc", @@ -188,10 +190,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0", - "docs/evidence-engineered-coding.md": "82b129eeacdcea2ccfca0eeb579c412a0d3938e3413d0f57c7c953f55762f25f", + "docs/evidence-engineered-coding.md": "0083581913336f9612f321997a3e9afb8b7f2549e41d6fbae122920174ae4779", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "868026dabc8ae86dec955195d6f78aa45a5d74cb44ece1c873fa29e69ff8c8b1", + "docs/public-claims.json": "aa2ee5ecd6a3f29d5dd3c4e538d29931191606a52fad6433d43184af814334c3", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -205,7 +207,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": "dc27d78e3c23888025cda5f56ce058e48e009584da08ea111d095b8ce2ccb29b", + "labs/diagram-json/plan.lock.json": "9c2377941197d6bd160f338e0bc55dd74f9118844291b5ee73a17867757a5edb", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -324,12 +326,13 @@ "release-notes/2026-07-27-discoverable-planning-errors.md": "d8099d1a6cd1805c3fcd446d9fa95739dec93e57ff421ecf4f99562b143c9966", "release-notes/2026-07-27-guard-dual-reward-corpus.md": "6bec0385c6c553f00517259821e502796ca1b1907aeab718a287560e3e0fa0d6", "release-notes/2026-07-27-invalid-delivery-block-actionable.md": "8fac8e3921e2285291703efa46e624b72cb5bac1b8492beca4c4b633abb5ba16", - "release-notes/2026-07-27-read-only-inspection-pipelines.md": "0963286371e9a12592915c23a958dd013bf2a35e9fca6921691bc8bb3c3d8dc8" + "release-notes/2026-07-27-read-only-inspection-pipelines.md": "0963286371e9a12592915c23a958dd013bf2a35e9fca6921691bc8bb3c3d8dc8", + "release-notes/2026-07-27-sandboxed-migration-grading.md": "03cebc372bbdfed37cc70d18f3b6374d1aa5e585bafefa073dbcced58bd0336a" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "fbb9ecffc548440fadb5d32339115bd20166a29f", + "commit": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/migrate_effect_grade.go b/boatstack/migrate_effect_grade.go new file mode 100644 index 0000000..fbf6c8a --- /dev/null +++ b/boatstack/migrate_effect_grade.go @@ -0,0 +1,92 @@ +package boatstack + +import ( + "bytes" + "os" + "os/exec" + "strings" +) + +// MigrationEffectStatus is the verdict of grading a migration by its observed effect. +type MigrationEffectStatus string + +const ( + // MigrationEffectSkipped means the project declared no migration commands, so no + // effect was executed — a repository without a database is unaffected. + MigrationEffectSkipped MigrationEffectStatus = "SKIPPED" + // MigrationEffectPass means the migration applied and verified against the + // disposable database. + MigrationEffectPass MigrationEffectStatus = "PASS" + // MigrationEffectFail means applying or verifying the migration failed — real + // breakage the static guard cannot see, because a migration is inert as data. + MigrationEffectFail MigrationEffectStatus = "FAIL" +) + +// MigrationEffectResult is the outcome of GradeMigrationEffect. +type MigrationEffectResult struct { + Status MigrationEffectStatus + Reason string +} + +// GradeMigrationEffect grades a project's migrations by their OBSERVED EFFECT rather +// than by their SQL text. Sandboxed-Effect law: when an effect's safety cannot be +// certified statically, execute it in a disposable environment and read the oracle; +// never approximate it by reading the source. The guard keeps treating a committed +// migration as a data artifact (it is applied later by the controlled pipeline); this +// harness IS that controlled executor for grading purposes. +// +// It runs the project's configured apply_command, then verify_command, via `sh -c` +// with the caller-provided environment (which carries the disposable database +// coordinate, BOATSTACK_MIGRATE_DB). PASS iff both succeed; FAIL if either fails; +// SKIPPED when no apply_command is configured. The caller owns the disposable +// database and its guaranteed teardown (a fresh-per-run service container in CI, or a +// temp file removed by the test) — this function only executes and grades. +func GradeMigrationEffect(repo string, extraEnv []string) (MigrationEffectResult, error) { + config, _, err := LoadConfig(WorkspaceFor(repo).ProjectConfigPath()) + if err != nil { + return MigrationEffectResult{}, err + } + apply := strings.TrimSpace(config.Project.Migration.ApplyCommand) + verify := strings.TrimSpace(config.Project.Migration.VerifyCommand) + if apply == "" { + return MigrationEffectResult{Status: MigrationEffectSkipped, Reason: "no migration apply_command is configured; effect grading skipped"}, nil + } + if out, runErr := runMigrationShell(repo, apply, extraEnv); runErr != nil { + return MigrationEffectResult{Status: MigrationEffectFail, Reason: "apply failed: " + firstOutputLine(out)}, nil + } + if verify != "" { + if out, runErr := runMigrationShell(repo, verify, extraEnv); runErr != nil { + return MigrationEffectResult{Status: MigrationEffectFail, Reason: "verify failed: " + firstOutputLine(out)}, nil + } + } + return MigrationEffectResult{Status: MigrationEffectPass, Reason: "migration applied and verified against the disposable database"}, nil +} + +// runMigrationShell keeps stdout (authority-bearing) and stderr (diagnostic) +// separate. Grading needs only the exit status and a diagnostic line, so it reports +// stderr, falling back to stdout when a tool writes its error there. +func runMigrationShell(dir, command string, extraEnv []string) (string, error) { + cmd := exec.Command("sh", "-c", command) + cmd.Dir = dir + cmd.Env = append(os.Environ(), extraEnv...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + diagnostic := strings.TrimSpace(stderr.String()) + if diagnostic == "" { + diagnostic = strings.TrimSpace(stdout.String()) + } + return diagnostic, err +} + +func firstOutputLine(s string) string { + s = strings.TrimSpace(s) + if index := strings.IndexByte(s, '\n'); index >= 0 { + s = s[:index] + } + if s == "" { + return "(no output)" + } + return s +} diff --git a/boatstack/migrate_effect_grade_test.go b/boatstack/migrate_effect_grade_test.go new file mode 100644 index 0000000..f9b53b9 --- /dev/null +++ b/boatstack/migrate_effect_grade_test.go @@ -0,0 +1,96 @@ +package boatstack + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +// migrateGradeRepo builds a repo whose project config declares the given migration +// apply/verify commands. A disposable SQLite database is created under the test's +// temp dir (removed automatically when the test ends — the guaranteed-teardown +// invariant), seeded with one row, and returned as the BOATSTACK_MIGRATE_DB env the +// commands read. +func migrateGradeRepo(t *testing.T, apply, verify string) (repo string, env []string) { + t.Helper() + repo = t.TempDir() + if err := os.MkdirAll(filepath.Join(repo, ".product-loop", "features"), 0o755); err != nil { + t.Fatal(err) + } + config := testConfig() + config.Project.Migration = MigrationConfig{ApplyCommand: apply, VerifyCommand: verify} + value, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil { + t.Fatal(err) + } + db := filepath.Join(t.TempDir(), "disposable.sqlite") + if out, seedErr := exec.Command("sqlite3", db, "CREATE TABLE accounts(id INTEGER); INSERT INTO accounts VALUES (1);").CombinedOutput(); seedErr != nil { + t.Fatalf("seed disposable db: %v: %s", seedErr, out) + } + return repo, []string{"BOATSTACK_MIGRATE_DB=" + db} +} + +// Sandboxed-Effect law: a migration's safety is graded by EXECUTING it against a +// fresh, disposable database and reading the oracle — never approximated from its +// SQL text. The guard treats the same migration as inert data; this harness is the +// controlled executor. A repo that declares no migration commands is unaffected. +func TestMigrationEffectGradingSandbox(t *testing.T) { + if _, err := exec.LookPath("sqlite3"); err != nil { + t.Skip("sqlite3 not available") + } + if _, err := exec.LookPath("sh"); err != nil { + t.Skip("sh not available") + } + + const apply = `sqlite3 "$BOATSTACK_MIGRATE_DB" < migrate.sql` + // Verify the invariant the migration must preserve: the seeded row still exists. + const verify = `test "$(sqlite3 "$BOATSTACK_MIGRATE_DB" 'SELECT count(*) FROM accounts')" = "1"` + + t.Run("skips cleanly when no migration commands are declared", func(t *testing.T) { + repo, env := migrateGradeRepo(t, "", "") + result, err := GradeMigrationEffect(repo, env) + if err != nil { + t.Fatal(err) + } + if result.Status != MigrationEffectSkipped { + t.Fatalf("unconfigured repo did not skip: %+v", result) + } + }) + + t.Run("safe forward migration grades PASS", func(t *testing.T) { + repo, env := migrateGradeRepo(t, apply, verify) + // A declarative migration full of DDL — the guard treats this as data. + if err := os.WriteFile(filepath.Join(repo, "migrate.sql"), + []byte("ALTER TABLE accounts ADD COLUMN active INTEGER DEFAULT 1;\n"), 0o644); err != nil { + t.Fatal(err) + } + result, err := GradeMigrationEffect(repo, env) + if err != nil { + t.Fatal(err) + } + if result.Status != MigrationEffectPass { + t.Fatalf("safe migration did not grade PASS: %+v", result) + } + }) + + t.Run("destructive migration grades FAIL against the disposable db", func(t *testing.T) { + repo, env := migrateGradeRepo(t, apply, verify) + // Dropping the populated table is inert as TEXT (the static guard allows it as + // a data artifact) but its EFFECT is caught by executing it in the sandbox. + if err := os.WriteFile(filepath.Join(repo, "migrate.sql"), + []byte("DROP TABLE accounts;\n"), 0o644); err != nil { + t.Fatal(err) + } + result, err := GradeMigrationEffect(repo, env) + if err != nil { + t.Fatal(err) + } + if result.Status != MigrationEffectFail { + t.Fatalf("destructive migration was not caught by effect grading: %+v", result) + } + }) +} diff --git a/boatstack/references/config-schema.md b/boatstack/references/config-schema.md index ca3a25c..0b4d214 100644 --- a/boatstack/references/config-schema.md +++ b/boatstack/references/config-schema.md @@ -8,6 +8,9 @@ boatstack-config-field:project.default_branch boatstack-config-field:project.context boatstack-config-field:project.commands boatstack-config-field:project.high_risk_paths +boatstack-config-field:project.migration +boatstack-config-field:project.migration.apply_command +boatstack-config-field:project.migration.verify_command boatstack-config-field:workflow boatstack-config-field:workflow.human_plan_approval boatstack-config-field:workflow.independent_review_for_high_risk @@ -62,6 +65,9 @@ This is the exhaustive serialization contract, not a list of recommended user ed - `test` (string, required): The exact command to execute project-local tests. - Other command names (string, optional): Additional repository-owned commands such as `build`, `lint`, or `typecheck`. - `high_risk_paths` (array of strings, optional): Glob patterns of files requiring independent reviewer sign-off before shipping. +- `migration` (object, optional): Declares how migrations are graded by EFFECT against a disposable database, so a committed migration stays a data artifact for the guard while its real effect is executed and observed by a conformance harness. Both commands run via `sh -c` with the disposable database coordinate in the environment as `BOATSTACK_MIGRATE_DB`; when `apply_command` is absent, grading is skipped. + - `apply_command` (string, optional): The command that applies the migration set to the disposable database. + - `verify_command` (string, optional): The command that asserts the post-migration invariant; a non-zero exit grades the migration FAIL. ### workflow Fields diff --git a/boatstack/runtime.go b/boatstack/runtime.go index 6abf9b5..da16dde 100644 --- a/boatstack/runtime.go +++ b/boatstack/runtime.go @@ -44,6 +44,21 @@ type Project struct { Context []string `json:"context,omitempty"` Commands map[string]string `json:"commands"` HighRiskPaths []string `json:"high_risk_paths,omitempty"` + Migration MigrationConfig `json:"migration,omitempty"` +} + +// MigrationConfig declares how a project APPLIES and VERIFIES its migrations against +// a disposable database, so their EFFECT can be graded by executing them +// (GradeMigrationEffect) rather than approximated from their SQL text. This is the +// Sandboxed-Effect law: the guard treats a committed migration as data (the +// data-artifact exemption), and the deploy pipeline — modelled here by a disposable +// database and these commands — is the controlled executor that observes the real +// effect. Both commands run via `sh -c` with the disposable database coordinate in +// the environment as BOATSTACK_MIGRATE_DB. When apply_command is absent, grading is +// skipped, so a repository without a database is unaffected. +type MigrationConfig struct { + ApplyCommand string `json:"apply_command,omitempty"` + VerifyCommand string `json:"verify_command,omitempty"` } type Workflow struct { diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index e052b36..0a657f7 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 [`fbb9ecffc548440fadb5d32339115bd20166a29f`](https://github.com/operatorstack/intelligence-flow/tree/fbb9ecffc548440fadb5d32339115bd20166a29f/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 [`2364eaedaefbe73d8996108dfac261c38b0ce2b3`](https://github.com/operatorstack/intelligence-flow/tree/2364eaedaefbe73d8996108dfac261c38b0ce2b3/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 9c9bdd6..bb59722 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "fbb9ecffc548440fadb5d32339115bd20166a29f", + "source_commit": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" }, { "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:fbb9ecffc548440fadb5d32339115bd20166a29f" + "last_verified_version": "source:2364eaedaefbe73d8996108dfac261c38b0ce2b3" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 2fd0b3c..5123418 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": "fbb9ecffc548440fadb5d32339115bd20166a29f", + "source_commit": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", "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-27-sandboxed-migration-grading.md b/release-notes/2026-07-27-sandboxed-migration-grading.md new file mode 100644 index 0000000..0e04d49 --- /dev/null +++ b/release-notes/2026-07-27-sandboxed-migration-grading.md @@ -0,0 +1,17 @@ +### Migrations are graded by running them, not by reading them + +The guard treats a committed migration as data. A migration file, however destructive its SQL looks, +does not change any database by sitting in a diff; the deploy pipeline applies it later. So the guard +allows it, which is why committing a migration no longer blocks work. The open question was how to catch +a migration that is genuinely unsafe, since its text alone cannot tell you. + +This release adds effect grading. A project can declare how its migrations are applied and verified. The +grader runs those commands against a fresh, disposable database, then reads the result: the migration +passes only if it applies and the verification holds, and fails otherwise. The database is provisioned +per run and torn down after, so the effect is real but contained. A project that declares no such +commands is unaffected — grading is skipped. + +This puts the judgment where it belongs. The static guard never guesses a migration's effect from its +text; the effect is observed by executing it in a sandbox, the same way the deploy pipeline would. A +safe forward change grades clean, and a change that drops a populated table is caught — the exact case +the text-only guard cannot and should not decide.