@wcstack/state 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
4
 
5
- **これは便利な既存FWの別実装ではありません。フロントエンド開発の前提を組み替える、別系譜の試みです。**
5
+ **これは便利な既存FWの別実装ではありません。パスがビューとモデルの契約になる——フロントエンドの外で確立した系譜を、Web標準の上に持ち込む試みです。**
6
6
 
7
7
  多くのライブラリは、UI・状態・コンポーネントの結合点を JavaScript の中に置きます。`@wcstack/state` はそこを選びません。仮想DOMも、コンパイルも、hook も、selector も前提にせず、HTML とパス文字列だけを契約として UI と状態を結びつけます。
8
8
 
@@ -32,7 +32,7 @@
32
32
  | JavaScript が描画の中心 | HTML と DOM が中心 |
33
33
  | state を取り出して component へ流し込む | path を宣言して DOM を状態へ接続する |
34
34
  | hook / selector / signal で購読する | 属性とパスで束縛する |
35
- | フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
35
+ | フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に reactive layer を足す。ページはページのまま |
36
36
 
37
37
  より近い親戚は **属性ディレクティブ型・ビルド不要のライブラリ** — Alpine.js や petite-vue の系統です。前提(素の HTML への属性・コンパイラ不要)は共有しつつ、選択を分ける違いが 2 つあります。
38
38
 
@@ -45,6 +45,23 @@
45
45
 
46
46
  この軸に乗せれば比較は具体的になります。下の[パフォーマンス](#パフォーマンス)節がその一例で、`e2e/bench/` のドライバで手元のハードウェアでも再現できます。
47
47
 
48
+ ### JavaScript の外にある系譜
49
+
50
+ 「*パス文字列だけがビューとモデルの契約である*」という前提は、フレームワークの時代より古く、その大半は JavaScript の外で作られました。JavaScript の内側での直系は、Knockout の `data-bind="text: user.name"`(バインディングを属性に載せる。ただし式を評価し、`ko.observable` のラップが要る)と、Polymer のパス体系(ドットパス、`items.*` オブザーバ、`this.set("users.0.name", v)`)です。後者が `set()` / `notifyPath()` を必須にしたのは、当時のプラットフォームでは素の代入を観測できなかったからでした。新規性を主張するより、先行系譜を名指しするほうが有益です。
51
+
52
+ | 系譜 | すでに持っていたもの | ここで異なる点 |
53
+ |---|---|---|
54
+ | **表計算**(VisiCalc, 1979) | アドレス、セルが**何であるか**を宣言する数式、依存グラフ、遅延再計算 — 更新コードはどこにもない | 格子座標ではなく名前を使い、数式をセル単位ではなく*形*単位で書く。`get "cart.items.*.subtotal"()` は各行へフィルダウンされるのではなく、ワイルドカードそのものが定義 |
55
+ | **Cocoa Bindings / KVC–KVO**(NeXT の EOF, 1994/Mac OS X 10.3, 2003) | キーパス(`person.address.street`)、「対象 + キーパス + value transformer」の三つ組、パスに沿って集計するコレクション演算子(`@sum.items.price`) | その三つ組が nib や `bind:toObject:withKeyPath:` の呼び出しではなくマークアップにあるので、grep・lint・diff できる。変更検知は KVC 準拠ではなく素のオブジェクトへの ES Proxy |
56
+ | **XForms**(W3C 勧告, 2003) | model / instance / view の分離、instance を指す `ref` パス、そして `<bind calculate="…">` — **パスの位置に**宣言される算出値であり、パス getter の直系の祖先 | パスはアドレスに徹し、計算は属性内の XPath ではなく state 上の JavaScript getter。XForms プロセッサなしで素のブラウザで動く |
57
+ | **WPF / XAML**(2006) | `{Binding Path=User.Name, Mode=TwoWay}`、部分木ごと基点を張り替える `DataContext`、ソースへの書き戻し時点を選ぶ `UpdateSourceTrigger`、両端を変換する `IValueConverter` | ビジュアルツリーを継承するコンテキストと `RelativeSource` / `ElementName` の脱出口ではなく、ルートごとに 1 本の state ツリー。`state: user` がその基点張り替えで、しかもホストの HTML に書かれる。変換器は登録するクラスではなく 46 個の閉じたフィルタで、コンパイルも不要 |
58
+ | **Android Data Binding**(2015) | レイアウトファイル自体に書くパス — `android:text="@{user.name}"`、双方向は `@={}` | ビルド手順も生成されるバインディングクラスもなく、属性の中に式を書かない |
59
+ | **SCADA / HMI のタグバインド**(産業用・数十年) | 設定だけでウィジェットのプロパティをタグパス(`Line1/Tank/Level`)へ配線する。パスを媒介変数化する*間接*バインド(`Folder/Tag_{1}`)で 1 画面が多数の機器を駆動する | ツリーが持つのはスカラーのフラットな名前空間ではなく、派生値・リスト・マウントされたコンポーネント。媒介変数はドロップダウンから与えるものではなく、バインディングが置かれた行が解決するループのワイルドカード |
60
+
61
+ ワイルドカードは MQTT のトピックフィルタ(`sensor/+/temperature`)や OSC のアドレスパターン(`/synth/*/freq`)にも似ていますが、あちらが選ぶのは**流れているメッセージ**です。`items.*.price` が名指すのは state のアドレスで、同じ 1 本の文字列が購読先であり書き込み先でもあります。
62
+
63
+ この比較を経て本当に新しいと言えるのは狭い範囲です。**ワイルドカードパス getter** — *キー*がパスパターンである getter なので、定義 1 本が全行に効き、依存エッジはセル単位ではなくパターン単位で保持されます。それ以外は、上の系譜を、いずれもが前提にできなかった 3 つのもの(Custom Elements・ES Proxy・Import Maps)の上へ組み直したものです。
64
+
48
65
  ## 第一原理: パスが唯一の契約
49
66
 
50
67
  既存の多くのフレームワークでは、**コンポーネント**がUIと状態の結合点になっています。状態ストアを外部に切り出しても、コンポーネント内にフックやセレクタ、リアクティブプリミティブといった**状態を引き込むためのコード**が必ず必要になります。つまり、UIと状態は常にJavaScriptの中で結びついているのです。
@@ -55,23 +72,23 @@
55
72
  |----------|----------------|--------------|
56
73
  | **状態** (`<wcs-state>`) | データ構造とビジネスロジック | どのDOM要素がバインドされているか |
57
74
  | **UI** (`data-wcs`) | パス文字列と表示意図 | 状態がどう保存・算出されているか |
58
- | **コンポーネント** (`state: path`) | ホストが書くマウント表 | 他コンポーネントの内部実装 |
75
+ | **コンポーネント** (`state: path`) | ホストが書くマウント表 | 誰がマウントしたのか、ツリーの他の部分に何があるのか |
59
76
 
60
77
  3つのレベルのパス契約が疎結合を実現しています:
61
78
 
62
- 1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネントのJavaScriptには、状態を参照するコードが**一行も**必要ありません。
79
+ 1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネント側のコードがリアクティブプリミティブを import することも、購読を登録することもありません。`bind-component` するクラスは素の `state` オブジェクトを自分で宣言し、素のオブジェクトとして読み書きします。存在しないのは、状態をコンポーネントへ引き込む glue コードのほうです。
63
80
 
64
- 2. **コンポーネント ↔ コンポーネント** — ホストが各コンポーネントへ部分木をマウントし(`<my-card data-wcs="state: user">`)、ボリュームが追加モジュールをツリーに接ぎ木します(`<wcs-state mount="i18n">`)。コンポーネント同士がお互いを直接インポートしたり参照したりすることはありません。すべての接続は単一ツリー上のパス接頭辞だけです。
81
+ 2. **コンポーネント ↔ コンポーネント** — ホストが各コンポーネントへ部分木をマウントし(`<my-card data-wcs="state: user">`)、ボリュームが追加モジュールをツリーに接ぎ木します(`<wcs-state mount="i18n">`)。コンポーネント同士がお互いを import することはなく、丸ごとマウントは単一ツリー上のパス接頭辞そのものです。これより踏み込む宣言形式が 2 つあり、それぞれ定義箇所に明記しています。[個別対応付け](#ホスト側の使用方法)(`state.message: user.name`)はホストがコンポーネント自身のキー名を書くことになり、[公開 getter](#公開-getterコンポーネントの-getter-を外から読む) はコンポーネントが計算した値をホストが読むので、そのバインディングはそこにコンポーネントがマウントされていることに依存します。
65
82
 
66
83
  3. **ループコンテキスト** — `for` ループ内では `*` が抽象インデックスとして機能します。`items.*.price` のようなバインディングは自動的に現在の要素へと解決されます。テンプレートは自身の具体的な位置(インデックス)を知る必要がなく、ワイルドカードがその契約となります。
67
84
 
68
85
  ### なぜこれが重要なのか
69
86
 
70
- これはUIと状態の完全な分離を、**JavaScriptのコードを介することなく**実現していることを意味します。つまり:
87
+ これはUIと状態の分離を、**JavaScriptのコードを介することなく**実現していることを意味します。つまり:
71
88
 
72
- - UIをすべて作り直しても、状態のロジックに触れる必要がありません。
89
+ - UIを作り直しても、状態のロジックに触れる必要がありません — ただしロジックが描画内容にぶら下がっていない範囲で。live binding は 3 つある[評価のきっかけ](#評価のきっかけdemand-root)の 1 つなので、表示専用のつもりの要素がページ唯一の購読になりえます。
73
90
  - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
74
- - HTMLを読むだけで、すべてのデータ依存関係を把握できます。
91
+ - HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$streams`・`$scan`)は、すべて state という 1 箇所に宣言されています。
75
92
 
76
93
  このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
77
94
 
@@ -101,29 +118,46 @@
101
118
 
102
119
  ## この原理から導かれる機能
103
120
 
104
- - **宣言的データバインディング** `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
105
- - **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
106
- - **構造ディレクティブ** `<template>` 要素による `for`, `if` / `elseif` / `else`
107
- - **ボリューム** — `<wcs-state mount="cart">` がモジュールを 1 本の state ツリーに接ぎ木し、バインディングは `cart.…` で読む
108
- - **行の同一性** `$listKeys` が再取得した配列でも行の DOM と行オブジェクトを保つ
109
- - **ワイルドカード集計** `$getAll` / `$setAll` が配列を作り直さずに `items.*.price` を横断して読み書きする
110
- - **組み込みフィルタ** フォーマット、比較、算術、日付など 46 種類
111
- - **双方向バインディング** `<input>`, `<select>`, `<textarea>` で自動有効
112
- - **Web Component バインディング** Shadow DOM コンポーネントとの双方向状態バインディング
113
- - **command token** pub/sub チャネル(`command.<method>: tokenName`)で state から wc-bindable カスタム要素のメソッドを起動
114
- - **event token** command token の双対。wc-bindable 要素が dispatch するイベントを `eventToken.<prop>: tokenName` + `$on` マップで state が受信
115
- - **stream** `$streams` 宣言で連続的な非同期フロー(async iterable / `ReadableStream`)を fold して reactive プロパティ化。switchMap 型の依存駆動 restart 付き
116
- - **パス getter** ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
117
- - **再帰パス** `$recursion: { "nodes.*": "children.*" }` で木の形が繰り返す場所を宣言し、1 本の `**` getter(`get "nodes.**.total"()`)が全深さを覆う。`$getAll(path, [])` は全深さを合併し、`$setAll(path, [], value)` は全深さへブロードキャストする
118
- - **Mustache 構文** テキストノードでの `{{ path|filter }}`
119
- - **複数の状態ソース** JSON, JS モジュール, インラインスクリプト, API, 属性
120
- - **SVG サポート**`<svg>` 要素内でのフルバインディング対応
121
- - **ライフサイクルフック** `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback`、Web Component `$stateReadyCallback`
122
- - **headless watch** `$watch` はパスが描画されていてもいなくても state の変化で発火する
123
- - **診断** 解決しないパス・添字の本数・getter の循環を `@wcstack/lint`・VS Code 拡張と同じ診断 code で報告する
124
- - **TypeScript サポート** `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))。`@wcstack/typescript` は同じ型を HTML の検証器へ運び(`wcs-schema`)、インライン state スクリプトを型検査する(`wcs-tsc`)— [docs/typescript.ja.md](../../docs/typescript.ja.md)
125
- - **サーバーサイドレンダリング** `enable-ssr` 属性 + `@wcstack/server` でフル SSR と自動ハイドレーション
126
- - **依存ゼロ** ランタイム依存なし
121
+ 各行はこの README 1 節に対応します。[周辺パッケージの役割](#周辺パッケージの役割)に挙げたもの以外は、すべてこのパッケージ単体の機能です。
122
+
123
+ | 領域 | 一行で言うと | 参照 |
124
+ |---|---|---|
125
+ | **パスモデル** | ドットパスが state を指す。`*` は抽象添字、`**` は深さ、`$1` / `$2` は軸の名前 | [第一原理](#第一原理-パスが唯一の契約) · [ループインデックス変数](#ループインデックス変数1-2) |
126
+ | **バインディング構文** | 1 つの `data-wcs` 属性にプロパティ / テキスト / class / style / 属性 / イベントを載せる。テキストノードでは `{{ }}`、`<svg>` の内側でも同じ | [バインディング構文](#バインディング構文) · [Mustache](#mustache-構文) · [SVG](#svg-サポート) |
127
+ | **構造ディレクティブ** | `<template>` 上の `for` と `if` / `elseif` / `else` | [構造ディレクティブ](#構造ディレクティブ) |
128
+ | **行の同一性** | 行は参照で差分を取るので、並べ替えや絞り込みで DOM を再利用する。`$listKeys` は再取得した配列でも行 DOM と行オブジェクトを保つ | [`$listKeys`](#listkeys--再取得された行の同一性) |
129
+ | **フォーム** | `input` / `select` / `textarea` の双方向バインディング、ラジオグループと単一値、チェックボックスグループと配列、`#ro` / `#onchange` / `#prevent` / `#stop` | [双方向バインディング](#双方向バインディング) · [修飾子](#修飾子) |
130
+ | **フィルタ** | 46 種類の組み込み、チェーン可能、`<html lang>` を見るロケール依存フォーマット | [フィルタ](#フィルタ) · [ロケール](#ロケール) |
131
+ | **派生状態** | パス getter がデータツリーの任意の深さの仮想プロパティを 1 箇所にフラットに宣言する。チェーンでき、setter も書ける | [パス getter](#パス-getter算出プロパティ) |
132
+ | **集計と一括書き込み** | `$getAll` / `$setAll` / `$resolve` `items.*.price` を横断して読み書きする。配列は作り直さない | [Proxy API](#proxy-api) |
133
+ | **再帰パス** | `$recursion` が木の形の繰り返し地点を宣言し、1 本の `**` getter が全深さを覆う | [再帰パス](#再帰パスrecursion) |
134
+ | **リアクティビティ** | ES Proxy がアドレス単位で読み取りを追跡・キャッシュし、依存順に無効化し、DOM 書き込みをマイクロタスクでまとめる | [状態の更新](#状態の更新) · [依存追跡の境界](#依存追跡の境界) |
135
+ | **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$streams` の `args` の 3 箇所からしか生まれない | [評価のきっかけ](#評価のきっかけdemand-root) |
136
+ | **モジュール化** | `mount=` がモジュールを 1 本のツリーに接ぎ木し、`state: path` が部分木をコンポーネントにマウントする。個別対応付けは単一キーを繋ぎ、マウントされたコンポーネントの getter はマウント点で公開される | [ボリューム](#追加の状態をマウントするmount) · [丸ごとマウント](#丸ごとマウントstate-path) |
137
+ | **コンポーネント** | 排他的な 2 方式 JavaScript クラス+`bind-component` か、HTML だけの DCC か | [機構の選び方](#コンポーネント機構の選び方) |
138
+ | **他要素との配線** | wc-bindable プロトコル、spread(`...: obj`)、`#init=` / `#sync=` authority、プロパティ→属性ミラー | [バインディング authority](#バインディング-authority-init--sync) · [Spread](#spread-バインディング) · [Inputs](#inputs-と属性ミラー) |
139
+ | **トークン** | command token state から要素のメソッドを呼び、event token が要素のイベントを state へ戻す | [Command token](#command-tokenメソッドバインディング) · [Event token](#event-tokenイベントバインディング) |
140
+ | **時間** | `$streams` が非同期ソースを fold し、`$watch` headless に反応し、`$scan` が両者を越えて残る累積値を持つ | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
141
+ | **初期化とライフサイクル** | state の供給は 6 通り。`$connectedCallback` 〜 `$stateReadyCallback`、`bootstrapState()` / `createState()` | [状態の初期化](#状態の初期化) · [ライフサイクルフック](#ライフサイクルフック) · [API リファレンス](#api-リファレンス) |
142
+ | **診断** | 解決しないパス・添字の本数・階数・getter の循環を報告する。失敗はそのバインディング 1 本に閉じ、値も DOM も巻き戻さない | [診断と失敗の扱い](#診断と失敗の扱い) |
143
+ | **配布** | ランタイム依存ゼロ、ビルド不要、ESM、CDN の `/auto` 1 タグ。`unsafe-eval` 不要で Trusted Types 対応 | [インストール](#インストール) · [docs/csp.ja.md](../../docs/csp.ja.md) |
144
+
145
+ ### 周辺パッケージの役割
146
+
147
+ `@wcstack/state` はリアクティブコアだけを担います。ツール・I/O・ルーティングは別パッケージで、分担はいつも同じ形です —— このパッケージが接続点と契約を用意し、周辺パッケージが機構を持ち込みます。
148
+
149
+ | パッケージ | 加わるもの | このパッケージ側にあるもの |
150
+ |---|---|---|
151
+ | [`@wcstack/server`](../server/) | サーバー側での描画と、クライアントに届いた markup のハイドレーション | `enable-ssr` 属性とハイドレーション契約 — [SSR](#サーバーサイドレンダリング) |
152
+ | [`@wcstack/lint`](../lint/) | `npx @wcstack/lint <file>` が HTML 中の `data-wcs` を実行前に検査する | 診断 code と `getWcsManifest()`(どちらもこの実装から導出)— [診断](#診断と失敗の扱い) |
153
+ | VS Code 拡張(`wcstack-intellisense`) | 同じ診断と補完をエディタ内で | 同じ manifest と診断 code |
154
+ | [`@wcstack/typescript`](../typescript/) | `wcs-schema` が型を HTML 検証器へ運び、`wcs-tsc` がインライン state スクリプトを型検査する | `defineState()`、`WcsPaths<T>` / `WcsPathValue<T, P>` — [TypeScript サポート](#typescript-サポート) |
155
+ | [`@wcstack/devtools`](../devtools/) | state・配線・更新履歴を見るブラウザパネル | パネルが読む計装 |
156
+ | [`@wcstack/testing`](../testing/) | `mount()` / `settle()` / `fire()` を 1 import で | 追加パッケージなしで書ける素のレシピ — [ページをテストする](#ページをテストする) |
157
+ | [`@wcstack/view-transition`](../view-transition/) | リストの移動・削除や分岐の入れ替えを View Transition API でアニメーションさせる | transition-runner の受け渡し。ページに arbiter がなければ変更はそのまま適用される — [遷移アニメーション](#遷移アニメーション) |
158
+ | [`@wcstack/router`](../router/) · [`@wcstack/autoloader`](../autoloader/) | 宣言的ルーティング、未定義カスタム要素の自動読み込み | ルートが書き込めるパスと、遅延定義を待つバインディング |
159
+ | [I/O ノード群](../../README.ja.md#追加パッケージ) — `fetch`, `storage`, `ws`, `midi`, … | プラットフォーム API を要素として | それらを繋ぐ wc-bindable 配線・spread・トークンプロトコル — [Spread](#spread-バインディング) |
160
+ | [`@wcstack/signals`](../signals/) | もう 1 つのリアクティブコア。巨大な keyed リストの生成・追加が 2.5〜3.5 倍速い | 相互運用 —— どちらも wc-bindable を話すので I/O ノードと DCC は共有できる — [パフォーマンス](#パフォーマンス) |
127
161
 
128
162
  ## インストール
129
163
 
@@ -248,7 +282,7 @@
248
282
  <div data-wcs="textContent: cart.total"></div>
249
283
  ```
250
284
 
251
- ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さず、マウントの枠もその rootNode が生きている限り予約されたままなので(枠の台帳は rootNode をキーにした `WeakMap` で、枠が解放されることはありません)、あとから修正版のルートを接続しても復帰しません。ルートの `<wcs-state>` を直してページを読み直してください。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
285
+ ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
252
286
 
253
287
  > **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
254
288
 
@@ -1103,6 +1137,8 @@ export default {
1103
1137
  };
1104
1138
  ```
1105
1139
 
1140
+ この書き方で行を入れ替えると、入れ替えが揃った時点で、描画済みの行が値と一緒に並べ替わります(その場で中身を書き換えるのではありません)。行の `$1` と、バインドの外にある行の状態(バインドしていない input に入力中のテキストなど)も値に付いてきます。リストに無かった値を書き込むと、その行はその場で置き換わります。ブロックは動かず、バインドが新しい値を映すので、行にバインドした input は入力中もフォーカスを保ちます。プリミティブのリストでは等しい値を区別できないため、書き込みの結果が同じ値の並べ替えになれば入れ替えとして扱います。
1141
+
1106
1142
  ## 再帰パス(`$recursion`)
1107
1143
 
1108
1144
  パスは深さを文字列に焼き付けます。`nodes.*.children.*.total` はワイルドカードちょうど 2 段のパスであり、木が 1 段深くなっても 3 段には伸びません。しかし木の深さはコードではなく**データの性質**です。`$recursion` はこの隔たりを埋めます。形が繰り返す場所を宣言し、あとは「いま何段目であれ」を `**` と書きます。
@@ -1435,10 +1471,10 @@ export default {
1435
1471
 
1436
1472
  1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
1437
1473
  2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
1438
- 3. 結合点は**パス名のみ**であるため、親と子は完全に疎結合な状態を保ち、それぞれ独立してテスト可能です。
1439
- 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)と、依存グラフを通じた変更の伝播のみです。
1474
+ 3. 結合点は**パス名のみ**であるため、親と子は疎結合に保たれます。Shadow DOM のコンポーネントは単体でも動作しますが([独立した Web Component への状態注入](#独立した-web-component-への状態注入e2esingle-component))、Light DOM はホストからの配線が必須です。
1475
+ 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)、依存グラフを通じた変更の伝播、そして描画する行ごとに構築されるバインディング台帳です。
1440
1476
 
1441
- これは、コンポーネントレベルの複雑な抽象化ではなく、「パスの解決」に基づいたコンポーネント間状態管理への軽量なアプローチです。
1477
+ これは、コンポーネントレベルの抽象化ではなく「パスの解決」に基づくコンポーネント間の状態管理です。描画が最も安い方式という意味ではありません。[パフォーマンス](#パフォーマンス)のとおり生成・追加は [`@wcstack/signals`](../signals/) の 2.5〜3.5 倍で、それが行ごとの台帳の代価です。得られるのは、配線が宣言的で検査可能であることです。
1442
1478
 
1443
1479
  ### コンポーネント定義(Shadow DOM)
1444
1480
 
@@ -1608,6 +1644,7 @@ customElements.define("my-component", MyComponent);
1608
1644
  - `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
1609
1645
  - 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
1610
1646
  - Light DOM コンポーネントはホストからの配線が必須(plain 形は v2 で廃止)
1647
+ - **マウントされた**スコープは宣言面を実行しない。`$watch`・`$streams`・`$scan` は一度だけ警告を出して無視され、`$recursion` / `**` getter は拒否される。ルート state に宣言すること(ボリューム `<wcs-state mount>` は `$watch` を持てるが、`$scan` と `$recursion` はルート専用)。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
1611
1648
 
1612
1649
  ### ループ内でのコンポーネント使用
1613
1650
 
@@ -1957,11 +1994,27 @@ $on: {
1957
1994
 
1958
1995
  event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されても event-token registry は保持されるので、ルート `<wcs-state>` を付け直せば `$on` ハンドラ(と `on` の scan)は再びイベントを受けます。切断中に dispatch されたイベントは state ツリーが見つからず、届きません。
1959
1996
 
1997
+ ## 時間を扱う機構の選び方
1998
+
1999
+ 続く 4 節は、それぞれ別の問いに答えるものです。取り違えが起きやすいので、データの出どころではなく**何を宣言しているか**で選んでください。
2000
+
2001
+ | 宣言 | 宣言するもの | 値を持つか | 発火 | 主な用途 |
2002
+ |---|---|---|---|---|
2003
+ | [パス getter](#パス-getter算出プロパティ) | その値が現在の state から見て**何であるか** | 持たない(アドレス単位で再計算・キャッシュ) | 評価需要が読んだときに遅延評価 | 小計、分類、集計 |
2004
+ | [`$streams`](#streamstreams) | 非同期の供給元と、**1 回の実行の中で** fold される値 | 持つ(出力は runtime の所有) | chunk ごと。`args` が変われば `initial` に戻して restart | フィード、ソケット、継続的な観測 |
2005
+ | [`$watch`](#watchwatch) | 変更への反応 | 持たない | 変化したアドレスごとにバッチ 1 回、scan の書き込みの後 | 副作用、「条件が成立したとき」 |
2006
+ | [`$scan`](#scanscan) | 時間をまたぐ累積値と、それを戻す条件 | 持つ(出力は runtime の所有) | 着地ごと(`from`)またはイベントごと(`on`) | ページ蓄積、履歴、件数 |
2007
+
2008
+ 混乱のほとんどは、次の 2 点で解けます。
2009
+
2010
+ - **`$updatedCallback` はこの表に入りません。** 適用されたバインディングを報告するものなので、そこに処理をぶら下げると描画内容に暗黙に依存します。[評価のきっかけ](#評価のきっかけdemand-root)を参照してください。
2011
+ - **`$streams` の fold は restart のたびに戻り、`$scan` は戻りません。** restart を越えて値を保ちたいとき、あるいは状態ではなく出来事を数えたいときは `$scan` の領分です。
2012
+
1960
2013
  ## Stream(`$streams`)
1961
2014
 
1962
2015
  command token / event token が運ぶのは離散的なやり取りです。**`$streams`** は残る形 —— 連続的なフローをカバーします。非同期 producer(async iterable / async generator / `ReadableStream`)を宣言すると、フレームワークがそれを **fold して単一の reactive プロパティに畳み込みます** —— 各チャンクは通常のパス代入を通るため、バインディング・パス getter・`$updatedCallback` は自分で値を代入した場合とまったく同じように反応します。`args` 関数が読んだ state パスが変化すると、実行中の producer は abort され、新しい引数で source が張り直されます(switchMap 型の依存駆動 restart)。stream は `$connectedCallback` 完了後に eager に起動し、要素の disconnect で abort されます。
1963
2016
 
1964
- `$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watch-watch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
2017
+ `$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watchwatch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
1965
2018
 
1966
2019
  ```html
1967
2020
  <wcs-state>
@@ -2864,7 +2917,7 @@ bootstrapState();
2864
2917
  | `setterPaths` | setter として定義されたパスの Set |
2865
2918
  | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
2866
2919
  | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
2867
- | `setInitialState(state)` | プログラムから状態を設定(初期化前)。初期化に失敗した要素では throw します 再武装はできないので、要素を取り除いて作り直してください |
2920
+ | `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$updatedCallback` も呼びません。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再セットは非対応です)。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素では throw します —— 失敗した要素は再武装できないので、取り除いて作り直してください |
2868
2921
  | `nextVersion()` | バージョン番号をインクリメントして返す |
2869
2922
 
2870
2923
  ## アーキテクチャ
@@ -2905,7 +2958,7 @@ buildBindings(root)
2905
2958
  - **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
2906
2959
  - **ListIndex** — ランタイムループインデックスチェーン
2907
2960
  - **StateAddress** — PathInfo + ListIndex の組み合わせ
2908
- - **AbsolutePathInfo / AbsoluteStateAddress** — ツリーを持つ state 要素に固定した PathInfo と、その ListIndex の組。マウントされたコンポーネントとボリュームは相対パスをこの層でホストのツリーへ翻訳する。v2 は 1 root 1 ツリーなので、アドレスに状態名はない
2961
+ - **TreePath / AbsoluteStateAddress** — ツリーを持つ state 要素に固定した PathInfo と、その ListIndex の組。マウントされたコンポーネントとボリュームは相対パスをこの層でホストのツリーへ翻訳する。v2 は 1 root 1 ツリーなので、アドレスに状態名はない
2909
2962
 
2910
2963
  ## パフォーマンス
2911
2964
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
4
 
5
- **This is not another convenient frontend framework. It is a different lineage that rearranges the premises of frontend development.**
5
+ **This is not another convenient frontend framework. It brings a lineage established outside frontend development — the one where a path string is the contract between view and model — onto web standards.**
6
6
 
7
7
  Most libraries place the coupling point between UI, state, and components inside JavaScript. `@wcstack/state` does not. It assumes no virtual DOM, no compilation step, no hooks, no selectors. UI and state are connected by HTML and path strings alone.
8
8
 
@@ -32,7 +32,7 @@ This is not React / Vue / Solid with a different syntax. Those put the coupling
32
32
  | JavaScript is the center of rendering | HTML and the DOM are the center |
33
33
  | State is pulled into components | Paths are declared and the DOM connects to state |
34
34
  | hooks / selectors / signals express subscriptions | Attributes and paths express bindings |
35
- | The whole app runs inside a framework execution model | A thin reactive layer is added on top of web standards |
35
+ | The whole app runs inside a framework execution model | A reactive layer is added on top of web standards, and the page stays a page |
36
36
 
37
37
  The nearer relatives are the **attribute-directive, no-build libraries** — Alpine.js, petite-vue and their kind. They share the premise (attributes on plain HTML, no compiler) and differ on two points that decide the choice:
38
38
 
@@ -45,6 +45,23 @@ The nearer relatives are the **attribute-directive, no-build libraries** — Alp
45
45
 
46
46
  On those axes the comparison is concrete: the [Performance](#performance) section below is one, and the drivers under `e2e/bench/` regenerate it on your own hardware.
47
47
 
48
+ ### The Lineage Outside JavaScript
49
+
50
+ The premise — *a path string is the whole contract between a view and a model* — is older than the framework era, and most of it was worked out outside JavaScript. Inside it, the direct ancestors are Knockout's `data-bind="text: user.name"` (an attribute carrying the binding, though it evaluates expressions and needs `ko.observable` wrappers) and Polymer's path system, which had dotted paths, `items.*` observers and `this.set("users.0.name", v)` — but required `set()` / `notifyPath()`, because plain assignment could not be observed on the platform of its time. Naming the older lineage is more useful than claiming novelty:
51
+
52
+ | Lineage | What it already had | What differs here |
53
+ |---|---|---|
54
+ | **Spreadsheets** (VisiCalc, 1979) | An address, a formula declaring what a cell **is**, a dependency graph, lazy recomputation — and no update code anywhere | Names instead of grid coordinates, and one formula per *shape* rather than per cell: `get "cart.items.*.subtotal"()` is not filled down into the rows; the wildcard is the definition |
55
+ | **Cocoa Bindings / KVC–KVO** (NeXT's EOF, 1994; Mac OS X 10.3, 2003) | Key paths (`person.address.street`), a binding triple of target + key path + value transformer, and collection operators that aggregate along a path (`@sum.items.price`) | The triple lives in the markup instead of a nib or a `bind:toObject:withKeyPath:` call, so it can be grepped, linted and diffed. Change detection is an ES Proxy over plain objects rather than KVC compliance |
56
+ | **XForms** (W3C Recommendation, 2003) | Model / instance / view separation, `ref` paths into the instance, and `<bind calculate="…">` — a computed value declared **at a path**, the direct ancestor of a path getter | The path is an address and nothing else: the computation is a JavaScript getter on the state, not XPath inside an attribute. And it runs in a stock browser, with no XForms processor |
57
+ | **WPF / XAML** (2006) | `{Binding Path=User.Name, Mode=TwoWay}`; a `DataContext` that re-roots a whole subtree; `UpdateSourceTrigger` choosing when the source is written; `IValueConverter` between the ends | One state tree per root, rather than a context inherited down the visual tree with `RelativeSource` / `ElementName` escapes — `state: user` is that re-rooting, written in the host's HTML. Converters are a closed set of 46 filters, not classes you register, and nothing is compiled |
58
+ | **Android Data Binding** (2015) | The path in the layout file itself — `android:text="@{user.name}"`, `@={}` for two-way | No build step and no generated binding class, and no expressions inside the attribute |
59
+ | **SCADA / HMI tag binding** (industrial, decades) | Widget properties wired to tag paths (`Line1/Tank/Level`) by configuration alone, and *indirect* bindings that parameterize the path (`Folder/Tag_{1}`) so one screen drives many devices | The tree carries derived values, lists and mounted components, not a flat namespace of scalars; the parameter is a loop's wildcard, resolved by the row the binding sits in rather than assigned from a dropdown |
60
+
61
+ Wildcards also resemble MQTT topic filters (`sensor/+/temperature`) and OSC address patterns (`/synth/*/freq`), but those select **messages in flight**. `items.*.price` names state addresses, and the one string is both the subscription and the write target.
62
+
63
+ What survives the comparison as genuinely new is narrow: the **wildcard path getter** — a getter whose *key* is a path pattern, so one definition serves every row and the dependency edge is held per pattern instead of per cell. The rest is a recombination of the lineage above onto three things none of them could assume: Custom Elements, ES Proxy and Import Maps.
64
+
48
65
  ## First Principle: Path as the Universal Contract
49
66
 
50
67
  In every existing framework, the **component** is the coupling point between UI and state. Components import state hooks, selectors, or reactive primitives, and the binding happens inside JavaScript. No matter how cleanly you separate your state store, there is always glue code in the component that pulls state in.
@@ -55,23 +72,23 @@ In every existing framework, the **component** is the coupling point between UI
55
72
  |-------|---------------|----------------------|
56
73
  | **State** (`<wcs-state>`) | Data structure and business logic | Which DOM nodes are bound |
57
74
  | **UI** (`data-wcs`) | Path strings and display intent | How state is stored or computed |
58
- | **Components** (`state: path`) | The mount table the host writes | The other component's internals |
75
+ | **Components** (`state: path`) | The mount table the host writes | Who mounted it, and what the rest of the tree holds |
59
76
 
60
77
  Three levels of path contracts keep everything loosely coupled:
61
78
 
62
- 1. **UI ↔ State** — A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives. The component's JavaScript doesn't contain a single line that references state.
79
+ 1. **UI ↔ State** — A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives: no component code imports a reactive primitive or registers a subscription. A `bind-component` class still declares its own plain `state` object and reads it like a plain object — what never appears is glue that pulls state into the component.
63
80
 
64
- 2. **Component ↔ Component** — The host mounts a subtree onto each component (`<my-card data-wcs="state: user">`), and volumes graft extra modules onto the tree (`<wcs-state mount="i18n">`). Components never import or depend on each other; every connection is a path prefix on the single tree, nothing more.
81
+ 2. **Component ↔ Component** — The host mounts a subtree onto each component (`<my-card data-wcs="state: user">`), and volumes graft extra modules onto the tree (`<wcs-state mount="i18n">`). Components never import one another, and a whole-object mount is a path prefix on the single tree and nothing more. Two declarative forms reach further, each spelled out where it is defined: the [per-property form](#host-usage) (`state.message: user.name`) has the host name the component's own keys, and an [exported getter](#exported-getters-reading-a-components-getter-from-outside) lets the host read a value the component computes — that binding then depends on a component being mounted there.
65
82
 
66
83
  3. **Loop context** — Inside a `for` loop, `*` acts as an abstract index. Bindings like `items.*.price` resolve to the current element automatically. The template doesn't know its concrete position — the wildcard is the contract.
67
84
 
68
85
  ### Why This Matters
69
86
 
70
- This is complete separation of UI and state with **no JavaScript intermediary**. You can:
87
+ This separates UI and state with **no JavaScript intermediary**. You can:
71
88
 
72
- - Redesign the entire UI without touching state logic
89
+ - Redesign the UI without touching state logic — as far as the logic does not hang off what is rendered: a live binding is one of the three [demand roots](#demand-roots--what-makes-a-getter-run), so an element you think of as display-only can be the page's only subscription
73
90
  - Refactor state structure and only update path strings
74
- - Read the HTML alone and understand every data dependency
91
+ - Read the HTML and know every binding; the dependencies that are not in the HTML (`$watch`, `$streams`, `$scan`) are all declared in one place, the state
75
92
 
76
93
  The path contract works like a URL in a REST API — a simple string that both sides agree on, with no shared code between them. It's the natural result of building on HTML's declarative nature rather than inventing a template language on top of JavaScript.
77
94
 
@@ -101,29 +118,46 @@ That's it. No build, no bootstrap code, no framework.
101
118
 
102
119
  ## Features Derived from This Principle
103
120
 
104
- - **Declarative data binding** `data-wcs` attribute for property / text / event / structural binding
105
- - **Reactive Proxy** — ES Proxy-based automatic DOM updates with dependency tracking
106
- - **Structural directives** `for`, `if` / `elseif` / `else` via `<template>` elements
107
- - **Volumes** — `<wcs-state mount="cart">` grafts a module onto the single state tree; bindings read it as `cart.…`
108
- - **Row identity** `$listKeys` keeps row DOM and row objects across refetched arrays
109
- - **Wildcard aggregation** `$getAll` / `$setAll` read and write across `items.*.price` without rebuilding the array
110
- - **Built-in filters** 46 filters for formatting, comparison, arithmetic, date, and more
111
- - **Two-way binding** automatic for `<input>`, `<select>`, `<textarea>`
112
- - **Web Component binding** bidirectional state binding with Shadow DOM components
113
- - **Command tokens** invoke methods on wc-bindable custom elements from state via a pub/sub channel (`command.<method>: tokenName`)
114
- - **Event tokens** the dual of command tokens: receive a wc-bindable element's dispatched events in state via `eventToken.<prop>: tokenName` + the `$on` map
115
- - **Streams** fold continuous async flows (async iterables / `ReadableStream`) into reactive properties via the `$streams` declaration, with switchMap-style dependency-driven restart
116
- - **Path getters** dot-path key getters (`get "users.*.fullName"()`) for virtual properties at any depth in a data tree, all defined flat in one place with automatic dependency tracking and caching
117
- - **Recursive paths** `$recursion: { "nodes.*": "children.*" }` declares where a tree's shape repeats, and one `**` getter (`get "nodes.**.total"()`) covers every depth; `$getAll(path, [])` unions all depths and `$setAll(path, [], value)` broadcasts to all of them
118
- - **Mustache syntax** `{{ path|filter }}` in text nodes
119
- - **Multiple state sources** JSON, JS module, inline script, API, attribute
120
- - **SVG support** full binding support inside `<svg>` elements
121
- - **Lifecycle hooks** `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback`, plus `$stateReadyCallback` for Web Components
122
- - **Headless watch** `$watch` fires on state changes whether or not the path is rendered
123
- - **Diagnostics** unresolved paths, index arity and getter cycles are reported with the same codes as `@wcstack/lint` and the VS Code extension
124
- - **TypeScript support** `defineState()` for typed state definitions with dot-path autocompletion ([details](docs/define-state.md)); `@wcstack/typescript` carries the same types into the HTML validator (`wcs-schema`) and type-checks inline state scripts (`wcs-tsc`) see [docs/typescript.md](../../docs/typescript.md)
125
- - **Server-Side Rendering** `enable-ssr` attribute + `@wcstack/server` for full SSR with automatic hydration
126
- - **Zero dependencies** no runtime dependencies
121
+ Every row is a section of this README. Unless it appears under [Where the neighbours come in](#where-the-neighbours-come-in), it ships in this package.
122
+
123
+ | Area | In one line | Reference |
124
+ |---|---|---|
125
+ | **Path model** | Dot paths address state; `*` is an abstract index, `**` a depth, `$1` / `$2` name the axes | [First principle](#first-principle-path-as-the-universal-contract) · [Loop index variables](#loop-index-variables-1-2) |
126
+ | **Binding syntax** | One `data-wcs` attribute carries property / text / class / style / attribute / event bindings; `{{ }}` in text nodes; the same inside `<svg>` | [Binding syntax](#binding-syntax) · [Mustache](#mustache-syntax) · [SVG](#svg-support) |
127
+ | **Structural directives** | `for` and `if` / `elseif` / `else` on `<template>` elements | [Structural directives](#structural-directives) |
128
+ | **Row identity** | Rows diff by reference, so a sort or a filter reuses the DOM; `$listKeys` keeps row DOM and row objects across refetched arrays | [`$listKeys`](#listkeys--identity-for-refetched-rows) |
129
+ | **Forms** | Two-way binding for `input` / `select` / `textarea`, a radio group to one value, a checkbox group to an array, `#ro` / `#onchange` / `#prevent` / `#stop` | [Two-way binding](#two-way-binding) · [Modifiers](#modifiers) |
130
+ | **Filters** | 46 built-ins, chainable, locale-aware formatting that reads `<html lang>` | [Filters](#filters) · [Locale](#locale) |
131
+ | **Derived state** | Path getters declare virtual properties at any depth from one flat place; they chain, and they take setters | [Path getters](#path-getters-computed-properties) |
132
+ | **Aggregation and bulk write** | `$getAll` / `$setAll` / `$resolve` read and write across `items.*.price` without rebuilding the array | [Proxy APIs](#proxy-apis) |
133
+ | **Recursive paths** | `$recursion` declares where a tree's shape repeats; one `**` getter covers every depth | [Recursive paths](#recursive-paths-recursion) |
134
+ | **Reactivity** | An ES Proxy tracks reads per address, caches per address, invalidates in dependency order and batches DOM writes on a microtask | [Updating state](#updating-state) · [Dependency tracking boundaries](#dependency-tracking-boundaries) |
135
+ | **What makes a getter run** | Getters are lazy. Demand comes from a live binding, a `$watch` or a `$streams` `args` and from nowhere else | [Demand roots](#demand-roots--what-makes-a-getter-run) |
136
+ | **Modularity** | `mount=` grafts a module onto the one tree; `state: path` mounts a subtree onto a component; the per-property form maps single keys, and a mounted component's getters are exported at its mount point | [Volumes](#mounting-additional-state-mount) · [Whole-object mount](#whole-object-mount-state-path) |
137
+ | **Components** | Two mutually exclusive mechanisms: a JavaScript class with `bind-component`, or HTML-only DCC | [Choosing a mechanism](#choosing-a-component-mechanism) |
138
+ | **Wiring to other elements** | The wc-bindable protocol, spread (`...: obj`), `#init=` / `#sync=` authority, property-to-attribute mirroring | [Binding authority](#binding-authority-init--sync) · [Spread](#spread-binding) · [Inputs](#inputs-and-attribute-mirror) |
139
+ | **Tokens** | Command tokens call an element's methods from state; event tokens carry the element's events back | [Command token](#command-token-method-binding) · [Event token](#event-token-event-binding) |
140
+ | **Time** | `$streams` folds an async source, `$watch` reacts headlessly, `$scan` owns an accumulation that outlives both | [Choosing a time mechanism](#choosing-a-time-mechanism) |
141
+ | **Initialization and lifecycle** | Six ways to supply the state; `$connectedCallback` `$stateReadyCallback`; `bootstrapState()` / `createState()` | [State initialization](#state-initialization) · [Lifecycle hooks](#lifecycle-hooks) · [API reference](#api-reference) |
142
+ | **Diagnostics** | Unresolved paths, index arity, wildcard rank and getter cycles are reported; one failing binding stays confined, and neither values nor the DOM are rolled back | [Diagnostics](#diagnostics-and-failure-handling) |
143
+ | **Delivery** | Zero runtime dependencies, no build step, ESM, one CDN `/auto` tag; no `unsafe-eval`, Trusted Types supported | [Installation](#installation) · [docs/csp.md](../../docs/csp.md) |
144
+
145
+ ### Where the neighbours come in
146
+
147
+ `@wcstack/state` is the reactive core and nothing else. Tooling, I/O and routing live in sibling packages, and the split is always the same shape: this package provides the hook and the contract, the neighbour provides the machinery.
148
+
149
+ | Package | What it adds | What this package already provides |
150
+ |---|---|---|
151
+ | [`@wcstack/server`](../server/) | Renders the page on the server and hydrates the markup the client receives | The `enable-ssr` attribute and the hydration contract — [SSR](#server-side-rendering) |
152
+ | [`@wcstack/lint`](../lint/) | `npx @wcstack/lint <file>` checks every `data-wcs` in an HTML file before it runs | The diagnostic codes and `getWcsManifest()`, both derived from this implementation — [Diagnostics](#diagnostics-and-failure-handling) |
153
+ | VS Code extension (`wcstack-intellisense`) | The same diagnostics, plus completion, inside the editor | The same manifest and codes |
154
+ | [`@wcstack/typescript`](../typescript/) | `wcs-schema` carries the types into the HTML validator; `wcs-tsc` type-checks inline state scripts | `defineState()`, `WcsPaths<T>` / `WcsPathValue<T, P>` — [TypeScript support](#typescript-support) |
155
+ | [`@wcstack/devtools`](../devtools/) | A browser panel over state, wiring and update history | The instrumentation the panel reads |
156
+ | [`@wcstack/testing`](../testing/) | `mount()` / `settle()` / `fire()` as one import | The bare recipes that need no extra package — [Testing your page](#testing-your-page) |
157
+ | [`@wcstack/view-transition`](../view-transition/) | Animates list moves, removals and branch swaps through the View Transition API | The transition-runner hand-off; with no arbiter on the page the mutation applies directly — [Transition animations](#transition-animations) |
158
+ | [`@wcstack/router`](../router/) · [`@wcstack/autoloader`](../autoloader/) | Declarative routing; automatic loading of undefined custom elements | Paths a route can write into, and bindings that wait for a late definition |
159
+ | The [I/O nodes](../../README.md#additional-packages) — `fetch`, `storage`, `ws`, `midi`, … | The platform APIs as elements | The wc-bindable wiring, spread and the token protocols that connect them — [Spread](#spread-binding) |
160
+ | [`@wcstack/signals`](../signals/) | A different reactive core, 2.5–3.5× faster on create / append for very large keyed lists | Interop — both speak wc-bindable, so the I/O nodes and DCCs are shared — [Performance](#performance) |
127
161
 
128
162
  ## Installation
129
163
 
@@ -248,7 +282,7 @@ There is **one state tree per root**. To split state across modules, mount a vol
248
282
  <div data-wcs="textContent: cart.total"></div>
249
283
  ```
250
284
 
251
- A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). If the root `<wcs-state>` fails to initialize, the volumes already waiting for it settle with a report of their own instead of waiting forever. That report is the end of the line for those volumes: a volume reported as an orphan does not graft itself later, and its mount slot stays reserved for as long as that root node is alive the slot ledger is a `WeakMap` keyed by the root node and a slot is never released so connecting a corrected root afterwards does not bring it back. Fix the root `<wcs-state>` and reload the page. Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
285
+ A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). If the root `<wcs-state>` fails to initialize, the volumes already waiting for it settle with a report of their own instead of waiting forever. That report is the end of the line for those volumes: a volume reported as an orphan does not graft itself later, so connecting a corrected root afterwards does not bring it back. A volume that settles without grafting — orphaned, failed to load, or failed to graft — releases its mount slot, and so does a volume detached while it is still loading or waiting for its root. Such a volume takes the slot back when it is re-attached to the same root, or otherwise just before it grafts, and still grafts as before when the slot is freeeven while detached; if another volume took the slot in the meantime, it reports that and does not graft. A synchronous throw from a volume's `$connectedCallback` is reported like an asynchronous one, and the volume counts as grafted. To recover without reloading the page, remove the broken root and the orphaned volumes and add new elements. A grafted volume keeps its slot even when detached, because its data stays in the tree. Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
252
286
 
253
287
  > **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
254
288
 
@@ -1104,6 +1138,8 @@ export default {
1104
1138
  };
1105
1139
  ```
1106
1140
 
1141
+ Swapping rows this way moves the rendered rows with their values once the swap is complete: the row blocks are reordered rather than rewritten in place, so a row's `$1` and any state it holds outside bindings (such as text typed into an unbound input) follow the value. Writing a value that was not in the list replaces that row in place: its block stays where it is and its bindings show the new value, so an input bound to the row keeps focus while you type. In a list of primitives, equal values cannot be told apart, so writes that end in a reordering of the same values count as a swap.
1142
+
1107
1143
  ## Recursive Paths (`$recursion`)
1108
1144
 
1109
1145
  A path burns its depth into the string. `nodes.*.children.*.total` has exactly two wildcard levels, and nothing about it stretches to three when the tree grows a level — but a tree's depth belongs to the data, not to the code. `$recursion` closes that gap: declare where the shape repeats, then write `**` for "however deep this is".
@@ -1436,10 +1472,10 @@ Many frameworks use patterns like prop drilling, context providers, or external
1436
1472
 
1437
1473
  1. The child references and updates the parent's state through its own state proxy — no props, no events, no awareness of the parent.
1438
1474
  2. When the parent's state changes, the Proxy `set` trap automatically notifies any child bindings that reference the affected path.
1439
- 3. Because the only coupling is the **path name**, both sides remain loosely coupled and independently testable.
1440
- 4. The cost is path resolution (cached at O(1) after first access) plus change propagation through the dependency graph.
1475
+ 3. Because the only coupling is the **path name**, both sides stay loosely coupled. A Shadow DOM component also runs on its own ([standalone injection](#standalone-web-component-injection-e2esingle-component)); a Light DOM one does not — the host has to wire it.
1476
+ 4. The cost is path resolution (cached at O(1) after first access), change propagation through the dependency graph, and the per-row binding ledger the package builds for every row it renders.
1441
1477
 
1442
- This provides a lightweight approach to cross-component state management based on path resolution rather than component-level abstractions.
1478
+ This is cross-component state management built on path resolution rather than on component-level abstractions. It is not the cheapest way to render: [Performance](#performance) puts create and append at 2.5–3.5× [`@wcstack/signals`](../signals/), which is what the per-row ledger costs. What it buys is wiring that stays declarative and inspectable.
1443
1479
 
1444
1480
  ### Component Definition (Shadow DOM)
1445
1481
 
@@ -1608,6 +1644,7 @@ customElements.define("my-component", MyComponent);
1608
1644
  - `<wcs-state>` with `bind-component` must be a **direct child** of the component element (top-level)
1609
1645
  - The parent element must be a **custom element** (tag name containing a hyphen)
1610
1646
  - Light DOM components must be wired from the host (the plain, unwired form was removed in v2)
1647
+ - A **mounted** scope does not execute declaration surfaces: `$watch`, `$streams` and `$scan` are ignored there with a one-time warning, and `$recursion` / `**` getters are rejected. Declare them on the root state — a volume (`<wcs-state mount>`) can host `$watch`, while `$scan` and `$recursion` are root-only. An unwired Shadow DOM child owns an independent tree and can declare all of them
1611
1648
 
1612
1649
  ### Loop with Components
1613
1650
 
@@ -1961,6 +1998,22 @@ $on: {
1961
1998
 
1962
1999
  Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers. Disconnecting the owning `<wcs-state>` keeps the event-token registry, so `$on` handlers (and `on` scans) receive events again once the root `<wcs-state>` is re-attached; an event dispatched while it is disconnected finds no state tree and is not delivered.
1963
2000
 
2001
+ ## Choosing a Time Mechanism
2002
+
2003
+ The next four sections answer four different questions, and the usual mistake is to reach for the wrong one. Choose by **what you are declaring**, not by where the data comes from:
2004
+
2005
+ | Declaration | What you declare | Owns a value | Fires | Typical use |
2006
+ |---|---|---|---|---|
2007
+ | [Path getter](#path-getters-computed-properties) | What a value **is**, in terms of the current state | No — it is recomputed and cached per address | Lazily, when a demand root reads it | Subtotals, classification, aggregates |
2008
+ | [`$streams`](#streams-streams) | An async producer, and the value folded **within one run** | Yes — the runtime owns the output | Per chunk; restarts, back to `initial`, when `args` change | Feeds, sockets, continuous observation |
2009
+ | [`$watch`](#watch-watch) | A reaction to a change | No | Once per batch per changed address, after the scan write | Side effects, "when this becomes true" |
2010
+ | [`$scan`](#scan-scan) | An accumulation over time, and what resets it | Yes — the runtime owns the output | Once per landing (`from`) or once per event (`on`) | Paging accumulation, history, counters |
2011
+
2012
+ Two rules cut most of the confusion:
2013
+
2014
+ - **`$updatedCallback` is not on this list.** It reports the bindings that were applied, so anything hung on it silently depends on what is rendered. See [Demand roots](#demand-roots--what-makes-a-getter-run).
2015
+ - **A `$streams` fold resets on every restart; a `$scan` does not.** When the value has to survive the restart, or has to count events rather than states, it belongs in `$scan`.
2016
+
1964
2017
  ## Streams (`$streams`)
1965
2018
 
1966
2019
  Command tokens and event tokens carry discrete interactions. **`$streams`** covers the remaining shape: a continuous flow. Declare an async producer (async iterable / async generator / `ReadableStream`) and the framework **folds it into a single reactive property** — each chunk goes through normal path assignment, so bindings, path getters, and `$updatedCallback` react exactly as if you had assigned the value yourself. When a state path read by the `args` function changes, the running producer is aborted and the source is restarted with the new arguments (switchMap-style dependency-driven restart). Streams start eagerly after `$connectedCallback` completes and are aborted when the element disconnects.
@@ -2872,7 +2925,7 @@ Subpath entries for tooling: `@wcstack/state/parser` (the `data-wcs` parser as a
2872
2925
  | `setterPaths` | Set of paths defined as setters |
2873
2926
  | `createState(mutability, callback)` | Create a state proxy (`"readonly"` or `"writable"`) |
2874
2927
  | `createStateAsync(mutability, callback)` | Async version of `createState` |
2875
- | `setInitialState(state)` | Set state programmatically (before initialization). Throws if the element already failed to initialize — such an element cannot be re-armed; remove it and create a new one |
2928
+ | `setInitialState(state)` | Set state programmatically. Before initialization it supplies the initial state. On an initialized element it replaces the whole state and re-applies every established binding to the new state before it returns (a detached element re-applies them when it reconnects); a binding whose path the new state no longer has reports a failed apply instead of keeping the old text. A re-set is not a write: it fires no `$watch` handler and no `$updatedCallback`. Lists are matched by array identity, so pass a new array when a list's length changed — re-setting with the same array instance after pushing to or splicing it in place is not supported. Throws on a loaded volume (`<wcs-state mount="…">` — its data was copied into the root tree, so write the paths under the mount path on the root instead), on a tree with grafted volumes or mounted components, and on an element that already failed to initialize — such an element cannot be re-armed; remove it and create a new one |
2876
2929
  | `nextVersion()` | Increment and return version number |
2877
2930
 
2878
2931
  ## Architecture
@@ -2913,7 +2966,7 @@ Paths like `users.*.name` are decomposed into:
2913
2966
  - **PathInfo** — static path metadata (segments, wildcard count, parent path)
2914
2967
  - **ListIndex** — runtime loop index chain
2915
2968
  - **StateAddress** — combination of PathInfo + ListIndex
2916
- - **AbsolutePathInfo / AbsoluteStateAddress** — a PathInfo pinned to the state element that owns the tree, plus its ListIndex. Mounted components and volumes translate their relative paths onto the host tree at this level; v2 has one tree per root, so an address carries no state name
2969
+ - **TreePath / AbsoluteStateAddress** — a PathInfo pinned to the state element that owns the tree, plus its ListIndex. Mounted components and volumes translate their relative paths onto the host tree at this level; v2 has one tree per root, so an address carries no state name
2917
2970
 
2918
2971
  ## Performance
2919
2972