Repository navigation
fix(migration): require destructive-scope approval only when stored rows can be deleted - #56
Merged
Conversation
…ows can be deleted A rebuild item needs an approved destructive-scope decision only when it rebuilds output, its output can delete records (entity/relation output or the _isDeleted_ hard-deletion property), and the record whose rows would be deleted exists in the source schema. Six readers re-implemented different subsets of this rule, so a migration that creates the host table of a HardDeletionProperty demanded an empty approval (refused without it), and a state-only _isDeleted_ decision was refused both with and without approval. - Add getDestructiveScopeRecordName as the single applicability decision and route the simulated and analytic scope estimates, the migrate-time check, getRecomputeBlockingChanges, the MigrationScheduler hard-deletion guard and the execution-time deletion audit through it. - Treat an approved empty scope without an actual entry as matching at the migrate-time check, as the execution-time check already does. - Never run the output recompute for a plan item with rebuildOutput=false. A state-only decision on a computation without bound state previously fell through to a full recompute; with the corrected guard it would have deleted stored rows without approval. Tests: a 13-cell regression matrix (7 red on v4.11.1), two new mutation kinds and a scope-necessity oracle in migrationDestructiveFuzz, a dimension registry row, and an escape-analysis retrospective. Closes #55 Closes #54 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A migration rebuild item needs an approved
destructive-scopedecision only when it can delete stored rows. All three conditions must hold:rebuildOutput);_isDeleted_hard-deletion property);On v4.11.1, six readers re-implemented different subsets of this rule. As a result:
HardDeletionPropertydemanded a destructive-scope approval withids: []. Without it, the migration was refused for bothchangedandstate-onlydecisions (issue Migration requires destructive-scope approval for a HardDeletionProperty whose host table the same migration creates #55).state-onlydecision on an_isDeleted_computation could not be applied on any host. Without an approval, the scheduler guard refused it. With the diff's empty scope approved, the migrate-time check refused it as "approved but not executed" (issue Migration apply refuses a state-only HardDeletionProperty on an empty new host without destructive scope #54 and the cell beyond it).Closes #55. Closes #54. Supersedes PR #53, which fixes only the scheduler guard: the diff and the migrate-time check still demanded an approval for a new host.
Changes
getDestructiveScopeRecordName(new, exported) is the single applicability decision. Every reader now uses it:getDestructiveDeletionScope);getRecomputeBlockingChanges(a fifth reader not listed in the issue);MigrationSchedulerhard-deletion guard;assertExecutedDeletionsApprovedalready did; the two checks previously disagreed. Approving non-empty ids for a deletion that will not run is still a mismatch.MigrationScheduler.runnever runs the output recompute for a plan item withrebuildOutput=false. Astate-onlydecision on a computation without bound state (for example aCustom_isDeleted_) producedrebuildState=false, rebuildOutput=false. Such an item skipped the state-reset branch and fell through to a full output recompute. Before this change the hard-deletion guard refused it; with the corrected guard alone it would have deleted stored rows without approval. A dedicated test pins this cell.The execution-time audit (
assertExecutedDeletionsApproved) still rolls back any deletion of stored rows that was not approved. Its scope is now explicitly limited to record types that existed before the migration. Rows of a record type introduced by the same migration are created by the migration itself. Before this change, a new host filled by a Transform and then partly hard-deleted could never be approved: the rolled-back simulation and the real run allocate different ids.Tests
tests/runtime/migrationDestructiveScopeApplicability.spec.ts: a 13-cell matrix covering host existence × decision × approval × scope-estimation path × output kind. All five rows of the issue table are included. 7 cells fail on v4.11.1; the other 6 pin the preserved behaviour (existing host with stored rows: refused without approval or with the empty estimate; applied with the exact id).tests/runtime/migrationDestructiveFuzz.spec.ts:hardDeletionNewHost(a new host filled by a Transform; its_isDeleted_deletes some of the migration-created rows) andhardDeletionStateOnly(approval either omitted or approved with empty ids);On v4.11.1, every default-pool seed of both new kinds fails (4, 30, 36, 41, 43, 44). The menu grew from 9 to 11 kinds, so the default-pool mapping was re-derived; it still covers all 11 kinds.
tests/runtime/WritingComputationTests.md: new dimension registry row.agentspace/output/issue-55-destructive-scope-applicability-retrospective.md: escape analysis. The generator only added_isDeleted_to existing hosts with decisionchanged. The approval helper approves every requirement, and no oracle checked whether a demanded approval was necessary.agent/skill/interaqt-migration.md: documents the applicability rule and thestate-onlyapproval forms.Validation (Node 22, matching CI)
npx vitest run: 237 files and 2817 tests passed (71 env-gated skips).npm run test:postgresagainst local PostgreSQL 17: 11 files and 53 tests passed.migrationDestructiveFuzzdefault pool 1–44 passed, and the expanded pool 45–151 (full coverage of each mutation variant with and without kill-resume) passed except seed 150.migrationGenerativeFuzzseeds 100–159 passed except seed 148.takeover/generative kill-resume cases that fail identically on unmodified v4.11.1: "Migration is already running" after the injected crash, a lock-release issue. They are outside the nightly 1–60 pools, unrelated to this change, and tracked separately.npm run check:all,npm run checkandnpm run buildpass.Known limitation (unchanged)
The diff is generated before the computation decisions are made, so it estimates scopes and event-rebuild-handler requirements as if every changed computation rebuilds output. For an existing host approved as
state-only, omit the estimated scope or approve it with empty ids. Removing this would need conditional requirements in the approval file format.🤖 Generated with Claude Code