Skip to content

fix(migration): require destructive-scope approval only when stored rows can be deleted - #56

Merged
sskyy merged 1 commit into
mainfrom
fix/migration-destructive-scope-applicability
Oct 10, 2026
Merged

sskyy merged 1 commit into
mainfrom
fix/migration-destructive-scope-applicability

Conversation

@sskyy

@sskyy sskyy commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Summary

A migration rebuild item needs an approved destructive-scope decision only when it can delete stored rows. All three conditions must hold:

  1. it rebuilds output (rebuildOutput);
  2. its output can delete records (entity/relation output, or the _isDeleted_ hard-deletion property);
  3. the record whose rows would be deleted exists in the source schema. An unknown source schema counts as existing.

On v4.11.1, six readers re-implemented different subsets of this rule. As a result:

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:
    • the simulated (cascade-aware) scope estimate, including the zero-entry rule;
    • the analytic fallback scope estimate (getDestructiveDeletionScope);
    • the migrate-time entry check;
    • getRecomputeBlockingChanges (a fifth reader not listed in the issue);
    • the MigrationScheduler hard-deletion guard;
    • the execution-time deletion audit.
  • The migrate-time check now treats an approved empty scope with no actual entry as matching. assertExecutedDeletionsApproved already did; the two checks previously disagreed. Approving non-empty ids for a deletion that will not run is still a mismatch.
  • MigrationScheduler.run never runs the output recompute for a plan item with rebuildOutput=false. A state-only decision on a computation without bound state (for example a Custom _isDeleted_) produced rebuildState=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:

    • new mutation kinds hardDeletionNewHost (a new host filled by a Transform; its _isDeleted_ deletes some of the migration-created rows) and hardDeletionStateOnly (approval either omitted or approved with empty ids);
    • new oracle 0b: a destructive scope may only name a record type that exists in the v1 schema.

    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 decision changed. 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 the state-only approval forms.

Validation (Node 22, matching CI)

  • npx vitest run: 237 files and 2817 tests passed (71 env-gated skips).
  • npm run test:postgres against local PostgreSQL 17: 11 files and 53 tests passed.
  • migrationDestructiveFuzz default 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.
  • migrationGenerativeFuzz seeds 100–159 passed except seed 148.
  • Seeds 150 and 148 are both 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 check and npm run build pass.

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

…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>
@sskyy
sskyy merged commit 613e0fe into main Oct 10, 2026
2 checks passed
@sskyy
sskyy deleted the fix/migration-destructive-scope-applicability branch October 10, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant