fix(state): evaluate a re-set state against its own generation - #266
Merged
Merged
Conversation
Part of #258. Re-setting a state object left two mechanisms pointing at the previous generation. - Cache entries carry the generation they were written in, and a read only hits an entry from the current generation. A wildcard-free getter's absolute address is the same object across generations, so the old value used to come back with dirty:false. The generation advances after the previous generation is torn down and before the new state object is installed. - The path registrations of live bindings are re-derived after the setter's clears, so listPaths, elementPaths and the static chains are rebuilt for the new generation. Clearing pathSet stays: it is the self-heal that restores the static edges the recursion teardown removes. On main a whole-list write after a re-set threw while the DOM moved on regardless. The known-defect pin for 欠陥8 now asserts the fix. Two symptoms stay open and are pinned as known holes: a re-set does not re-apply any binding, so reads move to the new generation while the DOM does not, and a re-set on a mount volume is still a silent no-op. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2 of #258. Round 1 wrote "a re-set that throws leaves the element in the previous generation" as a general rule and pinned one validator out of several; measurement showed some validators behave the other way. - $listKeys reads nothing but the incoming value, so it moved ahead of the generation bump. $streams and $watch measurably need what the setter installs and cannot move; that is now stated as what it is, with a test per validator instead of one test and a general claim. - The rebuild exclusion accumulates across generations. It was taken from the previous teardown alone, so a second re-set with no read in between re-registered static edges for generated paths that were never materialized. - The volume re-set is pinned as a known hole rather than fixed, and the unmeasurable claim about re-diagnosing the second generation is replaced by the measured reason, itself pinned by a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 of #258, plus the two sentences its review found unpinned. - $on is a seventh declaration validator, validated after the generation advances; it now has tests like the other six, and the counts in the setter comment match the code. - The prose describing what a late validator leaves behind was wrong for $streams and is gone. Three tests pin the ledger state after a throw from each distinct position, and whether a later whole-list write succeeds. - The $watch gain and the $listKeys move are pinned rather than only described, and the volume known-hole comment no longer argues about a design that was never built. - The path-registration ledger's note no longer claims data and display diverge without it; measurement shows both follow while the write keeps throwing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 12, 2026
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.
Part of #258. It fixes two of the three symptoms and pins the rest.
What was wrong
Re-setting a state object left machinery pointing at the previous generation, because the things it is keyed by are identical across generations.
X10, the cache. A wildcard-free getter's absolute address is the same object before and after a re-set, so the old value came back with
dirty:false.mount({items:[1,2], get sum(){…}})read 3, was re-set to{items:[5,6]}, and still read 3.X7's internal half, the ledgers. The setter clears
pathSet,listPathsandelementPaths, and the recursion teardown removes the edges of the previous generation's generated accessors. Nothing rebuilt them, so after a re-set a whole-list write threwCannot expand dynamic dependency with wildcard for non-list addresswhile the DOM moved on regardless.What this does
pathSetstays: it is the self-heal that restores the static edges the recursion teardown removes, and keeping it was measured to matter.$watchnow reaches its handler after a re-set in a shape where the write used to throw before getting there.The generation contract, as measured
Seven
$declaration validators run in the setter. Four are pure validation and run before the generation advances, so a throw there leaves the element wholly in the previous generation. Three need what the setter installs and run after it, so a throw there leaves the new state in place with the generation already advanced. What each one leaves behind differs, and rather than describe it in prose the tests pin it: one test per validator, plus three that assert the ledger state after the throw and whether a later whole-list write succeeds.That split is the result of three rounds of getting it wrong. Round 1 wrote the rule as universal and pinned one validator. Round 2 corrected that, said there were six validators and that the late ones landed alike; there are seven, and they do not. Round 3 removed the prose and let the tests be the enumeration.
Still open, pinned as known holes
forrows keep the first generation's text. This is the author-visible half of X7.<wcs-state mount="…">volume is a silent no-op for the page. The graft is a one-time copy, so the root tree and the DOM stay on the first generation with no error and no warning.Both open symptoms are pinned with
DEFECT:comments inintegration.stateGenerationReset.test.tsand will be split into their own issues.Verification
Every behavioural claim was measured on this branch and on
mainwith the same probe, and the numbers are in the commit messages and the test titles.npm run test:coveragenpm run lintnpx tsc --noEmitThe known-defect pin for 欠陥8 now asserts the fix rather than the stale read.
🤖 Generated with Claude Code