@wcstack/state 3.5.4 → 4.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.ja.md +669 -428
  2. package/README.md +667 -426
  3. package/dist/auto.min.js +1 -2
  4. package/dist/define.d.ts +4 -8
  5. package/dist/define.js +1 -1
  6. package/dist/index.d.ts +139 -1483
  7. package/dist/index.esm.js +1 -2
  8. package/dist/manifest.d.ts +18 -59
  9. package/dist/manifest.esm.js +542 -1202
  10. package/dist/parser.d.ts +49 -103
  11. package/dist/parser.esm.js +662 -831
  12. package/dist/split/auto.js +1 -0
  13. package/dist/split/chunks/chunk-732CB4QN.js +1 -0
  14. package/dist/split/chunks/chunk-7PN5YNZZ.js +1 -0
  15. package/dist/split/chunks/chunk-FF6KHTYS.js +1 -0
  16. package/dist/split/chunks/chunk-HFKGUDDJ.js +1 -0
  17. package/dist/split/chunks/chunk-PKDBVBAP.js +1 -0
  18. package/dist/split/chunks/chunk-QI7P7YPP.js +1 -0
  19. package/dist/split/chunks/chunk-QQH6N5CJ.js +1 -0
  20. package/dist/split/chunks/chunk-UM3O6QXD.js +1 -0
  21. package/dist/split/chunks/chunk-WTD5TVPZ.js +1 -0
  22. package/dist/split/chunks/chunk-WWJNAZVP.js +1 -0
  23. package/dist/split/chunks/chunk-YOPPVWPQ.js +1 -0
  24. package/dist/split/core.d.ts +114 -1168
  25. package/dist/split/core.js +1 -2
  26. package/dist/split/features/devtools.d.ts +23 -3
  27. package/dist/split/features/devtools.js +1 -2
  28. package/dist/split/features/diagnostics.d.ts +27 -5
  29. package/dist/split/features/diagnostics.js +1 -2
  30. package/dist/split/features/formats.d.ts +21 -5
  31. package/dist/split/features/formats.js +1 -2
  32. package/dist/split/features/list-keys.d.ts +24 -0
  33. package/dist/split/features/list-keys.js +1 -0
  34. package/dist/split/features/recursion.d.ts +23 -3
  35. package/dist/split/features/recursion.js +1 -2
  36. package/dist/split/features/scopes.d.ts +25 -4
  37. package/dist/split/features/scopes.js +1 -2
  38. package/dist/split/features/ssr.d.ts +23 -3
  39. package/dist/split/features/ssr.js +1 -2
  40. package/dist/split/features/temporal.d.ts +19 -4
  41. package/dist/split/features/temporal.js +1 -2
  42. package/dist/wcs-manifest.json +43 -49
  43. package/package.json +15 -17
  44. package/dist/auto.min.js.map +0 -1
  45. package/dist/index.esm.js.map +0 -1
  46. package/dist/split/chunks/binder.js +0 -2
  47. package/dist/split/chunks/binder.js.map +0 -1
  48. package/dist/split/chunks/checkDependency.js +0 -2
  49. package/dist/split/chunks/checkDependency.js.map +0 -1
  50. package/dist/split/chunks/declarationAliases.js +0 -2
  51. package/dist/split/chunks/declarationAliases.js.map +0 -1
  52. package/dist/split/chunks/declarationHooks.js +0 -2
  53. package/dist/split/chunks/declarationHooks.js.map +0 -1
  54. package/dist/split/chunks/diagnosticsHooks.js +0 -2
  55. package/dist/split/chunks/diagnosticsHooks.js.map +0 -1
  56. package/dist/split/chunks/errorGuidance.js +0 -2
  57. package/dist/split/chunks/errorGuidance.js.map +0 -1
  58. package/dist/split/chunks/featureBridge.js +0 -2
  59. package/dist/split/chunks/featureBridge.js.map +0 -1
  60. package/dist/split/chunks/features.d.ts +0 -29
  61. package/dist/split/chunks/filterRegistry.js +0 -2
  62. package/dist/split/chunks/filterRegistry.js.map +0 -1
  63. package/dist/split/chunks/getAllPropertyDescriptors.js +0 -2
  64. package/dist/split/chunks/getAllPropertyDescriptors.js.map +0 -1
  65. package/dist/split/chunks/lifecycleHooks.js +0 -2
  66. package/dist/split/chunks/lifecycleHooks.js.map +0 -1
  67. package/dist/split/chunks/pathDiagnostics.js +0 -2
  68. package/dist/split/chunks/pathDiagnostics.js.map +0 -1
  69. package/dist/split/chunks/registerComponents.js +0 -2
  70. package/dist/split/chunks/registerComponents.js.map +0 -1
  71. package/dist/split/chunks/setAll.js +0 -2
  72. package/dist/split/chunks/setAll.js.map +0 -1
  73. package/dist/split/chunks/version.js +0 -2
  74. package/dist/split/chunks/version.js.map +0 -1
  75. package/dist/split/core.js.map +0 -1
  76. package/dist/split/features/devtools.js.map +0 -1
  77. package/dist/split/features/diagnostics.js.map +0 -1
  78. package/dist/split/features/formats.js.map +0 -1
  79. package/dist/split/features/recursion.js.map +0 -1
  80. package/dist/split/features/scopes.js.map +0 -1
  81. package/dist/split/features/ssr.js.map +0 -1
  82. package/dist/split/features/temporal.js.map +0 -1
package/README.ja.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
4
 
5
- > **4.0 への準備**: 4.0 はまだリリースされていません。何が変わり、3.x のうちに何ができるかは [3.x → 4.0 移行ガイド(プレビュー)](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.ja.md) にまとめています。
5
+ > **この README は 4.0 を説明しています。** 3.x から移行する場合は、[3.x からのアップグレード](#3x-からのアップグレード) と [3.x → 4.0 移行ガイド](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.ja.md) を読んでください。
6
6
 
7
7
  **これは便利な既存FWの別実装ではありません。パスがビューとモデルの契約になる——フロントエンドの外で確立した系譜を、Web標準の上に持ち込む試みです。**
8
8
 
@@ -43,7 +43,7 @@
43
43
 
44
44
  **選ぶとき**: HTML が主役のページ — サーバー描画や静的なマークアップにリアクティブな部分を足す、カスタム要素を組み合わせてページを作る、「HTML を読めばデータ依存が全部わかる」ことに価値があり、ビルド工程が前提ではなくコストであるとき。
45
45
 
46
- **選ばないとき**: チームが既にコンポーネント型 FW の中で暮らしている — I/O ノードは[フレームワークアダプタ](../../docs/framework-adapter-integration.ja.md)経由で使ってください。ホットパスが巨大な keyed リスト — [パフォーマンス](#パフォーマンス)節の計測では生成・追加が [`@wcstack/signals`](../signals/) の 2.5〜3.5 倍で、相互運用できる signals のほうが適任です。テンプレートに式を書きたい — 意図的に存在しません。テンプレートの型検査をツールではなくコンパイラに求める — パスは文字列で、`@wcstack/typescript` は差を縮めますが埋めはしません。
46
+ **選ばないとき**: チームが既にコンポーネント型 FW の中で暮らしている — I/O ノードは[フレームワークアダプタ](../../docs/framework-adapter-integration.ja.md)経由で使ってください。ホットパスが巨大な keyed リストで、生成・追加の時間が決め手になる — 計測したうえで([パフォーマンス](#パフォーマンス))、このパッケージと相互運用できる [`@wcstack/signals`](../signals/) を検討してください。テンプレートに式を書きたい — 意図的に存在しません。テンプレートの型検査をツールではなくコンパイラに求める — パスは文字列で、`@wcstack/typescript` は差を縮めますが埋めはしません。
47
47
 
48
48
  この軸に乗せれば比較は具体的になります。下の[パフォーマンス](#パフォーマンス)節がその一例で、`e2e/bench/` のドライバで手元のハードウェアでも再現できます。
49
49
 
@@ -56,7 +56,7 @@
56
56
  | **表計算**(VisiCalc, 1979) | アドレス、セルが**何であるか**を宣言する数式、依存グラフ、遅延再計算 — 更新コードはどこにもない | 格子座標ではなく名前を使い、数式をセル単位ではなく*形*単位で書く。`get "cart.items.*.subtotal"()` は各行へフィルダウンされるのではなく、ワイルドカードそのものが定義 |
57
57
  | **Cocoa Bindings / KVC–KVO**(NeXT の EOF, 1994/Mac OS X 10.3, 2003) | キーパス(`person.address.street`)、「対象 + キーパス + value transformer」の三つ組、パスに沿って集計するコレクション演算子(`@sum.items.price`) | その三つ組が nib や `bind:toObject:withKeyPath:` の呼び出しではなくマークアップにあるので、grep・lint・diff できる。変更検知は KVC 準拠ではなく素のオブジェクトへの ES Proxy |
58
58
  | **XForms**(W3C 勧告, 2003) | model / instance / view の分離、instance を指す `ref` パス、そして `<bind calculate="…">` — **パスの位置に**宣言される算出値であり、パス getter の直系の祖先 | パスはアドレスに徹し、計算は属性内の XPath ではなく state 上の JavaScript getter。XForms プロセッサなしで素のブラウザで動く |
59
- | **WPF / XAML**(2006) | `{Binding Path=User.Name, Mode=TwoWay}`、部分木ごと基点を張り替える `DataContext`、ソースへの書き戻し時点を選ぶ `UpdateSourceTrigger`、両端を変換する `IValueConverter` | ビジュアルツリーを継承するコンテキストと `RelativeSource` / `ElementName` の脱出口ではなく、ルートごとに 1 本の state ツリー。`state: user` がその基点張り替えで、しかもホストの HTML に書かれる。変換器は登録するクラスではなく 46 個の閉じたフィルタで、コンパイルも不要 |
59
+ | **WPF / XAML**(2006) | `{Binding Path=User.Name, Mode=TwoWay}`、部分木ごと基点を張り替える `DataContext`、ソースへの書き戻し時点を選ぶ `UpdateSourceTrigger`、両端を変換する `IValueConverter` | ビジュアルツリーを継承するコンテキストと `RelativeSource` / `ElementName` の脱出口ではなく、ルートごとに 1 本の state ツリー。`state: user` がその基点張り替えで、しかもホストの HTML に書かれる。変換器は登録するクラスではなく 47 個の閉じたフィルタで、コンパイルも不要 |
60
60
  | **Android Data Binding**(2015) | レイアウトファイル自体に書くパス — `android:text="@{user.name}"`、双方向は `@={}` | ビルド手順も生成されるバインディングクラスもなく、属性の中に式を書かない |
61
61
  | **SCADA / HMI のタグバインド**(産業用・数十年) | 設定だけでウィジェットのプロパティをタグパス(`Line1/Tank/Level`)へ配線する。パスを媒介変数化する*間接*バインド(`Folder/Tag_{1}`)で 1 画面が多数の機器を駆動する | ツリーが持つのはスカラーのフラットな名前空間ではなく、派生値・リスト・マウントされたコンポーネント。媒介変数はドロップダウンから与えるものではなく、バインディングが置かれた行が解決するループのワイルドカード |
62
62
 
@@ -90,7 +90,7 @@
90
90
 
91
91
  - UIを作り直しても、状態のロジックに触れる必要がありません — ただしロジックが描画内容にぶら下がっていない範囲で。live binding は 3 つある[評価のきっかけ](#評価のきっかけdemand-root)の 1 つなので、表示専用のつもりの要素がページ唯一の購読になりえます。
92
92
  - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
93
- - HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$stream`・`$scan`)は、すべて state という 1 箇所に宣言されています。
93
+ - HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$stream`)は、すべて state という 1 箇所に宣言されています。
94
94
 
95
95
  このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
96
96
 
@@ -124,12 +124,13 @@
124
124
 
125
125
  | 領域 | 一行で言うと | 参照 |
126
126
  |---|---|---|
127
- | **パスモデル** | ドットパスが state を指す。`*` は抽象添字、`**` は深さ、`$1` / `$2` は軸の名前 | [第一原理](#第一原理-パスが唯一の契約) · [ループインデックス変数](#ループインデックス変数1-2) |
128
- | **バインディング構文** | 1 つの `data-wcs` 属性にプロパティ / テキスト / class / style / 属性 / イベントを載せる。テキストノードでは `{{ }}`、`<svg>` の内側でも同じ | [バインディング構文](#バインディング構文) · [Mustache](#mustache-構文) · [SVG](#svg-サポート) |
127
+ | **パスモデル** | ドットパスが state を指す。`*` は抽象添字、`**` は深さ、`$1` / `$2` は軸の名前 | [第一原理](#第一原理-パスが唯一の契約) · [ループインデックス変数](#ループインデックス変数1-2-) |
128
+ | **バインディング構文** | 1 つの `data-wcs` 属性にプロパティ / テキスト / class / style / 属性 / イベントを載せる。テキストでは `{{ }}` と `<!--@@: path-->`、`<svg>` の内側でも同じ | [バインディング構文](#バインディング構文) · [Mustache](#mustache-構文) · [SVG](#svg-サポート) |
129
+ | **イベント** | よく使うバブリングイベントの `on*:` ハンドラはルートへ委譲される。`#direct` を付けたものは要素自身に付く | [イベントハンドリング](#イベントハンドリング) |
129
130
  | **構造ディレクティブ** | `<template>` 上の `for` と `if` / `elseif` / `else` | [構造ディレクティブ](#構造ディレクティブ) |
130
131
  | **行の同一性** | 行は参照で差分を取るので、並べ替えや絞り込みで DOM を再利用する。`$listKeys` は再取得した配列でも行 DOM と行オブジェクトを保つ | [`$listKeys`](#listkeys--再取得された行の同一性) |
131
132
  | **フォーム** | `input` / `select` / `textarea` の双方向バインディング、ラジオグループと単一値、チェックボックスグループと配列、`#ro` / `#onchange` / `#prevent` / `#stop` | [双方向バインディング](#双方向バインディング) · [修飾子](#修飾子) |
132
- | **フィルタ** | 46 種類の組み込み、チェーン可能、`<html lang>` を見るロケール依存フォーマット | [フィルタ](#フィルタ) · [ロケール](#ロケール) |
133
+ | **フィルタ** | 47 種類の組み込み、チェーン可能、`<html lang>` を見るロケール依存フォーマット | [フィルタ](#フィルタ) · [ロケール](#ロケール) |
133
134
  | **派生状態** | パス getter がデータツリーの任意の深さの仮想プロパティを 1 箇所にフラットに宣言する。チェーンでき、setter も書ける | [パス getter](#パス-getter算出プロパティ) |
134
135
  | **集計と一括書き込み** | `$getAll` / `$setAll` / `$resolve` が `items.*.price` を横断して読み書きする。配列は作り直さない | [Proxy API](#proxy-api) |
135
136
  | **再帰パス** | `$recursion` が木の形の繰り返し地点を宣言し、1 本の `**` getter が全深さを覆う | [再帰パス](#再帰パスrecursion) |
@@ -137,12 +138,14 @@
137
138
  | **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$stream` の `args` の 3 箇所からしか生まれない | [評価のきっかけ](#評価のきっかけdemand-root) |
138
139
  | **モジュール化** | `mount=` がモジュールを 1 本のツリーに接ぎ木し、`state: path` が部分木をコンポーネントにマウントする。個別対応付けは単一キーを繋ぎ、マウントされたコンポーネントの getter はマウント点で公開される | [ボリューム](#追加の状態をマウントするmount) · [丸ごとマウント](#丸ごとマウントstate-path) |
139
140
  | **コンポーネント** | 排他的な 2 方式 — JavaScript クラス+`bind-component` か、HTML だけの DCC か | [機構の選び方](#コンポーネント機構の選び方) |
140
- | **他要素との配線** | wc-bindable プロトコル、spread(`...: obj`)、`#init=` / `#sync=` の authority、プロパティ→属性ミラー | [バインディング authority](#バインディング-authority-init--sync) · [Spread](#spread-バインディング) · [Inputs](#inputs-と属性ミラー) |
141
+ | **他要素との配線** | wc-bindable プロトコル、spread(`...: obj`)、`#init=` / `#sync=` の authority、プロパティ→属性ミラー | [バインディング authority](#バインディング-authority-init--sync) · [Spread](#spread-バインディング-) · [Inputs](#inputs-と属性ミラー) |
141
142
  | **トークン** | command token が state から要素のメソッドを呼び、event token が要素のイベントを state へ戻す | [Command token](#command-tokenメソッドバインディング) · [Event token](#event-tokenイベントバインディング) |
142
- | **時間** | `$stream` が非同期ソースを fold し、`$watch` が headless に反応し、`$scan` が両者を越えて残る累積値を持つ(`$scan` は 3.5 で警告が出て 4.0 で外れる — [4.0 への準備](#40-への準備wcsv4-migration)) | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
143
+ | **時間** | `$stream` が非同期ソースを fold し、`$watch` が headless に反応する | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
143
144
  | **初期化とライフサイクル** | state の供給は 6 通り。`$connectedCallback` 〜 `$stateReadyCallback`、`bootstrapState()` / `createState()` | [状態の初期化](#状態の初期化) · [ライフサイクルフック](#ライフサイクルフック) · [API リファレンス](#api-リファレンス) |
144
- | **診断** | 解決しないパス・添字の本数・階数・getter の循環を報告する。失敗はそのバインディング 1 本に閉じ、値も DOM も巻き戻さない | [診断と失敗の扱い](#診断と失敗の扱い) |
145
- | **配布** | ランタイム依存ゼロ、ビルド不要、ESM、CDN の `/auto` 1 タグ。`unsafe-eval` 不要で Trusted Types 対応 | [インストール](#インストール) · [docs/csp.ja.md](../../docs/csp.ja.md) |
145
+ | **設定** | `bootstrapState()` はそのページでのマークアップの綴りを決め、各 state は自分の振る舞い(`$behavior`)と必要なアドオン(`$features`)を自分で宣言する | [設定](#設定) |
146
+ | **診断** | 解決しないパス・添字の本数・階数・getter の循環を、lint と同じ診断 code とメッセージ番号付きで報告する。失敗はそのバインディング 1 本に閉じ、値も DOM も巻き戻さない | [診断と失敗の扱い](#診断と失敗の扱い) |
147
+ | **セキュリティ** | 値はサニタイズせず、マークアップとしても読まない。自分で書いていないマークアップをページ走査に届かせてはならない | [セキュリティ](#セキュリティ) |
148
+ | **配布** | ランタイム依存ゼロ、ビルド不要、ESM、CDN の `/auto` 1 タグ、またはページが使うアドオンだけを入れる分割エントリ。`unsafe-eval` 不要で Trusted Types 対応 | [インストール](#インストール) · [docs/csp.ja.md](../../docs/csp.ja.md) |
146
149
 
147
150
  ### 周辺パッケージの役割
148
151
 
@@ -154,12 +157,12 @@
154
157
  | [`@wcstack/lint`](../lint/) | `npx @wcstack/lint <file>` が HTML 中の `data-wcs` を実行前に検査する | 診断 code と `getWcsManifest()`(どちらもこの実装から導出)— [診断](#診断と失敗の扱い) |
155
158
  | VS Code 拡張(`wcstack-intellisense`) | 同じ診断と補完をエディタ内で | 同じ manifest と診断 code |
156
159
  | [`@wcstack/typescript`](../typescript/) | `wcs-schema` が型を HTML 検証器へ運び、`wcs-tsc` がインライン state スクリプトを型検査する | `defineState()`、`WcsPaths<T>` / `WcsPathValue<T, P>` — [TypeScript サポート](#typescript-サポート) |
157
- | [`@wcstack/devtools`](../devtools/) | state・配線・更新履歴を見るブラウザパネル | パネルが読む計装 |
160
+ | [`@wcstack/devtools`](../devtools/) | state・配線・更新履歴を見るブラウザパネル | パネルが読む計装(Hook Protocol v2。全部入りのエントリに含まれる `devtools` アドオン) |
158
161
  | [`@wcstack/testing`](../testing/) | `mount()` / `settle()` / `fire()` を 1 import で | 追加パッケージなしで書ける素のレシピ — [ページをテストする](#ページをテストする) |
159
162
  | [`@wcstack/view-transition`](../view-transition/) | リストの移動・削除や分岐の入れ替えを View Transition API でアニメーションさせる | transition-runner の受け渡し。ページに arbiter がなければ変更はそのまま適用される — [遷移アニメーション](#遷移アニメーション) |
160
- | [`@wcstack/router`](../router/) · [`@wcstack/autoloader`](../autoloader/) | 宣言的ルーティング、未定義カスタム要素の自動読み込み | ルートが書き込めるパスと、遅延定義を待つバインディング |
161
- | [I/O ノード群](../../README.ja.md#追加パッケージ) — `fetch`, `storage`, `ws`, `midi`, … | プラットフォーム API を要素として | それらを繋ぐ wc-bindable 配線・spread・トークンプロトコル — [Spread](#spread-バインディング) |
162
- | [`@wcstack/signals`](../signals/) | もう 1 つのリアクティブコア。巨大な keyed リストの生成・追加が 2.5〜3.5 倍速い | 相互運用 —— どちらも wc-bindable を話すので I/O ノードと DCC は共有できる — [パフォーマンス](#パフォーマンス) |
163
+ | [`@wcstack/router`](../router/) · [`@wcstack/autoloader`](../autoloader/) | 宣言的ルーティング、未定義カスタム要素の自動読み込み | ルートが書き込めるパス、ルートの中身が挿入されたときにそれをバインドするバインダ([挿入された内容の中の構造テンプレート](#挿入された内容の中の構造テンプレート))、遅延定義を待つバインディング |
164
+ | [I/O ノード群](../../README.ja.md#追加パッケージ) — `fetch`, `storage`, `ws`, `midi`, … | プラットフォーム API を要素として | それらを繋ぐ wc-bindable 配線・spread・トークンプロトコル — [Spread](#spread-バインディング-) |
165
+ | [`@wcstack/signals`](../signals/) | もう 1 つのリアクティブコア。JavaScript で書く signals と、非同期 IO のリソースアダプタ | 相互運用 —— どちらも wc-bindable を話すので I/O ノードと DCC は共有できる — [パフォーマンス](#パフォーマンス) |
163
166
 
164
167
  ## インストール
165
168
 
@@ -188,15 +191,15 @@
188
191
  <script type="importmap">
189
192
  {
190
193
  "imports": {
191
- "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.5.4/dist/split/core.js",
192
- "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.5.4/dist/split/features/temporal.js",
193
- "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.5.4/dist/split/features/scopes.js"
194
+ "@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.0.0-rc.1/dist/split/core.js",
195
+ "@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.0.0-rc.1/dist/split/features/temporal.js",
196
+ "@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.0.0-rc.1/dist/split/features/scopes.js"
194
197
  }
195
198
  }
196
199
  </script>
197
200
  <script type="module">
198
201
  import { bootstrapState, installFeatures } from '@wcstack/state/core';
199
- import temporal from '@wcstack/state/features/temporal'; // $watch / $stream($scan は 3.5 で警告、4.0 で外れる)
202
+ import temporal from '@wcstack/state/features/temporal'; // $watch / $stream
200
203
  import scopes from '@wcstack/state/features/scopes'; // bind-component・mount=・DCC
201
204
 
202
205
  installFeatures([temporal, scopes]);
@@ -204,10 +207,12 @@
204
207
  </script>
205
208
  ```
206
209
 
207
- `installFeatures([...])` は上の例のとおり `bootstrapState()` の**前**に呼びます。機能は自分のタグ
208
- (`<wcs-ssr>`)を登録することがあり、タグを定義するのは bootstrap だからです。逆順でもエラーには
209
- なりません — 後から install した機能は、すでに bootstrap 済みのレジストリへ自分のタグを定義して
210
- 追いつきます — が、宣言どおりの順で呼べば、最初の要素が upgrade する前にすべてのタグが揃います。
210
+ **アドオンは、どの state が定義されるよりも前に入れてください** — 上の例のとおり、`installFeatures([...])`
211
+ を `bootstrapState()` の前に呼びます。アドオンのキー(`$watch`・`$listKeys` …)を宣言した state は、
212
+ そのアドオンが入っていなければ `[wcs/feature-not-installed]` で失敗し、アドオンを入れる前に作られた
213
+ エンジンが後からそのアドオンを得ることもありません。`bootstrapState()` の後に入れたアドオンも、自分の
214
+ タグ(`<wcs-ssr>`)はすでに bootstrap 済みのレジストリへ定義しますが、最初の `<wcs-state>` が動き出す
215
+ 前にすべてのアドオンが揃っているのは、宣言どおりの順で呼んだときだけです。
211
216
 
212
217
  分割の形は、パッケージのファイルそのものから読みます — 上の例のように jsDelivr の素の `/npm/` パスで
213
218
  版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
@@ -216,27 +221,69 @@
216
221
  見えないコピーへ install され、`installFeatures` を呼んでも `[wcs/feature-not-installed]` で落ちます。
217
222
  素のファイルなら、どのエントリの相対 import も同じチャンクの URL に解決され、エンジンは 1 回だけ評価
218
223
  されます。この形の integrity は [docs/sri.ja.md §5.1](../../docs/sri.ja.md#51-wcstackstate-の分割エントリ) を参照。
224
+ `dist/split/chunks/` 以下のファイル名にはコンテンツハッシュが入ります。チャンクのファイルを自分で列挙する
225
+ 場合(preload リンク、import map の `integrity`)は、読み込むビルドからファイル名を取ってください。
219
226
 
220
227
  | エントリ | 足されるもの |
221
228
  |---|---|
222
- | `@wcstack/state/core` | バインディングの本体: `data-wcs`・`for` / `if`・パス getter・フィルタ・イベント・`$command` / `$on`・`bootstrapState`・`installFeatures` |
223
- | `@wcstack/state/features/temporal` | `$watch`・`$scan`(3.5 で警告、4.0 で外れる — [4.0 への準備](#40-への準備wcsv4-migration))・`$stream` |
229
+ | `@wcstack/state/core` | バインディングの本体: `data-wcs`・`{{ }}` とコメントバインディング・`for` / `if`・パス getter・イベント・`$command` / `$on`・24 個のロジックフィルタ(比較・算術・型変換・デフォルト)・`bootstrapState`・`installFeatures` |
230
+ | `@wcstack/state/features/temporal` | `$watch`・`$stream` |
231
+ | `@wcstack/state/features/list-keys` | `$listKeys` |
224
232
  | `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
225
233
  | `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
226
234
  | `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
227
- | `@wcstack/state/features/formats` | 書式フィルタ群(`upper`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
235
+ | `@wcstack/state/features/formats` | 23 個の書式フィルタ(`upper`・`date`・`round`・`truncate` …) |
228
236
  | `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
229
- | `@wcstack/state/features/diagnostics` | 開発時の警告: 束縛・`$watch`・`$scan` のパスが state 上で解決できないとき、did-you-mean 付きで知らせる。入れなければ静か(本番向け)。throw するエラーの文言はどちらでも変わらない |
237
+ | `@wcstack/state/features/diagnostics` | エラーメッセージの文章(did-you-mean と lint への案内を含む)と、開発時の警告(束縛や `$watch` のパスが解決しない、`wcs/default-getter-mismatch`)。入れなければ、メッセージは code・番号・値だけになり([エラーメッセージ](#エラーメッセージと番号))、これらの警告は出ない |
230
238
  | `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
231
239
 
232
- 機能が入っていない宣言は黙って無視されません。state の定義時に
233
- `[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
234
- で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。`$watch` を宣言した
235
- ボリューム(`<wcs-state mount=…>`)も接ぎ木の時点で同じ `[wcs/feature-not-installed]` で落ちます —
236
- ボリュームの state は要素自身の宣言の門を通らないためです。install は冪等で、
237
- どのエントリも core のチャンクを 1 つだけ共有します(機能側がエンジンの 2 つ目のコピーを抱える
238
- ことも、**別の機能**のコピーを抱えることもありません — 機能をまたぐ呼び出しは、持ち主の機能が
239
- install 時に埋める受け口を通ります)。
240
+ アドオンが入っていない宣言は黙って無視されません。state の読み込み時に
241
+ `[wcs/feature-not-installed] $watch needs the add-on @wcstack/state/features/temporal` を throw し
242
+ (`mount=`・`bind-component`・`data-wc-definition` は同じ形で `scopes` を、`enable-ssr` は `ssr` を
243
+ 名指します)、`formats` なしの書式フィルタは束縛計画の段で、アドオン名を挙げた `[wcs/filter-unknown]`
244
+ を throw します。install は冪等で — 2 回目の install は読み飛ばされます — どのエントリも core の
245
+ チャンクを 1 つだけ共有します。アドオンがエンジンの 2 つ目のコピーを抱えることも、**別のアドオン**の
246
+ コピーを抱えることもありません(アドオンをまたぐ呼び出しは、持ち主のアドオンが install 時に埋める
247
+ 受け口を通ります)。
248
+
249
+ #### 分割版の auto エントリ
250
+
251
+ `dist/split/auto.js` は、import map なしで、script タグ 1 つから分割ビルドを読み込みます。core と、
252
+ ページが名指したアドオンを、同じビルドから読み込みます。
253
+
254
+ ```html
255
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.0.0-rc.1/dist/split/auto.js"></script>
256
+ <wcs-state features="scopes diagnostics">
257
+ <script type="module">
258
+ export default {
259
+ $features: ["temporal", "formats"],
260
+ price: 1200,
261
+ $watch: { price(cur) { console.log(cur); } },
262
+ };
263
+ </script>
264
+ </wcs-state>
265
+ <p>{{ price|locale }}</p>
266
+ ```
267
+
268
+ アドオンは 2 箇所で、それぞれ別の理由から名指します:
269
+
270
+ | | ルートの `<wcs-state>` の `features="…"` | state の中の `$features: [...]` |
271
+ |---|---|---|
272
+ | どこに書くか | 文書のルートの `<wcs-state>` — `mount` も `bind-component` も持たない最初の 1 つ | ボリューム以外の任意の state(ルート、マウントされたコンポーネント、DCC) |
273
+ | いつ読まれるか | 1 回だけ、`<wcs-state>` が定義される前 | その state が読み込まれるとき、エンジンが作られる前 |
274
+ | 何のためか | どの `<wcs-state>` が動き出すよりも前に揃っているべきもの — `mount=` / `bind-component` / DCC の要素が何であるかを決める `scopes` — と、ページの開発補助(`diagnostics`・`devtools`) | その state が必要とするアドオン(`$watch` のための `temporal`、フィルタのための `formats`) |
275
+ | 誰が読むか | 分割版の auto エントリだけ | すべてのエントリ: 分割版の auto エントリは足りないアドオンを読み込み、それ以外のエントリは入っているかを確かめ、入っていなければその state を `[wcs/feature-not-installed]` で失敗させる |
276
+
277
+ 名前は `formats`・`diagnostics`・`temporal`・`list-keys`・`scopes`・`recursion`・`ssr`・`devtools` です。
278
+ `features=` にそれ以外の名前があると、分割版の auto エントリは `<wcs-state>` を定義する前に
279
+ `[wcs/feature-unknown]` で失敗します。`$features` にあれば、その state が失敗します(分割版の auto
280
+ エントリでは `[wcs/feature-unknown]`、それ以外では `[wcs/feature-not-installed]`)。配列でない
281
+ `$features` は `#46` を throw し、ボリュームは `$features` を宣言できません。
282
+
283
+ `dist/split/auto.js` も、上と同じ理由で、版を固定した素の `/npm/` パスから読み、`esm.run` は通さない
284
+ でください。`exports` には入っていません: 実行時に組み立てる URL をバンドラは追えないので、バンドルする
285
+ ページは `@wcstack/state/core` を import して `installFeatures` を呼びます。実行時に import するアドオンは
286
+ その `<script>` の `integrity` の外にあり、CSP の下では CDN のホストを許可しておく必要があります。
240
287
 
241
288
  ## 基本的な使い方
242
289
 
@@ -330,6 +377,10 @@ install 時に埋める受け口を通ります)。
330
377
 
331
378
  解決順序: `state` → `src` (.json / .js) → `json` → 内包 `<script>` → `setInitialState()` 待機。
332
379
 
380
+ 状態の読み込み中にルートから外されたルートの `<wcs-state>`(ページの中身が入れ替わった、要素が移された)は、そこには何もマウントせず、次に接続された場所で(すでに接続されていればすぐに)始め直します。`setInitialState()` で渡された状態はそのために保持しますが、`state` / `src` / `json` / 内包スクリプトのソースは読み直します —— fetch はもう一度走り、内包モジュールはトップレベルのコードも含めてもう一度評価されます。
381
+
382
+ `state="<id>"` が読むのは、その id を持つ `<script type="application/json">` だけです — まず要素自身のルート(shadow root)で、次に document で探し、同じ id を持つ別の要素は対象になりません。読めないソースは、その要素の初期化を失敗させます: そのような script がない(`#16`)、`.json` を取得できない(`#13`、HTTP ステータス付き)、JSON がパースできない、モジュールが throw する([初期化の失敗](#初期化の失敗))。空の JSON script は `{}` として扱います。
383
+
333
384
  5 番の `<script type="module">` はブラウザ自身も評価します(`<wcs-state>` の中にあっても止まりません)。export はどこにも届かないので state には影響しませんが、トップレベルのコードは 2 回走ります(ブラウザが 1 回、state が 1 回)。トップレベルに副作用(リクエスト・ログ出力・グローバルへの代入)を置かないでください。CSP 下では下の注記を参照してください。
334
385
 
335
386
  > **Content-Security-Policy 下では:** 5 番(内包 `<script type="module">`)は `blob:` URL 経由で評価されるため、state を読み込む `<script>` にページの nonce を付けるか(blob: の import がその nonce を引き継ぎます)、`script-src blob:` が必要です。厳格な CSP を敷く場合は 4 番(`src="./state.js"`)を使ってください。追加ディレクティブは不要です。なお 5 番の `<script>` はブラウザ自身も評価します。CSP 下では、その `<script>` にも nonce を付けない限りコンソールに違反が 1 件出ます。nonce を付けた場合や CSP が無い場合は、トップレベルのコードが 2 回走ります(副作用を置かないこと)。詳細は [docs/csp.ja.md](../../docs/csp.ja.md)。
@@ -345,34 +396,30 @@ install 時に埋める受け口を通ります)。
345
396
  <div data-wcs="textContent: cart.total"></div>
346
397
  ```
347
398
 
348
- ボリュームは getter・`$watch`・`$listKeys`・`$renderedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
399
+ ボリュームはデータ・getter・setter・メソッドを持ち、`$connectedCallback` / `$disconnectedCallback` を宣言できます。そのどれの中でも `this` はマウントパスを基点にします: `this.items` はツリーの `cart.items` で、パスを取る `$` API(`$getAll`・`$setAll`・`$resolve`・`$postUpdate`・`$dependOn`・`$eq`・`$eqPath`・`$eqIndex`)もマウント相対のパスを取ります。getter と setter はツリーの `cart.<キー>` のアクセサになり、メソッドはパスで呼べます —— `onclick: cart.checkout`、ルートからは `this["cart.checkout"]()`。読み込み順は自由です: ルートより先に接続されたボリュームは、ルートのエンジンが作られるとき、ページがバインドされる前に接ぎ木されます。
349
400
 
350
- **ルートのパスをボリュームへ注入する(3.1)。** ボリュームのコードが自分の外のパスを読むには、ボリューム要素の `data-wcs` にコンポーネントの部分マウントと同じ形で書きます:
401
+ ボリュームは、ツリー全体に反応する宣言を実行しません。`$watch`・`$stream`・`$listKeys`・`$renderedCallback`・`$recursion`・`$behavior`・`$features` —— そして削除された `$scan`・`$streams`・`$updatedCallback` —— を宣言したボリュームは拒否されます: 接ぎ木されず、`console.error` が要素を名指して `$watch is not run in a volume — declare it on the root state.` と伝えます。これらはルートの state にフルパスで宣言してください —— ルートのハンドラの中では `this` はルートで(`$watch: { "cart.total"(cur) { … } }`)、ルートの `$renderedCallback(paths)` はルートから書いたパス(`cart.items.*.name`)を受け取るので、接頭辞で絞り込みます。`$commandTokens`・`$eventTokens`・`$on`・`$errorCallback` は、`console.warn` を出して実行されません。ボリュームは注入も受けません: ボリューム要素に `data-wcs`(`state.taxRate: settings.taxRate`)があると、そのボリュームは拒否されます —— ルートのパスはルートの getter で読んでください(`get cartTotalWithTax() { return this["cart.subtotal"] * (1 + this["settings.taxRate"]); }`)。
351
402
 
352
- ```html
353
- <wcs-state mount="cart" src="./cart.js" data-wcs="state.taxRate: settings.taxRate"></wcs-state>
354
- ```
355
-
356
- `cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$renderedCallback` はその更新を内側の名前 `taxRate` で受けます。注入はボリュームのコードの中の名前です — ページは今までどおり `settings.taxRate` を読み、`cart.taxRate` はツリーにありません。
403
+ 拒否されたボリュームは、接ぎ木せずに決着し、理由を報告して、`connectedCallbackPromise` を resolve します。state の読み込みに失敗したボリュームも同じです。そのほかの拒否: 別のボリュームがすでに持っているマウントパス(`will not graft: another volume already holds "cart".`)、ルートの state がすでに持っているパス(`will not graft: the root state already has "cart".` —— ただし SSR では、ボリュームはそこにあるサーバーのデータを引き継ぎます)、ホストに配線されたコンポーネントの shadow root の中のボリューム(`will not graft: its component is wired to its host.` —— そのコンポーネントはホストのツリーを読むので、データはホストの state に置いてください)。ルートの `<wcs-state>` が初期化に失敗した場合、それを待っていたボリュームと、後から読み込まれるボリュームは `will not graft: the root state failed to initialize.` を報告し、永久に待たずに決着します。それが終点です: あとから修正版のルートを接続しても復帰しません —— 壊れたルートとボリュームを取り除き、新しい要素を追加してください。ボリュームの `$connectedCallback` が throw した場合は `console.error` で報告され、ボリュームは接ぎ木されたままです。
357
404
 
358
- - **1 キーずつ**: 左辺は `state.<キー>` の 1 段です(`state: …` でボリューム全体を動かすことはできません — `mount` を変えてください)。
359
- - **注入が自前のキーに勝つ**: ボリュームが同名のデータキー(`taxRate: 0`)を宣言していても、その値は接ぎ木されません。同名の getter・setter・メソッドはどちらが `this.taxRate` か曖昧なので、そのボリュームは接ぎ木せずにエラーを報告します。
360
- - **`#ro`**: `state.taxRate#ro: settings.taxRate` なら読めても書けません。ボリュームのコードからの代入・`$setAll`・書きの `$resolve` は `[wcs/mount-readonly]` を投げます(ルート自身は書けます)。
361
- - **静的なパスだけ**: 右辺にワイルドカード(`items.*.x`)や `$` 始まりのパスは使えません(ボリュームにはループ文脈がありません)。フィルタも受けません — 変換は getter で行ってください。形の誤りは `[wcs/mount-path-invalid]` で、ロードの前に報告されます。
405
+ 接ぎ木済みのボリュームは、外してもデータとマウントパスを保ちます: アンマウントはありません。ルートを待っている間にページから外されたボリューム(ページの中身が入れ替わった)は、同じマウントパスを求める新しいボリュームにそれを譲り、接ぎ木せずに settle します。ルートはその下のパスに書き込めますが、ボリュームの接ぎ木先より上にあるオブジェクトを置き換える書き込み(`mount="shop.cart"` のときの `this.shop = …`)は throw し、ルートやボリュームの再設定(`setInitialState()`)も throw します。マウントパスは静的パスのみです(`*`・`$`・`#`・`@`・空白は不可)。初期化後に `mount` 属性を変更することはできず、変更は無視されます —— 要素を取り除き、望むパスで新しい要素を追加してください。
362
406
 
363
- **スコープごとに動くもの**(3.0 で 1 つの表にしました — 要件 B11。ここに無言で無視されるものはありません):
407
+ **スコープごとに動くもの**(ここに無言で無視されるものはありません):
364
408
 
365
409
  | 宣言 | ルートの `<wcs-state>` | ボリューム `<wcs-state mount="p">` | マウントされたコンポーネント(`state: …` 付きの `bind-component`) |
366
410
  |---|---|---|---|
367
- | データキー・getter・setter・メソッド | 動く | 動く(`p` 相対) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
411
+ | データキー・getter・setter・メソッド | 動く | 動く(`p` 相対。メソッドはパスで呼べる — `p.method`) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
368
412
  | `$connectedCallback` / `$disconnectedCallback` | 動く | 動く(`p` 相対) | 動く |
369
- | `$watch`・`$listKeys`・`$renderedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
370
- | `$stream`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
371
- | `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — 警告 | 動かない — 警告 |
372
- | `$errorCallback` | 動く | 動かない — 警告(3.0 より前は無言) | 動かない — 警告(3.0 より前は無言) |
373
- | 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着して枠を返す。`connectedCallbackPromise` は resolve(`scopes` 機能が無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
413
+ | `$watch`・`$stream`・`$renderedCallback` | 動く | 拒否: 接ぎ木されず `console.error` | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
414
+ | `$listKeys` | 動く | 拒否: 接ぎ木されず `console.error` | 動く(コンポーネント自身のリストに対して) |
415
+ | `$recursion`・`**` の getter | 動く | 拒否 | `[wcs/mount-dollar-declaration]` を throw |
416
+ | `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — `console.warn` | 動く(コンポーネント自身のバインディングに対して) |
417
+ | `$errorCallback` | 動く | 動かない — `console.warn` | 動く(コンポーネント自身のバインディングの失敗に対して) |
418
+ | `$behavior`・`$features` | 動く | 拒否: 接ぎ木されず `console.error` | 動く(コンポーネント自身のツリーに対して) |
419
+ | 要素への注入(`data-wcs="state.<key>: …"`) | — | 拒否: 接ぎ木されず `console.error` | —(ホストの配線がマウントそのもの) |
420
+ | 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着。`connectedCallbackPromise` は resolve(`scopes` アドオンが無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
374
421
 
375
- マウントされていないコンポーネント(自前の `<wcs-state>` を持つ素の Shadow DOM の子)はそれ自体がルートで、1 列目のすべてが動きます。
422
+ マウントされていないコンポーネント(自前の `<wcs-state>` を持つ素の Shadow DOM の子や、ホストが配線しない Shadow DOM の `bind-component`)はそれ自体がルートで、1 列目のすべてが動きます。
376
423
 
377
424
  > **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。
378
425
 
@@ -387,6 +434,12 @@ this["user.name"] = "Bob"; // パス "user.name"
387
434
 
388
435
  ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
389
436
 
437
+ state にないトップレベルのキーへの書き込みは、そのキーを作ります(`this.later = 5`)。そのようなキーの読み取りは
438
+ `[wcs/binding-path-missing] Path "later" does not exist on the state tree.`(`#301`)を throw するので、
439
+ state のどこにも宣言されていないキーへのバインディングは失敗します([診断](#診断と失敗の扱い))——
440
+ バインドするキーは、`null` も含めてすべて初期状態に宣言してください。存在しない深いパス
441
+ (`user.nickname`)は `undefined` と読めます。
442
+
390
443
  ### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
391
444
 
392
445
  これは単なる制約ではなく、**契約境界が見えている箇所**です。
@@ -422,6 +475,9 @@ this.items.splice(index, 1);
422
475
  this.items.sort((a, b) => a.id - b.id);
423
476
  ```
424
477
 
478
+ 要素 1 つへの書き込み(`this["items.0"] = obj`)もパスへの代入です: その位置の値を置き換え、その位置の
479
+ 行が新しい値を表示します。行を値ごと動かすには、新しい配列を代入してください —— [`$resolve`](#resolve--明示的なインデックスでのアクセス) を参照。
480
+
425
481
  ## バインディング構文
426
482
 
427
483
  ### `data-wcs` 属性
@@ -436,12 +492,12 @@ property[#modifier]: path[|filter[|filter(args)...]]
436
492
  <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
437
493
  ```
438
494
 
439
- 区切りの `;` と `|` は引用符の外でだけ区切るので(3.0)、引用符付きのフィルタ引数に含められます: `textContent: tags|join('; ')`、`title: parts|join(' | ')`。3.0 より前はどちらも束縛が壊れていました。
495
+ 区切りの `;` と `|` は引用符の外でだけ区切るので、引用符付きのフィルタ引数に含められます: `textContent: tags|join('; ')`、`title: parts|join(' | ')`。
440
496
 
441
497
  | 要素 | 説明 | 例 |
442
498
  |---|---|---|
443
499
  | `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
444
- | `#modifier` | バインディング修飾子 | `#ro`, `#prevent`, `#stop`, `#onchange` |
500
+ | `#modifier` | バインディング修飾子 | `#ro`, `#prevent`, `#stop`, `#onchange`, `#direct` |
445
501
  | `path` | 状態プロパティパス | `count`, `user.name`, `users.*.name` |
446
502
  | `\|filter` | 変換フィルタチェーン | `\|gt(0)`, `\|round\|locale` |
447
503
 
@@ -459,16 +515,16 @@ property[#modifier]: path[|filter[|filter(args)...]]
459
515
  | `attr.NAME` | 属性の設定(SVG 名前空間対応) |
460
516
  | `radio` | ラジオボタングループバインディング(双方向) |
461
517
  | `checkbox` | チェックボックスグループの配列バインディング(双方向) |
462
- | `onclick`, `on*` | イベントハンドラバインディング |
463
- | `.NAME` | 明示のプロパティ(3.1)— `on` で始まってもイベントにしない |
518
+ | `onclick`, `on*` | イベントハンドラバインディング — [イベントハンドリング](#イベントハンドリング) 参照 |
519
+ | `.NAME` | 明示のプロパティ — `on` で始まってもイベントにしない |
464
520
 
465
- **`on` で始まるプロパティ(3.1)。** 名前が `on` で始まる束縛はイベントになります(`onclick:`)。そのため `online: x` は "line" イベントを待ち、要素の `online` プロパティには書きません。`on` で始まるプロパティに値を書くには、左辺の先頭にドットを付けます:
521
+ **`on` で始まるプロパティ。** 名前が `on` で始まる束縛はイベントになります(`onclick:`)。そのため `online: x` は `"line"` イベントを待ち、要素の `online` プロパティには書きません。`on` で始まるプロパティに値を書くには、左辺の先頭にドットを付けます:
466
522
 
467
523
  ```html
468
524
  <my-status data-wcs=".online: isOnline; onclick: refresh"></my-status>
469
525
  ```
470
526
 
471
- ドットの付いた形はドットの無い形と同じ束縛で、イベントにならない点だけが違います(`.value:` は `value:` と同じく双方向、修飾子と入力フィルタもそのまま使えます)。名前空間の語(`.class` / `.attr` / `.style` / `.command` / `.eventToken`)と空の名前は `[wcs/binding-syntax]` で拒否します。
527
+ ドットの付いた形はドットの無い形と同じ束縛で、イベントにならない点だけが違います(`.value:` は `value:` と同じく双方向、修飾子と入力フィルタもそのまま使えます)。名前空間の語(`.class` / `.attr` / `.style` / `.command` / `.eventToken` / `.state`)と空の名前は `[wcs/binding-syntax]` で拒否します。
472
528
 
473
529
  ### 修飾子
474
530
 
@@ -476,16 +532,17 @@ property[#modifier]: path[|filter[|filter(args)...]]
476
532
  |---|---|
477
533
  | `#ro` | 読み取り専用 — 双方向バインディングを無効化 |
478
534
  | `#prevent` | イベントハンドラで `event.preventDefault()` を呼び出す |
479
- | `#stop` | イベントハンドラで `event.stopPropagation()` を呼び出す |
535
+ | `#stop` | イベントハンドラで `event.stopPropagation()` を呼び出す — 委譲されたハンドラでは、外側の `on*:` ハンドラを止めるが、祖先に自分で付けたリスナーは止めない([イベントハンドリング](#イベントハンドリング) 参照) |
536
+ | `#direct` | `on*:` バインディングで、リスナーをルートへ委譲せず要素自身に付ける — [イベントハンドリング](#イベントハンドリング) 参照 |
480
537
  | `#onchange` | 双方向バインディングで `input` の代わりに `change` イベントを使用 |
481
538
  | `#init=<authority>` | バインディングの authority / 初期同期の向き — [バインディング authority](#バインディング-authority-init--sync) 参照 |
482
539
  | `#sync=<timing>` | 要素スナップショットの読み取りタイミング — [バインディング authority](#バインディング-authority-init--sync) 参照 |
483
540
 
484
- 複数の修飾子は 1 つの `#` の後にカンマ区切りで書きます: `value#ro,init=none: path`
541
+ 複数の修飾子は 1 つの `#` の後にカンマ区切りで書きます: `value#ro,init=none: path`、`onclick#direct,stop: save`
485
542
 
486
- 修飾子は束縛の種別を変えません: `radio#ro:` と `checkbox#ro:` は radio / checkbox の束縛のままです(3.0)。3.0 より前は修飾子を付けると `radio` という名前の素のプロパティ束縛になり、radio グループの `#ro` が効いていませんでした。
543
+ 修飾子は束縛の種別を変えません: `radio#ro:` と `checkbox#ro:` は radio / checkbox の束縛のままです。その束縛が使わない修飾子は無視されます(プロパティ束縛の `#direct`、未知の `#foo`)。未知の `key=value` 形式の修飾子(`#foo=1`)は throw します。
487
544
 
488
- 3.0 からパーサは、以前黙って丸めていた書き方を `[wcs/binding-syntax]` で拒否します: 2 つ目の `#`(`value#ro#wo` は `ro` だけ残して後ろを捨てていた — `value#ro,wo` と書く)、`else:` の後ろの値(`else:` と書く)、`for` / `if` / `elseif` / `else` / `...` の左辺の修飾子やフィルタ、フィルタ引数の閉じていない引用符。
545
+ パーサは次の書き方を `[wcs/binding-syntax]` で拒否します: 2 つ目の `#`(`value#ro#wo` ではなく `value#ro,wo` と書く)、`else:` の後ろの値(`else:` と書く)、`for` / `if` / `elseif` / `else` / `...` の左辺の修飾子やフィルタ、`for:` の出力フィルタ(`#121` — [ループ](#ループ-for) 参照)、`__proto__` や `prototype` というパスセグメント(`#120` — 書き込み・`$resolve`・`$setAll`、そして `this.__proto__` のような読み取りでも拒否される)、フィルタ引数の閉じていない引用符。
489
546
 
490
547
  ### 双方向バインディング
491
548
 
@@ -498,13 +555,13 @@ property[#modifier]: path[|filter[|filter(args)...]]
498
555
  | `<select>` | `value` | `change` |
499
556
  | `<textarea>` | `value` | `input` |
500
557
 
501
- `<input type="button">` は除外されます。`#ro` で無効化、`#onchange` でイベントを変更できます。
558
+ `<input type="button">` は除外されます。`#ro` で無効化、`#onchange` でイベントを変更できます。値を書き戻すリスナーは要素自身に付きます —— 双方向バインディング・radio・checkbox は委譲されません。
502
559
 
503
560
  ### バインディング authority (`#init=` / `#sync=`)
504
561
 
505
562
  **これが解決する問題。** バインディングが attach する時点で既に値を持っている要素 —— 永続値をロード済みの `<wcs-storage>`、時計、自分のスナップショットを復元するウィジェット —— は state の初期値に上書きされます。双方向バインディングの初期同期が state→element に書くからです。そのバインディングにだけ `#init=element` を付けると、初期同期は*要素*が勝ちます。以後の変更は通常どおり両方向に流れます。このケース(load-before-bind)は下で具体的に説明します。この節の残りは、それを一例として含む一般規則です。
506
563
 
507
- `static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority**(バインディング attach 時の**初期同期**をどちら側が勝つか)を解決します。定常時の方向は authority とは別に、メンバの宣言形状で決まります: output-only メンバは state からの書き込みを恒久的に受け付けず(契約)、双方向メンバは初期同期の勝者と無関係に以後は両方向に流れます。既定 authority はメンバの宣言位置から導出されます(`enableDirectionalInitialSync` で既定 ON):
564
+ `static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority**(バインディング attach 時の**初期同期**をどちら側が勝つか)を解決します。定常時の方向は authority とは別に、メンバの宣言形状で決まります: output-only メンバは state からの書き込みを恒久的に受け付けず(契約)、双方向メンバは初期同期の勝者と無関係に以後は両方向に流れます。既定 authority はメンバの宣言位置から導出されます(既定 ON。`$behavior: { enableDirectionalInitialSync: false }` でその state ツリーについて OFF にできます — [`$behavior`](#behavior) 参照):
508
565
 
509
566
  | メンバの宣言位置 | 既定 authority | 効果 |
510
567
  |---|---|---|
@@ -519,7 +576,7 @@ property[#modifier]: path[|filter[|filter(args)...]]
519
576
 
520
577
  要素が `properties[].event` を dispatch したとき、state に書かれる値は **`getter(event)`** です。`getter` を省略するとプロトコル既定の [`(e) => e.detail`](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/SPEC.md#default-getter) が適用され、**`detail` 全体がそのまま**書かれます。このとき宣言したプロパティは要素から読まれ*ません*。イベントのペイロードが正です。`wcBindable` を持たない素の HTML 要素は逆で、`input`/`change` 時に `element[propName]` を読みます。
521
578
 
522
- したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state に書きます。しかもこの失敗はほぼ無言です。書き戻し(`Number({ value: … })` → `NaN`)は例外を投げず、`@wcstack/lint` にも見えません(ペイロードの形は静的に分かりません)。ランタイムは、イベント時点で見分けられる 2 形についてだけ、要素 × プロパティごとに 1 回警告します(`wcs/default-getter-mismatch`): 要素プロパティに値があるのに `detail` が `undefined`(素の `Event`、または `detail` の付け忘れ)、および要素プロパティがオブジェクトでないのに `detail` が `<propName>` キーを持つオブジェクト(上のラッパー)。それ以外の不整合は気づかれずに通り、どちらの場合も書き込み自体はそのまま行われます。次の 2 形のどちらかに揃えてください:
579
+ したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state に書きます。しかもこの失敗はほぼ無言です。書き戻し(`Number({ value: … })` → `NaN`)は例外を投げず、`@wcstack/lint` にも見えません(ペイロードの形は静的に分かりません)。`diagnostics` アドオン(全部入りのエントリに含まれる)は、イベント時点で見分けられる 2 形についてだけ、要素 × プロパティごとに 1 回警告します(`wcs/default-getter-mismatch`): 要素プロパティに値があるのに `detail` が `undefined`(素の `Event`、または `detail` の付け忘れ)、および要素プロパティがオブジェクトでないのに `detail` が `<propName>` キーを持つオブジェクト(上のラッパー)。それ以外の不整合は気づかれずに通り、どちらの場合も書き込み自体はそのまま行われます。次の 2 形のどちらかに揃えてください:
523
580
 
524
581
  ```javascript
525
582
  class YenInput extends HTMLElement {
@@ -576,9 +633,9 @@ authority はバインディング単位で `#init=` により上書きできま
576
633
 
577
634
  注意:
578
635
 
579
- - `enableDirectionalInitialSync: false`(opt-out)のとき `#init=`/`#sync=` を書くと throw します。
580
- - **1.20 以前からの移行:** output-only メンバに対して state 側に都合のよい初期値(`value: []`、`query: ""` 等)をシードしないでください — 要素側の実初期値(多くは `null`/`undefined`)がシードを置き換えます。シードは要素の実初期値に合わせ、表示用の値は派生 getter で null ガードしてください。
581
- - **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.ja.md` §3.6)。
636
+ - `$behavior: { enableDirectionalInitialSync: false }`(opt-out)のとき、`#init=`/`#sync=` を書くと throw し(`#31`)、メンバは宣言と照合されず、output-only メンバも含めてすべての初期同期で state が勝ちます。
637
+ - output-only メンバに対して state 側に都合のよい初期値(`value: []`、`query: ""` 等)をシードしないでください — 要素側の実初期値(多くは `null`/`undefined`)がシードを置き換えます。シードは要素の実初期値に合わせ、表示用の値は派生 getter で null ガードしてください。
638
+ - 宣言に載っていないメンバへのバインディングは throw します(`Property "x" is not declared by wcBindable.`、`#38`)。メンバの形状が許さない `#init=`(`#39`)、観測可能なプロパティでないメンバへの `sync=connect`(`#40`)も同様です。authority が支配するのは初期同期だけです(`docs/architecture-hardening/09-remediation-design.ja.md` §3.6)。
582
639
 
583
640
  ### ラジオボタンバインディング
584
641
 
@@ -616,18 +673,35 @@ authority はバインディング単位で `#init=` により上書きできま
616
673
 
617
674
  ### Mustache 構文
618
675
 
619
- `enableMustache` が `true`(デフォルト)の場合、テキストノードで `{{ expression }}` が使用できます:
676
+ テキストノードの中の `{{ expression }}` はテキストバインディングです —— 中身はパスと省略可能なフィルタチェーンで、`textContent:` の右辺と同じです:
620
677
 
621
678
  ```html
622
679
  <p>こんにちは、{{ user.name }}さん!</p>
623
680
  <p>カウント: {{ count|locale }}</p>
624
681
  ```
625
682
 
626
- 内部的にはコメントベースのバインディング(`<!--@@:expression-->`)に変換されます。
683
+ テキストノードは波括弧の位置で分割されます: 文字どおりのテキストはそのまま残り、`{{ }}` はそれぞれ値を表示する独立したテキストノードになります。`$behavior: { enableMustache: false }` でその state ツリーについて OFF にできます([`$behavior`](#behavior))。その場合、波括弧はテキストのまま残ります。
684
+
685
+ state が読み込まれる前は、`<template>` の外の `{{ }}` は書いたとおりに見えています。そのちらつきが問題になり、ページがサーバー描画でない場合は、代わりにコメントバインディングを書いてください。
686
+
687
+ #### コメントバインディング
688
+
689
+ `<!--@@: expression-->`(または `<!--@@wcs-text: expression-->`)は、フィルタも含めて `{{ expression }}` と同じテキストバインディングを、コメントとして書いたものです —— そのため、バインドされる前には何も表示されず、要素も増えません:
690
+
691
+ ```html
692
+ <p>こんにちは、<!--@@: user.name-->さん!</p>
693
+ <p>合計: <!--@@wcs-text: total|locale--></p>
694
+ ```
695
+
696
+ - コメントは同じ位置のテキストノードに置き換えられます。ページレベルでも、`for:` / `if:` のテンプレートの中でも同じです。
697
+ - `enableMustache` の値にかかわらずバインドされます: `{{ }}` を OFF にしても、コメントバインディングは OFF になりません。
698
+ - キーワードは空か `wcs-text` です。それ以外のコメント(`<!--@@route:…-->`、このパッケージや `@wcstack/server` が書くアンカー、普通のコメント)には手を付けません。式は複数行にまたがってもかまいません。
699
+ - `<textarea>` や `<title>` の中のコメントはバインドされません: ブラウザはその中身をテキストとしてパースするためです。
700
+ - `@wcstack/lint` と VS Code 拡張は、`<template>` の外の `{{ }}` よりもこの形を推奨します。
627
701
 
628
702
  ### Spread バインディング (`...`)
629
703
 
630
- `wc-bindable` プロトコルを宣言したカスタム要素に対して、`...: target` を使うと要素の **properties + inputs を 1 行で一括配線**できます:
704
+ [`wc-bindable` プロトコル](#bindables--commands-と-wc-bindable-プロトコル)を宣言したカスタム要素に対して、`...: target` を使うと要素の **properties + inputs を 1 行で一括配線**できます:
631
705
 
632
706
  ```html
633
707
  <wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
@@ -664,20 +738,34 @@ export default {
664
738
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
665
739
  ```
666
740
 
667
- **`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` でログが出ます。
741
+ **`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。(このスキップは spread に限らず、要素の input に値を渡すすべてのプロパティバインディングに適用されます。)**表示の表面は別です:** `textContent` / `innerText` / `innerHTML`・mustache やコメントバインディングのテキスト・`attr.*`・`style.*`・`class.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除され、クラスは外れます。`class.*` だけは範囲が狭く、「値が無い」扱いになるのは `undefined` と `null` だけです — **それ以外の非 boolean は今も throw します**。クラスの束縛は真偽の切り替えなので、truthy な文字列は意図というより書き間違いである方がずっと多いためです。「truthy として扱う」つもりなら `class.on: flag|truthy` と書いてください。
668
742
 
669
- **フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`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)`)。
743
+ **フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`String(value)` で結果を組み立てるフィルタは、値が無いときに `"undefined"` という**文字**を作ってしまいかねません。書式の族(`upper` / `lower` / `capitalize` / `trim` / `slice` / `padStart` / `padEnd` / `repeat` / `reverse` / `truncate` / `unit`)は代わりに `undefined` / `null` を素通しするので、`attr.title: x|trim` はフィルタの無い `attr.title: x` と同じく属性を消します。次の 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)`)。
670
744
 
671
745
  **制約事項**:
672
746
 
673
747
  - spread 右辺へのフィルタ(`...: target|filter`)はエラー
674
748
  - 右辺パスの途中に `*` を含めても OK(例:`...: stores.*.fetch`)
675
749
  - 右辺は素のツリーパス(`...: fetchX`、途中の `*` も可)
676
- - カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)。`for:` / `if:` のテンプレートの中でも同じ — 行や枝はすぐに描かれ、クラスが定義されたらそれぞれの行の要素をその行の値で展開する。定義前に消えた行・閉じた枝は展開しない。待つ間に文書から外した要素(タブやパネルの DOM 退避)は、外れている間はレジストリに何も残さず、戻ったら戻った先のツリーのレジストリで待ち直し、クラスが定義されたら(定義済みならすぐに)展開する(行の要素は、その行がまだ一覧に居る間だけ)
677
- - `wcBindable` 宣言**のない**要素はエラー(明示配線で書いてください)。spread は何を展開すべきかを契約から読み取るため
750
+ - カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)。`for:` / `if:` のテンプレートの中でも同じ — 行や枝はすぐに描かれ、クラスが定義されたらそれぞれの行の要素をその行の値で展開する。定義前に消えた行・閉じた枝は展開せず、何も生かし続けない。待つのはその要素を定義するレジストリ —— 要素が置かれた shadow root のスコープ付きレジストリも含む
751
+ - `wcBindable` 宣言**のない**要素はエラー(`[wcs/spread-no-bindable]`。明示配線で書いてください)— ネイティブ要素は定義を待たずに即座にエラーになる。spread は何を展開すべきかを契約から読み取るため
678
752
 
679
753
  **Composite shell**(wc-bindable Composition Profile)はそのままサポートされます:composite shell は標準の `target.constructor.wcBindable` を通じて synthesized declaration を露出するため、`"s3.progress"` のような composed name はフラットな要素メンバーキーとして扱われます。state を composed 構造に合わせて (`{ s3: { progress: 0 } }`) 持てば、`...: pipeline` が自動的に nested state path へ展開されます。
680
754
 
755
+ ### ページ走査が何をバインドするか
756
+
757
+ `<wcs-state>` は自分のルート(document、shadow root、または Light DOM コンポーネントのホスト)をバインドするとき、マークアップを 1 回走査し、その中の `data-wcs` 属性・`{{ }}`・コメントバインディングを読みます:
758
+
759
+ - **値は決してマークアップにならない。** バインディングがページに入れるもの —— `textContent:` や `innerHTML:` の値、カスタム要素がバインドされた値から描画する子 —— がバインディングとして読まれることはありません: その中の `{{ }}`・`data-wcs`・コメントバインディングは、ただのテキストのまま残ります。
760
+ - バインディングが中身を設定する要素(`textContent:`・`text:`・`innerText:`・`innerHTML:`・`html:`)の子はバインドされません。ページレベルでもテンプレートの中でも同じで、`#init=element` や `#init=none` によって書いた子がそのまま残る場合も同じです。`outerHTML:` / `outerText:` をバインドしたページレベルの要素の子もバインドされません。バインディングが必要なマークアップは別の要素に置いてください。
761
+ - `<script>`・`<style>`・`<noscript>`・`<iframe>` の中身は走査しません。その要素自身の `data-wcs`(`srcdoc:`、`attr.src:`)はバインドされます。
762
+ - `<wcs-state>` 要素の内側に書いたマークアップはページの一部です。Light DOM コンポーネントの中身は、ページではなくそのコンポーネント自身のエンジンがバインドします。
763
+ - ページレベルでは、要素の子が要素自身のバインディングより先にバインドされます。そのため、カスタム要素が最初のプロパティ書き込みを受け取るのは子がバインドされた後で、`$errorCallback` も子の失敗から先に受け取ります。テンプレートの中では文書順です。
764
+ - `for:` の行と `if:` の枝は、`data-wcs` 属性を除いたテンプレートの複製です。そのため `[data-wcs…]` に対する CSS ルールやテストのセレクタはそれらの要素にマッチしません —— クラスや自前の `data-*` 属性を使ってください。
765
+ - `for:` / `if:` のテンプレートの中で、自分の要素を置き換えるバインディング(`outerHTML:`、`outerText:`)は `[wcs/template-syntax] #203` で初期化を失敗させます: 行は自分のノードを位置で保持しているためです。代わりにラッパー要素に `innerHTML:` をバインドしてください(`class.outerHTML:`、`attr.outerText:` は影響を受けません)。
766
+
767
+ 自分で書いていないマークアップをこの走査に届かせてはいけません —— [セキュリティ](#セキュリティ) を参照してください。
768
+
681
769
  ## 構造ディレクティブ
682
770
 
683
771
  構造ディレクティブは `<template>` 要素で使用します:
@@ -701,6 +789,17 @@ export default {
701
789
 
702
790
  例外は、**新しく生成されたオブジェクトとして届くデータ**です。`fetch(...).json()`、ストレージからの `JSON.parse`、WebSocket / SSE の全件スナップショット、Worker の `postMessage` などがこれにあたります。これらの行は参照で照合できないため、全行が破棄・再構築されます。次の [`$listKeys`](#listkeys--再取得された行の同一性) を参照してください。
703
791
 
792
+ **`for:` は出力フィルタを取りません。** `for: items|take(2)` は、フィルタが何であれ `[wcs/binding-syntax] #121` で初期化を失敗させます。`for: items` の行は `items.<index>` なので、フィルタ後の配列の行は別の要素を指してしまい、その行を通した書き込みが間違った要素に届きます。絞り込んだリストを返す getter を宣言し、それをループしてください:
793
+
794
+ ```html
795
+ <!-- get firstTwo() { return this.items.slice(0, 2); } がある場合 -->
796
+ <template data-wcs="for: firstTwo">
797
+ <li data-wcs="textContent: .name"></li>
798
+ </template>
799
+ ```
800
+
801
+ getter はコピーを返すので、元のパスを通した書き込み(`this["items.1.name"] = "z"`)はコピー側の行に届きません。行を通して書いてください(`firstTwo.1.name`、または行の中のバインディング)。1 つのオブジェクトが 2 つのリストから到達できる場合(`todos` を絞り込む `get shown()` を `for: shown` で描画する場合)も同じです: [同じオブジェクトが 2 つの位置にあるとき](#resolve--明示的なインデックスでのアクセス) を参照してください。
802
+
704
803
  #### `$listKeys` — 再取得された行の同一性
705
804
 
706
805
  行がバインディングの管理外の DOM 状態(フォーカス、IME 変換中の文字列、`<details>` の開閉、行内スクロール位置、`<canvas>` の描画内容、`<video>` の再生位置)を持つ場合、行の再構築でそれらは失われます。キーを宣言すると、リフレッシュをまたいで行を同定できます:
@@ -730,7 +829,9 @@ this.items = await (await fetch("/api/items")).json();
730
829
  - **無変化のリフレッシュはゼロコスト**です。何も変わっていなければフィールド書き込みも DOM 操作も一切発生しません。
731
830
  - **行は plain object** である必要があり、キーは存在かつ一意でなければなりません。キーの重複・欠落・クラスインスタンスは、静かに劣化させず即座にエラーになります。
732
831
  - **行から消えたフィールドは `null` でクリア**されます。`null` はこのパッケージにおける明示的なクリアの語彙です(`undefined` は「状態が値を持たない」を意味し、書き込み自体がスキップされます)。
832
+ - 名前が 1 つのパスセグメントにならないフィールド(`@odata.etag`・`2fa`・`a.b`)や、行 getter と同じ名前のフィールドは、据え置かれる行へそのままコピーされ、行は全体として更新されます。JSON から来た `__proto__` キーはコピーされません。
733
833
  - 格納される配列は照合済みの行オブジェクトから組み直されるため、代入後は `this.items !== 代入した配列` になります。
834
+ - `$listKeys` は `list-keys` アドオンです: 全部入りのエントリには含まれており、`/core` のページは `@wcstack/state/features/list-keys` を入れます。マウントされたコンポーネントの `$listKeys` はそのコンポーネント自身のリストにキーを付けます。ボリュームは宣言できません。
734
835
 
735
836
  #### ドット省略記法
736
837
 
@@ -781,6 +882,20 @@ this.items = await (await fetch("/api/items")).json();
781
882
 
782
883
  条件をチェーンできます。`elseif` は前の条件を自動的に反転します。
783
884
 
885
+ ### 挿入された内容の中の構造テンプレート
886
+
887
+ ページがバインドされた後に別のパッケージが挿入する内容 —— `@wcstack/router` が表示するルートの本体や、`<wcs-head>` の子 —— は、挿入する側がバインダプロトコル(`globalThis[Symbol.for("wcstack.binder")].bind(subtree, options?)`)を通して引き渡したときにバインドされます。そのような内容の**最上位**にある `for:` / `if:` テンプレートは自分の隣に描画されるため、自分のノードだけを動かす挿入側では行が置き去りになります。そこで、挿入側が範囲全体をまとめて出し入れすると宣言したときだけ描画されます —— `bind(subtree, { range: true })`。`@wcstack/router` 3.5 以降はルートの中身についてそうしています。この宣言がなければ描画されず、コンソールに `[wcs/template-syntax] #204` が報告されます。要素で包んだテンプレートは、どの場合でも描画されます:
888
+
889
+ ```html
890
+ <wcs-route path="/items">
891
+ <ul>
892
+ <template data-wcs="for: items"><li>{{ .name }}</li></template>
893
+ </ul>
894
+ </wcs-route>
895
+ ```
896
+
897
+ バインダはマークアップを理由に throw しません: 引き渡された部分木の中のエラーは `console.error` で報告され、それより前の部分はバインドされたまま残り、ナビゲーションは続きます。もう一度引き渡された部分木(router は挿入のたびに中身を引き渡します)がバインドされるのは 1 回だけです。引き渡されるのは要素だけなので、ルートの本体に要素の外で直接書いた `{{ }}` は、ナビゲーションでそのルートに入ったときにはバインドされません —— 要素で包んでください。これが問題になるのは、バインダを自分で呼ぶ場合か、`<wcs-route>` の直下に `for:` / `if:` を書く場合だけです。
898
+
784
899
  ## パス getter(算出プロパティ)
785
900
 
786
901
  **パス getter** は `@wcstack/state` の中核機能です。JavaScript の getter に**ドットパス文字列キー**とワイルドカード(`*`)を使って定義します。**データツリーの任意の深さに仮想プロパティを追加でき、すべてを1箇所にフラットに定義できます**。データのネストがどれほど深くても、定義側は同じレベルに並び、ループ要素ごとの自動依存追跡が機能します。
@@ -1050,13 +1165,15 @@ export default {
1050
1165
 
1051
1166
  ### 仕組み
1052
1167
 
1053
- 1. **コンテキスト解決** — `for:` ループのレンダリング時に、各イテレーションが `ListIndex` をアドレススタックにプッシュします。パス getter 内の `this["users.*.name"]` はこのスタックを使って `*` を解決するため、常に現在の要素を参照します。
1168
+ 1. **コンテキスト解決** — リストの各要素は、位置を持つ行です。行について評価されるパス getter の中(`for:` の行、その中に束縛したイベントハンドラ、`$getAll` の走査)では、`this["users.*.name"]` の各 `*` がその行とその祖先の行に解決されるため、常に現在の要素を参照します。`*` が解決されるのは*その*リストの行を通してだけです: 別のリストの行はそのコンテキストになりません(`wcs/wildcard-rank`)。
1169
+
1170
+ 2. **自動依存追跡** — getter が `this["users.*.name"]` にアクセスすると、`users.*.name` から getter のパスへの依存が登録されます。`users.*.name` が変更されると、getter のキャッシュが dirty になります。
1054
1171
 
1055
- 2. **自動依存追跡** — getter が `this["users.*.name"]` にアクセスすると、`users.*.name` から getter のパスへの動的依存が登録されます。`users.*.name` が変更されると、getter のキャッシュが dirty になります。
1172
+ 3. **キャッシュ** — getter の結果は行ごとにキャッシュされます。`users.*.fullName` のインデックス 0 とインデックス 1 は別々のキャッシュエントリを持ちます。依存先が変更された場合のみキャッシュが無効化されます。
1056
1173
 
1057
- 3. **キャッシュ** — getter の結果は具体的なアドレス(パス + ループインデックス)ごとにキャッシュされます。`users.*.fullName` のインデックス 0 とインデックス 1 は別々のキャッシュエントリを持ちます。依存先が変更された場合のみキャッシュが無効化されます。
1174
+ 4. **直接インデックスアクセス** — 行をインデックスで指すこともできます: `this["users.0.name"]` は、ループコンテキストなしで「いま 0 番目にある行」を指します。要素への書き込み(`this["users.0"] = { ...this["users.0"], name: "z" }`)はその位置の値を置き換えます: 行はその場にとどまって新しい値を受け取り、その下で派生するもの(`users.0.name`・`$getAll("users.*.name")`・行 getter)はすべて計算し直されます —— `for:` でリストを描いているかどうかに関わらず同じです。オブジェクトをその場で書き換えて `$postUpdate("users.0")` / `$postUpdate("users")` で知らせると、行は据え置いたまま、その下を読み直します。数値の添字を含むバインディングパスも同じ意味 ——「*いま 0 番目にある行*」—— を持ちます: `textContent: users.0.name`、`{{ users.1.name }}`、`{{ users.0.fullName }}` のような行 getter、そして添字を複数含むパス(`groups.0.items.1.v`)。添字のパスを通した書き込み、`for:` の行やその行に束縛した入力欄を通した書き込み、要素の差し替え、並べ替え、行の削除、リストの丸ごと置換に追従します。数値の添字を含む `$watch` のキー(`"users.0.name"`)は、その位置の値が変わったときだけ発火します。リストの末尾より先の添字は `undefined` と読め、そこへの書き込みは `no row for "users.*.name"`(`#3`)を throw して何も変えません。
1058
1175
 
1059
- 4. **直接インデックスアクセス** — 数値インデックスで特定の要素にアクセスすることもできます:`this["users.0.name"]` はループコンテキストなしで `users[0].name` に解決されます。要素のパスへ別のオブジェクトを代入する(`this["users.0"] = { ...this["users.0"], name: "z" }`)と、`for:` でリストを描いているかどうかに関わらず、その位置は新しい行になります。その下のパス(`users.0.name`・`$getAll("users.*.name")`・行 getter)は、差し替える前の行のキャッシュではなく新しいオブジェクトから読みます。その場で書き換えた同じオブジェクトを書き戻す(`const u = this["users.0"]; u.name = "z"; this["users.0"] = u;`)か、`$postUpdate("users.0")` / `$postUpdate("users")` で知らせると、行は据え置いたまま、その下のパスとそれを読む getter を読み直します — 行の中でそれらを描く `for:` が無くても同じです。この修正の前は、`for:` が描いていない限りその下のパスはキャッシュの値のままで、同じリストを `$getAll` で集める getter が先に評価されていると、`users.0.name` を読む getter が要素の差し替えを取りこぼすことがありました。数値の添字が 1 つのパスなら、バインディングにも同じ綴りを書けて、意味も同じ「*いま 0 番目にある行*」です:`textContent: users.0.name`、`{{ users.1.name }}`、行 getter なら `{{ users.0.fullName }}`。添字のパスへの書き込み(`this["users.0.name"] = …`)、その行を描く `for:` の行やその行に束縛した入力欄からの書き込み、要素の差し替え(`this["users.0"] = {…}`)、並べ替え・行の削除・リストの丸ごと置換に追従し、行 getter はその入力の変化に追従します — リストを `for:` で描いていてもいなくても同じです。このバインディングは state オブジェクトに足す列挙されないアクセサを通して getter と同じように読まれ、getter と同じく、リストのどの行への書き込みでも評価し直されます。そのため `$watch` のキー(`"users.0.name"` など)は値が変わらないまま発火することがあります。その位置に行が無い(空のリスト)ときは空で、配列でないオブジェクトの下のキー(`sales.2024.total`)は素のキーとして読みます。3.3.0 までは、この書き方のバインディングは最初の値のまま止まってリストの丸ごと置換にしか追従せず、行 getter をこの綴りで書くと空のまま `wcs/binding-path-missing` の警告が出ていました。数値の添字が 2 つ以上のパス(`groups.0.items.1.v`)と、拡張できない state オブジェクト(`Object.freeze`・`Object.seal`・`Object.preventExtensions`)の上の数値添字のバインディングは、いまもそのままです。
1176
+ 素のオブジェクトの下の数値キー(`sales.2024.total`、`usersById.42.name`)は、このバージョンの既知の制約です: マークアップでは描画されますが、スクリプトからの読み取り(`this["sales.2024.total"]`、getter の中でも同じ)は `undefined` を返し、そのようなパスへの `$eq` は常に false で、書き込みや双方向の書き戻しは `no row for "sales.*.total"` を throw します。読み取りは `this.sales[2024].total` と書き、書き込みはトップレベルのキーに新しいオブジェクトを代入する(`this.sales = { ...this.sales, 2024: { ...this.sales[2024], total: 10 } }`)か、数値でないキーを使ってください。
1060
1177
 
1061
1178
  ### getter は state に対して純粋であること
1062
1179
 
@@ -1097,7 +1214,7 @@ getter の例外は握り潰されません。評価された場所(バイン
1097
1214
  |---|---|
1098
1215
  | **追跡されるのは `this` を通した *パス* の読み取りだけ。** `this.form` は `form` を、`this["form.name"]` は `form.name` を追跡する。`this.form.name` が追跡するのは **`form` だけ** —— `.name` は返ってきたオブジェクトへの素のプロパティアクセスでしかない。`Date.now()`・DOM・モジュール変数・クロージャで掴んだオブジェクトは何も登録しない | その入力に対して getter は二度と再評価されず、最初の値が残り続ける(上の例)。`this.form.name` を読む getter は、`<input data-wcs="value: form.name">` を編集しても再実行されない —— `this["form.name"]` で読む |
1099
1216
  | **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
1100
- | **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`config.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
1217
+ | **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`$behavior.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
1101
1218
 
1102
1219
  規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。
1103
1220
 
@@ -1132,8 +1249,8 @@ export default {
1132
1249
 
1133
1250
  - **`path` への書き込み** — オブジェクトを含むどんな値でも(`$eq("selected", this["items.*"])` に対する `this.selected = row`): 選ばれていた行と、新たに選ばれる行。
1134
1251
  - **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
1135
- - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出し、この形に落ちた path には `tracked` バッジを付けます(3.0)。
1136
- - **数値の添字を含む `path`**(`$eq("items.0.v", …)`): 書き込みは添字をワイルドカードにした綴り(`items.*.v`・`items.*`)で知らせるので、綴りのままの購読には届きません。この形も依存を張る普通の読み取りに戻り、その行の `v` への書き込み・要素の差し替え・リストの差し替えに追従します(getter と同じく、別の行の `v` への書き込みでも再評価されます)。この修正の前は、同じ数値のパスのバインディングが無い限り最初の値のまま止まっていました。
1252
+ - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出します。この形に落ちた path は購読を持たないので、そこには載りません。
1253
+ - **リストの中の `path`** —— ワイルドカード(`$eq("items.*.v", …)`)か数値の添字(`$eq("items.0.v", …)`): `$eq` / `$eqPath` が鍵で購読できるのはリストの外のパスだけなので、この形も依存を張る普通の読み取りに戻り、その行の `v` への書き込み・要素の差し替え・リストの差し替えに追従します(getter と同じく、別の行の `v` への書き込みでも再評価されます)。
1137
1254
  - **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
1138
1255
  - **スコープの中では `path` は相対**です — パスを取る他の `$` API と同じです。マウントされたコンポーネント(`bind-component`)とボリューム(`<wcs-state mount="cart">`)では、`$eq("selectedId", …)`・`$eqPath` の 2 つのパス・`$eqIndex`・`$dependOn` はどれもそのスコープ(`cart.selectedId`)に解決され、ルートには向きません。`$untracked` はコールバックを取るので翻訳の対象外です。
1139
1256
 
@@ -1181,9 +1298,10 @@ export default {
1181
1298
  | `this.$setAll(path, indexes, value, options?)` | ワイルドカードパスにマッチする全アドレスへ一括書き込み |
1182
1299
  | `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
1183
1300
  | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
1184
- | `this.$dependOn(path)`(3.2。旧名 `$trackDependency`) | キャッシュ無効化のための依存関係を手動で登録 |
1185
- | `this.$untracked(fn)`(3.2。旧名 `$untrackDependency`) | fn 実行中の依存追跡を抑止して値を読む |
1301
+ | `this.$dependOn(path)` | キャッシュ無効化のための依存関係を手動で登録 |
1302
+ | `this.$untracked(fn)` | fn 実行中の依存追跡を抑止して値を読む |
1186
1303
  | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | 鍵付き購読: 「`path` の値はこの行の鍵か」([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
1304
+ | `this.$command.<name>` | `$commandTokens` で宣言した `CommandToken` へのアクセス([Command Token](#command-tokenメソッドバインディング) 参照) |
1187
1305
  | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
1188
1306
  | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
1189
1307
 
@@ -1224,7 +1342,7 @@ export default {
1224
1342
 
1225
1343
  #### `$setAll` — 配列要素を作り直さずに一括更新
1226
1344
 
1227
- `$setAll` は `$getAll` の書き側の対称形で、ワイルドカードパスにマッチする全アドレスへ書き込みます。狙いは記述の短さではなく、**配列そのものを保つ**ことです。`this.users = this.users.map(...)` のように作り直すと ListIndex・行 getter のキャッシュ・差分描画がまとめて捨てられますが、`$setAll` は行ごとの in-place な書き込みに分解するのでリストの同一性が保たれます。
1345
+ `$setAll` は `$getAll` の書き側の対称形で、ワイルドカードパスにマッチする全アドレスへ書き込みます。狙いは記述の短さではなく、**配列そのものを保つ**ことです。`this.users = this.users.map(...)` のように作り直すとリストの添字・行 getter のキャッシュ・差分描画がまとめて捨てられますが、`$setAll` は行ごとの in-place な書き込みに分解するのでリストの同一性が保たれます。
1228
1346
 
1229
1347
  ```javascript
1230
1348
  export default {
@@ -1266,12 +1384,21 @@ this.$setAll("users.*", [], rows, { spread: true }); // 配列を保ったま
1266
1384
 
1267
1385
  #### `$resolve` — 明示的なインデックスでのアクセス
1268
1386
 
1269
- `$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 のプロキシから書けていました):
1387
+ `$resolve` は特定のワイルドカードインデックスの値を読み書きします。**読みか書きかは引数の個数で決まります**: `$resolve(path, indexes)` は読み、`$resolve(path, indexes, value)` は `value` の書き込みで、`undefined` も書きます。readonly のプロキシでは、この書き込みは直接代入と同じく `This state is readonly.` で throw し、`$setAll` も同じです。リストの末尾より先の添字は `undefined` と読め、そこへの書き込みは `no row for "items.*"` を throw して何も変えません。
1388
+
1389
+ **要素への書き込みは、その位置の値を置き換えます。** その位置の行は `$1` ごとその場にとどまって新しい値を受け取ります: バインディングがその値を表示し、行の下で派生するものはすべて計算し直されます。バインディングが持たない DOM の状態 —— フォーカス、バインドしていない input に入力中のテキスト、`<details>` の開閉 —— は位置に残ります。そのため、行にバインドした input(`value: .`)は入力中もフォーカスを保ち、自分の行に書く行内の要素(`value: .`、`status: .` でバインドした wc-bindable の出力)の書き込みは、その行に届き続けます。行を**値ごと**動かすには、新しい配列を代入してください: 行は新しい配列の要素と同一性で照合されるので、オブジェクトと一緒に動きます。
1270
1390
 
1271
1391
  ```javascript
1272
1392
  export default {
1273
- items: ["A", "B", "C"],
1393
+ items: [{ name: "A" }, { name: "B" }, { name: "C" }],
1394
+ // ✅ 2 つの行が DOM ごと入れ替わる
1274
1395
  swapFirstTwo() {
1396
+ const items = this.items.slice();
1397
+ [items[0], items[1]] = [items[1], items[0]];
1398
+ this.items = items;
1399
+ },
1400
+ // 値が入れ替わり、行はその場にとどまる
1401
+ swapValues() {
1275
1402
  const a = this.$resolve("items.*", [0]);
1276
1403
  const b = this.$resolve("items.*", [1]);
1277
1404
  this.$resolve("items.*", [0], b);
@@ -1280,13 +1407,7 @@ export default {
1280
1407
  };
1281
1408
  ```
1282
1409
 
1283
- この書き方で行を入れ替えると、入れ替えが揃った時点で、描画済みの行が値と一緒に並べ替わります(その場で中身を書き換えるのではありません)。行の `$1` と、バインドの外にある行の状態(バインドしていない input に入力中のテキストなど)も値に付いてきます。リストに無かった値を書き込むと、その行はその場で置き換わります。ブロックは動かず、バインドが新しい値を映すので、行にバインドした input は入力中もフォーカスを保ちます。プリミティブのリストでは等しい値を区別できないため、書き込みの結果が同じ値の並べ替えになれば入れ替えとして扱います。
1284
-
1285
- 入れ替えが揃うのは、動かした値がどれも元の位置を離れたときです。それまで(2 回の書き込みの間、動かした値が両方の位置にある間)は、行はブロックを保ったまま位置どおりの値を映し、添字のパス(`this["items.0"]`)はその位置を読み書きします。2 回の書き込みは別々の更新(2 回のクリックなど)に分かれていても構いません。入れ替えを揃えた更新が `items.*` の `$watch` に知らせるのは、その更新で書いた位置だけです(`prev` もその位置の値です)。揃う前に配列を代入すると、前にそのパスが持っていた配列でも、代入した配列の値が描かれます。別々の行に同じ値を持つリストもほかと同じく入れ替わりますが、別の行が持ち続ける値を写す書き込みは、入れ替えの片側と区別できません。そのため、コードが同じ値を持ち続ける行を作ると、配列を置き換えるまで、その配列への添字の書き込みは位置どおりに描かれます。値と `$1` は正しく、ブロックが値に付いて動かなくなるだけです。
1286
-
1287
- 行の中の要素が自分の行へ書く値には、ここまでの話は当てはまりません(`value: .` でバインドした input、`status: .` でバインドした wc-bindable の出力)。その書き込みは行の値の更新で、入れ替えの一部には数えません。行はブロックと `$1` を保ち、いくつもの行が同じ値を返しても、ある行の要素が別の行の持つ値を書いても、その要素の以後の書き込みは自分の行に届きます。
1288
-
1289
- **同じオブジェクトが 2 つの位置にあるとき。** リストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"`)でオブジェクトは変わりますが、もう片方の行は古い値を表示し、読みも古い値を返します。行の下の値は行ごとにキャッシュされ、書き込みはそれを通した行にしか届かないためです。こうした書き込みの後に `$postUpdate("items")` を呼ぶと、`for:` の行・`$getAll("items.*.name")`・`items.1.name` のような添字のパスのすべてがオブジェクトを読み直します。
1410
+ **同じオブジェクトが 2 つの位置にあるとき。** リストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"`)でオブジェクトは変わりますが、もう片方の行のバインディングと行 getter は古い値を表示し続けます: 書き込みはそれを通した行にしか届かないためです。素の読み取り・ルートの getter・`$getAll` には新しい値が見えます。1 つのオブジェクトが 2 つのリストから到達できる場合も同じことが起きます。TodoMVC 風の絞り込み —— `todos` を絞り込んだコピーを返す `get shown()` を `for: shown` で描画し、チェックボックスがその行に書き込む場合です。こうした書き込みの後に `$postUpdate("items")` を呼ぶと、そのリストのすべての行がオブジェクトを表示し直します。これはこのバージョンの既知の制約です。
1290
1411
 
1291
1412
  ## 再帰パス(`$recursion`)
1292
1413
 
@@ -1325,7 +1446,7 @@ export default {
1325
1446
 
1326
1447
  この森の total は `131 / 110 / 100 / 20 / 2`、`treeTotal` は `133` になります。
1327
1448
 
1328
- **`**` はオーサリング層だけの記号で、エンジンには決して降りません。** 具体パス(`nodes.*.children.*.total`)を読んだ時点で、その深さの getter が遅延実体化されます(実際に触れた深さのぶんだけアクセサが生えます)。その先 —— `PathInfo`、依存グラフ、`$1`…`$n`、`$resolve`、リスト差分 —— が見るのは、いつもどおりワイルドカード本数が固定された普通のパスです。リアクティブの中核は新しい形を覚えていません。
1449
+ **`**` はオーサリング層だけの記号で、エンジンには決して降りません。** 具体パス(`nodes.*.children.*.total`)を読んだ時点で、その深さの getter が遅延実体化されます(実際に触れた深さのぶんだけアクセサが生えます)。その先 —— パスパターン、依存グラフ、`$1`…`$n`、`$resolve`、リスト差分 —— が見るのは、いつもどおりワイルドカード本数が固定された普通のパスです。リアクティブの中核は新しい形を覚えていません。
1329
1450
 
1330
1451
  ### 再帰点を宣言する
1331
1452
 
@@ -1337,7 +1458,7 @@ $recursion: { "data.tree.*": "kids.*" } // 深い位置のアンカーも可
1337
1458
  $recursion: { "nodes.*": "nodes.*" } // 自己相似な綴りも可
1338
1459
  ```
1339
1460
 
1340
- `**` に意味を与えるのはこの宣言だけです。`$recursion` の無い state では `**` はパスの文字ですらなく(`wcs/recursion-unsupported`)、この記法が子孫検索へ黙って滑り落ちることはありません。このバージョンが受け付けるのは **state ごとに単一の自己再帰アンカー**です。アンカーの途中のワイルドカード、2 つ目のエントリ、2 つのアンカー間の相互再帰、1 本のパスに 2 つ目の `**`、`get "nodes.**"`(これはノード自身であって、ノード配下の計算パスではありません)、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()` / `.children.*` / `.children.length` —— 全深さで実データの子リストを影にしてしまいます)、同じ具体パスへ展開される 2 本の `**` getter、そして再帰 **setter** は、宣言を読んだ時点で拒否します —— 別の意味に解釈することはありません。
1461
+ `**` に意味を与えるのはこの宣言だけです。`$recursion` の無い state では `**` はパスの文字ですらなく(`wcs/recursion-unsupported`)、この記法が子孫検索へ黙って滑り落ちることはありません。このバージョンが受け付けるのは **state ごとに単一の自己再帰アンカー**です。アンカーの途中のワイルドカード、2 つ目のエントリ、2 つのアンカー間の相互再帰、1 本のパスに 2 つ目の `**`、`get "nodes.**"`(これはノード自身であって、ノード配下の計算パスではありません)、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()` / `.children.*` / `.children.length` —— 全深さで実データの子リストを影にしてしまいます)、同じ具体パスへ展開される 2 本の `**` getter、そして再帰 **setter** は、宣言を読んだ時点で拒否します —— 別の意味に解釈することはありません。このうち 3 つの検査 —— 接尾辞が構造を名指す getter、同じ具体パスへ展開される 2 本の getter、展開形と同名の具体 getter —— は `diagnostics` アドオン(全部入りのエントリに含まれる)が行います。それを入れていない `/core` のページでは行われません。
1341
1462
 
1342
1463
  宣言が定義する族は無限ですが、state に生えるのは実際に要求された深さだけです:
1343
1464
 
@@ -1361,7 +1482,7 @@ k=2 nodes.*.children.*.children.*
1361
1482
  | `$setAll(path, [], value)` | 全深さへのブロードキャスト(合併と同じ走査・同じ順序) |
1362
1483
  | `$resolve` / `$postUpdate` / `$dependOn` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
1363
1484
 
1364
- 束縛形は束縛先の深さを必要とするので、**再帰 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 でもメソッドでも読めます。
1485
+ 束縛形は束縛先の深さを必要とするので、**再帰 getter の中**でしか解決できません(アンカー配下の普通の行 getter や、その行に紐づくイベントハンドラも同じく、深さを読み取れる実体の行について実行されるので使えます)。トップレベルで `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 でもメソッドでも読めます。
1365
1486
 
1366
1487
  ```javascript
1367
1488
  this.$getAll("nodes.**.value", []); // [1, 10, 100, 20, 2] —— 深さ優先・行きがけ
@@ -1425,11 +1546,13 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
1425
1546
  | 形 | 拒否する理由 |
1426
1547
  |---|---|
1427
1548
  | 非空の接頭辞 | 接頭辞ではどの深さに適用されるのか言えない(`wcs/recursion-setall-form`) |
1428
- | 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す |
1429
- | mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる |
1430
- | `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない |
1549
+ | 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す(`indexes` の無い `$setAll` と同じく `#9`) |
1550
+ | mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる(`wcs/recursion-setall-form`) |
1551
+ | `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない(`wcs/recursion-setall-form`) |
1431
1552
  | `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれるので `nodes.**.children.0` は子ノード | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
1432
- | `nodes.**.total`、およびその値の内側を指すパス —— 添字綴りも同じ形に畳まれるので `nodes.**.children.0.total` も getter | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
1553
+ | `nodes.**.total`、およびその値の内側を指すパス —— 子ノードのパスも同じ形に畳まれるので `nodes.**.children.0.total` と `nodes.**.children.*.total` も getter。子ノードの下に宣言した getter(`get "nodes.**.children.*.x"`)があれば `nodes.**.x` も拒否する | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
1554
+
1555
+ それ以外は葉です。子ノードの下の葉もそうで、`$setAll("nodes.**.children.*.value", [], v)` は各ノードの子の `value`(根以外のすべてのノード)に書き込みます。
1433
1556
 
1434
1557
  読み取り専用の規則は `**` の綴りに依存しません。再帰 getter の具体的な展開形 —— `nodes.*.total` / `nodes.*.children.*.total` / … —— への書き込みも、固定本数の `$setAll` でも値付きの `$resolve(path, indexes, value)` でも直接代入でも、またその深さが実体化済みかどうかに関わらず、書き込みの入口で拒否します。この検査が無かったときは、未実体化の展開形が「無いキー」に見えてノードのオブジェクトに書き込まれ、代入値が getter のキャッシュ結果として固定されていました。
1435
1558
 
@@ -1437,9 +1560,9 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
1437
1560
 
1438
1561
  走査は深さ方向に降りながら、必要な形をその場で検査します。**同じ配列インスタンス**に 2 度到達したら拒否します。その配列が現在のノードの祖先のものなら循環(`wcs/recursion-cycle`)、そうでなければ 2 つのノードが 1 本の子リストを共有しています(`wcs/recursion-shared-list`)。各ノードに自分の `children` 配列を持たせてください —— **空**配列の使い回しは行を持たず別名化のしようがないので、追跡もせず正当です。
1439
1562
 
1440
- 行オブジェクトを作り直して `children` 配列を引き継ぐ置換 —— `this.nodes = this.nodes.map(n => ({ ...n }))` —— はふつうの更新で、集計もそのまま追従します。子リストの行オブジェクトはそのまま生き残り(行の identity で持っているもの —— `bind-component` の子スコープが描画した行や、そこにバインドしていない状態 —— は保たれます)、行がぶら下がっていた**退役した親**だけが生きている行に差し替わるので、次の葉の更新は画面に出ている行を dirty にします([#256](https://github.com/wcstack/wcstack/issues/256))。2 つの行が 1 本の `children` 配列を**共有**する形はこれとは別です。2 行とも配列に居る間は従来どおりで、1 本の配列には 1 組の行しかないので 2 行は必ず同じ値で一致し、親を読む行 getter(`this["nodes.*.value"]`)はその行集合の**持ち主**(最初にその配列を展開した行)の文脈で評価されます。変わったのは**持ち主をリストから外したとき**で、行集合はリストに残っている行のどれか 1 行へ移ります(`if:` で隠れている行のこともあります) —— その行の集計が、外した行の数字で凍る代わりに共有データを追従します(行集合は 1 組しかないので、3 行で共有していれば残った 1 行だけが追従し、他は凍ったままです)。3.4.0 からは、持ち主がリストに残ったまま配列を手放したとき(`this["nodes.0.children"] = [ … ]`)も同じで、行集合は同じキーでその配列を持つ別の行へ移り、その行の集計が共有データを追従します(3.3.0 では凍ったままでした)。外した行**そのもののオブジェクト**が戻ってくれば持ち主も戻ります —— 同じ配列インスタンスでも、同じ行を並べた新しい配列でも、違う位置に戻しても返ります。一方、**全ての行を作り直す**綴り(`this.nodes = this.nodes.map(n => ({ ...n }))` —— この項の冒頭の更新)ではどの行も一致しないので、行集合は**持ち主が居た位置を占める行**に付きます。2.3.0 でこの通りに戻るのは同じ配列インスタンスの綴りだけで、新しい配列で戻すと 2.3.0 では両方の行が凍ります。子の getter が上を読むなら、ノードごとに自分の配列を持たせてください —— 持ち主が配列を手放した後、そうした getter は、読む値が変わるまで前の持ち主の文脈で求めた値を返し続けることがあります。
1563
+ 行オブジェクトを作り直して `children` 配列を引き継ぐ置換 —— `this.nodes = this.nodes.map(n => ({ ...n }))` —— はふつうの更新で、集計もそのまま追従します。
1441
1564
 
1442
- 共有の形のうち次の 3 つは既知の制約です。別の外側の行が同じ配列を**別のキー**に持つと(ある行は `items` に、別の行は `alt` に)、片方のキーを通した書き込みが別の要素に着地することがあります([#396](https://github.com/wcstack/wcstack/issues/396))。同じ配列を 2 つの `<wcs-state>`(ルートとそのボリュームを含む)で共有しないでください —— 片方で写しや作り直しをした後、もう片方を通した書き込みが別の配列に着地したり、投げて失われたりします([#397](https://github.com/wcstack/wcstack/issues/397))。また、共有した配列の写し・移動・作り直しを 1 回の更新に重ねると、state は正しいのに一部の `for:` の描画が古いまま残ることがあります([#398](https://github.com/wcstack/wcstack/issues/398))。
1565
+ 再帰の走査の外では、行が 1 本の配列を共有してもかまいません(走査は上のとおりそれを拒否します)。同じ配列インスタンスを持つリストはどれも、そのうちのどれを通した書き込みも表示します —— `groups.0.items.1` の下への書き込みは、`groups.2.items.1` も同じ配列を(同じキーでも別のキーでも)持っていれば、そこにも届きます。1 つのオブジェクトに*別々の*配列を通して到達する場合(共有配列のコピー、絞り込んだコピーを返す getter)は、[同じオブジェクトが 2 つの位置にあるとき](#resolve--明示的なインデックスでのアクセス) で説明した既知の制約です。
1443
1566
 
1444
1567
  上限は展開後のパスの**ワイルドカード 128 段**です。上の集計 getter は評価中のノードより 1 段下を読むので、127 段の鎖までは畳めて、128 段で `wcs/recursion-depth-exceeded` になります(アンカー・到達した深さ・組み立てようとしたパス・上限を名指しします)。この検査は getter 評価スタック自身の 128 段の上限(`wcs/getter-depth-exceeded`)より先に効くので、深い木は「深い」と報告され、循環の疑いを掛けられることはありません。途中で打ち切ることもしません —— 部分的な集計は、誤った値を正しい値として返すことだからです。
1445
1568
 
@@ -1521,11 +1644,36 @@ export default {
1521
1644
  </template>
1522
1645
  ```
1523
1646
 
1647
+ ハンドラの中の `this` は書き込み可能な state です。ドットを含む名前はパスです: `onclick: cart.checkout` は `cart` にマウントされたボリュームのメソッドを呼びます。throw したハンドラや、reject する Promise を返したハンドラは `console.error` で報告され、イベントには影響しません。
1648
+
1649
+ ### 委譲されるイベント
1650
+
1651
+ `click`・`dblclick`・`input`・`change`・`submit`・`keydown`・`keyup`・`mousedown`・`mouseup`・`pointerdown`・`pointerup` の `on*:` バインディングは**委譲**されます: イベント種別ごとに 1 つのリスナーが、state がバインドするルート —— document、shadow root、あるいは Light DOM のマウントされたコンポーネントの中ではホスト要素 —— に置かれ、イベントが通過した要素のハンドラを内側から順に実行します。そのため:
1652
+
1653
+ - ハンドラの中の `event.currentTarget` は要素ではなく、そのルートです。要素は `event.target` から探すか(`event.target.closest("li")`)、ハンドラが受け取るループインデックスを使ってください(`removeItem(event, index)`)。
1654
+ - 内側の `on*:` バインディングの `#stop` は、それより外側の `on*:` ハンドラを止めます。自分のコードが祖先に付けたリスナー(クリックできるカードへの `addEventListener`)は止められません: そのリスナーは、イベントがルートに届く前に実行済みです。
1655
+ - 自分のコードが祖先で `stopPropagation()` を呼ぶと(モーダルの中身がオーバーレイにクリックを渡さないようにする場合など)、その内側の委譲されたハンドラはまったく実行されません。
1656
+ - 別のルートの下へ移した要素(shadow root から `document.body` へ移したダイアログ)では、委譲されたハンドラはもう実行されません。
1657
+ - 1 つの要素に同じ種別のハンドラが 2 つあれば(`onclick: a; onclick: b`)、どちらも実行されます。DOM と同じく、1 つ目の `#stop` は 2 つ目を止めません。
1658
+
1659
+ それ以外のイベント種別と、カスタム要素が `bubbles: false` で dispatch するイベントは、要素自身で受け取ります。双方向バインディング(`value:`・`checked:`・`radio:`・`checkbox:`)と `eventToken.` バインディングは、リスナーを要素に置いたままです。
1660
+
1661
+ ### `#direct`
1662
+
1663
+ `on*#direct:` は、リスナーを委譲せずに要素自身に付けます: `currentTarget` は要素で、`#stop` は祖先に自分で付けたリスナーを止め、祖先の `stopPropagation()` に実行を妨げられず、要素が別のルートの下へ移った後も実行されます。`#prevent`・`#stop` と組み合わせられ、`for:` / `if:` の中では行ごとに付けられ、行とともに外されます。
1664
+
1665
+ ```html
1666
+ <!-- 自分のコードがクリックリスナーを付けたカードの中のボタン -->
1667
+ <button data-wcs="onclick#direct,stop: save">保存</button>
1668
+ ```
1669
+
1670
+ ハンドラは DOM が置く場所で実行されます。外側の `#direct` ハンドラは、その内側の委譲されたハンドラより**先に**実行されます。委譲されたハンドラは、イベントが外側の要素を通過した後にルートで実行されるからです。そのため、内側の `#stop` が外側の `#direct` ハンドラを止められるのは、内側のバインディングも `#direct` の場合(`onclick#direct,stop:`)だけです。イベントバインディングでないバインディングの `#direct` は何もしません(`@wcstack/lint` が警告します)。
1671
+
1524
1672
  ## フィルタ
1525
1673
 
1526
- 48 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
1674
+ 47 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます: core に 24 個のロジックフィルタ(比較・算術・型変換・真偽値 / デフォルト)、`formats` アドオンに 23 個の書式フィルタ(数値フォーマット・文字列・日付 / 時刻)があり、全部入りのエントリには両方が含まれます。
1527
1675
 
1528
- **3.2 で一部の名前を改めました**(要件 B12)。表は正式名で、括弧の中が旧名です。旧名は 3.x の間は同じに動き、4.0 で外れます。lint と VS Code 拡張は、旧名に正式名を提案する info(`wcs/name-alias`)を出します。3.1 以前を使うページでは旧名で書いてください。
1676
+ 3.x が 3.2 の改名の別名として残していた名前(`inc`・`dec`・`fix`・`uc`・`lc`・`cap`・`rep`・`rev`・`pad`・`null`)と `substr` フィルタは存在しません: 未知の名前と同じく失敗し、メッセージが代わりに書くものを示します(`"uc" was renamed "upper" in 3.2 and removed in 4.0 — write "upper".`。`substr(start, length)` は `slice(start, start + length)`)。
1529
1677
 
1530
1678
  ### 比較
1531
1679
 
@@ -1533,7 +1681,7 @@ export default {
1533
1681
  |---|---|---|
1534
1682
  | `eq(value)` | 等しい | `count\|eq(0)` → `true/false` |
1535
1683
  | `ne(value)` | 等しくない | `count\|ne(0)` |
1536
- | `not` | 真偽性を反転 — `0` / `""` / `null` / `undefined` はすべて `true`(3.x。以前は非 boolean で throw し、`else:` がどちらの枝も描かなかった) | `isActive\|not` |
1684
+ | `not` | 真偽性を反転 — `0` / `""` / `null` / `undefined` はすべて `true` | `isActive\|not` |
1537
1685
  | `lt(n)` | より小さい | `count\|lt(10)` |
1538
1686
  | `le(n)` | 以下 | `count\|le(10)` |
1539
1687
  | `gt(n)` | より大きい | `count\|gt(0)` |
@@ -1543,8 +1691,8 @@ export default {
1543
1691
 
1544
1692
  | フィルタ | 説明 | 例 |
1545
1693
  |---|---|---|
1546
- | `add(n)`(`inc`) | 加算 | `count\|add(1)` |
1547
- | `sub(n)`(`dec`) | 減算 | `count\|sub(1)` |
1694
+ | `add(n)` | 加算 | `count\|add(1)` |
1695
+ | `sub(n)` | 減算 | `count\|sub(1)` |
1548
1696
  | `mul(n)` | 乗算 | `price\|mul(1.1)` |
1549
1697
  | `div(n)` | 除算 | `total\|div(100)` |
1550
1698
  | `mod(n)` | 剰余 | `index\|mod(2)` |
@@ -1555,7 +1703,7 @@ export default {
1555
1703
 
1556
1704
  | フィルタ | 説明 | 例 |
1557
1705
  |---|---|---|
1558
- | `toFixed(n)`(`fix`) | 固定小数点桁数 | `price\|toFixed(2)` → `"100.00"` |
1706
+ | `toFixed(n?)` | 固定小数点桁数(既定 0) | `price\|toFixed(2)` → `"100.00"` |
1559
1707
  | `round(n?)` | 四捨五入 | `value\|round(2)` |
1560
1708
  | `floor(n?)` | 切り捨て | `value\|floor` |
1561
1709
  | `ceil(n?)` | 切り上げ | `value\|ceil` |
@@ -1567,17 +1715,16 @@ export default {
1567
1715
 
1568
1716
  | フィルタ | 説明 | 例 |
1569
1717
  |---|---|---|
1570
- | `upper`(`uc`) | 大文字変換 | `name\|upper` |
1571
- | `lower`(`lc`) | 小文字変換 | `name\|lower` |
1572
- | `capitalize`(`cap`) | 先頭大文字 | `name\|capitalize` |
1718
+ | `upper` | 大文字変換 | `name\|upper` |
1719
+ | `lower` | 小文字変換 | `name\|lower` |
1720
+ | `capitalize` | 先頭大文字 | `name\|capitalize` |
1573
1721
  | `trim` | 空白除去 | `text\|trim` |
1574
- | `slice(n)` | 文字列スライス | `text\|slice(5)` |
1575
- | `substr(start, length)` | 部分文字列(引数は 2 つとも必須)。3.5 で警告が出て 4.0 で外れる: `slice(start, start + length)` と書く — [4.0 への準備](#40-への準備wcsv4-migration) | `text\|substr(0,10)` |
1576
- | `padStart(n, char?)`(`pad`) | 先頭を埋める(既定 `0`) | `id\|padStart(5,0)` → `"00001"` |
1577
- | `padEnd(n, char?)` | 末尾を埋める(既定は空白。3.2) | `code\|padEnd(8)` |
1578
- | `repeat(n)`(`rep`) | 繰り返し | `text\|repeat(3)` |
1579
- | `reverse`(`rev`) | 反転 | `text\|reverse` |
1580
- | `truncate(n, suffix?)` | 切り詰めて省略記号を付加(既定は `…` — U+2026 の 1 文字) | `title\|truncate(20)` → `"…"` 付き |
1722
+ | `slice(start, end?)` | 文字列スライス(`end` は長さではなく位置) | `text\|slice(5)` / `code\|slice(2,5)` |
1723
+ | `padStart(n, char?)` | 先頭を埋める(既定 `0`) | `id\|padStart(5,0)` → `"00001"` |
1724
+ | `padEnd(n, char?)` | 末尾を埋める(既定は空白) | `code\|padEnd(8)` |
1725
+ | `repeat(n)` | 繰り返し | `text\|repeat(3)` |
1726
+ | `reverse` | 反転 | `text\|reverse` |
1727
+ | `truncate(n, suffix?)` | 切り詰めて省略記号を付加(既定は `…` — U+2026 の 1 文字) | `title\|truncate(20)` |
1581
1728
  | `join(sep?)` | 配列を連結(既定 `", "`) | `tags\|join` / `tags\|join(/)` |
1582
1729
 
1583
1730
  ### 型変換
@@ -1589,7 +1736,7 @@ export default {
1589
1736
  | `boolean` | 真偽値に変換 | `value\|boolean` |
1590
1737
  | `number` | 数値に変換 | `value\|number` |
1591
1738
  | `string` | 文字列に変換 | `value\|string` |
1592
- | `nullIfEmpty`(`null`) | 空文字を `null` にする(それ以外はそのまま) | `value\|nullIfEmpty` |
1739
+ | `nullIfEmpty` | 空文字を `null` にする(それ以外はそのまま) | `value\|nullIfEmpty` |
1593
1740
 
1594
1741
  ### 日付 / 時刻
1595
1742
 
@@ -1601,16 +1748,16 @@ export default {
1601
1748
  | `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
1602
1749
  | `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
1603
1750
 
1604
- **5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定は `config.locale` で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。
1751
+ **5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定はページのロケール([ロケール](#ロケール))で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。
1605
1752
 
1606
1753
  ### 真偽値 / デフォルト
1607
1754
 
1608
1755
  | フィルタ | 説明 | 例 |
1609
1756
  |---|---|---|
1610
- | `truthy` | truthy チェック — JavaScript の真偽判定そのもので `boolean` と同じ(3.0 から `0n` は偽) | `value\|truthy` |
1757
+ | `truthy` | truthy チェック — JavaScript の真偽判定そのもので `boolean` と同じ(そのため `0n` は偽) | `value\|truthy` |
1611
1758
  | `falsy` | falsy チェック(JavaScript の真偽判定。`defaults` も同じ判定を使う) | `value\|falsy` |
1612
1759
  | `defaults(v)` | 偽値(`0`・`false`・`""` を含む)のときのフォールバック値 | `name\|defaults(Anonymous)` |
1613
- | `coalesce(v)` | `null` / `undefined` のときだけのフォールバック値(`0`・`false`・`""` は残す。3.2) | `count\|coalesce(0)` |
1760
+ | `coalesce(v)` | `null` / `undefined` のときだけのフォールバック値(`0`・`false`・`""` は残す) | `count\|coalesce(0)` |
1614
1761
 
1615
1762
  ### フィルタチェーン
1616
1763
 
@@ -1620,9 +1767,9 @@ export default {
1620
1767
  <div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
1621
1768
  ```
1622
1769
 
1623
- フィルタは束縛計画の段で解決されます。未知の名前は `[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)` を取り違えません。
1770
+ フィルタは束縛計画の段で解決されます。未知の名前は `[wcs/filter-unknown]`(`#501`。`diagnostics` アドオンがあれば did-you-mean 付き)で、受け付ける個数の外の引数は `[wcs/filter-arity]`(`join(a,b)`: "accepts at most 1 argument(s) (2 given)")で落ちます — lint と同じコード・同じ範囲です。どちらも `<wcs-state>` の初期化を止めます([初期化の失敗](#初期化の失敗))。引数は構造のままキャッシュされるので、`join('a,b')` と `join(a)` を取り違えません。
1624
1771
 
1625
- **引数のリテラルは型を持ちます(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`。
1772
+ **引数のリテラルは型を持ちます。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列は値で比べます — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` も `eq(1)` に一致します。**数値**の値と型付きの `true` / `false` / `null` は単に「一致しない」です: `selectedId|eq(null)` は id が `null` の間は `true`、数値になれば `false` になります。型の食い違いとして報告されるのは、引用符の無い非数値(数値の値に対する `eq(abc)`)だけです — `eq` は値の型を選ばないので、`status|eq(active)` は値が分かるまで判定できません。`defaults(v)` は型付きの値を返します: `defaults(0)` は `0`、`defaults('0')` は `"0"`、`defaults(null)` は `null`。
1626
1773
 
1627
1774
  **引用符の規則。** 引用符(`'` / `"`)は「ここはリテラル」という宣言で、引数の文法は意図的に小さく保たれています:
1628
1775
 
@@ -1640,10 +1787,10 @@ export default {
1640
1787
 
1641
1788
  1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
1642
1789
  2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
1643
- 3. 結合点は**パス名のみ**であるため、親と子は疎結合に保たれます。Shadow DOM のコンポーネントは単体でも動作しますが([独立した Web Component への状態注入](#独立した-web-component-への状態注入e2esingle-component))、Light DOM はホストからの配線が必須です。
1644
- 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)、依存グラフを通じた変更の伝播、そして描画する行ごとに構築されるバインディング台帳です。
1790
+ 3. 結合点は**パス名のみ**であるため、親と子は疎結合に保たれます。Shadow DOM のコンポーネントは単体でも動作しますが([独立した Web Component への状態注入](#独立した-web-component-への状態注入__e2e__single-component))、Light DOM はホストからの配線が必須です。
1791
+ 4. 実行コストは、パスの解決と、依存グラフを通じた変更の伝播です。
1645
1792
 
1646
- これは、コンポーネントレベルの抽象化ではなく「パスの解決」に基づくコンポーネント間の状態管理です。描画が最も安い方式という意味ではありません。[パフォーマンス](#パフォーマンス)のとおり生成・追加は [`@wcstack/signals`](../signals/) の 2.5〜3.5 倍で、それが行ごとの台帳の代価です。得られるのは、配線が宣言的で検査可能であることです。
1793
+ これは、コンポーネントレベルの抽象化ではなく「パスの解決」に基づくコンポーネント間の状態管理です。得られるのは、配線が宣言的で検査可能であることで、そのコストは[パフォーマンス](#パフォーマンス)で計測しています。
1647
1794
 
1648
1795
  ### コンポーネント定義(Shadow DOM)
1649
1796
 
@@ -1686,9 +1833,11 @@ customElements.define("my-light-component", MyLightComponent);
1686
1833
  - `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
1687
1834
  - **ホストからの配線が必須**です(`<my-light-component data-wcs="state.message: user.name">` または `state: user`)。配線の無い plain な Light DOM `bind-component` は v2 では成立しません(親と root を共有したまま独立ツリーは持てない)—— 誘導文付きで loud に失敗します: shadow を付けるか、ホストから配線してマウントにしてください。
1688
1835
 
1689
- > **注意**: `State.getBindingsReady(root)` はマウント記録の確定後、マウントスコープも待ちます。
1690
- > コンポーネント内部の描画完了まで待ちたい場合は、コンポーネント側の `<wcs-state>` の初期化を
1691
- > 待ってください。
1836
+ > **注意**: Shadow DOM コンポーネントの `getBindingsReady(shadowRoot)` は、その `<wcs-state bind-component>` を
1837
+ > コンポーネントのバインディングが構築されるまで待ち、マウントに失敗すれば reject します。Light DOM
1838
+ > コンポーネントはページのルートの中身をバインドしますが、`getBindingsReady(document)` はそれを待ちません:
1839
+ > コンポーネント自身の `<wcs-state>` を待ってください(`initializePromise`、`$connectedCallback` まで含めるなら
1840
+ > `connectedCallbackPromise`)。
1692
1841
 
1693
1842
  ### ホスト側の使用方法
1694
1843
 
@@ -1750,23 +1899,23 @@ customElements.define("user-card", UserCard);
1750
1899
  - 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段です
1751
1900
 
1752
1901
  > プロパティ単位の形(`state.message: user.name`)はそのまま動きます — 同じ機構の上の部分マウントです。
1753
- > **明示した部分マウントはコンポーネント自前のキーに勝ちます(3.0):** マップされるキーに既定値を
1902
+ > **明示した部分マウントはコンポーネント自前のキーに勝ちます:** マップされるキーに既定値を
1754
1903
  > 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)もホストの値を読み、
1755
- > 既定値は使われません(2.x では R1 によって自前のキーが私有になり、ホストの値を隠していました —
1756
- > `wcs/mount-own-key-shadow` の警告付き)。マップしていない自前のキーは今も R1 で私有です。
1904
+ > 既定値は使われません。マップしていない自前のキーは今も R1 で私有です。
1757
1905
  > これは**1 段のエントリ**の話です: より深いエントリ(`state.a.b: outer.b`)は `a` の私有性に
1758
1906
  > 触らないので、コンポーネントが `a` を自分で宣言していれば `a` とその配下は私有のままで、
1759
1907
  > エントリは届きません。名前がコンポーネントの getter / setter / メソッドと一致するエントリも同じで、
1760
1908
  > コンポーネント自身の面が勝ちます。いずれの形も `wcs/mount-own-key-shadow` で 1 回報告されます —
1761
1909
  > どちらかの名前を変えてください。
1910
+ > コンポーネント側のパスに `*` を含む対応付け(`state.list.*: items`)は throw します — リストそのものを
1911
+ > 対応付けてください(`state.list: items`)。同じキーを 2 回対応付けるのも throw します。
1762
1912
  >
1763
- > **マウントの `#ro` を尊重します(3.0):** `state#ro: user` や `state.title#ro: doc.title` では、
1913
+ > **マウントの `#ro` を尊重します:** `state#ro: user` や `state.title#ro: doc.title` では、
1764
1914
  > コンポーネントはそのエントリを読めても書けません — `element.state.title = …`・メソッド内の
1765
1915
  > `this.title = …`・`$setAll` / `$resolve` の書き込みは `[wcs/mount-readonly]` で落ち、コンポーネント内の
1766
- > 双方向束縛(`value: title`)は書き戻しません。ホストはそのパスに書けます(2.x は修飾子を受け付けて
1767
- > 無視していました)。Light DOM のマウントに `name` は不要で、
1768
- > `element.state`(および getter / メソッド内の `this`)の `$getAll` / `$setAll` / `$resolve` /
1769
- > `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
1916
+ > 双方向束縛(`value: title`)は書き戻しません。ホストはそのパスに書けます。Light DOM のマウントに
1917
+ > `name` は不要で、`element.state`(および getter / メソッド内の `this`)の `$getAll` / `$setAll` /
1918
+ > `$resolve` / `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
1770
1919
  > 添字は自動で前置されます。
1771
1920
 
1772
1921
  #### 公開 getter(コンポーネントの getter を外から読む)
@@ -1784,7 +1933,7 @@ customElements.define("user-card", UserCard);
1784
1933
  - 未存在パスの警告は `getBindingsReady` とは独立に 1 マクロタスク(`setTimeout(0)`)だけ遅延します。autoloader や遅延したカスタム要素定義を使う場合、最終的にバインドが解決しても、登録前の初回警告が出ることがあります
1785
1934
  - ツリーに同名のキーがある場合(継承したプロパティも含む)はツリーが勝ち、ランタイムが 1 回だけ warn します(`wcs/mount-export-shadowed`)。同一インスタンスに同名キーを公開するコンポーネントが 2 つある形は設定ミスで、候補走査時に検出して throw します(`wcs/mount-export-ambiguous`)。検証付きキャッシュに命中すると他の候補は再走査しないため、初回解決後に追加した競合コンポーネントは検出されない場合があります
1786
1935
  - 公開キーへの外からの書き込みは accessor の setter を実行し、getter しか無ければ throw します(getter を隠すキーをツリーに作ることはありません)。`in` は公開キーを見ません
1787
- - **自己再帰コンポーネント**(深さが有界でない木)が書けるようになります: 自分自身を `<template data-wcs="for: children"><tree-node data-wcs="state: ."></tree-node></template>` で入れ子にするコンポーネントは、`get total() { return this.value + this.$getAll("children.*.total").reduce((a, b) => a + (b ?? 0), 0); }` と各段 1 段の式で書け、再帰は台帳が解きます。パス自体は再帰を表現できない(ワイルドカードの個数が固定)ので、再帰は DOM に置き、パスはその展開形になります。設計: [docs/state-overlay-export-design.md](../../docs/state-overlay-export-design.md)
1936
+ - **自己再帰コンポーネント**(深さが有界でない木)が書けるようになります: 自分自身を `<template data-wcs="for: children"><tree-node data-wcs="state: ."></tree-node></template>` で入れ子にするコンポーネントは、`get total() { return this.value + this.$getAll("children.*.total").reduce((a, b) => a + (b ?? 0), 0); }` と各段 1 段の式で書け、再帰は依存グラフが解きます。パス自体は再帰を表現できない(ワイルドカードの個数が固定)ので、再帰は DOM に置き、パスはその展開形になります。設計: [docs/state-overlay-export-design.md](../../docs/state-overlay-export-design.md)
1788
1937
 
1789
1938
  ### 独立した Web Component への状態注入(`__e2e__/single-component`)
1790
1939
 
@@ -1825,7 +1974,11 @@ customElements.define("my-component", MyComponent);
1825
1974
  - `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
1826
1975
  - 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
1827
1976
  - Light DOM コンポーネントはホストからの配線が必須(plain 形は v2 で廃止)
1828
- - **マウントされた**スコープは宣言面を実行しない。`$watch`・`$stream`・`$scan` は一度だけ警告を出して無視され、`$recursion` / `**` getter は拒否される。ルート state に宣言すること(ボリューム `<wcs-state mount>` は `$watch` を持てるが、`$scan` と `$recursion` はルート専用)。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
1977
+ - コンポーネントの state はホスト要素のプロパティである: `state`・`src`・`json`・内包スクリプトを持つ `<wcs-state bind-component>` は拒否され、1 つのコンポーネントの中の 2 つ目の接続済み `<wcs-state bind-component>` も拒否される(`<tag> already has a connected <wcs-state bind-component="state">.`)
1978
+ - **マウントされた**スコープは、自身のバインディングとリストに対して `$listKeys`・`$commandTokens` / `$eventTokens` / `$on`・`$errorCallback` を実行し、自身の `$behavior` / `$features` も実行する。`$watch`・`$stream`・`$renderedCallback` は実行せず(`wcs/mount-dollar-declaration` を 1 回警告)、`$recursion` / `**` getter は `[wcs/mount-dollar-declaration]` を throw する。これらはルート state に宣言すること。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
1979
+ - コンポーネントが state を読み込んだ後に追加されたホストの配線(後からバインドした `state.a: x`)はコンポーネントに届かず、throw する —— ホストの配線は、コンポーネントが読み込む前にバインドすること
1980
+ - マウントに失敗したコンポーネントは `connectedCallbackPromise` を reject する —— 初期化に失敗したルートに配線されたコンポーネントも含む(`<tag>.state will not mount: the root state failed to initialize.`)—— [初期化の失敗](#初期化の失敗) 参照
1981
+ - 古い `<wcs-state bind-component>` がバインド・描画したノードが残っている間に、コンポーネントがそれを新しい `<wcs-state bind-component>` に置き換えると、新しい要素がスコープを引き継ぐ: それらのバインディング・行・`{{ }}` のテキストはそのまま残り、その横に追加されたバインディング付きのノードはバインドされない(`console.warn` がそう伝える)。その中身を新しい要素で描画し直すと、新しい要素が改めてバインドする
1829
1982
 
1830
1983
  ### ループ内でのコンポーネント使用
1831
1984
 
@@ -1840,7 +1993,7 @@ customElements.define("my-component", MyComponent);
1840
1993
  </template>
1841
1994
  ```
1842
1995
 
1843
- 行の中のコンポーネントは自分の行を読み書きします。`$connectedCallback` / `$disconnectedCallback` の `this`(と、そこからタイマーや `await` の先へ取っておいた `this`)は要素の行に付いていきます — `if:` が要素を隠している間もその行に書き、要素の書き込み(`users[1] = …`)で行がその場で差し替わったら新しい行に書きます。要素がいったん外されて別の行に使い回された後は、取っておいた `this` はどちらの行にも書かずに `The host row of <user-row> was removed.` で投げます。行そのものが消えたときの `$disconnectedCallback` は、コンポーネント自身のキーを読み書きでき(`this.tid` に取っておいたタイマーの id を解除する)、行のキー(`this.name`)を読むと同じエラーで投げます。部分マウント(`state.name: .name`)の自身のキーは行ごとではなく要素ごとに 1 組なので、`$disconnectedCallback` がそこへ書いた値は、要素が別の行に使い回された後も残ります。
1996
+ 行の中のコンポーネントは自分の行を読み書きします。`$connectedCallback` / `$disconnectedCallback` の `this`(と、そこからタイマーや `await` の先へ取っておいた `this`)は要素の行に付いていきます — `if:` が要素を隠している間もその行に書きます。ホストの行への要素の書き込み(`this["users.1"] = obj`)はその位置の値を置き換えます: コンポーネント要素はそのまま残り、その私有キーも残りますが、行から読む値は新しい値になります。行が消えると、そのコンポーネントも一緒に消えて、もう描画しません: `$disconnectedCallback` はコンポーネント自身のキーを読み書きできます(`this.tid` に取っておいたタイマーの id を解除する)が、行のキー(`this.name`)を読み書きすると、別の行に届く代わりに `The host row of <user-row> was removed.` を throw します。
1844
1997
 
1845
1998
  ### コンポーネント側でリストを描画する
1846
1999
 
@@ -1880,7 +2033,7 @@ this.shadowRoot.innerHTML = `
1880
2033
 
1881
2034
  コンポーネントの中にさらにコンポーネントを置いて、**スコープを重ねる**こともできます。途中のコンポーネントが配列を素通しするだけで自分では `for` を回さなくても、正本スコープ起点の行フィールド書き込みは最下層の行まで届きます。
1882
2035
 
1883
- コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$renderedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
2036
+ コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
1884
2037
 
1885
2038
  ## Command Token(メソッドバインディング)
1886
2039
 
@@ -1939,7 +2092,7 @@ export default {
1939
2092
  - 重複するエントリは初期化時にエラーになる
1940
2093
  - 予約名 `$command` 自体は配列に含められない
1941
2094
  - token は `$command` 配下にまとめられるためトップレベルの state 名前空間を汚さない。token と同名のリアクティブプロパティが共存できる
1942
- - `$command` 上の未宣言の名前にアクセスする(例: `this.$command.typo`)と `undefined` が返る。typo はその後の `.emit()` 呼び出しで `TypeError` として、あるいはバインディングの右辺で使った場合は「CommandToken 値が必要」エラーとしてバインディング時に表面化する
2095
+ - `$command` 上の未宣言の名前にアクセスする(例: `this.$command.typo`)と `undefined` が返る。typo はその後の `.emit()` 呼び出しで `TypeError` として、あるいはバインディングの右辺で使った場合は、バインディングの取り付け時に `[wcs/token-undeclared] "$command.typo" is not declared in $commandTokens.`(`#1302`、did-you-mean 付き)として表面化する
1943
2096
 
1944
2097
  ### `command.<methodName>:` バインディング
1945
2098
 
@@ -1972,13 +2125,15 @@ class MyFetcher extends HTMLElement {
1972
2125
  }
1973
2126
  ```
1974
2127
 
1975
- > **v1.9.1 以降の破壊的変更**: `commands` フィールドは `{ name, async? }` オブジェクトの配列になりました。以前の `commands: ["fetch"]` という素の文字列形式はもう受け付けられません —— そのような宣言に対するバインディングは `Command "<name>" is not declared in wcBindable.commands` を throw します。レガシーフォールバックはありません。宣言をオブジェクト形式に更新してください。
2128
+ > `commands` のエントリは `{ name, async? }` オブジェクトでなければなりません。素の文字列のエントリ(`commands: ["fetch"]`)は何も宣言しないので、それへのバインディングは `[wcs/token-misconfigured] <my-fetcher> declares no command "fetch".` を throw します。
1976
2129
 
1977
2130
  検証ルール(バインディング時に強制):
1978
2131
 
1979
2132
  - 要素は `protocol: "wc-bindable"` かつ整数 `version` が `1` 以上(現行プロトコルは `1`。1 以上のすべてのバージョンが core 互換)の `static wcBindable` を公開するカスタム要素であること
1980
2133
  - `methodName` は `wcBindable.commands` に(`name` で)現れること
1981
- - バインドされる値は `CommandToken` であること(token 以外の値の代入は throw する —— 例えば未宣言の名前 `$command.typo` は `undefined` に解決され、ここで拒否される)
2134
+ - `<tokenName>` は `$commandTokens` で宣言されていること(`$command.typo` は `[wcs/token-undeclared]` を throw する)
2135
+
2136
+ カスタム要素はクラスが定義された時点で検査され、ネイティブ要素はその場で拒否されます(`[wcs/token-misconfigured]`)。ページのバインド中に見つかった違反は `<wcs-state>` の初期化を失敗させます。`for:` / `if:` の行の中で見つかった違反や、ページのバインド後にクラスが定義されて見つかった違反は、そのバインディング 1 本の失敗として `$errorCallback` に届き、行はそのまま構築されます([初期化の失敗](#初期化の失敗))。
1982
2137
 
1983
2138
  ### Token API
1984
2139
 
@@ -1998,10 +2153,10 @@ interface CommandToken {
1998
2153
 
1999
2154
  ### 購読のライフサイクル
2000
2155
 
2001
- - 購読者は要素を `WeakRef` で保持するので、token の購読者セット**それ自体**は要素を生かし続けない
2002
- - **注意(現在の実装):** 購読者のクロージャは `binding` も捕捉しており、`binding.node` はその同じ要素 — 上の `WeakRef` を打ち消す強参照です。実際には、取り外された要素は binding が解放されるまで残ります。つまり `WeakRef` が効くのは「binding は消えたが token に購読者が残っている」場合だけです。行や要素が消えた時点で購読者を解放するのが本筋の修正ですが、binding 機構に要素ライフサイクルのフックが無く、その追加とセットで入れるべき変更なので 3.x には入れていません。**今日頼れる仕組みは下の lazy purge**だと考えてください
2003
- - `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
2004
- - 所有する `<wcs-state>` が disconnect されても token レジストリは保持されるので、ルート `<wcs-state>` を付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。切断中は state を作れないので、`$command` 経由の emit は起きない
2156
+ - 購読者は要素を `WeakRef` でだけ保持する: token が要素を生かし続けることはない。`emit` 時、要素が回収済みの購読者は自分で購読を解除する
2157
+ - `for:` の行や `if:` の枝の中の要素は、その行や枝が消えるときに購読を解除する
2158
+ - token が emit されたときに接続されていない要素(ノードを保持したまま隠れているルートなど)は呼ばれず、購読は残る: 文書に戻れば次の `emit` を受け取る
2159
+ - 所有する `<wcs-state>` が disconnect されても token は保持されるので、付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。再設定(`setInitialState()`)は、新しい state が改めて宣言する名前の token をすべて保持する
2005
2160
 
2006
2161
  要素のメソッドは `emit` の引数で呼び出されます:
2007
2162
 
@@ -2140,7 +2295,7 @@ class MyTarget extends HTMLElement {
2140
2295
 
2141
2296
  - 要素は wc-bindable なカスタム要素であること(`static wcBindable`・`protocol: "wc-bindable"`・整数 `version` が 1 以上。1 以上のすべてのバージョンが core 互換)。非 wc-bindable 要素は attach 時に拒否される。
2142
2297
  - `<property>` は `wcBindable.properties` に現れること —— **attach 時** に検証(fail-fast。クラス参照のみで足り、DOM 接続に非依存)。
2143
- - `<tokenName>` は `$eventTokens` で宣言されていること —— **発火時** に検証。state はイベント発火時に要素の live root から解決されるため、attach 時にノードが detached になりうる `for` / `if` ブロック内や SSR ハイドレーション後でも機能する。
2298
+ - `<tokenName>` は `$eventTokens` で宣言されていること —— **発火時** に検証。token はイベント発火時に引かれるので、再設定した state の `$on` ハンドラにも届く。未宣言の名前は `console.error` で報告される(`[wcs/token-undeclared]`、did-you-mean 付き)。
2144
2299
  - 修飾子 `#prevent` / `#stop` は通常のイベントバインディングと同様に機能する: `eventToken.error#prevent: createFailed`。
2145
2300
 
2146
2301
  ### ループ内での使用
@@ -2178,29 +2333,26 @@ $on: {
2178
2333
 
2179
2334
  ### Token API
2180
2335
 
2181
- event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されても event-token registry は保持されるので、ルート `<wcs-state>` を付け直せば `$on` ハンドラ(と `on` の scan)は再びイベントを受けます。切断中に dispatch されたイベントは state ツリーが見つからず、届きません。
2336
+ event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されても、その event token と `$on` ハンドラは保持されます。
2182
2337
 
2183
2338
  ## 時間を扱う機構の選び方
2184
2339
 
2185
- 続く 4 節は、それぞれ別の問いに答えるものです。取り違えが起きやすいので、データの出どころではなく**何を宣言しているか**で選んでください。
2340
+ 続く節は、それぞれ別の 3 つの問いに答えるものです。取り違えが起きやすいので、データの出どころではなく**何を宣言しているか**で選んでください。
2186
2341
 
2187
2342
  | 宣言 | 宣言するもの | 値を持つか | 発火 | 主な用途 |
2188
2343
  |---|---|---|---|---|
2189
2344
  | [パス getter](#パス-getter算出プロパティ) | その値が現在の state から見て**何であるか** | 持たない(アドレス単位で再計算・キャッシュ) | 評価需要が読んだときに遅延評価 | 小計、分類、集計 |
2190
2345
  | [`$stream`](#streamstream) | 非同期の供給元と、**1 回の実行の中で** fold される値 | 持つ(出力は runtime の所有) | chunk ごと。`args` が変われば `initial` に戻して restart | フィード、ソケット、継続的な観測 |
2191
- | [`$watch`](#watchwatch) | 変更への反応 | 持たない | 変化したアドレスごとにバッチ 1 回、scan の書き込みの後 | 副作用、「条件が成立したとき」 |
2192
- | [`$scan`](#scanscan) | 時間をまたぐ累積値と、それを戻す条件 | 持つ(出力は runtime の所有) | 着地ごと(`from`)またはイベントごと(`on`) | ページ蓄積、履歴、件数。3.5 で警告が出て 4.0 で外れる: `$watch`(state のパス)か `$on`(イベントトークン)で畳む — [4.0 への準備](#40-への準備wcsv4-migration) |
2346
+ | [`$watch`](#watchwatch) | 変更への反応 | 持たない —— ただしハンドラが自分のキーに書くことはできる | 変化したアドレスごとにバッチ 1 回 | 副作用、「条件が成立したとき」、時間をまたぐ累積 |
2193
2347
 
2194
2348
  混乱のほとんどは、次の 2 点で解けます。
2195
2349
 
2196
2350
  - **`$renderedCallback` はこの表に入りません。** 適用されたバインディングを報告するものなので、そこに処理をぶら下げると描画内容に暗黙に依存します。[評価のきっかけ](#評価のきっかけdemand-root)を参照してください。
2197
- - **`$stream` の fold は restart のたびに戻り、`$scan` は戻りません。** restart を越えて値を保ちたいとき、あるいは状態ではなく出来事を数えたいときは `$scan` の領分です。
2351
+ - **`$stream` の fold は restart のたびに戻ります。** 累積した値を restart を越えて保ちたいときは、それを普通のキーとして持ち、供給元のパスに付けた `$watch` ハンドラから書き込んでください(状態ではなく出来事を数えるなら `$on` ハンドラから)—— リセットは、初期値を書き戻す `$watch` ハンドラです。その値は自分が所有する素の state になり、再設定ではほかのキーと同じく置き換えられます。
2198
2352
 
2199
2353
  ## Stream(`$stream`)
2200
2354
 
2201
- > **3.2 から宣言キーの正式名は `$stream` です**(要件 B12 — `$watch` / `$scan` と単数で揃えた)。旧名 `$streams` は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。`$streamStatus` / `$streamError` の名前空間は変わりません。3.1 以前では `$streams` と書いてください。
2202
-
2203
- 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 されます。
2355
+ 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 されます。`$stream` は `temporal` アドオンです(全部入りのエントリに含まれる)。3.x の名前 `$streams` は `[wcs/declaration-alias] $streams was removed: write $stream.`(`#1601`)を throw します。
2204
2356
 
2205
2357
  `$renderedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watchwatch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
2206
2358
 
@@ -2274,8 +2426,8 @@ $stream: {
2274
2426
  - **協調キャンセル(MUST)** —— `source` は渡された `AbortSignal` を必ず監視し、発火したら生産を停止すること。`ReadableStream` の source はこの契約を `cancel()` コールバック経由で自動的に満たします(abort 時に runtime が reader を cancel します)。自前の async iterable だけが `signal` を自分で監視する必要があります。
2275
2427
  - **有界 fold** —— 需要は producer に逆流しません(backpressure は明示的に放棄)。無限 / 長寿命ストリームでは latest・count・last-N(`(acc, chunk) => [...acc.slice(-99), chunk]`)・ウィンドウ集計など有界な fold を使うこと。生の全チャンク累積は有限ストリーム限定。
2276
2428
  - **`args` は同期** —— Promise を返すとエラー。`args` 内での wildcard 読みも拒否されます。
2277
- - **自己依存・相互サイクルの禁止** —— `args` が自 stream の値や status を読むとエラーになります。2 つの stream の相互サイクル(A の `args` が B の値を読み、B の `args` が A の値を読む)は検出されず無限 restart になるため組まないこと。一方向のチェイン(A の値を B の `args` が読む)は正当です。
2278
- - **プロパティは runtime の所有物** —— エントリ名が state の getter / setter / メソッドと衝突すると、`$scan` の出力と同じく宣言時にエラーになります(検査が無いと、そのメソッドは起動時の `initial` リセットで無言に上書きされます)。
2429
+ - **自己依存・相互サイクルの禁止** —— `args` が自 stream の値や status を読むとエラーになります。2 つの stream の相互サイクル(A の `args` が B の値を読み、B の `args` が A の値を読む)は宣言時には拒否されません: source が実行開始と同じタスクの中で値を出す場合は連鎖の上限がサイクルを断ち切りますが([Watch](#watchwatch) 参照)、値が後のタスクで届く場合(ネットワークの応答、メッセージ)は周回ごとに数え直しになり、サイクルは無限に restart します —— 組まないでください。一方向のチェイン(A の値を B の `args` が読む)は正当です。
2430
+ - **プロパティは runtime の所有物** —— エントリ名が state の getter / setter / メソッドと衝突すると、宣言時にエラーになります(検査が無いと、そのメソッドは起動時の `initial` リセットで無言に上書きされます)。
2279
2431
  - **SSR では起動しない** —— サーバーでは宣言のパースとプロパティの実体化(`initial`)のみ行い、source は実行されません。クライアント側は通常どおり起動します。
2280
2432
 
2281
2433
  完全な契約 —— ライフサイクルと所有権・restart セマンティクス・flush 粒度・スコープ外リスト —— は [docs/streams.ja.md](docs/streams.ja.md) を参照してください。
@@ -2312,7 +2464,7 @@ $renderedCallback(paths) {
2312
2464
  }
2313
2465
  ```
2314
2466
 
2315
- **規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$stream` の `args` のどれかに根を置いてください。`$renderedCallback` は「描かれたものに追随する」用途に限ります。(`$scan` は 3.5 で警告が出て 4.0 で外れます。同じことは `$watch` か `$on` のハンドラで書けます — [4.0 への準備](#40-への準備wcsv4-migration)。)
2467
+ **規則:** 描画に依存させたくないロジックは、`$watch` か `$stream` の `args` に根を置いてください。`$renderedCallback` は「描かれたものに追随する」用途に限ります。
2316
2468
 
2317
2469
  上の例はいまは stream の値に付けた `$watch` で着地したページを feed へ畳み(sentinel の再武装は feed の `$watch`)、`<b>` は表示専用に戻っています。この形(`$renderedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
2318
2470
 
@@ -2320,11 +2472,11 @@ $renderedCallback(paths) {
2320
2472
 
2321
2473
  需要の根が 3 か所に分かれること自体は変わりません。**ある getter が評価されるかを知るには、その 3 か所(ページの全バインディング・全 `$watch`・全 `$stream.args`)を見る必要があり、getter の定義だけを読んでも分かりません。** lint と DevTools の配線カバレッジはこの照合を機械にやらせるためのものです。
2322
2474
 
2323
- なお `$watch` に宣言したスカラー getter は **eager** になります(接続時に 1 回、以後は依存に触れたバッチごとに評価)。ワイルドカード行の getter は eager 化しません(初回評価がリスト全体を舐めるため)。
2475
+ なお `$watch` に宣言した getter は **eager** になります: 接続時に 1 回、以後は依存に触れたバッチの終わりごとに評価されます。ワイルドカードの行 getter はリストの全行について評価されるので、長いリストの上のものを watch すると、そのリストを舐めるコストがかかります。
2324
2476
 
2325
2477
  ## Watch(`$watch`)
2326
2478
 
2327
- `$renderedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
2479
+ `$renderedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、行パスも含めて、state の変化で発火します。`temporal` アドオンです(全部入りのエントリに含まれる)。
2328
2480
 
2329
2481
  ```html
2330
2482
  <wcs-state>
@@ -2333,6 +2485,7 @@ $renderedCallback(paths) {
2333
2485
  isLoading: false,
2334
2486
  items: [],
2335
2487
  startedAt: 0,
2488
+ lastPriceChange: "",
2336
2489
 
2337
2490
  $watch: {
2338
2491
  // 立ち上がり検出は cur/prev を自分で比較する
@@ -2341,7 +2494,6 @@ $renderedCallback(paths) {
2341
2494
  },
2342
2495
 
2343
2496
  // ワイルドカードパスは変化した行ごとに 1 回発火する
2344
- //(リストを `for` で描画しているか、`$listKeys` の宣言が要る。後述)
2345
2497
  "items.*.price"(cur, prev, index) {
2346
2498
  this.lastPriceChange = `#${index}: ${prev} → ${cur}`;
2347
2499
  },
@@ -2356,109 +2508,38 @@ $renderedCallback(paths) {
2356
2508
  | 引数 | 契約 |
2357
2509
  |---|---|
2358
2510
  | `cur` | drain 時点の値(そのバッチの確定値) |
2359
- | `prev` | **バッチ開始時点**の値(first-write-wins)。記録されるのは**プリミティブを書いたときだけ**(書く前の値はオブジェクトでもよい。下記) |
2511
+ | `prev` | **バッチ開始時点**の値(first-write-wins)。記録されるのは、watch しているパスそのものに**プリミティブを書いたときだけ**(書く前の値はオブジェクトでもよい。下記)。getter では前回の評価値 |
2360
2512
  | `...indexes` | ワイルドカードパスのときのみ。そのスコープ自身のループ添字(`$1` / `$2` と同じ規約) |
2361
2513
 
2362
- **`prev` はプリミティブの書き込みにだけ付きます。** same-value guard がプリミティブを書く前に読む旧値を再利用するため watch のための追加読みは発生せず、その帰結として書く値が参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。オブジェクトの上にプリミティブを書いたときは、そのオブジェクトが `prev` に渡ります。
2514
+ **`prev` はプリミティブの書き込みにだけ付きます。** same-value guard がプリミティブを書く前に読む旧値を再利用するため watch のための追加読みは発生せず、その帰結として書く値が参照型(in-place 変異では同じ参照になるため)・パスより上のオブジェクトが書かれたとき・`$postUpdate` 経由・リストに入ってきた行・`$behavior.sameValueGuard` オフのときは `undefined` になります。オブジェクトの上にプリミティブを書いたときは、そのオブジェクトが `prev` に渡ります。
2363
2515
 
2364
- **`$watch` は独自の発火条件を持ちません。** 更新バッチに載ったものをそのまま発火します。これはうまく噛み合っていて、同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
2516
+ **`$watch` は独自の発火条件をほとんど持ちません。** 更新バッチに載ったものをそのまま発火します: 同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。値を比べるのは 2 つの場合です: 数値の添字を含むキー(`"items.0.v"`)は、その位置の値が変わったときだけ発火し、リストの代入や並べ替えで据え置かれた行の行 getter(`items.*.label`)は、値が変わったときだけ発火します —— ただし、同じバッチの書き込みが、getter が行の外で読んだもの(`now`・`items.length`・それらを読むルートの getter)を変えた場合や、別の行の同じパスを書いた場合は別です。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
2365
2517
 
2366
- **getter を watch すると eager になります。** computed は本来 lazy で、依存は評価時にしか記録されません —— つまり描画していない getter は本来一度も発火しません。`$watch` に宣言すると接続時に 1 回評価され、以後は依存に触れたバッチの終端で毎回評価されます。`prev` は前回の評価値です。重い computed を watch すればその評価コストが毎バッチ乗り、getter 内の例外は watch 経由で表面化します。ワイルドカード getter(`items.*.tax`)は eager 化**しません**(初回評価がリスト全体を舐めることになるため)。この形は DOM にバインドされている場合にのみ発火し、`prev` は常に `undefined` です(行ごとの評価値を保持しないため)。
2518
+ **getter を watch すると eager になります。** computed は本来 lazy で、依存は評価時にしか記録されません —— つまり描画していない getter は本来一度も発火しません。`$watch` に宣言すると接続時に 1 回 —— ワイルドカード getter(`items.*.tax`)なら全行について —— 評価され、以後は依存に触れたバッチの終端で毎回評価されます。`prev` は前回の評価値で、変更が届けば、値が変わったかどうかに関わらず watch は発火します(上の 2 つの場合を除く)。重い computed を watch すればその評価コストが毎バッチ乗り、getter 内の例外は眠ったままにならず watch 経由で表面化します。接続時の評価が throw した場合は報告され(`$watch initial evaluation of "…" threw.`)、その watch の最初の `prev` は `undefined` になり、ほかはすべて続行します。
2367
2519
 
2368
2520
  発火順序は 3 層に分かれ、利用者が意思を持てるのは真ん中の層だけです。
2369
2521
 
2370
2522
  | 層 | 順序 | 制御 |
2371
2523
  |---|---|---|
2372
- | 機構間 | `$renderedCallback` → `$scan` → `$watch` → `$stream` restart | 固定 |
2524
+ | 機構間 | `$renderedCallback` → `$watch` → `$stream` restart | 固定 |
2373
2525
  | ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
2374
2526
  | 同一パスの行間 | `indexes` 昇順 | 固定 |
2375
2527
 
2376
- **機構間の層を動かす唯一のもの**が、`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 を参照してください。
2528
+ **機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$renderedCallback` —— がフレームで着地する一方、`$watch`・`$stream` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$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 を参照してください。
2377
2529
 
2378
2530
  主なルール:
2379
2531
 
2380
- - **ツリーのパスのみ** —— パスに `@`(v1 の名前セレクタ)は書けません。含む宣言は loud に拒否されます。
2532
+ - **ツリーのパスのみ** —— キーは空にできず、`$` で始めることも、`@`(v1 の名前セレクタ)を含むことも、`Object.prototype` のメンバ(`toString`)を名指すこともできません。そうした宣言は throw します(`$watch path "…" is not a path of the state tree.`)。`**` を含むキーは `[wcs/recursion-unsupported]` を throw します。state 上で解決しないキーは `diagnostics` アドオンが警告します(`wcs/watch-path-missing`)。
2381
2533
  - **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
2382
- - **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。入れ子のリストを置き換えると、新しい配列の行がすべて発火します。
2383
- - **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
2384
- - **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。`for` が無ければ、同じ配列やその写しの代入と `$postUpdate("items")` でも発火しません。行の値を読み直させるだけなので、それを読む getter(`$getAll("items.*.price", [])`)はその場で変えた値を読みます。
2385
- - **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$renderedCallback` とは異なる扱いです。
2386
- - **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。段は書き込みごとに数えます。ハンドラ(と `$scan` の畳み)は自分を起こした書き込みの深さから始まるので、連鎖を伸ばすのは、ハンドラの書き込みが別のハンドラを起こしたときだけです。長い有限の描画の連鎖(`$renderedCallback` が描画ごとに見出しを 1 段ずつ詰める)を見るハンドラは、自分の書き込みが同じバッチに載っても打ち切られません。`$stream` の再開もハンドラと同じく数えます。再開の書き込み(`initial` への戻し・status)は再開を起こした書き込みの連鎖の続きで、上限を越えた書き込みが起こす再開は行いません。そのため、stream の `args` へ書き、再開でまた起こされる `$watch` も、`args` で互いの値を読む stream どうしも打ち切られます —— 上限を越えたバッチは、`$watch` を宣言していなくても報告されます。
2387
- - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$renderedCallback` を持てます)へ誘導します(`$stream` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
2534
+ - **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。配列を代入すると、リストに入ってきた行が発火します。同一性で据え置かれた行は変化していないので発火しません。
2535
+ - **行単位の差分を見たいなら `$listKeys`** —— 新しいオブジェクトとして届くデータ(再取得)は全行が新しい行として入ってくるので、行 watch は**全行**について `prev === undefined` で発火します。`$listKeys` を宣言すればキー突合が代入を per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
2536
+ - **行 watch も headless です** —— `for:` も `$listKeys` も要りません: watch は、自分が範囲とするリストを自分で追従させます。同じ配列やその写しの代入と `$postUpdate("items")` では発火しません —— どの行もリストに入っておらず、どの行パスも書かれていないためです —— が、行を読む getter(`$getAll("items.*.price", [])`)はその場で変えた値を読みます。
2537
+ - **共有配列は位置ごとに発火します** —— 複数の外側の行が同じ配列を持っているとき、その要素の 1 つへの書き込みは、その配列を持つ外側の行ごとに 1 回ずつ、それぞれの添字で `$watch("groups.*.items.*")` を発火させます: それらのどのパスでも値が変わったからです。
2538
+ - **ハンドラの例外は隔離されます** —— throw は報告され(`$watch "…" failed; the other watches still run.`)、残りの watch(と stream の restart)は続行します。
2539
+ - **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。書き込みはそれぞれ連鎖の深さを持ちます: その書き込みをしたハンドラを起こした書き込みの深さに 1 を足したものです。32 段より深くで起こされたハンドラは実行されません: 連鎖は断ち切られ、`console.error`(`$watch handlers / $stream restarts kept writing for 32 batches; the chain is cut (nothing is rolled back).`)と DevTools の `state:watch-chain-limit` で 1 回報告され、そのバッチのほかのハンドラは実行されます。値と DOM は巻き戻しません。同じバッチに載る書き込み —— 描画の書き戻し —— は連鎖を伸ばしもリセットもしないので、長い有限の描画の連鎖(`$renderedCallback` が描画ごとに見出しを 1 段ずつ詰める)を見るハンドラは打ち切られません。`$stream` の restart もハンドラと同じく数えます: restart の書き込み(`initial` への戻し・status)は `args` に届いた書き込みの連鎖の続きで、上限を越えた restart は行いません。source が実行の(再)開始と同じタスクの中で出した値はその実行の連鎖に数えるので、すぐに値を出す source を持ち、`args` で互いの値を読む 2 つの stream は、ページを固まらせずに打ち切られます。後のタスクから来た値と、それが起こすハンドラは、数え直しになります。
2540
+ - **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントの `$watch` 宣言は、ルート state へ誘導する `wcs/mount-dollar-declaration` の警告を 1 回出して無視されます。`$stream` と `$renderedCallback` も同様です。ボリュームは `$watch` を拒否します([`mount=`](#追加の状態をマウントするmount)): ルートにフルパス(`"cart.total"`)で宣言してください。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
2388
2541
  - **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
2389
2542
 
2390
- ## Scan(`$scan`)
2391
-
2392
- > **`$scan` は 3.5 で警告が出て、4.0 で外れます。** state のパスは累積値を書く `$watch` のハンドラで、イベントトークンは `$on` のハンドラで畳み、reset の条件もハンドラに書いてください。[4.0 への準備](#40-への準備wcsv4-migration)を参照。
2393
-
2394
- `$stream` が畳むのは 1 回の run の**内側**で、restart のたびに値は `initial` へ戻ります。`$watch` は値を所有しません。**`$scan`** はその両方を跨いで残る値 —— 時間軸方向の累積 —— を、持ち主・発火単位・reset 条件つきで宣言します。
2395
-
2396
- ```html
2397
- <wcs-state>
2398
- <script type="module">
2399
- export default {
2400
- page: 1,
2401
- host: "a",
2402
- $eventTokens: ["message"],
2403
- $stream: {
2404
- pageResult: { args: (s) => s.page, source: loadPage },
2405
- },
2406
- $scan: {
2407
- // from: state パスの着地ごとに畳む(ここでは stream の値)
2408
- feed: {
2409
- from: "pageResult",
2410
- initial: { items: [], pages: [] },
2411
- fold: (feed, chunk) =>
2412
- chunk?.kind === "success" && !feed.pages.includes(chunk.page)
2413
- ? { items: feed.items.concat(chunk.items), pages: [...feed.pages, chunk.page] }
2414
- : feed,
2415
- },
2416
- // on: 宣言済みイベントトークンの出来事ごとに畳む
2417
- log: {
2418
- on: "message",
2419
- initial: [],
2420
- fold: (log, event) => [...log.slice(-49), event.detail],
2421
- resetOn: ["host"], // host が変わるたびに [] へ戻す
2422
- },
2423
- },
2424
- };
2425
- </script>
2426
- </wcs-state>
2427
-
2428
- <template data-wcs="for: feed.items">…</template>
2429
- ```
2430
-
2431
- | フィールド | 契約 |
2432
- |---|---|
2433
- | `from` | state パス。ワイルドカード可。`$` 始まり・getter・getter の配下は不可。`from` と `on` はどちらか 1 つだけ。 |
2434
- | `on` | `$eventTokens` に宣言したイベントトークン名。 |
2435
- | `initial` | 必須。累積の種であり、`resetOn` の戻り先。 |
2436
- | `fold` | 必須。`from` は `(acc, cur, prev, ...indexes) => next`、`on` は `(acc, event, ...indexes) => next`。同期で、`this` 無しで呼ばれ、新しい値を返す。`acc` そのものを返すと書き込まない。 |
2437
- | `resetOn` | 任意。素の state パスの配列。どれかが書かれたら出力を `initial` に戻す。`from` の scan はそのバッチの fold を行わず、`on` の scan は書き込みより後に来たイベントを `initial` から畳む。`from` の配下は raise、`from` の祖先は可(親の差し替えで作り直す)。オブジェクトのパスはそのオブジェクト自身が書かれたときだけ reset し、子への書き込みでは reset しない(葉のパスを並べるか nonce を使う)。 |
2438
-
2439
- **出力はランタイムが所有します**(`$stream` の値と同じ)。state にそのプロパティが無ければ `initial` で実体化され(plain なデータは複製するので、出力の plain な部分の子パスへ書いても宣言の `initial` は変わりません。クラスのインスタンスや凍結された値など plain でない値は宣言と共有したままです)、他のパスと同じようにバインドできます。stream の restart・切断と再接続・同じオブジェクトの再セットを跨いで残り、新しい宣言での再セットでは作り直されます。出力名が getter・setter・メソッド・`$stream` のエントリと衝突すると raise します。
2440
-
2441
- 2 つの source の発火:
2442
-
2443
- | | `from`(パス) | `on`(イベントトークン) |
2444
- |---|---|---|
2445
- | 単位 | 更新バッチに載ったアドレス 1 つにつき 1 回。同じ job 内の複数の書き込みは 1 回に畳まれる。 | イベント 1 回につき 1 回。同じ task の 2 回は 2 回畳む。 |
2446
- | いつ | drain の終わり、`$watch` より先。 | イベントの中、そのトークンの `$on` ハンドラより先。 |
2447
- | 出力が見えるのは | 次のバッチから。出力を見る `$watch` はそこで発火し、`prev` はふつう `undefined`(下の注記)。 | すぐ。同じイベントの `$on` ハンドラは畳んだ後の値を見る。 |
2448
-
2449
- 主なルール:
2450
-
2451
- - **getter を畳まない。** getter は入力が変わるたびに再評価されるので、畳むと出来事ではなく再評価の回数を数えます。`from` や `resetOn` に getter を書く(`from` に `$recursion` の `**` getter の展開形 `nodes.*.total` を書くのも同じ)と、宣言時に raise します(`wcs/scan-source-computed`)。
2452
- - **1 回の fold は着地ごとで、ページごとではない。** `done` 後の再試行や、ページの再接続は同じページをもう一度着地させます。問題になるなら fold に冪等キーを持たせてください(上の `pages`)。
2453
- - **stream の `args` を自分の scan 出力から導出しない。** `feed` から導出した getter(`feed` を畳む別の scan の出力から導出したものを含む)を `pageResult` の `args` が読むと、stream が自分の結果で restart し続けるので、ランタイムは `wcs/scan-feedback-loop` を raise します。カーソルはイベントから進めてください。stream の restart と同じバッチに着地した chunk は abort される run のものなので畳みません。
2454
- - **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$stream` の restart の書き込みは `prev` を持ちます。
2455
- - **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$stream` と同じ)。
2456
- - **例外は隔離される。** throw・Promise の戻り値・読めない値はコンソールと DevTools に報告され、書き込みません(ワイルドカードの `from` で読めない行はその行だけを飛ばし、行の着地はリストの位置 1 つにつき 1 回に絞ります)。他の scan・watch・stream の restart は続行します。
2457
- - **`$watch` は scan の書き込みの後に走る。** 同じ drain の `$watch` ハンドラは畳んだ後の出力を読み、ハンドラが出力へ書いた値はそのまま残ります。出力の着地が drain される前に `from` の source がもう一度書かれる(その drain の `$watch` ハンドラが書くなど)と、両方が同じバッチに載ります。このとき出力を見る `$watch` は `prev` に着地した値を受け、`cur` に 1 段先の値を見て、次のバッチで同じ値でもう一度発火することがあるので、同じ値の重複に耐える形にしてください。ユーザー操作で累積を消すなら、`resetOn` に nonce を読ませてください。
2458
- - **ルートのみ。** ボリューム(`mount=`)は `$scan` を拒否し、マウントされた `bind-component` スコープは 1 回の warn で無視します。SSR では `from` は畳みません(出力の実体化は行います)。
2459
-
2460
- リファレンス: [docs/scan.ja.md](https://github.com/wcstack/wcstack/blob/main/packages/state/docs/scan.ja.md)。設計の決定レコード: [docs/state-scan-design.md](https://github.com/wcstack/wcstack/blob/main/docs/state-scan-design.md)。
2461
-
2462
2543
  ## Inputs と属性ミラー
2463
2544
 
2464
2545
  `wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
@@ -2502,7 +2583,7 @@ chip.payload = null → element.data = null かつ removeAttribute("dat
2502
2583
  補足:
2503
2584
 
2504
2585
  - `attribute` を**持たない** `inputs` エントリはプロパティのみ —— 値はプロパティに書き込まれるが属性には触れない
2505
- - ミラーはベストエフォート: `setAttribute` の失敗は握りつぶされ(`debug` 警告付き)、プロパティ書き込みをブロックしない
2586
+ - ミラーはベストエフォート: `setAttribute` の失敗は握りつぶされ、プロパティ書き込みをブロックしない
2506
2587
  - ネイティブ HTML 要素は `inputs` を完全に無視する —— ミラーは `static wcBindable` を公開するカスタム要素でのみ有効になる
2507
2588
 
2508
2589
  ## コンポーネント機構の選び方
@@ -2584,7 +2665,7 @@ export default {
2584
2665
 
2585
2666
  - クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ。各 `$bindables` メンバは `properties` と `inputs` の両方に宣言され(双方向)、方向認識初期同期の下でも親 state → DCC の書き込みが機能します — [バインディング authority](#バインディング-authority-init--sync) 参照。各 `$commands` メンバは `commands` エントリになります
2586
2667
  - `$bindables` はプロトタイプの getter/setter、`$commands` はメソッド — いずれもリアクティブプロキシ経由
2587
- - `CustomEvent` のディスパッチ — `$bindables` メンバが変更されるたびに `my-counter:count-changed` が発火
2668
+ - `CustomEvent` のディスパッチ — `$bindables` メンバが変更されるたび(メンバ自体の書き込み、その下のパスの書き込み、その `$postUpdate`)に `my-counter:count-changed` が発火。`detail` が書き込まれた値を運ぶのはメンバ自体への書き込みのときだけで、宣言された `getter` はメンバを要素から読む
2588
2669
 
2589
2670
  `commands` エントリは常に `async: true` です。DCC のメソッドは内側の `<wcs-state>` の初期化に chain するため、状態側のメソッドが `async` でなくても戻り値は Promise になります。
2590
2671
 
@@ -2654,7 +2735,7 @@ export default {
2654
2735
  | `$commands` | 起動可能メソッドの宣言 |
2655
2736
  | `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
2656
2737
  | `$disconnectedCallback` | クリーンアップフック |
2657
- | `$renderedCallback` | 状態変更後に呼ばれる |
2738
+ | `$renderedCallback` | バインディングの適用後に呼ばれる |
2658
2739
 
2659
2740
  ## SVG サポート
2660
2741
 
@@ -2672,7 +2753,7 @@ export default {
2672
2753
 
2673
2754
  状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$renderedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
2674
2755
 
2675
- > **3.2 から `$updatedCallback` の正式名は `$renderedCallback` です**(要件 B12)。このフックが受けるのは**適用されたバインディング**の更新で、state の更新全体ではない(それには `$watch`)ことを名前で言うためです。旧名は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。3.1 以前では `$updatedCallback` と書いてください。
2756
+ > `$renderedCallback` が受けるのは**適用されたバインディング**の更新で、state の更新全体ではありません(それには `$watch`)。3.x の名前 `$updatedCallback` は `[wcs/declaration-alias] $updatedCallback was removed: write $renderedCallback.`(`#1601`)を throw します。
2676
2757
 
2677
2758
  ```html
2678
2759
  <wcs-state>
@@ -2699,7 +2780,7 @@ export default {
2699
2780
 
2700
2781
  | フック | タイミング | 非同期 |
2701
2782
  |---|---|---|
2702
- | `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
2783
+ | `$connectedCallback` | 初回接続時は state が読み込まれてバインディングが構築された後、再接続時は毎回呼び出し | 可(await される) |
2703
2784
  | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
2704
2785
  | `$renderedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
2705
2786
  | `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$renderedCallback` の後に呼び出し | 可(await されない) |
@@ -2709,8 +2790,8 @@ export default {
2709
2790
  - フック内の `this` は読み書き可能な状態プロキシです。
2710
2791
  - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
2711
2792
  - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
2712
- - `$renderedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
2713
- - `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま)。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
2793
+ - `$renderedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのバインディングは、したがってその私有キーも、ホストの `paths` には決して現れません。`async` を使用できますが、戻り値は await されません。throw または reject した場合は `console.error` で報告されます。
2794
+ - `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま。`for:` で描画していないリスト —— `$getAll` か `$watch` だけが保持しているリスト —— では `node` は `null`)。`for:` / `if:` の行の中で取り付け時に拒否されたバインディング(未宣言の token、要素が宣言していないメンバ)も、そのバインディングの失敗としてここに報告され、行はそのまま構築されます。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
2714
2795
 
2715
2796
  ```js
2716
2797
  export default {
@@ -2722,7 +2803,7 @@ export default {
2722
2803
  };
2723
2804
  ```
2724
2805
 
2725
- フックはバッチの後(`$renderedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$renderedCallback` が投げた例外(loud に失敗する)は対象外です。
2806
+ フックはバッチの後(`$renderedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ボリューム(`<wcs-state mount>`)は実行しません(`console.warn`)。マウントされたコンポーネントのフックは、そのコンポーネント自身のバインディングの失敗を受け取ります。`$watch` ハンドラの失敗(別途隔離・報告)、throw した `$renderedCallback`(`console.error` で報告)、失敗した `$connectedCallback`([初期化の失敗](#初期化の失敗))は対象外です。
2726
2807
  - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
2727
2808
 
2728
2809
  ## 遷移アニメーション
@@ -2746,15 +2827,135 @@ li {
2746
2827
  そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
2747
2828
 
2748
2829
  - drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$renderedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
2749
- - `$scan`・`$watch`・`$stream` restart は元の microtask に留まるため、`$renderedCallback` の**前**に走るようになる。
2830
+ - `$watch`・`$stream` restart は元の microtask に留まるため、`$renderedCallback` の**前**に走る。
2750
2831
 
2751
2832
  適用すべきバインディングが実際にあるバッチだけがタグへ渡されるので、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 参照。
2752
2833
 
2753
2834
  ## 診断と失敗の扱い
2754
2835
 
2836
+ ### エラーメッセージと番号
2837
+
2838
+ メッセージはすべて `[@wcstack/state]` で始まります。`@wcstack/lint` と VS Code 拡張も検査する事柄についてのメッセージは同じ診断 code(`[wcs/filter-unknown]`)を持ち、ほとんどのメッセージは番号(`#501`)を持ちます。番号の百の位がその code を表します —— 1xx `binding-syntax`、2xx `template-syntax`、3xx `binding-path-missing`、4xx `binding-type-expectation`、5xx `filter-unknown`、6xx `filter-arity`、7xx `getter-cycle`、8xx `getter-depth-exceeded`、9xx `index-arity`、10xx `index-param-range`、11xx `recursion-unsupported`、12xx `token-misconfigured`、13xx `token-undeclared`、14xx `wildcard-rank`、15xx `spread-no-bindable`、16xx `declaration-alias`、17xx `name-alias`。1〜99 は code を持ちません。番号は追加されるだけなので、番号の意味はバージョンをまたいで変わりません。
2839
+
2840
+ 番号付きメッセージの文章 —— いちばん近い名前(lint と同じ規則による did-you-mean)、直し方、そして lint が検出できる場合は lint への案内 —— は、`@wcstack/state` と `/auto` に含まれる `diagnostics` アドオンが出します。それを入れていない `/core` のページでは、メッセージは code・番号・表示するはずだった値だけになります:
2841
+
2842
+ ```
2843
+ [@wcstack/state] [wcs/filter-unknown] filter not found: uc. "uc" was renamed "upper" in 3.2 and removed in 4.0 — write "upper". Validate statically: npx @wcstack/lint <file>.
2844
+ [@wcstack/state] [wcs/filter-unknown] #501 "uc"
2845
+ ```
2846
+
2847
+ 1 行目が `@wcstack/state` と `/auto` の出力、2 行目が `features/diagnostics` なしの `/core` の出力です。`/core` で開発するときは `diagnostics` を入れてください。アドオンなしのページが意図して突き当たる障壁(`[wcs/feature-not-installed]`、`formats` なしの書式フィルタ)は、core の中でも完全な文章です。
2848
+
2849
+ <details>
2850
+ <summary>番号付きメッセージ</summary>
2851
+
2852
+ | # | Code | メッセージ(`diagnostics` あり) |
2853
+ |---|---|---|
2854
+ | 1 | | `$scan was removed (use $watch or $on)` |
2855
+ | 2 | | `"<path>" is a getter without a setter` |
2856
+ | 3 | | `no row for "<path>"` |
2857
+ | 4 | | `cannot write "<path>": its parent is <value>` |
2858
+ | 5 | | `"<name>" is not a method` |
2859
+ | 6 | | `$getAll("<path>"): no loop level in common with the context` |
2860
+ | 7 | | `$eqIndex("<path>") needs a list row scope.` |
2861
+ | 8 | | `This state is readonly.` |
2862
+ | 9 | | `$setAll("<path>") needs indexes ([] for every match)` |
2863
+ | 10 | | `$setAll("<path>", …, { spread: true }) needs an array of <n> values` |
2864
+ | 11 | | `updates did not settle after 32 passes` |
2865
+ | 12 | | `binding "<type>: <path>" failed to apply.` |
2866
+ | 13 | | `failed to load "<src>": <status>` |
2867
+ | 14 | | `this <wcs-state> failed to initialize; create a new one` |
2868
+ | 15 | | `state is not initialized` |
2869
+ | 16 | | `no <script> with id "<id>"` |
2870
+ | 17 | | `a subscriber of token "<name>" threw.` |
2871
+ | 18 | | `<key> must be an array of strings.` |
2872
+ | 19 | | `<key> entries must be non-empty strings.` |
2873
+ | 20 | | `<key> entry "<name>" conflicts with the reserved namespace name "<reserved>".` |
2874
+ | 21 | | `<key> entry "<name>" is duplicated.` |
2875
+ | 22 | | `$on must be an object of handlers.` |
2876
+ | 23 | | `$on entry "<name>" is not declared in $eventTokens.` |
2877
+ | 24 | | `$on entry "<name>" must be a function.` |
2878
+ | 25 | | `view-transition naming-limit (<limit>) reached.` |
2879
+ | 26 | | `filter <name> requires at least one option` |
2880
+ | 27 | | `filter <name> requires a number as option` |
2881
+ | 28 | | `filter <name> requires a number value` |
2882
+ | 29 | | `filter <name> requires a date value` |
2883
+ | 30 | | `filter <name> requires an array value` |
2884
+ | 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
2885
+ | 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
2886
+ | 33 | | `Unknown binding modifier "<key>" in "<modifier>".` |
2887
+ | 34 | | `Binding modifier "<key>" may only be specified once.` |
2888
+ | 35 | | `Invalid <key> modifier value "<value>".` |
2889
+ | 36 | | `Event bindings only allow init=none.` |
2890
+ | 37 | | `Binding type "<type>" does not support init=<init>.` |
2891
+ | 38 | | `Property "<name>" is not declared by wcBindable.` |
2892
+ | 39 | | `init=<init> is incompatible with wcBindable member "<name>".` |
2893
+ | 40 | | `sync=connect requires observable property "<name>".` |
2894
+ | 41 | | `render chain depth limit exceeded (100 drains that rendering itself started); bindings for this batch were not applied.` |
2895
+ | 42 | | `The inline <script> of <wcs-state> was blocked by Content-Security-Policy. …` |
2896
+ | 43 | | `Failed to evaluate the inline <script> of <wcs-state>: <detail>. …` |
2897
+ | 44 | | `<where>: "<key>" is not one of its options, or not of the option's type.` (`<where>` は `bootstrapState`、`$behavior`、またはオブジェクトでない `$behavior` に対しては `state`) |
2898
+ | 45 | | `a re-set state may not change $behavior: create the element again.` |
2899
+ | 46 | | `$features must be an array of add-on names (["temporal", "formats"]).` |
2900
+ | 47 | | `a second <wcs-state> on the same root: there is one state tree per root — …` |
2901
+ | 48 | | `the locale "<locale>" (<html lang> or bootstrapState's locale) is not a language tag Intl takes (en-US, not en_US): the locale filters use "en".` |
2902
+ | 49 | | `<wcs-state src="…"> failed to initialize.` |
2903
+ | 50 | | `<wcs-state src="…"> $connectedCallback failed.` |
2904
+ | 101 | `binding-syntax` | `Invalid bindText: "<text>". Missing ':' separator between propPart and statePart.` |
2905
+ | 102 | `binding-syntax` | `"<text>": "<keyword>" takes no modifiers or filters on its left side — write "<keyword>:".` |
2906
+ | 103 | `binding-syntax` | `"<text>": "else" takes no value — write "else:".` |
2907
+ | 104 | `binding-syntax` | `Invalid spread binding "<text>": spread target path is required.` |
2908
+ | 105 | `binding-syntax` | `Invalid spread binding "<text>": filters are not allowed on spread targets.` |
2909
+ | 106 | `binding-syntax` | `"<prop>": a leading "." binds an element property by name — …` |
2910
+ | 107 | `binding-syntax` | `unterminated <quote> quote in the filter arguments "(<args>)". Close the quote.` |
2911
+ | 108 | `binding-syntax` | `Invalid filter format: missing closing parenthesis in "<filter>".` |
2912
+ | 109 | `binding-syntax` | `Invalid filter format: missing opening parenthesis in "<filter>".` |
2913
+ | 110 | `binding-syntax` | `Invalid filter format: ")" comes before "(" in "<filter>".` |
2914
+ | 111 | `binding-syntax` | `"<filter>": unexpected "<text>" after the filter's closing ")" — separate filters with "\|" …` |
2915
+ | 112 | `binding-syntax` | `an empty filter in "<source>" — remove the extra "\|" or name the filter.` |
2916
+ | 113 | `binding-syntax` | `"<name>" is not a filter name: a modifier list "#<modifiers>" comes before the input filters, not inside one` |
2917
+ | 114 | `binding-syntax` | `"<name>" is not a filter name: "#" cannot appear in one.` |
2918
+ | 115 | `binding-syntax` | `"<prop>": a binding takes one modifier list after a single "#"` |
2919
+ | 116 | `binding-syntax` | `"<prop>": the left side of a binding must name a property — …` |
2920
+ | 117 | `binding-syntax` | `"<path>" has <n> path segments — the limit is 512.` |
2921
+ | 119 | `binding-syntax` | `"<text>": the right side of a binding must name a state path — …` |
2922
+ | 120 | `binding-syntax` | `"<path>": a state path cannot go through "__proto__" or "prototype" (it would reach every object's prototype).` |
2923
+ | 121 | `binding-syntax` | `"<text>": "for:" takes no filters — … Declare a getter that returns the filtered list and loop over it ("for: <getter>").` |
2924
+ | 201 | `template-syntax` | `Invalid bindText: "<text>". 'if', 'elseif', 'else', and 'for' bindings must be single binding.` |
2925
+ | 202 | `template-syntax` | `"<type>:" must follow an "if:" template` |
2926
+ | 203 | `template-syntax` | `"<name>:" replaces its element, so it cannot be used inside a "for" / "if" template …: bind innerHTML: on a wrapper element instead.` |
2927
+ | 204 | `template-syntax` | `a "<type>:" template at the top of inserted content was not rendered, … wrap it in an element.` |
2928
+ | 301 | `binding-path-missing` | `Path "<path>" does not exist on the state tree.` |
2929
+ | 401 | `binding-type-expectation` | `class.<name> needs a boolean, got <type>.` |
2930
+ | 501 | `filter-unknown` | `filter not found: <name>.` |
2931
+ | 601 | `filter-arity` | `filter "<name>" requires at least <min> argument(s) (<given> given).` |
2932
+ | 602 | `filter-arity` | `filter "<name>" accepts at most <max> argument(s) (<given> given).` |
2933
+ | 701 | `getter-cycle` | `"<path>" depends on itself` |
2934
+ | 801 | `getter-depth-exceeded` | `"<path>"` |
2935
+ | 901 | `index-arity` | `<api>("<path>") takes <depth> index(es), got <n>.` |
2936
+ | 902 | `index-arity` | `<api>("<path>") takes at most <depth> index(es), got <n>.` |
2937
+ | 1001 | `index-param-range` | `"<key>": list index parameters run from $1 to $128.` |
2938
+ | 1101 | `recursion-unsupported` | `"<path>" uses "**", which is not accepted here.` |
2939
+ | 1201 | `token-misconfigured` | `"<prop>: <path>": the right-hand side must be $command.<name>` |
2940
+ | 1202 | `token-misconfigured` | `<tag> declares no static wcBindable (<binding>).` |
2941
+ | 1203 | `token-misconfigured` | `<tag> declares no command "<method>".` |
2942
+ | 1204 | `token-misconfigured` | `<tag> declares no property "<prop>".` |
2943
+ | 1301 | `token-undeclared` | `eventToken "<name>" is not declared in $eventTokens.` |
2944
+ | 1302 | `token-undeclared` | `"$command.<name>" is not declared in $commandTokens.` |
2945
+ | 1401 | `wildcard-rank` | `"<path>" needs <depth> enclosing loop level(s); the scope provides <n>.` |
2946
+ | 1402 | `wildcard-rank` | `"<path>" is relative: it needs an enclosing "for" template` |
2947
+ | 1403 | `wildcard-rank` | `"<path>" ranges over the rows of "<list>", but the enclosing "for" template at that level renders "<loop>".` |
2948
+ | 1501 | `spread-no-bindable` | `<tag> declares no static wcBindable (<binding>).` |
2949
+ | 1601 | `declaration-alias` | `<old> was removed: write <name>.` (`$streams`, `$updatedCallback`) |
2950
+ | 1701 | `name-alias` | `<old> was removed: write <name>.` (`$trackDependency`, `$untrackDependency`) |
2951
+
2952
+ 番号と文章の出典は `src/diagnostics/messages.ts` です。
2953
+
2954
+ </details>
2955
+
2755
2956
  ### 存在しないパスへの配線は報告されます
2756
2957
 
2757
- 配線したパスが state 上で解決しないことが**確実**なとき、バインド確立時(`$watch` と `$scan` は宣言時)に 1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2958
+ 配線したパスが state 上で解決しないことが**確実**なとき、`diagnostics` アドオンが、バインディング(または `$watch` のキー)を組み立ててから 1 マクロタスク後に、1 回だけ警告します。診断 code はコンソール・`@wcstack/lint`・VS Code 拡張で共通です:
2758
2959
 
2759
2960
  ```
2760
2961
  [@wcstack/state] [wcs/binding-path-missing] Bound path "user.nmae" does not resolve on the state tree:
@@ -2765,47 +2966,46 @@ dropped. Validate statically: npx @wcstack/lint <file>.
2765
2966
  | 状況 | 挙動 |
2766
2967
  |---|---|
2767
2968
  | ネストしたパスの打ち間違い(`user.nmae`) | `console.warn`(`wcs/binding-path-missing`)。更新は届かないままなので、直すのは書き手 |
2768
- | トップレベルのパスの打ち間違い(`cout`) | 読み取り時に throw。文面は上と同じ語彙(did-you-mean 付き) |
2969
+ | トップレベルのパスの打ち間違い(`cout`) | 読み取り時に throw —— `[wcs/binding-path-missing] Path "cout" does not exist on the state tree. Did you mean "count"?`(`#301`)—— そのためバインディングの適用が失敗し、ほかのバインディングの失敗と同じく報告される |
2769
2970
  | `$watch` のキーの打ち間違い | `console.warn`(`wcs/watch-path-missing`)。単一セグメントでも報告する |
2770
- | `$scan` の `from` / `resetOn` のパスの打ち間違い | `console.warn`(`wcs/scan-path-missing`)。単一セグメントでも報告する。その scan は一度も畳まれない(reset されない) |
2771
2971
 
2772
2972
  判定は**過小近似**です。静的に決められない形では黙ります —— 誤検知でページを騒がせないことを優先しているためで、以下はすべて警告しません:
2773
2973
 
2774
2974
  - 親が `null` / `undefined`(初期値 `null` に後から代入する形)
2775
2975
  - 初期値が空配列のリストの行フィールド(行の形が分からない)
2776
2976
  - 途中の getter の戻り値のサブプロパティ
2777
- - マウントされたコンポーネントのマーカーパス(`#m…` —— 私有キー・getter の実体はマウントのオーバーレイ側にあり raw state には無い)
2977
+ - まだ読み込み中のボリュームのマウントパス配下のパス —— ボリュームが決着した時点で検査される
2778
2978
  - `$` 始まりの予約名前空間(`$command.*` など)
2779
2979
 
2780
- 裏を返すと、**警告が出ない = 正しい保証にはなりません**。網羅した検査は `npx @wcstack/lint <file>` 側で行ってください。
2980
+ 裏を返すと、**警告が出ない = 正しい保証にはなりません**。網羅した検査は `npx @wcstack/lint <file>` 側で行ってください。再設定(`setInitialState()`)では、すべてのパスを新しい state に対して検査し直します。
2781
2981
 
2782
2982
  ### 添字の本数・階数・循環も検査されます
2783
2983
 
2784
- パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code で現れます。ただし下表の 6 つ —— `wcs/getter-depth-exceeded` / `wcs/index-param-range` / `wcs/recursion-context` / `wcs/recursion-shared-list` / `wcs/recursion-cycle` / `wcs/recursion-depth-exceeded` —— はこのリリースでは**実行時専用**で、lint は出しません。
2984
+ パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code で現れます。ただし下表の 5 つ —— `wcs/getter-depth-exceeded` / `wcs/recursion-context` / `wcs/recursion-shared-list` / `wcs/recursion-cycle` / `wcs/recursion-depth-exceeded` —— は**実行時専用**で、lint は出しません。
2785
2985
 
2786
2986
  | 診断 | 何を見るか | 直し方 |
2787
2987
  |---|---|---|
2788
2988
  | `wcs/index-arity` | `$resolve(path, indexes)` は `*` の本数と**厳密一致**、`$getAll(path, indexes)` / `$setAll(path, indexes, …)` は**上限**(不足は「残りの階層を全展開」という正当な接頭辞) | 本数を合わせる |
2789
- | `wcs/wildcard-rank` | パスの `*` の本数(と `$N` の N)が、囲む `for` の段数を超えていないか | `for` を足すか、`$resolve(path, indexes)` で行を明示する |
2790
- | `wcs/getter-cycle` | パス getter どうしが循環参照していないか。実行時は「アドレススタックが既に積んでいるアドレスへ戻る」ことで判定する | 循環を断つ |
2791
- | `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じアドレスを 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
2989
+ | `wcs/wildcard-rank` | マークアップで、パスの `*` の本数(と `$N` の N)が囲む `for` の段数を超えていないか(`#1401`)、`.` パスを囲む `for` があるか(`#1402`)、各 `*` がその段で囲んでいる `for` の行であって、別のリストの行ではないか(`for: a` の中の `{{ b.*.y }}`、`#1403`) | `for` を足すか、別のリストの行は getter の中で `$resolve(path, indexes)` で読む |
2990
+ | `wcs/getter-cycle` | パス getter どうしが循環参照していないか: 評価が、すでに評価中の getter の出現に戻ってきた | 循環を断つ |
2991
+ | `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じ出現を 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
2792
2992
  | `wcs/index-param-range` | `$N` は実在するワイルドカード段を指すこと(`$1`〜`$128`・先頭ゼロ不可) | 実在する段を使う |
2793
2993
  | `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$dependOn`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
2794
- | `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー、`get "nodes.**"`、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
2795
- | `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
2994
+ | `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー —— そして `diagnostics` アドオンがあれば、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
2995
+ | `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 何も無い(`get "nodes.**"`)、空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
2796
2996
  | `wcs/recursion-context` | **束縛**形の `**` を、束縛先の深さが無い場所で読んだ —— トップレベル、またはアンカー外の getter | 再帰 getter か行 getter の中から読むか、`[]` で全深さを合併する |
2797
- | `wcs/recursion-getall-form` / `wcs/recursion-setall-form` | `**` に対して定義できない `indexes` の形。コードが付くのは非空の接頭辞(両 API)と、`$getAll` の配列でない `indexes`(`null`・文字列など)。`$setAll` の省略・mapper・`{ spread: true }` も同じ誤りで、lint は同じコードで報告するが、実行時は形を名指しした文面で throw するだけでコードは付かない | 現在の深さなら省略、全深さなら `[]` |
2798
- | `wcs/recursion-structural-write` | 再帰 `$setAll` が構造そのもの(ノード・子リスト・その `length`・子ノード・反復サブパスが多段なら子リストへ至る途中のオブジェクト)を指している | 葉のプロパティへブロードキャストする |
2997
+ | `wcs/recursion-getall-form` / `wcs/recursion-setall-form` | `**` に対して定義できない `indexes` の形。コードが付くのは非空の接頭辞(両 API)と、`$getAll` の配列でない `indexes`(`null`・文字列など)。`$setAll` の省略・mapper・`{ spread: true }` も同じ誤りで、lint は同じコードで報告する。実行時は mapper と `{ spread: true }` が `[wcs/recursion-setall-form]` で、添字の省略は `indexes` の無い `$setAll` と同じく `#9` で throw する | 現在の深さなら省略、全深さなら `[]` |
2998
+ | `wcs/recursion-structural-write` | 再帰 `$setAll` が構造そのもの(ノード・子リスト・その `length`・子ノード・反復サブパスが多段なら子リストへ至る途中のオブジェクト)を指している | 葉のプロパティへブロードキャストする(子ノードの下の葉 `nodes.**.children.*.value` もその 1 つ) |
2799
2999
  | `wcs/recursion-readonly` | 書き込みが再帰 getter、またはその導出値の内側を指している —— 再帰 `$setAll` の `nodes.**.total` でも、`nodes.*.children.*.total` のような具体的な展開形への書き込み(固定本数の `$setAll`・値付き `$resolve`・直接代入)でも | getter の導出元を書く |
2800
3000
  | `wcs/recursion-shared-list` / `wcs/recursion-cycle` | 走査が同じ配列インスタンスに 2 度到達した —— 2 つのノードが 1 本の子リストを共有、または自分の祖先から到達可能 | 各ノードに自分の子配列を持たせる |
2801
3001
  | `wcs/recursion-depth-exceeded` | 展開後のパスがワイルドカード 128 段を超える —— 木がエンジンのアドレス可能な深さより深いか、循環している | 木を平らにするか、循環を探す |
2802
3002
 
2803
- `wcs/recursion-*` の各行がどの形を拒否していて、代わりに何を書けばよいのかは、上の**再帰パス**の節に書いてあります。
3003
+ `wcs/recursion-*` の各行がどの形を拒否していて、代わりに何を書けばよいのかは、[再帰パス](#再帰パスrecursion)の節に書いてあります。
2804
3004
 
2805
- `$resolve` / `$getAll` の**添字の超過は以前は黙って捨てられ**、取り違えたまま「もっともらしい値」が返っていました。現在はどちらもエラーです:
3005
+ 添字の超過が黙って捨てられることはありません: 取り違えたまま「もっともらしい値」を返すはずの呼び出しは、代わりに throw します。
2806
3006
 
2807
3007
  ```javascript
2808
- // ❌ "*" は 1 本しか無いのに 2 本渡している → 以前は items[0] の値が返っていた
3008
+ // ❌ "*" は 1 本しか無いのに 2 本渡している → [wcs/index-arity] を throw
2809
3009
  this.$resolve("items.*.price", [row, col]);
2810
3010
 
2811
3011
  // ✅ 2 次元なら 2 本
@@ -2814,29 +3014,63 @@ this.$resolve("matrix.*.*", [row, col]);
2814
3014
  this.$getAll("matrix.*.*", [row]);
2815
3015
  ```
2816
3016
 
3017
+ ### 初期化の失敗
3018
+
3019
+ 初期化に失敗した `<wcs-state>` は `console.error` で 1 回報告されます —— まず要素とその state の出どころ、続いてエラー:
3020
+
3021
+ ```
3022
+ [@wcstack/state] <wcs-state src="./state.js"> failed to initialize. SyntaxError: …
3023
+ ```
3024
+
3025
+ 見出し(`#49`)は、要素が持っていれば `mount=`・`bind-component=`・`state=`・`src=` を名指します。その要素の `connectedCallbackPromise` はエラーをそのまま(ラップせずに)伴って reject し、`initializePromise` は resolve するので、1 つの要素の失敗がページの残りを止めることはありません。
3026
+
3027
+ 初期化を失敗させるもの:
3028
+
3029
+ - 読めないソース([状態の初期化](#状態の初期化))。
3030
+ - 不正な宣言: 削除された名前(`$scan`・`$streams`・`$updatedCallback`)、形の誤った `$behavior` / `$features` / `$commandTokens` / `$eventTokens` / `$on`、アドオンが入っていない宣言(`[wcs/feature-not-installed]`)。
3031
+ - ページのバインド中に見つかったマークアップの誤り: 構文エラー、未知のフィルタや誤った引数の個数、wildcard-rank のエラー、フィルタ付きの `for:`、テンプレートの中の `outerHTML:`、クラスが定義済みの要素での wc-bindable の設定ミス。走査はそのエラーで止まります: 文書順でそれより前はバインドされ、後はバインドされません。デプロイの前に `npx @wcstack/lint` を実行してください。
3032
+ - 別の `<wcs-state>` がすでにバインドしているルートの上の、2 つ目のルート `<wcs-state>`(`#47`)。state ツリーはルートごとに 1 本です。部分木は `mount=` で接ぎ木してください。
3033
+
3034
+ `for:` / `if:` の行の中で、バインディングの取り付け時に見つかったエラー(未宣言の token、要素が宣言していないメンバ)は、初期化の失敗ではなく、そのバインディングの失敗になります: `$errorCallback` に届き、行はそのまま構築されます。state にないトップレベルのキーへのバインディングは、初期化の失敗ではなく適用の失敗です。初期化に失敗した要素は再始動できません —— `setInitialState()` は throw します(`#14`)。取り除いて新しい要素を作ってください。
3035
+
3036
+ **失敗した `$connectedCallback` は初期化の失敗ではありません。** `$connectedCallback` はバインディングが構築された後に走ります。throw または reject したものは `<wcs-state …> $connectedCallback failed.`(`#50`)に続けてエラーとして報告され、`connectedCallbackPromise` はそのエラーで reject します —— ただしページはバインドされています: `getBindingsReady()` は resolve し、要素は再設定もできます。再接続時に失敗した `$connectedCallback` は `console.error` で報告されます。
3037
+
3038
+ **各 Promise が約束すること:**
3039
+
3040
+ | | `initializePromise` | `connectedCallbackPromise` |
3041
+ |---|---|---|
3042
+ | ルートの `<wcs-state>` | state が読み込まれ、バインディングが構築されたら resolve —— 初期化に失敗したときも resolve | `$connectedCallback` の後に resolve。初期化の失敗と、失敗した `$connectedCallback` で reject |
3043
+ | DCC の定義(`data-wc-definition` の shadow root の中の `<wcs-state>`) | resolve | タグが定義されたら resolve。定義が失敗すると reject(state の読み込みの失敗、`$bindables` / `$commands` のエラー、そこにある `bind-component`)—— その場合タグは定義されない |
3044
+ | マウントされたコンポーネント(`<wcs-state bind-component>`) | コンポーネントのバインディングが構築されたら resolve —— 失敗したときも | マウントが失敗すると reject: ホストのプロパティがオブジェクトでない、配線のエラー、1 つのコンポーネントの中の 2 つ目の接続済み `<wcs-state bind-component>`、初期化に失敗したルートに配線されたコンポーネント(`<tag>.state will not mount: the root state failed to initialize.`)、コンポーネントの描画後に失敗した `$connectedCallback` |
3045
+ | ボリューム(`<wcs-state mount>`) | resolve | 常に resolve —— 失敗した・拒否されたボリュームは報告して、接ぎ木せずに決着する(`scopes` アドオンが無いときは reject) |
3046
+
3047
+ `getBindingsReady(root)` は、`root`(`document` または shadow root)のバインディングが、そこに接続された `<wcs-state>` によって構築されたら resolve します —— `$connectedCallback` は、どれほど遅くても、決着しても throw しても待ちません。reject するのは、そのルートに接続されたすべての `<wcs-state>` がバインディングを構築する前に失敗した場合だけで、最初の失敗を伴います。はぐれた要素(`#47` で拒否された 2 つ目のルート、ソースの読み込みに失敗したもの)は、別の要素がそのルートをバインドしている間は何も変えません。ルートの `<wcs-state>` が置き換えられた(取り除かれ、新しいものが追加された)ときは、新しいものが決めます。そこにある唯一の `<wcs-state>` が `setInitialState()` を待っている間は pending のままで、そこに 1 つも無いとき —— 初めから無い、またはそのルートから外されたものしか無い —— はすぐに resolve します。DCC の定義や Shadow DOM コンポーネントの shadow root では、その `<wcs-state>` に従います: コンポーネントのバインディングが構築されたら resolve し、マウントが失敗したら reject します。Light DOM コンポーネントはページのルートの中身をバインドし、その失敗は `getBindingsReady(document)` には届きません。ボリュームがこれを reject させることはありません。
3048
+
3049
+ `@wcstack/server` の `renderToString()` と `@wcstack/testing` の `mount()` は、Light DOM コンポーネントのものも含めてすべての `connectedCallbackPromise` を待つので、上の場合にはこれらも reject します。これらのケースは [`__tests__/init-failure.test.ts`](__tests__/init-failure.test.ts) で固定されています。
3050
+
2817
3051
  ### バインディング 1 本の失敗は 1 本に閉じ込められます
2818
3052
 
2819
- バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`console.error` と DevTools(`state:binding-apply-error`)に出ます。
3053
+ バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`$errorCallback` に、それが無ければ `console.error` にエラーを続けて出し、DevTools(`state:binding-apply-error`)にも届きます。
2820
3054
 
2821
3055
  ```
2822
- [@wcstack/state] binding "text: items.*.label" failed to apply; the rest of this batch continues.
3056
+ [@wcstack/state] binding "text: items.*.label" failed to apply.
2823
3057
  ```
2824
3058
 
2825
- 隔離しない場合、1 本の throw が「値は新しいのに DOM は途中まで」という半端な状態を作り、しかも `$watch` と stream の restart が丸ごと消えていました(README のこの下にある発火順の契約が黙って破れる)。
3059
+ 同期や描画に失敗したリスト(キーが無い、getter が throw した)も、その `for` バインディングの失敗として同じように報告され、それまでの行を保ちます。
2826
3060
 
2827
3061
  ### 描画中の書き込みは無限ループになりません
2828
3062
 
2829
- バインディングの適用の最中に起きる書き込みがあります。`for:` の新しい行の中の wc-bindable 要素が初期値を state へ渡す(出力専用メンバー・`#init=element`)ときや、`$renderedCallback` が書くときです。こうした書き込みはそれぞれ新しいバッチを作り、そのバッチがまた描画されます。描画が毎回新しい値を書く形 —— たとえば行の要素の出力を、その一覧の getter が読むキーそのものへ束ね、要素がインスタンスごとに違う値を返す —— では、microtask が回り続けてページが固まります。この連鎖は 100 段で打ち切ります。101 段目になるバッチは描画せず(値は state に残ります)、そのバッチに載っていたパスを 1 回だけ報告します:
3063
+ 描画そのものが書き込みを送り返すことがあります: 要素の書き戻し(双方向の入力欄、wc-bindable のイベント —— バインディングが要素のプロパティを設定したときに要素がその場で同期に出すものも含む ——、出力専用メンバーの初期値)と、`$renderedCallback` の中での書き込み(その中の `await` の後も、決着するまで)、`$watch` ハンドラの中での書き込み、`$stream` の restart による書き込みです。こうした書き込みはそれぞれ新しいバッチを作り、そのバッチがまた描画されます。描画が毎回新しい値を書く形 —— たとえば行の要素の出力を、その一覧の getter が読むキーそのものへ束ね、要素がインスタンスごとに違う値を返す —— では、microtask が回り続けてページが固まります。描画だけが送り込んだバッチが続いた数を数え、100 を超えると次のバッチは描画せず(値は state に残ります)、そのバッチに載っていたパスを 1 回だけ報告します:
2830
3064
 
2831
3065
  ```
2832
- [@wcstack/state] render chain depth limit exceeded; bindings for this batch were not applied. { maxDepth: 100, paths: ["mode", "view", "view.*", "view.*.m"] }
3066
+ [@wcstack/state] render chain depth limit exceeded (100 drains that rendering itself started); bindings for this batch were not applied. ["mode", "view", "view.*", "view.*.m"]
2833
3067
  ```
2834
3068
 
2835
3069
  上限がほかの 2 つ(32)より大きいのは、ここの 1 段が 1 回の描画だからです。描いて測って直す連鎖は、32 段を超えても正当に収まります —— 見出しの文字サイズを 64px から 18px まで 1px ずつ詰めると描画は 47 回、400 行を 10 行ずつ描くと 40 段です。
2836
3070
 
2837
- 連鎖を伸ばすのは、バインディングの適用中に**同期で**起きた書き込みだけです。バインディングが要素のプロパティを設定したとき、要素がその場で同期に出したイベントもこれに含まれます。drain の外から届く書き込み —— 利用者の操作・非同期に届く I/O ノードのイベント・`$stream` の値・`await` の続き —— は 0 から数え直させます。`$scan`・`$watch`・`$stream` の restart の書き込みは、伸ばしも数え直させもせず、連鎖をそのまま次のバッチへ引き継ぎます。そのため `$watch` や `$scan` を挟む循環(要素が `x` へ書き、`$watch: { x(v) { this.mode = v } }`、一覧が `mode` を読む)も、描画 1 回で 1 段ずつ伸びて打ち切られます。これは `$watch` の上限には掛かりません —— ハンドラの書き込みは、直接別のハンドラを起こすのではなく、描画を挟んで戻ってくるからです。普通の一覧の行の初期同期のように収まる書き戻しは 1 段で止まります。連鎖を引き継ぐので、`$watch` や `$scan` でつないだ有限の連鎖は 100 段を分け合います。47 段で収まる見出しの詰めを、終わったら `$watch` が次の詰めを始める形で 2 本つなぐと収まります(描画 94 回)が、同じように 3 本つなぐと 3 本目の途中で打ち切られます。報告は連鎖ごとに 1 回です。描画しなかったバッチが `$watch` や `$scan` を通して連鎖をさらに引き継いでも、続くバッチは報告せずに描画しません。打ち切った後も、次の外からの書き込みは普通に描画されます。DevTools には `state:render-chain-limit` が届きます。
3071
+ コードからの書き込み —— イベントハンドラ、`$connectedCallback`、`$stream` の値、自分のコードの中の `await` の続き —— は連鎖を終わらせ、1 タスクの間が空いたときも同じです: 数は 0 からやり直しになります。値を microtask で出す要素(Lit の `updated()` など)は連鎖の中に残るので、そうした要素を通るループも打ち切られます。`$watch` を挟む循環(要素が `x` へ書き、`$watch: { x(v) { this.mode = v } }`、一覧が `mode` を読む)も、描画 1 回で 1 段ずつ伸びて打ち切られます。これは `$watch` の上限には掛かりません —— ハンドラの書き込みは、直接別のハンドラを起こすのではなく、描画を挟んで戻ってくるからです。`$stream` の restart の書き込みも数えます: microtask 1 つ分の間隔で stream を restart させる要素の書き戻しは、描画 50 回ほどで打ち切られます(stream が無ければ 100 回)。普通の一覧の行の初期同期のように収まる書き戻しは 1 段で止まり、`$watch` でつないだ有限の連鎖は 100 段を分け合います。報告は連鎖ごとに 1 回で、次のコードからの書き込みは普通に描画されます。DevTools には `state:render-chain-limit` が届きます。
2838
3072
 
2839
- 循環を閉じる書き込みが microtask から届く形は、この上限の対象外です(上限が無かったときと同じです):値を microtask で出す要素(Lit の `updated()` など)と、`await` の後で書く async の `$renderedCallback` です。こうした書き込みは、有限の非同期のループと見分けられません —— 解決済みの Promise を `await` しながら 1 万回書く `async` 関数も、マクロタスクを 1 回も挟まずに 1 万回描画します。続いた描画の数に上限を設けると、正当なコードまで止めてしまいます。
3073
+ 1 回の更新が自分の中で仕事を生み続ける場合 —— バインディングの適用中に書き込みが起きる場合 —— は、32 周回で止められ、連鎖ごとに 1 回、パスを添えて `updates did not settle after 32 passes` と報告されます(DevTools: `maxDepth: 32` 付きの `state:render-chain-limit`)。
2840
3074
 
2841
3075
  ### 値と DOM は巻き戻しません
2842
3076
 
@@ -2844,46 +3078,55 @@ this.$getAll("matrix.*.*", [row]);
2844
3078
 
2845
3079
  | 機構 | 上限 | 超過時 |
2846
3080
  |---|---|---|
2847
- | 因果伝播の hop | 32 | その transaction の未処理レコードのみ quarantine |
2848
- | `$watch` の書き込み連鎖 | 32 | そのバッチの `$scan` / `$watch` の発火と、上限を越えた書き込みが起こす `$stream` の再開をスキップ |
3081
+ | 1 回の更新の中の周回 | 32 | その更新を止める。まだ描画するはずだったものは次の変更を待つ |
3082
+ | `$watch` / `$stream` restart の連鎖 | 32 | 上限を越えた書き込みが起こすハンドラと stream の restart をスキップ。そのバッチのほかのハンドラは実行される |
2849
3083
  | 描画中の書き込みの連鎖 | 100 | そのバッチのバインディング適用をスキップ |
3084
+ | getter の入れ子 | 128 | `wcs/getter-depth-exceeded` を throw |
2850
3085
  | バインディングの適用失敗 | — | その 1 本のみスキップ |
2851
3086
 
2852
- ### 4.0 への準備(`wcs/v4-migration`)
3087
+ ## 3.x からのアップグレード
2853
3088
 
2854
- 3.5 は 3.x の最後の minor です。4.0 が外す・移す名前と設定を名指しで知らせ、動きは 3.x のままにします。警告はコード `wcs/v4-migration` の `console.warn` で、名前ごとにページで 1 回だけ出ます。4.0 での扱いと書き換え先を示します:
3089
+ 4.0 は `@wcstack/state` の中のエンジンを置き換えます。書く対象は変わりません —— `<wcs-state>`、`data-wcs`、`{{ }}` とコメントバインディング、`for` / `if`、パスとパス getter、`$` API、`bind-component`、`mount=`、`enable-ssr`、そしてパッケージのエントリ。違いはすべて、その理由と代わりに書くものとともに [3.x → 4.0 移行ガイド](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.ja.md) にあります。手順は: まず 3.5 に上げて、その `[wcs/v4-migration]` 警告と 3.5 の lint が報告するものを片付け、それからすべての `@wcstack/*` パッケージを一度に 4.0 へ上げます —— `@wcstack/server` もクライアントと一緒に。
2855
3090
 
2856
- ```
2857
- [@wcstack/state] [wcs/v4-migration] filter "uc" is removed in 4.0: write "upper" (its name since 3.2).
2858
- See "Preparing for 4.0" in the @wcstack/state README.
2859
- ```
3091
+ 要点:
2860
3092
 
2861
- | 書き方 | 3.x | 4.0 | 書き換え先 |
2862
- |---|---|---|---|
2863
- | 3.2 で改名したフィルタの旧名: `inc` `dec` `fix` `uc` `lc` `cap` `rep` `rev` `pad` `null` | エイリアス | `[wcs/filter-unknown]` | `add` `sub` `toFixed` `upper` `lower` `capitalize` `repeat` `reverse` `padStart` `nullIfEmpty` |
2864
- | `$trackDependency` / `$untrackDependency` | エイリアス | `[wcs/name-alias]` を投げる | `$dependOn` / `$untracked` |
2865
- | `$updatedCallback` / `$streams` | エイリアス | `[wcs/declaration-alias]` を投げる | `$renderedCallback` / `$stream` |
2866
- | `substr(start, length)` | 動く | `[wcs/filter-unknown]` | `slice(start, start + length)`(第 2 引数は長さではなく終わりの位置) |
2867
- | `$scan` | 動く | 投げる | state のパスは `$watch`、イベントトークンは `$on` |
2868
- | `bootstrapState({ debug })`・`commentTextPrefix`・`enablePropagationContext` | 動く | 投げる | 消す。4.0 のコメント束縛は `<!--@@: path-->` と `<!--@@wcs-text: path-->` だけ |
2869
- | `bootstrapState({ enableMustache })`・`sameValueGuard`・`enableDirectionalInitialSync` | ページ全体の設定 | 投げる。state の木ごとに `$behavior` で宣言する | 3.x の間はそのまま。上げるときに、ルートとコンポーネントの各 state の `$behavior` へ移す(コンポーネントはホストのものを継がない。ボリュームには書けない) |
2870
- | `bootstrapState` の知らないキー・型の違う値(`null`、配列、文字列でないタグ名、`state` / `ssr` 以外の `tagNames` のキー) | 無視 | 投げる | 消すか直す |
2871
- | state のキー `$behavior` | ただのデータ(3.x は読まない) | その state の振る舞いの設定 | 3.x が動かしている設定と値が違うとき、4.0 が投げる形のときだけ警告 |
2872
- | state のキー `$features` | ただのデータ | その state が要る後付け | 4.0 が投げる形(後付けの名前の配列でない)のときだけ警告 |
2873
- | `$watch`・`$listKeys`・`$renderedCallback`(`$updatedCallback`)を宣言したボリューム(`<wcs-state mount>`) | マウントパスに相対で動く | 接ぎ木しない(`console.error`) | ルートの state へ移し、パスを絶対にする |
2874
- | ルートのパスを注入するボリューム(`data-wcs="state.<key>: …"`) | 動く(3.1) | 接ぎ木しない(`console.error`) | ルートの getter でルートのパスを読む |
2875
- | `$behavior` / `$features` を宣言したボリューム | ただのデータとして接ぎ木 | 接ぎ木しない(`console.error`) | ルートの state に書く |
3093
+ - **削除:** `$scan`(`$watch` か `$on` のハンドラで畳む)、3.2 の旧名 —— フィルタ `inc` `dec` `fix` `uc` `lc` `cap` `rep` `rev` `pad` `null`、`$trackDependency` / `$untrackDependency`、`$updatedCallback` / `$streams` ——、`substr` フィルタ(`slice(start, start + length)`)、`bootstrapState()` のオプション `debug`・`commentTextPrefix`・`enablePropagationContext`、`IStateElement.listPaths` / `getterPaths` / `setterPaths` / `nextVersion()`。
3094
+ - **設定:** `enableMustache`・`sameValueGuard`・`enableDirectionalInitialSync` は `bootstrapState()` から各ルート state の `$behavior` へ移り、すべてのパッケージの `bootstrapXxx()` は持たないオプションで throw します([設定](#設定))。
3095
+ - **イベント:** よく使うバブリングイベントの `on*:` ハンドラはルートへ委譲されるので、`event.currentTarget` はルートです。`#direct` でバインディングを委譲から外せます([イベントハンドリング](#イベントハンドリング))。
3096
+ - **リスト:** リストの要素への書き込みはその位置の値を置き換えます —— 行を動かすには新しい配列を代入します([`$resolve`](#resolve--明示的なインデックスでのアクセス))。`for:` はフィルタを取りません([ループ](#ループ-for))。行と枝の要素からは `data-wcs` が取り除かれ、テンプレートの中では `outerHTML:` / `outerText:` を使えません([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。
3097
+ - **ボリューム:** ボリュームはもう `$watch`・`$listKeys`・`$renderedCallback` を実行せず、注入も受けません([`mount=`](#追加の状態をマウントするmount))。
3098
+ - **SSR:** 3.x のサーバーの出力は、クライアントで描画し直されます([サーバーサイドレンダリング](#サーバーサイドレンダリング))。
3099
+ - **`$watch`:** 行の watch は `for:` も `$listKeys` も無くても発火し、行 getter の watch はすべての行で評価され、配列を丸ごと代入すると入ってきた行だけが発火します([Watch](#watchwatch))。
3100
+ - **TypeScript:** `defineState` は `@wcstack/state/define` から import します —— `@wcstack/state` から import すると、バンドルにエンジンが残ります([TypeScript サポート](#typescript-サポート))。
2876
3101
 
2877
- 出る時点: 宣言キーの旧名・`$scan`・`$behavior`・`$features` は state オブジェクトを読み込んだとき(ボリュームは接ぎ木したとき)、フィルタの旧名と `substr` はそれを使うバインディングを組み立てたとき、`bootstrapState` の設定は `bootstrapState()` を呼んだとき、`$trackDependency` / `$untrackDependency` はそれを読んだとき。検査は旧い書き方を解決する箇所にあるので、正式名で書いたページの動きは何も変わりません。実行するものの差は、フィルタを組み立てるときと `$dependOn` / `$untracked` を読むときの比較 1 つずつで、計測できる差はありません。
3102
+ `@wcstack/lint`・`@wcstack/typescript`・VS Code 拡張(2.0.0 から)は、4.0 と同時に公開されるリリースで 4.0 の規則を持ちます。3.x のプロジェクトでは 3.5 時代のバージョンを使い続けてください: 4.0 の規則は、3.x がまだ受け付ける書き方を報告します。
2878
3103
 
2879
- 警告は全部入りの入口(`@wcstack/state` と `/auto`)だけが持ちます。分割の入口(`@wcstack/state/core` と `features/*`)のページには出ません。それ以外の切り替えはありません(2.6 と同じ)。警告は旧い書き方に実際に届いた経路でだけ出るので、コンソールが静かでも、ページが通らなかった経路については何も分かりません。`npx @wcstack/lint <file>` と VS Code 拡張は旧名を静的に報告します(`wcs/name-alias`、info、3.2 から)。
3104
+ ## セキュリティ
2880
3105
 
2881
- `$behavior` へ移る 3 つの設定は、ページが渡している間は警告が出続けます。3.x はこの設定を `bootstrapState` からしか読まないので、上げる前に変えるところはありません。`$behavior` を先に書いても害はなく、3.x が動かしている設定と照らし合わせて検査されます。4.0 の規則のうち 1 つは 3.5 で警告しません: 再セット(初期化済みの要素への `setInitialState()`)で `$behavior` を変えることはできず、4.0 は投げます(#45)。要素を作り直してください。
3106
+ `@wcstack/state` は値を DOM に入れます。値をサニタイズはせず、値をマークアップとして読むこともありません。マークアップとして読むのはページそのものです —— そのため、規則は「何がページに届くか」と「何をどこにバインドするか」についてのものになります。
2882
3107
 
2883
- `on*#direct:` は 3.x でもう書けます。4.0 は `on*:` をルートへ委譲し、要素に直接付ける修飾子 `#direct` を足します。3.x はどの `on*:` も要素に付け、`#direct` は何も言わずに受け取ります。要素に付くことが要る束縛には今から書いておけます: `onclick#direct,stop:` は祖先に付けた自前のリスナーを止め、祖先が `stopPropagation()` してもハンドラは呼ばれます。
3108
+ **自分で書いていないマークアップをページ走査に届かせてはいけません。** `<wcs-state>` は自分のルートをバインドするとき、マークアップの中のすべての `{{ … }}`、すべての `data-wcs` 属性、すべてのコメントバインディング(`<!--@@: …-->`)を読みます([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。サーバーのテンプレートやページのコードが、state ではなくページのマークアップへ書き込んだユーザーの内容も同じように読まれます: `{{ apiKey }}` と入力されたユーザー名は state の `apiKey` を描画し、投稿されたメッセージの中の `data-wcs` 属性はバインドされます —— クライアントサイドテンプレートインジェクション(CSTI)です。コメントバインディングは `$behavior.enableMustache: false` でもバインドされるので、`{{ }}` を OFF にしても防げません。4.0 には除外用の属性はありません(`data-wcs-ignore` はありません)。代わりに:
3109
+
3110
+ - ユーザーの内容は **state** に入れ、`textContent:`(または、テキストである `{{ }}` / コメントバインディング)でバインドしてください —— 値がマークアップとして読まれることはありません。
3111
+ - ユーザーの HTML を `innerHTML:` / `html:` でバインドするのは、サニタイズした後だけにしてください。HTML のシンク(`innerHTML:`・`outerHTML:`・`srcdoc:`・`html:`)は、すでに `TrustedHTML` であるものを除くすべての値を、設置したポリシー(`setTrustedTypesPolicy()`、または `globalThis[Symbol.for("wcstack.trustedTypes.policy")]`)に通します。ブラウザに Trusted Types があってもなくても同じなので、設置したサニタイザは必ず走ります。このパッケージが identity ポリシーで値に署名することはありません([docs/csp.ja.md §7](../../docs/csp.ja.md))。
3112
+ - サーバーのテンプレートや自分のコードに、ユーザーの内容をページのマークアップへ書かせないでください: データとして届けてください(state の中に入れるか、state へ取得する)。HTML エスケープでは足りません —— `{{ }}` はそのまま残ります。
3113
+
3114
+ **値は検査しません。** `href:`・`src:`・`attr.href:` にバインドした `javascript:` URL や、`attr.onclick:`(任意の `attr.on*:`)にバインドしたハンドラ文字列は、そのまま書き込まれます。URL は自分で検証し、`script-src` に `unsafe-inline` を含まない Content-Security-Policy の下でページを動かしてください。そうすればどちらも実行されません([docs/csp.ja.md](../../docs/csp.ja.md))。
3115
+
3116
+ **SSR の出力のコメントは残してください。** `@wcstack/server` の出力では、テキストバインディングと、行や枝のマーカーはコメントです。後段でそれを取り除く(html-minifier の `removeComments` など)とハイドレーションが壊れ、さらにサーバーで描画された値の中の `{{ }}` がクライアントでバインディングとして読まれることがあります。
2884
3117
 
2885
3118
  ## 設定
2886
3119
 
3120
+ 設定は、何を決めるかで分かれています:
3121
+
3122
+ | どこで | 何を決めるか | いつ読まれるか |
3123
+ |---|---|---|
3124
+ | `bootstrapState({ … })` | このページでのマークアップの綴り —— タグ名、バインディング属性、アンカーのコメント —— と既定のロケール | 1 回だけ、ページ全体で、要素が定義されるとき |
3125
+ | state の中の `$behavior` | その state ツリーの振る舞い —— `{{ }}`、同値ガード、方向認識の初期同期 | その state が読み込まれるとき |
3126
+ | state の中の `$features`(と、分割版の auto エントリでのルートの `features=`) | その state が必要とするアドオン | その state が読み込まれるとき |
3127
+
3128
+ ### `bootstrapState()` のオプション
3129
+
2887
3130
  `bootstrapState()` に部分的な設定オブジェクトを渡します:
2888
3131
 
2889
3132
  ```javascript
@@ -2903,20 +3146,49 @@ bootstrapState({
2903
3146
  | `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
2904
3147
  | `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
2905
3148
  | `tagNames.ssr` | `'wcs-ssr'` | SSR ハイドレーションデータ要素のタグ名 |
2906
- | `locale` | `<html lang>`、無ければ `'en'` | ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)のロケール — [ロケール](#ロケール)を参照 |
2907
- | `debug` | `false` | デバッグモード。4.0 で外れる(3.5 は警告) |
2908
- | `enableMustache` | `true` | `{{ }}` 構文の有効化。4.0 で state の `$behavior` へ移る(3.5 は警告) |
2909
- | `enableDirectionalInitialSync` | `true` | 方向認識のバインディング authority(`#init=` / `#sync=` バインド modifier)— [バインディング authority](#バインディング-authority-init--sync) 参照。既定 on。`false` で opt-out。4.0 で state の `$behavior` へ移る(3.5 は警告) |
2910
- | `enablePropagationContext` | `true` | バインド間の因果伝播トラッキング(echo/diamond のループ防止)。既定 on。`false` で opt-out。4.0 で外れる(3.5 は警告) |
3149
+ | `commentForPrefix` | `'wcs-for'` | `for:` テンプレートが自分の位置にアンカーとして残すコメントのテキスト |
3150
+ | `commentIfPrefix` / `commentElseIfPrefix` / `commentElsePrefix` | `'wcs-if'` / `'wcs-elseif'` / `'wcs-else'` | `if:` / `elseif:` / `else:` の連なりが残すアンカーコメントのテキスト |
3151
+ | `locale` | `<html lang>`(モジュールの評価時に読む)、無ければ `'en'` | ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)のロケール — [ロケール](#ロケール)を参照 |
2911
3152
  | `enableContractAnalyzer` | `false` | opt-in の開発時 contract analyzer(`analyzeContract` を公開) |
2912
- | `sameValueGuard` | `true` | 現在値と `Object.is` で同値なプリミティブ書き込みを enqueue 前に落とす — バインディングと `$watch` は実質「変化時のみ」発火する(参照型は常に通す)。`false` で同値書き込みを通し、`$watch` の `prev` は `undefined` になる。4.0 で state の `$behavior` へ移る(3.5 は警告) |
2913
3153
 
2914
- 4.0 は `debug`・`commentTextPrefix`・`enablePropagationContext` を外し、`enableMustache`・`enableDirectionalInitialSync`・`sameValueGuard` を state の `$behavior` へ移します。3.5 はページがこれらを渡すと警告します — [4.0 への準備](#40-への準備wcsv4-migration)を参照。
3154
+ `bootstrapState()` は、持たないオプション —— 4.0 が `$behavior` へ移したもの・削除したものも含む ——、既定値と型の違う値(`null`、オブジェクトが要る場所の配列)、`tagNames` の下の `state` / `ssr` 以外の名前、文字列でないタグ名で throw します(`#44`)。どれかを適用する前にすべてのオプションを検査するので、throw したときは何も適用されていません。`undefined` の値は読み飛ばされます(`bootstrapState({ locale: maybeLocale })`)。
3155
+
3156
+ ```
3157
+ [@wcstack/state] bootstrapState: "enableMustache" is not one of its options, or not of the option's type. 4.0 moved it to the state's $behavior.
3158
+ ```
3159
+
3160
+ すべてのパッケージの `bootstrapXxx()` —— `bootstrapRouter`・`bootstrapFetch`・`bootstrapAutoloader` … —— が同じ規則に従います。`/auto` のエントリを読み込むだけのページはこれらをオプションなしで呼ぶので、既定値のままで、ロケールは `<html lang>` で設定します。`getConfig()` は現在の設定(読み取り専用)を返します。
3161
+
3162
+ ### `$behavior`
3163
+
3164
+ state ツリーの振る舞いは、ページではなくその state が宣言します:
3165
+
3166
+ ```javascript
3167
+ export default {
3168
+ $behavior: { enableMustache: false, sameValueGuard: false },
3169
+ count: 0,
3170
+ };
3171
+ ```
3172
+
3173
+ | キー | デフォルト | 説明 |
3174
+ |---|---|---|
3175
+ | `enableMustache` | `true` | テキストの中の `{{ }}` はバインディング([Mustache 構文](#mustache-構文))。`false` なら波括弧はテキストのまま残る。コメントバインディングはどちらでもバインドされる |
3176
+ | `sameValueGuard` | `true` | 現在値と `Object.is` で同値なプリミティブ書き込みを enqueue 前に落とす — バインディングと `$watch` は実質「変化時のみ」発火する(参照型は常に通す)。`false` で同値書き込みを通し、`$watch` の `prev` は `undefined` になる |
3177
+ | `enableDirectionalInitialSync` | `true` | 方向認識のバインディング authority(`#init=` / `#sync=` バインド modifier)— [バインディング authority](#バインディング-authority-init--sync) 参照。`false` で opt-out: `#init=` / `#sync=` は throw し、すべての初期同期で state が勝つ |
3178
+
3179
+ - 各キーは boolean です。未知のキー、boolean でない値、オブジェクトでない `$behavior`(`null` と配列も含む)は throw します(`#44`)。
3180
+ - 宣言した `<wcs-state>` のツリーに、そのボリュームも含めて適用されます。**ルートの state はそれぞれ自分で宣言します** —— ページのルート、shadow root の中のルートの `<wcs-state>`、マウントされたコンポーネント、DCC: ホストから継承されるものはありません。ボリュームは宣言できません([`mount=`](#追加の状態をマウントするmount))。
3181
+ - 再設定(初期化済みの要素への `setInitialState()`)でこれを変えることはできません: 変えると `#45` を throw します —— 要素を作り直してください。`$behavior` を持たない再設定の state は既定値と比べられるので、宣言を繰り返し書いてください。
3182
+ - `/auto` のページでも JSON の state(`"$behavior": { "enableMustache": false }`)でも使えます。SSR ではサーバーが同じ state を読むので、サーバーとクライアントの振る舞いは一致します。
3183
+
3184
+ ### `$features`
3185
+
3186
+ `$features: ["temporal", "formats"]` は、その state が必要とするアドオンを名指します。分割版の auto エントリは、state を組み立てる前に足りないものを読み込みます。それ以外のエントリは入っているかを確かめ、入っていなければその state を `[wcs/feature-not-installed]` で失敗させます。配列でない値は `#46` を throw し、ボリュームは宣言できません。名前と、分割版の auto エントリがどの state よりも先に読むルートの `features=` 属性については [分割版の auto エントリ](#分割版の-auto-エントリ) を参照してください。
2915
3187
 
2916
3188
  ### ロケール
2917
3189
 
2918
- ロケールで書式化するフィルタは 4 つある — `locale` / `date` / `time` / `datetime`。
2919
- これらは `config.locale` を読み、その既定は **`<html lang>`** である。
3190
+ ロケールで書式化するフィルタは 4 つある — `locale` / `date` / `time` / `datetime`(`formats` アドオン)。
3191
+ これらはページのロケール(`getConfig().locale`)を読み、その既定は **`<html lang>`** である。
2920
3192
 
2921
3193
  ```html
2922
3194
  <html lang="ja-JP">
@@ -2924,12 +3196,13 @@ bootstrapState({
2924
3196
  ```
2925
3197
 
2926
3198
  他に何も要らない。`<html lang>` はページの言語を書く HTML 標準の場所であり、
2927
- そこを既定にすればロケールの正本が 1 つで済む。同時に、**CDN 一発のページが
2928
- ロケールを設定できるようになる** — `auto` は `bootstrapState()` を引数なしで呼ぶので、
2929
- これが無いと渡す口が無かった。明示指定(`bootstrapState({ locale })`)は常に優先し、
2930
- 不正な BCP-47 タグは `Intl` の中で落ちる前に警告して無視する。
3199
+ そこを既定にすればロケールの正本が 1 つで済む。同時に、オプションを取らない CDN 一発の
3200
+ ページもロケールを設定できる。`<html lang>` はモジュールの評価時に読まれるので、
3201
+ マークアップに書くこと。明示指定(`bootstrapState({ locale })`)は常に優先する。
3202
+ `Intl` が受け付けないタグ(`<html lang="en_US">`)は、`Intl` の中で throw させる代わりに
3203
+ 1 回報告し(`#48`)、フィルタは `"en"` を使う。`getConfig().locale` は設定された値を返し続ける。
2931
3204
 
2932
- **`config.locale` を後から変えても何も再描画されない。** これは state ではなく
3205
+ **ロケールを後から変えても何も再描画されない。** これは state ではなく
2933
3206
  グローバル設定なので、依存グラフに載らない。フィルタ自体はバインド構築時に
2934
3207
  取り込むのではなく**適用のたびに読む**ので、別の理由で再描画されたバインドは新しい値を
2935
3208
  拾う — 起動順序の事故から復帰するには足りるが、ページの言語を切り替えるには足りない。
@@ -2956,21 +3229,18 @@ router 自身が `<html lang>` を読む案と、リンク単位の intercept
2956
3229
  記録として残した。別の形を選ぶ前に [docs/i18n-design.md](../../docs/i18n-design.md)
2957
3230
  §9-1 を読むこと。`examples/router-i18n` が参照レイアウト。
2958
3231
 
2959
- > この 3 つは **architecture-hardening** 機能で、規範は `docs/architecture-hardening/` に
2960
- > あります。`enablePropagationContext` は**既定 on** — write-path コストは一方向バインドで
2961
- > ほぼゼロ(echo しうる双方向 wire のみ因果 bookkeeping を行う)で、フラグは恒久的な
2962
- > opt-out として残します。`enableDirectionalInitialSync` も**既定 on**: プロパティ単位で
2963
- > 初期同期の authority を割り当てます(output-only な `wcBindable` メンバは初期値を
2964
- > element→state で読み取り、双方向 / input メンバは state→element を維持)。setup-path
2965
- > コストは初期 render の 5% 未満(producer-value observer は echo しうる双方向 wire にのみ
2966
- > 登録)で、フラグは恒久的な opt-out として残します。`enableContractAnalyzer` は opt-in
3232
+ > `$behavior.enableDirectionalInitialSync` と `enableContractAnalyzer` は **architecture-hardening** 機能で、
3233
+ > 規範は `docs/architecture-hardening/` にあります。`enableDirectionalInitialSync` は**既定 on**:
3234
+ > プロパティ単位で初期同期の authority を割り当て(output-only な `wcBindable` メンバは初期値を
3235
+ > element→state で読み取り、双方向 / input メンバは state→element を維持)、このキーは恒久的な
3236
+ > opt-out として残します。`enableContractAnalyzer` は opt-in
2967
3237
  > (既定 `false`・無効時ランタイムコストゼロ)で、有効な場合、公開 API `analyzeContract()`
2968
3238
  > が稼働中の `static wcBindable` サーフェスと sidecar manifest の drift を開発時診断として
2969
3239
  > 報告します。
2970
3240
 
2971
3241
  ## ページをテストする
2972
3242
 
2973
- `<wcs-state>` で組んだページは素の DOM なので、[happy-dom](https://github.com/capricorn86/happy-dom) でヘッドレスにテストできます — ブラウザ不要・ビルド不要・テスト専用 API 不要。レシピは 3 つ、いずれも書いてあるとおりに動きます(レシピ 1 は同じ行を実行する [`__tests__/readme.testingRecipe.test.ts`](__tests__/readme.testingRecipe.test.ts) で固定しています)。
3243
+ `<wcs-state>` で組んだページは素の DOM なので、[happy-dom](https://github.com/capricorn86/happy-dom) でヘッドレスにテストできます — ブラウザ不要・ビルド不要・テスト専用 API 不要。レシピは 3 つ、いずれも書いてあるとおりに動きます。
2974
3244
 
2975
3245
  1 import で済ませたいなら [`@wcstack/testing`](../testing/README.ja.md) がレシピ 1 を `mount()` / `settle()` / `fire()` にまとめています(`<wcs-router>` も待ちます)。以下の素のレシピはそれ無しでも有効です。
2976
3246
 
@@ -3039,11 +3309,10 @@ it("描画・再描画・ハンドラ実行", async () => {
3039
3309
 
3040
3310
  ユーザー操作と同じ経路で動かすなら、state はインライン(メソッド込み)のまま DOM イベントを発火します。`data-wcs="onclick: up"` のハンドラは `button.click()` で走り、`settle()` 1 回の後に DOM へ反映されます。
3041
3311
 
3042
- - `getBindingsReady(root)` は `root`(`document` か shadow root)配下の全バインド構築が終わると resolve し、バインド初期化に失敗すると reject します(v1.26+)。その root のルート `<wcs-state>` が初期化に失敗した場合も reject します —— ロードされなかったルートを「ready」と報告しません。
3312
+ - `getBindingsReady(root)` は `root`(`document` か shadow root)配下のバインド構築が終わると resolve し、その root のルート `<wcs-state>` が初期化に失敗した場合は reject します —— ロードされなかったルートを「ready」と報告せず、失敗を報告します([初期化の失敗](#初期化の失敗))。`$connectedCallback` は待たないので、それには `connectedCallbackPromise` を await してください。
3043
3313
  - 更新はマイクロタスク境界で収束します。書き込み後の `setTimeout(0)` 1 回で十分です。
3044
3314
  - `state.items = [...state.items, "cherry"]` がリアクティブな書き方です — `state.items.push()` は観測されません(ハンドラ内と同じ規則)。
3045
3315
  - happy-dom は `customElements.define` 時に既存ノードを**差し替えて**アップグレードします。「遅れて define された同一ノードに値が届く」はヘッドレスでは検証できません。happy-dom と実ブラウザのイベントタイミング差ももう 1 つの死角なので、そこは実ブラウザ e2e(Playwright)を 1 本残してください。
3046
- - happy-dom の `textContent` setter は数値 `0` を空文字にします(ブラウザは `"0"`)。このレシピでは `textContent: count` のバインドが 0 のとき `""` に読めます。state の値で assert するか、setter をシムする `@wcstack/testing` の `mount()` を使ってください。
3047
3316
 
3048
3317
  ### 2. 素の Node(vitest なし)
3049
3318
 
@@ -3086,10 +3355,10 @@ it("描画結果がスナップショットと一致する", async () => {
3086
3355
 
3087
3356
  ## TypeScript サポート
3088
3357
 
3089
- `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
3358
+ `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。**`@wcstack/state/define`** から import してください。`defineState` と型だけを持ち、ほかには何も持ちません(69 B):
3090
3359
 
3091
3360
  ```typescript
3092
- import { defineState } from '@wcstack/state';
3361
+ import { defineState } from '@wcstack/state/define';
3093
3362
 
3094
3363
  export default defineState({
3095
3364
  count: 0,
@@ -3107,6 +3376,8 @@ export default defineState({
3107
3376
  });
3108
3377
  ```
3109
3378
 
3379
+ `@wcstack/state` と `/core` も `defineState` を export しますが、バンドラはそれらの入口からエンジンを落とせません: `defineState` を `@wcstack/state` から import した state ファイルは、バンドルにエンジン全体(gzip で約 21 KB)を残します。どのみちエンジンを動かすバンドルでは害はありませんが、state ファイルだけを束ねるところ(ランタイムを CDN から読む、型検査やテストのビルド)では無駄になるので、`/define` を使ってください。
3380
+
3110
3381
  ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
3111
3382
 
3112
3383
  `defineState()` が型を付けるのは state ファイルです。その型を HTML まで運ぶのが [`@wcstack/typescript`](../typescript/README.ja.md) の 2 つの CLI で、`wcs-schema` は `@wcstack/lint` と VS Code 拡張が `data-wcs` パスの検証に使う `stateSchema` sidecar を書き出し、`wcs-tsc` はインラインの `<script type="module">` state に TypeScript コンパイラを掛けます。全体像は [docs/typescript.ja.md](../../docs/typescript.ja.md)。
@@ -3115,136 +3386,98 @@ export default defineState({
3115
3386
 
3116
3387
  ### `bootstrapState()`
3117
3388
 
3118
- 状態システムを初期化します。`<wcs-state>` カスタム要素を登録し、DOM コンテンツ読み込みハンドラを設定します。
3119
-
3120
3389
  ```javascript
3121
3390
  import { bootstrapState } from '@wcstack/state';
3122
- bootstrapState();
3391
+ bootstrapState(config?, registry?);
3123
3392
  ```
3124
3393
 
3394
+ すべてのアドオンを入れ(`@wcstack/state` の場合。`/core` エントリの `bootstrapState()` は何も入れないので、先に `installFeatures([...])` を呼ぶ)、`config` を適用し([`bootstrapState()` のオプション](#bootstrapstate-のオプション))、`<wcs-state>` —— と、入っているアドオンのタグ `<wcs-ssr>` —— を `registry` に登録します。`registry` の既定はグローバルの `customElements` で、スコープ付きの `CustomElementRegistry` も渡せます(グローバルのレジストリの定義は継承しません)。`/auto` のエントリは、オプションなしで同じことをします。
3395
+
3125
3396
  ### その他のエクスポート
3126
3397
 
3127
3398
  | エクスポート | 説明 |
3128
3399
  |---|---|
3129
- | `getBindingsReady(root)` | `root`(`document` または shadow root)配下の全バインディングが構築されたら解決。バインディング初期化が失敗した場合、およびその root のルート state 要素が初期化に失敗した場合は reject |
3130
- | `buildBindings(root)` | `document` / `ShadowRoot` 配下のバインディングを明示的に構築する — その root に最初に登録された `<wcs-state>` がスケジュールするもの |
3400
+ | `getBindingsReady(root)` | `root`(`document` または shadow root)配下のバインディングが構築されたら、`$connectedCallback` を待たずに解決。その root のルート `<wcs-state>` が初期化に失敗した場合は reject — [初期化の失敗](#初期化の失敗) 参照 |
3401
+ | `buildBindings(root)` | `getBindingsReady(root)` と同じく、`document` / `ShadowRoot` 配下のバインディングを待つ: root のバインディングは、その `<wcs-state>` が state を読み込んだときに構築される |
3131
3402
  | `getConfig()` | 現在の設定(読み取り専用ビュー) |
3132
- | `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照 |
3403
+ | `defineState(obj)` | メソッドと getter 内の `this` に型を付けるアイデンティティ関数 — [TypeScript サポート](#typescript-サポート) 参照。state ファイルでは `@wcstack/state/define` から import する |
3404
+ | `setTrustedTypesPolicy(policy)` / `getTrustedTypesPolicy()` / `TRUSTED_TYPES_POLICY_SLOT` | HTML のシンクが使う Trusted Types のポリシー — [セキュリティ](#セキュリティ) と [docs/csp.ja.md §7](../../docs/csp.ja.md) 参照 |
3133
3405
  | `VERSION` | パッケージのバージョン。`<wcs-ssr>` に刻印され、ハイドレーション時に照合される |
3134
- | `getWcsManifest()` / `WCS_MANIFEST_VERSION` | バインディング構文・組み込みフィルタ・予約名の機械可読 manifest — 実装から導出され、`@wcstack/lint` と VS Code 拡張が消費する |
3406
+ | `Ssr` | `<wcs-ssr>` 要素のクラス(`ISsrElement`: `version`・`stateData`・`templates`・`getTemplate()`・`verifyVersion()`。`hydrateProps` は常に空) |
3407
+ | `getWcsManifest()` / `WCS_MANIFEST_VERSION` | バインディング構文・組み込みフィルタ・予約名・`$behavior` のオプション(`behaviorOptions`)・アドオン名(`features`)の機械可読 manifest — 実装から導出され、`@wcstack/lint` と VS Code 拡張が消費する |
3135
3408
  | `builtinFilterMeta` | 全組み込みフィルタの引数・戻り値メタデータ |
3409
+ | `builtinFilterAliases` | それを読むツールのために残してある。中身は空(4.0 にフィルタの別名はない) |
3136
3410
  | `analyzeContract()` | 開発時の contract analyzer。`enableContractAnalyzer` が off なら no-op |
3137
3411
 
3138
- ツール向けのサブパスエントリ: `@wcstack/state/parser`(`data-wcs` パーサを DOM 非依存の純関数として公開)、`@wcstack/state/manifest`、`@wcstack/state/wcs-manifest.json`(manifest をビルド済み JSON として公開)。
3412
+ `@wcstack/state/core` は `bootstrapState`・`installFeatures`・`getBindingsReady`・`buildBindings`・`getConfig`・Trusted Types の関数・`defineState`・`VERSION` を export し、各 `@wcstack/state/features/<name>` は自分のアドオンを default export します。ツール向けのサブパスエントリ: `@wcstack/state/parser`(`data-wcs` パーサを DOM 非依存の純関数として公開)、`@wcstack/state/manifest`、`@wcstack/state/wcs-manifest.json`(manifest をビルド済み JSON として公開)。型 `IStateElement` も export されます。
3139
3413
 
3140
3414
  ### `<wcs-state>` 要素
3141
3415
 
3142
3416
  | 属性 | 説明 |
3143
3417
  |---|---|
3144
3418
  | `mount` | この state をルートツリーへ**ボリューム**として接ぎ木する静的ツリーパス(v2 — 撤去された `name` 属性の後継。ツリーは 1 root に 1 本) |
3145
- | `state` | document 内の `<script type="application/json">` 要素の ID(`document.getElementById` で探すので、shadow root の中の script は見つかりません) |
3419
+ | `state` | `<script type="application/json">` の ID — まず要素自身のルート(shadow root)で、次に document で探す。その id の JSON script だけが対象 |
3146
3420
  | `src` | `.json` または `.js` ファイルの URL |
3147
3421
  | `json` | インライン JSON 文字列 |
3148
3422
  | `bind-component` | Web Component バインディングのプロパティ名 |
3149
3423
  | `enable-ssr` | SSR を有効化: サーバーはこの state の `<wcs-ssr>` ハイドレーションデータを出力し、クライアントは再描画せずそこから復元する — [サーバーサイドレンダリング](#サーバーサイドレンダリング) 参照 |
3424
+ | `features` | 文書のルートの `<wcs-state>` に書き、分割版の auto エントリだけが読む: `<wcs-state>` を定義する前に読み込むアドオン — [分割版の auto エントリ](#分割版の-auto-エントリ) 参照 |
3150
3425
 
3151
3426
  ### IStateElement
3152
3427
 
3153
3428
  | プロパティ / メソッド | 説明 |
3154
3429
  |---|---|
3155
- | `initializePromise` | 状態の完全な初期化時に解決される Promise —— **初期化に失敗したときも解決**します(1 要素の失敗がページの他のバインディングを止めないため)。エラーは `connectedCallbackPromise` に届きます |
3156
- | `connectedCallbackPromise` | `connectedCallback` の完了(state のロードと `$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの。**ルート**要素が初期化に失敗すると、**元のエラーのまま reject** し、`console.error` にも 1 件報告します(`$` 宣言の不正・ソースのロード失敗・SSR データの merge 失敗・DCC や `bind-component` の設定エラー・同じ root node に 2 本目のルート `<wcs-state>`。2 本目は登録されないまま読み込んだ state を保持するので取り除いてください。健全な要素の DOM 移動は二重登録ではなく、拒否しません)。ただし 3.x では次の 2 つのソースを失敗として扱わず、この Promise を解決します: 取得またはパースできない `src="*.json"`(URL と、HTTP ステータスまたはエラーを `console.error` に記録し、空の state で始まります。ボリュームなら `{}` を接ぎ木します)と、その id の `<script type="application/json">` が document に無い `state="<id>"`(`console.warn` を 1 回出し、空の state で始まります。探すのは `document.getElementById` なので、shadow root の中の script は見つかりません)。4.0 はどちらの場合も reject します。`state=` については要素自身のルートを先に、次に document を探し、どちらにも無いときだけ reject します。**ボリューム**(`<wcs-state mount="…">`)はこの Promise を**拒否しません** —— ボリュームの失敗は解決し、種類によっては自分では何も報告しません。その場合エラーはカスタム要素リアクションが捨てる `connectedCallback` の戻り Promise として出ていき、ブラウザのコンソールには "Uncaught (in promise)" と出ますが、promise を待つ側(テストのレシピや `renderToString()`)には届きません。ロード中に切断された要素は reject しません —— その接続が黙って終わるだけで、付け直せば(行プール)通常どおり初期化して解決します。個々の失敗箇所の正確な挙動は `__tests__/integration.initFailureDiagnostics.test.ts` が固定しています |
3157
- | `listPaths` | `for` ループで使用されるパスの Set |
3158
- | `getterPaths` | getter として定義されたパスの Set |
3159
- | `setterPaths` | setter として定義されたパスの Set |
3160
- | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
3161
- | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
3162
- | `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$renderedCallback` も呼びません。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再セットは非対応です)。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素では throw します —— 失敗した要素は再武装できないので、取り除いて作り直してください |
3163
- | `nextVersion()` | バージョン番号をインクリメントして返す |
3430
+ | `initializePromise` | state が読み込まれ、バインディングが構築されたら解決される Promise —— **初期化に失敗したときも解決**します(1 要素の失敗がページの他のバインディングを止めないため)。エラーは `connectedCallbackPromise` に届きます |
3431
+ | `connectedCallbackPromise` | `connectedCallback` の完了(state のロード、バインディングの構築、`$connectedCallback` の実行)で解決される Promise — テストのレシピが await するもの。初期化に失敗したルート要素は、**元のエラーのまま reject** します。`$connectedCallback` が失敗した要素、失敗した DCC の定義やコンポーネントのマウントも同じです。ボリュームは常に解決します。[初期化の失敗](#初期化の失敗) 参照 |
3432
+ | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`)。state が読み込まれる前は `state is not initialized`(`#15`)を throw |
3433
+ | `createStateAsync(mutability, callback)` | `createState` の非同期版。readonly のプロキシはコールバックの `await` をまたいでも readonly のままで、`$setAll` と `$resolve(path, indexes, value)` の書き込みも拒否する |
3434
+ | `setInitialState(state)` | プログラムから状態を設定 — 下記参照 |
3435
+
3436
+ `setInitialState(state)` は、初期化前なら初期 state を渡します。初期化済みの要素では state 全体を入れ替え、戻る前にすべてのバインディングを新しい state で適用し直します。再設定は書き込みではないので、`$watch` のハンドラも `$renderedCallback` も呼びません。新しい state に無いパスは、深さによらず `undefined` と読みます。そこへのバインディングは空になり(要素の入力は、ほかの `undefined` の値と同じく自分の値を保ちます)、新しい state にキーが無いリストは行を描かず、それを読む getter は `undefined` を得ます。古い state にあったトップレベルのキーは、書き込まれるまでこのままです。どの state にも無かったキーは、どこでも同じく読むと失敗します(`#301`)。この扱いは要素の側で覚えていて、新しい state のオブジェクトには書き込まないので、凍結した state(Immer・Redux Toolkit)でも再設定できます。例外が 1 つあり、クラスの state のプロトタイプにある getter は列挙されないので、新しい state にそれが無いと、そこへのバインディングは適用に失敗します(報告され、テキストは元のまま)。読めないリスト(getter が throw する)はその `for` の失敗として報告され、再設定は続行します。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再設定は非対応です)。新しい state の `$behavior` が違う場合(`#45`)や、宣言の形が誤っている場合は throw し、古い state をそのまま残します。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、コンポーネント(`bind-component`)や DCC の定義、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素(`#14`)では throw します —— 失敗した要素は再始動できないので、取り除いて作り直してください。バインディングの構築後に `$connectedCallback` が失敗したルートは再設定できます。再設定の後も、`Object.keys(this)`・`in`・`delete`・`JSON.stringify(this)` は古い state オブジェクトを見ます(このバージョンの既知の制約)。
3164
3437
 
3165
3438
  ## アーキテクチャ
3166
3439
 
3167
3440
  ```
3168
- bootstrapState()
3169
- └── registerComponents() // <wcs-state> カスタム要素を登録
3441
+ bootstrapState(config?, registry?)
3442
+ ├── installFeatures(全アドオン) // @wcstack/state と /auto。/core は何も入れない
3443
+ ├── setConfig(config) // まずすべてのオプションを検査し、それから適用
3444
+ └── define(registry) // <wcs-state>(+ アドオンのタグ)、バインダ
3170
3445
 
3171
3446
  <wcs-state> connectedCallback
3172
- ├── 置かれ方によりいずれか 1 つ:
3173
- │ ├── _initializeDCC() // data-wc-definition ホスト配下: DCC クラスを定義
3174
- │ ├── _initializeVolume() // mount=: このボリュームをルートツリーへ接ぎ木
3175
- │ ├── _initializeBindWebComponent() // bind-component: ホストのツリーをマウント点でエイリアス
3176
- │ └── _initialize() // ルート: 状態をロード (state属性 / src / json / script / API)
3177
- │ └── setStateElement() // WeakMap<Node, IStateElement> に登録 — 1 root 1 ツリー
3178
- │ └── (rootNode への初回登録時)
3179
- │ └── queueMicrotask → buildBindings()
3180
- ├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
3181
-
3182
- buildBindings(root)
3183
- ├── waitForStateInitialize() // 全 <wcs-state> の initializePromise を待機
3184
- ├── convertMustacheToComments() // {{ }} → コメントノードに変換
3185
- ├── collectStructuralFragments() // for/if テンプレートを収集
3186
- └── initializeBindings() // DOM 走査、data-wcs 解析、バインディング設定
3447
+ ├── アドオン(scopes)が引き受ける — ルートではない:
3448
+ │ ├── ボリューム(mount=) // その state をマウントパスの位置でルートツリーへ接ぎ木
3449
+ │ ├── DCC の定義 // タグを定義。各インスタンスは自分のルートを持つ
3450
+ │ └── コンポーネント(bind-component)// 自前のエンジン。ホストのエンジンに配線される
3451
+ └── ルート:
3452
+ ├── state を読み込む // state= / src= / json= / インラインスクリプト / setInitialState()
3453
+ ├── $features を読み込む/検査する
3454
+ ├── new Engine(state) // $behavior・宣言・アクセサ
3455
+ ├── mount(engine, root) // ルートを 1 回走査: プラン・ビュー・バインディング —— そして描画
3456
+ ├── バインディング構築完了 // initializePromise、getBindingsReady()
3457
+ └── $connectedCallback // その後に connectedCallbackPromise
3187
3458
  ```
3188
3459
 
3189
3460
  ### リアクティビティフロー
3190
3461
 
3191
- 1. Proxy の `set` トラップによる状態変更 → `setByAddress()`
3192
- 2. アドレス解決 → updater が絶対アドレスをキューに登録
3193
- 3. 依存関係ウォーカーが下流のキャッシュを無効化(dirty)
3194
- 4. updater が `applyChangeFromBindings()` によりバインド済み DOM ノードに変更を適用
3462
+ 1. state proxy を通した書き込み(`this["user.name"] = …`)が、パスをパターンと行に解決し、データを書き込みます。
3463
+ 2. 依存の走査が、それを読む getter の出現を dirty にし、そのパスとその下のバインディングをキューに入れます。
3464
+ 3. microtask で drain がリストを同期し直し、行を描画し、キューに入ったバインディングを適用します —— ページに view-transition の arbiter があれば、それを通して。
3465
+ 4. drain の後: `$renderedCallback`、バインディングの失敗、それからアドオン(`$watch` のハンドラ、`$stream` の restart)。
3195
3466
 
3196
3467
  ### 状態アドレスシステム
3197
3468
 
3198
- `users.*.name` のようなパスは以下に分解されます:
3469
+ `users.*.name` のようなパスは 2 つの部分でアドレスされます:
3470
+
3471
+ - **パターン** — `*` で綴ったパス(`users.*.name`)ごとに 1 つ: そのセグメント、深さ(`*` の数)、親、getter や setter、それを読む getter、ルートでそのパスに登録されたバインディング。
3472
+ - **リストと行** — `for:`・`$getAll`・`$watch` が読む配列の値ごとにリストが 1 つ、その要素ごとに行が 1 つ: 要素、その位置、getter のキャッシュ、その中のバインディングと入れ子のリスト。具体パス(`users.0.name`)はパターンと行の組で、行を捨てるとそこにぶら下がるものはすべて捨てられます。
3199
3473
 
3200
- - **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
3201
- - **ListIndex** — ランタイムループインデックスチェーン
3202
- - **StateAddress** — PathInfo + ListIndex の組み合わせ
3203
- - **TreePath / AbsoluteStateAddress** — ツリーを持つ state 要素に固定した PathInfo と、その ListIndex の組。マウントされたコンポーネントとボリュームは相対パスをこの層でホストのツリーへ翻訳する。v2 は 1 root 1 ツリーなので、アドレスに状態名はない
3474
+ マウントされたコンポーネントは自前のエンジンを動かし、そのマウントされたキーはホストのエンジンのパスを読み書きします。ボリュームの getter・setter・メソッドは、マウントパスの位置でルートツリーのアクセサになります。ツリーは 1 root に 1 本なので、アドレスに状態名はありません。
3204
3475
 
3205
3476
  ## パフォーマンス
3206
3477
 
3207
- リポジトリ同梱の [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) 流
3208
- ドライバ(`e2e/bench/jsfb-verify.mjs`・`e2e/bench/memory-profile.mjs`)で、標準の
3209
- 1,000 / 10,000 行テーブルページを計測(headless Chromium・中央値・両実装を同一
3210
- セッションで連続実行)。`@wcstack/state` は公式の keyed 判定に合格しつつ、行 DOM
3211
- を上限 1,000 行の有界プールでリサイクルします。
3478
+ 4.0 のエンジンは書き直しで、操作ごとの数値はまだリポジトリのドライバで記録していません: リリースの一部として 4.0 のビルドで計測します。開発中、公式の [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) で計測した 4.0 エンジンの CPU 加重幾何平均は 1.08〜1.11 で、[`@wcstack/signals`](../signals/) は 1.21〜1.25、`@wcstack/state` 3.3.0 は 1.51 でした(小さいほど速い)。
3212
3479
 
3213
- | 所要時間(ms・中央値) | `@wcstack/state` | [`@wcstack/signals`](../signals/) |
3214
- |---|---|---|
3215
- | 1,000 行の生成 | 25.2 | 9.5 |
3216
- | 1,000 行の全置換 | 18.8 | 12.5 |
3217
- | 10,000 行の 10 行ごと更新 | 11.4 | 4.7 |
3218
- | 行の選択 | 0.1 | 0.4 |
3219
- | 2 行の入れ替え | 0.9 | 0.4 |
3220
- | 行の削除 | 2.8 | 0.6 |
3221
- | 10,000 行への 1,000 行追加 | 48.6 | 14.2 |
3222
- | 10,000 行のクリア | 54.6 | 52.2 |
3223
-
3224
- | 強制 GC 後のヒープ(MB) | `@wcstack/state` | `@wcstack/signals` |
3225
- |---|---|---|
3226
- | ページ表示直後 | 1.0 | 0.6 |
3227
- | 1,000 行生成後 | 5.6 | 3.5 |
3228
- | 1,000 行×5 回置換後 | 6.4 | 3.7 |
3229
- | 10,000 行生成後 | 35.1 | 18.0 |
3230
- | 10,000 行生成 + クリア後 | 13.2 | 1.9 |
3231
-
3232
- 正直な読み方:
3233
-
3234
- - 対話系の操作(選択 / 入れ替え / 削除)は数ミリ秒以下で、巨大リストのクリアは
3235
- signals 実装と同等です。
3236
- - 行の生成・追加は `@wcstack/signals` の約 2.5〜3.5 倍のコストです。これは本
3237
- パッケージが行ごとに構築する宣言的バインディング台帳の対価で、`data-wcs` の
3238
- 検査・DevTools の配線表示・SSR ハイドレーションを支えているのは同じ台帳です。
3239
- 両パッケージは相互運用できるため、ホットなリストだけ signals の `For` で描画
3240
- し、残りのページは宣言的なまま保てます。
3241
- - クリア後に残るヒープは、次のリスト生成を安くする有界の行プールです。
3242
-
3243
- 絶対値は 1 台の開発機で v1.21.6 + PR#87(clear リーク修正)時点に取ったものです。
3244
- v2.0 のマウント作業は同じドライバの同一セッション A/B でゲートし、実行ごとの
3245
- ノイズ内に収まった([docs/state-mount-impl-plan.md](../../docs/state-mount-impl-plan.md)
3246
- §2-2 と slice 27)ため、表は取り直していません。絶対値はマシン状態で ±20% 揺れます。
3247
- `e2e/bench/` のドライバで手元のハードウェアでも再現できます。
3480
+ 手元のハードウェアで比べるには、`e2e/bench/` のドライバ(`jsfb-verify.mjs`・`memory-profile.mjs`)を実行してください: 標準の 1,000 / 10,000 行テーブルページを headless Chromium で、両実装を同一セッションで連続して、中央値で計測します。絶対値はマシン状態で ±20% 揺れます。
3248
3481
 
3249
3482
  ## サーバーサイドレンダリング
3250
3483
 
@@ -3281,22 +3514,30 @@ const html = await renderToString(template, {
3281
3514
  });
3282
3515
  ```
3283
3516
 
3284
- これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し、再レンダリングなしでリアクティビティを再開します。
3517
+ これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を見つけ、そのスナップショットからサーバーのデータを受け取り、サーバーの DOM を描画し直さずにその場でバインドします。
3285
3518
 
3286
3519
  ### 仕組み
3287
3520
 
3288
3521
  | フェーズ | 動作 |
3289
3522
  |---------|------|
3290
- | **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
3291
- | **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
3292
- | **フォールバック** | サーバー/クライアントのバージョン不一致時、またはサーバーが描いた `for:` の行が別の `for:` / `if:` / `elseif:` / `else:` ブロックの中にあるとき(そのハイドレーションは既知の制限)、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行。どちらも理由を `console.warn` で 1 回知らせる |
3523
+ | **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用して、`<wcs-state>` の前に `<wcs-ssr>` 要素を置いたレンダリング済み HTML を出力。行・枝・テキストバインディングにはコメントで印を付ける。`$watch` のハンドラは実行されず、stream は `initial` の値のまま |
3524
+ | **クライアント** | `<wcs-state enable-ssr>` は自分のソースから state を読み込み、その上にスナップショットのデータを被せ(getter・setter・メソッドは残る)、`$connectedCallback` をスキップし、サーバーの DOM をその場でバインドする。行と枝はその場で引き継がれ —— その中のカスタム要素は 1 回だけ接続され、切断・再接続されることはない —— バインディングを得る。テンプレートに合わなくなったサーバーの行はその場所で描画し直され、クライアントが描画しない行や枝(行が少ない、別の枝)はページがバインドされた時点で取り除かれる。ボリュームはマウントパスの位置でサーバーのデータを引き継ぐ |
3525
+ | **バージョン不一致** | `<wcs-ssr version>` の major.minor が違うスナップショットは `console.warn` を 1 回出して破棄され、ページはサーバーが無かったかのようにクライアントで描画される: state は自分のソースから読み込まれ、`$connectedCallback` が走り、サーバーの行・枝・マーカーは、描画元のテンプレートに場所を譲る |
3293
3526
 
3294
3527
  ### `enable-ssr` の動作
3295
3528
 
3296
3529
  | コンテキスト | 動作 |
3297
3530
  |------------|------|
3298
- | **サーバー**(`renderToString`) | 状態 JSON、テンプレートフラグメント、プロパティデータを含む `<wcs-ssr>` を生成 |
3299
- | **クライアント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバインディングをハイドレート |
3531
+ | **サーバー**(`renderToString`) | バージョン、state のデータの JSON、ページレベルのテンプレートを含む `<wcs-ssr>` を生成 |
3532
+ | **クライアント**(ハイドレーション) | `<wcs-ssr>` を読み取り、データを復元、`$connectedCallback` をスキップ、サーバーの行と枝を引き継いでバインド |
3533
+
3534
+ 規則:
3535
+
3536
+ - **`@wcstack/server` とクライアントは一緒に、同じ major.minor でデプロイしてください。** 4.0 のクライアントは 3.x のサーバーの出力をクライアントで描画し直します(`<wcs-ssr version="3.5.0"> does not match 4.0.0: its snapshot is discarded, and the page renders on the client from its own state.`)—— ページは動きますが、サーバーがしたことはもう一度行われ、`$connectedCallback` が取得するデータももう一度取得されます。3.5.2 までの 3.x サーバーの出力では、テンプレートの外のテキストバインディング(ページレベルや Light DOM コンポーネントの中身の `{{ }}` や `<!--@@: -->`)はパスだけを保つのでフィルタを失い、警告がそう伝えます。3.5.3 からはマーカーが式全体を保ちますが、HTML のコメントに入れられない式(`|defaults('--')` のように `--` を含むもの)はパスだけを保ちます。切り替えるときは、3.x のサーバーからキャッシュした HTML を破棄してください。4.0 のサーバーと 3.x のクライアントの組み合わせは非対応です。詳細は [移行ガイド §3.6](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.ja.md) にあります。
3537
+ - **スナップショットは JSON です。** `Date`・`Set`・`Map`・クラスのインスタンスはそこを通り抜けられません: 文字列や素のオブジェクトとして届き、クライアントの値を置き換えます。SSR の state には JSON の値を持たせ、残りは getter で導出してください(`get created() { return new Date(this.createdIso); }`)。
3538
+ - **出力のコメントは残してください** —— 取り除くとハイドレーションが壊れます([セキュリティ](#セキュリティ))。マーカーに基づいて出力を後処理するのもやめてください: その形式は API ではありません。
3539
+ - `outerHTML:` / `outerText:` はサーバーではなくクライアントで適用されます: サーバーの出力には書いたとおりの要素が入るので、その値は、検索エンジンや JavaScript なしの表示が見る HTML にはありません。
3540
+ - ページレベルでバインドされた Light DOM のカスタム要素が値から描画する子は、サーバーの出力に含まれず、クライアントが描画します。自分で書いた子は出力に残ります。
3300
3541
 
3301
3542
  API の詳細は [`@wcstack/server` README](../server/README.ja.md) を参照してください。
3302
3543