@wcstack/state 2.6.1 → 3.0.0

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.
Files changed (64) hide show
  1. package/README.ja.md +91 -46
  2. package/README.md +93 -46
  3. package/dist/auto.min.js +1 -1
  4. package/dist/auto.min.js.map +1 -1
  5. package/dist/define.d.ts +295 -0
  6. package/dist/define.js +1 -0
  7. package/dist/index.d.ts +252 -94
  8. package/dist/index.esm.js +1 -23477
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/manifest.esm.js +21 -37
  11. package/dist/parser.d.ts +37 -39
  12. package/dist/parser.esm.js +147 -1153
  13. package/dist/split/chunks/binder.js +2 -0
  14. package/dist/split/chunks/binder.js.map +1 -0
  15. package/dist/split/chunks/checkDependency.js +2 -0
  16. package/dist/split/chunks/checkDependency.js.map +1 -0
  17. package/dist/split/chunks/diagnosticsHooks.js +2 -0
  18. package/dist/split/chunks/diagnosticsHooks.js.map +1 -0
  19. package/dist/split/chunks/errorGuidance.js +2 -0
  20. package/dist/split/chunks/errorGuidance.js.map +1 -0
  21. package/dist/split/chunks/features.d.ts +29 -0
  22. package/dist/split/chunks/filterRegistry.js +2 -0
  23. package/dist/split/chunks/filterRegistry.js.map +1 -0
  24. package/dist/split/chunks/getAllPropertyDescriptors.js +2 -0
  25. package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -0
  26. package/dist/split/chunks/lifecycleHooks.js +2 -0
  27. package/dist/split/chunks/lifecycleHooks.js.map +1 -0
  28. package/dist/split/chunks/mount.js +2 -0
  29. package/dist/split/chunks/mount.js.map +1 -0
  30. package/dist/split/chunks/pathDiagnostics.js +2 -0
  31. package/dist/split/chunks/pathDiagnostics.js.map +1 -0
  32. package/dist/split/chunks/registerComponents.js +2 -0
  33. package/dist/split/chunks/registerComponents.js.map +1 -0
  34. package/dist/split/chunks/setAll.js +2 -0
  35. package/dist/split/chunks/setAll.js.map +1 -0
  36. package/dist/split/chunks/version.js +2 -0
  37. package/dist/split/chunks/version.js.map +1 -0
  38. package/dist/split/chunks/watchRuntime.js +2 -0
  39. package/dist/split/chunks/watchRuntime.js.map +1 -0
  40. package/dist/split/core.d.ts +1417 -0
  41. package/dist/split/core.js +2 -0
  42. package/dist/split/core.js.map +1 -0
  43. package/dist/split/features/devtools.d.ts +9 -0
  44. package/dist/split/features/devtools.js +2 -0
  45. package/dist/split/features/devtools.js.map +1 -0
  46. package/dist/split/features/diagnostics.d.ts +11 -0
  47. package/dist/split/features/diagnostics.js +2 -0
  48. package/dist/split/features/diagnostics.js.map +1 -0
  49. package/dist/split/features/formats.d.ts +10 -0
  50. package/dist/split/features/formats.js +2 -0
  51. package/dist/split/features/formats.js.map +1 -0
  52. package/dist/split/features/recursion.d.ts +9 -0
  53. package/dist/split/features/recursion.js +2 -0
  54. package/dist/split/features/recursion.js.map +1 -0
  55. package/dist/split/features/scopes.d.ts +10 -0
  56. package/dist/split/features/scopes.js +2 -0
  57. package/dist/split/features/scopes.js.map +1 -0
  58. package/dist/split/features/ssr.d.ts +9 -0
  59. package/dist/split/features/ssr.js +2 -0
  60. package/dist/split/features/ssr.js.map +1 -0
  61. package/dist/split/features/temporal.d.ts +9 -0
  62. package/dist/split/features/temporal.js +2 -0
  63. package/dist/split/features/temporal.js.map +1 -0
  64. package/package.json +13 -1
package/README.ja.md CHANGED
@@ -177,6 +177,57 @@
177
177
  </script>
178
178
  ```
179
179
 
180
+ ### 分割エントリ(使う機能だけを入れる)
181
+
182
+ `@wcstack/state` と `/auto` は全機能入りで、こちらが引き続き推奨です(全部使うページなら、core +
183
+ 機能に分けるより 1 ファイルのほうが小さい)。機能を意図的に落とすページは、組み合わせて入れられます。
184
+
185
+ ```html
186
+ <script type="importmap">
187
+ {
188
+ "imports": {
189
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/core.js",
190
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/features/temporal.js",
191
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/features/scopes.js"
192
+ }
193
+ }
194
+ </script>
195
+ <script type="module">
196
+ import { bootstrapState, installFeatures } from '@wcstack/state/core';
197
+ import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $streams
198
+ import scopes from '@wcstack/state/features/scopes'; // bind-component・mount=・DCC
199
+
200
+ installFeatures([temporal, scopes]);
201
+ bootstrapState();
202
+ </script>
203
+ ```
204
+
205
+ 分割の形は、パッケージのファイルそのものから読みます — 上の例のように jsDelivr の素の `/npm/` パスで
206
+ 版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
207
+ 通します。**`esm.run` からは読まないでください**。`+esm` はエントリごとにサーバー側で再バンドルし、
208
+ 共有の core チャンクをそれぞれに取り込むため、エントリごとに別のエンジンを抱えます。機能は core から
209
+ 見えないコピーへ install され、`installFeatures` を呼んでも `[wcs/feature-not-installed]` で落ちます。
210
+ 素のファイルなら、どのエントリの相対 import も同じチャンクの URL に解決され、エンジンは 1 回だけ評価
211
+ されます。この形の integrity は [docs/sri.ja.md §5.1](../../docs/sri.ja.md#51-wcstackstate-の分割エントリ) を参照。
212
+
213
+ | エントリ | 足されるもの |
214
+ |---|---|
215
+ | `@wcstack/state/core` | バインディングの本体: `data-wcs`・`for` / `if`・パス getter・フィルタ・イベント・`$command` / `$on`・`bootstrapState`・`installFeatures` |
216
+ | `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$streams` |
217
+ | `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
218
+ | `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
219
+ | `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
220
+ | `@wcstack/state/features/formats` | 書式フィルタ群(`uc`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
221
+ | `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
222
+ | `@wcstack/state/features/diagnostics` | 開発時の警告: 束縛・`$watch`・`$scan` のパスが state 上で解決できないとき、did-you-mean 付きで知らせる。入れなければ静か(本番向け)。throw するエラーの文言はどちらでも変わらない |
223
+ | `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
224
+
225
+ 機能が入っていない宣言は黙って無視されません。state の定義時に
226
+ `[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
227
+ で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。install は冪等で、
228
+ どのエントリも core のチャンクを 1 つだけ共有します(機能側がエンジンの 2 つ目のコピーを抱える
229
+ ことはありません)。
230
+
180
231
  ## 基本的な使い方
181
232
 
182
233
  ```html
@@ -284,6 +335,20 @@
284
335
 
285
336
  ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
286
337
 
338
+ **スコープごとに動くもの**(3.0 で 1 つの表にしました — 要件 B11。ここに無言で無視されるものはありません):
339
+
340
+ | 宣言 | ルートの `<wcs-state>` | ボリューム `<wcs-state mount="p">` | マウントされたコンポーネント(`state: …` 付きの `bind-component`) |
341
+ |---|---|---|---|
342
+ | データキー・getter・setter・メソッド | 動く | 動く(`p` 相対) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
343
+ | `$connectedCallback` / `$disconnectedCallback` | 動く | 動く(`p` 相対) | 動く |
344
+ | `$watch`・`$listKeys`・`$updatedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
345
+ | `$streams`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
346
+ | `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — 警告 | 動かない — 警告 |
347
+ | `$errorCallback` | 動く | 動かない — 警告(3.0 より前は無言) | 動かない — 警告(3.0 より前は無言) |
348
+ | 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着して枠を返す。`connectedCallbackPromise` は resolve(`scopes` 機能が無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
349
+
350
+ マウントされていないコンポーネント(自前の `<wcs-state>` を持つ素の Shadow DOM の子)はそれ自体がルートで、1 列目のすべてが動きます。
351
+
287
352
  > **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。
288
353
 
289
354
  ## 状態の更新
@@ -346,6 +411,8 @@ property[#modifier]: path[|filter[|filter(args)...]]
346
411
  <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
347
412
  ```
348
413
 
414
+ 区切りの `;` と `|` は引用符の外でだけ区切るので(3.0)、引用符付きのフィルタ引数に含められます: `textContent: tags|join('; ')`、`title: parts|join(' | ')`。3.0 より前はどちらも束縛が壊れていました。
415
+
349
416
  | 要素 | 説明 | 例 |
350
417
  |---|---|---|
351
418
  | `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
@@ -369,8 +436,6 @@ property[#modifier]: path[|filter[|filter(args)...]]
369
436
  | `checkbox` | チェックボックスグループの配列バインディング(双方向) |
370
437
  | `onclick`, `on*` | イベントハンドラバインディング |
371
438
 
372
- `undefined` は書き込みをスキップします。要素は表示していたものを保ち、再利用されたリスト行では**前の行の**テキストが残ります。「値なし」は `""` か `null` を返してください(3.0 は表示のプロパティへの `undefined` を空にします — [3.0 への準備](#30-への準備wcsv3-migration))。
373
-
374
439
  ### 修飾子
375
440
 
376
441
  | 修飾子 | 説明 |
@@ -384,6 +449,10 @@ property[#modifier]: path[|filter[|filter(args)...]]
384
449
 
385
450
  複数の修飾子は 1 つの `#` の後にカンマ区切りで書きます: `value#ro,init=none: path`
386
451
 
452
+ 修飾子は束縛の種別を変えません: `radio#ro:` と `checkbox#ro:` は radio / checkbox の束縛のままです(3.0)。3.0 より前は修飾子を付けると `radio` という名前の素のプロパティ束縛になり、radio グループの `#ro` が効いていませんでした。
453
+
454
+ 3.0 からパーサは、以前黙って丸めていた書き方を `[wcs/binding-syntax]` で拒否します: 2 つ目の `#`(`value#ro#wo` は `ro` だけ残して後ろを捨てていた — `value#ro,wo` と書く)、`else:` の後ろの値(`else:` と書く)、`for` / `if` / `elseif` / `else` / `...` の左辺の修飾子やフィルタ、フィルタ引数の閉じていない引用符。
455
+
387
456
  ### 双方向バインディング
388
457
 
389
458
  以下の要素で自動的に有効化されます:
@@ -561,7 +630,7 @@ export default {
561
630
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
562
631
  ```
563
632
 
564
- **`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
633
+ **`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。**表示の表面は別です(3.0):** `textContent` / `innerText` / `innerHTML`・mustache のテキスト・`attr.*`・`style.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除されます。3.0 より前は `textContent:` も `undefined` をスキップしていたため使い回したリストの行に前の行の文字が残り、属性には文字列 `"undefined"` / `"null"` が入っていました。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
565
634
 
566
635
  **制約事項**:
567
636
 
@@ -1021,13 +1090,13 @@ export default {
1021
1090
  };
1022
1091
  ```
1023
1092
 
1024
- 規則: 3 つの呼び出しが購読を張るのはリスト行の getter の中で評価されたときだけで、それ以外では比較結果を返すだけです。行の購読はリスト差分がその行を外した時点で落ちます。鍵の比較は `Object.is` ですが、Map の意味論により `+0` と `-0`、`NaN` 同士は同じ鍵になります。`$eqPath` は鍵を依存なしで読むので、行の鍵がその場で書き換わっても行は再評価されません — 変わらない同一性(id)に使い、鍵自体が動く場合は追跡付きの読みと `$eq` を組み合わせてください。
1093
+ 規則: `$eq` と `$eqPath` は getter の中で評価されれば購読を張ります — 行の getter でも、`get isAdmin() { return this.$eq("role", "admin"); }` のようなトップレベルの getter でも同じです。メソッドやハンドラから呼ぶと比較結果を返すだけです。`$eqIndex` にはリスト行が要ります: 行の外では `$eqIndex("…") needs a list row scope.` を投げ、その深さにリストの index が無い `level` でも投げます。行の購読はリスト差分がその行を外した時点で落ちます。鍵の比較は `Object.is` ですが、Map の意味論により `+0` と `-0`、`NaN` 同士は同じ鍵になります。`$eqPath` は鍵を依存なしで読むので、行の鍵がその場で書き換わっても行は再評価されません — 変わらない同一性(id)に使い、鍵自体が動く場合は追跡付きの読みと `$eq` を組み合わせてください。
1025
1094
 
1026
1095
  行に届くもの:
1027
1096
 
1028
1097
  - **`path` への書き込み** — オブジェクトを含むどんな値でも(`$eq("selected", this["items.*"])` に対する `this.selected = row`): 選ばれていた行と、新たに選ばれる行。
1029
1098
  - **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
1030
- - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。
1099
+ - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出し、この形に落ちた path には `tracked` バッジを付けます(3.0)。
1031
1100
  - **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
1032
1101
 
1033
1102
  ### ループインデックス変数(`$1`, `$2`, ...)
@@ -1159,7 +1228,7 @@ this.$setAll("users.*", [], rows, { spread: true }); // 配列を保ったま
1159
1228
 
1160
1229
  #### `$resolve` — 明示的なインデックスでのアクセス
1161
1230
 
1162
- `$resolve` は特定のワイルドカードインデックスの値を読み書きします:
1231
+ `$resolve` は特定のワイルドカードインデックスの値を読み書きします。**読みか書きかは引数の個数で決まります**(3.0): `$resolve(path, indexes)` は読み、`$resolve(path, indexes, value)` は `value` の書き込みで、`undefined` も書きます(3.0 より前は第 3 引数が `undefined` なら読みでした)。readonly のプロキシでは、この書き込みは直接代入と同じく `This state is readonly.` で throw し、`$setAll` も同じです(3.0 より前はどちらも readonly のプロキシから書けていました):
1163
1232
 
1164
1233
  ```javascript
1165
1234
  export default {
@@ -1422,8 +1491,6 @@ export default {
1422
1491
  | `gt(n)` | より大きい | `count\|gt(0)` |
1423
1492
  | `ge(n)` | 以上 | `count\|ge(0)` |
1424
1493
 
1425
- `eq` / `ne` は数値の値を数値として、それ以外を引数の**文字列**と比べます。そのため 2.x では真偽値が `eq(true)` に一致することはなく、`done|eq(true)` は常に `false` です。真偽値そのものをバインドする(`class.done: .done`)か `not` を使ってください。3.0 は引用符の無い `true` / `false` / `null` を型付きの値として読みます([3.0 への準備](#30-への準備wcsv3-migration))。
1426
-
1427
1494
  ### 算術
1428
1495
 
1429
1496
  | フィルタ | 説明 | 例 |
@@ -1489,8 +1556,8 @@ export default {
1489
1556
 
1490
1557
  | フィルタ | 説明 | 例 |
1491
1558
  |---|---|---|
1492
- | `truthy` | truthy チェック | `value\|truthy` |
1493
- | `falsy` | falsy チェック | `value\|falsy` |
1559
+ | `truthy` | truthy チェック — JavaScript の真偽判定そのもので `boolean` と同じ(3.0 から `0n` は偽) | `value\|truthy` |
1560
+ | `falsy` | falsy チェック(JavaScript の真偽判定。`defaults` も同じ判定を使う) | `value\|falsy` |
1494
1561
  | `defaults(v)` | フォールバック値 | `name\|defaults(Anonymous)` |
1495
1562
 
1496
1563
  ### フィルタチェーン
@@ -1501,6 +1568,10 @@ export default {
1501
1568
  <div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
1502
1569
  ```
1503
1570
 
1571
+ フィルタは束縛計画の段で解決されます。未知の名前は `[wcs/filter-unknown]`(did-you-mean 付き)で、3.0 からは受け付ける個数の外の引数も `[wcs/filter-arity]`(`join(a,b)`: "accepts at most 1 argument(s) (2 given)")で落ちます — lint と同じコード・同じ範囲です。引数は構造のままキャッシュされるので、`join('a,b')` と `join(a)` を取り違えません。
1572
+
1573
+ **引数のリテラルは型を持ちます(3.0)。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し(3.0 より前は文字列 `"true"` と比べていて一致しなかった)、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列の比べ方は変わりません — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` は今も `eq(1)` に一致します。`defaults(v)` は型付きの値を返します: `defaults(0)` は `0`、`defaults('0')` は `"0"`、`defaults(null)` は `null`。
1574
+
1504
1575
  ## Web Component バインディング
1505
1576
 
1506
1577
  `@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
@@ -1618,10 +1689,16 @@ customElements.define("user-card", UserCard);
1618
1689
  - 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段です
1619
1690
 
1620
1691
  > プロパティ単位の形(`state.message: user.name`)はそのまま動きます — 同じ機構の上の部分マウントです。
1621
- > R1 はすべてのマウント形で厳格です — マップされるキーに既定値を
1622
- > 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)は自前のキーが
1623
- > **私有**になり、ホストの値を隠します(1 回だけ `wcs/mount-own-key-shadow` が指します)。ツリーを
1624
- > 読むには既定値を消してください。Light DOM のマウントに `name` は不要で、
1692
+ > **明示した部分マウントはコンポーネント自前のキーに勝ちます(3.0):** マップされるキーに既定値を
1693
+ > 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)もホストの値を読み、
1694
+ > 既定値は使われません(2.x では R1 によって自前のキーが私有になり、ホストの値を隠していました —
1695
+ > `wcs/mount-own-key-shadow` の警告付き)。マップしていない自前のキーは今も R1 で私有です。
1696
+ >
1697
+ > **マウントの `#ro` を尊重します(3.0):** `state#ro: user` や `state.title#ro: doc.title` では、
1698
+ > コンポーネントはそのエントリを読めても書けません — `element.state.title = …`・メソッド内の
1699
+ > `this.title = …`・`$setAll` / `$resolve` の書き込みは `[wcs/mount-readonly]` で落ち、コンポーネント内の
1700
+ > 双方向束縛(`value: title`)は書き戻しません。ホストはそのパスに書けます(2.x は修飾子を受け付けて
1701
+ > 無視していました)。Light DOM のマウントに `name` は不要で、
1625
1702
  > `element.state`(および getter / メソッド内の `this`)の `$getAll` / `$setAll` / `$resolve` /
1626
1703
  > `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
1627
1704
  > 添字は自動で前置されます。
@@ -2679,38 +2756,6 @@ this.$getAll("matrix.*.*", [row]);
2679
2756
  | `$watch` の書き込み連鎖 | 32 | そのバッチの watch 発火をスキップ |
2680
2757
  | バインディングの適用失敗 | — | その 1 本のみスキップ |
2681
2758
 
2682
- ### 3.0 への準備(`wcs/v3-migration`)
2683
-
2684
- 3.0 は互換層を持ちません。代わりにこのリリースは、3.0 が拒否する書き方・読み方が変わる書き方を 2.x のまま動かしつつ名指しで知らせます。警告は書き方とサイトごとに 1 回だけ、コード `wcs/v3-migration` で出し、3.0 での扱いと今の書き換え先を示します:
2685
-
2686
- ```
2687
- [@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
2688
- See "Preparing for 3.0" in the @wcstack/state README.
2689
- ```
2690
-
2691
- | 書き方 | 2.x | 3.0 | 今の書き方 |
2692
- |---|---|---|---|
2693
- | 2 つ目の `#`(`value#ro#wo:`) | 最初の修飾子列だけ残す | `[wcs/binding-syntax]` | `value#ro,wo:` |
2694
- | `else:` の後ろの値 | 無視 | `[wcs/binding-syntax]` | `else:` |
2695
- | `for` / `if` / `elseif` / `else` / `...` の修飾子・フィルタ | ただのプロパティのバインディングになる | `[wcs/binding-syntax]` | キーワードだけ |
2696
- | `radio#ro:` / `checkbox#ro:` | `radio` という名前のプロパティになり効かない | 修飾子を守る radio / checkbox のバインディング | —(意図どおりか確かめる) |
2697
- | フィルタ引数の閉じていない引用符 | 黙って閉じる | `[wcs/binding-syntax]` | 引用符を閉じる |
2698
- | フィルタが受け取る数を超える引数 | 余りは無視 | `[wcs/filter-arity]` | 余りを消す |
2699
- | `eq` / `ne` の引用符の無い `true` / `false` / `null`(`eq(true)`) | 真偽値・`null` の値を文字列と比べる | 型付きの値と比べる | 文字列で比べ続けるなら `eq('true')` |
2700
- | `defaults` の引用符の無い `true` / `false` / `null` | 文字列(`"null"`)を既定値にする | 型付きの値を既定値にする | 文字列のままなら `defaults('null')` |
2701
- | `truthy` / `falsy` / `defaults` に来た `0n` | 真 | 偽(JavaScript の真偽判定) | — |
2702
- | `textContent` / `innerText` / `innerHTML` への `undefined` | 前のテキストを残す(使い回した行では前の行のもの) | 空にする | 「値なし」は `""` か `null` を返す |
2703
- | `attr.*` への `undefined` / `null` | `"undefined"` / `"null"` を書く | 属性を削除する | — |
2704
- | `style.*` への `undefined` | 前の値を残す | 消す | — |
2705
- | `$resolve(path, indexes, undefined)` | 読む | `undefined` を書く(引数の個数で決める) | 読むなら `$resolve(path, indexes)` |
2706
- | readonly のプロキシからの `$resolve(path, indexes, value)` / `$setAll` | 書く | `This state is readonly.` を投げる | `createState("writable", …)` から書く |
2707
- | `#ro` のマウント(`state#ro: user`)を通るコンポーネントの書き込み | ホストのツリーに書く | `[wcs/mount-readonly]` | ホストで書くか、`#ro` を外す |
2708
- | 部分マウントに隠された自前の既定値(コンポーネントが `name` を宣言し、`state.name: user.name`) | 既定値が勝つ(警告あり) | 明示したマウントが勝つ | 既定値を消す |
2709
-
2710
- もう 1 つは予告でなく先に入れています:ボリュームやマウントされたコンポーネントの `$errorCallback` は黙って無視していましたが、ルート専用のキー(`$commandTokens`、`$on` など)を挙げる既存の警告で名指しするようにしました(3.0 と同じ)。動くのは今もルートの state だけです。
2711
-
2712
- 値の警告はその値が実際に来たときにだけ出ます。コンソールが静かでも、テストが通らなかった経路については何も分かりません。文法の行はバインディングを最初に解析したときに調べます。`npx @wcstack/lint <file>`(と VS Code 拡張)も同じ判定で `wcs/v3-migration`(info)として報告するので、ページを動かさずに見つけられます。
2713
-
2714
2759
  ## 設定
2715
2760
 
2716
2761
  `bootstrapState()` に部分的な設定オブジェクトを渡します:
package/README.md CHANGED
@@ -177,6 +177,58 @@ Every row is a section of this README. Unless it appears under [Where the neighb
177
177
  </script>
178
178
  ```
179
179
 
180
+ ### Split entries (only the features you use)
181
+
182
+ `@wcstack/state` and `/auto` ship everything, and they stay the recommended way in: a page that
183
+ uses every feature is smaller as one file than as core plus features. When a page deliberately
184
+ leaves features out, it can compose them instead:
185
+
186
+ ```html
187
+ <script type="importmap">
188
+ {
189
+ "imports": {
190
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/core.js",
191
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/features/temporal.js",
192
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.0.0/dist/split/features/scopes.js"
193
+ }
194
+ }
195
+ </script>
196
+ <script type="module">
197
+ import { bootstrapState, installFeatures } from '@wcstack/state/core';
198
+ import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $streams
199
+ import scopes from '@wcstack/state/features/scopes'; // bind-component, mount=, DCC
200
+
201
+ installFeatures([temporal, scopes]);
202
+ bootstrapState();
203
+ </script>
204
+ ```
205
+
206
+ Load the split form from the package's own files — jsDelivr's plain, version-pinned `/npm/` path as
207
+ above (it does not read `exports`, so name the file under `dist/split/`), or a bundler — and **never
208
+ through `esm.run`**. Its `+esm` endpoint re-bundles every entry on the server and inlines the shared
209
+ core chunk into each one, so every entry would carry its own engine: a feature would install into a
210
+ copy the core never sees, and the page throws `[wcs/feature-not-installed]` in spite of
211
+ `installFeatures`. From the plain files, every entry's relative import resolves to the same chunk URL,
212
+ so the browser evaluates the engine once. Integrity for this form: [docs/sri.md §5.1](../../docs/sri.md#51-the-split-entries-of-wcstackstate).
213
+
214
+ | Entry | What it adds |
215
+ |---|---|
216
+ | `@wcstack/state/core` | The binding engine: `data-wcs`, `for` / `if`, path getters, filters, events, `$command` / `$on`, `bootstrapState`, `installFeatures` |
217
+ | `@wcstack/state/features/temporal` | `$watch`, `$scan`, `$streams` |
218
+ | `@wcstack/state/features/scopes` | `bind-component`, `mount=` volumes, overlay exports, DCC (`data-wc-definition`) |
219
+ | `@wcstack/state/features/recursion` | `$recursion` and `**` paths |
220
+ | `@wcstack/state/features/ssr` | `enable-ssr`: server rendering and hydration |
221
+ | `@wcstack/state/features/formats` | The formatting filters (`uc`, `date`, `round`, `truncate`, …). The core answers only `not`, which `if` / `else` need |
222
+ | `@wcstack/state/features/devtools` | The DevTools hook protocol source |
223
+ | `@wcstack/state/features/diagnostics` | Development-time warnings: a bound / `$watch` / `$scan` path that does not resolve on the state is reported with a did-you-mean. Without it the page stays silent — thrown errors keep their full messages either way |
224
+ | `@wcstack/state/define` | `defineState` and the types only — no runtime at all |
225
+
226
+ A declaration whose feature is missing does not fail quietly: it throws
227
+ `[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
228
+ when the state is defined, and a filter with no implementation throws `[wcs/filter-unknown]` when the
229
+ bindings are planned. Installing is idempotent, and every entry shares one core chunk (a feature
230
+ never carries a second copy of the engine).
231
+
180
232
  ## Basic Usage
181
233
 
182
234
  ```html
@@ -284,6 +336,20 @@ There is **one state tree per root**. To split state across modules, mount a vol
284
336
 
285
337
  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 free — even 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.
286
338
 
339
+ **What each scope runs** (3.0 states it as one table — requirement B11; nothing here is ignored silently):
340
+
341
+ | Declaration | Root `<wcs-state>` | Volume `<wcs-state mount="p">` | Mounted component (`bind-component` with `state: …`) |
342
+ |---|---|---|---|
343
+ | Data keys, getters, setters, methods | yes | yes, relative to `p` | yes — own keys are private unless the host maps them; getters are exported |
344
+ | `$connectedCallback` / `$disconnectedCallback` | yes | yes, relative to `p` | yes |
345
+ | `$watch`, `$listKeys`, `$updatedCallback` | yes | yes, relative to `p` | not run — one `wcs/mount-dollar-declaration` warning |
346
+ | `$streams`, `$scan`, `$recursion`, `**` getters | yes | rejected with an error before grafting | not run — warning |
347
+ | `$commandTokens`, `$eventTokens`, `$on` | yes | not run — warning | not run — warning |
348
+ | `$errorCallback` | yes | not run — warning (silent before 3.0) | not run — warning (silent before 3.0) |
349
+ | An initialization failure | reported once; `connectedCallbackPromise` rejects | settles without grafting and releases its slot; `connectedCallbackPromise` resolves (a missing `scopes` feature rejects it) | reported once; the component's `connectedCallbackPromise` rejects |
350
+
351
+ A component that is not mounted (a plain Shadow DOM child with its own `<wcs-state>`) is a root of its own and runs everything in the first column.
352
+
287
353
  > **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.
288
354
 
289
355
  ## Updating State
@@ -346,6 +412,8 @@ Multiple bindings separated by `;`:
346
412
  <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
347
413
  ```
348
414
 
415
+ The separators `;` and `|` split only outside quotes (3.0), so a quoted filter argument may contain them: `textContent: tags|join('; ')`, `title: parts|join(' | ')`. Before 3.0 both broke the binding.
416
+
349
417
  | Part | Description | Example |
350
418
  |---|---|---|
351
419
  | `property` | DOM property to bind | `value`, `textContent`, `checked` |
@@ -369,8 +437,6 @@ Multiple bindings separated by `;`:
369
437
  | `checkbox` | Checkbox group binding to array (two-way) |
370
438
  | `onclick`, `on*` | Event handler binding |
371
439
 
372
- `undefined` skips the write: the element keeps what it showed, and in a reused list row that is the **previous row's** text. Return `""` or `null` for "no value" (3.0 empties display properties on `undefined` — [Preparing for 3.0](#preparing-for-30-wcsv3-migration)).
373
-
374
440
  ### Modifiers
375
441
 
376
442
  | Modifier | Description |
@@ -384,6 +450,10 @@ Multiple bindings separated by `;`:
384
450
 
385
451
  Multiple modifiers are comma-separated after a single `#`: `value#ro,init=none: path`.
386
452
 
453
+ A modifier never changes the kind of binding: `radio#ro:` and `checkbox#ro:` stay radio / checkbox bindings (3.0). Before 3.0 a modifier turned them into a plain property named `radio`, so `#ro` on a radio group did not take effect.
454
+
455
+ As of 3.0 the parser rejects what it used to round off silently, with `[wcs/binding-syntax]`: a second `#` (`value#ro#wo` kept `ro` and dropped the rest — write `value#ro,wo`), a value after `else:` (write `else:`), modifiers or left-side filters on `for` / `if` / `elseif` / `else` / `...`, and an unterminated quote in filter arguments.
456
+
387
457
  ### Two-Way Binding
388
458
 
389
459
  Automatically enabled for:
@@ -561,7 +631,7 @@ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name
561
631
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
562
632
  ```
563
633
 
564
- **`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding, not just spread; with `config.debug` each skipped write is logged via `console.debug`.)
634
+ **`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding that feeds an element input, not just spread; with `config.debug` each skipped write is logged via `console.debug`.) **Display surfaces are different (3.0):** `textContent` / `innerText` / `innerHTML`, mustache text, `attr.*` and `style.*` have no element default worth keeping, so `undefined` and `null` both mean "no value" there — the text becomes empty and the attribute or style is removed. Before 3.0 a `textContent:` binding skipped `undefined` too, which left the previous row's text in a reused list row, and an attribute got the string `"undefined"` / `"null"`.
565
635
 
566
636
  **Constraints**:
567
637
 
@@ -1021,13 +1091,13 @@ export default {
1021
1091
  };
1022
1092
  ```
1023
1093
 
1024
- Rules: the three calls only subscribe when evaluated inside a getter under a list row (elsewhere they just return the comparison); a row's subscription is dropped when the list diff removes the row; keys compare with `Object.is` except that Map semantics treat `+0`/`-0` and `NaN`/`NaN` as equal. `$eqPath` reads the key without a dependency, so a row whose key changes in place is not re-evaluated by that change — use it for identities that do not change (ids), and `$eq` with a tracked read when the key itself is live.
1094
+ Rules: `$eq` and `$eqPath` subscribe when evaluated inside any getter — a row getter, or a top-level one such as `get isAdmin() { return this.$eq("role", "admin"); }` — and called from a method or a handler they just return the comparison. `$eqIndex` needs a list row: outside one it throws `$eqIndex("…") needs a list row scope.`, and a `level` with no list index at that depth throws too. A row's subscription is dropped when the list diff removes the row; keys compare with `Object.is` except that Map semantics treat `+0`/`-0` and `NaN`/`NaN` as equal. `$eqPath` reads the key without a dependency, so a row whose key changes in place is not re-evaluated by that change — use it for identities that do not change (ids), and `$eq` with a tracked read when the key itself is live.
1025
1095
 
1026
1096
  What reaches the rows:
1027
1097
 
1028
1098
  - **A write to `path`** — of any value, objects included (`this.selected = row` with `$eq("selected", this["items.*"])`): the row that was selected and the row that becomes selected.
1029
1099
  - **A write to an object above `path`** — `this.sel = { id: 2 }` for `$eq("sel.id", …)`: the same two rows, keyed by the value `path` had under the old object and has under the new one.
1030
- - **A `path` that is a getter, or sits under one** (`$eq("current.id", …)` with `get current()`): its value changes without a write to it, so the calls fall back to an ordinary tracked read. The selection stays correct, but a change re-evaluates every row, as without the keyed form. Point `path` at the written state (`selectedId`) to keep the two-row cost.
1100
+ - **A `path` that is a getter, or sits under one** (`$eq("current.id", …)` with `get current()`): its value changes without a write to it, so the calls fall back to an ordinary tracked read. The selection stays correct, but a change re-evaluates every row, as without the keyed form. Point `path` at the written state (`selectedId`) to keep the two-row cost. The State pane of `@wcstack/devtools` counts the subscriptions per path under **Keyed selection** and marks a path that fell back this way with a `tracked` badge (3.0).
1031
1101
  - **No type conversion:** `"2"` does not match the id `2`. An `<input>` or `<select>` writes strings, so convert on the way in (`value|number: selectedId`) or keep the ids as strings.
1032
1102
 
1033
1103
  ### Loop Index Variables (`$1`, `$2`, ...)
@@ -1160,7 +1230,7 @@ One thing `$setAll` is not: a shortcut for the dependency walk. Rendering still
1160
1230
 
1161
1231
  #### `$resolve` — Access by Explicit Index
1162
1232
 
1163
- `$resolve` reads or writes a value at a specific wildcard index:
1233
+ `$resolve` reads or writes a value at a specific wildcard index. **The argument count decides which** (3.0): `$resolve(path, indexes)` reads, and `$resolve(path, indexes, value)` writes `value` — `undefined` included (before 3.0 an `undefined` third argument was a read). On a readonly proxy the write throws `This state is readonly.`, exactly as a direct assignment does, and so does `$setAll` (before 3.0 both helpers wrote through a readonly proxy):
1164
1234
 
1165
1235
  ```javascript
1166
1236
  export default {
@@ -1423,8 +1493,6 @@ export default {
1423
1493
  | `gt(n)` | Greater than | `count\|gt(0)` |
1424
1494
  | `ge(n)` | Greater than or equal | `count\|ge(0)` |
1425
1495
 
1426
- `eq` / `ne` compare a number value numerically and anything else with the argument **as text**, so a boolean never equals `eq(true)` in 2.x: `done|eq(true)` is always `false`. Bind the boolean itself (`class.done: .done`) or use `not`. 3.0 reads an unquoted `true` / `false` / `null` as the typed value ([Preparing for 3.0](#preparing-for-30-wcsv3-migration)).
1427
-
1428
1496
  ### Arithmetic
1429
1497
 
1430
1498
  | Filter | Description | Example |
@@ -1490,8 +1558,8 @@ export default {
1490
1558
 
1491
1559
  | Filter | Description | Example |
1492
1560
  |---|---|---|
1493
- | `truthy` | Truthy check | `value\|truthy` |
1494
- | `falsy` | Falsy check | `value\|falsy` |
1561
+ | `truthy` | Truthy check — JavaScript's own truthiness, the same as `boolean` (so `0n` is falsy since 3.0) | `value\|truthy` |
1562
+ | `falsy` | Falsy check (JavaScript's truthiness; `defaults` uses the same test) | `value\|falsy` |
1495
1563
  | `defaults(v)` | Fallback value | `name\|defaults(Anonymous)` |
1496
1564
 
1497
1565
  ### Filter Chaining
@@ -1502,6 +1570,10 @@ Filters can be chained with `|`:
1502
1570
  <div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
1503
1571
  ```
1504
1572
 
1573
+ A filter is resolved when the bindings are planned. An unknown name throws `[wcs/filter-unknown]` (with a did-you-mean), and — as of 3.0 — an argument count outside what the filter accepts throws `[wcs/filter-arity]` (`join(a,b)`: "accepts at most 1 argument(s) (2 given)"), the same code and bounds lint reports. Arguments are cached by their structure, so `join('a,b')` and `join(a)` are never confused.
1574
+
1575
+ **Argument literals are typed (3.0).** An unquoted `true`, `false`, `null` or number is that value; a quoted argument is a string. The comparison filters use it for booleans and `null`: `done|eq(true)` matches `true` (before 3.0 it compared with the string `"true"` and never matched), `eq('true')` does not, and `eq(null)` matches `null`. Numbers and strings compare as before — a numeric value against the number, a string value against the text — so a form value `"1"` still matches `eq(1)`. `defaults(v)` returns the typed value: `defaults(0)` gives `0`, `defaults('0')` gives `"0"`, `defaults(null)` gives `null`.
1576
+
1505
1577
  ## Web Component Binding
1506
1578
 
1507
1579
  `@wcstack/state` supports bidirectional state binding with custom elements using Shadow DOM or Light DOM.
@@ -1618,10 +1690,17 @@ customElements.define("user-card", UserCard);
1618
1690
  - Mounting an array as the root (`state: rows` with `for` over it inside) is not supported; mount the row (`state: .`) or the object that holds the array (`state: group` with `for: children` inside). Both forms are contract-tested; mounts are the only way to extend the tree.
1619
1691
 
1620
1692
  > The per-property form (`state.message: user.name`) keeps working — it is a partial mount on
1621
- > the same machinery. R1 is strict for every mount form — a component that declares a default
1622
- > for a mapped key (`state = { message: "" }` together with `state.message: ...`) keeps its
1623
- > own key **private**, hiding the host value (a one-time `wcs/mount-own-key-shadow` warning
1624
- > points at it). Drop the default to read the tree. The mounted `<wcs-state>` needs no `name`
1693
+ > the same machinery. **An explicit partial mount wins over the component's own key (3.0):** a
1694
+ > component that declares a default for a mapped key (`state = { message: "" }` together with
1695
+ > `state.message: ...`) reads the host value; the default is simply not used. (In 2.x R1 made
1696
+ > that own key private and it hid the host value, with a `wcs/mount-own-key-shadow` warning.)
1697
+ > R1 still keeps every *unmapped* own key private.
1698
+ >
1699
+ > **`#ro` on a mount is honoured (3.0):** `state#ro: user` or `state.title#ro: doc.title`
1700
+ > lets the component read the entry but not write it — `element.state.title = …`, `this.title = …`
1701
+ > in a method and `$setAll` / `$resolve` writes throw `[wcs/mount-readonly]`, and a two-way
1702
+ > binding inside the component (`value: title`) does not write back. The host can still write the
1703
+ > path. (2.x accepted the modifier and ignored it.) The mounted `<wcs-state>` needs no `name`
1625
1704
  > in Light DOM, and `$getAll` / `$setAll` / `$resolve` / `$postUpdate` on `element.state`
1626
1705
  > (and on `this` inside getters/methods) speak the component's own vocabulary — paths are
1627
1706
  > translated onto the mount and the host row's indexes are prepended automatically.
@@ -2683,38 +2762,6 @@ Every failure mode reports and continues; nothing already applied is reverted:
2683
2762
  | `$watch` write chain | 32 | Skip watch firing for that batch |
2684
2763
  | Binding apply failure | — | Skip that one binding |
2685
2764
 
2686
- ### Preparing for 3.0 (`wcs/v3-migration`)
2687
-
2688
- 3.0 has no compatibility layer. Instead, this release names each form that 3.0 rejects or reads differently, while still running it the 2.x way. Each warning is printed once per form and site, under the code `wcs/v3-migration`, and says what 3.0 does and what to write now:
2689
-
2690
- ```
2691
- [@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
2692
- See "Preparing for 3.0" in the @wcstack/state README.
2693
- ```
2694
-
2695
- | Form | 2.x | 3.0 | Write now |
2696
- |---|---|---|---|
2697
- | A second `#` (`value#ro#wo:`) | Keeps the first modifier list | `[wcs/binding-syntax]` | `value#ro,wo:` |
2698
- | A value after `else:` | Ignored | `[wcs/binding-syntax]` | `else:` |
2699
- | Modifiers or filters on `for` / `if` / `elseif` / `else` / `...` | A plain property binding | `[wcs/binding-syntax]` | The bare keyword |
2700
- | `radio#ro:` / `checkbox#ro:` | A property named `radio`, no effect | A radio / checkbox binding that honours the modifiers | — (check the binding still does what you meant) |
2701
- | An unterminated quote in filter arguments | Closed silently | `[wcs/binding-syntax]` | Close the quote |
2702
- | More filter arguments than the filter takes | The rest ignored | `[wcs/filter-arity]` | Remove them |
2703
- | Unquoted `true` / `false` / `null` in `eq` / `ne` (`eq(true)`) | Compares a boolean or `null` value with the string | Compares with the typed value | `eq('true')` to keep the text comparison |
2704
- | Unquoted `true` / `false` / `null` in `defaults` | Falls back to the string (`"null"`) | Falls back to the typed value | `defaults('null')` to keep the text |
2705
- | `0n` through `truthy` / `falsy` / `defaults` | Truthy | Falsy (JavaScript truthiness) | — |
2706
- | `undefined` into `textContent` / `innerText` / `innerHTML` | Keeps the previous text (in a reused row, the previous row's) | Empties it | Return `""` or `null` for "no value" |
2707
- | `undefined` / `null` into `attr.*` | Writes `"undefined"` / `"null"` | Removes the attribute | — |
2708
- | `undefined` into `style.*` | Keeps the previous value | Clears it | — |
2709
- | `$resolve(path, indexes, undefined)` | Reads | Writes `undefined` (the argument count decides) | `$resolve(path, indexes)` to read |
2710
- | `$resolve(path, indexes, value)` / `$setAll` on a readonly proxy | Writes | Throws `This state is readonly.` | Write from `createState("writable", …)` |
2711
- | A component writing through a `#ro` mount (`state#ro: user`) | Writes the host's tree | `[wcs/mount-readonly]` | Write on the host, or drop `#ro` |
2712
- | An own default shadowed by a partial mount (`state.name: user.name` while the component declares `name`) | The default wins, with a warning | The explicit mount wins | Remove the default |
2713
-
2714
- One more change arrives early instead of being announced: `$errorCallback` on a volume or a mounted component, which was ignored silently, is now named by the warning that already lists the root-only keys (`$commandTokens`, `$on`, …), as 3.0 does. It still runs only on the root state.
2715
-
2716
- A value warning fires only when that value actually arrives, so a clean console proves nothing about the paths your tests did not reach. The syntax rows are checked when a binding is first parsed, and `npx @wcstack/lint <file>` (and the VS Code extension) reports them with the same check, as `wcs/v3-migration` at info severity, without running the page.
2717
-
2718
2765
  ## Configuration
2719
2766
 
2720
2767
  Pass a partial configuration object to `bootstrapState()`: