@wcstack/state 3.1.0 → 3.3.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 (56) hide show
  1. package/README.ja.md +128 -90
  2. package/README.md +129 -91
  3. package/dist/auto.min.js +1 -1
  4. package/dist/auto.min.js.map +1 -1
  5. package/dist/define.d.ts +11 -7
  6. package/dist/index.d.ts +58 -15
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/manifest.d.ts +39 -5
  10. package/dist/manifest.esm.js +315 -253
  11. package/dist/parser.d.ts +16 -2
  12. package/dist/parser.esm.js +223 -50
  13. package/dist/split/chunks/binder.js +1 -1
  14. package/dist/split/chunks/binder.js.map +1 -1
  15. package/dist/split/chunks/checkDependency.js +1 -1
  16. package/dist/split/chunks/declarationAliases.js +2 -0
  17. package/dist/split/chunks/declarationAliases.js.map +1 -0
  18. package/dist/split/chunks/declarationHooks.js +2 -0
  19. package/dist/split/chunks/declarationHooks.js.map +1 -0
  20. package/dist/split/chunks/featureBridge.js +2 -0
  21. package/dist/split/chunks/featureBridge.js.map +1 -0
  22. package/dist/split/chunks/filterRegistry.js +1 -1
  23. package/dist/split/chunks/filterRegistry.js.map +1 -1
  24. package/dist/split/chunks/getAllPropertyDescriptors.js +1 -1
  25. package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -1
  26. package/dist/split/chunks/lifecycleHooks.js +1 -1
  27. package/dist/split/chunks/pathDiagnostics.js +1 -1
  28. package/dist/split/chunks/pathDiagnostics.js.map +1 -1
  29. package/dist/split/chunks/registerComponents.js +1 -1
  30. package/dist/split/chunks/registerComponents.js.map +1 -1
  31. package/dist/split/chunks/setAll.js +1 -1
  32. package/dist/split/chunks/setAll.js.map +1 -1
  33. package/dist/split/chunks/version.js +1 -1
  34. package/dist/split/core.d.ts +21 -9
  35. package/dist/split/core.js +1 -1
  36. package/dist/split/core.js.map +1 -1
  37. package/dist/split/features/devtools.js +1 -1
  38. package/dist/split/features/devtools.js.map +1 -1
  39. package/dist/split/features/diagnostics.js +1 -1
  40. package/dist/split/features/diagnostics.js.map +1 -1
  41. package/dist/split/features/formats.js +1 -1
  42. package/dist/split/features/formats.js.map +1 -1
  43. package/dist/split/features/recursion.js +1 -1
  44. package/dist/split/features/recursion.js.map +1 -1
  45. package/dist/split/features/scopes.js +1 -1
  46. package/dist/split/features/scopes.js.map +1 -1
  47. package/dist/split/features/ssr.js +1 -1
  48. package/dist/split/features/ssr.js.map +1 -1
  49. package/dist/split/features/temporal.js +1 -1
  50. package/dist/split/features/temporal.js.map +1 -1
  51. package/dist/wcs-manifest.json +98 -44
  52. package/package.json +1 -1
  53. package/dist/split/chunks/mount.js +0 -2
  54. package/dist/split/chunks/mount.js.map +0 -1
  55. package/dist/split/chunks/watchRuntime.js +0 -2
  56. package/dist/split/chunks/watchRuntime.js.map +0 -1
package/README.ja.md CHANGED
@@ -88,7 +88,7 @@
88
88
 
89
89
  - UIを作り直しても、状態のロジックに触れる必要がありません — ただしロジックが描画内容にぶら下がっていない範囲で。live binding は 3 つある[評価のきっかけ](#評価のきっかけdemand-root)の 1 つなので、表示専用のつもりの要素がページ唯一の購読になりえます。
90
90
  - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
91
- - HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$streams`・`$scan`)は、すべて state という 1 箇所に宣言されています。
91
+ - HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$stream`・`$scan`)は、すべて state という 1 箇所に宣言されています。
92
92
 
93
93
  このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
94
94
 
@@ -132,12 +132,12 @@
132
132
  | **集計と一括書き込み** | `$getAll` / `$setAll` / `$resolve` が `items.*.price` を横断して読み書きする。配列は作り直さない | [Proxy API](#proxy-api) |
133
133
  | **再帰パス** | `$recursion` が木の形の繰り返し地点を宣言し、1 本の `**` getter が全深さを覆う | [再帰パス](#再帰パスrecursion) |
134
134
  | **リアクティビティ** | ES Proxy がアドレス単位で読み取りを追跡・キャッシュし、依存順に無効化し、DOM 書き込みをマイクロタスクでまとめる | [状態の更新](#状態の更新) · [依存追跡の境界](#依存追跡の境界) |
135
- | **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$streams` の `args` の 3 箇所からしか生まれない | [評価のきっかけ](#評価のきっかけdemand-root) |
135
+ | **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$stream` の `args` の 3 箇所からしか生まれない | [評価のきっかけ](#評価のきっかけdemand-root) |
136
136
  | **モジュール化** | `mount=` がモジュールを 1 本のツリーに接ぎ木し、`state: path` が部分木をコンポーネントにマウントする。個別対応付けは単一キーを繋ぎ、マウントされたコンポーネントの getter はマウント点で公開される | [ボリューム](#追加の状態をマウントするmount) · [丸ごとマウント](#丸ごとマウントstate-path) |
137
137
  | **コンポーネント** | 排他的な 2 方式 — JavaScript クラス+`bind-component` か、HTML だけの DCC か | [機構の選び方](#コンポーネント機構の選び方) |
138
138
  | **他要素との配線** | wc-bindable プロトコル、spread(`...: obj`)、`#init=` / `#sync=` の authority、プロパティ→属性ミラー | [バインディング authority](#バインディング-authority-init--sync) · [Spread](#spread-バインディング) · [Inputs](#inputs-と属性ミラー) |
139
139
  | **トークン** | command token が state から要素のメソッドを呼び、event token が要素のイベントを state へ戻す | [Command token](#command-tokenメソッドバインディング) · [Event token](#event-tokenイベントバインディング) |
140
- | **時間** | `$streams` が非同期ソースを fold し、`$watch` が headless に反応し、`$scan` が両者を越えて残る累積値を持つ | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
140
+ | **時間** | `$stream` が非同期ソースを fold し、`$watch` が headless に反応し、`$scan` が両者を越えて残る累積値を持つ | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
141
141
  | **初期化とライフサイクル** | state の供給は 6 通り。`$connectedCallback` 〜 `$stateReadyCallback`、`bootstrapState()` / `createState()` | [状態の初期化](#状態の初期化) · [ライフサイクルフック](#ライフサイクルフック) · [API リファレンス](#api-リファレンス) |
142
142
  | **診断** | 解決しないパス・添字の本数・階数・getter の循環を報告する。失敗はそのバインディング 1 本に閉じ、値も DOM も巻き戻さない | [診断と失敗の扱い](#診断と失敗の扱い) |
143
143
  | **配布** | ランタイム依存ゼロ、ビルド不要、ESM、CDN の `/auto` 1 タグ。`unsafe-eval` 不要で Trusted Types 対応 | [インストール](#インストール) · [docs/csp.ja.md](../../docs/csp.ja.md) |
@@ -186,15 +186,15 @@
186
186
  <script type="importmap">
187
187
  {
188
188
  "imports": {
189
- "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/core.js",
190
- "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/temporal.js",
191
- "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/scopes.js"
189
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.3.0/dist/split/core.js",
190
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.3.0/dist/split/features/temporal.js",
191
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.3.0/dist/split/features/scopes.js"
192
192
  }
193
193
  }
194
194
  </script>
195
195
  <script type="module">
196
196
  import { bootstrapState, installFeatures } from '@wcstack/state/core';
197
- import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $streams
197
+ import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $stream
198
198
  import scopes from '@wcstack/state/features/scopes'; // bind-component・mount=・DCC
199
199
 
200
200
  installFeatures([temporal, scopes]);
@@ -202,6 +202,11 @@
202
202
  </script>
203
203
  ```
204
204
 
205
+ `installFeatures([...])` は上の例のとおり `bootstrapState()` の**前**に呼びます。機能は自分のタグ
206
+ (`<wcs-ssr>`)を登録することがあり、タグを定義するのは bootstrap だからです。逆順でもエラーには
207
+ なりません — 後から install した機能は、すでに bootstrap 済みのレジストリへ自分のタグを定義して
208
+ 追いつきます — が、宣言どおりの順で呼べば、最初の要素が upgrade する前にすべてのタグが揃います。
209
+
205
210
  分割の形は、パッケージのファイルそのものから読みます — 上の例のように jsDelivr の素の `/npm/` パスで
206
211
  版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
207
212
  通します。**`esm.run` からは読まないでください**。`+esm` はエントリごとにサーバー側で再バンドルし、
@@ -213,20 +218,23 @@
213
218
  | エントリ | 足されるもの |
214
219
  |---|---|
215
220
  | `@wcstack/state/core` | バインディングの本体: `data-wcs`・`for` / `if`・パス getter・フィルタ・イベント・`$command` / `$on`・`bootstrapState`・`installFeatures` |
216
- | `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$streams` |
221
+ | `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$stream` |
217
222
  | `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
218
223
  | `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
219
224
  | `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
220
- | `@wcstack/state/features/formats` | 書式フィルタ群(`uc`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
225
+ | `@wcstack/state/features/formats` | 書式フィルタ群(`upper`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
221
226
  | `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
222
227
  | `@wcstack/state/features/diagnostics` | 開発時の警告: 束縛・`$watch`・`$scan` のパスが state 上で解決できないとき、did-you-mean 付きで知らせる。入れなければ静か(本番向け)。throw するエラーの文言はどちらでも変わらない |
223
228
  | `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
224
229
 
225
230
  機能が入っていない宣言は黙って無視されません。state の定義時に
226
231
  `[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
227
- で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。install は冪等で、
232
+ で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。`$watch` を宣言した
233
+ ボリューム(`<wcs-state mount=…>`)も接ぎ木の時点で同じ `[wcs/feature-not-installed]` で落ちます —
234
+ ボリュームの state は要素自身の宣言の門を通らないためです。install は冪等で、
228
235
  どのエントリも core のチャンクを 1 つだけ共有します(機能側がエンジンの 2 つ目のコピーを抱える
229
- ことはありません)。
236
+ ことも、**別の機能**のコピーを抱えることもありません — 機能をまたぐ呼び出しは、持ち主の機能が
237
+ install 時に埋める受け口を通ります)。
230
238
 
231
239
  ## 基本的な使い方
232
240
 
@@ -333,7 +341,7 @@
333
341
  <div data-wcs="textContent: cart.total"></div>
334
342
  ```
335
343
 
336
- ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
344
+ ボリュームは getter・`$watch`・`$listKeys`・`$renderedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
337
345
 
338
346
  **ルートのパスをボリュームへ注入する(3.1)。** ボリュームのコードが自分の外のパスを読むには、ボリューム要素の `data-wcs` にコンポーネントの部分マウントと同じ形で書きます:
339
347
 
@@ -341,7 +349,7 @@
341
349
  <wcs-state mount="cart" src="./cart.js" data-wcs="state.taxRate: settings.taxRate"></wcs-state>
342
350
  ```
343
351
 
344
- `cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$updatedCallback` はその更新を内側の名前 `taxRate` で受けます。注入はボリュームのコードの中の名前です — ページは今までどおり `settings.taxRate` を読み、`cart.taxRate` はツリーにありません。
352
+ `cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$renderedCallback` はその更新を内側の名前 `taxRate` で受けます。注入はボリュームのコードの中の名前です — ページは今までどおり `settings.taxRate` を読み、`cart.taxRate` はツリーにありません。
345
353
 
346
354
  - **1 キーずつ**: 左辺は `state.<キー>` の 1 段です(`state: …` でボリューム全体を動かすことはできません — `mount` を変えてください)。
347
355
  - **注入が自前のキーに勝つ**: ボリュームが同名のデータキー(`taxRate: 0`)を宣言していても、その値は接ぎ木されません。同名の getter・setter・メソッドはどちらが `this.taxRate` か曖昧なので、そのボリュームは接ぎ木せずにエラーを報告します。
@@ -354,8 +362,8 @@
354
362
  |---|---|---|---|
355
363
  | データキー・getter・setter・メソッド | 動く | 動く(`p` 相対) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
356
364
  | `$connectedCallback` / `$disconnectedCallback` | 動く | 動く(`p` 相対) | 動く |
357
- | `$watch`・`$listKeys`・`$updatedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
358
- | `$streams`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
365
+ | `$watch`・`$listKeys`・`$renderedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
366
+ | `$stream`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
359
367
  | `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — 警告 | 動かない — 警告 |
360
368
  | `$errorCallback` | 動く | 動かない — 警告(3.0 より前は無言) | 動かない — 警告(3.0 より前は無言) |
361
369
  | 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着して枠を返す。`connectedCallbackPromise` は resolve(`scopes` 機能が無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
@@ -652,7 +660,9 @@ export default {
652
660
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
653
661
  ```
654
662
 
655
- **`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` でログが出ます。
663
+ **`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.*`・`class.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除され、クラスは外れます。3.0 より前は `textContent:` も `undefined` をスキップしていたため使い回したリストの行に前の行の文字が残り、属性には文字列 `"undefined"` / `"null"` が入っていました。`class.*` だけは範囲が狭く、「値が無い」扱いになるのは `undefined` と `null` だけです — **それ以外の非 boolean は今も throw します**。クラスの束縛は真偽の切り替えなので、truthy な文字列は意図というより書き間違いである方がずっと多いためです。「truthy として扱う」つもりなら `class.on: flag|truthy` と書いてください。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
664
+
665
+ **フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`String(value)` で結果を組み立てるフィルタは値が無いときに `"undefined"` という**文字**を作ってしまい、`attr.title: x|trim` が `title="undefined"` を書いていました(フィルタの無い `attr.title: x` は属性を消します)。書式の族(`upper` / `lower` / `capitalize` / `trim` / `slice` / `substr` / `padStart` / `padEnd` / `repeat` / `reverse` / `truncate` / `unit`)は `undefined` / `null` を素通しするようになり、適用側の「テキストは空・属性は削除」がそのまま効きます。次の 3 群は**意図的に素通ししません**: 空値そのものが入力として意味を持つフィルタ(`defaults` / `coalesce` / `nullIfEmpty` / `boolean` / `truthy` / `falsy` / `not` / `eq` / `ne`)、変換が仕事のフィルタ(`int` / `float` / `number` / `string` — `undefined|string` は今も `"undefined"`)、値の型を検査するフィルタ(数値・日付・配列の族。`toFixed` / `date` / `join` など)。最後の群は throw しますが、それは束縛 1 本に閉じ込められ `$errorCallback` に報告されます。値が無いことが正当にありうるなら前に `coalesce` を置いてください(`price|coalesce(0)|toFixed(2)`)。
656
666
 
657
667
  **制約事項**:
658
668
 
@@ -726,7 +736,7 @@ this.items = await (await fetch("/api/items")).json();
726
736
  |---|---|---|
727
737
  | `.name` | `users.*.name` | 現在の要素のプロパティ |
728
738
  | `.` | `users.*` | 現在の要素そのもの |
729
- | `.name\|uc` | `users.*.name\|uc` | フィルタは保持される |
739
+ | `.name\|upper` | `users.*.name\|upper` | フィルタは保持される |
730
740
 
731
741
  プリミティブ配列では、`.` が要素の値を直接参照します:
732
742
 
@@ -1059,9 +1069,9 @@ get total() { return this.price * exchangeRate; } // モジュール変
1059
1069
 
1060
1070
  | API | 用途 |
1061
1071
  |---|---|
1062
- | `this.$trackDependency(path)` | 依存を明示的に追加し、そのパスの変更でこの getter を dirty にする |
1072
+ | `this.$dependOn(path)` | 依存を明示的に追加し、そのパスの変更でこの getter を dirty にする |
1063
1073
  | `this.$postUpdate(path)` | 追跡外の入力が変わったことを getter の外から通知する |
1064
- | `this.$untrackDependency(fn)` | 依存として登録せずにパスを読む(上の対称) |
1074
+ | `this.$untracked(fn)` | 依存として登録せずにパスを読む(上の対称) |
1065
1075
  | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | 鍵付き購読: 「`path` はこの行の鍵に等しいか」をパターン依存なしで答える([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
1066
1076
 
1067
1077
  ```javascript
@@ -1085,9 +1095,9 @@ getter の例外は握り潰されません。評価された場所(バイン
1085
1095
  | **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
1086
1096
  | **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`config.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
1087
1097
 
1088
- 規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$streams` の fold)には出ません。
1098
+ 規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。
1089
1099
 
1090
- `$untrackDependency(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$trackDependency(path)` は最初の規則に対する逃げ道です。
1100
+ `$untracked(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$dependOn(path)` は最初の規則に対する逃げ道です。
1091
1101
 
1092
1102
  ### 鍵付き選択(`$eq` / `$eqPath` / `$eqIndex`)
1093
1103
 
@@ -1120,6 +1130,7 @@ export default {
1120
1130
  - **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
1121
1131
  - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出し、この形に落ちた path には `tracked` バッジを付けます(3.0)。
1122
1132
  - **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
1133
+ - **スコープの中では `path` は相対**です — パスを取る他の `$` API と同じです。マウントされたコンポーネント(`bind-component`)とボリューム(`<wcs-state mount="cart">`)では、`$eq("selectedId", …)`・`$eqPath` の 2 つのパス・`$eqIndex`・`$dependOn` はどれもそのスコープ(`cart.selectedId`)に解決され、ルートには向きません。`$untracked` はコールバックを取るので翻訳の対象外です。
1123
1134
 
1124
1135
  ### ループインデックス変数(`$1`, `$2`, ...)
1125
1136
 
@@ -1151,7 +1162,7 @@ export default {
1151
1162
 
1152
1163
  ```html
1153
1164
  <template data-wcs="for: items">
1154
- <td>{{ $1|inc(1) }}</td> <!-- 1始まりの行番号 -->
1165
+ <td>{{ $1|add(1) }}</td> <!-- 1始まりの行番号 -->
1155
1166
  </template>
1156
1167
  ```
1157
1168
 
@@ -1165,8 +1176,8 @@ export default {
1165
1176
  | `this.$setAll(path, indexes, value, options?)` | ワイルドカードパスにマッチする全アドレスへ一括書き込み |
1166
1177
  | `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
1167
1178
  | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
1168
- | `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
1169
- | `this.$untrackDependency(fn)` | fn 実行中の依存追跡を抑止して値を読む(`$trackDependency` と対称) |
1179
+ | `this.$dependOn(path)`(3.2。旧名 `$trackDependency`) | キャッシュ無効化のための依存関係を手動で登録 |
1180
+ | `this.$untracked(fn)`(3.2。旧名 `$untrackDependency`) | fn 実行中の依存追跡を抑止して値を読む |
1170
1181
  | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | 鍵付き購読: 「`path` の値はこの行の鍵か」([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
1171
1182
  | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
1172
1183
  | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
@@ -1337,7 +1348,7 @@ k=2 nodes.*.children.*.children.*
1337
1348
  | `$getAll(path, [])`(**明示**) | **全深さの合併** —— 深さ優先・行きがけ・添字昇順 |
1338
1349
  | `$getAll(path, [i, …])` | 拒否。接頭辞ではどの深さの話か言えない(`wcs/recursion-getall-form`) |
1339
1350
  | `$setAll(path, [], value)` | 全深さへのブロードキャスト(合併と同じ走査・同じ順序) |
1340
- | `$resolve` / `$postUpdate` / `$trackDependency` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
1351
+ | `$resolve` / `$postUpdate` / `$dependOn` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
1341
1352
 
1342
1353
  束縛形は束縛先の深さを必要とするので、**再帰 getter の中**でしか解決できません(アンカー配下の普通の行 getter や、その行に紐づくイベントハンドラも同じく実体の `ListIndex` を持つので使えます)。トップレベルで `this["nodes.**.value"]` を読むと `wcs/recursion-context` になります —— どのノードのつもりだったかを黙って推測することはありません。深さは行の添字と同じく**最も内側の評価フレームだけ**から読みます。再帰 getter が呼ぶ普通の getter(`get "nodes.**.x"() { return this.helper }` と `get helper() { return this["nodes.**.value"] }`)は自分の行を持たないので、これも `wcs/recursion-context` になります —— `**` は再帰 getter の側で読み、値を渡してください。合併形は深さを要求しないので、トップレベルの getter でも普通の行 getter でもメソッドでも読めます。
1343
1354
 
@@ -1406,8 +1417,8 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
1406
1417
  | 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す |
1407
1418
  | mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる |
1408
1419
  | `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない |
1409
- | `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれる(`nodes.**.children.0` は子ノード、`nodes.**.children.0.total` は getter) | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
1410
- | `nodes.**.total`、およびその値の内側を指すパス | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
1420
+ | `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれるので `nodes.**.children.0` は子ノード | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
1421
+ | `nodes.**.total`、およびその値の内側を指すパス —— 添字綴りも同じ形に畳まれるので `nodes.**.children.0.total` も getter | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
1411
1422
 
1412
1423
  読み取り専用の規則は `**` の綴りに依存しません。再帰 getter の具体的な展開形 —— `nodes.*.total` / `nodes.*.children.*.total` / … —— への書き込みも、固定本数の `$setAll` でも値付きの `$resolve(path, indexes, value)` でも直接代入でも、またその深さが実体化済みかどうかに関わらず、書き込みの入口で拒否します。この検査が無かったときは、未実体化の展開形が「無いキー」に見えてノードのオブジェクトに書き込まれ、代入値が getter のキャッシュ結果として固定されていました。
1413
1424
 
@@ -1463,7 +1474,7 @@ customElements.define("tree-node", class extends HTMLElement {
1463
1474
  - 再帰 setter、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()`)、`**` getter の展開形と同名の具体 getter、そして代入による `**` 経由の書き込み(`this["nodes.**.x"] = v`、`++` も含む)
1464
1475
  - 再帰 `$setAll` の mapper・`{ spread: true }`・添字省略・非空の接頭辞・配列でない `indexes`。書き込み API には深さを束縛する評価文脈が無いので、`[]` は必須です
1465
1476
  - 再帰 `$getAll` の非空の接頭辞・配列でない `indexes`。**添字省略は正当です** —— 再帰 getter の中では束縛形で、評価中の深さを読みます
1466
- - `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$trackDependency` への `**`
1477
+ - `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$dependOn` への `**`
1467
1478
  - ボリューム(`mount=`)やマウントされたコンポーネント(`bind-component`)の `$recursion` と `**` getter —— ルートの state に置きます
1468
1479
  - 再帰 `<template>`、`$depth` 変数、公開の `maxDepth` オプション(3 つとも存在しません)
1469
1480
 
@@ -1499,7 +1510,9 @@ export default {
1499
1510
 
1500
1511
  ## フィルタ
1501
1512
 
1502
- 46 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
1513
+ 48 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
1514
+
1515
+ **3.2 で一部の名前を改めました**(要件 B12)。表は正式名で、括弧の中が旧名です。旧名は 3.x の間は同じに動き、4.0 で外れます。lint と VS Code 拡張は、旧名に正式名を提案する info(`wcs/name-alias`)を出します。3.1 以前を使うページでは旧名で書いてください。
1503
1516
 
1504
1517
  ### 比較
1505
1518
 
@@ -1507,7 +1520,7 @@ export default {
1507
1520
  |---|---|---|
1508
1521
  | `eq(value)` | 等しい | `count\|eq(0)` → `true/false` |
1509
1522
  | `ne(value)` | 等しくない | `count\|ne(0)` |
1510
- | `not` | 論理否定 | `isActive\|not` |
1523
+ | `not` | 真偽性を反転 — `0` / `""` / `null` / `undefined` はすべて `true`(3.x。以前は非 boolean で throw し、`else:` がどちらの枝も描かなかった) | `isActive\|not` |
1511
1524
  | `lt(n)` | より小さい | `count\|lt(10)` |
1512
1525
  | `le(n)` | 以下 | `count\|le(10)` |
1513
1526
  | `gt(n)` | より大きい | `count\|gt(0)` |
@@ -1517,8 +1530,8 @@ export default {
1517
1530
 
1518
1531
  | フィルタ | 説明 | 例 |
1519
1532
  |---|---|---|
1520
- | `inc(n)` | 加算 | `count\|inc(1)` |
1521
- | `dec(n)` | 減算 | `count\|dec(1)` |
1533
+ | `add(n)`(`inc`) | 加算 | `count\|add(1)` |
1534
+ | `sub(n)`(`dec`) | 減算 | `count\|sub(1)` |
1522
1535
  | `mul(n)` | 乗算 | `price\|mul(1.1)` |
1523
1536
  | `div(n)` | 除算 | `total\|div(100)` |
1524
1537
  | `mod(n)` | 剰余 | `index\|mod(2)` |
@@ -1529,7 +1542,7 @@ export default {
1529
1542
 
1530
1543
  | フィルタ | 説明 | 例 |
1531
1544
  |---|---|---|
1532
- | `fix(n)` | 固定小数点桁数 | `price\|fix(2)` → `"100.00"` |
1545
+ | `toFixed(n)`(`fix`) | 固定小数点桁数 | `price\|toFixed(2)` → `"100.00"` |
1533
1546
  | `round(n?)` | 四捨五入 | `value\|round(2)` |
1534
1547
  | `floor(n?)` | 切り捨て | `value\|floor` |
1535
1548
  | `ceil(n?)` | 切り上げ | `value\|ceil` |
@@ -1541,16 +1554,17 @@ export default {
1541
1554
 
1542
1555
  | フィルタ | 説明 | 例 |
1543
1556
  |---|---|---|
1544
- | `uc` | 大文字変換 | `name\|uc` |
1545
- | `lc` | 小文字変換 | `name\|lc` |
1546
- | `cap` | 先頭大文字 | `name\|cap` |
1557
+ | `upper`(`uc`) | 大文字変換 | `name\|upper` |
1558
+ | `lower`(`lc`) | 小文字変換 | `name\|lower` |
1559
+ | `capitalize`(`cap`) | 先頭大文字 | `name\|capitalize` |
1547
1560
  | `trim` | 空白除去 | `text\|trim` |
1548
1561
  | `slice(n)` | 文字列スライス | `text\|slice(5)` |
1549
- | `substr(start, length)` | 部分文字列 | `text\|substr(0,10)` |
1550
- | `pad(n, char?)` | 先頭パディング | `id\|pad(5,0)` → `"00001"` |
1551
- | `rep(n)` | 繰り返し | `text\|rep(3)` |
1552
- | `rev` | 反転 | `text\|rev` |
1553
- | `truncate(n, suffix?)` | 切り詰めて省略記号を付加 | `title\|truncate(20)` → `"…"` 付き |
1562
+ | `substr(start, length)` | 部分文字列(引数は 2 つとも必須) | `text\|substr(0,10)` |
1563
+ | `padStart(n, char?)`(`pad`) | 先頭を埋める(既定 `0`) | `id\|padStart(5,0)` → `"00001"` |
1564
+ | `padEnd(n, char?)` | 末尾を埋める(既定は空白。3.2) | `code\|padEnd(8)` |
1565
+ | `repeat(n)`(`rep`) | 繰り返し | `text\|repeat(3)` |
1566
+ | `reverse`(`rev`) | 反転 | `text\|reverse` |
1567
+ | `truncate(n, suffix?)` | 切り詰めて省略記号を付加(既定は `…` — U+2026 の 1 文字) | `title\|truncate(20)` → `"…"` 付き |
1554
1568
  | `join(sep?)` | 配列を連結(既定 `", "`) | `tags\|join` / `tags\|join(/)` |
1555
1569
 
1556
1570
  ### 型変換
@@ -1562,17 +1576,19 @@ export default {
1562
1576
  | `boolean` | 真偽値に変換 | `value\|boolean` |
1563
1577
  | `number` | 数値に変換 | `value\|number` |
1564
1578
  | `string` | 文字列に変換 | `value\|string` |
1565
- | `null` | null に変換 | `value\|null` |
1579
+ | `nullIfEmpty`(`null`) | 空文字を `null` にする(それ以外はそのまま) | `value\|nullIfEmpty` |
1566
1580
 
1567
1581
  ### 日付 / 時刻
1568
1582
 
1569
1583
  | フィルタ | 説明 | 例 |
1570
1584
  |---|---|---|
1571
- | `date(loc?)` | 日付フォーマット | `timestamp\|date` / `timestamp\|date(ja-JP)` |
1572
- | `time(loc?)` | 時刻フォーマット | `timestamp\|time` |
1573
- | `datetime(loc?)` | 日付 + 時刻 | `timestamp\|datetime(en-US)` |
1574
- | `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
1575
- | `hms(sep?)` | HH:MM:SS | `timestamp\|hms` / `timestamp\|hms(-)` |
1585
+ | `date(loc?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` |
1586
+ | `time(loc?)` | 時刻フォーマット | `createdAt\|time` |
1587
+ | `datetime(loc?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` |
1588
+ | `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
1589
+ | `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
1590
+
1591
+ **5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定は `config.locale` で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。
1576
1592
 
1577
1593
  ### 真偽値 / デフォルト
1578
1594
 
@@ -1580,7 +1596,8 @@ export default {
1580
1596
  |---|---|---|
1581
1597
  | `truthy` | truthy チェック — JavaScript の真偽判定そのもので `boolean` と同じ(3.0 から `0n` は偽) | `value\|truthy` |
1582
1598
  | `falsy` | falsy チェック(JavaScript の真偽判定。`defaults` も同じ判定を使う) | `value\|falsy` |
1583
- | `defaults(v)` | フォールバック値 | `name\|defaults(Anonymous)` |
1599
+ | `defaults(v)` | 偽値(`0`・`false`・`""` を含む)のときのフォールバック値 | `name\|defaults(Anonymous)` |
1600
+ | `coalesce(v)` | `null` / `undefined` のときだけのフォールバック値(`0`・`false`・`""` は残す。3.2) | `count\|coalesce(0)` |
1584
1601
 
1585
1602
  ### フィルタチェーン
1586
1603
 
@@ -1592,7 +1609,15 @@ export default {
1592
1609
 
1593
1610
  フィルタは束縛計画の段で解決されます。未知の名前は `[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)` を取り違えません。
1594
1611
 
1595
- **引数のリテラルは型を持ちます(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`。
1612
+ **引数のリテラルは型を持ちます(3.0)。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し(3.0 より前は文字列 `"true"` と比べていて一致しなかった)、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列の比べ方は変わりません — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` は今も `eq(1)` に一致します。**数値**の値と型付きの `true` / `false` / `null` は単に「一致しない」です: `selectedId|eq(null)` は id が `null` の間は `true`、数値になれば `false` になります(3.2 まではこの組み合わせが**適用のたびに** `requires a number as option` で throw していたため、null と数値の両方を取りうるパスは値が数値になった瞬間にその束縛が壊れていました)。型の食い違いとして報告されるのは、引用符の無い非数値(数値の値に対する `eq(abc)`)だけです — `eq` は値の型を選ばないので、`status|eq(active)` は値が分かるまで判定できません。`defaults(v)` は型付きの値を返します: `defaults(0)` は `0`、`defaults('0')` は `"0"`、`defaults(null)` は `null`。
1613
+
1614
+ **引用符の規則。** 引用符(`'` / `"`)は「ここはリテラル」という宣言で、引数の文法は意図的に小さく保たれています:
1615
+
1616
+ - 引用符の中では `,` `;` `|` `:` はただの文字です。`join(', ')`・`join(': ')`・`join(';')` はそのまま通ります。
1617
+ - 空白のトリムは**引用符の外側だけ**です: `padStart(5, ' ')` は空白で埋め、`toFixed( 2 )` は `toFixed(2)` と同じです。
1618
+ - **エスケープ文字はありません。** 同じ種類の引用符をリテラルの中に入れることはできません — `'it\'s'` ではなく `"it's"` と書いてください(前者は未終端の引用符として `[wcs/binding-syntax]` で落ちます)。
1619
+ - 隣り合う並びは 1 つの引数に連結されます: `'a' 'b'` は 1 個の文字列 `a b`、`1'2'` は文字列 `12` です(引数のどこかに引用符があれば、その引数は文字列になります)。
1620
+ - 未終端の引用符は受理しません。末尾の空引数は落とされ(`filter()` は引数 0 個)、先頭・中間の空引数は位置を保ちます(`defaults(,)` は空文字 1 個を渡します)。
1596
1621
 
1597
1622
  ## Web Component バインディング
1598
1623
 
@@ -1715,6 +1740,11 @@ customElements.define("user-card", UserCard);
1715
1740
  > 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)もホストの値を読み、
1716
1741
  > 既定値は使われません(2.x では R1 によって自前のキーが私有になり、ホストの値を隠していました —
1717
1742
  > `wcs/mount-own-key-shadow` の警告付き)。マップしていない自前のキーは今も R1 で私有です。
1743
+ > これは**1 段のエントリ**の話です: より深いエントリ(`state.a.b: outer.b`)は `a` の私有性に
1744
+ > 触らないので、コンポーネントが `a` を自分で宣言していれば `a` とその配下は私有のままで、
1745
+ > エントリは届きません。名前がコンポーネントの getter / setter / メソッドと一致するエントリも同じで、
1746
+ > コンポーネント自身の面が勝ちます。いずれの形も `wcs/mount-own-key-shadow` で 1 回報告されます —
1747
+ > どちらかの名前を変えてください。
1718
1748
  >
1719
1749
  > **マウントの `#ro` を尊重します(3.0):** `state#ro: user` や `state.title#ro: doc.title` では、
1720
1750
  > コンポーネントはそのエントリを読めても書けません — `element.state.title = …`・メソッド内の
@@ -1781,7 +1811,7 @@ customElements.define("my-component", MyComponent);
1781
1811
  - `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
1782
1812
  - 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
1783
1813
  - Light DOM コンポーネントはホストからの配線が必須(plain 形は v2 で廃止)
1784
- - **マウントされた**スコープは宣言面を実行しない。`$watch`・`$streams`・`$scan` は一度だけ警告を出して無視され、`$recursion` / `**` getter は拒否される。ルート state に宣言すること(ボリューム `<wcs-state mount>` は `$watch` を持てるが、`$scan` と `$recursion` はルート専用)。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
1814
+ - **マウントされた**スコープは宣言面を実行しない。`$watch`・`$stream`・`$scan` は一度だけ警告を出して無視され、`$recursion` / `**` getter は拒否される。ルート state に宣言すること(ボリューム `<wcs-state mount>` は `$watch` を持てるが、`$scan` と `$recursion` はルート専用)。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
1785
1815
 
1786
1816
  ### ループ内でのコンポーネント使用
1787
1817
 
@@ -1834,7 +1864,7 @@ this.shadowRoot.innerHTML = `
1834
1864
 
1835
1865
  コンポーネントの中にさらにコンポーネントを置いて、**スコープを重ねる**こともできます。途中のコンポーネントが配列を素通しするだけで自分では `for` を回さなくても、正本スコープ起点の行フィールド書き込みは最下層の行まで届きます。
1836
1866
 
1837
- コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$updatedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
1867
+ コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$renderedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
1838
1868
 
1839
1869
  ## Command Token(メソッドバインディング)
1840
1870
 
@@ -1948,9 +1978,12 @@ interface CommandToken {
1948
1978
 
1949
1979
  `emit` は各購読者の戻り値の配列を(subscribe 順で)返します。`Promise` を返すメソッドは `Promise.all(token.emit(...))` でラップしてすべてを待ち受けてください。
1950
1980
 
1981
+ **1 つの購読者が throw しても、残りへの配信は止まりません。** token はファンアウトの口で購読者どうしは互いを知らないため、`emit` は全員を呼びます。投げた購読者は `console.error`(token 名つき)で報告され、結果配列のその位置は `undefined` になり、残りの購読者には届きます。例外は emitter へは**届きません** — state のイベントハンドラと同じ規則で、発火経路はハンドラの完了を待たず、DOM イベント起点では投げ返す呼び出し元が存在しないためです。呼び出し側に知らせたいときは throw ではなく戻り値(または reject した `Promise`)で返してください。`Promise` を返す購読者の reject はここでは捕捉しません。そのための `Promise.all(token.emit(...))` です。
1982
+
1951
1983
  ### 購読のライフサイクル
1952
1984
 
1953
- - 購読者は要素を `WeakRef` で保持するため、token の購読者セットに残っていても、取り外された要素はガベージコレクト可能
1985
+ - 購読者は要素を `WeakRef` で保持するので、token の購読者セット**それ自体**は要素を生かし続けない
1986
+ - **注意(現在の実装):** 購読者のクロージャは `binding` も捕捉しており、`binding.node` はその同じ要素 — 上の `WeakRef` を打ち消す強参照です。実際には、取り外された要素は binding が解放されるまで残ります。つまり `WeakRef` が効くのは「binding は消えたが token に購読者が残っている」場合だけです。行や要素が消えた時点で購読者を解放するのが本筋の修正ですが、binding 機構に要素ライフサイクルのフックが無く、その追加とセットで入れるべき変更なので 3.x には入れていません。**今日頼れる仕組みは下の lazy purge**だと考えてください
1954
1987
  - `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
1955
1988
  - 所有する `<wcs-state>` が disconnect されても token レジストリは保持されるので、ルート `<wcs-state>` を付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。切断中は state を作れないので、`$command` 経由の emit は起きない
1956
1989
 
@@ -1976,7 +2009,7 @@ command token は state コードから emit する必要はありません。DO
1976
2009
 
1977
2010
  これは純粋な配線です。イベント端点を command token 端点に接続するだけで、間にロジックは入りません。`emit` の引数はハンドラ呼び出しとまったく同じく透過されます —— まず DOM の `Event`、続いて内包するリストインデックス —— なので購読者は `(event, ...listIndexes)` を受け取ります。購読者の中で必要なものをイベントから取り出してください(`event.target.value`、`event.detail` など)。
1978
2011
 
1979
- - 右辺は `$command.<name>` であり、`<name>` は `$commandTokens` で宣言されていること。`CommandToken` に解決されないパス(例: typo)はイベント時に throw する。
2012
+ - 右辺は `$command.<name>` であり、`<name>` は `$commandTokens` で宣言されていること。`CommandToken` に解決されないパス(例: typo)はイベント時に失敗する。DOM のイベントには投げ返せないので、伝播ではなくイベントごとに 1 回 `console.error` で報告される(reject した state ハンドラと同じ着地)。
1980
2013
  - 修飾子はそのまま機能する: `onclick#prevent: $command.someToken` は emit の前に `preventDefault()` を呼ぶ(`#stop` も同様)。
1981
2014
  - これは state が emit するのと同じ token を emit するので、`command.<method>: $command.someToken` で配線された要素の購読者は、誰がトリガを引いたかに関わらず受け取る。
1982
2015
 
@@ -2138,20 +2171,22 @@ event token は command token と同じ `Token` pub/sub プリミティブを共
2138
2171
  | 宣言 | 宣言するもの | 値を持つか | 発火 | 主な用途 |
2139
2172
  |---|---|---|---|---|
2140
2173
  | [パス getter](#パス-getter算出プロパティ) | その値が現在の state から見て**何であるか** | 持たない(アドレス単位で再計算・キャッシュ) | 評価需要が読んだときに遅延評価 | 小計、分類、集計 |
2141
- | [`$streams`](#streamstreams) | 非同期の供給元と、**1 回の実行の中で** fold される値 | 持つ(出力は runtime の所有) | chunk ごと。`args` が変われば `initial` に戻して restart | フィード、ソケット、継続的な観測 |
2174
+ | [`$stream`](#streamstream) | 非同期の供給元と、**1 回の実行の中で** fold される値 | 持つ(出力は runtime の所有) | chunk ごと。`args` が変われば `initial` に戻して restart | フィード、ソケット、継続的な観測 |
2142
2175
  | [`$watch`](#watchwatch) | 変更への反応 | 持たない | 変化したアドレスごとにバッチ 1 回、scan の書き込みの後 | 副作用、「条件が成立したとき」 |
2143
2176
  | [`$scan`](#scanscan) | 時間をまたぐ累積値と、それを戻す条件 | 持つ(出力は runtime の所有) | 着地ごと(`from`)またはイベントごと(`on`) | ページ蓄積、履歴、件数 |
2144
2177
 
2145
2178
  混乱のほとんどは、次の 2 点で解けます。
2146
2179
 
2147
- - **`$updatedCallback` はこの表に入りません。** 適用されたバインディングを報告するものなので、そこに処理をぶら下げると描画内容に暗黙に依存します。[評価のきっかけ](#評価のきっかけdemand-root)を参照してください。
2148
- - **`$streams` の fold は restart のたびに戻り、`$scan` は戻りません。** restart を越えて値を保ちたいとき、あるいは状態ではなく出来事を数えたいときは `$scan` の領分です。
2180
+ - **`$renderedCallback` はこの表に入りません。** 適用されたバインディングを報告するものなので、そこに処理をぶら下げると描画内容に暗黙に依存します。[評価のきっかけ](#評価のきっかけdemand-root)を参照してください。
2181
+ - **`$stream` の fold は restart のたびに戻り、`$scan` は戻りません。** restart を越えて値を保ちたいとき、あるいは状態ではなく出来事を数えたいときは `$scan` の領分です。
2149
2182
 
2150
- ## Stream(`$streams`)
2183
+ ## Stream(`$stream`)
2151
2184
 
2152
- 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 されます。
2185
+ > **3.2 から宣言キーの正式名は `$stream` です**(要件 B12 — `$watch` / `$scan` と単数で揃えた)。旧名 `$streams` は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。`$streamStatus` / `$streamError` の名前空間は変わりません。3.1 以前では `$streams` と書いてください。
2153
2186
 
2154
- `$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watchwatch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
2187
+ command token / event token が運ぶのは離散的なやり取りです。**`$stream`** は残る形 —— 連続的なフローをカバーします。非同期 producer(async iterable / async generator / `ReadableStream`)を宣言すると、フレームワークがそれを **fold して単一の reactive プロパティに畳み込みます** —— 各チャンクは通常のパス代入を通るため、バインディング・パス getter・`$renderedCallback` は自分で値を代入した場合とまったく同じように反応します。`args` 関数が読んだ state パスが変化すると、実行中の producer は abort され、新しい引数で source が張り直されます(switchMap 型の依存駆動 restart)。stream は `$connectedCallback` 完了後に eager に起動し、要素の disconnect で abort されます。
2188
+
2189
+ `$renderedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watchwatch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
2155
2190
 
2156
2191
  ```html
2157
2192
  <wcs-state>
@@ -2159,7 +2194,7 @@ command token / event token が運ぶのは離散的なやり取りです。**`$
2159
2194
  export default {
2160
2195
  prompt: "",
2161
2196
 
2162
- $streams: {
2197
+ $stream: {
2163
2198
  // フル形: LLM トークンストリームを累積
2164
2199
  tokens: {
2165
2200
  args: (state) => state.prompt, // 依存はここでのみ捕捉される
@@ -2198,7 +2233,7 @@ error 時、プロパティは直前の fold 結果を保持し、エラーは `
2198
2233
  **イベント API の橋渡し** —— 実際の source の多くはコールバック型(`EventSource`・`WebSocket`・DOM イベント)で、async iterable ではありません。標準の `ReadableStream` で包んでください: `start` で enqueue し、`cancel` でリソースを解放します。`AbortSignal` には一切触れません —— restart / 破棄時は runtime が reader を cancel し、parked read を強制解放して `cancel()` まで届けます:
2199
2234
 
2200
2235
  ```js
2201
- $streams: {
2236
+ $stream: {
2202
2237
  metrics: {
2203
2238
  args: (state) => ({ host: state.host }),
2204
2239
  source: ({ host }) => {
@@ -2224,6 +2259,7 @@ $streams: {
2224
2259
  - **有界 fold** —— 需要は producer に逆流しません(backpressure は明示的に放棄)。無限 / 長寿命ストリームでは latest・count・last-N(`(acc, chunk) => [...acc.slice(-99), chunk]`)・ウィンドウ集計など有界な fold を使うこと。生の全チャンク累積は有限ストリーム限定。
2225
2260
  - **`args` は同期** —— Promise を返すとエラー。`args` 内での wildcard 読みも拒否されます。
2226
2261
  - **自己依存・相互サイクルの禁止** —— `args` が自 stream の値や status を読むとエラーになります。2 つの stream の相互サイクル(A の `args` が B の値を読み、B の `args` が A の値を読む)は検出されず無限 restart になるため組まないこと。一方向のチェイン(A の値を B の `args` が読む)は正当です。
2262
+ - **プロパティは runtime の所有物** —— エントリ名が state の getter / setter / メソッドと衝突すると、`$scan` の出力と同じく宣言時にエラーになります(検査が無いと、そのメソッドは起動時の `initial` リセットで無言に上書きされます)。
2227
2263
  - **SSR では起動しない** —— サーバーでは宣言のパースとプロパティの実体化(`initial`)のみ行い、source は実行されません。クライアント側は通常どおり起動します。
2228
2264
 
2229
2265
  完全な契約 —— ライフサイクルと所有権・restart セマンティクス・flush 粒度・スコープ外リスト —— は [docs/streams.ja.md](docs/streams.ja.md) を参照してください。
@@ -2238,9 +2274,9 @@ $streams: {
2238
2274
  |---|---|---|
2239
2275
  | **live DOM バインディング** | `data-wcs` / mustache / コメントバインディング | **する**(その要素が消えると需要も消える) |
2240
2276
  | **`$watch` の宣言** | state 側 | しない(headless) |
2241
- | **`$streams` の `args`** | state 側 | しない(起動・restart のたびに評価される) |
2277
+ | **`$stream` の `args`** | state 側 | しない(起動・restart のたびに評価される) |
2242
2278
 
2243
- **`$updatedCallback` は根ではありません。** それは「バインディングが適用された結果」の報告であり、需要を作りません。
2279
+ **`$renderedCallback` は根ではありません。** それは「バインディングが適用された結果」の報告であり、需要を作りません。
2244
2280
 
2245
2281
  ### 描画がプログラムの意味論を変えうる
2246
2282
 
@@ -2252,27 +2288,27 @@ $streams: {
2252
2288
  ```
2253
2289
 
2254
2290
  ```javascript
2255
- // $updatedCallback は binding 駆動 —— 上の <b> を消すと paths に現れなくなり、
2291
+ // $renderedCallback は binding 駆動 —— 上の <b> を消すと paths に現れなくなり、
2256
2292
  // フィードの commit が黙って止まる
2257
- $updatedCallback(paths) {
2293
+ $renderedCallback(paths) {
2258
2294
  if (!paths.includes("$streamStatus.pageResult")) return;
2259
2295
  this.items = this.items.concat(this.pageResult.items);
2260
2296
  }
2261
2297
  ```
2262
2298
 
2263
- **規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$streams` の `args` のどれかに根を置いてください。`$updatedCallback` は「描かれたものに追随する」用途に限ります。
2299
+ **規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$stream` の `args` のどれかに根を置いてください。`$renderedCallback` は「描かれたものに追随する」用途に限ります。
2264
2300
 
2265
- 上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$updatedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2301
+ 上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$renderedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2266
2302
 
2267
2303
  ### 残る制約
2268
2304
 
2269
- 需要の根が 3 か所に分かれること自体は変わりません。**ある getter が評価されるかを知るには、その 3 か所(ページの全バインディング・全 `$watch`・全 `$streams.args`)を見る必要があり、getter の定義だけを読んでも分かりません。** lint と DevTools の配線カバレッジはこの照合を機械にやらせるためのものです。
2305
+ 需要の根が 3 か所に分かれること自体は変わりません。**ある getter が評価されるかを知るには、その 3 か所(ページの全バインディング・全 `$watch`・全 `$stream.args`)を見る必要があり、getter の定義だけを読んでも分かりません。** lint と DevTools の配線カバレッジはこの照合を機械にやらせるためのものです。
2270
2306
 
2271
2307
  なお `$watch` に宣言したスカラー getter は **eager** になります(接続時に 1 回、以後は依存に触れたバッチごとに評価)。ワイルドカード行の getter は eager 化しません(初回評価がリスト全体を舐めるため)。
2272
2308
 
2273
2309
  ## Watch(`$watch`)
2274
2310
 
2275
- `$updatedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
2311
+ `$renderedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
2276
2312
 
2277
2313
  ```html
2278
2314
  <wcs-state>
@@ -2317,11 +2353,11 @@ $updatedCallback(paths) {
2317
2353
 
2318
2354
  | 層 | 順序 | 制御 |
2319
2355
  |---|---|---|
2320
- | 機構間 | `$updatedCallback` → `$scan` → `$watch` → `$streams` restart | 固定 |
2356
+ | 機構間 | `$renderedCallback` → `$scan` → `$watch` → `$stream` restart | 固定 |
2321
2357
  | ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
2322
2358
  | 同一パスの行間 | `indexes` 昇順 | 固定 |
2323
2359
 
2324
- **機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$updatedCallback` —— がフレームで着地する一方、`$scan`・`$watch`・`$streams` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$scan` → `$watch` → `$streams` restart → `$updatedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
2360
+ **機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$renderedCallback` —— がフレームで着地する一方、`$scan`・`$watch`・`$stream` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$scan` → `$watch` → `$stream` restart → `$renderedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
2325
2361
 
2326
2362
  主なルール:
2327
2363
 
@@ -2330,14 +2366,14 @@ $updatedCallback(paths) {
2330
2366
  - **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。入れ子のリストを置き換えると、新しい配列の行がすべて発火します。
2331
2367
  - **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
2332
2368
  - **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
2333
- - **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
2369
+ - **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$renderedCallback` とは異なる扱いです。
2334
2370
  - **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。
2335
- - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$updatedCallback` を持てます)へ誘導します(`$streams` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
2371
+ - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$renderedCallback` を持てます)へ誘導します(`$stream` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
2336
2372
  - **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
2337
2373
 
2338
2374
  ## Scan(`$scan`)
2339
2375
 
2340
- `$streams` が畳むのは 1 回の run の**内側**で、restart のたびに値は `initial` へ戻ります。`$watch` は値を所有しません。**`$scan`** はその両方を跨いで残る値 —— 時間軸方向の累積 —— を、持ち主・発火単位・reset 条件つきで宣言します。
2376
+ `$stream` が畳むのは 1 回の run の**内側**で、restart のたびに値は `initial` へ戻ります。`$watch` は値を所有しません。**`$scan`** はその両方を跨いで残る値 —— 時間軸方向の累積 —— を、持ち主・発火単位・reset 条件つきで宣言します。
2341
2377
 
2342
2378
  ```html
2343
2379
  <wcs-state>
@@ -2346,7 +2382,7 @@ $updatedCallback(paths) {
2346
2382
  page: 1,
2347
2383
  host: "a",
2348
2384
  $eventTokens: ["message"],
2349
- $streams: {
2385
+ $stream: {
2350
2386
  pageResult: { args: (s) => s.page, source: loadPage },
2351
2387
  },
2352
2388
  $scan: {
@@ -2382,7 +2418,7 @@ $updatedCallback(paths) {
2382
2418
  | `fold` | 必須。`from` は `(acc, cur, prev, ...indexes) => next`、`on` は `(acc, event, ...indexes) => next`。同期で、`this` 無しで呼ばれ、新しい値を返す。`acc` そのものを返すと書き込まない。 |
2383
2419
  | `resetOn` | 任意。素の state パスの配列。どれかが書かれたら出力を `initial` に戻す。`from` の scan はそのバッチの fold を行わず、`on` の scan は書き込みより後に来たイベントを `initial` から畳む。`from` の配下は raise、`from` の祖先は可(親の差し替えで作り直す)。オブジェクトのパスはそのオブジェクト自身が書かれたときだけ reset し、子への書き込みでは reset しない(葉のパスを並べるか nonce を使う)。 |
2384
2420
 
2385
- **出力はランタイムが所有します**(`$streams` の値と同じ)。state にそのプロパティが無ければ `initial` で実体化され(plain なデータは複製するので、出力の plain な部分の子パスへ書いても宣言の `initial` は変わりません。クラスのインスタンスや凍結された値など plain でない値は宣言と共有したままです)、他のパスと同じようにバインドできます。stream の restart・切断と再接続・同じオブジェクトの再セットを跨いで残り、新しい宣言での再セットでは作り直されます。出力名が getter・setter・メソッド・`$streams` のエントリと衝突すると raise します。
2421
+ **出力はランタイムが所有します**(`$stream` の値と同じ)。state にそのプロパティが無ければ `initial` で実体化され(plain なデータは複製するので、出力の plain な部分の子パスへ書いても宣言の `initial` は変わりません。クラスのインスタンスや凍結された値など plain でない値は宣言と共有したままです)、他のパスと同じようにバインドできます。stream の restart・切断と再接続・同じオブジェクトの再セットを跨いで残り、新しい宣言での再セットでは作り直されます。出力名が getter・setter・メソッド・`$stream` のエントリと衝突すると raise します。
2386
2422
 
2387
2423
  2 つの source の発火:
2388
2424
 
@@ -2397,8 +2433,8 @@ $updatedCallback(paths) {
2397
2433
  - **getter を畳まない。** getter は入力が変わるたびに再評価されるので、畳むと出来事ではなく再評価の回数を数えます。`from` や `resetOn` に getter を書く(`from` に `$recursion` の `**` getter の展開形 `nodes.*.total` を書くのも同じ)と、宣言時に raise します(`wcs/scan-source-computed`)。
2398
2434
  - **1 回の fold は着地ごとで、ページごとではない。** `done` 後の再試行や、ページの再接続は同じページをもう一度着地させます。問題になるなら fold に冪等キーを持たせてください(上の `pages`)。
2399
2435
  - **stream の `args` を自分の scan 出力から導出しない。** `feed` から導出した getter(`feed` を畳む別の scan の出力から導出したものを含む)を `pageResult` の `args` が読むと、stream が自分の結果で restart し続けるので、ランタイムは `wcs/scan-feedback-loop` を raise します。カーソルはイベントから進めてください。stream の restart と同じバッチに着地した chunk は abort される run のものなので畳みません。
2400
- - **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$streams` の restart の書き込みは `prev` を持ちます。
2401
- - **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$streams` と同じ)。
2436
+ - **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$stream` の restart の書き込みは `prev` を持ちます。
2437
+ - **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$stream` と同じ)。
2402
2438
  - **例外は隔離される。** throw・Promise の戻り値・読めない値はコンソールと DevTools に報告され、書き込みません(ワイルドカードの `from` で読めない行はその行だけを飛ばし、行の着地はリストの位置 1 つにつき 1 回に絞ります)。他の scan・watch・stream の restart は続行します。
2403
2439
  - **`$watch` は scan の書き込みの後に走る。** 同じ drain の `$watch` ハンドラは畳んだ後の出力を読み、ハンドラが出力へ書いた値はそのまま残ります。出力の着地が drain される前に `from` の source がもう一度書かれる(その drain の `$watch` ハンドラが書くなど)と、両方が同じバッチに載ります。このとき出力を見る `$watch` は `prev` に着地した値を受け、`cur` に 1 段先の値を見て、次のバッチで同じ値でもう一度発火することがあるので、同じ値の重複に耐える形にしてください。ユーザー操作で累積を消すなら、`resetOn` に nonce を読ませてください。
2404
2440
  - **ルートのみ。** ボリューム(`mount=`)は `$scan` を拒否し、マウントされた `bind-component` スコープは 1 回の warn で無視します。SSR では `from` は畳みません(出力の実体化は行います)。
@@ -2540,7 +2576,7 @@ export default {
2540
2576
  - 非空文字列でないエントリ
2541
2577
  - `$` 始まりのエントリ(内部プロパティはコンポーネントの prototype に公開されません)
2542
2578
  - 重複したエントリ — これは従来サイレントに壊れていました。重複名があると `wcBindable` 宣言全体が読み取り不能になり、その要素が黙って双方向バインド不可になります
2543
- - 状態に存在しないエントリ(自身とプロトタイプチェーンの両方を探索します。`$streams` の名前は値プロパティがインスタンスごとに実体化されるため「存在する」と見なされます)
2579
+ - 状態に存在しないエントリ(自身とプロトタイプチェーンの両方を探索します。`$stream` の名前は値プロパティがインスタンスごとに実体化されるため「存在する」と見なされます)
2544
2580
  - `$bindables` にメソッドを書いた場合、または `$commands` に値プロパティを書いた場合
2545
2581
 
2546
2582
  ### DCC のメソッドを起動する
@@ -2600,7 +2636,7 @@ export default {
2600
2636
  | `$commands` | 起動可能メソッドの宣言 |
2601
2637
  | `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
2602
2638
  | `$disconnectedCallback` | クリーンアップフック |
2603
- | `$updatedCallback` | 状態変更後に呼ばれる |
2639
+ | `$renderedCallback` | 状態変更後に呼ばれる |
2604
2640
 
2605
2641
  ## SVG サポート
2606
2642
 
@@ -2616,7 +2652,9 @@ export default {
2616
2652
 
2617
2653
  ## ライフサイクルフック
2618
2654
 
2619
- 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
2655
+ 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$renderedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
2656
+
2657
+ > **3.2 から `$updatedCallback` の正式名は `$renderedCallback` です**(要件 B12)。このフックが受けるのは**適用されたバインディング**の更新で、state の更新全体ではない(それには `$watch`)ことを名前で言うためです。旧名は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。3.1 以前では `$updatedCallback` と書いてください。
2620
2658
 
2621
2659
  ```html
2622
2660
  <wcs-state>
@@ -2645,15 +2683,15 @@ export default {
2645
2683
  |---|---|---|
2646
2684
  | `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
2647
2685
  | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
2648
- | `$updatedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
2649
- | `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$updatedCallback` の後に呼び出し | 可(await されない) |
2686
+ | `$renderedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
2687
+ | `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$renderedCallback` の後に呼び出し | 可(await されない) |
2650
2688
 
2651
2689
  `$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
2652
2690
 
2653
2691
  - フック内の `this` は読み書き可能な状態プロキシです。
2654
2692
  - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
2655
2693
  - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
2656
- - `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
2694
+ - `$renderedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
2657
2695
  - `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま)。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
2658
2696
 
2659
2697
  ```js
@@ -2666,7 +2704,7 @@ export default {
2666
2704
  };
2667
2705
  ```
2668
2706
 
2669
- フックはバッチの後(`$updatedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$updatedCallback` が投げた例外(loud に失敗する)は対象外です。
2707
+ フックはバッチの後(`$renderedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$renderedCallback` が投げた例外(loud に失敗する)は対象外です。
2670
2708
  - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
2671
2709
 
2672
2710
  ## 遷移アニメーション
@@ -2689,8 +2727,8 @@ li {
2689
2727
 
2690
2728
  そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
2691
2729
 
2692
- - drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$updatedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
2693
- - `$scan`・`$watch`・`$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
2730
+ - drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$renderedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
2731
+ - `$scan`・`$watch`・`$stream` restart は元の microtask に留まるため、`$renderedCallback` の**前**に走るようになる。
2694
2732
 
2695
2733
  適用すべきバインディングが実際にあるバッチだけがタグへ渡されるので、headless なパスへの書き込みが遷移を起こすことはない。タグが無ければ drain は従来どおり。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 参照。
2696
2734
 
@@ -2734,7 +2772,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2734
2772
  | `wcs/getter-cycle` | パス getter どうしが循環参照していないか。実行時は「アドレススタックが既に積んでいるアドレスへ戻る」ことで判定する | 循環を断つ |
2735
2773
  | `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じアドレスを 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
2736
2774
  | `wcs/index-param-range` | `$N` は実在するワイルドカード段を指すこと(`$1`〜`$128`・先頭ゼロ不可) | 実在する段を使う |
2737
- | `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$trackDependency`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
2775
+ | `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$dependOn`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
2738
2776
  | `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー、`get "nodes.**"`、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
2739
2777
  | `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
2740
2778
  | `wcs/recursion-context` | **束縛**形の `**` を、束縛先の深さが無い場所で読んだ —— トップレベル、またはアンカー外の getter | 再帰 getter か行 getter の中から読むか、`[]` で全深さを合併する |
@@ -2760,7 +2798,7 @@ this.$getAll("matrix.*.*", [row]);
2760
2798
 
2761
2799
  ### バインディング 1 本の失敗は 1 本に閉じ込められます
2762
2800
 
2763
- バインディングの適用が throw しても、そのバッチの残り・`$updatedCallback`・`$watch`・`$streams` の restart はすべて続行します。失敗は握り潰されず、`console.error` と DevTools(`state:binding-apply-error`)に出ます。
2801
+ バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`console.error` と DevTools(`state:binding-apply-error`)に出ます。
2764
2802
 
2765
2803
  ```
2766
2804
  [@wcstack/state] binding "text: items.*.label" failed to apply; the rest of this batch continues.
@@ -3054,7 +3092,7 @@ bootstrapState();
3054
3092
  | `setterPaths` | setter として定義されたパスの Set |
3055
3093
  | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
3056
3094
  | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
3057
- | `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$updatedCallback` も呼びません。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再セットは非対応です)。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素では throw します —— 失敗した要素は再武装できないので、取り除いて作り直してください |
3095
+ | `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$renderedCallback` も呼びません。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再セットは非対応です)。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素では throw します —— 失敗した要素は再武装できないので、取り除いて作り直してください |
3058
3096
  | `nextVersion()` | バージョン番号をインクリメントして返す |
3059
3097
 
3060
3098
  ## アーキテクチャ