Skip to content

fix(scratch-vm): repair missing variable and broadcast definitions on project load#555

Merged
cwillisf merged 4 commits intohotfix/scratch-blocks-release-triagefrom
fix/load-time-missing-definitions-repair
Apr 30, 2026
Merged

fix(scratch-vm): repair missing variable and broadcast definitions on project load#555
cwillisf merged 4 commits intohotfix/scratch-blocks-release-triagefrom
fix/load-time-missing-definitions-repair

Conversation

@cwillisf
Copy link
Copy Markdown
Contributor

@cwillisf cwillisf commented Apr 30, 2026

Resolves

Companion to #554. That PR stops new occurrences of the missing-definitions bug (backpack paste / sprite import leaving variable, list, or broadcast references that point at ids not defined anywhere in the project). This PR repairs already-corrupted projects on next load, so a Scratcher whose project hit the bug between the unfork release and #554's deploy gets a clean workspace the next time they open it.

Proposed Changes

Target gets a new public method reconcileVariableReferences that runs only the missing-definitions phase of fixUpVariableReferences: walk every variable, list, and broadcast reference; for each id not found anywhere, look up by name and type on the stage and either remap the field id to an existing global or create a fresh one. When two dangling references in the same target share an original name+type and the name has to be bumped, they coalesce to one stage variable rather than producing two with similar names. When a previous target's pass on the same load created a stage variable with a bumped name, later targets referencing the same id by lookup have their displayed names normalized to match.

fixUpVariableReferences now delegates the repair phase to the new helper, keeping its disambiguation phases (rename sprite-local that name-collides with a stage global, rename unreferenced colliding locals) for the sprite-import and backpack-paste callers.

VirtualMachine.installTargets now calls reconcileVariableReferences on every target during whole-project load (wholeProject=true). Idempotent on clean projects; self-heals corrupted ones on next open.

A log.warn fires whenever reconciliation modifies project state: when it creates a definition on the stage, remaps a reference to an existing global, coalesces two same-name dangling references, or normalizes a stale displayed name on a block field. Quiet on clean loads. Useful for surfacing the repair in dev tools and telemetry.

Reason for Changes

The repair-only path matters because running the full fixUpVariableReferences on every loaded sprite would also rename any sprite-local variable whose name and type match a stage global. That configuration has been a valid Scratch setup since forever; renaming such variables on project load would be a far worse regression than the bug we're fixing. Splitting the function lets the load path stay conservative while keeping the import path's disambiguation behavior intact.

A reference whose id is missing but whose name+type matches an unrelated stage variable will be remapped to that stage variable rather than create a new one. Same trade-off as sprite import: we can't tell from the saved field whether the original was meant to be a different variable. Flagging this as a known corner case rather than working around it.

Test Coverage

  • packages/scratch-vm/test/unit/engine_target.js: new Tap tests covering the reconciliation cases for variables, lists, and broadcasts (creation, same-name remap, idempotency); the critical regression that local-vs-global name collisions are NOT renamed; the stage-as-this case; cross-target displayed-name normalization after a bump; same-original-name coalescing within a target; and log.warn assertions (fires on create, fires on remap, silent on clean references).
  • packages/scratch-vm/test/unit/virtual-machine.js: new Tap tests for the installTargets wiring on whole-project loads. One covers repair of dangling variable + broadcast references; one is a regression guard that legitimate local-vs-global name collisions survive a whole-project load unchanged.
  • npm test --workspace=packages/scratch-vm passes (3761 / 3761).
  • npm run build and npm test from the repo root pass on every workspace.

… load

Projects saved while affected by the missing-definitions bug carry
dangling variable, list, or broadcast references that point at ids
not defined anywhere in the project. PR #554 stops new occurrences
but doesn't repair already-saved projects, because installTargets
skips reconciliation when wholeProject=true.

Extracts a new public method `Target.reconcileVariableReferences`
that runs only the missing-definitions phase of
`fixUpVariableReferences`: walk every variable / list / broadcast
reference; for each id not found anywhere, look up by name and type
on the stage and either remap to an existing global or create a
fresh one. The disambiguation phases of `fixUpVariableReferences`
(rename sprite-local that name-collides with a stage global, rename
unreferenced colliding locals) are left in `fixUpVariableReferences`
itself, which now delegates the repair phase to the new helper.
This separation matters because running the rename branches on
clean projects would rename legitimately-existing local-vs-global
name collisions, which has been a valid Scratch configuration since
forever.

`installTargets` now calls `reconcileVariableReferences` on every
target during whole-project load, so corrupted projects self-heal
on next open. Logs a warning per definition created or remapped so
the repair surfaces in dev tools and in telemetry. Quiet on clean
loads.

Refs #533
@cwillisf cwillisf requested a review from a team as a code owner April 30, 2026 17:44
@cwillisf cwillisf requested a review from Copilot April 30, 2026 17:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a conservative “self-heal on load” path in scratch-vm to repair historically corrupted projects that contain variable/list/broadcast block fields referencing IDs that aren’t defined anywhere in the project, without renaming legitimate local-vs-global name collisions.

Changes:

  • Introduce Target.reconcileVariableReferences() to repair missing variable/list/broadcast definitions (create on stage or remap by same name/type) without renaming.
  • Refactor Target.fixUpVariableReferences() to call reconciliation first, then apply import/backpack-only disambiguation renames for sprite-local collisions.
  • Wire whole-project loading (VirtualMachine.installTargets(..., wholeProject=true)) to reconcile every target on load; add unit coverage for the new behavior and regression guards.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/scratch-vm/src/engine/target.js Splits reconciliation from renaming; adds logging on create/remap and updates import logic to reuse the helper.
packages/scratch-vm/src/virtual-machine.js Runs reconciliation across all targets during whole-project load to repair corrupted projects without renames.
packages/scratch-vm/test/unit/engine_target.js Adds unit tests for reconciliation behavior and log.warn emission/silence cases.
packages/scratch-vm/test/unit/virtual-machine.js Adds integration tests to ensure whole-project loads reconcile dangling refs and don’t rename valid local-vs-global collisions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/scratch-vm/src/engine/target.js Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

Test Results

    4 files  ± 0    988 suites  +15   13m 22s ⏱️ +40s
2 455 tests +60  2 447 ✅ +60   8 💤 ±0  0 ❌ ±0 
5 856 runs  +93  5 818 ✅ +93  38 💤 ±0  0 ❌ ±0 

Results for commit 560626b. ± Comparison against base commit f687340.

♻️ This comment has been updated with latest results.

When two dangling references in one project share an original name and
type and the name has to be bumped (because some other target already
owns that name), the second reference must remap to the stage variable
the first reference created rather than create a second variable. A
Scratcher who pasted scripts referencing what they called the same
name almost certainly meant one variable.

Tracks created stage variables by their original (pre-bump) name+type
within the reconciliation pass and remaps subsequent same-name dangling
references to the earlier-created variable. Also pushes the fresh name
into the cached name list so unusedName accounts for it on later calls.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a project-load-time repair path in scratch-vm to self-heal projects that already contain dangling variable/list/broadcast references (missing definitions), without triggering the import-time renaming/disambiguation behavior that would be a regression for valid local-vs-global name collisions.

Changes:

  • Introduces Target.reconcileVariableReferences() to repair missing variable/list/broadcast definitions (create/remap on stage) without renaming existing variables.
  • Refactors Target.fixUpVariableReferences() to call reconcileVariableReferences() first, then run the existing import-time local/global disambiguation renames.
  • Wires VirtualMachine.installTargets(..., wholeProject=true) to run reconciliation across all targets on full project load, and adds unit tests covering load behavior and regressions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/scratch-vm/src/engine/target.js Adds reconcileVariableReferences, refactors fixUpVariableReferences to delegate repair vs rename phases, and emits log.warn on repairs.
packages/scratch-vm/src/virtual-machine.js Runs reconciliation for every target during whole-project load to repair historical dangling refs.
packages/scratch-vm/test/unit/engine_target.js Adds unit tests for reconciliation behavior, idempotency, collision non-renaming, and logging behavior.
packages/scratch-vm/test/unit/virtual-machine.js Adds unit tests ensuring whole-project load repairs dangling refs and does not rename valid local-vs-global collisions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/scratch-vm/src/engine/target.js
…onciliation

When whole-project load reconciles every target in turn, an earlier target's
pass can create a stage variable with a bumped name. A later target with a
block referencing the same id by lookup would otherwise keep its original
displayed name, leaving the same variable rendered with different names in
different sprites' blocks.

In `reconcileVariableReferences`, when `lookupVariableById` succeeds, queue
a name update if the field's displayed value doesn't match the resolved
variable's current name. The existing `conflictNamesToReplace` pass applies
the update.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a conservative “repair-only” reconciliation pass for variable/list/broadcast references during whole-project load, allowing historically corrupted projects (dangling ids) to self-heal on next open without renaming legitimate local-vs-global name collisions.

Changes:

  • Introduces Target.reconcileVariableReferences() to repair missing variable/list/broadcast definitions and remap dangling ids to existing stage globals when possible.
  • Refactors Target.fixUpVariableReferences() to delegate the missing-definition phase to reconcileVariableReferences() while keeping import/paste disambiguation renames.
  • Updates VirtualMachine.installTargets() to run reconciliation across all targets when wholeProject=true, plus adds unit tests for the new helper and wiring.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/scratch-vm/src/engine/target.js Adds reconcileVariableReferences, updates JSDoc, and refactors fixUpVariableReferences to call reconcile first.
packages/scratch-vm/src/virtual-machine.js Runs reconcileVariableReferences() across targets during whole-project installs to repair dangling references on load.
packages/scratch-vm/test/unit/engine_target.js Adds unit tests for reconcileVariableReferences, including logging and regression guards.
packages/scratch-vm/test/unit/virtual-machine.js Adds tests ensuring whole-project installs repair dangling references without renaming valid local-vs-global collisions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/scratch-vm/test/unit/engine_target.js
Comment thread packages/scratch-vm/src/engine/target.js
Adds two Tap tests for the list-reference path through
reconcileVariableReferences (creation on stage and remap to an existing
same-name stage list), filling a coverage gap noted in review.

Reword the warning emitted when reconciliation only normalizes a stale
displayed name; the previous wording said "dangling reference" but in
that branch the id resolves to a real variable.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a conservative “repair on load” path in scratch-vm to self-heal projects which contain dangling variable/list/broadcast IDs (from historical import/backpack bugs) without renaming legitimate local-vs-global name collisions.

Changes:

  • Introduces Target.reconcileVariableReferences() to repair missing variable/list/broadcast definitions by remapping to existing stage globals or creating new stage definitions (no renames).
  • Refactors Target.fixUpVariableReferences() to call reconciliation first, then keep the import/backpack-specific rename/disambiguation behavior.
  • Wires whole-project loads (VirtualMachine.installTargets(..., wholeProject=true)) to reconcile every target on install; adds unit tests covering reconciliation behavior and installTargets wiring.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/scratch-vm/src/engine/target.js Adds reconciliation-only helper and refactors fix-up to delegate missing-definition repair.
packages/scratch-vm/src/virtual-machine.js Runs reconciliation across all targets during whole-project load.
packages/scratch-vm/test/unit/engine_target.js Adds unit tests for reconciliation cases and log.warn behavior.
packages/scratch-vm/test/unit/virtual-machine.js Adds unit tests ensuring whole-project load repairs dangling refs without renaming clean collisions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/scratch-vm/src/engine/target.js
@cwillisf cwillisf merged commit b437181 into hotfix/scratch-blocks-release-triage Apr 30, 2026
17 checks passed
@cwillisf cwillisf deleted the fix/load-time-missing-definitions-repair branch April 30, 2026 20:12
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants