feat(state): declare accumulation over time with $scan (#272) - #275
Merged
Merged
Conversation
$streams folds within one run and $watch owns no value, so an accumulation that outlives a run had nowhere to live. $scan declares it: an output the runtime owns, fed by a state path (from) or an event token (on), with resetOn and one fold per landing. - from folds once per batch hit, before $watch, sharing the watch runtime's active set, chain depth, prev ledger and error isolation - on folds once per event, synchronously, before the token's $on - getter sources, self or cyclic references and malformed shapes raise at declaration, before the generation advances - a stream whose args derive from its own scan output raises (wcs/scan-feedback-loop); a chunk sharing a batch with its stream's restart is not folded (restart wins) Design record: docs/state-scan-design.md (Issue #272). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vscode-wcs and @wcstack/lint now check $scan with the runtime's code vocabulary, and devtools accepts the new protocol values. - wcs/scan-declaration-invalid (error): shapes the runtime rejects - wcs/scan-source-computed (error): from / resetOn on a getter - wcs/scan-path-missing (warning): from / resetOn paths that do not exist - scan outputs materialize as path candidates, like $streams values - the defineState preamble types $scan; devtools accepts phase "fold" and path-unresolved source "scan" Refs #272 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
examples/state-intersect-scroll now accumulates its feed with $scan instead of concatenating inside a $watch handler. The $watch keeps the error-time scrollY record and re-arms the sentinel after a full page; the fold keeps a page key, because the runtime folds once per landing, not once per page. The six e2e specs pass unchanged against the local dist, and wcs-validate reports the same diagnostics before and after. - packages/state/docs/scan(.ja).md: reference for from / on, resetOn, firing order, the fold contract and the lifecycle - README (en/ja): Scan section, mechanism order with $scan, and the rendering-semantics note that pointed at the old $watch commit - timing contract, streams / watch / event-lane design notes: cross references; the design record gains the measured results - scan.streamCommit.test.ts pins the example's new feed boundary - CHANGELOG: $scan, the lint codes and the devtools protocol values Refs #272 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of the $scan implementation found two ways it broke the once-per-landing contract. - A scan whose from is another scan's output folded the value that scan had just written in the same drain, then folded it again in the next batch, skipping the value that had landed ([3, 6, 6] instead of [1, 3, 6] when the upstream scan is declared first). The drain now plans every scan's next value by reading only, then writes them in declaration order. - resetOn on an on-scan took effect only at the end of the drain, so an event that came after the write was wiped, including one an element dispatched synchronously while the write was applied to its binding. The updater's enqueue now marks a pending reset (scan/eventReset.ts); the next event folds into initial, and the drain resets only if the mark is still there. Also: - a resetOn path under the entry's own from raises (the reset would win every time); an ancestor of from stays allowed - the chain-depth-limit message says $scan folds were skipped too - failures are reported as threw / returned-promise / write - recordWatchPrevValue is renamed recordDeclaredPrevValue - the scan tests share writeState / readState / flushTimes Refs #272 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- wcs/scan-declaration-invalid also covers an output name or a from / resetOn path inherited from Object.prototype, and a resetOn path under the entry's own from - the output-conflict message names setters, which were already caught - the preamble types fold with this: void, so a method-form fold that reads this is a type error (the runtime calls it without this) - findNonObjectDeclaration's local is renamed to declarationProp Refs #272 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- design record: D6 revised (pending reset for on-scans), D18 (plan, then write), the resetOn-under-from raise, and a review table in section 5-4 with the measurements taken before the fix - scan(.ja).md and README (en/ja): resetOn semantics per source, the ancestor pattern, and chained scans folding once per landing - CHANGELOG: the $scan entry reflects both Refs #272 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Isolate a row whose read fails instead of dropping the whole fold, and fold each list position once when a replacement lands in the same job - Report read, fold and write failures as separate devtools phases - Keep an on-scan's pending reset until its write succeeds, carry it across a re-set while the write is still queued, and discard it only for a batch that will not fire - Count the drain and enqueue gates by entry into the active set, so a disconnected element closes them - Follow chained scans in the feedback-loop fence, and raise for a recursive getter or a setter without a getter used as a source - Raise when an output name clashes with a method; accept a function output on re-set only when the runtime placed it there - Place a copy of a plain initial on materialize and reset, so a child write never changes the declared value; compare by value so a no-op reset writes nothing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vscode-wcs: - Report cycles between scan outputs, "**" as wcs/recursion-unsupported, non-string sources, an empty output name, an array declaration or entry, a method clash, and a setter without a getter as a source - Match $recursion expansions and recursive getters the way $watch does - Error for $scan in a mount= volume and warn in a bind-component - Decide bind-component from parsed attribute names instead of a regex over the tag (this also fixes the $recursion check) - Fold same-key declarations in evaluation order, last one wins - Type $scan and $streams outputs on `this` without widening properties that are already declared - Stop treating a same-named event token as an existing path lint: regenerate dist/cli.cjs. devtools: add the "write" phase and widen the watch-error title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Design doc: revise the decisions the fixes changed and add sections 5-5 to 5-9 (findings, measurements, the reverted reordering, and the row-landing defect handed off to #274) - scan.md and README (en, ja): the prev recording rule, resetOn object paths, re-set limits, initial copying, the lifecycle table and the declaration-time error codes - Align the mechanism order in the watch and view-transition design docs and the prev wording in the watch example - Note that the intersect-scroll example needs the next release - CHANGELOG entries Co-Authored-By: Claude Opus 5 (1M context) <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.
Closes #272.
What this adds
$scandeclares a value that accumulates over time — the one kind of accumulation that previously fell back to hand-written$watchhandlers.from(a state path) folds once per landing, inside the drain and before$watch. Wildcard paths fold per row.on(an event token) folds synchronously once per event, before$onfor the same token.resetOnreturns the output toinitialwhen a listed path is written.foldgets nothis. Returning the same reference writes nothing. A throw or a thenable is isolated and reported with a phase (evaluate/fold/write).**, a clash with a method, and a stream whose args reach the scan's output (wcs/scan-feedback-loop).vscode-wcs, and through it
wcs-lint, checks the declarations statically, and devtools shows scan failures by phase.examples/state-intersect-scrollnow builds its feed with$scan.docs/state-scan-design.md(D1–D18, §5).packages/state/docs/scan.md(andscan.ja.md).The quality pass (last three commits)
After the implementation and a first review, five more review cycles ran. Each cycle started a fresh reviewer, and the same reviewer re-verified every fix. Together they processed 74 findings. The notable fixes:
onscans keep their pending resets consistent withfromscans across a failed write, the chain limit, a disconnect, and a re-set while the write is still queued.initial. Plain data ininitialis copied when it is placed on the output and on reset, so writing a child path never changes the declaration.**, empty names, arrays, method clashes, setter-only sources,$recursionexpansions, and$scaninside volumes.Decisions recorded in §5-6 to §5-9:
$scanwrite →$watch. Writing after$watchwas tried and reverted. It lost handler writes to child paths and needed a hook in the write path. D12 now documents what the kept order means for$watch.<output>when the source is written again before the output lands:prevcan carry a value, and the handler can fire again with the same value.initialthat are not plain data stay shared by reference (D7).$watch.Known follow-ups
DEFECT(#274)tests; the root cause and a proposed fix are in the issue.<wcs-state>drops$onsubscriptions, andonscans share the problem (DEFECT(#273)).examples/state-intersect-scrollloads@wcstack/statefrom the CDN, so it needs the release that contains$scan. The README says how to run it from the working tree until then.$scan.Verification
npm run test:coveragesrc/scan100%npm run lint,npx tsc --noEmitnpx vitest run,npx tsc --noEmitnpx vitest run,npm run lint,npx tsc --noEmitdist/cli.cjse2e runs in CI.
🤖 Generated with Claude Code