@wcstack/state 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -220,6 +220,7 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
220
220
  const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
221
221
  const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
222
222
  const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
223
+ const STATE_ERROR_CALLBACK_NAME = "$errorCallback";
223
224
  const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
224
225
  const STATE_BINDABLES_NAME = "$bindables";
225
226
  const STATE_COMMANDS_NAME = "$commands";
@@ -229,6 +230,14 @@ const STATE_EVENT_TOKENS_NAME = "$eventTokens";
229
230
  const STATE_ON_NAME = "$on";
230
231
  const STATE_STREAMS_NAME = "$streams";
231
232
  const STATE_WATCH_NAME = "$watch";
233
+ const STATE_RECURSION_NAME = "$recursion";
234
+ /**
235
+ * 再帰ワイルドカード。オーサリング層($recursion 宣言・getter キー・API 引数)にだけ
236
+ * 現れ、PathInfo には決して降ろさない — wildcardCount が不定になると ListIndex 連鎖長・
237
+ * $1..$n・$resolve の厳密一致・走査の段数が同時に壊れる
238
+ * (docs/state-recursive-path-design.md §2-1)。
239
+ */
240
+ const RECURSION_WILDCARD = "**";
232
241
  const STATE_LIST_KEYS_NAME = "$listKeys";
233
242
  const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
234
243
  const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
@@ -275,6 +284,10 @@ function resolveInitializedBinding(node) {
275
284
  resolvedNodes.add(node);
276
285
  }
277
286
 
287
+ function raiseError(message) {
288
+ throw new Error(`[@wcstack/state] ${message}`);
289
+ }
290
+
278
291
  const _cache$4 = new Map();
279
292
  let id = 0;
280
293
  function getPathInfo(path) {
@@ -282,6 +295,16 @@ function getPathInfo(path) {
282
295
  if (typeof pathInfo !== "undefined") {
283
296
  return pathInfo;
284
297
  }
298
+ // 再帰ワイルドカードはオーサリング層の記号で、ここへ降りてきてはならない
299
+ // (降ろすと wildcardCount が不定になり ListIndex 連鎖長・$1..$n・$resolve の
300
+ // 厳密一致・走査の段数が同時に壊れる。設計書 D2)。到達したということは、
301
+ // `**` を解釈しない消費者に `**` パスが渡ったということ。通常のパスはこの検査を
302
+ // 初回 intern のときにしか払わない(`**` パスは intern されないので読むたびに落ちる)。
303
+ if (path.indexOf(RECURSION_WILDCARD) !== -1) {
304
+ raiseError(`[wcs/recursion-unsupported] "${path}" uses "${RECURSION_WILDCARD}", which is not accepted here. ` +
305
+ `It is only meaningful in a $recursion declaration, in a recursive getter key, and in the path ` +
306
+ `argument of $getAll / $setAll — and only when the state declares a $recursion anchor.`);
307
+ }
285
308
  pathInfo = Object.freeze(new PathInfo(path));
286
309
  _cache$4.set(path, pathInfo);
287
310
  return pathInfo;
@@ -541,10 +564,6 @@ function readNamedList(value, isValidEntry) {
541
564
  return entries;
542
565
  }
543
566
 
544
- function raiseError(message) {
545
- throw new Error(`[@wcstack/state] ${message}`);
546
- }
547
-
548
567
  function makeExpandedEntry(name, base) {
549
568
  // Dot-relative spread keeps the loop item root (`.`) without producing `..foo`.
550
569
  const expandedPath = base === "." ? `.${name}` : `${base}.${name}`;
@@ -2386,6 +2405,7 @@ function processDeferredNode(entry) {
2386
2405
  let nextMountId = 0;
2387
2406
  const MOUNT_DOLLAR_DECLARATIONS = [
2388
2407
  "$watch", "$streams", "$listKeys", "$updatedCallback", "$commandTokens", "$eventTokens", "$on",
2408
+ "$recursion",
2389
2409
  ];
2390
2410
  const dollarDeclarationWarned = new Set();
2391
2411
  /**
@@ -2400,6 +2420,15 @@ const dollarDeclarationWarned = new Set();
2400
2420
  */
2401
2421
  function warnMountedDollarDeclarations(record) {
2402
2422
  const declared = MOUNT_DOLLAR_DECLARATIONS.filter((name) => typeof record.stateObject[name] !== "undefined");
2423
+ // `**` getter(`get "node.**.total"()`)も同じ扱い。`markerizeAccessorPath` は `*` セグメントしか
2424
+ // 探さないので `**` を含むキーは私有アンカーに落ち、参照されないまま永久に登録されない
2425
+ // (ボリュームは接ぎ木前に拒否し、`$recursion` はこの誘導が出るのに、`**` getter だけが
2426
+ // 無言だった — 第 3 サイクルのレビューで実測)。
2427
+ for (const accessor of record.getterKeys) {
2428
+ if (accessor.indexOf(RECURSION_WILDCARD) !== -1) {
2429
+ declared.push(`"${accessor}"`);
2430
+ }
2431
+ }
2403
2432
  if (declared.length === 0) {
2404
2433
  return;
2405
2434
  }
@@ -2410,8 +2439,8 @@ function warnMountedDollarDeclarations(record) {
2410
2439
  dollarDeclarationWarned.add(key);
2411
2440
  console.warn(`[@wcstack/state] [wcs/mount-dollar-declaration] <${key.split("|")[0]}>.${record.stateProp} declares ` +
2412
2441
  `${declared.join(", ")}, which mounted components do not run. Declare them on the root state instead ` +
2413
- `(a volume <wcs-state mount="..."> can host $watch / $listKeys / $updatedCallback). ` +
2414
- `See docs/state-mount-design.md §4-6.`);
2442
+ `(a volume <wcs-state mount="..."> can host $watch / $listKeys / $updatedCallback; $recursion and ` +
2443
+ `"**" getters expand against the root tree). See docs/state-mount-design.md §4-6.`);
2415
2444
  }
2416
2445
  /**
2417
2446
  * マウントされたコンポーネントのライフサイクル呼び出し(`$connectedCallback` /
@@ -2508,6 +2537,7 @@ function buildMountRecord(component, stateProp, bindings, parentStateElement, st
2508
2537
  accessorBySuffixByMarkerParent: new Map(),
2509
2538
  indexShiftByLoopElementPath: new Map(),
2510
2539
  addedGetterPaths: new Set(),
2540
+ exports: new Map(),
2511
2541
  };
2512
2542
  }
2513
2543
  function firstSegmentOf(path) {
@@ -2966,6 +2996,59 @@ function hasLastListValueByAbsoluteStateAddress(address) {
2966
2996
  return lastListValueByAbsoluteStateAddress.has(address);
2967
2997
  }
2968
2998
 
2999
+ const stateListBaselineByAbsoluteStateAddress = new WeakMap();
3000
+ /**
3001
+ * 更新バッチ中の観測は**即座に確定しない**。バッチ内で同じリストへ 2 回構造書き込みすると、
3002
+ * 2 回目のウォークが「一度も描画されず直後に上書きされる中間値」を基準に diff を取り、
3003
+ * 中間値が落とした行の ListIndex が鋳造し直されて子リストの台帳が恒久的に切れるため。
3004
+ *
3005
+ * バッチが開いている間の観測はここに溜め、バッチ末尾(updater の drain 終了)でまとめて
3006
+ * 確定する。こうするとバッチ内のどのウォークも「バッチ開始時の値」と diff を取り、
3007
+ * これは描画側(applyChangeToFor が描画基準で取る diff)と一致する。
3008
+ *
3009
+ * 深さで数えるのは、drain の最中に $updatedCallback などが書いて新しいバッチが
3010
+ * 始まる形があるため。0 に戻ったバッチだけが確定する。
3011
+ */
3012
+ let pendingBaselines = null;
3013
+ let batchDepth = 0;
3014
+ function getStateListBaseline(address) {
3015
+ // pending は意図的に見ない。バッチ中の読み手には「バッチ開始時の値」を返す。
3016
+ return stateListBaselineByAbsoluteStateAddress.get(address) ?? [];
3017
+ }
3018
+ function setStateListBaseline(address, value) {
3019
+ if (pendingBaselines !== null) {
3020
+ pendingBaselines.set(address, value);
3021
+ return;
3022
+ }
3023
+ stateListBaselineByAbsoluteStateAddress.set(address, value);
3024
+ }
3025
+ /** 更新バッチの開始(updater が最初の enqueue で呼ぶ)。 */
3026
+ function beginStateListBaselineBatch() {
3027
+ batchDepth++;
3028
+ if (pendingBaselines === null) {
3029
+ pendingBaselines = new Map();
3030
+ }
3031
+ }
3032
+ /** 更新バッチの終了(updater が drain の finally で呼ぶ)。入れ子が全部閉じたら確定する。 */
3033
+ function endStateListBaselineBatch() {
3034
+ if (batchDepth === 0) {
3035
+ // enqueue を経ない直接 drain(testApplyChange)。開いていないバッチは閉じない。
3036
+ return;
3037
+ }
3038
+ batchDepth--;
3039
+ if (batchDepth > 0 || pendingBaselines === null) {
3040
+ return;
3041
+ }
3042
+ for (const [address, value] of pendingBaselines) {
3043
+ stateListBaselineByAbsoluteStateAddress.set(address, value);
3044
+ }
3045
+ pendingBaselines = null;
3046
+ }
3047
+ function hasStateListBaseline(address) {
3048
+ return (pendingBaselines?.has(address) ?? false)
3049
+ || stateListBaselineByAbsoluteStateAddress.has(address);
3050
+ }
3051
+
2969
3052
  const setLoopContextSymbol = Symbol("$$setLoopContext");
2970
3053
  const getByAddressSymbol = Symbol("$$getByAddress");
2971
3054
  const hasByAddressSymbol = Symbol("$$hasByAddress");
@@ -2973,6 +3056,7 @@ const setByAddressSymbol = Symbol("$$setByAddress");
2973
3056
  const connectedCallbackSymbol = Symbol("$$connectedCallback");
2974
3057
  const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
2975
3058
  const updatedCallbackSymbol = Symbol("$$updatedCallback");
3059
+ const errorCallbackSymbol = Symbol("$$errorCallback");
2976
3060
 
2977
3061
  const _cache$3 = new WeakMap();
2978
3062
  function getAbsolutePathInfo(stateElement, pathInfo) {
@@ -4225,6 +4309,46 @@ const handlerByHandlerKey = new Map();
4225
4309
  const bindingRegistry = createHandlerBindingRegistry();
4226
4310
  const producerValueObserversByNode = new WeakMap();
4227
4311
  const DEFAULT_GETTER = (e) => e.detail;
4312
+ /**
4313
+ * 既定 getter(`(e) => e.detail`)が要素の宣言と噛み合っていない典型 2 形を、
4314
+ * 要素 × プロパティごとに 1 回だけ警告する(README「What the element writes back」)。
4315
+ *
4316
+ * (a) detail が undefined なのに `element[propName]` には値がある —
4317
+ * CustomEvent でない Event を dispatch している / `detail` を付け忘れている
4318
+ * (b) detail が `{ <propName>: … }` の形のラッパーで、`element[propName]` はオブジェクトでない —
4319
+ * `getter: (e) => e.detail.<propName>` が要る
4320
+ *
4321
+ * どちらも state には黙って undefined / ラッパーが書かれ、例外も lint 診断も出ない
4322
+ * (payload の形は静的に見えない)。挙動は変えない — 書き込みはそのまま行う。
4323
+ * occurrence(`semantics: "event"`)は payload が任意なので対象外(呼び出し側で除外)。
4324
+ */
4325
+ const warnedDefaultGetter = new WeakMap();
4326
+ function warnDefaultGetterMismatch(node, propName, detail) {
4327
+ const propValue = node[propName];
4328
+ let reason = null;
4329
+ if (typeof detail === "undefined") {
4330
+ if (typeof propValue !== "undefined") {
4331
+ reason = `the event carried no detail (undefined) while element.${propName} is ${typeof propValue}`;
4332
+ }
4333
+ }
4334
+ else if (detail !== null && typeof detail === "object" && Object.prototype.hasOwnProperty.call(detail, propName)
4335
+ && (propValue === null || typeof propValue !== "object")) {
4336
+ reason = `the event's detail is an object with a "${propName}" key while element.${propName} is ${typeof propValue}`;
4337
+ }
4338
+ if (reason === null)
4339
+ return;
4340
+ let props = warnedDefaultGetter.get(node);
4341
+ if (typeof props === "undefined") {
4342
+ props = new Set();
4343
+ warnedDefaultGetter.set(node, props);
4344
+ }
4345
+ if (props.has(propName))
4346
+ return;
4347
+ props.add(propName);
4348
+ console.warn(`[@wcstack/state] [wcs/default-getter-mismatch] <${node.tagName.toLowerCase()}> "${propName}": ${reason}. ` +
4349
+ `With no getter, state receives e.detail as-is. Dispatch the value itself as detail, or declare ` +
4350
+ `getter (e.g. (e) => e.detail.${propName}, or (e) => e.target.${propName}) on that wcBindable property.`);
4351
+ }
4228
4352
  function getHandlerKey(binding, eventName, hasGetter, isOccurrence) {
4229
4353
  const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
4230
4354
  return `${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}::${isOccurrence ? 'o' : 's'}`;
@@ -4285,6 +4409,9 @@ const twowayEventHandlerFunction = (propName, statePathName, inFilters, valueGet
4285
4409
  let newValue;
4286
4410
  if (valueGetter !== null) {
4287
4411
  newValue = valueGetter(event);
4412
+ if (valueGetter === DEFAULT_GETTER && !isOccurrence) {
4413
+ warnDefaultGetterMismatch(node, propName, newValue);
4414
+ }
4288
4415
  }
4289
4416
  else {
4290
4417
  if (!(propName in node)) {
@@ -5164,6 +5291,10 @@ class BindingSession {
5164
5291
  if (newAbs !== oldAbs && hasLastListValueByAbsoluteStateAddress(oldAbs)) {
5165
5292
  setLastListValueByAbsoluteStateAddress(newAbs, getLastListValueByAbsoluteStateAddress(oldAbs));
5166
5293
  }
5294
+ // state 側の基準(E1)も同じ理由で引き継ぐ。記録の有無は has で見る(同上)
5295
+ if (newAbs !== oldAbs && hasStateListBaseline(oldAbs)) {
5296
+ setStateListBaseline(newAbs, getStateListBaseline(oldAbs));
5297
+ }
5167
5298
  }
5168
5299
  if (this.shouldApplyState(binding)) {
5169
5300
  rebound.push(binding);
@@ -7581,6 +7712,113 @@ function clearSsrPropertyStore() {
7581
7712
  trackedNodes.clear();
7582
7713
  }
7583
7714
 
7715
+ /**
7716
+ * Trusted Types (`require-trusted-types-for 'script'`) 対応。正本は docs/csp.md §7。
7717
+ *
7718
+ * state が HTML sink に流すのは **状態の値**(`innerHTML: path` などのプロパティ
7719
+ * バインド)で、ユーザー入力が混ざり得る文字列そのもの。ここに identity policy を
7720
+ * 噛ませて通すのは TT の無効化と同義なので、state は自前の policy を作らない。
7721
+ * 利用側が sanitizer を持つ policy を注入したときだけ通し、無ければ従来どおり
7722
+ * ブラウザに弾かせる(ただし何を設定すれば直るかは必ず言う)。
7723
+ *
7724
+ * 注入口は全 @wcstack パッケージ共通のグローバルスロット。buildless(CDN 一発)でも
7725
+ * inline script 1 本で差し込める:
7726
+ *
7727
+ * ```js
7728
+ * globalThis[Symbol.for("wcstack.trustedTypes.policy")] =
7729
+ * trustedTypes.createPolicy("my-app", { createHTML: (s) => DOMPurify.sanitize(s) });
7730
+ * ```
7731
+ *
7732
+ * バンドラ経由なら `setTrustedTypesPolicy()` を使う。値は毎回スロットから読むので
7733
+ * 後から差し替えても効く(identity policy を作る router / worker 側だけは
7734
+ * `createPolicy` の重複を避けるため生成結果をシングルトンで保持する)。
7735
+ */
7736
+ /** 利用側が policy を差し込むグローバルスロット(全 @wcstack パッケージ共通)。 */
7737
+ const TRUSTED_TYPES_POLICY_SLOT = Symbol.for("wcstack.trustedTypes.policy");
7738
+ /**
7739
+ * 利用側が注入した policy を返す。state はここに identity policy をフォールバック
7740
+ * させない(それをやると TT を無効化することになる)。
7741
+ */
7742
+ function getTrustedTypesPolicy() {
7743
+ const value = globalThis[TRUSTED_TYPES_POLICY_SLOT];
7744
+ if (value === null || typeof value !== "object")
7745
+ return null;
7746
+ return value;
7747
+ }
7748
+ /** 利用側 policy を設定する(`null` で解除)。最初のバインド適用前に呼ぶこと。 */
7749
+ function setTrustedTypesPolicy(policy) {
7750
+ globalThis[TRUSTED_TYPES_POLICY_SLOT] = policy;
7751
+ }
7752
+ /**
7753
+ * TrustedHTML が要求されるプロパティか。`textContent` などの安全な sink は含めない。
7754
+ * ホットパス(全プロパティ書き込み)から呼ばれるので文字列比較だけで済ませる。
7755
+ */
7756
+ function isHtmlSinkProp(prop) {
7757
+ return prop === "innerHTML" || prop === "outerHTML" || prop === "srcdoc";
7758
+ }
7759
+ /**
7760
+ * HTML sink へ書く値を利用側 policy に通す。policy が無ければ値をそのまま返す
7761
+ * = TT 有効下ではブラウザが弾く(意図どおり)。policy がある場合は TT 非対応
7762
+ * ブラウザでも通す: sanitizer は Chromium だけで効いても意味がないため。
7763
+ */
7764
+ function trustHtmlValue(value) {
7765
+ if (typeof value !== "string")
7766
+ return value;
7767
+ const policy = getTrustedTypesPolicy();
7768
+ const createHTML = policy?.createHTML;
7769
+ if (typeof createHTML !== "function")
7770
+ return value;
7771
+ return createHTML.call(policy, value);
7772
+ }
7773
+ let _enforced = undefined;
7774
+ /**
7775
+ * TT が実際に強制されているかを実測する。エラーメッセージの文言に依存しないよう、
7776
+ * 使い捨ての要素へ実際に書いて確かめる。`default` policy がある場合は書き込みが
7777
+ * 通る=我々の書き込みも通るので、正しく false になる。
7778
+ *
7779
+ * cold path(書き込みが失敗した後)でしか呼ばれない。
7780
+ */
7781
+ function isTrustedTypesEnforced() {
7782
+ if (_enforced !== undefined)
7783
+ return _enforced;
7784
+ if (!("trustedTypes" in globalThis)) {
7785
+ _enforced = false;
7786
+ return _enforced;
7787
+ }
7788
+ try {
7789
+ document.createElement("div").innerHTML = "<i></i>";
7790
+ _enforced = false;
7791
+ }
7792
+ catch {
7793
+ _enforced = true;
7794
+ }
7795
+ return _enforced;
7796
+ }
7797
+ let _reported = false;
7798
+ /**
7799
+ * HTML sink への書き込み失敗を診断する。applyChangeToProperty の catch は
7800
+ * `config.debug` 時しか warn しないため、TT が原因のときは黙って壊れていた。
7801
+ * 原因と直し方が分かる形で一度だけ報告する。
7802
+ */
7803
+ function reportTrustedTypesBlock(element, prop) {
7804
+ if (_reported)
7805
+ return;
7806
+ if (!isTrustedTypesEnforced())
7807
+ return;
7808
+ _reported = true;
7809
+ const hasPolicy = typeof getTrustedTypesPolicy()?.createHTML === "function";
7810
+ const cause = hasPolicy
7811
+ ? "The injected policy's createHTML() did not return a TrustedHTML."
7812
+ : "No sanitizing policy is installed, and @wcstack/state deliberately does not "
7813
+ + "pass state values through an identity policy — that would defeat the CSP.";
7814
+ console.error(`[@wcstack/state] Writing to "${prop}" was blocked by Trusted Types `
7815
+ + `(require-trusted-types-for 'script'). ${cause}\n`
7816
+ + `Install a sanitizing policy before the first binding is applied:\n`
7817
+ + ` globalThis[Symbol.for("wcstack.trustedTypes.policy")] =\n`
7818
+ + ` trustedTypes.createPolicy("my-app", { createHTML: (s) => DOMPurify.sanitize(s) });\n`
7819
+ + `Or bind the value as text instead of HTML. See docs/csp.md section 7.`, { element, property: prop });
7820
+ }
7821
+
7584
7822
  // SSR 時に HTML 属性で代替可能なプロパティ
7585
7823
  // これら以外のプロパティは ssrPropertyStore に蓄積してハイドレーション時に復元
7586
7824
  const SSR_ATTR_PROPS = {
@@ -7652,13 +7890,23 @@ function applyChangeToProperty(binding, _context, newValue) {
7652
7890
  && getCustomElement(element) !== null) {
7653
7891
  rememberOverwrittenObject(element, firstSegment, current);
7654
7892
  }
7893
+ // Trusted Types: HTML sink (`innerHTML` 等) への書き込みだけ、利用側が注入した
7894
+ // sanitizer 付き policy を通す。state が identity policy を作って素通しさせるのは
7895
+ // TT の無効化と同義なので採らない(docs/csp.md §7)。sink 以外は文字列比較 3 回で
7896
+ // 抜けるので、ホットパスの実コストはほぼ無い。
7897
+ const isHtmlSink = isHtmlSinkProp(firstSegment);
7655
7898
  const performWrite = () => {
7656
7899
  let propertyWriteSucceeded = false;
7657
7900
  try {
7658
- element[firstSegment] = newValue;
7901
+ element[firstSegment] = isHtmlSink ? trustHtmlValue(newValue) : newValue;
7659
7902
  propertyWriteSucceeded = true;
7660
7903
  }
7661
7904
  catch (error) {
7905
+ // TT が原因のときは config.debug に関係なく報告する。ここを黙って握り潰すと
7906
+ // 「バインドを書いたのに何も起きない」という最悪の壊れ方をする。
7907
+ if (isHtmlSink) {
7908
+ reportTrustedTypesBlock(element, firstSegment);
7909
+ }
7662
7910
  if (config.debug) {
7663
7911
  console.warn(`Failed to set property '${firstSegment}' on element.`, {
7664
7912
  element,
@@ -8044,6 +8292,9 @@ const EXISTS = Object.freeze({
8044
8292
  * `obj` 自身+プロトタイプチェーン(Object.prototype 手前まで)から descriptor を引く。
8045
8293
  * 打ち切り位置は getAllPropertyDescriptors と同じ — 「state が宣言したもの」だけを
8046
8294
  * 存在とみなし、`toString` 等の Object.prototype 由来を存在扱いしない。
8295
+ *
8296
+ * `State.findStateDescriptor`(再帰アクセサの衝突検査)も同じ走査を使う。打ち切り位置が
8297
+ * 2 本に分かれると、片方だけが `Object.prototype` を存在扱いするようなずれ方をする。
8047
8298
  */
8048
8299
  function findDescriptor(obj, key) {
8049
8300
  let proto = obj;
@@ -8177,6 +8428,16 @@ function indexArityMessage(api, path, wildcardCount, actual) {
8177
8428
  return `[wcs/index-arity] ${api}("${path}") requires ${requirement} index(es) ` +
8178
8429
  `("*" appears ${wildcardCount} time(s) in the path) but got ${actual}.${LINT_HINT}`;
8179
8430
  }
8431
+ /**
8432
+ * `**` を含むパスが宣言済みの再帰アンカーと合致しない(綴り違い・2 つ目の `**`)。
8433
+ * 束縛形(bind.ts)・合併形(getAllRecursive.ts)・ブロードキャスト(setAllRecursive.ts)の
8434
+ * 3 入口が同じ文面で報告する。
8435
+ */
8436
+ function recursionAnchorMismatchMessage(path, recursiveAnchor) {
8437
+ return `[wcs/recursion-anchor] "${path}" does not match the declared recursion anchor ` +
8438
+ `"${recursiveAnchor}". This version supports exactly one anchor per state, and "**" must be followed ` +
8439
+ `by a well-formed suffix (no second "**", no empty segment, no bare "*" right after "**").`;
8440
+ }
8180
8441
  /**
8181
8442
  * `$getAll(path)`(添字省略)の既定値はループ文脈の添字 `[$1..$n]` だが、それを
8182
8443
  * 敷けるのは path と文脈がワイルドカード連鎖を共有している場合だけ。共有ゼロなのに
@@ -8271,10 +8532,74 @@ function checkDeclaredPath(stateElement, state, path, source) {
8271
8532
  if (alreadyReported(stateElement, path)) {
8272
8533
  return;
8273
8534
  }
8535
+ // 再帰 getter の展開形は、バインド確立の時点ではまだ生えていない(読む直前に
8536
+ // 遅延実体化する — recursion/registry.ts)。素の存在検査では必ず「解決できない」に
8537
+ // なるので、宣言済みの `**` getter に合致するかを先に見る。実体化はしない。
8538
+ // 展開形の**値の内側**(`nodes.*.stats.count` で `get "nodes.**.stats"()` がオブジェクトを
8539
+ // 返す形)も同じ — 通常の getter なら下の「途中のプレフィックスがフラット宣言」で
8540
+ // UNKNOWN に倒れるところ、未実体化のアクセサは findDescriptor に見えないのでここで畳む。
8541
+ if (stateElement.hasRecursion === true && stateElement.recursionRegistry.recursiveGetterOwning(path) !== null) {
8542
+ return;
8543
+ }
8274
8544
  const result = resolvePathExistence(state, path, stateElement.getterPaths);
8275
8545
  if (result.existence !== "missing") {
8276
8546
  return;
8277
8547
  }
8548
+ if (isExportedPath(stateElement, path)) {
8549
+ return;
8550
+ }
8551
+ if (source === "binding") {
8552
+ // 遅延報告(docs/state-overlay-export-design.md X7): バインド確立時点では、その位置に
8553
+ // マウントされるコンポーネントの getter(公開 getter)がまだ登録されていない。
8554
+ // 1 マクロタスク待って、登録で解消しなかったものだけを報告する
8555
+ deferReport(stateElement, path, result);
8556
+ return;
8557
+ }
8558
+ reportMissing(stateElement, path, source, result);
8559
+ }
8560
+ const deferredReportsByStateElement = new WeakMap();
8561
+ const flushScheduled = new WeakSet();
8562
+ const exportedPathsByStateElement = new WeakMap();
8563
+ /** 公開 getter の登録(webComponent/exportIndex.ts)— このパスは「存在しない」ではない */
8564
+ function markExportedPath(stateElement, path) {
8565
+ let paths = exportedPathsByStateElement.get(stateElement);
8566
+ if (typeof paths === "undefined") {
8567
+ paths = new Set();
8568
+ exportedPathsByStateElement.set(stateElement, paths);
8569
+ }
8570
+ paths.add(path);
8571
+ deferredReportsByStateElement.get(stateElement)?.delete(path);
8572
+ }
8573
+ function isExportedPath(stateElement, path) {
8574
+ return exportedPathsByStateElement.get(stateElement)?.has(path) === true;
8575
+ }
8576
+ function deferReport(stateElement, path, result) {
8577
+ let pending = deferredReportsByStateElement.get(stateElement);
8578
+ if (typeof pending === "undefined") {
8579
+ pending = new Map();
8580
+ deferredReportsByStateElement.set(stateElement, pending);
8581
+ }
8582
+ pending.set(path, result);
8583
+ if (flushScheduled.has(stateElement)) {
8584
+ return;
8585
+ }
8586
+ flushScheduled.add(stateElement);
8587
+ setTimeout(() => flushDeferredPathReports(stateElement), 0);
8588
+ }
8589
+ /** 遅延中の報告を今すぐ流す(タイマー到達時・テスト用) */
8590
+ function flushDeferredPathReports(stateElement) {
8591
+ flushScheduled.delete(stateElement);
8592
+ const pending = deferredReportsByStateElement.get(stateElement);
8593
+ if (typeof pending === "undefined") {
8594
+ return;
8595
+ }
8596
+ deferredReportsByStateElement.delete(stateElement);
8597
+ // 登録で解消したものは markExportedPath が pending から消している
8598
+ for (const [path, result] of pending) {
8599
+ reportMissing(stateElement, path, "binding", result);
8600
+ }
8601
+ }
8602
+ function reportMissing(stateElement, path, source, result) {
8278
8603
  // 接頭辞は raiseError と同じ `[@wcstack/state] [wcs/...]` の並び(コンソールの
8279
8604
  // grep 単位をパッケージで揃える)
8280
8605
  console.warn(`[@wcstack/state] [${DIAGNOSTIC_CODE[source]}] ${SUBJECT[source]} "${path}" does not resolve on the state tree: ` +
@@ -8435,7 +8760,7 @@ function _applyChange(binding, context) {
8435
8760
  const value = getValue(context.state, binding);
8436
8761
  const filteredValue = getFilteredValue(value, binding.outFilters);
8437
8762
  if (deferredSelectBindingByBinding.get(binding) === true) {
8438
- context.deferredSelectBindings.push({ binding, value: filteredValue });
8763
+ context.deferredSelectBindings.push({ binding, value: filteredValue, stateElement: context.stateElement });
8439
8764
  return;
8440
8765
  }
8441
8766
  let fn = fnByBinding.get(binding);
@@ -8475,7 +8800,7 @@ function _applyChange(binding, context) {
8475
8800
  if (element.tagName === 'SELECT') {
8476
8801
  const propName = binding.propSegments[0];
8477
8802
  if (propName === 'value' || propName === 'selectedIndex') {
8478
- context.deferredSelectBindings.push({ binding, value: filteredValue });
8803
+ context.deferredSelectBindings.push({ binding, value: filteredValue, stateElement: context.stateElement });
8479
8804
  deferredSelectBindingByBinding.set(binding, true);
8480
8805
  return;
8481
8806
  }
@@ -8569,10 +8894,31 @@ function applyChange(binding, context) {
8569
8894
  * `console.error` だけだと devtools からは「静かに握られた失敗」が見えないため、
8570
8895
  * 同じ地点から sink にも流す(`state:watch-error` と同じ位置づけ)。
8571
8896
  * 値と DOM は巻き戻さない — 伝播 hop 上限超過・watch 連鎖打ち切りと同じ姿勢。
8897
+ *
8898
+ * state が `$errorCallback` を宣言していれば、console.error の代わりにそこへ配送する
8899
+ * (作者が報告を引き取った。ページ内で受けるための口)。配送は batch の末尾 —
8900
+ * $updatedCallback と同じ位置 — にまとめる。devtools sink へは宣言の有無に関わらず流す。
8572
8901
  */
8573
- function reportBindingApplyError(binding, error) {
8574
- console.error(`[@wcstack/state] binding "${binding.bindingType}: ${binding.statePathName}" failed to apply; ` +
8575
- `the rest of this batch continues.`, { node: binding.node, error });
8902
+ function reportBindingApplyError(binding, error, stateElement, failuresByStateElement) {
8903
+ const handled = stateElement !== null && stateElement.hasErrorCallback === true;
8904
+ if (handled) {
8905
+ const info = {
8906
+ path: binding.statePathName,
8907
+ bindingType: binding.bindingType,
8908
+ node: binding.node,
8909
+ };
8910
+ const failures = failuresByStateElement.get(stateElement);
8911
+ if (failures === undefined) {
8912
+ failuresByStateElement.set(stateElement, [{ error, info }]);
8913
+ }
8914
+ else {
8915
+ failures.push({ error, info });
8916
+ }
8917
+ }
8918
+ else {
8919
+ console.error(`[@wcstack/state] binding "${binding.bindingType}: ${binding.statePathName}" failed to apply; ` +
8920
+ `the rest of this batch continues.`, { node: binding.node, error });
8921
+ }
8576
8922
  if (devtoolsSink !== null) {
8577
8923
  devtoolsSink({
8578
8924
  type: "state:binding-apply-error",
@@ -8598,6 +8944,7 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
8598
8944
  const newListValueByAbsAddress = new Map();
8599
8945
  const updatedAbsAddressSetByStateElement = new Map();
8600
8946
  const deferredSelectBindings = [];
8947
+ const failuresByStateElement = new Map();
8601
8948
  // Phase 1: 構造的更新 + 値更新(select.value/selectedIndex は遅延)
8602
8949
  while (bindingIndex < bindings.length) {
8603
8950
  let binding = bindings[bindingIndex];
@@ -8643,7 +8990,7 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
8643
8990
  applyChange(binding, context);
8644
8991
  }
8645
8992
  catch (error) {
8646
- reportBindingApplyError(binding, error);
8993
+ reportBindingApplyError(binding, error, stateElement, failuresByStateElement);
8647
8994
  }
8648
8995
  bindingIndex++;
8649
8996
  const nextBindingInfo = bindings[bindingIndex];
@@ -8659,22 +9006,40 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
8659
9006
  // Phase 2: 遅延されたselect.value/selectedIndex を適用
8660
9007
  // applyChangeToProperty は propagationContextByBinding 以外の context を
8661
9008
  // 参照しないため、遅延分は最小 context を渡す
8662
- for (const { binding, value } of deferredSelectBindings) {
9009
+ for (const { binding, value, stateElement } of deferredSelectBindings) {
8663
9010
  try {
8664
9011
  applyChangeToProperty(binding, { propagationContextByBinding }, value);
8665
9012
  }
8666
9013
  catch (error) {
8667
- reportBindingApplyError(binding, error);
9014
+ reportBindingApplyError(binding, error, stateElement ?? null, failuresByStateElement);
8668
9015
  }
8669
9016
  }
8670
9017
  for (const [absAddress, newListValue] of newListValueByAbsAddress.entries()) {
8671
9018
  setLastListValueByAbsoluteStateAddress(absAddress, newListValue);
9019
+ // 描画の基準とは別に、state 側の基準(読み・依存ウォークの共有正本)も進める。
9020
+ // 初回描画はどの書き込みも経ていないので、ここで観測しておかないと最初の構造
9021
+ // 書き込みで基準が空のまま ListIndex を鋳造してしまう(E1)。
9022
+ setStateListBaseline(absAddress, newListValue);
8672
9023
  }
8673
9024
  for (const [stateElement, absAddressSet] of updatedAbsAddressSetByStateElement.entries()) {
8674
9025
  stateElement.createState("writable", (state) => {
8675
9026
  state[updatedCallbackSymbol](Array.from(absAddressSet));
8676
9027
  });
8677
9028
  }
9029
+ // $errorCallback の配送。$updatedCallback の後・失敗した本数ぶん・this は writable proxy。
9030
+ // callback 自身の throw は隔離する — 1 件の報告失敗が残りの報告と drain を道連れにしない
9031
+ for (const [stateElement, failures] of failuresByStateElement.entries()) {
9032
+ stateElement.createState("writable", (state) => {
9033
+ for (const { error, info } of failures) {
9034
+ try {
9035
+ state[errorCallbackSymbol](error, info);
9036
+ }
9037
+ catch (callbackError) {
9038
+ console.error(`[@wcstack/state] $errorCallback threw while handling the failure of binding "${info.bindingType}: ${info.path}".`, { error: callbackError, original: error, node: info.node });
9039
+ }
9040
+ }
9041
+ });
9042
+ }
8678
9043
  }
8679
9044
 
8680
9045
  function scheduleDeferredSpreads(deferredSpreads, parentLoopContext, session) {
@@ -9182,7 +9547,7 @@ async function buildBindings(root) {
9182
9547
  }
9183
9548
  }
9184
9549
 
9185
- var version = "2.1.1";
9550
+ var version = "2.3.0";
9186
9551
  var pkg = {
9187
9552
  version: version};
9188
9553
 
@@ -9321,8 +9686,21 @@ class Ssr extends HTMLElementBase {
9321
9686
  if (!raw || typeof raw !== 'object')
9322
9687
  return {};
9323
9688
  const data = {};
9324
- for (const [key, value] of Object.entries(raw)) {
9325
- if (!key.startsWith('$') && typeof value !== 'function') {
9689
+ for (const key of Object.keys(raw)) {
9690
+ if (key.startsWith('$'))
9691
+ continue;
9692
+ // **アクセサは評価しない。** スナップショットが運ぶのはデータで、派生値は
9693
+ // クライアントが同じ宣言から作り直す。ここは Object.entries で舐めていたので、
9694
+ // own かつ enumerable な getter を**生の state オブジェクト**を this にして
9695
+ // 評価していた — proxy の上でしか意味を持たない本体(`this["items.*.n"]` や
9696
+ // `this.$getAll(...)`)が、パス getter なら NaN → JSON の null で静かに壊れ、
9697
+ // `$getAll` を呼ぶ getter なら TypeError でページ全体の SSR を落としていた
9698
+ // (docs/state-recursive-path-impl-plan.md §7)。
9699
+ const descriptor = Object.getOwnPropertyDescriptor(raw, key);
9700
+ if (descriptor !== undefined && typeof descriptor.get === 'function')
9701
+ continue;
9702
+ const value = raw[key];
9703
+ if (typeof value !== 'function') {
9326
9704
  data[key] = value;
9327
9705
  }
9328
9706
  }
@@ -9861,6 +10239,8 @@ async function hydrateBindings(root) {
9861
10239
  const value = state[binding.statePathName];
9862
10240
  if (Array.isArray(value)) {
9863
10241
  setLastListValueByAbsoluteStateAddress(absAddr, value);
10242
+ // 描画の基準と同時に state 側の基準も進める(E1。applyChangeFromBindings と対称)
10243
+ setStateListBaseline(absAddr, value);
9864
10244
  }
9865
10245
  });
9866
10246
  }
@@ -10383,6 +10763,9 @@ class Updater {
10383
10763
  const requireStartProcess = this._queueUpdateRecords.length === 0;
10384
10764
  this._queueUpdateRecords.push({ absoluteAddress, context });
10385
10765
  if (requireStartProcess) {
10766
+ // このバッチのあいだ、依存ウォークと読みが観測したリスト値は保留にする。
10767
+ // 確定は drain の finally(list/stateListBaseline.ts の頭のコメント)。
10768
+ beginStateListBaselineBatch();
10386
10769
  queueMicrotask(() => {
10387
10770
  const updateRecords = this._queueUpdateRecords;
10388
10771
  this._queueUpdateRecords = [];
@@ -10510,6 +10893,10 @@ class Updater {
10510
10893
  }
10511
10894
  }
10512
10895
  finally {
10896
+ // バッチ中に溜めたリスト差分基準を確定する。notifyUpdateBatchListeners より先に
10897
+ // 置くのは、リスナー($watch / $streams restart)の中で走る書き込みが
10898
+ // 「このバッチの結果」を基準として見るべきだから。
10899
+ endStateListBaselineBatch();
10513
10900
  notifyUpdateBatchListeners(new Set(contextByAbsoluteAddress.keys()));
10514
10901
  }
10515
10902
  }
@@ -10856,6 +11243,7 @@ function registerDevtoolsSource() {
10856
11243
  delta: record.delta,
10857
11244
  privateKeys: Object.keys(record.privateSnapshot),
10858
11245
  getterKeys: [...record.getterKeys],
11246
+ exports: [...record.exports.keys()],
10859
11247
  }));
10860
11248
  },
10861
11249
  keys(rootNode) {
@@ -11686,6 +12074,13 @@ function processListKeysDeclaration(state) {
11686
12074
  raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" must be the list path itself, not the element path ` +
11687
12075
  `(drop the trailing "${DELIMITER}${WILDCARD}").`);
11688
12076
  }
12077
+ // `**` は `$listKeys` の消費者ではない。受理すると宣言は永久に効かず(キー突合は
12078
+ // 具体パスで引く)、他の壊れた形は raise するのと非対称になる。深さごとに具体パスで宣言する。
12079
+ if (path.indexOf(RECURSION_WILDCARD) !== -1) {
12080
+ raiseError(`[wcs/recursion-unsupported] ${STATE_LIST_KEYS_NAME} entry "${path}" uses "${RECURSION_WILDCARD}", ` +
12081
+ `which ${STATE_LIST_KEYS_NAME} does not interpret — a keyed list is one concrete list path. ` +
12082
+ `Declare the key per depth instead (for example "nodes${DELIMITER}${WILDCARD}${DELIMITER}children").`);
12083
+ }
11689
12084
  if (typeof spec === "function") {
11690
12085
  entries.set(path, spec);
11691
12086
  continue;
@@ -11712,123 +12107,962 @@ function processListKeysDeclaration(state) {
11712
12107
  }
11713
12108
 
11714
12109
  /**
11715
- * stream/streamNamespace.ts
12110
+ * recursion/declaration.ts
11716
12111
  *
11717
- * `$streamStatus` / `$streamError` read-only namespace proxy
11718
- * (docs/state-streams-design.md §4-1 / §4-2)。commandNamespace と対称。
12112
+ * `$recursion: { <anchor>: <repeat> }` 宣言を検証して仕様に落とす
12113
+ * (docs/state-recursive-path-impl-plan.md §1-1)。宣言が無ければ null で、
12114
+ * その state は再帰の経路にまったく入らない(`$listKeys` と同じゼロコスト規約)。
11719
12115
  *
11720
- * - state element 単位で memo 化し、同一 stateElement なら同じ proxy が返る。
11721
- * - 宣言された stream 名(`$streams` に列挙されたもの)のみ registry entry の
11722
- * status / error を返す。宣言外の名前・Symbol キーは undefined
11723
- * (`then` / `constructor` 等を内部機構が触っても throw しない寛容規約、
11724
- * $command と同じ)。
11725
- * - 値は memo しない: proxy は getStreamEntries を毎回読む thin gateway
11726
- * (status / error は runtime が随時書き換えるため。registry entry が正本、§2-1)。
11727
- * - set / deleteProperty は raiseError。setByAddress の親走査が namespace proxy に
11728
- * 到達したときの Reflect.set もここで落ちる(書き込み防御 S11 の終端)。
12116
+ * 初版が受け付けるのは **単一の自己再帰** だけ。アンカーも反復サブパスも
12117
+ * 「固定プロパティ列 + 末尾の `.*` ひとつ」に限る。途中のワイルドカード・複数宣言・
12118
+ * 相互再帰は、黙って別の意味に解釈せず明示的に拒否する。
11729
12119
  */
11730
- const statusNamespaceByStateElement = new WeakMap();
11731
- const errorNamespaceByStateElement = new WeakMap();
11732
- function createStreamNamespaceProxy(stateElement, namespaceName, pick) {
11733
- return new Proxy(Object.create(null), {
11734
- get(_target, prop) {
11735
- if (typeof prop !== "string") {
11736
- return undefined;
11737
- }
11738
- const entry = getStreamEntries(stateElement).get(prop);
11739
- if (typeof entry === "undefined") {
11740
- return undefined;
11741
- }
11742
- return pick(entry);
11743
- },
11744
- has(_target, prop) {
11745
- return typeof prop === "string" && getStreamEntries(stateElement).has(prop);
11746
- },
11747
- ownKeys() {
11748
- return Array.from(getStreamEntries(stateElement).keys());
11749
- },
11750
- getOwnPropertyDescriptor(_target, prop) {
11751
- if (typeof prop !== "string") {
11752
- return undefined;
11753
- }
11754
- const entry = getStreamEntries(stateElement).get(prop);
11755
- if (typeof entry === "undefined") {
11756
- return undefined;
11757
- }
11758
- return {
11759
- configurable: true,
11760
- enumerable: true,
11761
- value: pick(entry),
11762
- };
11763
- },
11764
- set() {
11765
- raiseError(`${namespaceName} namespace is read-only; assigning to it is not allowed.`);
11766
- },
11767
- deleteProperty() {
11768
- raiseError(`${namespaceName} namespace is read-only; deleting from it is not allowed.`);
11769
- },
11770
- });
11771
- }
11772
- function getStreamStatusNamespace(stateElement) {
11773
- const cached = statusNamespaceByStateElement.get(stateElement);
11774
- if (typeof cached !== "undefined") {
11775
- return cached;
12120
+ /** `a.b.*` の形(末尾だけがワイルドカード・空セグメント無し・`**` 無し・添字無し)か。 */
12121
+ function assertNodePath(kind, path) {
12122
+ if (typeof path !== "string" || path.length === 0) {
12123
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} must be a non-empty string.`);
11776
12124
  }
11777
- const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_STATUS_NAMESPACE_NAME, (entry) => entry.status);
11778
- statusNamespaceByStateElement.set(stateElement, proxy);
11779
- return proxy;
11780
- }
11781
- function getStreamErrorNamespace(stateElement) {
11782
- const cached = errorNamespaceByStateElement.get(stateElement);
11783
- if (typeof cached !== "undefined") {
11784
- return cached;
12125
+ const segments = path.split(DELIMITER);
12126
+ if (segments.some((segment) => segment.length === 0)) {
12127
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must not contain empty path segments.`);
12128
+ }
12129
+ if (segments.length < 2) {
12130
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must name a list element: ` +
12131
+ `a property path ending with "${DELIMITER}${WILDCARD}" (for example "nodes${DELIMITER}${WILDCARD}").`);
12132
+ }
12133
+ if (segments[segments.length - 1] !== WILDCARD) {
12134
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must end with "${DELIMITER}${WILDCARD}" ` +
12135
+ `— it names the element of the list, not the list itself.`);
12136
+ }
12137
+ // 予約セグメント。マウントのマーカー(`#m1`)と `$` 名前空間は raw state に実体を
12138
+ // 持たないので、再帰のアンカーにはなり得ない(checkDeclaredPath が同じ 2 つで
12139
+ // 早期 return しているのと対称)。
12140
+ if (segments[0].charCodeAt(0) === 36 /* '$' */) {
12141
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must not start with "$" — that namespace is reserved.`);
12142
+ }
12143
+ if (path.indexOf("#") !== -1) {
12144
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must not contain "#" — that segment is reserved for mounts.`);
12145
+ }
12146
+ for (let i = 0; i < segments.length - 1; i++) {
12147
+ if (segments[i] === WILDCARD) {
12148
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must have exactly one "${WILDCARD}", at the end. ` +
12149
+ `Wildcards in the middle are not supported in this version.`);
12150
+ }
12151
+ if (segments[i] === RECURSION_WILDCARD) {
12152
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must not contain "${RECURSION_WILDCARD}" — ` +
12153
+ `the declaration is what gives "${RECURSION_WILDCARD}" its meaning.`);
12154
+ }
12155
+ // 添字セグメント(`children.0.*`)。エンジンは具体パスの添字を `*` に畳むので
12156
+ // (`indexSegmentsToWildcard` — ResolvedAddress と同じ規則)、宣言の途中に書かれた
12157
+ // 添字は意味を持たない奇形になる。黙って `*` と同じに読み替えず、ここで落とす。
12158
+ if (!isNaN(Number(segments[i]))) {
12159
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} ${kind} "${path}" must not contain an index segment ` +
12160
+ `("${segments[i]}") — the recursion is declared over the shape of the tree, not over one row.`);
12161
+ }
11785
12162
  }
11786
- const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_ERROR_NAMESPACE_NAME, (entry) => entry.error);
11787
- errorNamespaceByStateElement.set(stateElement, proxy);
11788
- return proxy;
11789
12163
  }
11790
12164
  /**
11791
- * namespace proxy の memo を破棄する(clearCommandNamespace と対称)。
11792
- * disconnectedCallback と `_state` 再 set 時に呼ばれる。
12165
+ * `$recursion` 宣言を検証して仕様にする。宣言が無ければ null(=ゼロコスト経路)。
11793
12166
  */
11794
- function clearStreamNamespace(stateElement) {
11795
- statusNamespaceByStateElement.delete(stateElement);
11796
- errorNamespaceByStateElement.delete(stateElement);
12167
+ function processRecursionDeclaration(state) {
12168
+ const declared = state[STATE_RECURSION_NAME];
12169
+ if (typeof declared === "undefined") {
12170
+ return null;
12171
+ }
12172
+ if (typeof declared !== "object" || declared === null) {
12173
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} must be an object mapping one anchor path to its repeating sub-path ` +
12174
+ `(for example { "nodes.*": "children.*" }).`);
12175
+ }
12176
+ const entries = Object.entries(declared);
12177
+ if (entries.length === 0) {
12178
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} must declare exactly one anchor; it is empty.`);
12179
+ }
12180
+ if (entries.length > 1) {
12181
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} declares ${entries.length} anchors (${entries.map(([k]) => `"${k}"`).join(", ")}). ` +
12182
+ `This version supports exactly one self-recursive anchor per state.`);
12183
+ }
12184
+ const [anchor, repeat] = entries[0];
12185
+ assertNodePath("anchor", anchor);
12186
+ if (typeof repeat !== "string") {
12187
+ raiseError(`[wcs/recursion-declaration-invalid] ${STATE_RECURSION_NAME} entry "${anchor}" must map to the repeating sub-path as a string ` +
12188
+ `(for example "children${DELIMITER}${WILDCARD}").`);
12189
+ }
12190
+ assertNodePath("repeating sub-path", repeat);
12191
+ // 反復サブパスが相対か絶対かは**名前の形では判定できない**。`{ "nodes.*": "nodes.*" }`
12192
+ // は `{ nodes: [{ nodes: [...] }] }` という自己相似な木の最も自然な綴りなので、
12193
+ // 「アンカーと同じ語で始まる」ことを理由に拒否してはならない。
12194
+ const anchorList = anchor.slice(0, anchor.lastIndexOf(DELIMITER));
12195
+ const repeatList = repeat.slice(0, repeat.lastIndexOf(DELIMITER));
12196
+ const recursiveAnchor = anchorList + DELIMITER + RECURSION_WILDCARD;
12197
+ return Object.freeze({ anchor, repeat, recursiveAnchor, anchorList, repeatList });
12198
+ }
12199
+
12200
+ function getAllPropertyDescriptors(obj) {
12201
+ const chain = [];
12202
+ let proto = obj;
12203
+ while (proto && proto !== Object.prototype) {
12204
+ chain.push(proto);
12205
+ proto = Object.getPrototypeOf(proto);
12206
+ }
12207
+ const descriptors = {};
12208
+ for (let i = chain.length - 1; i >= 0; i--) {
12209
+ Object.assign(descriptors, Object.getOwnPropertyDescriptors(chain[i]));
12210
+ }
12211
+ return descriptors;
11797
12212
  }
11798
12213
 
11799
12214
  /**
11800
- * stream/argsTrace.ts
12215
+ * recursion/expand.ts
11801
12216
  *
11802
- * `$streams` の args トレース(依存捕捉、docs/state-streams-design.md §3-1)。
12217
+ * `**` を含むオーサリング層のパスと、エンジンが扱う具体パスの相互変換。
12218
+ * **純関数だけ**を置く(state も proxy も触らない)。
11803
12219
  *
11804
- * - モジュールスコープの collector を立てて readonly proxy 上で args を評価し、
11805
- * getByAddress を通った読みを絶対アドレス(IAbsoluteStateAddress)として捕捉する。
11806
- * AbsolutePathInfo / AbsoluteStateAddress は両方キャッシュ済みのため、捕捉した
11807
- * アドレスは drain バッチと Set.has のインスタンス同一性で O(1) 照合できる(§2-1)。
11808
- * - collectStreamDependency は getByAddress のホットパスから毎読み呼ばれるため、
11809
- * collector === null なら即 return し、それ以外の計算を一切しない。
11810
- * - 起動・restart のたびに traceArgs が呼ばれ、成功時は entry.depAddresses を
11811
- * 丸ごと置換する(per-run の動的再捕捉)。失敗時は前回成功 run の検証済み
11812
- * 捕捉を保持する(§2-2 の「error からも依存変化で restart」を保つ)。
11813
- * - lastNotified.ts と同じく import 循環回避のための小モジュール
11814
- * (getByAddress → argsTrace ← streamRuntime の一方向依存に保つ)。
12220
+ * 変換は 1 1 ではなく 1 対多である。`nodes.**.total` は深さごとに
12221
+ * `nodes.*.total` / `nodes.*.children.*.total` / … という無限の族を表し、
12222
+ * エンジンが見るのは常にそのうちの 1 本だけ(設計書 D2)。
11815
12223
  */
11816
- /** トレース中のみ非 null。getByAddress を通った読みの絶対アドレスが溜まる。 */
11817
- let collector = null;
12224
+ /** `**` を含むか(含まない大多数のパスを 1 回の indexOf で抜ける)。 */
12225
+ function hasRecursionWildcard(path) {
12226
+ return path.indexOf(RECURSION_WILDCARD) !== -1;
12227
+ }
11818
12228
  /**
11819
- * getByAddress の入口(checkDependency 直後)から毎読み呼ばれるフック。
11820
- * トレース外(collector === null)では何もしない。
12229
+ * `**` を含むパスを宣言と突き合わせ、接尾辞(`**` より後ろ。無ければ空文字)を返す。
12230
+ * 宣言に合致しない `**` は「宣言なしの `**`」として呼び出し側が診断する(null を返す)。
11821
12231
  */
11822
- function collectStreamDependency(stateElement, address) {
11823
- if (collector === null) {
11824
- return;
12232
+ function splitRecursivePath(spec, path) {
12233
+ if (path === spec.recursiveAnchor) {
12234
+ return "";
11825
12235
  }
11826
- const absolutePathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
11827
- collector.add(createAbsoluteStateAddress(absolutePathInfo, address.listIndex));
11828
- }
11829
- /**
11830
- * args を readonly proxy で同期評価し、読まれたパスを entry.depAddresses に
11831
- * 丸ごと置換で再捕捉する(§3-1)。評価値(source の第 1 引数になる)を返す。
12236
+ const prefix = spec.recursiveAnchor + DELIMITER;
12237
+ if (!path.startsWith(prefix)) {
12238
+ return null;
12239
+ }
12240
+ const suffix = path.slice(spec.recursiveAnchor.length);
12241
+ // 接尾辞に 2 つ目の `**` があるのは初版では未対応(複数の再帰点)。
12242
+ if (hasRecursionWildcard(suffix)) {
12243
+ return null;
12244
+ }
12245
+ // 接尾辞は整形されたパスでなければならない: 空セグメント(`nodes.**.` / `nodes.**..x`)と
12246
+ // `**` 直後の素の `*`(`nodes.**.*` — 展開すると `nodes.*.*` でアンカー行そのもの)は
12247
+ // 受理しない。`assertNodePath` / `$watch` が同じ形を拒否するのと対称(第 4 サイクルで実測:
12248
+ // 受理すると `[undefined×n]` や生の `Reflect.set called on non-object` になっていた)。
12249
+ const segments = suffix.slice(DELIMITER.length).split(DELIMITER);
12250
+ if (segments[0] === WILDCARD || segments.some((segment) => segment.length === 0)) {
12251
+ return null;
12252
+ }
12253
+ return suffix;
12254
+ }
12255
+ /**
12256
+ * 接尾辞(`.` で始まる)の添字セグメントだけを `*` に畳む。先頭の空セグメントは区切りの
12257
+ * 都合なので畳まない(`indexSegmentsToWildcard` に丸ごと渡すと `Number("") === 0` で `*` になる)。
12258
+ * `**` パスの検査(構造・読み取り専用)と `**` getter キーの検査が共有する。
12259
+ */
12260
+ function foldSuffixIndexes(suffix) {
12261
+ return suffix.length === 0 ? suffix : DELIMITER + indexSegmentsToWildcard(suffix.slice(DELIMITER.length));
12262
+ }
12263
+ /**
12264
+ * 2 つの接尾辞が**同じ具体パス族**を指すか。片方がもう片方の末尾で、差分が反復語の
12265
+ * 整数倍(0 回を含む)のとき真。`nodes.**.total` と `nodes.**.children.*.total` は
12266
+ * 深さ k と k+1 で同じ `nodes.*.children.*.total` になる、という関係を捉える。
12267
+ * 静的側の `recursionPaths.sameFamily` と同じ純関数。
12268
+ */
12269
+ function sameFamily(spec, a, b) {
12270
+ const unit = DELIMITER + spec.repeat;
12271
+ const shorter = a.length <= b.length ? a : b;
12272
+ const longer = a.length <= b.length ? b : a;
12273
+ if (!longer.endsWith(shorter)) {
12274
+ return false;
12275
+ }
12276
+ const gap = longer.slice(0, longer.length - shorter.length);
12277
+ if (gap.length === 0) {
12278
+ return true;
12279
+ }
12280
+ if (gap.length % unit.length !== 0) {
12281
+ return false;
12282
+ }
12283
+ for (let cursor = 0; cursor < gap.length; cursor += unit.length) {
12284
+ if (!gap.startsWith(unit, cursor)) {
12285
+ return false;
12286
+ }
12287
+ }
12288
+ return true;
12289
+ }
12290
+ /**
12291
+ * 接尾辞 `suffix` が、`**` getter の接尾辞 `familySuffix` の族そのもの、またはその値の内側を
12292
+ * 指しているか。`.` 境界で切った各接頭辞 `p`(全体を含む)について `sameFamily(familySuffix, p)`
12293
+ * を見る — `startsWith(familySuffix + ".")` だけでは、反復語ぶんずれた展開形の値の内側
12294
+ * (`nodes.**.children.*.total.x` で `nodes.**.total`)を取りこぼす(第 4 サイクルで実測)。
12295
+ */
12296
+ function coversSuffix(spec, familySuffix, suffix) {
12297
+ for (let end = suffix.length; end > 0; end = suffix.lastIndexOf(DELIMITER, end - 1)) {
12298
+ if (sameFamily(spec, familySuffix, suffix.slice(0, end))) {
12299
+ return true;
12300
+ }
12301
+ }
12302
+ return false;
12303
+ }
12304
+ /**
12305
+ * 添字セグメント(`nodes.1.total` の `1`)を `*` に畳む。判定は `address/ResolvedAddress.ts`
12306
+ * と同じ「`Number()` が NaN でない区切り」。API のパス引数(`$getAll` / `$setAll` / `$resolve`)と
12307
+ * `**` パスの接尾辞は set トラップと違って `getResolvedAddress` の正規化を経ないので、
12308
+ * 再帰の検査(読み取り専用・構造)に掛ける前にここで畳む。
12309
+ */
12310
+ function indexSegmentsToWildcard(path) {
12311
+ const segments = path.split(DELIMITER);
12312
+ for (let i = 0; i < segments.length; i++) {
12313
+ if (segments[i] !== WILDCARD && !Number.isNaN(Number(segments[i]))) {
12314
+ segments[i] = WILDCARD;
12315
+ }
12316
+ }
12317
+ return segments.join(DELIMITER);
12318
+ }
12319
+ /**
12320
+ * `**` パスの接尾辞が「再帰の構造そのもの」を名指しているか。
12321
+ *
12322
+ * ノード自身(`nodes.**` / `nodes.**.children.*`)・子リスト(`nodes.**.children`)・その
12323
+ * `length`(`arr.length = 0` は配列を切り詰める)・多段の反復サブパスなら子リストへ至る
12324
+ * 途中のオブジェクト(`nodes.**.branch`)。書き側(`setAllRecursive`)は確定済みの
12325
+ * 子アドレスを壊すので拒否し、宣言側(`**` getter のキー)は生成 getter が実データの
12326
+ * 子リストを影にするので拒否する — 同じ述語を両方が使う。
12327
+ *
12328
+ * 反復サブパスを**途中まで**名指す形もすべて構造。`"." + repeatList` との完全一致だけを
12329
+ * 見ると、多段の repeat で途中のオブジェクトが素通りし、深さ 0 の `branch` を置き換えた
12330
+ * 瞬間に確定済みの深さ 1 のアドレスが宙に浮く(着地後レビューで実測。実装計画 §7-3)。
12331
+ */
12332
+ function isStructuralSuffix(spec, suffix) {
12333
+ const repeatSegments = spec.repeat.split(DELIMITER);
12334
+ const unit = DELIMITER + spec.repeat;
12335
+ let rest = suffix;
12336
+ while (rest.startsWith(unit)) {
12337
+ rest = rest.slice(unit.length);
12338
+ }
12339
+ if (rest.length === 0 || rest === DELIMITER + spec.repeatList + DELIMITER + "length") {
12340
+ return true;
12341
+ }
12342
+ for (let i = 1; i < repeatSegments.length; i++) {
12343
+ if (rest === DELIMITER + repeatSegments.slice(0, i).join(DELIMITER)) {
12344
+ return true;
12345
+ }
12346
+ }
12347
+ return false;
12348
+ }
12349
+ /** 深さ k の具体パスを作る。上限超過は生成前に throw する(設計書 D11)。 */
12350
+ function concretePathAt(spec, suffix, depth) {
12351
+ if (depth < 0) {
12352
+ raiseError(`Recursion depth must not be negative (got ${depth}).`);
12353
+ }
12354
+ let path = spec.anchor;
12355
+ for (let i = 0; i < depth; i++) {
12356
+ path += DELIMITER + spec.repeat;
12357
+ }
12358
+ const full = path + suffix;
12359
+ // ワイルドカード段数は展開後のパス全体で数える(アンカー・反復・接尾辞をすべて含む)。
12360
+ // intern(getPathInfo)より**前**に文字列から数える — 上限超過のパスを永続キャッシュ
12361
+ // (PathInfo の `_cache`)に残さない(設計書 D10「毎ノードの固有パスを intern しない」)。
12362
+ let wildcardCount = 0;
12363
+ for (const segment of full.split(DELIMITER)) {
12364
+ if (segment === WILDCARD) {
12365
+ wildcardCount++;
12366
+ }
12367
+ }
12368
+ if (wildcardCount > MAX_WILDCARD_DEPTH) {
12369
+ raiseError(`[wcs/recursion-depth-exceeded] Recursion on "${spec.anchor}" reached depth ${depth} ` +
12370
+ `("${full}"), which needs ${wildcardCount} wildcard levels — the limit is ${MAX_WILDCARD_DEPTH}. ` +
12371
+ `Either the data nests deeper than the engine can address, or the tree contains a cycle.`);
12372
+ }
12373
+ return full;
12374
+ }
12375
+ /** 深さ k のノードパス(接尾辞なし)。リストパスの登録に使う(このモジュール内だけ)。 */
12376
+ function nodePathAt(spec, depth) {
12377
+ return concretePathAt(spec, "", depth);
12378
+ }
12379
+ /**
12380
+ * 深さ k のノードが持つ子リストのパス(`nodes.*.children` / `nodes.*.children.*.children` …)。
12381
+ * `listPaths` へ登録する対象(E4)。深さ 0 のアンカー自身のリスト(`nodes`)も含める。
12382
+ */
12383
+ function listPathsUpTo(spec, depth) {
12384
+ const paths = [];
12385
+ // アンカー自身のリスト(末尾の `.*` を落とした形)
12386
+ paths.push(spec.anchorList);
12387
+ for (let k = 0; k < depth; k++) {
12388
+ paths.push(nodePathAt(spec, k) + DELIMITER + spec.repeatList);
12389
+ }
12390
+ return paths;
12391
+ }
12392
+ /**
12393
+ * 具体パスが「その再帰 getter の深さ k の展開形」なら深さを返す。違えば null。
12394
+ *
12395
+ * 文字列中の反復語の出現数で数えない — 接尾辞が反復語と同じ綴りを含む場合に
12396
+ * 取り違える。前から `anchor`、後ろから `suffix` を確かめ、間が `repeat` の
12397
+ * 反復ちょうどであることを見る。
12398
+ */
12399
+ function depthOfConcretePath(spec, suffix, path) {
12400
+ if (!path.startsWith(spec.anchor)) {
12401
+ return null;
12402
+ }
12403
+ if (suffix.length > 0 && !path.endsWith(suffix)) {
12404
+ return null;
12405
+ }
12406
+ // 接頭辞と接尾辞が**重なって**はならない。重なると slice が空文字に畳まれて
12407
+ // 「深さ 0 で一致」に見え、アンカーそのもの(`nodes.*` — 実データの行)が
12408
+ // 生成 getter に隠される。接尾辞が `.*` の `nodes.**.*` で実際に踏んだ。
12409
+ if (path.length < spec.anchor.length + suffix.length) {
12410
+ return null;
12411
+ }
12412
+ const middle = path.slice(spec.anchor.length, path.length - suffix.length);
12413
+ if (middle.length === 0) {
12414
+ return 0;
12415
+ }
12416
+ const unit = DELIMITER + spec.repeat;
12417
+ let depth = 0;
12418
+ let cursor = 0;
12419
+ while (cursor < middle.length) {
12420
+ if (!middle.startsWith(unit, cursor)) {
12421
+ return null;
12422
+ }
12423
+ cursor += unit.length;
12424
+ depth++;
12425
+ }
12426
+ return depth;
12427
+ }
12428
+
12429
+ const cacheEntryByAbsoluteStateAddress = new WeakMap();
12430
+ function getCacheEntryByAbsoluteStateAddress(address) {
12431
+ return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
12432
+ }
12433
+ function setCacheEntryByAbsoluteStateAddress(address, cacheEntry) {
12434
+ if (cacheEntry === null) {
12435
+ cacheEntryByAbsoluteStateAddress.delete(address);
12436
+ }
12437
+ else {
12438
+ cacheEntryByAbsoluteStateAddress.set(address, cacheEntry);
12439
+ }
12440
+ }
12441
+ function dirtyCacheEntryByAbsoluteStateAddress(address) {
12442
+ const cacheEntry = cacheEntryByAbsoluteStateAddress.get(address);
12443
+ if (cacheEntry) {
12444
+ cacheEntry.dirty = true;
12445
+ }
12446
+ }
12447
+
12448
+ /**
12449
+ * recursion/generation.ts
12450
+ *
12451
+ * 再帰レジストリの**世代の後始末**。state の再セットで前世代のレジストリが捨てられるとき、
12452
+ * その世代が生やしたもの — own の生成アクセサ・依存表の辺・評価結果のキャッシュ — を忘れる。
12453
+ * レジストリ本体(宣言の検証・パス族の代数・遅延実体化)から切り出してある: ここだけが
12454
+ * 依存表・キャッシュ・台帳という state 全体の構造に触る。
12455
+ */
12456
+ /**
12457
+ * この機構が生やした getter。作者が手で書いた同名 getter と見分けるために使う
12458
+ * (前者は忘れてよい・後者は衝突として拒否する)。
12459
+ */
12460
+ const generatedGetters = new WeakSet();
12461
+ function markGeneratedGetter(getter) {
12462
+ generatedGetters.add(getter);
12463
+ }
12464
+ /** この機構の生成物か(own に残った生成 getter)。 */
12465
+ function isGeneratedGetter(descriptor) {
12466
+ return typeof descriptor.get === "function" && generatedGetters.has(descriptor.get);
12467
+ }
12468
+ /**
12469
+ * 前世代が生やしたものを忘れる(state の再セット時に呼ぶ)。忘れるのは 3 つ。
12470
+ *
12471
+ * **own の生成アクセサ。** 生成 getter は state オブジェクトの own プロパティとして残る。
12472
+ * 同じオブジェクトを `$recursion` 無し(または別アンカー)で再セットしたとき、残したままだと
12473
+ * `getStateInfo` が `getterPaths` に拾い直し、読むと作者が書いていない `nodes.**.value` を
12474
+ * 名指す `wcs/recursion-unsupported` になる(第 4 サイクルで実測)。`getStateInfo` の
12475
+ * **前**に消す。
12476
+ *
12477
+ * **依存辺。** `_state` のセッタは `_listPaths` / `_getterPaths` / `_pathSet` をクリアするが、
12478
+ * 依存表(`_staticDependency` / `_dynamicDependency`)は state の寿命を越えて残る。
12479
+ * 通常のパスはそれで正しい — 同じ綴りのパスは新しい state でも同じ意味を持つ。
12480
+ * だが**生成アクセサは違う**。新しい世代ではまだ実体化されておらず、それを指す辺だけが
12481
+ * 残ると、次の構造書き込みで依存ウォークが「アクセサの無い具体パス」へ降りて落ちる。
12482
+ * 依存表そのものをクリアしてはならない(既存バインディングの辺まで消えて、再セット後の
12483
+ * 集計が更新されなくなる — 実測済み)。この世代が作った辺だけを外す。
12484
+ *
12485
+ * **キャッシュ。** 辺を外した以上、生成アクセサの評価結果も一緒に落とさなければ
12486
+ * ならない。同じ state オブジェクト(または同じ配列)を再セットすると、台帳は配列の
12487
+ * identity をキーにしているので ListIndex も絶対アドレスも世代を跨いで同一のまま残り、
12488
+ * 旧世代の `dirty:false` の値がそのまま次の読みに返る。辺が無いので、次に再帰 getter を
12489
+ * 読むまでの間の構造書き込み(`nodes.0.children = […]`)はそれを dirty にできない。
12490
+ * 別のオブジェクト・別の配列なら ListIndex が新しく鋳造されるので何も残らない。
12491
+ */
12492
+ function forgetGeneration(stateElement, previousState, generatedPaths) {
12493
+ if (generatedPaths.size === 0) {
12494
+ return;
12495
+ }
12496
+ for (const path of generatedPaths) {
12497
+ const descriptor = Object.getOwnPropertyDescriptor(previousState, path);
12498
+ if (typeof descriptor !== "undefined" && isGeneratedGetter(descriptor)) {
12499
+ delete previousState[path];
12500
+ }
12501
+ }
12502
+ for (const map of [stateElement.staticDependency, stateElement.dynamicDependency]) {
12503
+ for (const path of generatedPaths) {
12504
+ map.delete(path);
12505
+ }
12506
+ for (const [source, targets] of map) {
12507
+ let kept = null;
12508
+ for (let i = 0; i < targets.length; i++) {
12509
+ if (generatedPaths.has(targets[i])) {
12510
+ kept ??= targets.slice(0, i);
12511
+ continue;
12512
+ }
12513
+ kept?.push(targets[i]);
12514
+ }
12515
+ if (kept !== null) {
12516
+ if (kept.length === 0) {
12517
+ map.delete(source);
12518
+ }
12519
+ else {
12520
+ map.set(source, kept);
12521
+ }
12522
+ }
12523
+ }
12524
+ }
12525
+ forgetCacheEntries(stateElement, previousState, generatedPaths);
12526
+ }
12527
+ /**
12528
+ * 生成アクセサの評価結果のキャッシュを落とす。生成パスごとに、その `wildcardParentPathInfos`
12529
+ * (`nodes` / `nodes.*.children` / … に加えて、接尾辞側のリスト `nodes.*.tags` 等)を旧 state の
12530
+ * データと台帳に沿って降り、末端の行 ListIndex で絶対アドレスを引く。
12531
+ *
12532
+ * 深さ方向だけを降りて「ノード行の ListIndex × その深さのパス」で引くのでは足りない —
12533
+ * 接尾辞にワイルドカードを持つ getter(`get "nodes.**.tags.*.up"()`)のキャッシュは
12534
+ * タグ行の ListIndex(連鎖長 depth+2)に載っていて、ノード行の ListIndex では届かない
12535
+ * (第 2 サイクルのレビューで実測: 再セット後の読みが旧値のまま残った)。
12536
+ */
12537
+ function forgetCacheEntries(stateElement, previousState, generatedPaths) {
12538
+ for (const concretePath of generatedPaths) {
12539
+ const pathInfo = getPathInfo(concretePath);
12540
+ const absPathInfo = getAbsolutePathInfo(stateElement, pathInfo);
12541
+ const lists = pathInfo.wildcardParentPathInfos;
12542
+ const forget = (owner, ownerListIndex, level) => {
12543
+ if (level === lists.length) {
12544
+ setCacheEntryByAbsoluteStateAddress(createAbsoluteStateAddress(absPathInfo, ownerListIndex), null);
12545
+ return;
12546
+ }
12547
+ // 直前のリストの行(または state のルート)から、次のリストまでの相対セグメントを辿る
12548
+ const from = level === 0 ? 0 : lists[level - 1].segments.length + 1;
12549
+ let list = owner;
12550
+ for (const segment of lists[level].segments.slice(from)) {
12551
+ list = list?.[segment];
12552
+ }
12553
+ if (!Array.isArray(list)) {
12554
+ return;
12555
+ }
12556
+ // 台帳が無い = 走査を一度も経ていないリスト。その行に絶対アドレスは作られていない。
12557
+ const rows = getListIndexesByList(list);
12558
+ if (rows === null) {
12559
+ return;
12560
+ }
12561
+ const count = Math.min(rows.length, list.length);
12562
+ for (let i = 0; i < count; i++) {
12563
+ forget(list[i], rows[i], level + 1);
12564
+ }
12565
+ };
12566
+ forget(previousState, null, 0);
12567
+ }
12568
+ }
12569
+
12570
+ /**
12571
+ * recursion/registry.ts
12572
+ *
12573
+ * state 1 つぶんの再帰レジストリ。宣言・`**` getter の定義・展開済みアクセサの台帳を
12574
+ * 持ち、「具体パスを読む直前に、その深さのアクセサを生やす」遅延実体化を担う。
12575
+ *
12576
+ * 遅延であることは実装の**不変条件**である(Phase A の A6/A7)。そのパスを一度でも
12577
+ * 読んでから生やしても、`isCacheable` が `wildcardCount > 0` だけでキャッシュ可を返す
12578
+ * ため `undefined` が `dirty:false` で固定され、以後どう書いても回復しない。
12579
+ * したがって実体化は `getByAddress` のキャッシュ参照**前**に置く(E5)。
12580
+ *
12581
+ * 寿命は state の世代と共にする。`_state` の再セットで `getterPaths` / `listPaths` は
12582
+ * クリアされるので、レジストリも作り直す(§1-3)。ただし**生やしたアクセサは state
12583
+ * オブジェクトの側に残る**ので、同じ state を再セットすると `getStateInfo` がそれを
12584
+ * `getterPaths` に復元する。そのとき「もう生えているから何もしない」と早期 return して
12585
+ * しまうと `listPaths` の登録だけが抜け落ちるため、生成物は WeakSet で見分けて
12586
+ * 登録だけをやり直す。
12587
+ */
12588
+ /** 宣言時(構築時)の診断コード。lint(vscode-wcs)が同じコードで先に出す。 */
12589
+ const DECLARATION_INVALID = "[wcs/recursion-declaration-invalid]";
12590
+ class RecursionRegistry {
12591
+ spec;
12592
+ _definitions = new Map();
12593
+ _accessors = new Map();
12594
+ /**
12595
+ * `recursiveGetterOwning` の記憶。キーは添字を `*` に畳んだ形(`nodes.1.total` と `nodes.2.total`
12596
+ * は 1 つ)、値は「その具体パスを展開形(またはその値の内側)として持つ `**` getter」、
12597
+ * 無ければ null。
12598
+ *
12599
+ * 有界である: キーは添字を畳んだワイルドカード形のパス文字列で、`PathInfo` が intern する集合
12600
+ * (バインディング・getter・API 引数に綴られたパスと、その展開形)の部分集合にしかならない。
12601
+ * intern 済みパスの集合が有界であることは D10 で受け入れ済みなので、ここも同じ上限に収まる。
12602
+ * 文字列は WeakSet に入らないので、寿命はレジストリ(= state の世代)と共にする。
12603
+ */
12604
+ _ownerByPath = new Map();
12605
+ /**
12606
+ * 書き込みのホットパス(`setByAddress`)向けの記憶。キーは intern 済みの `PathInfo` なので
12607
+ * 寿命と上限は PathInfo の intern 集合と同じ(WeakMap)。畳み(split + Number + join)は
12608
+ * miss のときだけ払う — 宣言のある state では**アンカー外を含む全書き込み**がここを通る
12609
+ * (第 4 サイクルで実測: 畳みを毎回払うと `s.counter = i` で +100ns/書き込み)。
12610
+ */
12611
+ _ownerByPathInfo = new WeakMap();
12612
+ /**
12613
+ * 読みのホットパス(`getByAddress`)向けの記憶。`_ownerByPathInfo` と対称で、キーは
12614
+ * intern 済みの `PathInfo`、値は「そのパスの展開アクセサ」、展開形でなければ null。
12615
+ * 宣言のある state では**アンカー外を含む全読み**(親ウォークの各段を含む)がここを
12616
+ * 通るので、文字列キーの `Map.get` + `Set.has` + `startsWith` を毎回払わせない
12617
+ * (第 5 サイクルで実測)。
12618
+ *
12619
+ * 読みの否定判定の記憶は**ここ 1 つ**(第 5 サイクル再検証で文字列キーの `_nonAccessors` を撤去 —
12620
+ * 前段にこの記憶を置いた後は、PathInfo とパス文字列が 1:1 なので二重に持つだけだった)。
12621
+ * 否定を記憶してよい根拠は、定義集合が state の世代内で不変であること —
12622
+ * 同じ `PathInfo` は同じパス文字列なので、いちど「展開形でない」と決まった PathInfo が
12623
+ * 後から実体化されることはない。実体化した側は `materializeForPathInfo` が
12624
+ * `_define` の戻り値でそのまま記憶を更新する(否定が実体化を隠さない)。
12625
+ */
12626
+ _accessorByPathInfo = new WeakMap();
12627
+ /** `concretePathAt` の記憶(接尾辞 → 深さ順の具体パス)。 */
12628
+ _concreteBySuffix = new Map();
12629
+ _registeredListPaths = new Set();
12630
+ constructor(spec, state) {
12631
+ this.spec = spec;
12632
+ // getter 本体は実行しない。descriptor だけを見て `**` を含むキーを拾う。
12633
+ const descriptors = getAllPropertyDescriptors(state);
12634
+ for (const [key, descriptor] of Object.entries(descriptors)) {
12635
+ if (!hasRecursionWildcard(key)) {
12636
+ continue;
12637
+ }
12638
+ if (typeof descriptor.set === "function") {
12639
+ raiseError(`${DECLARATION_INVALID} Recursive setters are not supported in this version: "${key}". ` +
12640
+ `Declare a plain path setter, or write through the concrete path.`);
12641
+ }
12642
+ if (typeof descriptor.get !== "function") {
12643
+ raiseError(`${DECLARATION_INVALID} "${key}" contains "**" but is not a getter. ` +
12644
+ `The recursion wildcard only names a family of computed paths.`);
12645
+ }
12646
+ const suffix = splitRecursivePath(spec, key);
12647
+ if (suffix === null) {
12648
+ raiseError(recursionAnchorMismatchMessage(key, spec.recursiveAnchor));
12649
+ }
12650
+ if (suffix.length === 0) {
12651
+ // `get "nodes.**"` は展開すると `nodes.*` そのもの。`getByAddress` は
12652
+ // 「パスが target にあるか」を先に見るので、実データの行が丸ごと隠れる。
12653
+ raiseError(`${DECLARATION_INVALID} "${key}" names the recursive node itself. "**" names a computed path ` +
12654
+ `under a node (for example "${spec.recursiveAnchor}${DELIMITER}total"), not the node.`);
12655
+ }
12656
+ // 構造の判定は添字綴り(`get "nodes.**.children.0"()`)も畳んでから掛ける(書き側と同じ)
12657
+ if (isStructuralSuffix(spec, foldSuffixIndexes(suffix))) {
12658
+ // `get "nodes.**.children"` / `.children.*` / `.children.length` / 多段なら `.branch` は
12659
+ // 展開すると実データの子リスト(子ノード・その length・途中のオブジェクト)そのもの。
12660
+ // 生成 getter が `getByAddress` の「パスが target にあるか」で勝ち、実データの木を
12661
+ // 深さ 1 以下ごと無言で影にする(第 2 サイクルのレビューで実測: `$getAll("nodes.**.value", [])`
12662
+ // が `[1, 2]` に縮んだ)。書き側が同じ形を `recursion-structural-write` で拒否するのと対称。
12663
+ raiseError(`${DECLARATION_INVALID} "${key}" names the recursion structure itself (a node, its ` +
12664
+ `"${spec.repeatList}" list or that list's length, or an ` +
12665
+ `object on the way to that list). A recursive getter would hide the real child list at every ` +
12666
+ `depth — "**" names a computed leaf under a node (for example "${spec.recursiveAnchor}${DELIMITER}total").`);
12667
+ }
12668
+ this._definitions.set(key, {
12669
+ recursivePath: key,
12670
+ suffix,
12671
+ get: descriptor.get,
12672
+ });
12673
+ }
12674
+ this._assertNoColliding();
12675
+ this._assertNoConcreteCollision(descriptors);
12676
+ }
12677
+ /**
12678
+ * 作者が手で書いた具体パス(`get "nodes.*.children.*.total"()` / データプロパティ)が、宣言済み
12679
+ * `**` getter の展開形と同名でないことを**構築時に**確かめる。
12680
+ *
12681
+ * `_define` の衝突検査は「その深さを最初に読んだとき」にしか走らないので、データが浅い間は
12682
+ * 通り、木が 1 段深くなった瞬間にバインディングが落ちていた(第 3 サイクルのレビューで実測)。
12683
+ * 前世代の生成物(own に残った生成 getter)は衝突ではない — 同じ state の再セットで必ず居る。
12684
+ */
12685
+ _assertNoConcreteCollision(descriptors) {
12686
+ for (const [key, descriptor] of Object.entries(descriptors)) {
12687
+ if (hasRecursionWildcard(key) || isGeneratedGetter(descriptor)) {
12688
+ continue;
12689
+ }
12690
+ const owner = this._matchExpansion(key);
12691
+ if (owner !== null) {
12692
+ raiseError(`${DECLARATION_INVALID} "${key}" is already defined on the state, so the recursive getter ` +
12693
+ `"${owner}" cannot expand to it. Rename one of them.`);
12694
+ }
12695
+ }
12696
+ }
12697
+ /**
12698
+ * 2 本の `**` getter が同じ具体パスへ展開しないことを、宣言だけから静的に確かめる。
12699
+ *
12700
+ * 衝突するのは「片方の接尾辞がもう片方の接尾辞の末尾で、差分が反復語の整数倍」の
12701
+ * ときだけ(`nodes.**.total` と `nodes.**.children.*.total` は深さ k と k+1 で
12702
+ * 同じ `nodes.*.children.*.total` になる)。検出しないと `_definitions` の挿入順で
12703
+ * 最初に一致した方が無言で勝つ。
12704
+ */
12705
+ _assertNoColliding() {
12706
+ const definitions = Array.from(this._definitions.values());
12707
+ for (let i = 0; i < definitions.length; i++) {
12708
+ for (let j = i + 1; j < definitions.length; j++) {
12709
+ if (sameFamily(this.spec, definitions[i].suffix, definitions[j].suffix)) {
12710
+ raiseError(`${DECLARATION_INVALID} "${definitions[i].recursivePath}" and "${definitions[j].recursivePath}" ` +
12711
+ `expand to the same concrete path at different depths (they differ by whole repetitions of ` +
12712
+ `"${this.spec.repeat}"). Rename one of them.`);
12713
+ }
12714
+ }
12715
+ }
12716
+ }
12717
+ /**
12718
+ * `**` getter を 1 本でも宣言しているか。
12719
+ * **テスト・診断専用**(ランタイムの経路は `_definitions.size` を直接見る)。
12720
+ */
12721
+ get hasDefinitions() {
12722
+ return this._definitions.size > 0;
12723
+ }
12724
+ /**
12725
+ * その接尾辞が宣言済みの `**` getter と衝突するなら、その getter のパスを返す。
12726
+ *
12727
+ * 完全一致だけでは足りない。①反復語の整数倍だけ違う接尾辞は同じ族を指す
12728
+ * (`_assertNoColliding` が宣言どうしについて既に見ている条件)②getter の**下**を
12729
+ * 指す形(`nodes.**.total.x` / 反復語ぶんずれた `nodes.**.children.*.total.x`)は、
12730
+ * getter が返したオブジェクトへ書いてキャッシュを汚し、次の無効化で無言に戻る。
12731
+ * どちらも書き込みの入口(列挙より前)で止める — 述語は expand.ts の `coversSuffix`。
12732
+ */
12733
+ conflictingRecursiveGetter(suffix) {
12734
+ for (const definition of this._definitions.values()) {
12735
+ if (coversSuffix(this.spec, definition.suffix, suffix)) {
12736
+ return definition.recursivePath;
12737
+ }
12738
+ }
12739
+ return null;
12740
+ }
12741
+ /**
12742
+ * `recursiveGetterOwning` の intern 済み `PathInfo` 版(書き込みのホットパス用)。
12743
+ * WeakMap の hit なら畳みも照合も払わない。
12744
+ */
12745
+ recursiveGetterOwningPath(pathInfo) {
12746
+ const known = this._ownerByPathInfo.get(pathInfo);
12747
+ if (typeof known !== "undefined") {
12748
+ return known;
12749
+ }
12750
+ const owner = this.recursiveGetterOwning(pathInfo.path);
12751
+ this._ownerByPathInfo.set(pathInfo, owner);
12752
+ return owner;
12753
+ }
12754
+ /**
12755
+ * 具体パスを展開形(またはその値の内側)として持つ `**` getter のパス。無ければ null。
12756
+ * **実体化はしない。**
12757
+ *
12758
+ * `conflictingRecursiveGetter` の**具体パス版**で、`**` を経ない 2 つの入口が使う:
12759
+ *
12760
+ * - バインド確立時のパス存在検査(`checkDeclaredPath`)。あの時点ではまだ生えて
12761
+ * いないので、素の存在検査では必ず「解決できない」になる。展開形そのもの
12762
+ * (`nodes.*.total`)だけでなく、その値の中を指す形(`nodes.*.stats.count` で
12763
+ * `get "nodes.**.stats"()` がオブジェクトを返す)も、通常の getter の下と同じく
12764
+ * 評価しないと分からないので黙る側に倒す。
12765
+ * - 書き込みの入口(`setByAddress`)。`$setAll("nodes.*.children.*.total", [], v)` や
12766
+ * `this["nodes.1.total"] = v` は `**` を含まないので `setAllRecursive` の
12767
+ * 読み取り専用検査を通らず、未実体化なら fast path が行オブジェクトへ素の
12768
+ * プロパティとして書いてしまう(ノードを汚し、代入値が `dirty:false` で載って
12769
+ * 以後 getter が評価されない)。展開形への書き込みは、実体化の前後に関わらず
12770
+ * `wcs/recursion-readonly` で止める。
12771
+ */
12772
+ recursiveGetterOwning(concretePath) {
12773
+ const accessor = this._accessors.get(concretePath);
12774
+ if (typeof accessor !== "undefined") {
12775
+ return accessor.recursivePath;
12776
+ }
12777
+ // 添字綴り(`$setAll("nodes.1.total", [], v)` — API のパス引数は set トラップと違って
12778
+ // getResolvedAddress の正規化を経ない)は、添字を `*` に畳んでから照合する。畳まないと
12779
+ // `nodes[1].total` へ素の値が書かれる(第 3 回レビューで実測)。**無条件に**畳む —
12780
+ // 「アンカーで始まらないときだけ」にすると、ワイルドカードと添字の混在綴り
12781
+ // (`nodes.*.children.0.total`)がアンカーで始まるせいで畳まれず、`depthOfConcretePath` が
12782
+ // `.children.0` を反復単位と認めずに素通りする(第 4 回レビューで実測)。
12783
+ // 記憶のキーは畳んだ形 — 添字綴りのまま記憶すると綴りの数だけ単調に増える。
12784
+ const pattern = indexSegmentsToWildcard(concretePath);
12785
+ const known = this._ownerByPath.get(pattern);
12786
+ if (typeof known !== "undefined") {
12787
+ return known;
12788
+ }
12789
+ let owner = null;
12790
+ if (pattern.startsWith(this.spec.anchor)) {
12791
+ // 展開形そのもの → その値の内側(`.` 境界で切った接頭辞を長い方から)の順に照合する。
12792
+ // 接頭辞はアンカーより長いものだけ — アンカー自身は接尾辞が空なので getter になり得ない。
12793
+ owner = this._matchExpansion(pattern);
12794
+ for (let end = pattern.lastIndexOf(DELIMITER); owner === null && end > this.spec.anchor.length; end = pattern.lastIndexOf(DELIMITER, end - 1)) {
12795
+ owner = this._matchExpansion(pattern.slice(0, end));
12796
+ }
12797
+ }
12798
+ // 定義集合は state の世代内で不変なので、判定は記憶してよい(アンカー外の否定も含む)。
12799
+ this._ownerByPath.set(pattern, owner);
12800
+ return owner;
12801
+ }
12802
+ /** 具体パスが宣言済み `**` getter の展開形そのものなら、その getter のパス。 */
12803
+ _matchExpansion(concretePath) {
12804
+ for (const definition of this._definitions.values()) {
12805
+ if (depthOfConcretePath(this.spec, definition.suffix, concretePath) !== null) {
12806
+ return definition.recursivePath;
12807
+ }
12808
+ }
12809
+ return null;
12810
+ }
12811
+ /**
12812
+ * `materializeFor` の `PathInfo` 版。**読みのホットパス(`getByAddress`)専用**で、
12813
+ * 判定そのものは `materializeFor` に委ね、結果(否定を含む)を PathInfo に記憶する。
12814
+ * 書き側の `recursiveGetterOwningPath` と対称。
12815
+ */
12816
+ materializeForPathInfo(stateElement, pathInfo) {
12817
+ // `**` getter の無い宣言(レジストリは空)は、記憶を作らずに抜ける
12818
+ if (this._definitions.size === 0) {
12819
+ return null;
12820
+ }
12821
+ const known = this._accessorByPathInfo.get(pathInfo);
12822
+ if (typeof known !== "undefined") {
12823
+ return known;
12824
+ }
12825
+ const accessor = this.materializeFor(stateElement, pathInfo.path);
12826
+ this._accessorByPathInfo.set(pathInfo, accessor);
12827
+ return accessor;
12828
+ }
12829
+ /**
12830
+ * 具体パスが再帰 getter の展開形なら、そのアクセサを(未登録なら生やして)返す。
12831
+ * 該当しなければ null。読みは `materializeForPathInfo` を通るので、ここへ来るのは
12832
+ * 記憶が外れたときだけ — 判定は接頭辞 1 回で抜け、ここでは否定を記憶しない(記憶は
12833
+ * `materializeForPathInfo` の PathInfo キーの 1 か所)。
12834
+ * (`**` getter の無い空レジストリを弾くのは呼び出し側の役目。)
12835
+ */
12836
+ materializeFor(stateElement, concretePath) {
12837
+ const known = this._accessors.get(concretePath);
12838
+ if (typeof known !== "undefined") {
12839
+ return known;
12840
+ }
12841
+ if (!concretePath.startsWith(this.spec.anchor)) {
12842
+ return null;
12843
+ }
12844
+ let matched = null;
12845
+ let matchedDepth = 0;
12846
+ for (const definition of this._definitions.values()) {
12847
+ const depth = depthOfConcretePath(this.spec, definition.suffix, concretePath);
12848
+ if (depth === null) {
12849
+ continue;
12850
+ }
12851
+ if (matched !== null) {
12852
+ // コンストラクタの静的検査で弾いているはずの形。保険として先着を無言で採らない。
12853
+ raiseError(`"${concretePath}" matches both "${matched.recursivePath}" and "${definition.recursivePath}".`);
12854
+ }
12855
+ matched = definition;
12856
+ matchedDepth = depth;
12857
+ }
12858
+ if (matched === null) {
12859
+ return null;
12860
+ }
12861
+ return this._define(stateElement, matched, matchedDepth, concretePath);
12862
+ }
12863
+ _define(stateElement, definition, depth, concretePath) {
12864
+ // 作者が同じ具体パスを手で定義していないか。**プロトタイプチェーンまで**見る —
12865
+ // class 構文の getter は own ではなく prototype に載る(`getStateInfo` が
12866
+ // `getterPaths` に拾うのと同じ範囲)。own しか見ないと、生成アクセサが own に
12867
+ // 定義されて作者の getter を無言で影にする。前世代の生成物(own・WeakSet に載る
12868
+ // get)だけは上書きしてよい。
12869
+ const existing = stateElement.findStateDescriptor(concretePath);
12870
+ if (typeof existing !== "undefined" && !isGeneratedGetter(existing)) {
12871
+ raiseError(`"${concretePath}" is already defined on the state, so the recursive getter ` +
12872
+ `"${definition.recursivePath}" cannot expand to it. Rename one of them.`);
12873
+ }
12874
+ const accessor = Object.freeze({
12875
+ recursivePath: definition.recursivePath,
12876
+ depth,
12877
+ });
12878
+ const body = definition.get;
12879
+ const generated = function () {
12880
+ return body.call(this);
12881
+ };
12882
+ markGeneratedGetter(generated);
12883
+ // 前世代の生成物が残っていても、登録(getterPaths / setPathInfo / listPaths)は
12884
+ // この世代でやり直す必要があるので、descriptor ごと定義し直す。
12885
+ stateElement.defineTreeAccessor(concretePath, {
12886
+ get: generated,
12887
+ enumerable: false,
12888
+ configurable: true,
12889
+ });
12890
+ this._registerListPaths(stateElement, depth);
12891
+ this._accessors.set(concretePath, accessor);
12892
+ return accessor;
12893
+ }
12894
+ /**
12895
+ * 経路上のリストパスを `listPaths` に載せる(E4)。`setPathInfo(path, "for")` は
12896
+ * 使えない — あちらは `elementPaths` にも入れて `setByAddress` の swap 経路
12897
+ * (`isSwappable`)を変えてしまう。ここで要るのは「依存ウォークがこのパスを
12898
+ * リストとして展開する」ことだけ。
12899
+ */
12900
+ _registerListPaths(stateElement, depth) {
12901
+ for (const listPath of listPathsUpTo(this.spec, depth)) {
12902
+ if (this._registeredListPaths.has(listPath)) {
12903
+ continue;
12904
+ }
12905
+ this._registeredListPaths.add(listPath);
12906
+ stateElement.addListPath(listPath);
12907
+ }
12908
+ }
12909
+ /**
12910
+ * この世代が生やしたもの(own の生成アクセサ・依存辺・キャッシュ)を忘れる(state の
12911
+ * 再セット時、`getStateInfo` の再収集より**前**に呼ぶ)。実体は generation.ts。
12912
+ */
12913
+ forgetGenerated(stateElement, previousState) {
12914
+ forgetGeneration(stateElement, previousState, new Set(this._accessors.keys()));
12915
+ }
12916
+ /**
12917
+ * `**` 接尾辞の深さ `depth` の具体パス(`concretePathAt` の記憶付き版)。
12918
+ * 束縛形の読み(`this["nodes.**.value"]` / 省略形 `$getAll`)は再帰 getter の評価ごとに
12919
+ * ここを通るので、深さぶんの文字列連結とワイルドカード数えを毎回やり直さない。
12920
+ * 上限は「接尾辞の種類 × 128」で有界(上限超過は `concretePathAt` が throw するので載らない)。
12921
+ */
12922
+ concretePathAt(suffix, depth) {
12923
+ let byDepth = this._concreteBySuffix.get(suffix);
12924
+ if (typeof byDepth === "undefined") {
12925
+ byDepth = [];
12926
+ this._concreteBySuffix.set(suffix, byDepth);
12927
+ }
12928
+ let path = byDepth[depth];
12929
+ if (typeof path === "undefined") {
12930
+ path = concretePathAt(this.spec, suffix, depth);
12931
+ byDepth[depth] = path;
12932
+ }
12933
+ return path;
12934
+ }
12935
+ /** 展開済みアクセサのメタデータ(深さ解決・診断・テスト用)。 */
12936
+ accessorFor(concretePath) {
12937
+ return this._accessors.get(concretePath) ?? null;
12938
+ }
12939
+ /**
12940
+ * これまでに実体化した具体パスの一覧。**テスト専用**(「読んだ深さだけが生える」という
12941
+ * 遅延実体化の不変条件を外から確かめる口。ランタイムはどの経路からも呼ばない)。
12942
+ */
12943
+ get materializedPaths() {
12944
+ return new Set(this._accessors.keys());
12945
+ }
12946
+ }
12947
+
12948
+ /**
12949
+ * stream/streamNamespace.ts
12950
+ *
12951
+ * `$streamStatus` / `$streamError` の read-only namespace proxy
12952
+ * (docs/state-streams-design.md §4-1 / §4-2)。commandNamespace と対称。
12953
+ *
12954
+ * - state element 単位で memo 化し、同一 stateElement なら同じ proxy が返る。
12955
+ * - 宣言された stream 名(`$streams` に列挙されたもの)のみ registry entry の
12956
+ * status / error を返す。宣言外の名前・Symbol キーは undefined
12957
+ * (`then` / `constructor` 等を内部機構が触っても throw しない寛容規約、
12958
+ * $command と同じ)。
12959
+ * - 値は memo しない: proxy は getStreamEntries を毎回読む thin gateway
12960
+ * (status / error は runtime が随時書き換えるため。registry entry が正本、§2-1)。
12961
+ * - set / deleteProperty は raiseError。setByAddress の親走査が namespace proxy に
12962
+ * 到達したときの Reflect.set もここで落ちる(書き込み防御 S11 の終端)。
12963
+ */
12964
+ const statusNamespaceByStateElement = new WeakMap();
12965
+ const errorNamespaceByStateElement = new WeakMap();
12966
+ function createStreamNamespaceProxy(stateElement, namespaceName, pick) {
12967
+ return new Proxy(Object.create(null), {
12968
+ get(_target, prop) {
12969
+ if (typeof prop !== "string") {
12970
+ return undefined;
12971
+ }
12972
+ const entry = getStreamEntries(stateElement).get(prop);
12973
+ if (typeof entry === "undefined") {
12974
+ return undefined;
12975
+ }
12976
+ return pick(entry);
12977
+ },
12978
+ has(_target, prop) {
12979
+ return typeof prop === "string" && getStreamEntries(stateElement).has(prop);
12980
+ },
12981
+ ownKeys() {
12982
+ return Array.from(getStreamEntries(stateElement).keys());
12983
+ },
12984
+ getOwnPropertyDescriptor(_target, prop) {
12985
+ if (typeof prop !== "string") {
12986
+ return undefined;
12987
+ }
12988
+ const entry = getStreamEntries(stateElement).get(prop);
12989
+ if (typeof entry === "undefined") {
12990
+ return undefined;
12991
+ }
12992
+ return {
12993
+ configurable: true,
12994
+ enumerable: true,
12995
+ value: pick(entry),
12996
+ };
12997
+ },
12998
+ set() {
12999
+ raiseError(`${namespaceName} namespace is read-only; assigning to it is not allowed.`);
13000
+ },
13001
+ deleteProperty() {
13002
+ raiseError(`${namespaceName} namespace is read-only; deleting from it is not allowed.`);
13003
+ },
13004
+ });
13005
+ }
13006
+ function getStreamStatusNamespace(stateElement) {
13007
+ const cached = statusNamespaceByStateElement.get(stateElement);
13008
+ if (typeof cached !== "undefined") {
13009
+ return cached;
13010
+ }
13011
+ const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_STATUS_NAMESPACE_NAME, (entry) => entry.status);
13012
+ statusNamespaceByStateElement.set(stateElement, proxy);
13013
+ return proxy;
13014
+ }
13015
+ function getStreamErrorNamespace(stateElement) {
13016
+ const cached = errorNamespaceByStateElement.get(stateElement);
13017
+ if (typeof cached !== "undefined") {
13018
+ return cached;
13019
+ }
13020
+ const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_ERROR_NAMESPACE_NAME, (entry) => entry.error);
13021
+ errorNamespaceByStateElement.set(stateElement, proxy);
13022
+ return proxy;
13023
+ }
13024
+ /**
13025
+ * 両 namespace proxy の memo を破棄する(clearCommandNamespace と対称)。
13026
+ * disconnectedCallback と `_state` 再 set 時に呼ばれる。
13027
+ */
13028
+ function clearStreamNamespace(stateElement) {
13029
+ statusNamespaceByStateElement.delete(stateElement);
13030
+ errorNamespaceByStateElement.delete(stateElement);
13031
+ }
13032
+
13033
+ /**
13034
+ * stream/argsTrace.ts
13035
+ *
13036
+ * `$streams` の args トレース(依存捕捉、docs/state-streams-design.md §3-1)。
13037
+ *
13038
+ * - モジュールスコープの collector を立てて readonly proxy 上で args を評価し、
13039
+ * getByAddress を通った読みを絶対アドレス(IAbsoluteStateAddress)として捕捉する。
13040
+ * AbsolutePathInfo / AbsoluteStateAddress は両方キャッシュ済みのため、捕捉した
13041
+ * アドレスは drain バッチと Set.has のインスタンス同一性で O(1) 照合できる(§2-1)。
13042
+ * - collectStreamDependency は getByAddress のホットパスから毎読み呼ばれるため、
13043
+ * collector === null なら即 return し、それ以外の計算を一切しない。
13044
+ * - 起動・restart のたびに traceArgs が呼ばれ、成功時は entry.depAddresses を
13045
+ * 丸ごと置換する(per-run の動的再捕捉)。失敗時は前回成功 run の検証済み
13046
+ * 捕捉を保持する(§2-2 の「error からも依存変化で restart」を保つ)。
13047
+ * - lastNotified.ts と同じく import 循環回避のための小モジュール
13048
+ * (getByAddress → argsTrace ← streamRuntime の一方向依存に保つ)。
13049
+ */
13050
+ /** トレース中のみ非 null。getByAddress を通った読みの絶対アドレスが溜まる。 */
13051
+ let collector = null;
13052
+ /**
13053
+ * getByAddress の入口(checkDependency 直後)から毎読み呼ばれるフック。
13054
+ * トレース外(collector === null)では何もしない。
13055
+ */
13056
+ function collectStreamDependency(stateElement, address) {
13057
+ if (collector === null) {
13058
+ return;
13059
+ }
13060
+ const absolutePathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
13061
+ collector.add(createAbsoluteStateAddress(absolutePathInfo, address.listIndex));
13062
+ }
13063
+ /**
13064
+ * args を readonly proxy で同期評価し、読まれたパスを entry.depAddresses に
13065
+ * 丸ごと置換で再捕捉する(§3-1)。評価値(source の第 1 引数になる)を返す。
11832
13066
  *
11833
13067
  * - args === null(宣言で省略)なら depAddresses を clear して undefined
11834
13068
  * (依存なし = 起動後 restart しない)。
@@ -12894,20 +14128,6 @@ function isInternalProperty(name) {
12894
14128
  return name.startsWith("$");
12895
14129
  }
12896
14130
 
12897
- function getAllPropertyDescriptors(obj) {
12898
- const chain = [];
12899
- let proto = obj;
12900
- while (proto && proto !== Object.prototype) {
12901
- chain.push(proto);
12902
- proto = Object.getPrototypeOf(proto);
12903
- }
12904
- const descriptors = {};
12905
- for (let i = chain.length - 1; i >= 0; i--) {
12906
- Object.assign(descriptors, Object.getOwnPropertyDescriptors(chain[i]));
12907
- }
12908
- return descriptors;
12909
- }
12910
-
12911
14131
  /**
12912
14132
  * DCC の `$bindables` / `$commands` 宣言を解析・検証する。
12913
14133
  *
@@ -13073,9 +14293,32 @@ function defineDCC(hostElement, shadowRoot, state) {
13073
14293
  // 一意性はレジストリ単位なので、別スコープの同名 DCC は衝突しない。
13074
14294
  raiseError(`DCC: "${tagName}" is already registered. A custom element name can only be defined once.`);
13075
14295
  }
13076
- // ShadowRoot cloneNode 不可のため、template 経由で内容をクローン
14296
+ // ShadowRoot 自体は cloneNode 不可なので、子ノードを 1 つずつ template へ取り込む。
14297
+ //
14298
+ // かつては `template.innerHTML = shadowRoot.innerHTML` と serialize → parse で
14299
+ // 往復していた。これをやめたのは 3 点の理由による(docs/csp.md §7):
14300
+ // (1) `require-trusted-types-for 'script'` 下では innerHTML sink が弾かれる。
14301
+ // ここはテンプレート=作者が書いた DOM の複製でしかないので、policy を作って
14302
+ // 署名するより sink 自体を無くすほうが筋が良い(state が CSP の
14303
+ // `trusted-types` allowlist を要求しなくなる)。
14304
+ // (2) 往復のたびに HTML パーサの再解釈が挟まり、元の DOM と一致しない結果に
14305
+ // なり得る(mXSS と同じ機序)。
14306
+ // (3) 単純に serialize + parse のぶん遅い。
14307
+ //
14308
+ // importNode は **取り込み先 document** で要素を作るため、template の inert な
14309
+ // contents document 側から呼べば従来どおり「未 upgrade の複製」になる。live
14310
+ // document 側で cloneNode すると upgrade reaction が走り、_ensureShadow の明示
14311
+ // upgrade と二重になる。
14312
+ //
14313
+ // 挙動差が 1 つある: script 要素の already-started フラグは複製時に引き継がれる
14314
+ // ため、テンプレート内のインライン `<script>` はインスタンス生成のたびにネイティブ
14315
+ // 実行されなくなる。`<wcs-state>` の状態定義スクリプトは text を読んで評価する実装
14316
+ // (loadFromInnerScript)なので影響を受けない。
13077
14317
  const template = document.createElement("template");
13078
- template.innerHTML = shadowRoot.innerHTML;
14318
+ const inertDocument = template.content.ownerDocument;
14319
+ for (const childNode of Array.from(shadowRoot.childNodes)) {
14320
+ template.content.appendChild(inertDocument.importNode(childNode, true));
14321
+ }
13079
14322
  const shadowRootMode = shadowRoot.mode;
13080
14323
  // $bindables / $commands から wcBindable + bindableEventMap を生成
13081
14324
  const { bindables, commands, streamBackedBindables } = processDccDeclarations(state);
@@ -13373,25 +14616,6 @@ function getContextListIndex(handler, structuredPath) {
13373
14616
  return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
13374
14617
  }
13375
14618
 
13376
- const cacheEntryByAbsoluteStateAddress = new WeakMap();
13377
- function getCacheEntryByAbsoluteStateAddress(address) {
13378
- return cacheEntryByAbsoluteStateAddress.get(address) ?? null;
13379
- }
13380
- function setCacheEntryByAbsoluteStateAddress(address, cacheEntry) {
13381
- if (cacheEntry === null) {
13382
- cacheEntryByAbsoluteStateAddress.delete(address);
13383
- }
13384
- else {
13385
- cacheEntryByAbsoluteStateAddress.set(address, cacheEntry);
13386
- }
13387
- }
13388
- function dirtyCacheEntryByAbsoluteStateAddress(address) {
13389
- const cacheEntry = cacheEntryByAbsoluteStateAddress.get(address);
13390
- if (cacheEntry) {
13391
- cacheEntry.dirty = true;
13392
- }
13393
- }
13394
-
13395
14619
  function checkDependency(handler, address) {
13396
14620
  // $untrackDependency スコープ中/setter 実行中は依存を張らない
13397
14621
  if (handler.untracking) {
@@ -13620,6 +14844,22 @@ function createOverlayValue(record, address, receiver, handler) {
13620
14844
  const privateData = isBase ? getPrivateData(record, address.listIndex) : {};
13621
14845
  return new Proxy(privateData, new OverlayValueHandler(record, markerParentPath, address.listIndex, isBase, receiver, handler));
13622
14846
  }
14847
+ /**
14848
+ * 公開 getter の読み(docs/state-overlay-export-design.md §2-1 の 4)。
14849
+ * `P.#m<id>` のオーバーレイ値に対する `Reflect.get(proxy, k)` と等価 — 作者の getter は
14850
+ * マーカーアドレスを push して評価されるので、依存辺・キャッシュはマーカー側に載る。
14851
+ */
14852
+ function readExportedAccessor(record, entry, listIndex, receiver, handler) {
14853
+ const address = createStateAddress(getPathInfo(entry.markerTerminalPath), listIndex);
14854
+ const proxy = createOverlayValue(record, address, receiver, handler);
14855
+ return Reflect.get(proxy, entry.suffix);
14856
+ }
14857
+ /** 公開 getter への書き込み(X9): setter があれば評価、無ければ overlay の set が raise する。 */
14858
+ function writeExportedAccessor(record, entry, listIndex, value, receiver, handler) {
14859
+ const address = createStateAddress(getPathInfo(entry.markerTerminalPath), listIndex);
14860
+ const proxy = createOverlayValue(record, address, receiver, handler);
14861
+ return Reflect.set(proxy, entry.suffix, value);
14862
+ }
13623
14863
  /**
13624
14864
  * `element.state` の公開面(chroot・M13)。相対キーを変換して親の proxy を通すだけの
13625
14865
  * 薄い翻訳で、値の解決(私有・getter・ツリー)は全て親ウォーク+オーバーレイが担う。
@@ -13705,6 +14945,203 @@ function createPublicMountState(record) {
13705
14945
  });
13706
14946
  }
13707
14947
 
14948
+ const exportIndexByStateElement = new WeakMap();
14949
+ const reportedShadows = new Set();
14950
+ function slotFor(stateElement, parentPath, key, create) {
14951
+ let byParent = exportIndexByStateElement.get(stateElement);
14952
+ if (typeof byParent === "undefined") {
14953
+ if (!create)
14954
+ return null;
14955
+ byParent = new Map();
14956
+ exportIndexByStateElement.set(stateElement, byParent);
14957
+ }
14958
+ let byKey = byParent.get(parentPath);
14959
+ if (typeof byKey === "undefined") {
14960
+ if (!create)
14961
+ return null;
14962
+ byKey = new Map();
14963
+ byParent.set(parentPath, byKey);
14964
+ }
14965
+ let slot = byKey.get(key);
14966
+ if (typeof slot === "undefined") {
14967
+ if (!create)
14968
+ return null;
14969
+ slot = { holders: new Set(), byListIndex: new WeakMap(), noIndex: null };
14970
+ byKey.set(key, slot);
14971
+ }
14972
+ return slot;
14973
+ }
14974
+ /**
14975
+ * 記録の getter / setter を公開索引に載せる(初回登録で 1 回・冪等)。
14976
+ * translateInnerPath のマーカー化を通すので accessorBySuffixByMarkerParent も同時に埋まる。
14977
+ * 翻訳できないアクセサ(ワイルドカード終端・部分マウントのみで接頭辞不一致)と、
14978
+ * `$` 名前空間のアクセサ(翻訳されずマーカーが付かない)は公開しない。
14979
+ * ルートエントリの無い部分マウントは公開位置(ツリー上のパス)を持たないので対象外。
14980
+ */
14981
+ function registerExports(record) {
14982
+ if (record.exports.size > 0 || record.rootEntry === null) {
14983
+ return;
14984
+ }
14985
+ const keys = new Set([...record.getterKeys, ...record.setterKeys]);
14986
+ for (const key of keys) {
14987
+ let markerPath;
14988
+ try {
14989
+ markerPath = translateInnerPath(record, key);
14990
+ }
14991
+ catch {
14992
+ continue;
14993
+ }
14994
+ const markerIndex = markerPath.indexOf(DELIMITER + record.marker);
14995
+ if (markerIndex === -1) {
14996
+ continue;
14997
+ }
14998
+ // `users.*.#m7.display` → 末端マーカーパス `users.*.#m7`・接尾 `display`・公開 `users.*.display`
14999
+ // (接尾は常に非空 — markerizeAccessorPath が空を raise 済み。公開パスはルート
15000
+ // エントリの外側パス+接尾なので常に 2 セグメント以上 = 親パスを持つ)
15001
+ const markerTerminalPath = markerPath.slice(0, markerIndex + 1 + record.marker.length);
15002
+ const suffix = markerPath.slice(markerTerminalPath.length + 1);
15003
+ const exportedPath = markerPath.slice(0, markerIndex) + DELIMITER + suffix;
15004
+ const exportedInfo = getPathInfo(exportedPath);
15005
+ // Internal wildcard accessors need their own row resolution and lifecycle
15006
+ // notifications. Only publish accessors at the mount instance's depth.
15007
+ if (exportedInfo.wildcardCount !== record.delta) {
15008
+ continue;
15009
+ }
15010
+ const entry = { markerTerminalPath, suffix, markerPath, exportedPath };
15011
+ record.exports.set(exportedPath, entry);
15012
+ slotFor(record.parentStateElement, exportedInfo.parentPath, exportedInfo.lastSegment, true)
15013
+ .holders.add({ ref: new WeakRef(record), entry });
15014
+ // エイリアス辺(X5): 子 getter のアドレス → 公開パス
15015
+ record.parentStateElement.addDynamicDependency(markerPath, exportedPath);
15016
+ // 未存在パスの遅延診断(X7): この公開パスへのバインドは「存在しない」ではない
15017
+ markExportedPath(record.parentStateElement, exportedPath);
15018
+ }
15019
+ record.parentStateElement.markHasMounts?.();
15020
+ }
15021
+ /** 読みの listIndex がホスト要素のループ文脈と一致するか(配下の深い文脈も一致とみなす) */
15022
+ function isInstanceOf(record, listIndex) {
15023
+ if (!record.component.isConnected) {
15024
+ return false;
15025
+ }
15026
+ const own = getLoopContextByNode(record.component)?.listIndex ?? null;
15027
+ if (listIndex === null) {
15028
+ return own === null;
15029
+ }
15030
+ let current = own;
15031
+ while (current !== null) {
15032
+ if (current === listIndex) {
15033
+ return true;
15034
+ }
15035
+ current = current.parentListIndex;
15036
+ }
15037
+ return false;
15038
+ }
15039
+ /** ホルダーが生きていて、この listIndex のインスタンスなら記録を返す */
15040
+ function liveInstance(holder, listIndex) {
15041
+ const record = holder.ref.deref();
15042
+ if (typeof record === "undefined" || !isInstanceOf(record, listIndex)) {
15043
+ return null;
15044
+ }
15045
+ return record;
15046
+ }
15047
+ /**
15048
+ * `P.k`(listIndex)に答える記録を引く。索引に無ければ null(今日どおり undefined 解決)。
15049
+ * 複数一致は raise。
15050
+ */
15051
+ function resolveExport(stateElement, parentPath, key, listIndex) {
15052
+ const slot = slotFor(stateElement, parentPath, key, false);
15053
+ if (slot === null) {
15054
+ return null;
15055
+ }
15056
+ const cached = listIndex === null ? slot.noIndex : (slot.byListIndex.get(listIndex) ?? null);
15057
+ if (cached !== null) {
15058
+ const record = liveInstance(cached, listIndex);
15059
+ if (record !== null) {
15060
+ return { record, entry: cached.entry };
15061
+ }
15062
+ }
15063
+ let found = null;
15064
+ let foundRecord = null;
15065
+ for (const holder of slot.holders) {
15066
+ const record = holder.ref.deref();
15067
+ if (typeof record === "undefined") {
15068
+ // 記録は回収済み(finalizer 発火前の窓)— 遅延 prune
15069
+ slot.holders.delete(holder);
15070
+ continue;
15071
+ }
15072
+ if (!isInstanceOf(record, listIndex)) {
15073
+ continue;
15074
+ }
15075
+ if (foundRecord !== null) {
15076
+ raiseError(`[wcs/mount-export-ambiguous] "${parentPath}${DELIMITER}${key}" is exported by two mounted components on the same instance: ` +
15077
+ `<${foundRecord.component.tagName.toLowerCase()}> and <${record.component.tagName.toLowerCase()}>. ` +
15078
+ `Mount only one of them there, or rename one accessor. See docs/state-overlay-export-design.md X4.`);
15079
+ }
15080
+ found = holder;
15081
+ foundRecord = record;
15082
+ }
15083
+ if (found === null || foundRecord === null) {
15084
+ return null;
15085
+ }
15086
+ if (listIndex === null) {
15087
+ slot.noIndex = found;
15088
+ }
15089
+ else {
15090
+ slot.byListIndex.set(listIndex, found);
15091
+ }
15092
+ return { record: foundRecord, entry: found.entry };
15093
+ }
15094
+ /** 公開パスの `$postUpdate` を、記録のホスト要素のループ文脈で打つ(X6)。 */
15095
+ function notifyExports(record) {
15096
+ const parent = record.parentStateElement;
15097
+ const loopContext = getLoopContextByNode(record.component);
15098
+ if (parent.isConnected === false || (record.delta > 0 && loopContext === null)) {
15099
+ // A removed tree needs no notification. A removed row is handled by its
15100
+ // parent's list update. Other notification failures must remain visible.
15101
+ return;
15102
+ }
15103
+ for (const entry of record.exports.values()) {
15104
+ parent.createState("readonly", (state) => {
15105
+ state[setLoopContextSymbol](loopContext, () => {
15106
+ state.$postUpdate(entry.exportedPath);
15107
+ });
15108
+ });
15109
+ }
15110
+ }
15111
+ /**
15112
+ * X1: ツリーに同名キーがある公開 getter は親から読まれない(ツリーが勝つ)。
15113
+ * 登録時に 1 回 warn(タグ × 公開パス)。行マウントはホスト要素のループ文脈で読む。
15114
+ */
15115
+ function warnShadowedExports(record) {
15116
+ const loopContext = getLoopContextByNode(record.component);
15117
+ if (record.delta > 0 && loopContext === null) {
15118
+ // 行マウントでループ文脈が無い(行の実体化前)— 読めないので黙る
15119
+ return;
15120
+ }
15121
+ const tag = record.component.tagName.toLowerCase();
15122
+ for (const entry of record.exports.values()) {
15123
+ const reportKey = `${tag}|${entry.exportedPath}`;
15124
+ if (reportedShadows.has(reportKey)) {
15125
+ continue;
15126
+ }
15127
+ const exportedInfo = getPathInfo(entry.exportedPath);
15128
+ let parentValue = undefined;
15129
+ record.parentStateElement.createState("readonly", (state) => {
15130
+ state[setLoopContextSymbol](loopContext, () => {
15131
+ parentValue = state[exportedInfo.parentPath];
15132
+ });
15133
+ });
15134
+ if (parentValue === null || typeof parentValue === "undefined"
15135
+ || !(exportedInfo.lastSegment in Object(parentValue))) {
15136
+ continue;
15137
+ }
15138
+ reportedShadows.add(reportKey);
15139
+ console.warn(`[@wcstack/state] [wcs/mount-export-shadowed] <${tag}>.${record.stateProp}.${entry.suffix} is exported at ` +
15140
+ `"${entry.exportedPath}" but the tree already has that key, so readers outside the component get the tree value. ` +
15141
+ `Remove the tree key or rename the accessor. See docs/state-overlay-export-design.md X1.`);
15142
+ }
15143
+ }
15144
+
13708
15145
  /**
13709
15146
  * このアドレスの値をキャッシュしてよいか(getByAddress / setByAddress 共通の判定)。
13710
15147
  *
@@ -13839,6 +15276,16 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
13839
15276
  return undefined;
13840
15277
  }
13841
15278
  const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
15279
+ // 公開 getter の dispatch(docs/state-overlay-export-design.md §2-1): 掛かるのは
15280
+ // 「ツリーの未存在キー」の分岐だけ(X1 — 命中する読みは無改造)。マウントの無い
15281
+ // state は boolean 1 個で抜ける(D18)
15282
+ if (stateElement.hasMounts === true && lastSegment !== WILDCARD
15283
+ && !(lastSegment in Object(parentValue))) {
15284
+ const exported = resolveExport(stateElement, parentAddress.pathInfo.path, lastSegment, address.listIndex);
15285
+ if (exported !== null) {
15286
+ return readExportedAccessor(exported.record, exported.entry, address.listIndex, receiver, handler);
15287
+ }
15288
+ }
13842
15289
  if (lastSegment === WILDCARD) {
13843
15290
  // listIndex が無いまま末尾ワイルドカードに到達 = そのパスの階数を満たす
13844
15291
  // ループ文脈が無い(`matrix.*.*` を 1 段の `for` の中で読む等)。元の文面は
@@ -13867,7 +15314,15 @@ function _getByAddressWithCache(target, address, receiver, handler, stateElement
13867
15314
  return value;
13868
15315
  }
13869
15316
  function getByAddress(target, address, receiver, handler) {
15317
+ // 再帰 getter の遅延実体化(Phase B)。**キャッシュ参照より前**でなければならない。
15318
+ // 未定義のまま一度読まれると isCacheable が wildcardCount > 0 だけでキャッシュ可を
15319
+ // 返すので undefined が dirty:false で固定され、後からアクセサを生やしても恒久的に
15320
+ // 直らない(Phase A の A7)。宣言の無い state は boolean 判定 1 個で抜け、宣言のある
15321
+ // state も 2 回目からは PathInfo キーの記憶 1 回で抜ける(書き側と対称・第 5 サイクル)。
13870
15322
  checkDependency(handler, address);
15323
+ if (handler.stateElement.hasRecursion === true) {
15324
+ handler.stateElement.recursionRegistry.materializeForPathInfo(handler.stateElement, address.pathInfo);
15325
+ }
13871
15326
  // $streams の args トレース中のみ絶対アドレスを捕捉(collector 非活性なら即 return)
13872
15327
  collectStreamDependency(handler.stateElement, address);
13873
15328
  const stateElement = handler.stateElement;
@@ -13902,15 +15357,6 @@ function safeVolumeRootNode(stateElement) {
13902
15357
  *
13903
15358
  * Throws: LIST-201(インデックス未解決)、BIND-201(ワイルドカード情報不整合)
13904
15359
  */
13905
- /**
13906
- * 各ワイルドカード階層で最後に観測したリスト値。**次の読みの差分基準**であり、
13907
- * ListIndex の同一性を跨いで保つために使う。
13908
- *
13909
- * 所有権は読み(`$getAll`)側にある。書き(`$setAll`)はこの走査を借りるだけで
13910
- * 記録を更新しない(`commitDiffBaseline: false`。設計 §6-2)。
13911
- */
13912
- // ToDo: IAbsoluteStateAddressに変更する
13913
- const lastValueByListAddress = new WeakMap();
13914
15360
  /**
13915
15361
  * `pathInfo` のワイルドカードを `indexes`(前方一致の接頭辞)で絞り込みつつ展開し、
13916
15362
  * マッチする添字タプルを列挙する。
@@ -13926,12 +15372,13 @@ function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, op
13926
15372
  return;
13927
15373
  }
13928
15374
  const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
13929
- const oldValue = lastValueByListAddress.get(wildcardAddress);
15375
+ const wildcardAbsAddress = createAbsoluteStateAddress(getAbsolutePathInfo(handler.stateElement, wildcardParentPathInfo), listIndex);
15376
+ const oldValue = getStateListBaseline(wildcardAbsAddress);
13930
15377
  const newValue = getByAddress(target, wildcardAddress, receiver, handler);
13931
15378
  const listDiff = createListDiff(listIndex, oldValue, newValue);
13932
15379
  const listIndexes = listDiff.newIndexes;
13933
15380
  const index = indexes[indexPos] ?? null;
13934
- newValueByAddress.set(wildcardAddress, newValue);
15381
+ newValueByAddress.set(wildcardAbsAddress, newValue);
13935
15382
  if (index === null) {
13936
15383
  for (let i = 0; i < listIndexes.length; i++) {
13937
15384
  const listIndex = listIndexes[i];
@@ -13956,7 +15403,7 @@ function collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, op
13956
15403
  walkWildcardPattern(pathInfo.wildcardParentPathInfos, 0, null, indexes, 0, [], resultIndexes);
13957
15404
  if (options.commitDiffBaseline) {
13958
15405
  for (const [address, newValue] of newValueByAddress.entries()) {
13959
- lastValueByListAddress.set(address, newValue);
15406
+ setStateListBaseline(address, Array.isArray(newValue) ? newValue : []);
13960
15407
  }
13961
15408
  }
13962
15409
  return resultIndexes;
@@ -14329,9 +15776,10 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
14329
15776
  const parentAbsPathInfo = getAbsolutePathInfo(context.stateElement, parentPathInfo);
14330
15777
  const parentAddress = createStateAddress(parentPathInfo, parentListIndex);
14331
15778
  const parentAbsAddress = createAbsoluteStateAddress(parentAbsPathInfo, parentListIndex);
14332
- const lastValue = getLastListValueByAbsoluteStateAddress(parentAbsAddress);
15779
+ const lastValue = getStateListBaseline(parentAbsAddress);
14333
15780
  const newValue = context.stateProxy[getByAddressSymbol](parentAddress);
14334
15781
  const listDiff = createListDiff(parentAddress.listIndex, lastValue, newValue);
15782
+ context.observedListValueByAbsAddress.set(parentAbsAddress, Array.isArray(newValue) ? newValue : []);
14335
15783
  const loopIndexes = getIndexes(listDiff, context.searchType);
14336
15784
  if (currentWildcardIndex === context.wildcardPaths.length - 1) {
14337
15785
  context.targetListIndexes.push(...loopIndexes);
@@ -14473,8 +15921,9 @@ function _collectDependencies(context, address, nextEntries) {
14473
15921
  const newValue = context.stateProxy[getByAddressSymbol](address);
14474
15922
  const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
14475
15923
  const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
14476
- const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
15924
+ const lastValue = getStateListBaseline(absAddress);
14477
15925
  const listDiff = createListDiff(address.listIndex, lastValue, newValue);
15926
+ context.observedListValueByAbsAddress.set(absAddress, Array.isArray(newValue) ? newValue : []);
14478
15927
  const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
14479
15928
  for (const listIndex of selection.fullRows) {
14480
15929
  const depAddress = createStateAddress(depPathInfo, listIndex);
@@ -14556,6 +16005,7 @@ function _collectDependencies(context, address, nextEntries) {
14556
16005
  }
14557
16006
  const expandContext = {
14558
16007
  stateElement: context.stateElement,
16008
+ observedListValueByAbsAddress: context.observedListValueByAbsAddress,
14559
16009
  targetListIndexes: [],
14560
16010
  wildcardPaths: depPathInfo.wildcardPaths,
14561
16011
  wildcardParentPaths: depPathInfo.wildcardParentPaths,
@@ -14598,12 +16048,14 @@ function walkDependency(stateElement, startAddress, staticDependency, dynamicDep
14598
16048
  callback(startAddress);
14599
16049
  return [];
14600
16050
  }
14601
- // パス単位のトポロジカル順位。値を一切読まずに求まり、依存グラフは追記のみで
14602
- // 成長するため epoch でメモ化される(topologicalRank.ts)。
16051
+ // パス単位のトポロジカル順位。値を一切読まないグラフ走査で毎回求める(キャッシュは
16052
+ // 持たない topologicalRank.ts)。依存グラフは追記が基本だが、再帰の再セットでは
16053
+ // 旧世代の生成アクセサを指す辺が外れることがある(recursion/registry.ts の forgetGenerated)。
14603
16054
  const ranks = getTopologicalRanks(startPath, staticDependency, dynamicDependency, MAX_DEPENDENCY_DEPTH);
14604
16055
  const context = {
14605
16056
  ranks: ranks,
14606
16057
  stateElement: stateElement,
16058
+ observedListValueByAbsAddress: new Map(),
14607
16059
  staticMap: staticDependency,
14608
16060
  dynamicMap: dynamicDependency,
14609
16061
  result: new Set(),
@@ -14615,6 +16067,12 @@ function walkDependency(stateElement, startAddress, staticDependency, dynamicDep
14615
16067
  keyedMergePath: options?.keyedMergePath ?? null,
14616
16068
  };
14617
16069
  _walkDependency(context, startAddress, callback);
16070
+ // 観測したリスト値を state 側の基準として確定する(E1)。ウォークの最中に進めると
16071
+ // 同じウォーク内の 2 度目の観測が「変化なし」になるため、走査を終えてからまとめて書く
16072
+ // (`collectWildcardIndexes` の commitDiffBaseline と同じ形)。
16073
+ for (const [absAddress, value] of context.observedListValueByAbsAddress) {
16074
+ setStateListBaseline(absAddress, value);
16075
+ }
14618
16076
  return Array.from(context.result);
14619
16077
  }
14620
16078
 
@@ -14739,6 +16197,8 @@ function _setByAddress(target, address, absAddress, value, receiver, handler, ke
14739
16197
  return Reflect.set(parentValue, index, value);
14740
16198
  }
14741
16199
  else {
16200
+ // 公開 getter への書き込み(X9)は setByAddressCore の fast path(親がオブジェクトの
16201
+ // 未存在キー)で dispatch 済み。ここに来るのは親が非オブジェクトの形だけ
14742
16202
  return Reflect.set(parentValue, lastSegment, value);
14743
16203
  }
14744
16204
  }
@@ -14871,6 +16331,21 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
14871
16331
  `Write "${shadowedSlot}" itself, or individual fields inside "${path}", instead.`);
14872
16332
  }
14873
16333
  }
16334
+ // 再帰 getter の展開形(`nodes.*.children.*.total`)とその値の内側への書き込みは、`**` を
16335
+ // 含まないので `setAllRecursive` の読み取り専用検査を通らない。未実体化なら下の fast path が
16336
+ // 「親オブジェクトの未存在キー」として行オブジェクトへ素の値を書き、代入値を `dirty:false` で
16337
+ // キャッシュに載せる — ノードが汚れ、実体化後も getter が評価されず、深さ 0 の集計まで
16338
+ // 巻き込む(レビュー P18 で実測)。実体化後は `Reflect.set` が false を返すだけの無言 no-op。
16339
+ // 読み側の遅延実体化(getByAddress の E5)と対称に、書き側はここで止める。
16340
+ // 宣言の無い state は boolean 判定 1 個で抜ける(D18)
16341
+ if (stateElement.hasRecursion === true) {
16342
+ const owner = stateElement.recursionRegistry.recursiveGetterOwningPath(address.pathInfo);
16343
+ if (owner !== null) {
16344
+ raiseError(`[wcs/recursion-readonly] "${path}" writes into the recursive getter "${owner}" ` +
16345
+ `(this path is that getter at one depth, or a path inside the value it derives), which has ` +
16346
+ `no setter. Write the values it derives from instead.`);
16347
+ }
16348
+ }
14874
16349
  // occurrence(wc-bindable の `semantics: "event"`)由来の書き込みは、同値でも
14875
16350
  // 「もう一度起きた」ことを落としてはならないため same-value guard を 1 回だけ飛ばす。
14876
16351
  // トークンはここで消費されるので、この write の内側で走る他の書き込みには波及しない。
@@ -14914,17 +16389,35 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
14914
16389
  });
14915
16390
  }
14916
16391
  recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
16392
+ let dispatchedExport = false;
14917
16393
  try {
14918
16394
  if (key === undefined) {
14919
16395
  // fast path 版の同じ取り違え(末尾ワイルドカードに listIndex が無い)。
14920
16396
  // 通常経路と同じ語彙で「何段必要か」を言う(pathDiagnostics.ts)。
14921
16397
  raiseError(wildcardScopeMessage(`path "${path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
14922
16398
  }
16399
+ // 公開 getter への書き込み(docs/state-overlay-export-design.md X9): 未存在キーへの
16400
+ // 書き込みは今日「ツリーに作る」が、その位置に公開 getter があると以後ツリーが勝ち
16401
+ // (X1)getter を無言で隠す。setter があれば setter、無ければ raise(overlay の set)
16402
+ if (stateElement.hasMounts === true && lastSegment !== WILDCARD && !(key in parentValue)) {
16403
+ const exported = resolveExport(stateElement, address.parentAddress.pathInfo.path, lastSegment, address.listIndex);
16404
+ if (exported !== null) {
16405
+ dispatchedExport = true;
16406
+ return writeExportedAccessor(exported.record, exported.entry, address.listIndex, value, receiver, handler);
16407
+ }
16408
+ }
14923
16409
  return Reflect.set(parentValue, key, value);
14924
16410
  }
14925
16411
  finally {
14926
16412
  notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
14927
- commitWriteCache(stateElement, path, absAddress, value, cacheable);
16413
+ if (dispatchedExport) {
16414
+ // Exported row paths are cacheable but absent from getterPaths. The
16415
+ // accessor may normalize or reject the input; never pin that input.
16416
+ dirtyCacheEntryByAbsoluteStateAddress(absAddress);
16417
+ }
16418
+ else {
16419
+ commitWriteCache(stateElement, path, absAddress, value, cacheable);
16420
+ }
14928
16421
  // DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
14929
16422
  dispatchBindableEvent(stateElement, address.pathInfo, { value });
14930
16423
  }
@@ -15028,6 +16521,292 @@ function resolve(target, _prop, receiver, handler) {
15028
16521
  };
15029
16522
  }
15030
16523
 
16524
+ /**
16525
+ * recursion/bind.ts
16526
+ *
16527
+ * オーサリング層の `**` を「いま評価している深さ」へ束縛する。
16528
+ *
16529
+ * 深さの根拠は**生成アクセサのアドレス**であって、文字列中の反復語の出現数ではない。
16530
+ * `getByAddress` は getterPaths に載るパスを読むときアドレスをスタックへ積むので、
16531
+ * 深さ k の再帰 getter の本体を評価している最中は、スタック先頭がその具体パスの
16532
+ * アドレスになっている。そこから深さを復元する(実装計画 §1-2)。
16533
+ *
16534
+ * 見るのは**スタック先頭だけ**である。添字(ListIndex)を供給する `getContextListIndex` /
16535
+ * `$getAll` の省略形も先頭しか見ないので、深さだけを外側のフレームから拾うと「深さは
16536
+ * 束縛されたが行は無い」という定義にない状態になる — `**` getter が別の素の getter を
16537
+ * 経由して `**` を読む形(`get "nodes.**.x"() { return this.helper }` /
16538
+ * `get helper() { return this["nodes.**.value"] }`)がそれで、直接読みは生の
16539
+ * `ListIndex not found`、`$getAll` の省略形は「束縛した深さ × 全行」という値を無言で
16540
+ * 返していた。深さと行は同じフレームから取る。
16541
+ */
16542
+ /**
16543
+ * 具体パスの**接頭辞**として最深のノードパスを見つけ、その深さを返す。
16544
+ * `nodes.*.children.*.label` のような行 getter の文脈から深さ 1 を取り出す用。
16545
+ */
16546
+ function depthOfConcretePathPrefix(registry, path) {
16547
+ const spec = registry.spec;
16548
+ if (!path.startsWith(spec.anchor)) {
16549
+ return null;
16550
+ }
16551
+ const unit = DELIMITER + spec.repeat;
16552
+ let depth = 0;
16553
+ let cursor = spec.anchor.length;
16554
+ while (path.startsWith(unit, cursor)) {
16555
+ cursor += unit.length;
16556
+ depth++;
16557
+ }
16558
+ // 接頭辞の直後はパス境界(末尾、または `.`)でなければならない。
16559
+ if (cursor !== path.length && path.charCodeAt(cursor) !== 46 /* '.' */) {
16560
+ return null;
16561
+ }
16562
+ return depth;
16563
+ }
16564
+ /**
16565
+ * 評価中のアドレス(スタック先頭)から再帰の深さを求める。再帰文脈でなければ null。
16566
+ * 先頭が null(ループ文脈の無いイベントハンドラ・初期同期)も再帰文脈ではない。
16567
+ */
16568
+ function currentRecursionDepth(handler, registry) {
16569
+ if (handler.addressStackLength === 0) {
16570
+ return null;
16571
+ }
16572
+ const address = handler.lastAddressStack;
16573
+ if (address === null) {
16574
+ return null;
16575
+ }
16576
+ const accessor = registry.accessorFor(address.pathInfo.path);
16577
+ if (accessor !== null) {
16578
+ return accessor.depth;
16579
+ }
16580
+ // 生成アクセサでなくても、宣言に合致する具体パス(行 getter・行のイベントハンドラが
16581
+ // 積むループのアドレス)ならそこから深さを取れる。
16582
+ return depthOfConcretePathPrefix(registry, address.pathInfo.path);
16583
+ }
16584
+ /**
16585
+ * `**` を含むパスを、いま評価している深さの具体パスへ書き換える。
16586
+ * 再帰文脈が無いところで `**` を直接読むのは、深さが決まらないので診断する。
16587
+ */
16588
+ function bindRecursivePath(stateElement, handler, path) {
16589
+ // 呼び出し元は 2 つとも `hasRecursion === true` をゲートにしているので、
16590
+ // ここに来た時点でレジストリは必ずある。到達不能な `??` 分岐は置かない
16591
+ // (カバレッジ閾値に効く — walkDependency の `address.listIndex!` と同じ綴り)。
16592
+ const registry = stateElement.recursionRegistry;
16593
+ // アンカー照合を先に行う。深さ解決を先にすると、綴り違いのアンカーが
16594
+ // 「文脈が無い」と報告されて原因に辿り着けない。
16595
+ const suffix = splitRecursivePath(registry.spec, path);
16596
+ if (suffix === null) {
16597
+ raiseError(recursionAnchorMismatchMessage(path, registry.spec.recursiveAnchor));
16598
+ }
16599
+ const depth = currentRecursionDepth(handler, registry);
16600
+ if (depth === null) {
16601
+ raiseError(`[wcs/recursion-context] "${path}" uses "**", which is bound to the depth of the recursive getter ` +
16602
+ `being evaluated, and there is no recursion context here. Read it from inside a recursive getter ` +
16603
+ `or a row getter under "${registry.spec.anchor}", or name a concrete depth ` +
16604
+ `(for example "${registry.spec.anchor}${path.slice(registry.spec.recursiveAnchor.length)}"). ` +
16605
+ `The depth comes from the innermost frame only: a plain getter reached from a recursive getter ` +
16606
+ `has no row of its own, so read "**" in the recursive getter and pass the value on.`);
16607
+ }
16608
+ // 照合済みの接尾辞をそのまま使う。具体パスは記憶付き(再帰 getter の評価ごとに通る経路)。
16609
+ return registry.concretePathAt(suffix, depth);
16610
+ }
16611
+
16612
+ /**
16613
+ * recursion/walk.ts
16614
+ *
16615
+ * 再帰アンカー配下を**全深さ**にわたって走査し、マッチする具体アドレスを列挙する。
16616
+ * `$getAll(path, [])` の合併形(設計書 §6-2)と `$setAll` のブロードキャストが共有する。
16617
+ *
16618
+ * 固定 arity の走査(`proxy/apis/wildcardIndexes.ts`)は「ワイルドカードの本数が
16619
+ * 静的に決まっている」ことに立脚しているので、そのままでは深さが動的な族を扱えない。
16620
+ * ここは深さ方向だけを自前で降り、**各深さの具体パスは固定 arity のまま**扱う
16621
+ * — つまりエンジンが見るパスは常に `**` を含まない普通のパスである(設計書 D2)。
16622
+ *
16623
+ * 順序は**深さ優先・行きがけ・添字昇順**(§1-2)。ノードを 1 つ出したら、その子へ
16624
+ * 降りきってから次の兄弟へ移る。読みと書きが同じ順序を使うことが `$setAll` の
16625
+ * 契約の前提になる。
16626
+ *
16627
+ * 走査が throw したとき、その走査が観測したリスト値は差分基準へ確定**しない**
16628
+ * (途中まで進めた基準を残すと、次の読みが「変化なし」と誤認しうる)。
16629
+ */
16630
+ /**
16631
+ * アンカー配下の全深さを列挙する。深さ優先・行きがけ・添字昇順。
16632
+ *
16633
+ * 終端は「その深さの子リストが空」。上限超過は `concretePathAt` が**その深さに実際に
16634
+ * ノードが居るときだけ**検査する(葉の 1 段先を投機的に見て落ちないように)。
16635
+ */
16636
+ function collectRecursiveAddresses(target, receiver, handler, registry, suffix) {
16637
+ const spec = registry.spec;
16638
+ const results = [];
16639
+ const observed = new Map();
16640
+ const pathsByDepth = [];
16641
+ const repeatList = spec.repeatList;
16642
+ const anchorList = spec.anchorList;
16643
+ /**
16644
+ * 「同じ配列インスタンスが 2 つ以上の親から到達可能」を**走査そのもの**で判定する
16645
+ * (設計書 D12・E6)。台帳の親(`newIndexes[0].parentListIndex`)で見てはならない
16646
+ * — 台帳はリスト配列の identity だけをキーにしていて、行オブジェクトを作り直す
16647
+ * ふつうのイミュータブル更新(`nodes.map(n => ({...n}))` は children を参照ごと
16648
+ * 引き継ぐ)でも親 ListIndex が別物になるため、正当な木を恒久的に拒否してしまう。
16649
+ *
16650
+ * 走査で見た配列を覚えておけば、共有も循環も「同じ配列に 2 度到達したか」で決まる。
16651
+ * 祖先の集合に居れば循環(自分より上へ戻る)、そうでなければ兄弟共有。
16652
+ * 空配列は行を持たないので別名化のしようがなく、追跡しない(`[]` の使い回しは正当)。
16653
+ */
16654
+ const ancestors = new Set();
16655
+ const visited = new Set();
16656
+ const guardShape = (listPath, value, seen) => {
16657
+ if (!Array.isArray(value) || value.length === 0) {
16658
+ return null;
16659
+ }
16660
+ const list = value;
16661
+ if (ancestors.has(list)) {
16662
+ raiseError(`[wcs/recursion-cycle] "${listPath}" is reachable from itself: the recursion on ` +
16663
+ `"${spec.anchor}" walked into a list that one of its own ancestors already owns. ` +
16664
+ `The data contains a cycle, which this version does not support.`);
16665
+ }
16666
+ if (seen.has(list)) {
16667
+ raiseError(`[wcs/recursion-shared-list] "${listPath}" is the same array instance as a list reached ` +
16668
+ `from another node. The recursion on "${spec.anchor}" needs a tree: give each node its own ` +
16669
+ `"${repeatList}" array.`);
16670
+ }
16671
+ return list;
16672
+ };
16673
+ const pathsAt = (depth) => {
16674
+ const known = pathsByDepth[depth];
16675
+ if (typeof known !== "undefined") {
16676
+ return known;
16677
+ }
16678
+ // 上限検査はここ(=その深さに実際にノードが居ると分かってから)。具体パスはレジストリの
16679
+ // 記憶(接尾辞 × 深さ)から引き、走査ごとに文字列連結をやり直さない。
16680
+ const concretePath = registry.concretePathAt(suffix, depth);
16681
+ const nodePath = suffix.length === 0 ? concretePath : registry.concretePathAt("", depth);
16682
+ const paths = {
16683
+ nodePath,
16684
+ concretePathInfo: getPathInfo(concretePath),
16685
+ childListPathInfo: getPathInfo(nodePath + DELIMITER + repeatList),
16686
+ };
16687
+ pathsByDepth[depth] = paths;
16688
+ return paths;
16689
+ };
16690
+ /** 接尾辞側に残ったワイルドカード段だけを、行の ListIndex を起点に展開する。 */
16691
+ const expandSuffix = (concretePathInfo, level, listIndex) => {
16692
+ const parents = concretePathInfo.wildcardParentPathInfos;
16693
+ if (level >= parents.length) {
16694
+ results.push(createStateAddress(concretePathInfo, listIndex));
16695
+ return;
16696
+ }
16697
+ // 接尾辞側のリストは検査しない。接尾辞が反復語を含む形(`nodes.**.children.*.value`)
16698
+ // では、接尾辞の展開と深さ方向の降下が**同じ配列**を通る — 同じ族を 2 通りに綴れる
16699
+ // ことの帰結で、共有ではない。次元をまたいでも、同じ次元の中でも(深さ 0 の接尾辞
16700
+ // 展開と深さ 1 の接尾辞展開が同じ配列に当たる)自己衝突するので、共有の判定は
16701
+ // 深さ方向にだけ掛ける。
16702
+ //
16703
+ // 結果として `$setAll("nodes.**.tags", [], arr)` のように**ブロードキャストが作った**
16704
+ // 配列共有は、ここでは捕まらない(`[wcs/wildcard-rank]` という無関係な文面で落ちる)。
16705
+ // 既知の制限として設計書に記録してある。
16706
+ const rows = readRows(parents[level], listIndex, null).rows;
16707
+ for (let i = 0; i < rows.length; i++) {
16708
+ expandSuffix(concretePathInfo, level + 1, rows[i]);
16709
+ }
16710
+ };
16711
+ /**
16712
+ * リストを 1 本読んで行と、追跡対象のリスト配列を返す。差分基準は state 側の
16713
+ * 共有正本(E1)から取り、観測値は走査の最後にまとめて確定する。`guardPath` が
16714
+ * 非 null のときだけ共有・循環の検査を掛ける(接尾辞側の普通のリストは再帰の
16715
+ * 対象ではない)。
16716
+ */
16717
+ function readRows(listPathInfo, parentListIndex, seen) {
16718
+ const listAddress = createStateAddress(listPathInfo, parentListIndex);
16719
+ const absAddress = createAbsoluteStateAddress(getAbsolutePathInfo(handler.stateElement, listPathInfo), parentListIndex);
16720
+ const value = getByAddress(target, listAddress, receiver, handler);
16721
+ const tracked = seen === null ? null : guardShape(listPathInfo.path, value, seen);
16722
+ const listDiff = createListDiff(parentListIndex, getStateListBaseline(absAddress), value);
16723
+ observed.set(absAddress, Array.isArray(value) ? value : []);
16724
+ if (tracked !== null && seen !== null) {
16725
+ seen.add(tracked);
16726
+ }
16727
+ return { rows: listDiff.newIndexes, tracked };
16728
+ }
16729
+ const descend = (depth, listPathInfo, parentListIndex) => {
16730
+ const { rows, tracked } = readRows(listPathInfo, parentListIndex, visited);
16731
+ if (rows.length === 0) {
16732
+ return;
16733
+ }
16734
+ const paths = pathsAt(depth);
16735
+ // 行が 1 つでもある ⟹ そのリストは非空配列だった ⟹ guardShape が追跡対象を返している
16736
+ // (非配列も空配列も createListDiff が空の行に畳むので、上の早期 return で抜ける)。
16737
+ // このリストは、いま降りている枝の祖先になる。子で同じ配列に当たれば循環。
16738
+ const branch = tracked;
16739
+ ancestors.add(branch);
16740
+ const flat = paths.concretePathInfo.wildcardCount === depth + 1;
16741
+ for (let i = 0; i < rows.length; i++) {
16742
+ const row = rows[i];
16743
+ if (flat) {
16744
+ // 接尾辞にワイルドカードが無い(大多数)。行の ListIndex がそのまま具体パスの
16745
+ // 連鎖長を満たすので、追加の走査は要らない。この行 ListIndex は createListDiff が
16746
+ // 台帳へ登録した正本そのものなので、`getListIndexByIndexes` で引き直す必要も無い。
16747
+ results.push(createStateAddress(paths.concretePathInfo, row));
16748
+ }
16749
+ else {
16750
+ expandSuffix(paths.concretePathInfo, depth + 1, row);
16751
+ }
16752
+ descend(depth + 1, paths.childListPathInfo, row);
16753
+ }
16754
+ ancestors.delete(branch);
16755
+ };
16756
+ descend(0, getPathInfo(anchorList), null);
16757
+ // 観測したリスト値を差分基準へ確定する。合併形の `$getAll` も再帰の `$setAll` も必ず確定する
16758
+ // (実装計画 §6 — cold な書き込みが ListIndex 世代を鋳造したまま基準を残さないと、次の
16759
+ // 構造変更で深い子台帳が孤児になる)。走査が throw したときは上の raise でここに来ない。
16760
+ for (const [address, value] of observed) {
16761
+ setStateListBaseline(address, value);
16762
+ }
16763
+ return results;
16764
+ }
16765
+
16766
+ /**
16767
+ * recursion/getAllRecursive.ts
16768
+ *
16769
+ * `$getAll("<anchor>.**.<suffix>", [])` — **全深さの合併**(設計書 D7 / §6-2)。
16770
+ *
16771
+ * 省略形(文脈束縛)とは別経路にする。省略形は「いま評価している深さの 1 本」を
16772
+ * 具体パスに直して既存の固定 arity 走査へ渡すだけだが、合併形は深さそのものを
16773
+ * 走査対象にするので、返る添字タプルの長さが結果ごとに変わる。だから合併形は
16774
+ * **値の配列しか返さない**(`$resolve` への往復は保証しない。設計書 §7-2)。
16775
+ */
16776
+ function getAllRecursive(target, receiver, handler, path, indexes) {
16777
+ // 呼び出し元(getAll.ts)は `hasRecursion === true` をゲートにしている。
16778
+ const registry = handler.stateElement.recursionRegistry;
16779
+ // 判定順はアンカー照合 → 添字の形。静的側(vscode-wcs recursionValidator)と同じ順に
16780
+ // しておかないと、綴り違いのアンカーに `[0]` を渡した呼び出しが片側では
16781
+ // `recursion-anchor`、もう片側では `recursion-getall-form` になる。
16782
+ const suffix = splitRecursivePath(registry.spec, path);
16783
+ if (suffix === null) {
16784
+ raiseError(recursionAnchorMismatchMessage(path, registry.spec.recursiveAnchor));
16785
+ }
16786
+ // 合併形の添字は `[]` だけ。`null` 等の非配列は素の TypeError にせず、形の診断にする。
16787
+ if (!Array.isArray(indexes)) {
16788
+ raiseError(`[wcs/recursion-getall-form] $getAll("${path}", indexes) with "**" takes either no indexes ` +
16789
+ `(to read the depth of the recursive getter being evaluated) or [] (to walk every depth) — ` +
16790
+ `got ${indexes === null ? "null" : typeof indexes}.`);
16791
+ }
16792
+ if (indexes.length > 0) {
16793
+ raiseError(`[wcs/recursion-getall-form] $getAll("${path}", indexes) with "**" takes no partial ` +
16794
+ `prefix: a prefix cannot say which depth it applies to. Omit the indexes to read the ` +
16795
+ `depth of the recursive getter being evaluated, or pass [] to walk every depth.`);
16796
+ }
16797
+ // 走査は観測したリスト値を差分基準へ確定する(再帰の `$setAll` も同じ。
16798
+ // 固定 arity の `$setAll` だけが確定しない — setAllRecursive.ts 第 1 相の注記)。
16799
+ const addresses = collectRecursiveAddresses(target, receiver, handler, registry, suffix);
16800
+ const values = [];
16801
+ for (let i = 0; i < addresses.length; i++) {
16802
+ // `**` は依存グラフに載らない(D2)。呼び出し元の getter は「触れた深さの
16803
+ // 具体パス」に依存する — その登録は getByAddress の checkDependency が行う
16804
+ // (他行読み取りの検出も含む)。ここで書き写すと untrack を見ない劣化版になる。
16805
+ values.push(getByAddress(target, addresses[i], receiver, handler));
16806
+ }
16807
+ return values;
16808
+ }
16809
+
15031
16810
  /**
15032
16811
  * getAllReadonly
15033
16812
  *
@@ -15044,7 +16823,25 @@ function resolve(target, _prop, receiver, handler) {
15044
16823
  function getAll(target, prop, receiver, handler) {
15045
16824
  const resolveFn = resolve(target, prop, receiver, handler);
15046
16825
  return (path, indexes) => {
16826
+ // オーサリング層の `**`。省略形は「いま評価している深さ」に束縛し、`[]` 明示は
16827
+ // 全深さの合併になる(設計書 §6-2)。部分接頭辞は `**` に対して定義できない。
16828
+ if (handler.stateElement.hasRecursion === true && hasRecursionWildcard(path)) {
16829
+ if (typeof indexes === "undefined") {
16830
+ path = bindRecursivePath(handler.stateElement, handler, path);
16831
+ }
16832
+ else {
16833
+ // アンカー照合と添字の形の検査は合併形の側で行う(判定順を静的側と揃えるため)
16834
+ return getAllRecursive(target, receiver, handler, path, indexes);
16835
+ }
16836
+ }
15047
16837
  const pathInfo = getPathInfo(path);
16838
+ // 渡された添字が配列でない(`null` 等)のは素の TypeError にせず、形の診断にする。
16839
+ // 省略(undefined)だけが「文脈の添字」を意味する。
16840
+ if (typeof indexes !== "undefined" && !Array.isArray(indexes)) {
16841
+ raiseError(`$getAll("${path}") requires the indexes to be an array when given ` +
16842
+ `(omit them for the loop context, or pass [] to expand every level) — got ` +
16843
+ `${indexes === null ? "null" : typeof indexes}.`);
16844
+ }
15048
16845
  if (handler.addressStackLength > 0) {
15049
16846
  const lastInfo = handler.lastAddressStack?.pathInfo ?? null;
15050
16847
  const stateElement = handler.stateElement;
@@ -15091,7 +16888,7 @@ function getAll(target, prop, receiver, handler) {
15091
16888
  indexes = [];
15092
16889
  }
15093
16890
  }
15094
- // 読みなので差分基準を更新する(`$setAll` は更新しない。設計 §6-2)
16891
+ // 読みなので差分基準を更新する(固定 arity の `$setAll` は更新しない。設計 §6-2)
15095
16892
  const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: true });
15096
16893
  const resultValues = [];
15097
16894
  for (let i = 0; i < resultIndexes.length; i++) {
@@ -15180,6 +16977,100 @@ function postUpdate(target, _prop, receiver, handler) {
15180
16977
  };
15181
16978
  }
15182
16979
 
16980
+ /**
16981
+ * recursion/setAllRecursive.ts
16982
+ *
16983
+ * `$setAll("<anchor>.**.<suffix>", [], value)` — **全深さへのブロードキャスト**
16984
+ * (設計書 D8 / §7-3)。
16985
+ *
16986
+ * 読み(`$getAll` の合併形)と同じ列挙を使い、同じ順序で書く。許すのは `[]` の
16987
+ * ブロードキャストだけで、mapper と `{ spread: true }` は受け付けない:
16988
+ *
16989
+ * - **mapper** の `(current, ...indexes)` は、深さごとに添字の本数が変わるので
16990
+ * そのままでは渡せない。深さを渡す別のシグネチャを決めてから入れる。
16991
+ * - **`{ spread: true }`** は「マッチ順に 1 件ずつ配る」形。順序は決定的に定義できるが、
16992
+ * 木に 1 次元配列を配るのは作者が走査順を知らないと使えず、実用にならない。
16993
+ *
16994
+ * 添字の省略も受け付けない。`$setAll` は「書き込み API に暗黙の文脈依存を持たせない」
16995
+ * という既存の決定(docs/state-set-all-design.md の D4)を継ぐので、読み側にある
16996
+ * 省略形(文脈束縛)の対応物を書き側には置かない。
16997
+ */
16998
+ /**
16999
+ * 接尾辞が「再帰の構造そのもの」を名指していないか(述語は expand.ts の `isStructuralSuffix`)。
17000
+ *
17001
+ * ノード自身(`nodes.**`)と子リスト(`nodes.**.children`)と子ノード
17002
+ * (`nodes.**.children.*`)と子リストの `length` は、書き換えると確定済みの子アドレスを壊す。
17003
+ * 初版は葉の属性の更新に限る(実装計画 §1-3)。判定対象は宣言から導出する。
17004
+ */
17005
+ function assertNotStructural(spec, path, suffix) {
17006
+ if (isStructuralSuffix(spec, suffix)) {
17007
+ raiseError(`[wcs/recursion-structural-write] "${path}" writes the recursion structure itself ` +
17008
+ `(a node, its "${spec.repeatList}" list or that list's length, or an object on the way to that list). ` +
17009
+ `This version broadcasts to leaf properties only — ` +
17010
+ `replacing a node would invalidate the child addresses already resolved for this write.`);
17011
+ }
17012
+ }
17013
+ function setAllRecursive(target, receiver, handler, path, indexes, value, options) {
17014
+ // 呼び出し元(setAll.ts)は `hasRecursion === true` をゲートにしているので必ずある。
17015
+ const registry = handler.stateElement.recursionRegistry;
17016
+ const suffix = splitRecursivePath(registry.spec, path);
17017
+ if (suffix === null) {
17018
+ raiseError(recursionAnchorMismatchMessage(path, registry.spec.recursiveAnchor));
17019
+ }
17020
+ // 検査には添字を `*` に畳んだ接尾辞を掛ける。`nodes.**.children.0`(子ノード)・
17021
+ // `nodes.**.children.0.children`(孫リスト)・`nodes.**.children.0.total`(getter の展開形)は
17022
+ // 添字綴りのままだと素の文字列一致をすり抜け、構造を置き換えたり部分書き込みの途中で
17023
+ // 生の TypeError になったりしていた(第 2 サイクルのレビューで実測)。列挙は綴りのまま
17024
+ // 行う — 接尾辞の添字は「その子だけ」を指す意味を持つ。
17025
+ const checkedSuffix = foldSuffixIndexes(suffix);
17026
+ // --- 形の検査は列挙より前(1 件も書かないことを保証する。設計 §7-3) ---
17027
+ if (!Array.isArray(indexes)) {
17028
+ raiseError(setAllValueKindMessage(path, `with "**" requires an explicit empty indexes array ([]) — the write API takes no context.`));
17029
+ }
17030
+ if (indexes.length > 0) {
17031
+ raiseError(`[wcs/recursion-setall-form] $setAll("${path}", indexes, …) with "**" takes no partial prefix: ` +
17032
+ `a prefix cannot say which depth it applies to. Pass [] to broadcast to every depth.`);
17033
+ }
17034
+ if (typeof value === "function") {
17035
+ raiseError(setAllValueKindMessage(path, `with "**" does not take a mapper yet — the index tuple has a different length at each depth.`));
17036
+ }
17037
+ if (options?.spread === true) {
17038
+ raiseError(setAllValueKindMessage(path, `with "**" does not take { spread: true } — handing a flat array to a tree needs the author ` +
17039
+ `to know the walk order, which is not a usable contract.`));
17040
+ }
17041
+ assertNotStructural(registry.spec, path, checkedSuffix);
17042
+ const conflicting = registry.conflictingRecursiveGetter(checkedSuffix);
17043
+ if (conflicting !== null) {
17044
+ raiseError(`[wcs/recursion-readonly] "${path}" writes into the recursive getter "${conflicting}", which has ` +
17045
+ `no setter — the two name the same family of concrete paths (or this path points inside the value ` +
17046
+ `that getter derives). Write the values it derives from instead.`);
17047
+ }
17048
+ // --- 第 1 相: 書き込み先を全部確定する ---
17049
+ // **観測したリスト値は基準へ確定する**(走査が必ず行う — walk.ts)。固定 arity の
17050
+ // `$setAll` は `commitDiffBaseline: false` で走るが、あれは「読みの私有基準を書きから
17051
+ // 動かさない」という E1 以前の所有権モデルの話で、いまの基準は読み・描画・依存ウォークが
17052
+ // 共有する state 側の正本である(実装計画 §3-2 の E1)。
17053
+ //
17054
+ // 確定しないと cold(読みも描画も一度も走っていない)状態の `$setAll` が全深さぶんの
17055
+ // ListIndex 世代を鋳造したまま基準を残さず、次の構造変更でその世代が見えない diff が
17056
+ // 行を鋳造し直す。生き残った深い children の台帳だけが死んだ世代の親を指し、以後
17057
+ // 再帰 getter の読みが恒久的に落ちる(値の合併は動き続けるので無症状のまま進む)。
17058
+ // 1 件も書かない `undefined` のブロードキャストでも同じなので、「書き込み 0 件」は
17059
+ // 「状態が動いていない」を意味しない。
17060
+ const addresses = collectRecursiveAddresses(target, receiver, handler, registry, suffix);
17061
+ // --- 第 2 相: 確定したアドレスにだけ書く ---
17062
+ let written = 0;
17063
+ for (let i = 0; i < addresses.length; i++) {
17064
+ // undefined は常にスキップ(設計 §5)。クリアは null。
17065
+ if (typeof value === "undefined") {
17066
+ continue;
17067
+ }
17068
+ setByAddress(target, addresses[i], value, receiver, handler);
17069
+ written++;
17070
+ }
17071
+ return written;
17072
+ }
17073
+
15183
17074
  /**
15184
17075
  * setAll.ts
15185
17076
  *
@@ -15198,6 +17089,12 @@ function postUpdate(target, _prop, receiver, handler) {
15198
17089
  */
15199
17090
  function setAll(target, _prop, receiver, handler) {
15200
17091
  return (path, indexes, value, options) => {
17092
+ // オーサリング層の `**`。書き側は `[]` のブロードキャストだけを受け付ける
17093
+ // (形の検査は列挙より前に行い、1 件も書かないことを保証する。設計 §7-3)。
17094
+ // 宣言の無い state は boolean 判定 1 個で抜ける。
17095
+ if (handler.stateElement.hasRecursion === true && hasRecursionWildcard(path)) {
17096
+ return setAllRecursive(target, receiver, handler, path, indexes, value, options);
17097
+ }
15201
17098
  const pathInfo = getPathInfo(path);
15202
17099
  // 書き込み API に暗黙の文脈依存は持たせない。`for` の中で `[]` と書けば
15203
17100
  // 「現在行」ではなく「全行」を意味する(設計 §4-1)。
@@ -15218,7 +17115,8 @@ function setAll(target, _prop, receiver, handler) {
15218
17115
  }
15219
17116
  // --- 第 1 相: 書き込み先を全部確定する(設計 §6) ---
15220
17117
  // 走査しながら書くと書き込みが ListIndex 集合を動かしうる。
15221
- // 差分基準(lastValueByListAddress)は読みの持ち物なので commit しない(§6-2)。
17118
+ // 差分基準(state 側の共有正本 stateListBaseline)は、固定 arity の `$setAll` は走査を
17119
+ // 借りるだけで確定しない(§6-2。再帰の `$setAll` は確定する — recursion/walk.ts)。
15222
17120
  const resultIndexes = collectWildcardIndexes(target, receiver, handler, pathInfo, indexes, { commitDiffBaseline: false });
15223
17121
  if (spread && value.length !== resultIndexes.length) {
15224
17122
  raiseError(setAllSpreadArityMessage(path, resultIndexes.length, value.length));
@@ -15275,6 +17173,15 @@ function setAll(target, _prop, receiver, handler) {
15275
17173
  */
15276
17174
  function trackDependency(_target, _prop, _receiver, handler) {
15277
17175
  return (path) => {
17176
+ // `**` はここでは解釈しない(依存辺は展開後の具体パスにしか張れない)。`$postUpdate` /
17177
+ // `$resolve` は `getPathInfo` の不変条件で落ちるが、この API は生の文字列を依存表へ
17178
+ // そのまま載せるので、ゲートを置かないと無言で受理されて getter が stale になる
17179
+ // (第 2 サイクルのレビューで実測)。宣言の有無に関わらず拒否する。
17180
+ if (hasRecursionWildcard(path)) {
17181
+ raiseError(`[wcs/recursion-unsupported] $trackDependency("${path}") cannot take "**" — a dependency is ` +
17182
+ `registered against a concrete path (a fixed number of "*"). Track the concrete depth, or read ` +
17183
+ `the path through this[...] / $getAll inside the getter so the dependency is recorded automatically.`);
17184
+ }
15278
17185
  if (handler.addressStackLength === 0) {
15279
17186
  raiseError(`No active state reference to track dependency for path "${path}".`);
15280
17187
  }
@@ -15424,6 +17331,27 @@ function updatedCallback(target, refs, receiver, handler) {
15424
17331
  return result;
15425
17332
  }
15426
17333
 
17334
+ /**
17335
+ * errorCallback.ts
17336
+ *
17337
+ * StateClass のライフサイクルフック「$errorCallback」を呼び出すユーティリティ関数。
17338
+ *
17339
+ * 主な役割:
17340
+ * - target に $errorCallback メソッドが定義されていれば、(error, info) で呼び出す
17341
+ * - this は writable な state proxy(receiver)— 作者はここで自分の state にエラーを書ける
17342
+ *
17343
+ * 設計ポイント:
17344
+ * - Reflect.get で安全に取得し、無ければ何もしない(disconnectedCallback と同型)
17345
+ * - 呼び出し元(apply/applyChangeFromBindings.ts)が drain 末尾でまとめて呼び、
17346
+ * callback 自身の throw もそこで隔離する。ここでは await しない
17347
+ */
17348
+ function errorCallback(target, error, info, receiver, _handler) {
17349
+ const callback = Reflect.get(target, STATE_ERROR_CALLBACK_NAME);
17350
+ if (typeof callback === "function") {
17351
+ callback.call(receiver, error, info);
17352
+ }
17353
+ }
17354
+
15427
17355
  /**
15428
17356
  * setLoopContext.ts
15429
17357
  *
@@ -15496,6 +17424,8 @@ function setLoopContext(handler, loopContext, callback) {
15496
17424
  * - 通常のプロパティアクセスもバインディングや多重ループに対応
15497
17425
  * - シンボルAPIやReflect.getで拡張性・互換性も確保
15498
17426
  */
17427
+ /** `$` + 数字だけの prop(`$1` / `$129`)。範囲外を無言で通さないための判別。 */
17428
+ const INDEX_PARAM_RE = /^\$\d+$/;
15499
17429
  // `$streamStatus.<name>` / `$streamError.<name>` の dotted パス判定用プレフィックス
15500
17430
  const STREAM_STATUS_PATH_PREFIX = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}`;
15501
17431
  const STREAM_ERROR_PATH_PREFIX = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}`;
@@ -15513,6 +17443,19 @@ function getSymbolApiCache(handler) {
15513
17443
  }
15514
17444
  function get(target, prop, receiver, handler) {
15515
17445
  const index = INDEX_BY_INDEX_NAME[prop];
17446
+ // `$` で始まらない読み(=通常のパス読みのほぼ全部)は charCode 1 個で抜ける。
17447
+ // 表引き失敗だけを条件にすると `$1`..`$N` 以外の**全プロパティ読み**が正規表現に
17448
+ // 触れることになり、行数×バインド数ぶん get トラップを回すリスト描画で効いてくる。
17449
+ if (typeof index === "undefined" && typeof prop === "string"
17450
+ && prop.charCodeAt(0) === 36 /* '$' */ && INDEX_PARAM_RE.test(prop)) {
17451
+ // `$1`..`$N` の表は MAX_WILDCARD_DEPTH ぶんしか無い。表引きに失敗した `$<数字>` は
17452
+ // これまで通常のプロパティ解決へ落ちて診断ゼロで undefined になっていた(`$128` は
17453
+ // 0 を返すのに `$129` だけが無言で壊れる)。境界のすぐ外側こそ名指しする
17454
+ // (docs/state-recursive-path-impl-plan.md §3-2 の E3)。綴り不正(`$0` / `$01`)も
17455
+ // 同じ入口で落ちるので、範囲だけでなく綴りも文面に含める。
17456
+ raiseError(`[wcs/index-param-range] "${prop}" is not a valid list index parameter: they run from ` +
17457
+ `${INDEX_PARAM_PREFIX}1 to ${INDEX_PARAM_PREFIX}${MAX_WILDCARD_DEPTH}, with no leading zeros.`);
17458
+ }
15516
17459
  if (typeof index !== "undefined") {
15517
17460
  if (handler.addressStackLength === 0) {
15518
17461
  raiseError(`No active state reference to get list index for "${prop.toString()}".`);
@@ -15602,7 +17545,12 @@ function get(target, prop, receiver, handler) {
15602
17545
  return undefined;
15603
17546
  }
15604
17547
  }
15605
- const resolvedAddress = getResolvedAddress(prop);
17548
+ // オーサリング層の `**` を、いま評価している再帰 getter の深さへ束縛する。
17549
+ // 宣言の無い state は boolean 判定 1 個で抜ける(D18 の形)。
17550
+ const path = (handler.stateElement?.hasRecursion === true && hasRecursionWildcard(prop))
17551
+ ? bindRecursivePath(handler.stateElement, handler, prop)
17552
+ : prop;
17553
+ const resolvedAddress = getResolvedAddress(path);
15606
17554
  const listIndex = getListIndex(target, resolvedAddress, receiver, handler);
15607
17555
  const stateAddress = createStateAddress(resolvedAddress.pathInfo, listIndex);
15608
17556
  return getByAddress(target, stateAddress, receiver, handler);
@@ -15657,6 +17605,12 @@ function get(target, prop, receiver, handler) {
15657
17605
  };
15658
17606
  break;
15659
17607
  }
17608
+ case errorCallbackSymbol: {
17609
+ api = (error, info) => {
17610
+ return errorCallback(target, error, info, receiver);
17611
+ };
17612
+ break;
17613
+ }
15660
17614
  default: {
15661
17615
  return Reflect.get(target, prop, receiver);
15662
17616
  }
@@ -15739,8 +17693,18 @@ class StateHandler {
15739
17693
  // getter の相互参照(`get a(){return this.b}` / `get b(){return this.a}`)は
15740
17694
  // 実際にこれを踏み、原因と無関係な文面だけが残っていた。
15741
17695
  if (this._addressStackIndex + 1 >= MAX_LOOP_DEPTH) {
15742
- raiseError(`Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH}. ` +
15743
- `Possible circular dependency between path getters: ${this._describeAddressCycle()}`);
17696
+ // 深さ超過と循環は別の原因で、助言も違う。末尾に同じパスが再登場していれば
17697
+ // getter どうしが呼び合っている(循環)、全部別パスなら単に深すぎる(正当に
17698
+ // 深いツリーの集計など)。両方を「循環の可能性」と告発していたため、循環の無い
17699
+ // 直線の木でも「相互参照を直せ」と読める文面が出ていた
17700
+ // (docs/state-recursive-path-impl-plan.md §3-2 の E2)。
17701
+ if (this._hasRepeatedAddress()) {
17702
+ raiseError(`[wcs/getter-cycle] Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH}. ` +
17703
+ `Possible circular dependency between path getters: ${this._describeAddressCycle()}`);
17704
+ }
17705
+ raiseError(`[wcs/getter-depth-exceeded] Exceeded maximum address stack depth of ${MAX_LOOP_DEPTH} ` +
17706
+ `with no address visited twice — the data is simply nested deeper than the engine evaluates ` +
17707
+ `in one pass. Deepest path first: ${this._describeAddressCycle()}`);
15744
17708
  }
15745
17709
  this._addressStackIndex++;
15746
17710
  this._addressStack[this._addressStackIndex] = address;
@@ -15749,7 +17713,35 @@ class StateHandler {
15749
17713
  * スタック末尾の繰り返し区間をパス名で示す(循環の当事者だけを見せる)。
15750
17714
  * 上限に達したときのみ呼ばれるので、コストは異常系に閉じている。
15751
17715
  */
15752
- _describeAddressCycle() {
17716
+ /**
17717
+ * スタック全体(最大 MAX_LOOP_DEPTH 段)に**同じアドレスが再登場する**か。
17718
+ *
17719
+ * 循環と深さ超過を分ける述語。パス文字列ではなくアドレスの同一性で見るのは、
17720
+ * どちらの側にも文字列では判別できない形があるため:
17721
+ * - 末尾 N 段のパス重複だけを見ると、周期が N より長い getter の輪を取り逃がす
17722
+ * (そして「重複が無い=ただ深いだけ」と**積極的に誤った断定**をしてしまう)
17723
+ * - 逆に「同じパスを別の行で読む」正当な再帰(隣接項目参照・累積 getter)は
17724
+ * パス文字列が全段同じなので、文字列で見ると循環に誤告発される
17725
+ *
17726
+ * IStateAddress は (pathInfo, listIndex) で intern されているので、真の輪だけが
17727
+ * 同じインスタンスに戻る。コストは異常系に閉じた O(MAX_LOOP_DEPTH) の Set 構築 1 回。
17728
+ */
17729
+ _hasRepeatedAddress() {
17730
+ const seen = new Set();
17731
+ for (let i = 0; i <= this._addressStackIndex; i++) {
17732
+ const entry = this._addressStack[i];
17733
+ if (!entry) {
17734
+ continue;
17735
+ }
17736
+ if (seen.has(entry)) {
17737
+ return true;
17738
+ }
17739
+ seen.add(entry);
17740
+ }
17741
+ return false;
17742
+ }
17743
+ /** スタック末尾の CYCLE_REPORT_DEPTH 段のパス(深い順)。診断の表示に使う。 */
17744
+ _tailAddressPaths() {
15753
17745
  const paths = [];
15754
17746
  for (let i = this._addressStackIndex; i >= 0 && paths.length < CYCLE_REPORT_DEPTH; i--) {
15755
17747
  const entry = this._addressStack[i];
@@ -15757,6 +17749,10 @@ class StateHandler {
15757
17749
  paths.push(entry.pathInfo.path);
15758
17750
  }
15759
17751
  }
17752
+ return paths;
17753
+ }
17754
+ _describeAddressCycle() {
17755
+ const paths = this._tailAddressPaths();
15760
17756
  const unique = Array.from(new Set(paths));
15761
17757
  return `${unique.reverse().join(" -> ")} -> ...`;
15762
17758
  }
@@ -15975,6 +17971,11 @@ function initializeMountScope(record, scopeRoot) {
15975
17971
  setStateElementAlias(scopeRoot, record.parentStateElement);
15976
17972
  }
15977
17973
  buildMountScopeBindings(record, scopeRoot);
17974
+ // Register exports and alias edges once. Notify parents that evaluated before
17975
+ // registration, including on reinitialization when values may have changed.
17976
+ registerExports(record);
17977
+ warnShadowedExports(record);
17978
+ notifyExports(record);
15978
17979
  setBindingsReadyForScope(scopeRoot, Promise.resolve());
15979
17980
  }
15980
17981
  function buildMountScopeBindings(record, walkRoot) {
@@ -16008,6 +18009,8 @@ function remountScopeBindings(record, scopeRoot) {
16008
18009
  const rebound = session.rebindAddresses();
16009
18010
  // 空でも呼んで良い(ループが回らないだけ)— 分岐を持たない
16010
18011
  applyChangeFromBindings(rebound);
18012
+ // 別の行に付け替わった = その行の公開パスの答えが変わった(X6)
18013
+ notifyExports(record);
16011
18014
  }
16012
18015
 
16013
18016
  /**
@@ -16217,6 +18220,20 @@ function validateVolumeDeclarations(rootStateElement, mountPath, volumeState) {
16217
18220
  if (typeof volumeState["$streams"] !== "undefined") {
16218
18221
  raiseError(`Volume "${mountPath}" declares $streams, which volumes do not support yet. Declare the stream on the root state.`);
16219
18222
  }
18223
+ // $recursion も同じく未対応。宣言だけ受理されたように見えて、どの深さも解決しない
18224
+ // 状態を作らない(docs/state-recursive-path-impl-plan.md §7)。
18225
+ if (typeof volumeState[STATE_RECURSION_NAME] !== "undefined") {
18226
+ raiseError(`Volume "${mountPath}" declares ${STATE_RECURSION_NAME}, which volumes do not support yet. ` +
18227
+ `Declare the recursion anchor on the root state — the anchor path is resolved against the root tree.`);
18228
+ }
18229
+ // `**` getter は接ぎ木の**途中で**落ちる(アクセサ登録が getPathInfo の不変条件ガードに
18230
+ // 当たる)ので、データだけ載ってアクセサが無い半端な状態が残る。接ぎ木の前に弾く。
18231
+ for (const key of Object.keys(getAllPropertyDescriptors(volumeState))) {
18232
+ if (key.indexOf(RECURSION_WILDCARD) !== -1) {
18233
+ raiseError(`Volume "${mountPath}" declares "${key}", which uses "${RECURSION_WILDCARD}". ` +
18234
+ `Volumes do not support recursive getters yet — declare them on the root state.`);
18235
+ }
18236
+ }
16220
18237
  }
16221
18238
  /**
16222
18239
  * 宣言面の接頭辞登録($watch / $listKeys / $updatedCallback — ヘッダ参照)。
@@ -16448,6 +18465,8 @@ class State extends HTMLElementBase {
16448
18465
  }
16449
18466
  __state;
16450
18467
  _hasUpdatedCallback = false;
18468
+ /** $errorCallback の有無(_hasUpdatedCallback と同じく state セット時に確定。ルートのみ) */
18469
+ _hasErrorCallback = false;
16451
18470
  /** enable-ssr のスナップショットから初期化された(D14: ボリュームはデータを採用する) */
16452
18471
  _hydratedFromSsr = false;
16453
18472
  // 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
@@ -16468,6 +18487,7 @@ class State extends HTMLElementBase {
16468
18487
  _resolveSetState = null;
16469
18488
  _listPaths = new Set();
16470
18489
  _listKeys = null;
18490
+ _recursionRegistry = null;
16471
18491
  _elementPaths = new Set();
16472
18492
  _getterPaths = new Set();
16473
18493
  _setterPaths = new Set();
@@ -16527,8 +18547,28 @@ class State extends HTMLElementBase {
16527
18547
  return this.__state;
16528
18548
  }
16529
18549
  set _state(value) {
16530
- this._commandTokenNames = processCommandTokensDeclaration(value);
16531
- this._eventTokenNames = processEventTokensDeclaration(value);
18550
+ // 旧世代のデータ。再帰の生成物(辺・キャッシュ)を忘れるとき、台帳を辿る起点になる
18551
+ const previousState = this.__state;
18552
+ // 順序: **純検証をすべて** → 旧世代の後始末 → 差し替え → 再収集。
18553
+ // `value` しか読まない検証($recursion の宣言とレジストリの構築・$commandTokens・$eventTokens)は
18554
+ // 何かを書き換える前に全部済ませる。どれかが throw すれば要素は丸ごと旧世代に留まる
18555
+ // (旧 state・旧レジストリ・旧世代の辺とキャッシュ・トークン名がそのまま)。後始末を先に
18556
+ // すると「レジストリは新・own 生成アクセサと辺は消えた・`__state` は旧」という半端な状態で
18557
+ // throw する(第 4 サイクルの再検証で実測 — 別アンカー+不正な $commandTokens で旧世代の
18558
+ // 集計が無言で消えた)。
18559
+ const recursionSpec = processRecursionDeclaration(value);
18560
+ const recursionRegistry = recursionSpec === null ? null : new RecursionRegistry(recursionSpec, value);
18561
+ const commandTokenNames = processCommandTokensDeclaration(value);
18562
+ const eventTokenNames = processEventTokensDeclaration(value);
18563
+ // 旧世代の生成アクセサ(own)・それを指す依存辺・評価結果のキャッシュを忘れてから
18564
+ // 差し替える(recursion/generation.ts)。own の生成アクセサは、同じオブジェクトを再セットする
18565
+ // ときに下の `getStateInfo` が `getterPaths` へ拾い直す前に消えていなければならない。
18566
+ if (this._recursionRegistry !== null) {
18567
+ this._recursionRegistry.forgetGenerated(this, previousState);
18568
+ }
18569
+ this._recursionRegistry = recursionRegistry;
18570
+ this._commandTokenNames = commandTokenNames;
18571
+ this._eventTokenNames = eventTokenNames;
16532
18572
  this.__state = value;
16533
18573
  // $updatedCallback の有無を state セット時に確定しておく(in はプロトタイプ
16534
18574
  // チェーンも見る・getter を評価しない)。drain 側はこのフラグで更新アドレスの
@@ -16537,6 +18577,7 @@ class State extends HTMLElementBase {
16537
18577
  // パターンは検知できない(bindProperty / _state 再セットは検知する)。
16538
18578
  // ライフサイクルフックは宣言時に定義するのが規約。
16539
18579
  this._hasUpdatedCallback = STATE_UPDATED_CALLBACK_NAME in value;
18580
+ this._hasErrorCallback = STATE_ERROR_CALLBACK_NAME in value;
16540
18581
  // 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
16541
18582
  clearEventTokenRegistry(this);
16542
18583
  processOnDeclaration(this, value, this._eventTokenNames);
@@ -16564,6 +18605,22 @@ class State extends HTMLElementBase {
16564
18605
  // $listKeys: 宣言が無ければ null のままで、setByAddress のキー突合経路には
16565
18606
  // 一切入らない(docs/state-list-key-design.md §7-1)。再 set で必ず置き換える。
16566
18607
  this._listKeys = processListKeysDeclaration(value);
18608
+ // $recursion: 宣言が無ければ null のままで、読みのホットパスには一切入らない。
18609
+ // レジストリの構築・旧世代の後始末・差し替えはセッタの先頭で済んでいる(`__state` の
18610
+ // 差し替え前)。ここに残るのはリストパスの登録だけ(`_listPaths.clear()` の後であること)。
18611
+ if (recursionSpec !== null) {
18612
+ // アンカーのリストパス(`nodes.*` なら `nodes`)は**宣言から静的に分かる**ので、
18613
+ // 展開を待たずに今すぐ登録する。
18614
+ //
18615
+ // これが無いと、再帰パスを一度読んだ後の再セットで構造書き込みが恒久的に落ちる。
18616
+ // 生成アクセサの `setPathInfo` が張った静的辺(`nodes` → `nodes.*`)は依存グラフに
18617
+ // 残るのに、`_listPaths` はこのセッタでクリアされ、次に再帰パスを読むまで
18618
+ // 張り直されない。その隙間に構造書き込みが来ると `walkDependency` が
18619
+ // 「リストではないパス」として `nodes.*` に到達し、listIndex を持たないアドレスで
18620
+ // `Cannot expand dynamic dependency…` になる(値は書かれるので、データと表示が
18621
+ // 乖離したまま自己回復しない)。
18622
+ this._listPaths.add(recursionSpec.anchorList);
18623
+ }
16567
18624
  // $watch: 旧宣言のハンドラが残らないよう registry を落としてから新宣言を解析する。
16568
18625
  // _pathSet.clear() の後であること(依存グラフ登録をやり直す必要がある、
16569
18626
  // docs/state-watch-hook-design.md §8)。宣言が無ければ watchPaths は null で、
@@ -17136,6 +19193,9 @@ class State extends HTMLElementBase {
17136
19193
  // 台帳エイリアスは消さない(プール再利用の再接続が同じスコープに戻る)。
17137
19194
  // $disconnectedCallback だけは要素のライフサイクルとして呼ぶ(例外は隔離)
17138
19195
  callMountLifecycleCallback(this._mountRecord, "$disconnectedCallback");
19196
+ // 公開 getter の答えが消えた(X6)— 親の依存者を再評価させる。プール返却も
19197
+ // 恒久破棄もここを通る(行ごと消えた形は $postUpdate が届かず無視される)
19198
+ notifyExports(this._mountRecord);
17139
19199
  this._rootNode = null;
17140
19200
  return;
17141
19201
  }
@@ -17189,6 +19249,12 @@ class State extends HTMLElementBase {
17189
19249
  get listKeys() {
17190
19250
  return this._listKeys;
17191
19251
  }
19252
+ get hasRecursion() {
19253
+ return this._recursionRegistry !== null;
19254
+ }
19255
+ get recursionRegistry() {
19256
+ return this._recursionRegistry;
19257
+ }
17192
19258
  get watchPaths() {
17193
19259
  return this._watchPaths;
17194
19260
  }
@@ -17234,6 +19300,15 @@ class State extends HTMLElementBase {
17234
19300
  get hydratedFromSsr() {
17235
19301
  return this._hydratedFromSsr;
17236
19302
  }
19303
+ addListPath(path) {
19304
+ this._listPaths.add(path);
19305
+ }
19306
+ findStateDescriptor(path) {
19307
+ // own → プロトタイプチェーン(Object.prototype 手前まで)。打ち切り位置は
19308
+ // getAllPropertyDescriptors / getStateInfo と同じ = 「state が宣言したもの」の範囲。
19309
+ // 走査そのものは pathDiagnostics と共有する(2 本に分かれると打ち切り位置がずれる)。
19310
+ return findDescriptor(this._state, path);
19311
+ }
17237
19312
  defineTreeAccessor(path, descriptor) {
17238
19313
  Object.defineProperty(this._state, path, descriptor);
17239
19314
  if (typeof descriptor.get === "function") {
@@ -17384,6 +19459,9 @@ class State extends HTMLElementBase {
17384
19459
  get hasUpdatedCallback() {
17385
19460
  return this._hasUpdatedCallback;
17386
19461
  }
19462
+ get hasErrorCallback() {
19463
+ return this._hasErrorCallback;
19464
+ }
17387
19465
  get crossRowListPaths() {
17388
19466
  return this._crossRowListPaths;
17389
19467
  }
@@ -17743,6 +19821,7 @@ function getWcsManifest() {
17743
19821
  STATE_CONNECTED_CALLBACK_NAME,
17744
19822
  STATE_DISCONNECTED_CALLBACK_NAME,
17745
19823
  STATE_UPDATED_CALLBACK_NAME,
19824
+ STATE_ERROR_CALLBACK_NAME,
17746
19825
  WEBCOMPONENT_STATE_READY_CALLBACK_NAME,
17747
19826
  ],
17748
19827
  reservedStateApi: [
@@ -17755,6 +19834,7 @@ function getWcsManifest() {
17755
19834
  STATE_STREAMS_NAME,
17756
19835
  STATE_WATCH_NAME,
17757
19836
  STATE_LIST_KEYS_NAME,
19837
+ STATE_RECURSION_NAME,
17758
19838
  STATE_STREAM_STATUS_NAMESPACE_NAME,
17759
19839
  STATE_STREAM_ERROR_NAMESPACE_NAME,
17760
19840
  ],
@@ -17889,5 +19969,5 @@ function resolveLiveDeclaration(tag) {
17889
19969
  return { propertyEvents, inputs, commands };
17890
19970
  }
17891
19971
 
17892
- export { Ssr, VERSION, WCS_MANIFEST_VERSION, analyzeContract, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getWcsManifest };
19972
+ export { Ssr, TRUSTED_TYPES_POLICY_SLOT, VERSION, WCS_MANIFEST_VERSION, analyzeContract, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getTrustedTypesPolicy, getWcsManifest, setTrustedTypesPolicy };
17893
19973
  //# sourceMappingURL=index.esm.js.map