@wcstack/state 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,3 +1,60 @@
1
+ // ===========================================================================
2
+ // AUTO-GENERATED FILE - DO NOT EDIT.
3
+ // Generated from /protocol/ssr-snapshot.ts by scripts/sync-protocol-types.mjs.
4
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
5
+ // ===========================================================================
6
+ // ssr-snapshot protocol — how the SSR renderer asks whoever owns reactive
7
+ // state to build hydration snapshots (<wcs-ssr>) as a final pass, after every
8
+ // DOM inserter (router route content, late custom elements) has settled.
9
+ //
10
+ // Without this, the snapshot is built inside <wcs-state>'s connectedCallback
11
+ // and races DOM inserted by other packages: whether a route's structural
12
+ // templates make it into the snapshot depends on document order and state's
13
+ // load mechanism (docs/ssr-router-design.md §5).
14
+ //
15
+ // The provider (@wcstack/state) installs itself on a well-known global symbol
16
+ // at bootstrap. The renderer (@wcstack/server) looks the builder up after
17
+ // running bootstraps: if present it announces orchestration by setting
18
+ // `data-wcs-server="orchestrated"` on the document element BEFORE parsing, and
19
+ // calls build() right before serialization. The provider keeps its inline
20
+ // per-element fallback whenever the attribute value is anything else, so:
21
+ // - old renderer + new provider -> inline build, yesterday's behavior
22
+ // - new renderer + old provider -> no builder found, attribute stays "",
23
+ // the old provider builds inline as before
24
+ // - new renderer + new provider -> orchestrated: snapshots are built last
25
+ // and therefore always see settled DOM
26
+ //
27
+ // The symbol (rather than a package import) also pins the builder to the state
28
+ // copy that actually runs on the page — its module-scoped fragment registries
29
+ // are the ones the snapshot must read.
30
+ //
31
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/ssr-snapshot.ts), then
32
+ // run `node scripts/sync-protocol-types.mjs` to regenerate the per-package
33
+ // copies (packages/<pkg>/src/protocol/ssrSnapshot.ts). Those copies are
34
+ // generated — do not edit them.
35
+ /**
36
+ * Global key the snapshot builder installs itself under. `Symbol.for` so
37
+ * independently loaded copies of this file (state's and server's) still agree.
38
+ */
39
+ const SSR_SNAPSHOT_BUILDER_KEY = Symbol.for("wcstack.ssr.snapshotBuilder");
40
+ /**
41
+ * `data-wcs-server` attribute value announcing that the renderer will call the
42
+ * builder as a final pass. Providers must skip their inline per-element build
43
+ * when they see this value, and keep it for any other value (including "").
44
+ */
45
+ const SSR_ORCHESTRATED_VALUE = "orchestrated";
46
+
47
+ /**
48
+ * サーバー主導スナップショット(orchestrated)の判定
49
+ * (docs/ssr-router-design.md §5)。renderToString が snapshot builder を
50
+ * 見つけたときだけ `data-wcs-server="orchestrated"` を宣言する — 値が他の
51
+ * もの(旧 server の "" を含む)なら inline 生成が従来どおり働く。
52
+ * inSsr と同じ理由でキャッシュしない。
53
+ */
54
+ function isOrchestratedSsr() {
55
+ const html = document.documentElement;
56
+ return html ? html.getAttribute('data-wcs-server') === SSR_ORCHESTRATED_VALUE : false;
57
+ }
1
58
  function inSsr() {
2
59
  // キャッシュしない: SSR モードはプロセスの属性ではなく「現在の document」の
3
60
  // 属性。@wcstack/server はグローバル document を差し替えてサーバーレンダリング
@@ -351,14 +408,7 @@ function getCustomElement(node) {
351
408
  }
352
409
  }
353
410
 
354
- /**
355
- * Resolve the registry at operation time so importing the runtime remains safe
356
- * when browser globals are absent. The owner hook is reserved for scoped
357
- * registries; current callers fall back to the global registry.
358
- */
359
- function getCustomElementRegistry(owner = null) {
360
- const globalRegistry = globalThis.customElements;
361
- const registry = owner?.customElements ?? globalRegistry;
411
+ function toAdapter(registry) {
362
412
  if (typeof registry !== "object" || registry === null)
363
413
  return null;
364
414
  const candidate = registry;
@@ -367,6 +417,33 @@ function getCustomElementRegistry(owner = null) {
367
417
  }
368
418
  return candidate;
369
419
  }
420
+ /**
421
+ * Resolve the registry that governs `owner` at operation time, so importing the
422
+ * runtime stays safe when browser globals are absent.
423
+ *
424
+ * Pass the node the operation is about (the bound element, its shadow root):
425
+ * with scoped custom element registries the same tag name can resolve to a
426
+ * different constructor per tree, so "is this tag defined?" is only meaningful
427
+ * relative to a node. Nodes on platforms without scoped registries report
428
+ * `undefined` and fall back to the global registry, which keeps every existing
429
+ * caller on today's behaviour.
430
+ */
431
+ function getCustomElementRegistry(owner = null) {
432
+ if (owner !== null && typeof owner !== "undefined") {
433
+ const { customElementRegistry: scoped, customElements: owned } = owner;
434
+ // A node in a null-registry subtree resolves to no registry at all. Falling
435
+ // back to the global one would report globally-defined tags as usable and
436
+ // let us write own properties onto elements that are still un-upgraded --
437
+ // exactly the accessor shadowing the deferred-apply path exists to avoid.
438
+ if (scoped === null)
439
+ return null;
440
+ if (typeof scoped !== "undefined")
441
+ return toAdapter(scoped);
442
+ if (typeof owned !== "undefined")
443
+ return toAdapter(owned);
444
+ }
445
+ return toAdapter(globalThis.customElements);
446
+ }
370
447
  function upgradeCustomElement(registry, root) {
371
448
  registry.upgrade?.(root);
372
449
  }
@@ -544,7 +621,7 @@ function expandSpread(node, results, options = {}) {
544
621
  if (tagName === null) {
545
622
  raiseError(`Spread binding "${result.statePathName}" requires a custom element with wcBindable, but <${element.tagName.toLowerCase()}> is not a custom element.`);
546
623
  }
547
- const registry = getCustomElementRegistry();
624
+ const registry = getCustomElementRegistry(element);
548
625
  if (registry === null) {
549
626
  raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
550
627
  }
@@ -1122,16 +1199,33 @@ const fix = (options) => {
1122
1199
  /**
1123
1200
  * Locale number filter - formats number according to locale.
1124
1201
  *
1202
+ * ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は
1203
+ * **明示引数だけを構築時に確定し、既定の `config.locale` は適用のたびに読む**。
1204
+ *
1205
+ * 以前は `options?.[0] ?? config.locale` を返り値の関数の**外**で解決していた。
1206
+ * フィルタ関数はバインド構築時に一度だけ作られるので、これはロケールを
1207
+ * クロージャに焼き込むことを意味する。`config.locale` の確定がバインド構築より
1208
+ * 遅れると、それ以降どう直しても「同じページの中で日付だけ既定ロケール」が
1209
+ * 永続し、しかも `config.locale` は依存グラフに載らないので再描画で回復もしない。
1210
+ * 症状(日付だけ英語)は原因(起動順序)から遠く、追いにくい。
1211
+ *
1212
+ * 適用のたびに読めば、少なくとも**再適用されたバインドは回復する**。ロケールは
1213
+ * 起動時に確定する前提(docs/i18n-design.md D1)なので通常この差は現れず、
1214
+ * これは順序事故から復帰できるようにするための保険である。
1215
+ *
1216
+ * 明示引数(`|date(ja-JP)`)は構築時に固定でよい — バインド式の一部であり、
1217
+ * 実行中に変わらない。
1218
+ *
1125
1219
  * @param options - Array with locale string as first element (default: config.locale)
1126
1220
  * @returns Filter function that returns localized number string
1127
1221
  */
1128
1222
  const locale = (options) => {
1129
- const opt = options?.[0] ?? config.locale;
1223
+ const explicit = options?.[0];
1130
1224
  return (value) => {
1131
1225
  if (typeof value !== 'number') {
1132
1226
  valueMustBeNumber('locale');
1133
1227
  }
1134
- return value.toLocaleString(opt);
1228
+ return value.toLocaleString(explicit ?? config.locale);
1135
1229
  };
1136
1230
  };
1137
1231
  /**
@@ -1444,12 +1538,13 @@ const truncate = (options) => {
1444
1538
  * @returns Filter function that returns date string
1445
1539
  */
1446
1540
  const date = (options) => {
1447
- const opt = options?.[0] ?? config.locale;
1541
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1542
+ const explicit = options?.[0];
1448
1543
  return (value) => {
1449
1544
  if (!(value instanceof Date)) {
1450
1545
  valueMustBeDate('date');
1451
1546
  }
1452
- return value.toLocaleDateString(opt);
1547
+ return value.toLocaleDateString(explicit ?? config.locale);
1453
1548
  };
1454
1549
  };
1455
1550
  /**
@@ -1459,12 +1554,13 @@ const date = (options) => {
1459
1554
  * @returns Filter function that returns time string
1460
1555
  */
1461
1556
  const time = (options) => {
1462
- const opt = options?.[0] ?? config.locale;
1557
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1558
+ const explicit = options?.[0];
1463
1559
  return (value) => {
1464
1560
  if (!(value instanceof Date)) {
1465
1561
  valueMustBeDate('time');
1466
1562
  }
1467
- return value.toLocaleTimeString(opt);
1563
+ return value.toLocaleTimeString(explicit ?? config.locale);
1468
1564
  };
1469
1565
  };
1470
1566
  /**
@@ -1474,12 +1570,13 @@ const time = (options) => {
1474
1570
  * @returns Filter function that returns datetime string
1475
1571
  */
1476
1572
  const datetime = (options) => {
1477
- const opt = options?.[0] ?? config.locale;
1573
+ // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
1574
+ const explicit = options?.[0];
1478
1575
  return (value) => {
1479
1576
  if (!(value instanceof Date)) {
1480
1577
  valueMustBeDate('datetime');
1481
1578
  }
1482
- return value.toLocaleString(opt);
1579
+ return value.toLocaleString(explicit ?? config.locale);
1483
1580
  };
1484
1581
  };
1485
1582
  /**
@@ -2302,6 +2399,23 @@ function setLoopContextByNode(node, loopContext) {
2302
2399
  loopContextByNode.set(node, loopContext);
2303
2400
  }
2304
2401
 
2402
+ /**
2403
+ * devtools/sink.ts
2404
+ *
2405
+ * 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
2406
+ * 計装される側(stateElementByName / setByAddress / binding / token)と
2407
+ * bridge の間の循環 import を避ける。
2408
+ *
2409
+ * コスト規範(protocol §1-1): フック未接続時、計装点のコストは
2410
+ * `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
2411
+ * 必ずこのチェックの内側で行うこと。
2412
+ */
2413
+ /** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
2414
+ let devtoolsSink = null;
2415
+ function setDevtoolsSink(sink) {
2416
+ devtoolsSink = sink;
2417
+ }
2418
+
2305
2419
  const lastListValueByAbsoluteStateAddress = new WeakMap();
2306
2420
  function getLastListValueByAbsoluteStateAddress(address) {
2307
2421
  return lastListValueByAbsoluteStateAddress.get(address) ?? [];
@@ -2576,23 +2690,6 @@ function clearAbsoluteStateAddressByBinding(binding) {
2576
2690
  absoluteStateAddressByBinding.delete(binding);
2577
2691
  }
2578
2692
 
2579
- /**
2580
- * devtools/sink.ts
2581
- *
2582
- * 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
2583
- * 計装される側(stateElementByName / setByAddress / binding / token)と
2584
- * bridge の間の循環 import を避ける。
2585
- *
2586
- * コスト規範(protocol §1-1): フック未接続時、計装点のコストは
2587
- * `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
2588
- * 必ずこのチェックの内側で行うこと。
2589
- */
2590
- /** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
2591
- let devtoolsSink = null;
2592
- function setDevtoolsSink(sink) {
2593
- devtoolsSink = sink;
2594
- }
2595
-
2596
2693
  /**
2597
2694
  * 絶対アドレス → 登録 binding の台帳。
2598
2695
  *
@@ -3100,7 +3197,7 @@ function attachEventTokenHandler(binding) {
3100
3197
  const element = binding.node;
3101
3198
  // カスタム要素が未定義なら定義後に再試行(wcBindable が必要なため)。
3102
3199
  const customTagName = getCustomElement(element);
3103
- const registry = getCustomElementRegistry();
3200
+ const registry = getCustomElementRegistry(element);
3104
3201
  if (customTagName !== null && registry?.get(customTagName) === undefined) {
3105
3202
  if (registry === null) {
3106
3203
  raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
@@ -3396,7 +3493,7 @@ function isPossibleTwoWay(node, propName) {
3396
3493
  }
3397
3494
  const customTagName = getCustomElement(element);
3398
3495
  if (customTagName !== null) {
3399
- const customClass = getCustomElementRegistry()?.get(customTagName);
3496
+ const customClass = getCustomElementRegistry(element)?.get(customTagName);
3400
3497
  if (typeof customClass === "undefined") {
3401
3498
  raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
3402
3499
  }
@@ -3579,7 +3676,7 @@ function getEventName(binding) {
3579
3676
  // 2.wcBindable protocol
3580
3677
  const customTagName = getCustomElement(binding.node);
3581
3678
  if (customTagName !== null) {
3582
- const customClass = getCustomElementRegistry()?.get(customTagName);
3679
+ const customClass = getCustomElementRegistry(binding.node)?.get(customTagName);
3583
3680
  if (typeof customClass === "undefined") {
3584
3681
  raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
3585
3682
  }
@@ -3751,7 +3848,7 @@ function addTwowayValueObserver(node, propName, observer) {
3751
3848
  function attachTwowayEventHandler(binding) {
3752
3849
  const customTagName = getCustomElement(binding.node);
3753
3850
  if (customTagName !== null) {
3754
- const registry = getCustomElementRegistry();
3851
+ const registry = getCustomElementRegistry(binding.node);
3755
3852
  const customClass = registry?.get(customTagName);
3756
3853
  if (typeof customClass === "undefined") {
3757
3854
  if (registry === null) {
@@ -3777,7 +3874,7 @@ function attachTwowayEventHandler(binding) {
3777
3874
  function detachTwowayEventHandler(binding) {
3778
3875
  const customTagName = getCustomElement(binding.node);
3779
3876
  if (customTagName !== null) {
3780
- const registry = getCustomElementRegistry();
3877
+ const registry = getCustomElementRegistry(binding.node);
3781
3878
  const customClass = registry?.get(customTagName);
3782
3879
  if (typeof customClass === "undefined") {
3783
3880
  if (registry === null) {
@@ -4162,7 +4259,9 @@ function propagateListPathToOuterState(innerStateElement, innerPath) {
4162
4259
  if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
4163
4260
  return;
4164
4261
  }
4165
- outerAbsPathInfo.stateElement.setPathInfo(outerAbsPathInfo.pathInfo.path, "for");
4262
+ // source="internal": 翻訳済みの外側パスであり、書き手が書いた文字列ではない。
4263
+ // 存在検査に掛けても直せる相手が居ないので掛けない(pathDiagnostics.ts)。
4264
+ outerAbsPathInfo.stateElement.setPathInfo(outerAbsPathInfo.pathInfo.path, "for", "internal");
4166
4265
  }
4167
4266
  /**
4168
4267
  * 子スコープのリスト行パス(`items.*.name`)に対応する親スコープの絶対パス情報を返す。
@@ -4546,6 +4645,16 @@ function addInterestedSession(node, session) {
4546
4645
  }
4547
4646
  interestedSessionsByNode.set(node, new Set([current, session]));
4548
4647
  }
4648
+ /**
4649
+ * このノードに既にバインドが張られているか。
4650
+ *
4651
+ * binder プロトコル(`bind()`)の冪等判定に使う。`remember` が binding ごとに
4652
+ * `addInterestedSession(binding.replaceNode, …)` を呼ぶので、バインド済みノードは
4653
+ * 必ずこの台帳に載っている。新しい台帳を足さずに済むぶん、二重管理の齟齬が無い。
4654
+ */
4655
+ function hasInterestedSession(node) {
4656
+ return interestedSessionsByNode.has(node);
4657
+ }
4549
4658
  function forEachInterestedSession(node, callback) {
4550
4659
  const current = interestedSessionsByNode.get(node);
4551
4660
  if (typeof current === "undefined")
@@ -4799,7 +4908,7 @@ class BindingSession {
4799
4908
  return true;
4800
4909
  }
4801
4910
  deferUntilDefined(node, tagName, callback, reject = () => undefined) {
4802
- const registry = getCustomElementRegistry();
4911
+ const registry = getCustomElementRegistry(node);
4803
4912
  if (registry === null) {
4804
4913
  raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
4805
4914
  }
@@ -5253,7 +5362,7 @@ class BindingSession {
5253
5362
  attach();
5254
5363
  return;
5255
5364
  }
5256
- const registry = getCustomElementRegistry();
5365
+ const registry = getCustomElementRegistry(record.info.node);
5257
5366
  if (registry === null) {
5258
5367
  raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
5259
5368
  }
@@ -5626,7 +5735,7 @@ function getWcBindable(element) {
5626
5735
  if (customTagName === null) {
5627
5736
  return null;
5628
5737
  }
5629
- const customClass = getCustomElementRegistry()?.get(customTagName);
5738
+ const customClass = getCustomElementRegistry(element)?.get(customTagName);
5630
5739
  if (typeof customClass === "undefined") {
5631
5740
  raiseError(`Custom element <${customTagName}> is not defined for command binding.`);
5632
5741
  }
@@ -6430,8 +6539,23 @@ class Content {
6430
6539
  let anchor = targetNode;
6431
6540
  for (const node of this._movableNodes()) {
6432
6541
  if (anchor.nextSibling !== node) {
6542
+ // moveBefore も childList mutation record を出すため、マークは両分岐の前
6433
6543
  markObserverSkipOnAdd(node);
6434
- parentNode.insertBefore(node, anchor.nextSibling);
6544
+ // moveBefore は取り外しを伴わない移動 — 接続済み行の reorder で
6545
+ // フォーカス・iframe・アニメーション状態を保存する(docs/a11y-design.md §4-1)。
6546
+ // この 1 文は 4 つのノード状態を共有する: (a) 接続済み reorder、
6547
+ // (b) clone フラグメント由来(root 違い)、(c) プール/unmount 済み(親なし)、
6548
+ // (d) バッチフラグメント内。moveBefore は「同一ツリー・親あり」を要求し
6549
+ // (b)(c)(d) では HierarchyRequestError を投げるため、same-parent ガード
6550
+ // (同 root かつ親が非 null の同時証明 = フォーカス保存が意味を持つ (a) と
6551
+ // 正確に一致)は外せない。ガードを外す「簡略化」をしてはならない。
6552
+ const mover = parentNode;
6553
+ if (node.parentNode === parentNode && typeof mover.moveBefore === "function") {
6554
+ mover.moveBefore(node, anchor.nextSibling);
6555
+ }
6556
+ else {
6557
+ parentNode.insertBefore(node, anchor.nextSibling);
6558
+ }
6435
6559
  }
6436
6560
  anchor = node;
6437
6561
  }
@@ -6643,6 +6767,157 @@ function createContent(bindingInfo) {
6643
6767
  return content;
6644
6768
  }
6645
6769
 
6770
+ // ===========================================================================
6771
+ // AUTO-GENERATED FILE - DO NOT EDIT.
6772
+ // Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
6773
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
6774
+ // ===========================================================================
6775
+ // transition-runner protocol — how a package that mutates the DOM hands that
6776
+ // mutation to whoever is arbitrating view transitions on the page.
6777
+ //
6778
+ // @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
6779
+ // (zero runtime dependencies, independently publishable), so the arbiter installs
6780
+ // itself on a well-known global symbol and the participants look it up lazily.
6781
+ // No arbiter installed means the mutation is invoked directly, synchronously —
6782
+ // byte-for-byte the behavior these packages had before the protocol existed.
6783
+ //
6784
+ // docs/view-transition-design.md §4 is the normative description.
6785
+ //
6786
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
6787
+ // `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
6788
+ // (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
6789
+ /**
6790
+ * Global key the arbiter installs itself under. `Symbol.for` so independently
6791
+ * loaded copies of this file (two CDN bundles on one page) still agree.
6792
+ */
6793
+ const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
6794
+ /**
6795
+ * The installed arbiter, or null when there is none, it speaks a version this
6796
+ * reader does not, or it does not accept this participant.
6797
+ *
6798
+ * Looked up on every call rather than cached: the tag can be added, removed, or
6799
+ * reconfigured at any point in a page's life, and a stale cache would either
6800
+ * animate what the author just switched off or miss what they switched on.
6801
+ */
6802
+ function getTransitionRunner(source) {
6803
+ const candidate = globalThis[TRANSITION_RUNNER_KEY];
6804
+ if (candidate === undefined || candidate === null)
6805
+ return null;
6806
+ if (candidate.protocol !== "wcs-transition-runner")
6807
+ return null;
6808
+ if (typeof candidate.version !== "number" || candidate.version < 1)
6809
+ return null;
6810
+ if (typeof candidate.run !== "function")
6811
+ return null;
6812
+ if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
6813
+ return null;
6814
+ return candidate;
6815
+ }
6816
+ /**
6817
+ * Run `mutate` under the installed arbiter, or directly when there is none.
6818
+ *
6819
+ * Returns `undefined` in the no-arbiter case instead of a resolved promise: the
6820
+ * state drain calls this on every batch, and awaiting is a caller's choice, not
6821
+ * an allocation the common path should pay for. `await` accepts both.
6822
+ */
6823
+ function runTransition(source, mutate, types) {
6824
+ const runner = getTransitionRunner(source);
6825
+ if (runner === null) {
6826
+ mutate();
6827
+ return undefined;
6828
+ }
6829
+ return runner.run(mutate, { source, types });
6830
+ }
6831
+
6832
+ /** Elements that already carry a generated name (never renamed). */
6833
+ const namedElements = new WeakSet();
6834
+ /**
6835
+ * The generated-name ledger is per *document*, not per module instance.
6836
+ *
6837
+ * `view-transition-name` has to be unique across the whole document: the moment
6838
+ * two elements share one, the browser aborts the transition outright. A
6839
+ * module-scope counter breaks that as soon as `@wcstack/state` is loaded twice on
6840
+ * one page (two CDN bundles), because both copies would start minting
6841
+ * `wcs-row-1`. The transition-runner key is a `Symbol.for` for exactly this
6842
+ * reason, and the counter needs the same protection.
6843
+ *
6844
+ * Sharing the cap is right for the same reason: the cost a cap exists to bound —
6845
+ * one snapshot group per named element — is a document-wide cost, not a
6846
+ * per-bundle one.
6847
+ */
6848
+ const NAMING_LEDGER_KEY = Symbol.for("wcstack.state.view-transition-naming");
6849
+ function getLedger() {
6850
+ const slot = globalThis;
6851
+ return (slot[NAMING_LEDGER_KEY] ??= { counter: 0, assigned: 0, warned: false });
6852
+ }
6853
+ /**
6854
+ * The active auto-naming policy, or null when names are the author's business
6855
+ * (the default) — one arbiter lookup per structural apply, not per row.
6856
+ */
6857
+ function getAutoNaming() {
6858
+ const runner = getTransitionRunner("state");
6859
+ if (runner === null || runner.naming !== "auto") {
6860
+ return null;
6861
+ }
6862
+ return { limit: runner.namingLimit };
6863
+ }
6864
+ function firstElementOf(content) {
6865
+ const first = content.firstNode;
6866
+ if (first === null) {
6867
+ return null;
6868
+ }
6869
+ const last = content.lastNode;
6870
+ for (let node = first; node !== null; node = node.nextSibling) {
6871
+ if (node.nodeType === Node.ELEMENT_NODE) {
6872
+ return node;
6873
+ }
6874
+ if (node === last) {
6875
+ break;
6876
+ }
6877
+ }
6878
+ return null;
6879
+ }
6880
+ /**
6881
+ * Give this content's first element a unique name plus a class for group
6882
+ * styling, unless it already has one or the cap has been reached.
6883
+ *
6884
+ * The cap exists because every named element becomes its own snapshot group; a
6885
+ * few hundred of them make a transition visibly slow. Past it naming stops and
6886
+ * says so once — silently degrading would leave the author wondering why only
6887
+ * the first part of a list animates.
6888
+ */
6889
+ function applyTransitionName(content, kind, naming) {
6890
+ const element = firstElementOf(content);
6891
+ if (element === null || namedElements.has(element)) {
6892
+ return;
6893
+ }
6894
+ // A node without `style` (anything outside HTMLElement / SVGElement) cannot
6895
+ // carry a name. Bail before touching the ledger: consuming the cap and marking
6896
+ // the element as named would burn a slot for a name that was never written,
6897
+ // and leave that element permanently ineligible.
6898
+ const style = element.style;
6899
+ if (style === undefined) {
6900
+ return;
6901
+ }
6902
+ const ledger = getLedger();
6903
+ if (ledger.assigned >= naming.limit) {
6904
+ if (!ledger.warned) {
6905
+ ledger.warned = true;
6906
+ console.warn(`[@wcstack/state] auto view-transition-name limit (${naming.limit}) reached; ` +
6907
+ "further elements are left unnamed. Raise naming-limit on <wcs-view-transition>, " +
6908
+ 'or switch to naming="manual" and name only what should morph.');
6909
+ }
6910
+ return;
6911
+ }
6912
+ namedElements.add(element);
6913
+ ledger.assigned += 1;
6914
+ ledger.counter += 1;
6915
+ style.setProperty("view-transition-name", `wcs-${kind}-${ledger.counter}`);
6916
+ // Group handle for CSS (`::view-transition-group(*.wcs-row)`). Ignored by
6917
+ // engines that predate view-transition-class, which costs nothing.
6918
+ style.setProperty("view-transition-class", `wcs-${kind}`);
6919
+ }
6920
+
6646
6921
  const lastNodeByNode = new WeakMap();
6647
6922
  const contentByListIndexByNode = new WeakMap();
6648
6923
  const pooledContentsByNode = new WeakMap();
@@ -6810,6 +7085,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
6810
7085
  setRootNodeByFragment(fragment, context.rootNode);
6811
7086
  }
6812
7087
  const ssrMode = inSsr();
7088
+ // 自動命名ポリシーは行ごとではなく apply ごとに 1 回だけ引く
7089
+ // (docs/view-transition-design.md §6)。既定の manual では null で、
7090
+ // 以降の行ループは分岐 1 つ分しか増えない。
7091
+ const autoNaming = getAutoNaming();
6813
7092
  const uuid = bindingInfo.uuid ?? '';
6814
7093
  // 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
6815
7094
  // 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
@@ -6853,6 +7132,9 @@ function applyChangeToFor(bindingInfo, context, newValue) {
6853
7132
  }
6854
7133
  // コンテントを活性化
6855
7134
  activateContent(content, loopContext, context);
7135
+ if (autoNaming !== null) {
7136
+ applyTransitionName(content, "row", autoNaming);
7137
+ }
6856
7138
  });
6857
7139
  if (typeof content === 'undefined') {
6858
7140
  raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
@@ -6952,6 +7234,11 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
6952
7234
  }
6953
7235
  const loopContext = getLoopContextByNode(bindingInfo.node);
6954
7236
  activateContent(content, loopContext, context);
7237
+ // 自動命名(docs/view-transition-design.md §6)。manual(既定)では null。
7238
+ const autoNaming = getAutoNaming();
7239
+ if (autoNaming !== null) {
7240
+ applyTransitionName(content, "branch", autoNaming);
7241
+ }
6955
7242
  }
6956
7243
  }
6957
7244
 
@@ -6969,7 +7256,7 @@ function getInputAttributeMirror(element, propName) {
6969
7256
  if (customTagName === null) {
6970
7257
  return null;
6971
7258
  }
6972
- const customClass = getCustomElementRegistry()?.get(customTagName);
7259
+ const customClass = getCustomElementRegistry(element)?.get(customTagName);
6973
7260
  if (typeof customClass === "undefined") {
6974
7261
  return null;
6975
7262
  }
@@ -7329,6 +7616,281 @@ function applyChangeToWebComponent(binding, _context, _newValue) {
7329
7616
  });
7330
7617
  }
7331
7618
 
7619
+ /**
7620
+ * pathDiagnostics.ts — バインド / `$watch` 対象パスの存在検査(silent failure の可視化)。
7621
+ *
7622
+ * なぜ必要か:
7623
+ * `getByAddress` は「親が null / undefined のパスの読み」を undefined で返し、
7624
+ * undefined はプロパティ書き込みがスキップされる値なので、`user.nmae` のような
7625
+ * 打ち間違いは**エラーも警告も出さずに DOM が更新されない**だけになる。一方で
7626
+ * トップレベルの打ち間違い(`cout`)は parentAddress を辿れず raiseError で落ちる。
7627
+ * 同じ「パスを打ち間違えた」という 1 つの失敗が、パスの深さで silent / loud に
7628
+ * 割れており、書き手からは区別がつかない。ここはその silent 側を埋める。
7629
+ *
7630
+ * 精度方針(過小近似):
7631
+ * 「確実に存在しない」と言い切れる場合にだけ報告する。getter の戻り値の先・
7632
+ * 空配列・null 親・mapped な `bind-component` など、静的に決められない形はすべて
7633
+ * `"unknown"` に倒して黙る(偽陽性ゼロ優先。docs/static-wiring-dx-design.md D7 /
7634
+ * [ADR-06](../../docs/architecture-hardening/06-path-type-safety.md) の精度哲学)。
7635
+ *
7636
+ * 診断 code はコンソール → lint → IDE の三面で共有する(errorGuidance.ts の規約)。
7637
+ */
7638
+ const UNKNOWN = Object.freeze({
7639
+ existence: "unknown",
7640
+ missingSegment: "",
7641
+ candidates: Object.freeze([]),
7642
+ });
7643
+ const EXISTS = Object.freeze({
7644
+ existence: "exists",
7645
+ missingSegment: "",
7646
+ candidates: Object.freeze([]),
7647
+ });
7648
+ /**
7649
+ * `obj` 自身+プロトタイプチェーン(Object.prototype 手前まで)から descriptor を引く。
7650
+ * 打ち切り位置は getAllPropertyDescriptors と同じ — 「state が宣言したもの」だけを
7651
+ * 存在とみなし、`toString` 等の Object.prototype 由来を存在扱いしない。
7652
+ */
7653
+ function findDescriptor(obj, key) {
7654
+ let proto = obj;
7655
+ while (proto !== null && proto !== Object.prototype) {
7656
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
7657
+ if (typeof descriptor !== "undefined") {
7658
+ return descriptor;
7659
+ }
7660
+ proto = Object.getPrototypeOf(proto);
7661
+ }
7662
+ return undefined;
7663
+ }
7664
+ /** `obj` 自身+プロトタイプチェーンのキー名(did-you-mean の候補集合) */
7665
+ function ownKeys(obj) {
7666
+ const keys = [];
7667
+ let proto = obj;
7668
+ while (proto !== null && proto !== Object.prototype) {
7669
+ for (const key of Object.getOwnPropertyNames(proto)) {
7670
+ keys.push(key);
7671
+ }
7672
+ proto = Object.getPrototypeOf(proto);
7673
+ }
7674
+ return keys;
7675
+ }
7676
+ /**
7677
+ * 失敗した階層の兄弟候補。生オブジェクトのキーに加え、その階層にフラット宣言
7678
+ * (ドットパス getter)されているものも混ぜる — `cart.items.*.subtotl` の正解
7679
+ * `subtotal` は行オブジェクトには無く getterPaths にしか居ないため。
7680
+ */
7681
+ function collectCandidates(container, parentPrefix, declaredPaths) {
7682
+ const candidates = ownKeys(container);
7683
+ const prefix = parentPrefix.length > 0 ? parentPrefix + DELIMITER : "";
7684
+ for (const declared of declaredPaths) {
7685
+ if (prefix.length > 0 && !declared.startsWith(prefix)) {
7686
+ continue;
7687
+ }
7688
+ const rest = declared.slice(prefix.length);
7689
+ // 直下の 1 セグメントだけを候補にする(孫は別階層の名前なので提案しない)
7690
+ if (rest.length > 0 && rest.indexOf(DELIMITER) === -1) {
7691
+ candidates.push(rest);
7692
+ }
7693
+ }
7694
+ return candidates;
7695
+ }
7696
+ /**
7697
+ * `target` に対して `path` が解決しうるかを、値を読まずに(getter を評価せずに)判定する。
7698
+ *
7699
+ * 解決の順序は `getByAddress` の実装に合わせる: まず「パス文字列そのものがキーか」
7700
+ * (ドットパス getter がこれ)、次にセグメントを 1 つずつ降りる。
7701
+ */
7702
+ function resolvePathExistence(target, path, declaredPaths) {
7703
+ // ドットパス getter / フラットキーの完全一致(`get "users.*.fullName"()` 等)
7704
+ if (findDescriptor(target, path) !== undefined) {
7705
+ return EXISTS;
7706
+ }
7707
+ const segments = getPathInfo(path).segments;
7708
+ let current = target;
7709
+ let prefix = "";
7710
+ for (let i = 0; i < segments.length; i++) {
7711
+ const segment = segments[i];
7712
+ const parentPrefix = prefix;
7713
+ prefix = i === 0 ? segment : prefix + DELIMITER + segment;
7714
+ // 途中のプレフィックスがフラット宣言されている(`cart.totalPrice` が getter で、
7715
+ // その戻り値のサブプロパティを読む形)。戻り値の形は評価しないと分からない
7716
+ if (i > 0 && i < segments.length - 1 && findDescriptor(target, prefix) !== undefined) {
7717
+ return UNKNOWN;
7718
+ }
7719
+ // null / undefined / primitive より深い読みは実行時 undefined 解決 = 判定不能。
7720
+ // 「初期値 null のオブジェクトに後から代入する」形を偽陽性で潰さないため
7721
+ if (Object(current) !== current) {
7722
+ return UNKNOWN;
7723
+ }
7724
+ if (segment === WILDCARD) {
7725
+ // 行の形は「いま入っている要素」からしか分からない。空配列・非配列は判定不能
7726
+ if (!Array.isArray(current) || current.length === 0) {
7727
+ return UNKNOWN;
7728
+ }
7729
+ current = current[0];
7730
+ continue;
7731
+ }
7732
+ const descriptor = findDescriptor(current, segment);
7733
+ if (typeof descriptor === "undefined") {
7734
+ return {
7735
+ existence: "missing",
7736
+ missingSegment: segment,
7737
+ candidates: collectCandidates(current, parentPrefix, declaredPaths),
7738
+ };
7739
+ }
7740
+ if (typeof descriptor.get === "function") {
7741
+ // getter の戻り値の先は評価しないと分からない(末尾なら存在は確定)
7742
+ return i === segments.length - 1 ? EXISTS : UNKNOWN;
7743
+ }
7744
+ current = descriptor.value;
7745
+ }
7746
+ return EXISTS;
7747
+ }
7748
+ /** 診断 code は lint / IDE と同一語彙(errorGuidance.ts の三面共有規約) */
7749
+ const DIAGNOSTIC_CODE = {
7750
+ binding: "wcs/binding-path-missing",
7751
+ watch: "wcs/watch-path-missing",
7752
+ };
7753
+ const SUBJECT = {
7754
+ binding: "Bound path",
7755
+ watch: "$watch path",
7756
+ };
7757
+ /**
7758
+ * ルート直下(単一セグメント)のパスが state に無いときのエラーメッセージ。
7759
+ *
7760
+ * この形だけは親アドレスを辿れないので読み取りが throw する = 元から loud だが、
7761
+ * 文面が `address.parentAddress is undefined path: cout` という内部実装の言葉で、
7762
+ * 「打ち間違い」だと分からず did-you-mean も lint 誘導も無かった。深いパスの
7763
+ * `console.warn` と同じ語彙に揃える。
7764
+ */
7765
+ function missingRootPathMessage(stateName, path, target, declaredPaths) {
7766
+ return `[${DIAGNOSTIC_CODE.binding}] Path "${path}" does not exist on state "${stateName}".` +
7767
+ `${didYouMean(path, collectCandidates(target, "", declaredPaths))}${LINT_HINT}`;
7768
+ }
7769
+ /**
7770
+ * `$resolve` / `$getAll` に渡した添字の本数がワイルドカードの本数と噛み合わない。
7771
+ *
7772
+ * 不足(`$resolve`)は元から throw していたが、**超過は両 API とも黙って無視**され、
7773
+ * 取り違えた添字のまま「もっともらしい値」を返していた。本数はパス文字列から
7774
+ * 決まるので、噛み合わないことは常にプログラマのミス。
7775
+ */
7776
+ function indexArityMessage(api, path, wildcardCount, actual) {
7777
+ // `$getAll` / `$setAll` の添字は前方一致の接頭辞なので上限、`$resolve` だけが厳密一致
7778
+ // (docs/state-set-all-design.md §4)。
7779
+ const requirement = api === "$resolve"
7780
+ ? `exactly ${wildcardCount}`
7781
+ : `at most ${wildcardCount}`;
7782
+ return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
7783
+ `("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
7784
+ }
7785
+ /**
7786
+ * `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
7787
+ * 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
7788
+ * 文脈が添字を持っている場合、黙って全展開に倒すと「文脈で絞られている」という
7789
+ * 書き手の期待と食い違い、異なる文脈の添字の流用とも区別が付かないため throw する。
7790
+ *
7791
+ * 実行時の評価文脈に依存する(`$setAll` の spread 長と同種)ので lint へは誘導しない。
7792
+ */
7793
+ function getAllContextMismatchMessage(path, contextPath) {
7794
+ return `$getAll("${path}") was called without indexes inside the loop context of ` +
7795
+ `"${contextPath}", but the path shares no wildcard level with that context, ` +
7796
+ `so the context indexes ($1..$n) do not apply. ` +
7797
+ `Pass indexes explicitly ([] expands every level).`;
7798
+ }
7799
+ /**
7800
+ * `$setAll(path, indexes, values, { spread: true })` の配列長がマッチ件数と噛み合わない。
7801
+ *
7802
+ * 静的には件数が分からない(実行時のリスト長に依存する)ので lint へは誘導しない。
7803
+ * 黙って切り詰める/余りを捨てると誤配が通ってしまうため throw する
7804
+ * (docs/state-set-all-design.md §3-3)。
7805
+ */
7806
+ function setAllSpreadArityMessage(path, matched, actual) {
7807
+ return `$setAll("${path}", …, { spread: true }) requires the values array to have ` +
7808
+ `exactly one entry per matched address (matched ${matched}) but got ${actual}. ` +
7809
+ `Did the list change between $getAll and $setAll?`;
7810
+ }
7811
+ /**
7812
+ * `$setAll` の値と `options` の組み合わせが意味を成さない。
7813
+ * (docs/state-set-all-design.md §3-1)
7814
+ */
7815
+ function setAllValueKindMessage(path, reason) {
7816
+ return `$setAll("${path}") ${reason}`;
7817
+ }
7818
+ /**
7819
+ * ワイルドカードを解決するループ文脈が足りない(=パスの階数 > スコープの階数)。
7820
+ *
7821
+ * `matrix.*.*` を 1 段の `for` の中で読む、`$2` を 1 段のループの中で読む、といった
7822
+ * 取り違えがこれ。元の文面は `address.listIndex?.index is undefined path: matrix.*` /
7823
+ * `Index not found at position 1 for loopContext:` という内部実装の言葉で、
7824
+ * **何を間違えたのかが書かれていなかった**。
7825
+ */
7826
+ function wildcardScopeMessage(subject, needed, available) {
7827
+ return `[wcs/wildcard-rank] ${subject} needs ${needed} enclosing loop level(s) but the current ` +
7828
+ `scope provides ${available}. Wrap it in that many "for" templates, or use $resolve(path, indexes) ` +
7829
+ `to name the row explicitly.${LINT_HINT}`;
7830
+ }
7831
+ /** 同じ (state 要素, パス) の報告は 1 回だけにする台帳 */
7832
+ const reportedPathsByStateElement = new WeakMap();
7833
+ function alreadyReported(stateElement, path) {
7834
+ let reported = reportedPathsByStateElement.get(stateElement);
7835
+ if (typeof reported === "undefined") {
7836
+ reported = new Set();
7837
+ reportedPathsByStateElement.set(stateElement, reported);
7838
+ }
7839
+ if (reported.has(path)) {
7840
+ return true;
7841
+ }
7842
+ reported.add(path);
7843
+ return false;
7844
+ }
7845
+ /**
7846
+ * バインド確立時 / `$watch` 宣言時にパスの存在を検査し、確実に存在しないものだけ報告する。
7847
+ *
7848
+ * 報告は `console.warn` に留める(`raiseError` にしない):
7849
+ * 判定は過小近似とはいえ動的にキーが生える形まで排除できたわけではなく、
7850
+ * 既存ページを起動不能にする代償に見合わない。silent を破ることが目的であり、
7851
+ * 停止させることではない。
7852
+ */
7853
+ function checkDeclaredPath(stateElement, state, path, source) {
7854
+ if (source === "internal" || typeof state === "undefined") {
7855
+ return;
7856
+ }
7857
+ // `$command` / `$streamStatus` / `$1` 等の予約名前空間は raw state に実体を持たない
7858
+ if (path.startsWith("$")) {
7859
+ return;
7860
+ }
7861
+ // mapped な bind-component の子スコープはパスの正本を持たない(親側で解決される)
7862
+ if (stateElement.hasMappedComponentState === true) {
7863
+ return;
7864
+ }
7865
+ // 単一セグメントのバインディングは読み取り時に raiseError で loud に落ちるので、
7866
+ // ここで二重に報告しない。`$watch` は落ちずに黙って発火しないだけなので検査する
7867
+ const segments = getPathInfo(path).segments;
7868
+ if (source === "binding" && segments.length < 2) {
7869
+ return;
7870
+ }
7871
+ if (alreadyReported(stateElement, path)) {
7872
+ return;
7873
+ }
7874
+ const result = resolvePathExistence(state, path, stateElement.getterPaths);
7875
+ if (result.existence !== "missing") {
7876
+ return;
7877
+ }
7878
+ // 接頭辞は raiseError と同じ `[@wcstack/state] [wcs/...]` の並び(コンソールの
7879
+ // grep 単位をパッケージで揃える)
7880
+ console.warn(`[@wcstack/state] [${DIAGNOSTIC_CODE[source]}] ${SUBJECT[source]} "${path}" does not resolve on state "${stateElement.name}": ` +
7881
+ `"${result.missingSegment}" is not declared.${didYouMean(result.missingSegment, result.candidates)}` +
7882
+ ` Updates to this path will be silently dropped.${LINT_HINT}`);
7883
+ if (devtoolsSink !== null) {
7884
+ devtoolsSink({
7885
+ type: "state:path-unresolved",
7886
+ source,
7887
+ stateName: stateElement.name,
7888
+ path,
7889
+ missingSegment: result.missingSegment,
7890
+ });
7891
+ }
7892
+ }
7893
+
7332
7894
  // indexName ... $1, $2, ...
7333
7895
  function getIndexValueByLoopContext(loopContext, indexName) {
7334
7896
  if (loopContext.listIndex === null) {
@@ -7340,7 +7902,10 @@ function getIndexValueByLoopContext(loopContext, indexName) {
7340
7902
  }
7341
7903
  const listIndex = listIndexAtWildcard(loopContext.listIndex, indexPos, loopContext.pathInfo.wildcardCount);
7342
7904
  if (listIndex === null) {
7343
- raiseError(`Index not found at position ${indexPos} for loopContext:`);
7905
+ // 位置が範囲外 `$2` 1 段のループの中で読んだ、という取り違え。
7906
+ // 元の文面(`Index not found at position 1 for loopContext:`)は内部の言葉で、
7907
+ // 何段必要で何段あるのかが書かれていなかった。
7908
+ raiseError(wildcardScopeMessage(`"${indexName}"`, indexPos + 1, loopContext.pathInfo.wildcardCount));
7344
7909
  }
7345
7910
  return listIndex.index;
7346
7911
  }
@@ -7392,7 +7957,7 @@ function scheduleDeferredApply(binding, tagName) {
7392
7957
  return;
7393
7958
  }
7394
7959
  // Compatibility fallback for direct applyChange() callers outside a session.
7395
- const registry = getCustomElementRegistry();
7960
+ const registry = getCustomElementRegistry(binding.replaceNode);
7396
7961
  if (registry === null) {
7397
7962
  scheduledBindings.delete(binding);
7398
7963
  reportFailure(tagName, new Error("CustomElementRegistry is unavailable."));
@@ -7536,7 +8101,7 @@ function applyChange(binding, context) {
7536
8101
  if (definedApplyVerifiedByBinding.get(binding) !== true) {
7537
8102
  const customTag = getCustomElement(binding.replaceNode);
7538
8103
  if (customTag) {
7539
- if (getCustomElementRegistry()?.get(customTag) === undefined) {
8104
+ if (getCustomElementRegistry(binding.replaceNode)?.get(customTag) === undefined) {
7540
8105
  // 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
7541
8106
  // 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
7542
8107
  // whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
@@ -7587,6 +8152,26 @@ function applyChange(binding, context) {
7587
8152
  }
7588
8153
  }
7589
8154
 
8155
+ /**
8156
+ * バインディング 1 本の適用失敗を報告する(握り潰しではない)。
8157
+ *
8158
+ * `console.error` だけだと devtools からは「静かに握られた失敗」が見えないため、
8159
+ * 同じ地点から sink にも流す(`state:watch-error` と同じ位置づけ)。
8160
+ * 値と DOM は巻き戻さない — 伝播 hop 上限超過・watch 連鎖打ち切りと同じ姿勢。
8161
+ */
8162
+ function reportBindingApplyError(binding, error) {
8163
+ console.error(`[@wcstack/state] binding "${binding.bindingType}: ${binding.statePathName}" failed to apply; ` +
8164
+ `the rest of this batch continues.`, { node: binding.node, error });
8165
+ if (devtoolsSink !== null) {
8166
+ devtoolsSink({
8167
+ type: "state:binding-apply-error",
8168
+ stateName: binding.stateName,
8169
+ path: binding.statePathName,
8170
+ bindingType: binding.bindingType,
8171
+ error,
8172
+ });
8173
+ }
8174
+ }
7590
8175
  /**
7591
8176
  * バインディング情報の配列を処理し、各バインディングに対して状態の変更を適用する。
7592
8177
  *
@@ -7642,7 +8227,16 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
7642
8227
  propagationContextByBinding: propagationContextByBinding,
7643
8228
  };
7644
8229
  do {
7645
- applyChange(binding, context);
8230
+ // 1 本の失敗を 1 本に閉じ込める(§ エラー隔離)。隔離しないと、stale な
8231
+ // アドレスを読んだ 1 本の throw がバッチの残り・$updatedCallback・drain
8232
+ // リスナー($watch / $streams restart)まで道連れにし、「値は新しいのに
8233
+ // DOM は途中まで」という再現困難な半端状態を作る。
8234
+ try {
8235
+ applyChange(binding, context);
8236
+ }
8237
+ catch (error) {
8238
+ reportBindingApplyError(binding, error);
8239
+ }
7646
8240
  bindingIndex++;
7647
8241
  const nextBindingInfo = bindings[bindingIndex];
7648
8242
  if (!nextBindingInfo)
@@ -7658,7 +8252,12 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
7658
8252
  // applyChangeToProperty は propagationContextByBinding 以外の context を
7659
8253
  // 参照しないため、遅延分は最小 context を渡す
7660
8254
  for (const { binding, value } of deferredSelectBindings) {
7661
- applyChangeToProperty(binding, { propagationContextByBinding }, value);
8255
+ try {
8256
+ applyChangeToProperty(binding, { propagationContextByBinding }, value);
8257
+ }
8258
+ catch (error) {
8259
+ reportBindingApplyError(binding, error);
8260
+ }
7662
8261
  }
7663
8262
  for (const [absAddress, newListValue] of newListValueByAbsAddress.entries()) {
7664
8263
  setLastListValueByAbsoluteStateAddress(absAddress, newListValue);
@@ -8109,7 +8708,13 @@ function collectStructuralFragments(rootNode, walkRoot, forPath) {
8109
8708
  async function waitForStateInitialize(root) {
8110
8709
  const elements = root.querySelectorAll(config.tagNames.state);
8111
8710
  const promises = [];
8112
- await customElements.whenDefined(config.tagNames.state);
8711
+ const registry = getCustomElementRegistry(root);
8712
+ if (registry === null) {
8713
+ // null レジストリのサブツリーでは <wcs-state> が upgrade されないので
8714
+ // initializePromise が生えず、待っても永久に初期化されない。
8715
+ raiseError(`CustomElementRegistry is unavailable for <${config.tagNames.state}>.`);
8716
+ }
8717
+ await registry.whenDefined(config.tagNames.state);
8113
8718
  for (const element of elements) {
8114
8719
  // Light DOM の mapped コンポーネントの state は待たない。それはこの root の
8115
8720
  // バインディングが張られてからでないと初期化できず(自分を束ねるホスト binding を
@@ -8148,7 +8753,7 @@ async function buildBindings(root) {
8148
8753
  }
8149
8754
  }
8150
8755
 
8151
- var version = "1.30.0";
8756
+ var version = "1.32.0";
8152
8757
  var pkg = {
8153
8758
  version: version};
8154
8759
 
@@ -8871,28 +9476,262 @@ async function hydrateBindings(root) {
8871
9476
  return true;
8872
9477
  }
8873
9478
 
8874
- const stateElementByNameByNode = new WeakMap();
8875
- const bindingsReadyByNode = new WeakMap();
8876
- // devtools 用の列挙可能な登録簿(protocol §4.1 唯一の常時 ON 台帳)。
8877
- // サイズは <wcs-state> 要素数に拘束され、unregister(disconnectedCallback)で
8878
- // 必ず削除されるためリークしない。
8879
- const liveStateElements = new Set();
8880
- function getLiveStateElements() {
8881
- return liveStateElements;
8882
- }
8883
- function getStateElementByName(rootNode, name) {
8884
- let stateElementByName = stateElementByNameByNode.get(rootNode);
8885
- if (!stateElementByName) {
8886
- return null;
8887
- }
8888
- return stateElementByName.get(name) || null;
8889
- }
9479
+ // ===========================================================================
9480
+ // AUTO-GENERATED FILE - DO NOT EDIT.
9481
+ // Generated from /protocol/binder.ts by scripts/sync-protocol-types.mjs.
9482
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
9483
+ // ===========================================================================
9484
+ // binder protocol how a package that inserts DOM hands those nodes to whoever
9485
+ // owns data bindings on the page.
9486
+ //
9487
+ // The dual of transition-runner: that one hands a *mutation* to whoever animates
9488
+ // it, this one hands *new nodes* to whoever binds them.
9489
+ //
9490
+ // A `data-wcs` binding exists only for nodes @wcstack/state walked when it built
9491
+ // its bindings. Nodes that arrive later — the content of a route that was not
9492
+ // active at that moment, a <wcs-head> child reflected into <head> — were never
9493
+ // walked, so their bindings silently do nothing, however often they are inserted.
9494
+ // @wcstack/router must not depend on @wcstack/state (zero runtime dependencies,
9495
+ // independently publishable), so state installs a binder on a well-known global
9496
+ // symbol and inserters look it up lazily.
9497
+ //
9498
+ // No binder installed means nothing happens — byte-for-byte the behavior these
9499
+ // packages had before the protocol existed.
9500
+ //
9501
+ // docs/binder-protocol-design.md is the normative description.
9502
+ //
9503
+ // SINGLE SOURCE OF TRUTH: edit only this file (/protocol/binder.ts), then run
9504
+ // `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
9505
+ // (packages/<pkg>/src/protocol/binder.ts). Those copies are generated — do not edit them.
9506
+ /**
9507
+ * Global key the binder installs itself under. `Symbol.for` so independently
9508
+ * loaded copies of this file (two CDN bundles on one page) still agree.
9509
+ */
9510
+ const BINDER_KEY = Symbol.for("wcstack.binder");
9511
+ /**
9512
+ * The installed binder, or null when there is none or it speaks a version this
9513
+ * reader does not.
9514
+ *
9515
+ * Looked up on every call rather than cached, for the same reason
9516
+ * transition-runner does: the page's composition can change at any point, and a
9517
+ * stale cache would keep calling into a binder that is no longer there.
9518
+ */
9519
+ function getBinder() {
9520
+ const candidate = globalThis[BINDER_KEY];
9521
+ if (candidate === undefined || candidate === null)
9522
+ return null;
9523
+ if (candidate.protocol !== "wcs-binder")
9524
+ return null;
9525
+ if (typeof candidate.version !== "number" || candidate.version < 1)
9526
+ return null;
9527
+ if (typeof candidate.bind !== "function")
9528
+ return null;
9529
+ return candidate;
9530
+ }
9531
+ /**
9532
+ * Subtrees offered before a binder existed, and the set of everything a binder
9533
+ * has taken. Both live on global symbols so that independently loaded copies of
9534
+ * this file — the router's and state's — share one queue.
9535
+ *
9536
+ * The queue is needed because of load order: the router's auto bundle runs
9537
+ * before state's, so `<wcs-head>` reflects its children into `<head>` while
9538
+ * there is still nothing to bind them. Offering them to a binder that arrives
9539
+ * later is the difference between working and silently blank.
9540
+ */
9541
+ const PENDING_KEY = Symbol.for("wcstack.binder.pending");
9542
+ const TAKEN_KEY = Symbol.for("wcstack.binder.taken");
9543
+ function pendingQueue() {
9544
+ const globals = globalThis;
9545
+ let queue = globals[PENDING_KEY];
9546
+ if (queue === undefined) {
9547
+ queue = [];
9548
+ globals[PENDING_KEY] = queue;
9549
+ }
9550
+ return queue;
9551
+ }
9552
+ function takenSet() {
9553
+ const globals = globalThis;
9554
+ let taken = globals[TAKEN_KEY];
9555
+ if (taken === undefined) {
9556
+ taken = new WeakSet();
9557
+ globals[TAKEN_KEY] = taken;
9558
+ }
9559
+ return taken;
9560
+ }
9561
+ /**
9562
+ * Bind everything offered before this binder existed. Called by the binder right
9563
+ * after it installs itself.
9564
+ */
9565
+ function flushPendingBinds() {
9566
+ const binder = getBinder();
9567
+ if (binder === null)
9568
+ return;
9569
+ const queue = pendingQueue();
9570
+ if (queue.length === 0)
9571
+ return;
9572
+ const pending = queue.splice(0, queue.length);
9573
+ const taken = takenSet();
9574
+ for (const subtree of pending) {
9575
+ taken.add(subtree);
9576
+ binder.bind(subtree);
9577
+ }
9578
+ }
9579
+
9580
+ /**
9581
+ * binder プロトコルの提供側(docs/binder-protocol-design.md)。
9582
+ *
9583
+ * `buildBindings` は起動時に `document.body` を 1 回走査するだけなので、そのとき
9584
+ * document に居なかったノードのバインドは存在しない。router が後から差し込む
9585
+ * ルート内容や `<wcs-head>` のクローンがこれに当たり、書いたバインドが黙って
9586
+ * 何もしない状態になっていた。`bind()` はその取りこぼしを 1 サブツリー分だけ
9587
+ * 埋める。
9588
+ *
9589
+ * **走査を勝手に広げない。** MutationObserver が見た全追加ノードを走査する形に
9590
+ * すると、バインドを 1 個も持たない挿入(大多数)にコストが乗り、さらに
9591
+ * `innerHTML` で入れた外部由来の DOM が `data-wcs` を発火させることになる。
9592
+ * ここで束ねるのは**明示的に渡されたものだけ**である。
9593
+ */
9594
+ const BIND_ATTRIBUTE_SELECTOR = () => `[${config.bindAttributeName}]`;
9595
+ /**
9596
+ * このサブツリーは既にバインド済みか。
9597
+ *
9598
+ * ルート内容は「起動時に active だったので全部バインド済み」か「一度も走査されて
9599
+ * いないので全部未バインド」のどちらかで、途中の状態を取らない。したがって
9600
+ * **宣言を持つ最初のノード 1 個**を見れば足りる。全ノードを走査して判定するのは
9601
+ * 同じ結論により高いコストを払うだけになる。
9602
+ */
9603
+ function alreadyBound(subtree) {
9604
+ if (hasInterestedSession(subtree)) {
9605
+ return true;
9606
+ }
9607
+ if (!isElement(subtree)) {
9608
+ return false;
9609
+ }
9610
+ if (subtree.hasAttribute(config.bindAttributeName)) {
9611
+ // 属性を持つのに台帳に居ない = 未バインド
9612
+ return false;
9613
+ }
9614
+ const first = subtree.querySelector(BIND_ATTRIBUTE_SELECTOR());
9615
+ return first !== null && hasInterestedSession(first);
9616
+ }
9617
+ function isElement(node) {
9618
+ return node.nodeType === 1;
9619
+ }
9620
+ function bindNow(subtree) {
9621
+ if (alreadyBound(subtree)) {
9622
+ return;
9623
+ }
9624
+ convertMustacheToComments(subtree);
9625
+ collectStructuralFragments(subtree.getRootNode(), subtree);
9626
+ // `getSubscriberNodes` の TreeWalker は**ルート自身を返さない**。`buildBindings` は
9627
+ // `document.body` を渡すので今まで問題にならなかったが、ここには宣言をルートに
9628
+ // 持つノードが来る(`<wcs-head>` が head へ入れる `<title data-wcs="…">`)。
9629
+ // そのときだけ親から走査して、ルートを走査範囲に含める。兄弟の重複登録は
9630
+ // `registeredNodeSet` が弾くので、余計なバインドは生まれない。
9631
+ // 親は Element とは限らない(ShadowRoot 直下なら DocumentFragment、head 直下なら
9632
+ // Element)。`parentElement` だと前者で null になり、ルートを含められない。
9633
+ const declaresOnRoot = subtree.hasAttribute(config.bindAttributeName);
9634
+ const parent = subtree.parentNode;
9635
+ const canWalkFromParent = parent !== null
9636
+ && (parent.nodeType === 1 || parent.nodeType === 9 || parent.nodeType === 11);
9637
+ const walkRoot = declaresOnRoot && canWalkFromParent
9638
+ ? parent
9639
+ : subtree;
9640
+ initializeBindings(walkRoot, null);
9641
+ }
9642
+ /**
9643
+ * 初期バインド構築より前に差し出されたサブツリー。
9644
+ *
9645
+ * `<wcs-head>` は `connectedCallback` の中でクローンを head へ入れるので、
9646
+ * state / router のどちらを先に読み込んでも「まだ構築が終わっていない」時点で
9647
+ * bind を求めてくる。そこで同期に束ねても `<wcs-state>` の登録が済んでおらず、
9648
+ * バインドは state を見つけられない。**構築の完了を唯一の合図にする。**
9649
+ */
9650
+ const beforeFirstBuild = [];
9651
+ function bind(subtree) {
9652
+ if (!isElement(subtree) || alreadyBound(subtree)) {
9653
+ return;
9654
+ }
9655
+ if (!areBindingsBuilt(subtree.getRootNode())) {
9656
+ beforeFirstBuild.push(subtree);
9657
+ return;
9658
+ }
9659
+ bindNow(subtree);
9660
+ }
9661
+ /**
9662
+ * 初期バインド構築の完了時に呼ぶ(stateElementByName.ts)。binder が居ない時点で
9663
+ * 差し出された分(プロトコルの保留キュー)と、居たが早すぎた分をまとめて束ねる。
9664
+ */
9665
+ function drainPendingBinds() {
9666
+ const pending = beforeFirstBuild.splice(0, beforeFirstBuild.length);
9667
+ for (const subtree of pending) {
9668
+ bindNow(subtree);
9669
+ }
9670
+ flushPendingBinds();
9671
+ }
9672
+ const binder = {
9673
+ protocol: "wcs-binder",
9674
+ version: 1,
9675
+ bind,
9676
+ };
9677
+ /**
9678
+ * グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
9679
+ *
9680
+ * 既に別のコピーが載っているなら譲る。1 ページに 2 つの state バンドルが載る構成
9681
+ * (CDN の取り違え)で、後から読まれた側が先客を追い出すと、先客がバインドした
9682
+ * ノードの台帳と食い違う。
9683
+ */
9684
+ function registerBinder() {
9685
+ const globals = globalThis;
9686
+ if (globals[BINDER_KEY] === undefined) {
9687
+ globals[BINDER_KEY] = binder;
9688
+ }
9689
+ // ここでは引き取らない。`<wcs-state>` の登録は connectedCallback の await より
9690
+ // 後なので、この時点ではまだ state が居ない。保留分は初期バインド構築の完了時に
9691
+ // 流す(stateElementByName.ts)。そこが「state が確実に居る」最初の瞬間である。
9692
+ }
9693
+
9694
+ const stateElementByNameByNode = new WeakMap();
9695
+ const bindingsReadyByNode = new WeakMap();
9696
+ // devtools 用の列挙可能な登録簿(protocol §4.1 — 唯一の常時 ON 台帳)。
9697
+ // サイズは <wcs-state> 要素数に拘束され、unregister(disconnectedCallback)で
9698
+ // 必ず削除されるためリークしない。
9699
+ const liveStateElements = new Set();
9700
+ function getLiveStateElements() {
9701
+ return liveStateElements;
9702
+ }
9703
+ function getStateElementByName(rootNode, name) {
9704
+ let stateElementByName = stateElementByNameByNode.get(rootNode);
9705
+ if (!stateElementByName) {
9706
+ return null;
9707
+ }
9708
+ return stateElementByName.get(name) || null;
9709
+ }
8890
9710
  /**
8891
9711
  * 指定された rootNode のバインディング初期化が完了するまで待機する Promise を返す。
8892
9712
  */
8893
9713
  function getBindingsReady(rootNode) {
8894
9714
  return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
8895
9715
  }
9716
+ const bindingsBuiltRoots = new WeakSet();
9717
+ /**
9718
+ * この rootNode の初期バインド構築が完了しているか。
9719
+ *
9720
+ * binder プロトコル(`bind()`)が使う。router の `<wcs-head>` はクローンを
9721
+ * `connectedCallback` の中で head へ入れるので、**state が最初の走査を終える前**に
9722
+ * bind を求めてくる。そこで同期に束ねても state 要素の初期化が済んでおらず、
9723
+ * 結果は空のままになる。完了までは binder 側で保留する。
9724
+ *
9725
+ * 「まだ登録も済んでいない」と「もう構築が終わった」を取り違えないよう、判定は
9726
+ * 完了の側で持つ。<wcs-state> の登録は connectedCallback の await より後に起きるので、
9727
+ * 「エントリの有無」で進行中かを測ると読み込み順によって逆の答えを返す。
9728
+ */
9729
+ function areBindingsBuilt(rootNode) {
9730
+ return bindingsBuiltRoots.has(rootNode);
9731
+ }
9732
+ function markBindingsBuilt(rootNode) {
9733
+ bindingsBuiltRoots.add(rootNode);
9734
+ }
8896
9735
  function setStateElementByName(rootNode, name, element) {
8897
9736
  let stateElementByName = stateElementByNameByNode.get(rootNode);
8898
9737
  if (element === null) {
@@ -8945,6 +9784,11 @@ function setStateElementByName(rootNode, name, element) {
8945
9784
  else {
8946
9785
  await buildBindings(rootNode);
8947
9786
  }
9787
+ markBindingsBuilt(rootNode);
9788
+ // binder が居ない時点で差し出されたサブツリーを引き取る。ここが
9789
+ // 「state が確実に居る」最初の瞬間で、router の auto バンドルが
9790
+ // state のそれより先に走る順序を吸収できる唯一の場所である。
9791
+ drainPendingBinds();
8948
9792
  resolve();
8949
9793
  }
8950
9794
  catch (error) {
@@ -8959,6 +9803,11 @@ function setStateElementByName(rootNode, name, element) {
8959
9803
  queueMicrotask(async () => {
8960
9804
  try {
8961
9805
  await buildBindings(rootNode);
9806
+ markBindingsBuilt(rootNode);
9807
+ // binder が居ない時点で差し出されたサブツリーを引き取る。ここが
9808
+ // 「state が確実に居る」最初の瞬間で、router の auto バンドルが
9809
+ // state のそれより先に走る順序を吸収できる唯一の場所である。
9810
+ drainPendingBinds();
8962
9811
  resolve();
8963
9812
  }
8964
9813
  catch (error) {
@@ -9068,6 +9917,17 @@ function notifyUpdateBatchListeners(batch) {
9068
9917
  registered.listener(batch);
9069
9918
  }
9070
9919
  }
9920
+ /**
9921
+ * 遷移越しの適用が失敗したときの報告。
9922
+ *
9923
+ * 遷移の中では例外を同期的に呼び出し元へ投げ返せない。今日の drain は
9924
+ * queueMicrotask の中で throw する = uncaught として観測されるので、それと同じ
9925
+ * 「loud に出す」挙動へ揃える。握り潰すと `$updatedCallback` の throw が黙って
9926
+ * 消える(README の 3 層表が定める伝播の契約が破れる)。
9927
+ */
9928
+ function reportDeferredApplyFailure(error) {
9929
+ queueMicrotask(() => { throw error; });
9930
+ }
9071
9931
  class Updater {
9072
9932
  _queueUpdateRecords = [];
9073
9933
  constructor() {
@@ -9166,17 +10026,49 @@ class Updater {
9166
10026
  }
9167
10027
  }
9168
10028
  }
9169
- // context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
9170
- if (propagationContextByBinding.size > 0) {
9171
- applyChangeFromBindings(processBindings, propagationContextByBinding);
9172
- }
9173
- else {
9174
- applyChangeFromBindings(processBindings);
9175
- }
9176
10029
  // drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
9177
10030
  // testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
9178
10031
  // quarantine された address も state 値は適用済みのため通知対象に含める。
9179
- notifyUpdateBatchListeners(new Set(contextByAbsoluteAddress.keys()));
10032
+ //
10033
+ // try/finally なのは、適用側が throw しても `$watch` / `$streams` restart を
10034
+ // 落とさないため。binding 1 本の失敗は applyChangeFromBindings が隔離するので
10035
+ // ここへ来るのは $updatedCallback の throw(契約どおり loud に伝播させる)等に
10036
+ // 限られるが、そのとき drain フックまで道連れにすると「機構間の順序は固定」
10037
+ // (README の 3 層表)が黙って破れる。例外は握らない = 伝播は維持する。
10038
+ try {
10039
+ const applyBindings = () => {
10040
+ // context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
10041
+ if (propagationContextByBinding.size > 0) {
10042
+ applyChangeFromBindings(processBindings, propagationContextByBinding);
10043
+ }
10044
+ else {
10045
+ applyChangeFromBindings(processBindings);
10046
+ }
10047
+ };
10048
+ // View transition 参加点(docs/view-transition-design.md §7.2)。arbiter が
10049
+ // 居なければ runTransition はその場で applyBindings を呼び、undefined を返す
10050
+ // = 従来と完全に同じ同期適用。SSR では遷移そのものを持たない(G5)。
10051
+ //
10052
+ // 適用する binding が 0 本のバッチは arbiter へ渡さない。書き込みはバインドの
10053
+ // 有無に関わらず enqueue される(setByAddress)ため、headless なパス
10054
+ // (`$watch` 専用・`$streams` の内部状態・リスト置換の中間アドレス)への
10055
+ // 書き込みだけでもここへ到達する。それでページ全体をスナップショットするのは
10056
+ // 無駄なだけでなく、既定の mode="latest" では「アニメーションすべき DOM 変更が
10057
+ // 無い遷移」が実行中の本物の遷移をスキップしてしまう(ルート遷移が毎回途中で
10058
+ // 切れる/active が空撃ちで振動する)。
10059
+ if (inSsr() || processBindings.length === 0) {
10060
+ applyBindings();
10061
+ }
10062
+ else {
10063
+ const pending = runTransition("state", applyBindings);
10064
+ if (pending !== undefined) {
10065
+ pending.catch(reportDeferredApplyFailure);
10066
+ }
10067
+ }
10068
+ }
10069
+ finally {
10070
+ notifyUpdateBatchListeners(new Set(contextByAbsoluteAddress.keys()));
10071
+ }
9180
10072
  }
9181
10073
  }
9182
10074
  const updater = new Updater();
@@ -9566,6 +10458,58 @@ function registerDevtoolsSource() {
9566
10458
  getOrCreateHookRegistry().register(source);
9567
10459
  }
9568
10460
 
10461
+ /**
10462
+ * ssr-snapshot プロトコルの提供側(docs/ssr-router-design.md §5)。
10463
+ *
10464
+ * `<wcs-ssr>` スナップショットを document 全体に対する最終パスとして生成する。
10465
+ * connectedCallback 内の inline 生成は「その時点の DOM」しか見えず、router が
10466
+ * 後から挿入するルート内容の構造テンプレートを取り逃がすレースがあった
10467
+ * (state のロード方式と文書順に依存)。renderToString が全要素の完了と
10468
+ * バインディング構築の後にこれを呼ぶことで、スナップショットは常に確定後の
10469
+ * DOM を見る。
10470
+ *
10471
+ * 複数 `enable-ssr` state の意味論は inline 生成と同一に保つ(文書順に生成・
10472
+ * fragment レジストリはモジュール共有・props store は生成ごとにクリア)。
10473
+ * その整理は本プロトコルの範囲外の既存挙動として引き継ぐ。
10474
+ */
10475
+ function buildSsrDocument(root) {
10476
+ const stateTag = config.tagNames.state;
10477
+ const ssrTag = config.tagNames.ssr;
10478
+ const stateElements = root.querySelectorAll(`${stateTag}[enable-ssr]`);
10479
+ for (const stateEl of stateElements) {
10480
+ const name = stateEl.getAttribute("name") || "default";
10481
+ // 既に直前へ生成済み(旧 server との組み合わせで inline 生成された等)なら
10482
+ // 何もしない — build() は冪等でなければならない(プロトコル契約)
10483
+ const prev = stateEl.previousElementSibling;
10484
+ if (prev !== null &&
10485
+ prev.tagName.toLowerCase() === ssrTag &&
10486
+ (prev.getAttribute("name") || "default") === name) {
10487
+ continue;
10488
+ }
10489
+ const ssrEl = document.createElement(ssrTag);
10490
+ ssrEl.setAttribute("name", name);
10491
+ ssrEl.setAttribute("version", VERSION);
10492
+ Ssr.buildContent(ssrEl, Ssr.extractStateData(stateEl));
10493
+ stateEl.parentNode?.insertBefore(ssrEl, stateEl);
10494
+ }
10495
+ }
10496
+ const builder = {
10497
+ protocol: "wcs-ssr-snapshot",
10498
+ version: 1,
10499
+ build: buildSsrDocument,
10500
+ };
10501
+ /**
10502
+ * グローバル symbol へ自分を載せる。`bootstrapState` から呼ぶ。
10503
+ * binder(registerBinder)と同じ規範 — 既に別のコピーが載っているなら譲る
10504
+ * (そのコピーのレジストリが、そのページの正本だからである)。
10505
+ */
10506
+ function registerSsrSnapshotBuilder() {
10507
+ const globals = globalThis;
10508
+ if (globals[SSR_SNAPSHOT_BUILDER_KEY] === undefined) {
10509
+ globals[SSR_SNAPSHOT_BUILDER_KEY] = builder;
10510
+ }
10511
+ }
10512
+
9569
10513
  const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
9570
10514
  /**
9571
10515
  * インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
@@ -9646,9 +10590,26 @@ async function loadFromJsonFile(url) {
9646
10590
  }
9647
10591
  }
9648
10592
 
10593
+ /**
10594
+ * `src` の値を **document の base URL** に対して解決する。
10595
+ *
10596
+ * `import(url)` の相対解決は「import を書いたモジュール」を基準にする。ここは
10597
+ * `@wcstack/state` の中なので、素の `import(url)` は `<wcs-state src>` を
10598
+ * **state パッケージの所在**から解決してしまう。同一オリジンに置いたページでは
10599
+ * たまたま一致して見えるが、CDN 一発(`https://esm.run/@wcstack/state/auto`)で
10600
+ * 読み込んだ瞬間に `src="/app.js"` が CDN 側の URL を指して 404 になる。
10601
+ *
10602
+ * `src` は HTML 属性なので、正しい基準は document の base URL である
10603
+ * (`src="*.json"` 側は `fetch` がそう解決しており、同じ属性が形式によって
10604
+ * 違う基準で解決されていた)。絶対 URL・`data:`・`blob:` は URL 解決で
10605
+ * そのまま素通りするため、既存の使い方は影響を受けない。
10606
+ */
10607
+ function resolveAgainstDocument(url) {
10608
+ return new URL(url, document.baseURI).href;
10609
+ }
9649
10610
  async function loadFromScriptFile(url) {
9650
10611
  try {
9651
- const module = await import(/* @vite-ignore */ url);
10612
+ const module = await import(/* @vite-ignore */ resolveAgainstDocument(url));
9652
10613
  return module.default || {};
9653
10614
  }
9654
10615
  catch (e) {
@@ -10988,7 +11949,9 @@ function processWatchDeclaration(stateElement, state) {
10988
11949
  paths.add(path);
10989
11950
  // 依存グラフ登録(§8)。"for" 以外の bindingType は親 → 子の staticDependency
10990
11951
  // チェーンを生やすだけで listPaths / elementPaths を触らない(State.setPathInfo 参照)。
10991
- stateElement.setPathInfo(path, "prop");
11952
+ // source="watch" は存在検査の診断 code を `wcs/watch-path-missing` に切り替える
11953
+ // (watch キーの miss は raiseError にも掛からず、黙って発火しないだけになる)。
11954
+ stateElement.setPathInfo(path, "prop", "watch");
10992
11955
  }
10993
11956
  setWatchEntries(stateElement, entries);
10994
11957
  return paths.size > 0 ? paths : null;
@@ -11579,11 +12542,18 @@ function defineDCC(hostElement, shadowRoot, state) {
11579
12542
  if (!tagName.includes("-")) {
11580
12543
  raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
11581
12544
  }
11582
- if (customElements.get(tagName)) {
12545
+ // 定義先は「定義元ホストを支配するレジストリ」。scoped registry を持つツリーで
12546
+ // global に define すると、その定義は自分の兄弟にすら適用されない。
12547
+ const definitionRegistry = getCustomElementRegistry(hostElement);
12548
+ if (definitionRegistry === null || typeof definitionRegistry.define !== "function") {
12549
+ raiseError(`DCC: CustomElementRegistry is unavailable for "${tagName}".`);
12550
+ }
12551
+ if (definitionRegistry.get(tagName)) {
11583
12552
  // 重複定義は authoring error として落とす。従来は warn してスキップしていたが、
11584
12553
  // 先勝ちで別テンプレートのインスタンスが生えるため「動いているように見えて中身が違う」
11585
12554
  // 状態になる。state 名の重複(stateElementByName)が raiseError なのと作法を揃える
11586
12555
  // (docs/architecture-hardening/15-state-component-mechanism-consistency.md §3.4)。
12556
+ // 一意性はレジストリ単位なので、別スコープの同名 DCC は衝突しない。
11587
12557
  raiseError(`DCC: "${tagName}" is already registered. A custom element name can only be defined once.`);
11588
12558
  }
11589
12559
  // ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
@@ -11638,7 +12608,7 @@ function defineDCC(hostElement, shadowRoot, state) {
11638
12608
  // カスタム要素として upgrade されていない。ホストが接続済みなら appendChild の時点で
11639
12609
  // upgrade されるが、未接続の shadow に挿した場合は upgrade 契機が無く、内側の
11640
12610
  // <wcs-state> が素の HTMLElement のまま残って createState が生えない。明示的に upgrade する。
11641
- const registry = getCustomElementRegistry();
12611
+ const registry = getCustomElementRegistry(this._shadow);
11642
12612
  if (registry !== null) {
11643
12613
  upgradeCustomElement(registry, this._shadow);
11644
12614
  }
@@ -11705,7 +12675,7 @@ function defineDCC(hostElement, shadowRoot, state) {
11705
12675
  });
11706
12676
  }
11707
12677
  // カスタム要素登録
11708
- customElements.define(tagName, DCCElement);
12678
+ definitionRegistry.define(tagName, DCCElement);
11709
12679
  }
11710
12680
 
11711
12681
  /**
@@ -11852,6 +12822,41 @@ function disconnectedCallback(target, _prop, receiver, _handler) {
11852
12822
  }
11853
12823
  }
11854
12824
 
12825
+ /**
12826
+ * getContextListIndex.ts
12827
+ *
12828
+ * Stateの内部APIとして、現在のプロパティ参照スコープにおける
12829
+ * 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
12830
+ * リストインデックス(IListIndex)を取得する関数です。
12831
+ *
12832
+ * 主な役割:
12833
+ * - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
12834
+ * - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
12835
+ *
12836
+ * 設計ポイント:
12837
+ * - 直近のプロパティ参照情報を取得
12838
+ * - info.indexByWildcardPathからstructuredPathのインデックスを特定
12839
+ * - listIndex.at(index)で該当階層のリストインデックスを取得
12840
+ * - パスが一致しない場合や参照が存在しない場合はnullを返す
12841
+ */
12842
+ function getContextListIndex(handler, structuredPath) {
12843
+ if (handler.addressStackLength === 0) {
12844
+ return null;
12845
+ }
12846
+ const address = handler.lastAddressStack;
12847
+ if (address === null) {
12848
+ return null;
12849
+ }
12850
+ const index = address.pathInfo.indexByWildcardPath[structuredPath];
12851
+ if (typeof index === "undefined") {
12852
+ return null;
12853
+ }
12854
+ if (address.listIndex === null) {
12855
+ return null;
12856
+ }
12857
+ return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
12858
+ }
12859
+
11855
12860
  const cacheEntryByAbsoluteStateAddress = new WeakMap();
11856
12861
  function getCacheEntryByAbsoluteStateAddress(address) {
11857
12862
  return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
@@ -12077,7 +13082,10 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
12077
13082
  }
12078
13083
  }
12079
13084
  else {
12080
- const parentAddress = address.parentAddress ?? raiseError(`address.parentAddress is undefined path: ${address.pathInfo.path}`);
13085
+ // 親アドレスが無い 単一セグメントのパスが state に存在しない。ここは元から
13086
+ // throw していたが、文面が内部実装の言葉だったので打ち間違いだと分からなかった。
13087
+ // 深いパスの console.warn(pathDiagnostics.checkDeclaredPath)と語彙を揃える。
13088
+ const parentAddress = address.parentAddress ?? raiseError(missingRootPathMessage(stateElement.name, address.pathInfo.path, target, stateElement.getterPaths));
12081
13089
  const parentValue = getByAddress(target, parentAddress, receiver, handler);
12082
13090
  // 親が居ないパスの読みは undefined(=「state に意見が無い」)。`Reflect.get` に
12083
13091
  // そのまま渡すと生の `TypeError: Reflect.get called on non-object` になり、
@@ -12095,7 +13103,11 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
12095
13103
  }
12096
13104
  const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
12097
13105
  if (lastSegment === WILDCARD) {
12098
- const index = address.listIndex?.index ?? raiseError(`address.listIndex?.index is undefined path: ${address.pathInfo.path}`);
13106
+ // listIndex が無いまま末尾ワイルドカードに到達 そのパスの階数を満たす
13107
+ // ループ文脈が無い(`matrix.*.*` を 1 段の `for` の中で読む等)。元の文面は
13108
+ // 内部の言葉(address.listIndex?.index is undefined)で、何段必要なのかが
13109
+ // 書かれていなかった(pathDiagnostics.ts)。
13110
+ const index = address.listIndex?.index ?? raiseError(wildcardScopeMessage(`path "${address.pathInfo.path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
12099
13111
  return Reflect.get(parentValue, index);
12100
13112
  }
12101
13113
  else {
@@ -12132,38 +13144,112 @@ function getByAddress(target, address, receiver, handler) {
12132
13144
  }
12133
13145
 
12134
13146
  /**
12135
- * getContextListIndex.ts
13147
+ * wildcardIndexes.ts
12136
13148
  *
12137
- * Stateの内部APIとして、現在のプロパティ参照スコープにおける
12138
- * 指定したstructuredPath(ワイルドカード付きプロパティパス)に対応する
12139
- * リストインデックス(IListIndex)を取得する関数です。
13149
+ * ワイルドカードを含むパスから「解決済み添字タプルの集合」を列挙する共有走査。
13150
+ * `$getAll`(読み)と `$setAll`(書き)が**同じ展開規則・同じ順序**で動くための単一の正本
13151
+ * (docs/state-set-all-design.md §6-1)。
12140
13152
  *
12141
- * 主な役割:
12142
- * - handlerの最後にアクセスされたAddressから、指定パスに対応するリストインデックスを取得
12143
- * - ワイルドカード階層に対応し、多重ループやネストした配列バインディングにも利用可能
13153
+ * 添字は**前方一致の接頭辞**で、足りない分は「その階層を全部展開する」という意味を持つ
13154
+ * (README の `$getAll("scores.*", [])` がこれ)。返るタプルは常にワイルドカードの本数と
13155
+ * 同じ長さになるので、そのまま `$resolve` の厳密一致な添字として使える。
12144
13156
  *
12145
- * 設計ポイント:
12146
- * - 直近のプロパティ参照情報を取得
12147
- * - info.indexByWildcardPathからstructuredPathのインデックスを特定
12148
- * - listIndex.at(index)で該当階層のリストインデックスを取得
12149
- * - パスが一致しない場合や参照が存在しない場合はnullを返す
13157
+ * 順序は**深さ優先・添字昇順**(ネストは添字タプルの辞書順)で決定的。
13158
+ * `$getAll(p, i)` の戻り順と `$setAll(p, i, …)` の適用順が一致する根拠がこれであり、
13159
+ * `$setAll` の `{ spread: true }` 形はこの順序に乗っている。
13160
+ *
13161
+ * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
12150
13162
  */
12151
- function getContextListIndex(handler, structuredPath) {
12152
- if (handler.addressStackLength === 0) {
12153
- return null;
12154
- }
12155
- const address = handler.lastAddressStack;
12156
- if (address === null) {
12157
- return null;
12158
- }
12159
- const index = address.pathInfo.indexByWildcardPath[structuredPath];
12160
- if (typeof index === "undefined") {
12161
- return null;
12162
- }
12163
- if (address.listIndex === null) {
12164
- return null;
13163
+ /**
13164
+ * 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
13165
+ * ListIndex の同一性を跨いで保つために使う。
13166
+ *
13167
+ * 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
13168
+ * 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
13169
+ */
13170
+ // ToDo: IAbsoluteStateAddressに変更する
13171
+ const lastValueByListAddress = new WeakMap();
13172
+ /**
13173
+ * `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
13174
+ * マッチする添字タプルを列挙する。
13175
+ *
13176
+ * 添字の本数検査(上限)は呼び出し側の責務 — API 名を診断メッセージに出すため。
13177
+ */
13178
+ function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, options) {
13179
+ const newValueByAddress = new Map();
13180
+ const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
13181
+ const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
13182
+ if (wildcardParentPathInfo === null) {
13183
+ results.push(parentIndexes);
13184
+ return;
13185
+ }
13186
+ const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13187
+ const oldValue = lastValueByListAddress.get(wildcardAddress);
13188
+ const newValue = getByAddress(target, wildcardAddress, receiver, handler);
13189
+ const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
13190
+ const listIndexes = listDiff.newIndexes;
13191
+ const index = indexes[indexPos] ?? null;
13192
+ newValueByAddress.set(wildcardAddress, newValue);
13193
+ if (index === null) {
13194
+ for (let i = 0; i < listIndexes.length; i++) {
13195
+ const listIndex = listIndexes[i];
13196
+ walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13197
+ }
13198
+ }
13199
+ else {
13200
+ // 範囲外 index はリスト自体の不在と別原因なので index を含める
13201
+ // (docs/state-bind-component-nested-for-design.md §8.4)
13202
+ const listIndex = listIndexes[index] ??
13203
+ raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13204
+ if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
13205
+ walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13206
+ }
13207
+ else {
13208
+ // 最終ワイルドカード層まで到達しているので、結果を確定
13209
+ results.push(parentIndexes.concat(listIndex.index));
13210
+ }
13211
+ }
13212
+ };
13213
+ const resultIndexes = [];
13214
+ walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
13215
+ if (options.commitDiffBaseline) {
13216
+ for (const [address, newValue] of newValueByAddress.entries()) {
13217
+ lastValueByListAddress.set(address, newValue);
13218
+ }
12165
13219
  }
12166
- return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
13220
+ return resultIndexes;
13221
+ }
13222
+
13223
+ /**
13224
+ * getListIndexByIndexes.ts
13225
+ *
13226
+ * 解決済みの添字タプル(ワイルドカード 1 段につき 1 個)から、対応する ListIndex を
13227
+ * **正本レジストリ**(listIndexesByList)経由で引き当てる。
13228
+ *
13229
+ * `$resolve` と `$setAll` の共有部分。列挙側(wildcardIndexes.ts)が走査中に生成した
13230
+ * ListIndex をそのまま書き込み先にせず、ここで引き直すことで、binding が使っている
13231
+ * ListIndex と同一の同一性に載る(docs/state-set-all-design.md §6-2)。
13232
+ *
13233
+ * 添字の本数がワイルドカードの本数と一致していることは呼び出し側の責務。
13234
+ */
13235
+ function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
13236
+ // ワイルドカード階層ごとにListIndexを解決していく
13237
+ let listIndex = null;
13238
+ for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13239
+ const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
13240
+ const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13241
+ const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
13242
+ const listIndexes = getListIndexesByList(tmpValue);
13243
+ if (listIndexes == null) {
13244
+ raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
13245
+ }
13246
+ const index = indexes[i];
13247
+ // 範囲外 index はリスト自体の不在と別原因なので index を含める
13248
+ // (docs/state-bind-component-nested-for-design.md §8.4)
13249
+ listIndex = listIndexes[index] ??
13250
+ raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13251
+ }
13252
+ return listIndex;
12167
13253
  }
12168
13254
 
12169
13255
  /**
@@ -12891,7 +13977,8 @@ function _setByAddress(target, address, absAddress, value, receiver, handler, ke
12891
13977
  const parentValue = getByAddress(target, parentAddress, receiver, handler);
12892
13978
  const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
12893
13979
  if (lastSegment === WILDCARD) {
12894
- const index = address.listIndex?.index ?? raiseError(`address.listIndex?.index is undefined path: ${address.pathInfo.path}`);
13980
+ // 読み取り側(getByAddress)と同じ取り違え。書き込みでも何段必要かを言う。
13981
+ const index = address.listIndex?.index ?? raiseError(wildcardScopeMessage(`path "${address.pathInfo.path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
12895
13982
  return Reflect.set(parentValue, index, value);
12896
13983
  }
12897
13984
  else {
@@ -13060,7 +14147,9 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
13060
14147
  recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
13061
14148
  try {
13062
14149
  if (key === undefined) {
13063
- raiseError(`address.listIndex?.index is undefined path: ${path}`);
14150
+ // fast path 版の同じ取り違え(末尾ワイルドカードに listIndex が無い)。
14151
+ // 通常経路と同じ語彙で「何段必要か」を言う(pathDiagnostics.ts)。
14152
+ raiseError(wildcardScopeMessage(`path "${path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
13064
14153
  }
13065
14154
  return Reflect.set(parentValue, key, value);
13066
14155
  }
@@ -13159,25 +14248,15 @@ function resolve(target, _prop, receiver, handler) {
13159
14248
  }
13160
14249
  }
13161
14250
  }
13162
- if (pathInfo.wildcardParentPathInfos.length > indexes.length) {
13163
- raiseError(`indexes length is insufficient: ${path}`);
13164
- }
13165
- // ワイルドカード階層ごとにListIndexを解決していく
13166
- let listIndex = null;
13167
- for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13168
- const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
13169
- const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13170
- const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
13171
- const listIndexes = getListIndexesByList(tmpValue);
13172
- if (listIndexes == null) {
13173
- raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
13174
- }
13175
- const index = indexes[i];
13176
- // 範囲外 index はリスト自体の不在と別原因なので index を含める
13177
- // (docs/state-bind-component-nested-for-design.md §8.4)
13178
- listIndex = listIndexes[index] ??
13179
- raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
14251
+ // 添字の本数はワイルドカードの本数と**厳密に一致**する必要がある。
14252
+ // 不足は元から throw していたが、超過は黙って無視されていた(余分な要素を
14253
+ // 誰も読まないため)= `$resolve("items.*.price", [row, col])` のような
14254
+ // 「1 本しか無いのに 2 本渡す」取り違えが、間違った値を返したまま通っていた。
14255
+ if (indexes.length !== pathInfo.wildcardParentPathInfos.length) {
14256
+ raiseError(indexArityMessage("$resolve", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
13180
14257
  }
14258
+ // ワイルドカード階層ごとにListIndexを解決していく(`$setAll` と共有)
14259
+ const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, indexes);
13181
14260
  // ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
13182
14261
  const address = createStateAddress(pathInfo, listIndex);
13183
14262
  const hasSetValue = typeof value !== "undefined";
@@ -13194,14 +14273,18 @@ function resolve(target, _prop, receiver, handler) {
13194
14273
  * getAllReadonly
13195
14274
  *
13196
14275
  * ワイルドカードを含む State パスから、対象となる全要素を配列で取得する。
13197
- * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
14276
+ * 走査そのものは `$setAll` と共有する(wildcardIndexes.ts)。
14277
+ *
14278
+ * `indexes` 省略時の既定はループ文脈の添字 `[$1..$n]`。正確には「path と文脈が
14279
+ * 共有するワイルドカード連鎖の分だけ文脈の添字を接頭辞として敷く」(整合最長接頭辞)。
14280
+ * 共有が無いのに文脈が添字を持つ場合は throw する — 異なる文脈の添字は流用しない。
14281
+ *
14282
+ * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)、
14283
+ * 添字本数超過(wcs/index-arity)、省略時の文脈不整合(getAllContextMismatchMessage)
13198
14284
  */
13199
- // ToDo: IAbsoluteStateAddressに変更する
13200
- const lastValueByListAddress = new WeakMap();
13201
14285
  function getAll(target, prop, receiver, handler) {
13202
14286
  const resolveFn = resolve(target, prop, receiver, handler);
13203
14287
  return (path, indexes) => {
13204
- const newValueByAddress = new Map();
13205
14288
  const pathInfo = getPathInfo(path);
13206
14289
  if (handler.addressStackLength > 0) {
13207
14290
  const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
@@ -13213,61 +14296,48 @@ function getAll(target, prop, receiver, handler) {
13213
14296
  }
13214
14297
  }
13215
14298
  }
14299
+ // 明示的に渡された添字だけを検査する。`$getAll` の添字は**前方一致の接頭辞**で、
14300
+ // 足りない分は「その階層を全部展開する」という正しい意味を持つ(README の
14301
+ // `$getAll("scores.*", [])` がこれ)。一方**超過は意味を持たず黙って捨てられ**、
14302
+ // ワイルドカードの本数を取り違えたまま部分集合が返っていた。
14303
+ // 省略時に下で導出する添字は文脈由来なので、この検査には掛けない。
14304
+ if (typeof indexes !== "undefined" && indexes.length > pathInfo.wildcardParentPathInfos.length) {
14305
+ raiseError(indexArityMessage("$getAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
14306
+ }
13216
14307
  if (typeof indexes === "undefined") {
13217
- for (let i = 0; i < pathInfo.wildcardParentPathInfos.length; i++) {
13218
- const wildcardPattern = pathInfo.wildcardParentPathInfos[i];
13219
- const listIndex = getContextListIndex(handler, wildcardPattern.path);
14308
+ // 省略時の既定はループ文脈の添字 `[$1..$n]`。ただし敷けるのは path と文脈が
14309
+ // **共有するワイルドカード連鎖**の分だけなので、path のワイルドカードを
14310
+ // 内側(最深)から探し、最初に文脈にヒットした階層の scoped indexes を接頭辞にする。
14311
+ // ワイルドカードパスの序数はパス文字列自身の `*` の本数で決まるため、深い側が
14312
+ // ヒットすれば浅い側は必ず含まれ、これが整合する最長の接頭辞になる。文脈が
14313
+ // path より深い分は自然に切り詰められ、導出した接頭辞は path のワイルドカード
14314
+ // 本数を超えないので、上の本数検査には掛けない。
14315
+ for (let i = pathInfo.wildcardPaths.length - 1; i >= 0; i--) {
14316
+ const listIndex = getContextListIndex(handler, pathInfo.wildcardPaths[i]);
13220
14317
  if (listIndex) {
13221
14318
  indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
13222
14319
  break;
13223
14320
  }
13224
14321
  }
13225
14322
  if (typeof indexes === "undefined") {
14323
+ // 共有ゼロ。文脈が自スコープの添字を実際に持っているなら、既定の `[...$n]` は
14324
+ // **異なる文脈の添字の流用(混入)**になるため、黙って全展開へ倒さず throw する。
14325
+ // 文脈そのものが無い(トップレベル getter / メソッド直下)なら全展開が既定。
14326
+ const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
14327
+ const contextListIndex = lastAddress?.listIndex ?? null;
14328
+ if (pathInfo.wildcardCount > 0 && lastAddress !== null && contextListIndex !== null &&
14329
+ contextListIndex.length - getBaseDepth(handler.stateElement) > 0) {
14330
+ raiseError(getAllContextMismatchMessage(path, lastAddress.pathInfo.path));
14331
+ }
13226
14332
  indexes = [];
13227
14333
  }
13228
14334
  }
13229
- const walkWildcardPattern = (wildcardParentPathInfos, wildcardIndexPos, listIndex, indexes, indexPos, parentIndexes, results) => {
13230
- const wildcardParentPathInfo = wildcardParentPathInfos[wildcardIndexPos] ?? null;
13231
- if (wildcardParentPathInfo === null) {
13232
- results.push(parentIndexes);
13233
- return;
13234
- }
13235
- const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13236
- const oldValue = lastValueByListAddress.get(wildcardAddress);
13237
- const newValue = getByAddress(target, wildcardAddress, receiver, handler);
13238
- const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
13239
- const listIndexes = listDiff.newIndexes;
13240
- const index = indexes[indexPos] ?? null;
13241
- newValueByAddress.set(wildcardAddress, newValue);
13242
- if (index === null) {
13243
- for (let i = 0; i < listIndexes.length; i++) {
13244
- const listIndex = listIndexes[i];
13245
- walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13246
- }
13247
- }
13248
- else {
13249
- // 範囲外 index はリスト自体の不在と別原因なので index を含める
13250
- // (docs/state-bind-component-nested-for-design.md §8.4)
13251
- const listIndex = listIndexes[index] ??
13252
- raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
13253
- if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
13254
- walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
13255
- }
13256
- else {
13257
- // 最終ワイルドカード層まで到達しているので、結果を確定
13258
- results.push(parentIndexes.concat(listIndex.index));
13259
- }
13260
- }
13261
- };
13262
- const resultIndexes = [];
13263
- walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
14335
+ // 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
14336
+ const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
13264
14337
  const resultValues = [];
13265
14338
  for (let i = 0; i < resultIndexes.length; i++) {
13266
14339
  resultValues.push(resolveFn(pathInfo.path, resultIndexes[i]));
13267
14340
  }
13268
- for (const [address, newValue] of newValueByAddress.entries()) {
13269
- lastValueByListAddress.set(address, newValue);
13270
- }
13271
14341
  return resultValues;
13272
14342
  };
13273
14343
  }
@@ -13351,6 +14421,83 @@ function postUpdate(target, _prop, receiver, handler) {
13351
14421
  };
13352
14422
  }
13353
14423
 
14424
+ /**
14425
+ * setAll.ts
14426
+ *
14427
+ * ワイルドカードを含む State パスにマッチする**全アドレスへ一括で書き込む**。
14428
+ * `$getAll`(読み)の対称形(docs/state-set-all-design.md)。
14429
+ *
14430
+ * 存在理由は糖衣ではなく「**リスト全置換の回避**」(設計 §1-1)。
14431
+ * `this.users = this.users.map(...)` は配列を作り直すので ListIndex・行 getter
14432
+ * キャッシュ・差分描画がまとめて作り直しになる。`$setAll` は意味としては一括更新、
14433
+ * 実体は in-place な個別書き込みで、同じことを差分に載せたまま行う。
14434
+ *
14435
+ * 3 つの形(設計 §2):
14436
+ * - ブロードキャスト `$setAll(path, indexes, value)`
14437
+ * - mapper(第一級) `$setAll(path, indexes, (current, ...indexes) => next)`
14438
+ * - spread `$setAll(path, indexes, values, { spread: true })`
14439
+ */
14440
+ function setAll(target, _prop, receiver, handler) {
14441
+ return (path, indexes, value, options) => {
14442
+ const pathInfo = getPathInfo(path);
14443
+ // 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
14444
+ // 「現在行」ではなく「全行」を意味する(設計 §4-1)。
14445
+ if (!Array.isArray(indexes)) {
14446
+ raiseError(setAllValueKindMessage(path, "requires an explicit indexes array (pass [] to expand every level)."));
14447
+ }
14448
+ // 添字は前方一致の接頭辞なので不足は正当。超過だけを弾く(`$getAll` と同じ規則)。
14449
+ if (indexes.length > pathInfo.wildcardParentPathInfos.length) {
14450
+ raiseError(indexArityMessage("$setAll", path, pathInfo.wildcardParentPathInfos.length, indexes.length));
14451
+ }
14452
+ const spread = options?.spread === true;
14453
+ const isMapper = typeof value === "function";
14454
+ if (spread && isMapper) {
14455
+ raiseError(setAllValueKindMessage(path, "cannot combine { spread: true } with a mapper function."));
14456
+ }
14457
+ if (spread && !Array.isArray(value)) {
14458
+ raiseError(setAllValueKindMessage(path, "requires an array as the value when { spread: true } is set."));
14459
+ }
14460
+ // --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
14461
+ // 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
14462
+ // 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
14463
+ const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
14464
+ if (spread && value.length !== resultIndexes.length) {
14465
+ raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
14466
+ }
14467
+ const addresses = [];
14468
+ for (let i = 0; i < resultIndexes.length; i++) {
14469
+ const listIndex = getListIndexByIndexes(target, receiver, handler, pathInfo, resultIndexes[i]);
14470
+ addresses.push(createStateAddress(pathInfo, listIndex));
14471
+ }
14472
+ // --- 第 2 相: 確定したアドレスにだけ書く ---
14473
+ let written = 0;
14474
+ for (let i = 0; i < addresses.length; i++) {
14475
+ const address = addresses[i];
14476
+ let nextValue;
14477
+ if (isMapper) {
14478
+ // 現在値は書く直前に読む。先行する書き込みが getter 経由で他行に及ぶ場合、
14479
+ // mapper が見るべきなのは最新値。
14480
+ const currentValue = getByAddress(target, address, receiver, handler);
14481
+ nextValue = value(currentValue, ...resultIndexes[i]);
14482
+ }
14483
+ else if (spread) {
14484
+ nextValue = value[i];
14485
+ }
14486
+ else {
14487
+ nextValue = value;
14488
+ }
14489
+ // undefined は常にスキップ(設計 §5)。mapper の return 忘れで全行を潰さないため、
14490
+ // かつ「この行は変えない」を表現できるようにするため。クリアは null。
14491
+ if (typeof nextValue === "undefined") {
14492
+ continue;
14493
+ }
14494
+ setByAddress(target, address, nextValue, receiver, handler);
14495
+ written++;
14496
+ }
14497
+ return written;
14498
+ };
14499
+ }
14500
+
13354
14501
  /**
13355
14502
  * trackDependency.ts
13356
14503
  *
@@ -13525,7 +14672,7 @@ function setLoopContext(handler, loopContext, callback) {
13525
14672
  * StateClassのProxyトラップとして、プロパティアクセス時の値取得処理を担う関数(get)の実装です。
13526
14673
  *
13527
14674
  * 主な役割:
13528
- * - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $postUpdate,
14675
+ * - 文字列プロパティの場合、特殊プロパティ($1〜、$stateElement, $getAll, $setAll, $postUpdate,
13529
14676
  * $resolve, $trackDependency, $command, $streamStatus, $streamError)に応じた値やAPIを返却
13530
14677
  * - 通常のプロパティはgetResolvedPathInfoでパス情報を解決し、getListIndexでリストインデックスを取得
13531
14678
  * - getByRefで構造化パス・リストインデックスに対応した値を取得
@@ -13588,6 +14735,11 @@ function get(target, prop, receiver, handler) {
13588
14735
  return getAll(target, prop, receiver, handler)(path, indexes);
13589
14736
  };
13590
14737
  }
14738
+ case "$setAll": {
14739
+ return (path, indexes, value, options) => {
14740
+ return setAll(target, prop, receiver, handler)(path, indexes, value, options);
14741
+ };
14742
+ }
13591
14743
  case "$postUpdate": {
13592
14744
  return (path) => {
13593
14745
  return postUpdate(target, prop, receiver, handler)(path);
@@ -13719,6 +14871,8 @@ function set(target, prop, value, receiver, handler) {
13719
14871
  }
13720
14872
  }
13721
14873
 
14874
+ /** 循環報告に載せるスタック末尾の段数(当事者が見える最小限) */
14875
+ const CYCLE_REPORT_DEPTH = 8;
13722
14876
  class StateHandler {
13723
14877
  _stateElement;
13724
14878
  _stateName;
@@ -13759,12 +14913,36 @@ class StateHandler {
13759
14913
  return this._loopContext;
13760
14914
  }
13761
14915
  pushAddress(address) {
13762
- this._addressStackIndex++;
13763
- if (this._addressStackIndex >= MAX_LOOP_DEPTH) {
13764
- raiseError(`Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH}. Possible infinite loop.`);
14916
+ // 上限判定は **increment より前**に行う。後にすると、深さ超過で throw した時点で
14917
+ // `_addressStackIndex` だけが進み `_addressStack[index]` は未代入のまま残る。
14918
+ // 呼び出し側(getByAddress)は `pushAddress` try の外で呼ぶので自分では pop
14919
+ // しないが、外側フレームの finally が順に pop していき、その 1 本目が未代入の枠を
14920
+ // 引いて `Address stack at index N is undefined.` を投げる = **本来の
14921
+ // 「無限ループの疑い」という診断が巻き戻しの最中に上書きされて消える**。
14922
+ // getter の相互参照(`get a(){return this.b}` / `get b(){return this.a}`)は
14923
+ // 実際にこれを踏み、原因と無関係な文面だけが残っていた。
14924
+ if (this._addressStackIndex + 1 >= MAX_LOOP_DEPTH) {
14925
+ raiseError(`Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH}. ` +
14926
+ `Possible circular dependency between path getters: ${this._describeAddressCycle()}`);
13765
14927
  }
14928
+ this._addressStackIndex++;
13766
14929
  this._addressStack[this._addressStackIndex] = address;
13767
14930
  }
14931
+ /**
14932
+ * スタック末尾の繰り返し区間をパス名で示す(循環の当事者だけを見せる)。
14933
+ * 上限に達したときのみ呼ばれるので、コストは異常系に閉じている。
14934
+ */
14935
+ _describeAddressCycle() {
14936
+ const paths = [];
14937
+ for (let i = this._addressStackIndex; i >= 0 && paths.length < CYCLE_REPORT_DEPTH; i--) {
14938
+ const entry = this._addressStack[i];
14939
+ if (entry) {
14940
+ paths.push(entry.pathInfo.path);
14941
+ }
14942
+ }
14943
+ const unique = Array.from(new Set(paths));
14944
+ return `${unique.reverse().join(" -> ")} -> ...`;
14945
+ }
13768
14946
  popAddress() {
13769
14947
  if (this._addressStackIndex < 0) {
13770
14948
  return null;
@@ -14353,7 +15531,13 @@ class State extends HTMLElementBase {
14353
15531
  raiseError(`"bind-component" cannot be combined with ${conflicting.join(", ")}. The component's "${this.getAttribute("bind-component")}" property is the only state source.`);
14354
15532
  }
14355
15533
  const boundComponentStateProp = this.getAttribute("bind-component");
14356
- await customElements.whenDefined(customTagName.toLowerCase());
15534
+ const componentRegistry = getCustomElementRegistry(boundComponent);
15535
+ if (componentRegistry === null) {
15536
+ // null レジストリのサブツリーではホストは永久に upgrade されない。
15537
+ // whenDefined を待つと無言でウェッジするので落とす。
15538
+ raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
15539
+ }
15540
+ await componentRegistry.whenDefined(customTagName.toLowerCase());
14357
15541
  // data-wcs属性がある場合は、上位の状態によりbinding情報の設定が完了するまで待機する
14358
15542
  if (boundComponent.hasAttribute(config.bindAttributeName)) {
14359
15543
  await waitInitializeBinding(boundComponent);
@@ -14518,8 +15702,12 @@ class State extends HTMLElementBase {
14518
15702
  if (!this.hasAttribute('enable-ssr') || inSsr()) {
14519
15703
  await this._callStateConnectedCallback();
14520
15704
  }
14521
- // サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成
14522
- if (inSsr() && this.hasAttribute('enable-ssr')) {
15705
+ // サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
15706
+ // orchestrated(サーバー主導の最終パス、docs/ssr-router-design.md §5)では
15707
+ // 生成しない — renderToString が全要素の完了後にまとめて生成するため。
15708
+ // ここで生成すると、router 等が後から挿入した内容の構造テンプレートを
15709
+ // 取り逃がすレースがある(state のロード方式と文書順に依存)
15710
+ if (inSsr() && this.hasAttribute('enable-ssr') && !isOrchestratedSsr()) {
14523
15711
  try {
14524
15712
  await getBindingsReady(this.rootNode);
14525
15713
  const name = this.getAttribute('name') || 'default';
@@ -14727,7 +15915,7 @@ class State extends HTMLElementBase {
14727
15915
  addStaticDependency(sourcePath, targetPath) {
14728
15916
  return this._addDependency(this._staticDependency, sourcePath, targetPath);
14729
15917
  }
14730
- setPathInfo(path, bindingType) {
15918
+ setPathInfo(path, bindingType, source = "binding") {
14731
15919
  if (bindingType === "for") {
14732
15920
  const isNewListPath = !this._listPaths.has(path);
14733
15921
  this._listPaths.add(path);
@@ -14742,6 +15930,10 @@ class State extends HTMLElementBase {
14742
15930
  if (!this._pathSet.has(path)) {
14743
15931
  const pathInfo = getPathInfo(path);
14744
15932
  this._pathSet.add(path);
15933
+ // 存在しないパスへの配線は「黙って更新されない」だけで終わるため、
15934
+ // 新規パスを 1 回だけ検査して確実な miss を報告する(pathDiagnostics.ts)。
15935
+ // パスごとに 1 回・バインド確立時のみで、更新のホットパスには乗らない。
15936
+ checkDeclaredPath(this, this.__state, path, source);
14745
15937
  if (pathInfo.parentPath !== null) {
14746
15938
  let currentPathInfo = pathInfo;
14747
15939
  while (currentPathInfo.parentPath !== null) {
@@ -14803,20 +15995,77 @@ class State extends HTMLElementBase {
14803
15995
  }
14804
15996
  }
14805
15997
 
14806
- function registerComponents() {
14807
- if (!customElements.get(config.tagNames.ssr)) {
14808
- customElements.define(config.tagNames.ssr, Ssr);
15998
+ /**
15999
+ * Register this package's tags. Pass a scoped `CustomElementRegistry` to define
16000
+ * them for a single shadow tree -- scoped registries do not inherit the global
16001
+ * one, so a tree using one needs its own definitions.
16002
+ */
16003
+ function registerComponents(registry = customElements) {
16004
+ if (!registry.get(config.tagNames.ssr)) {
16005
+ registry.define(config.tagNames.ssr, Ssr);
14809
16006
  }
14810
- if (!customElements.get(config.tagNames.state)) {
14811
- customElements.define(config.tagNames.state, State);
16007
+ if (!registry.get(config.tagNames.state)) {
16008
+ registry.define(config.tagNames.state, State);
14812
16009
  }
14813
16010
  }
14814
16011
 
14815
- function bootstrapState(config) {
14816
- if (config) {
14817
- setConfig(config);
16012
+ /**
16013
+ * `<html lang>` を既定ロケールとして採る。
16014
+ *
16015
+ * ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は `config.locale`
16016
+ * を読むが、それを設定できる公開の入口は `bootstrapState({ locale })` しかない。
16017
+ * 一方 `auto` エントリは `bootstrapState()` を引数なしで呼ぶため、CDN 一発
16018
+ * (`<script src=".../@wcstack/state/auto">`)で読み込んだページには**ロケールを
16019
+ * 渡す口が無かった**。auto バンドルは SRI のため自己完結で、別途 `@wcstack/state`
16020
+ * を import して `bootstrapState` を呼んでも別インスタンスになり効かない。
16021
+ *
16022
+ * `<html lang>` はページのロケールを書く HTML 標準の場所であり、SSR ではサーバーが、
16023
+ * 静的ページでは head のスニペットが DOM 解析前に書く。そこを既定にすると
16024
+ * **ロケールの正本が 1 つになり**、「設定を早く呼ぶ」という守りにくい順序の約束が
16025
+ * 「`<html lang>` が state のロードより前にある」という構造的な保証に変わる。
16026
+ *
16027
+ * 明示指定(`bootstrapState({ locale })`)が常に優先する。
16028
+ */
16029
+ function localeFromDocument() {
16030
+ const lang = document.documentElement?.lang;
16031
+ if (!lang) {
16032
+ return undefined;
16033
+ }
16034
+ try {
16035
+ // 妥当な BCP-47 タグでなければ Intl が RangeError を投げる。不正な lang を
16036
+ // そのまま採ると、これまで既定 'en' で動いていたページのフィルタが実行時に
16037
+ // 落ちる。既定へ落として警告するほうが、黙って壊すより回復しやすい。
16038
+ Intl.getCanonicalLocales(lang);
16039
+ return lang;
16040
+ }
16041
+ catch {
16042
+ console.warn(`[@wcstack/state] <html lang="${lang}"> is not a valid BCP-47 language tag. ` +
16043
+ `Falling back to the default locale for filters.`);
16044
+ return undefined;
16045
+ }
16046
+ }
16047
+ function resolveConfig(config) {
16048
+ if (typeof config?.locale === "string") {
16049
+ return config;
16050
+ }
16051
+ const locale = localeFromDocument();
16052
+ if (locale === undefined) {
16053
+ return config;
16054
+ }
16055
+ return { ...config, locale };
16056
+ }
16057
+ function bootstrapState(config, registry) {
16058
+ const resolved = resolveConfig(config);
16059
+ if (resolved) {
16060
+ setConfig(resolved);
14818
16061
  }
14819
- registerComponents();
16062
+ registerComponents(registry);
16063
+ // binder プロトコルの提供(docs/binder-protocol-design.md)。router が後から
16064
+ // 差し込むノードをバインドできるようにする。登録は冪等。
16065
+ registerBinder();
16066
+ // ssr-snapshot プロトコルの提供(docs/ssr-router-design.md §5)。renderToString が
16067
+ // <wcs-ssr> 生成をサーバー主導の最終パスへ回せるようにする。登録は冪等。
16068
+ registerSsrSnapshotBuilder();
14820
16069
  // DevTools Hook Protocol への source 登録(SSR では no-op・冪等)
14821
16070
  registerDevtoolsSource();
14822
16071
  }