Dirty-lifecycle correctness: guarded dirty set, undo phantom sweep, animation split - #758
Conversation
The panel's DIRTY readout filters to live nodes, so scripted matrix runs could not see phantom marks (deleted-node ids) or distinguish stuck kinds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…nimation split Three stuck-mark classes kept scenes from ever settling to DIRTY 0 (charter findings 7/8): - ~120 call sites add to dirtyNodes directly, bypassing markDirty's consumer-kind guard — a wall's parentId is a level, so unconsumable level/building marks wedged forever. dirtyNodes is now a GuardedDirtySet whose add() applies the guard itself. - Undo/redo rewrites nodes without the delete actions, leaving marks for nodes that no longer exist (rich-2x: 47 phantoms after one scripted run). The temporal subscriber now sweeps marks whose node is gone. - Door/window animation systems marked dirty every tween tick, so DIRTY 0 was unreachable while anything animated. A dirty mark is one-shot work: DoorSystem rebuilds doors straight off doorAnimations entries, window types without a direct pose path use a transient rebuild set, and only the settled pose gets a final one-shot mark. The ?perf settle detector counts the raw set again (its live-only filter papered over the phantoms it now must catch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…ords, not markDirty per tick The 'system advances animation then calls markDirty' recipe was the exact pattern behind charter finding 8; the dirtyTracking section now describes the GuardedDirtySet enforcement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Unqualified, the aside could wave through an animation loop that marks dirty every frame — the finding-8 class the GuardedDirtySet cannot block, since animating kinds are legitimately consumable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
|
I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…). Please try again, rephrase, or reach out if it keeps failing. Error id: b9ac14df-209c-41bc-b454-bc690c3583a2 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7c2fe83. Configure here.
| if (nodes[id]?.type === 'window' && !dirtyWindowIds.includes(id)) dirtyWindowIds.push(id) | ||
| } | ||
| pendingWindowAnimationRebuilds.clear() | ||
| } |
There was a problem hiding this comment.
Pending window rebuilds dropped under load
Medium Severity
pendingWindowAnimationRebuilds is cleared before the rebuild loop, so a progressive cap or time budget can drop animation-only windows that are not already in dirtyNodes. Those ids are gone until the next animation tick, so mid-tween poses can skip frames when many windows are already dirty. Doors avoid this by keeping doorAnimations until the work actually runs.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7c2fe83. Configure here.


Metric (protocol step 1): every fixture settles to DIRTY 0 after every scripted action (charter backlog #1, findings 7/8).
Three mechanisms, three fixes
dirtyNodes.add()directly, skippingmarkDirty's consumer-kind guard. A wall'sparentIdis a level (dirtyTracking: false, no consumer), so blind parent-marks wedged forever ("1 level" dirty on the tower fixture). →dirtyNodesis now aGuardedDirtySet: the guard lives inadd()itself, covering every call site with zero call-site churn.markDirtykeeps its own guard (tests inject plain Sets).DoorSystemrebuilds doors straight offdoorAnimationsentries, window types without a direct pose path use a transientpendingWindowAnimationRebuildsset, and only the settled pose gets a final one-shot mark. Skylight/elevator already pose directly — untouched.The
?perfsettle detector counts the raw set again (its live-only filter papered over exactly the leaks it must catch), and__pascalPerf.dirtyResidue()exposes the raw census (total / phantom / live-by-kind) to the scaling-matrix runner.Before/after (protocol step 2, quiet machine)
Stuck marks were defeating every consumer's empty-set early exit each frame — hence the idle gain on furnished/tall fixtures. Everything else within noise. JSONs linked from the charter evidence row (
plans/performance/editor-scalable-scene-runtime.mdin private-editor).Gates (protocol step 3)
perf-regression-pack5/5;click-batched-wall,click-hidden-wall,opening-placement-previewgreen.placement-preview-follows-cursorfails identically on the base build — pre-existing.Docs sweep (protocol step 5)
node-definitions.mdtaught the finding-8 pattern verbatim ("system advances animation, then calls markDirty") — corrected;dirtyTrackingsection now documents theGuardedDirtySetenforcement; the "markDirty per tick is fine" aside intools.md+ the review-architecture skill is scoped to bounded gestures, with per-animation-tick marks called out as a blocker.🤖 Generated with Claude Code
https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Note
Medium Risk
Touches core scene dirty semantics, undo/redo, and per-frame door/window rebuild paths—high churn but localized; behavior is heavily tested and perf-gated.
Overview
Fixes dirty-set lifecycle so perf settle can reach DIRTY 0 and stuck marks stop forcing full-frame rebuild work.
Scene store (
use-scene) replaces the plaindirtyNodesSetwithGuardedDirtySet:add()applies the same rules asmarkDirty(skipdirtyTracking: false, disabled plugin kinds; allow ids with no node yet).markDirtykeeps an explicit guard for tests that inject plain sets. After undo/redo, a temporal subscriber sweeps phantom marks for node ids that no longer exist.Door/window animation stops
markDirtyevery tween tick (that blocked settle).DoorSystemrebuilds doors with activedoorAnimationsentries even when the dirty set is empty;WindowAnimationSystemusespendingWindowAnimationRebuildsfor types without direct pose paths; both issue a one-shotmarkDirtyon completion.Perf:
PerfActionSettleSystemcounts the raw dirty set again;__pascalPerf.dirtyResidue()exposes total/phantom/live-by-kind for scripted runs.Tests + docs: three dirty-lifecycle unit tests; wiki/skill text clarifies bounded-gesture
markDirtyvs animation-loop marking.Reviewed by Cursor Bugbot for commit 7c2fe83. Bugbot is set up for automated code reviews on this repo. Configure here.