@wcstack/state 1.27.0 → 1.28.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.d.ts CHANGED
@@ -226,7 +226,12 @@ interface IFilterInfo {
226
226
  readonly args: string[];
227
227
  readonly filterFn: FilterFn;
228
228
  }
229
- interface IBindingInfo {
229
+ /**
230
+ * バインディング式のパース結果(DOM 非依存の部分)。`@wcstack/state/parser` の
231
+ * ParseBindTextResult がこれをそのまま公開するため、Node 等の DOM lib 型を
232
+ * ここに足してはならない(足すなら IBindingInfo 側へ)。
233
+ */
234
+ interface IParsedBinding {
230
235
  readonly propName: string;
231
236
  readonly propSegments: string[];
232
237
  readonly propModifiers: string[];
@@ -235,11 +240,13 @@ interface IBindingInfo {
235
240
  readonly stateName: string;
236
241
  readonly inFilters: IFilterInfo[];
237
242
  readonly outFilters: IFilterInfo[];
238
- readonly node: Node;
239
- readonly replaceNode: Node;
240
243
  readonly bindingType: BindingType;
241
244
  readonly uuid?: string | null;
242
245
  }
246
+ interface IBindingInfo extends IParsedBinding {
247
+ readonly node: Node;
248
+ readonly replaceNode: Node;
249
+ }
243
250
 
244
251
  interface IState {
245
252
  [key: string]: any;
@@ -691,6 +698,41 @@ interface IWcsManifest {
691
698
  };
692
699
  /** 構造ディレクティブ(`<template data-wcs="for: ...">` 等) */
693
700
  structuralDirectives: readonly string[];
701
+ /**
702
+ * 修飾子(`#` 後)の語彙。flags は値を取らない形(`#prevent`)、keyValue は
703
+ * `=` で値を取る形(`#init=element`)、eventNamePrefix は `on` + イベント名の形
704
+ * (`#onchange` — two-way / radio / checkbox のイベント名上書き。README「Modifiers」)。
705
+ * define.ts の定数が単一正本で、ランタイムの消費箇所も同じ定数に分岐する。
706
+ */
707
+ modifiers: {
708
+ flags: readonly string[];
709
+ keyValue: readonly string[];
710
+ eventNamePrefix: string;
711
+ };
712
+ /** リストインデックス参照名(`$1`..`$N`)。prefix + 1 始まり連番、maxDepth まで。 */
713
+ indexParam: {
714
+ prefix: string;
715
+ maxDepth: number;
716
+ };
717
+ /**
718
+ * bindingType 判別の語彙(parseBindTextsForElement の分岐と同一の定数から導出)。
719
+ * 判別順: else → spread → 構造ディレクティブ/radio/checkbox → eventToken・`on*`
720
+ * (event)→ prop。propNamespaces は左辺先頭セグメントの特殊 namespace で、
721
+ * apply 層のディスパッチキー集合との一致はテストが強制する。
722
+ * 既知の未収載: `radio` / `checkbox`(BindingType union のみが正本)。
723
+ */
724
+ bindingTypes: {
725
+ elseKeyword: string;
726
+ spread: string;
727
+ eventPropertyPrefix: string;
728
+ propNamespaces: {
729
+ eventToken: string;
730
+ command: string;
731
+ class: string;
732
+ attr: string;
733
+ style: string;
734
+ };
735
+ };
694
736
  };
695
737
  /** 組み込みフィルタ名(builtinFilters から自動導出=実装が正本) */
696
738
  filters: string[];
package/dist/index.esm.js CHANGED
@@ -113,6 +113,38 @@ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
113
113
  const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
114
114
  const STATE_NAME_SEPARATOR = '@'; // path と @stateName の区切り
115
115
  const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
116
+ // 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
117
+ // フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
118
+ // 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
119
+ // この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
120
+ // (docs/static-wiring-dx-design.md §2-2)。
121
+ const MODIFIER_PREVENT = 'prevent';
122
+ const MODIFIER_STOP = 'stop';
123
+ const MODIFIER_READONLY = 'ro';
124
+ const MODIFIER_FLAGS = Object.freeze([
125
+ MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
126
+ ]);
127
+ const MODIFIER_KEY_INIT = 'init';
128
+ const MODIFIER_KEY_SYNC = 'sync';
129
+ const MODIFIER_KEYS = Object.freeze([
130
+ MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
131
+ ]);
132
+ // bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
133
+ // 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
134
+ // apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
135
+ // キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
136
+ // manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
137
+ const ELSE_KEYWORD = 'else';
138
+ const SPREAD_PROP = '...';
139
+ const EVENT_PROP_PREFIX = 'on';
140
+ const EVENT_TOKEN_NAMESPACE = 'eventToken';
141
+ const COMMAND_NAMESPACE = 'command';
142
+ const CLASS_NAMESPACE = 'class';
143
+ const ATTR_NAMESPACE = 'attr';
144
+ const STYLE_NAMESPACE = 'style';
145
+ // リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
146
+ // manifest.syntax.indexParam で公開される。
147
+ const INDEX_PARAM_PREFIX = '$';
116
148
  /**
117
149
  * stackIndexByIndexName
118
150
  * インデックス名からスタックインデックスへのマッピング
@@ -124,7 +156,7 @@ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
124
156
  */
125
157
  const tmpIndexByIndexName = {};
126
158
  for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
127
- tmpIndexByIndexName[`$${i + 1}`] = i;
159
+ tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
128
160
  }
129
161
  const INDEX_BY_INDEX_NAME = Object.freeze(tmpIndexByIndexName);
130
162
  const NO_SET_TIMEOUT = 60 * 1000; // 1分
@@ -640,6 +672,75 @@ const STRUCTURAL_BINDING_TYPE_SET = new Set([
640
672
  "for",
641
673
  ]);
642
674
 
675
+ /**
676
+ * errorGuidance.ts — エラーメッセージへの self-fix 誘導(GTM 2-5 /
677
+ * docs/static-wiring-dx-design.md §3)。
678
+ *
679
+ * コンソールは「書き手(人間・AI とも)が誤った瞬間に必ず読む面」なので、
680
+ * (a) did-you-mean 候補 (b) lint への誘導 をエラーメッセージ自体に埋め込む。
681
+ * ここの関数は全て**エラーパスでのみ**呼ばれる — 正常系のコストはゼロ。
682
+ * auto.min.js に同梱されるため文字列は最小限に保つ(エラーパス専用モジュールの
683
+ * 遅延 import は `src/auto.ts` の SRI 自己完結制約で不可)。
684
+ *
685
+ * 診断 code の語彙はコンソール → lint → IDE の三面で共有する:
686
+ * メッセージ先頭の `[wcs/...]` は wcstack-intellisense / @wcstack/lint の
687
+ * 安定診断 code(packages/vscode-wcs/src/core/diagnostics.ts)と同一。
688
+ */
689
+ /** 挿入・削除・置換の編集距離。長さ差が max を超えたら早期に max+1 を返す。 */
690
+ function editDistance(a, b, max) {
691
+ if (Math.abs(a.length - b.length) > max) {
692
+ return max + 1;
693
+ }
694
+ const prev = new Array(b.length + 1);
695
+ const curr = new Array(b.length + 1);
696
+ for (let j = 0; j <= b.length; j++) {
697
+ prev[j] = j;
698
+ }
699
+ for (let i = 1; i <= a.length; i++) {
700
+ curr[0] = i;
701
+ for (let j = 1; j <= b.length; j++) {
702
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
703
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
704
+ }
705
+ for (let j = 0; j <= b.length; j++) {
706
+ prev[j] = curr[j];
707
+ }
708
+ }
709
+ return prev[b.length];
710
+ }
711
+ /**
712
+ * 候補集合から編集距離 2 以内の最近傍を探し、` Did you mean "<best>"?` を返す。
713
+ * 該当なしは空文字。規準(距離 2・同距離は先勝ち・大小文字は畳んで比較)は
714
+ * lint の did-you-mean(ioNodeValidator の suggestion)と同じ — 三面で提案が
715
+ * 割れないように揃えている。動的キー等で候補が列挙できないサイトでは呼ばない
716
+ * = 誘導文のみに縮退(設計 §3 の縮退)。
717
+ */
718
+ function didYouMean(input, candidates) {
719
+ // 空入力(`a|` の末尾パイプ等)に短い候補を提案しても無意味なので出さない。
720
+ if (input.length === 0) {
721
+ return "";
722
+ }
723
+ const folded = input.toLowerCase();
724
+ let best = null;
725
+ let bestDistance = 3;
726
+ for (const candidate of candidates) {
727
+ const distance = editDistance(folded, candidate.toLowerCase(), 2);
728
+ if (distance < bestDistance) {
729
+ best = candidate;
730
+ bestDistance = distance;
731
+ }
732
+ }
733
+ return best !== null ? ` Did you mean "${best}"?` : "";
734
+ }
735
+ /**
736
+ * lint への誘導(誘導付きメッセージ共通の一文)。
737
+ * **lint が実際にそのケースを検出するサイトにだけ付ける** — 検出しないケースに
738
+ * 付けると「エラー → lint 実行 → clean」の空振りで検証ループの信頼を毀損する
739
+ * (DCC 宣言・watch の一部 shape・構造型単独バインディング違反は lint 未検出のため
740
+ * 付けない。lint 側への検査追加は follow-up)。
741
+ */
742
+ const LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
743
+
643
744
  /**
644
745
  * errorMessages.ts
645
746
  *
@@ -1558,7 +1659,8 @@ const builtinFiltersByFilterIOType = {
1558
1659
  const builtinFilterFn = (name, options) => (filters) => {
1559
1660
  const filter = filters[name];
1560
1661
  if (!filter) {
1561
- raiseError(`filter not found: ${name}`);
1662
+ // lint の wcs/filter-unknown と同じ語彙・同じ did-you-mean 規準(三面同語彙)。
1663
+ raiseError(`[wcs/filter-unknown] filter not found: ${name}.${didYouMean(name, Object.keys(filters))}${LINT_HINT}`);
1562
1664
  }
1563
1665
  return filter(options);
1564
1666
  };
@@ -1779,11 +1881,11 @@ function parseBindTextsForElement(bindText) {
1779
1881
  }
1780
1882
  const propPart = bindText.slice(0, separatorIndex).trim();
1781
1883
  const statePart = bindText.slice(separatorIndex + 1).trim();
1782
- if (propPart === 'else') {
1884
+ if (propPart === ELSE_KEYWORD) {
1783
1885
  const pathInfo = getPathInfo('#else');
1784
1886
  return {
1785
- propName: 'else',
1786
- propSegments: ['else'],
1887
+ propName: ELSE_KEYWORD,
1888
+ propSegments: [ELSE_KEYWORD],
1787
1889
  propModifiers: [],
1788
1890
  statePathName: '#else',
1789
1891
  statePathInfo: pathInfo,
@@ -1793,7 +1895,7 @@ function parseBindTextsForElement(bindText) {
1793
1895
  bindingType: 'else',
1794
1896
  };
1795
1897
  }
1796
- else if (propPart === '...') {
1898
+ else if (propPart === SPREAD_PROP) {
1797
1899
  const stateResult = parseStatePart(statePart);
1798
1900
  if (stateResult.outFilters.length > 0) {
1799
1901
  raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets.`);
@@ -1802,8 +1904,8 @@ function parseBindTextsForElement(bindText) {
1802
1904
  raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
1803
1905
  }
1804
1906
  return {
1805
- propName: '...',
1806
- propSegments: ['...'],
1907
+ propName: SPREAD_PROP,
1908
+ propSegments: [SPREAD_PROP],
1807
1909
  propModifiers: [],
1808
1910
  inFilters: [],
1809
1911
  ...stateResult,
@@ -1830,14 +1932,14 @@ function parseBindTextsForElement(bindText) {
1830
1932
  const propResult = parsePropPart(propPart);
1831
1933
  // eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
1832
1934
  // 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
1833
- if (propResult.propSegments[0] === 'eventToken') {
1935
+ if (propResult.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
1834
1936
  return {
1835
1937
  ...propResult,
1836
1938
  ...stateResult,
1837
1939
  bindingType: 'event',
1838
1940
  };
1839
1941
  }
1840
- if (propResult.propSegments[0].startsWith('on')) {
1942
+ if (propResult.propSegments[0].startsWith(EVENT_PROP_PREFIX)) {
1841
1943
  return {
1842
1944
  ...propResult,
1843
1945
  ...stateResult,
@@ -1857,7 +1959,9 @@ function parseBindTextsForElement(bindText) {
1857
1959
  if (results.length > 1) {
1858
1960
  const isIncludeSingleBinding = results.some(r => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
1859
1961
  if (isIncludeSingleBinding) {
1860
- raiseError(`Invalid bindText: "${bindText}". 'if', 'elseif', 'else', and 'for' bindings must be single binding.`);
1962
+ // LINT_HINT は付けない: 単独バインディング検査は lint 側に未実装で、誘導が
1963
+ // 空振りする(lint への検査追加は follow-up)。
1964
+ raiseError(`[wcs/template-syntax] Invalid bindText: "${bindText}". 'if', 'elseif', 'else', and 'for' bindings must be single binding. Put the structural binding alone in its own data-wcs (e.g. <template data-wcs="for: items">).`);
1861
1965
  }
1862
1966
  }
1863
1967
  return results;
@@ -2733,8 +2837,8 @@ function getHandlerKey$3(binding, eventName) {
2733
2837
  function getEventName$2(binding) {
2734
2838
  let eventName = 'input';
2735
2839
  for (const modifier of binding.propModifiers) {
2736
- if (modifier.startsWith('on')) {
2737
- eventName = modifier.slice(2);
2840
+ if (modifier.startsWith(EVENT_PROP_PREFIX)) {
2841
+ eventName = modifier.slice(EVENT_PROP_PREFIX.length);
2738
2842
  }
2739
2843
  }
2740
2844
  return eventName;
@@ -2789,7 +2893,7 @@ const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (e
2789
2893
  });
2790
2894
  };
2791
2895
  function attachCheckboxEventHandler(binding) {
2792
- if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf('ro') === -1) {
2896
+ if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
2793
2897
  const eventName = getEventName$2(binding);
2794
2898
  const key = getHandlerKey$3(binding, eventName);
2795
2899
  let checkboxEventHandler = handlerByHandlerKey$3.get(key);
@@ -2804,7 +2908,7 @@ function attachCheckboxEventHandler(binding) {
2804
2908
  return false;
2805
2909
  }
2806
2910
  function detachCheckboxEventHandler(binding) {
2807
- if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf('ro') === -1) {
2911
+ if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
2808
2912
  const eventName = getEventName$2(binding);
2809
2913
  const key = getHandlerKey$3(binding, eventName);
2810
2914
  const checkboxEventHandler = handlerByHandlerKey$3.get(key);
@@ -2987,7 +3091,7 @@ function getWcBindable$1(element) {
2987
3091
  return readBindableDeclaration(element);
2988
3092
  }
2989
3093
  function attachEventTokenHandler(binding) {
2990
- if (binding.propSegments[0] !== "eventToken") {
3094
+ if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
2991
3095
  return false;
2992
3096
  }
2993
3097
  const element = binding.node;
@@ -3014,16 +3118,16 @@ function attachEventTokenHandler(binding) {
3014
3118
  }
3015
3119
  const propDesc = bindable.knownProperties.get(propertyName);
3016
3120
  if (typeof propDesc === "undefined") {
3017
- raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.`);
3121
+ raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.${didYouMean(propertyName, bindable.knownProperties.keys())}`);
3018
3122
  }
3019
3123
  const eventName = propDesc.event;
3020
3124
  const tokenName = binding.statePathName;
3021
3125
  const stateName = binding.stateName;
3022
3126
  const modifiers = binding.propModifiers;
3023
3127
  const handler = (event) => {
3024
- if (modifiers.includes("prevent"))
3128
+ if (modifiers.includes(MODIFIER_PREVENT))
3025
3129
  event.preventDefault();
3026
- if (modifiers.includes("stop"))
3130
+ if (modifiers.includes(MODIFIER_STOP))
3027
3131
  event.stopPropagation();
3028
3132
  // state は発火時の live root から解決する(attach 時は detached の可能性があるため)。
3029
3133
  const rootNode = element.getRootNode();
@@ -3032,7 +3136,8 @@ function attachEventTokenHandler(binding) {
3032
3136
  raiseError(`State element with name "${stateName}" not found for eventToken handler.`);
3033
3137
  }
3034
3138
  if (!stateElement.eventTokenNames.has(tokenName)) {
3035
- raiseError(`eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".`);
3139
+ // lint も同じケースを wcs/token-undeclared で検出する(三面同語彙)。
3140
+ raiseError(`[wcs/token-undeclared] eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".${didYouMean(tokenName, stateElement.eventTokenNames)}${LINT_HINT}`);
3036
3141
  }
3037
3142
  const loopContext = getLoopContextByNode(element);
3038
3143
  stateElement.createStateAsync("writable", async (state) => {
@@ -3052,7 +3157,7 @@ function attachEventTokenHandler(binding) {
3052
3157
  return true;
3053
3158
  }
3054
3159
  function detachEventTokenHandler(binding) {
3055
- if (binding.propSegments[0] !== "eventToken") {
3160
+ if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
3056
3161
  return false;
3057
3162
  }
3058
3163
  const listener = listenerByBinding.get(binding);
@@ -3105,13 +3210,13 @@ const handlerByHandlerKey$2 = new Map();
3105
3210
  // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
3106
3211
  const bindingRegistry$2 = createHandlerBindingRegistry();
3107
3212
  function getHandlerKey$2(binding) {
3108
- const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
3213
+ const modifierKey = binding.propModifiers.filter(m => m === MODIFIER_PREVENT || m === MODIFIER_STOP).sort().join(',');
3109
3214
  return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
3110
3215
  }
3111
3216
  const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
3112
- if (modifiers.includes('prevent'))
3217
+ if (modifiers.includes(MODIFIER_PREVENT))
3113
3218
  event.preventDefault();
3114
- if (modifiers.includes('stop'))
3219
+ if (modifiers.includes(MODIFIER_STOP))
3115
3220
  event.stopPropagation();
3116
3221
  const node = event.target;
3117
3222
  const rootNode = node.getRootNode();
@@ -3145,7 +3250,7 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
3145
3250
  });
3146
3251
  };
3147
3252
  function attachEventHandler(binding) {
3148
- if (!binding.propName.startsWith("on")) {
3253
+ if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
3149
3254
  return false;
3150
3255
  }
3151
3256
  const key = getHandlerKey$2(binding);
@@ -3160,7 +3265,7 @@ function attachEventHandler(binding) {
3160
3265
  return true;
3161
3266
  }
3162
3267
  function detachEventHandler(binding) {
3163
- if (!binding.propName.startsWith("on")) {
3268
+ if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
3164
3269
  return false;
3165
3270
  }
3166
3271
  const key = getHandlerKey$2(binding);
@@ -3189,8 +3294,8 @@ function getHandlerKey$1(binding, eventName) {
3189
3294
  function getEventName$1(binding) {
3190
3295
  let eventName = 'input';
3191
3296
  for (const modifier of binding.propModifiers) {
3192
- if (modifier.startsWith('on')) {
3193
- eventName = modifier.slice(2);
3297
+ if (modifier.startsWith(EVENT_PROP_PREFIX)) {
3298
+ eventName = modifier.slice(EVENT_PROP_PREFIX.length);
3194
3299
  }
3195
3300
  }
3196
3301
  return eventName;
@@ -3226,7 +3331,7 @@ const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (even
3226
3331
  });
3227
3332
  };
3228
3333
  function attachRadioEventHandler(binding) {
3229
- if (binding.bindingType === "radio" && binding.propModifiers.indexOf('ro') === -1) {
3334
+ if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
3230
3335
  const eventName = getEventName$1(binding);
3231
3336
  const key = getHandlerKey$1(binding, eventName);
3232
3337
  let radioEventHandler = handlerByHandlerKey$1.get(key);
@@ -3241,7 +3346,7 @@ function attachRadioEventHandler(binding) {
3241
3346
  return false;
3242
3347
  }
3243
3348
  function detachRadioEventHandler(binding) {
3244
- if (binding.bindingType === "radio" && binding.propModifiers.indexOf('ro') === -1) {
3349
+ if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
3245
3350
  const eventName = getEventName$1(binding);
3246
3351
  const key = getHandlerKey$1(binding, eventName);
3247
3352
  const radioEventHandler = handlerByHandlerKey$1.get(key);
@@ -3480,10 +3585,10 @@ function getEventName(binding) {
3480
3585
  eventName = propDesc.event;
3481
3586
  }
3482
3587
  }
3483
- // 3.modifier
3588
+ // 3.modifier(`#onchange` 等 — `on` + イベント名の修飾子形。README「Modifiers」参照)
3484
3589
  for (const modifier of binding.propModifiers) {
3485
- if (modifier.startsWith('on')) {
3486
- eventName = modifier.slice(2);
3590
+ if (modifier.startsWith(EVENT_PROP_PREFIX)) {
3591
+ eventName = modifier.slice(EVENT_PROP_PREFIX.length);
3487
3592
  }
3488
3593
  }
3489
3594
  return eventName;
@@ -3652,7 +3757,7 @@ function attachTwowayEventHandler(binding) {
3652
3757
  return;
3653
3758
  }
3654
3759
  }
3655
- if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
3760
+ if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
3656
3761
  const eventName = getEventName(binding);
3657
3762
  const valueGetter = getValueGetter(binding);
3658
3763
  const isOccurrence = isOccurrenceProperty(binding);
@@ -3678,7 +3783,7 @@ function detachTwowayEventHandler(binding) {
3678
3783
  return;
3679
3784
  }
3680
3785
  }
3681
- if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
3786
+ if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
3682
3787
  const eventName = getEventName(binding);
3683
3788
  const valueGetter = getValueGetter(binding);
3684
3789
  const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrenceProperty(binding));
@@ -4266,7 +4371,7 @@ function readOption(binding, key) {
4266
4371
  continue;
4267
4372
  const modifierKey = modifier.slice(0, separator).trim();
4268
4373
  const value = modifier.slice(separator + 1).trim();
4269
- if (modifierKey !== "init" && modifierKey !== "sync") {
4374
+ if (modifierKey !== MODIFIER_KEY_INIT && modifierKey !== MODIFIER_KEY_SYNC) {
4270
4375
  raiseError(`Unknown binding modifier "${modifierKey}" in "${modifier}".`);
4271
4376
  }
4272
4377
  if (modifierKey !== key)
@@ -4313,8 +4418,8 @@ function resolveInitialSyncPolicy(binding) {
4313
4418
  }
4314
4419
  return STATE_CALL_POLICY;
4315
4420
  }
4316
- const explicitAuthority = parseAuthority(readOption(binding, "init"));
4317
- const syncOn = parseSyncOn(readOption(binding, "sync"));
4421
+ const explicitAuthority = parseAuthority(readOption(binding, MODIFIER_KEY_INIT));
4422
+ const syncOn = parseSyncOn(readOption(binding, MODIFIER_KEY_SYNC));
4318
4423
  if (binding.bindingType === "event") {
4319
4424
  if (explicitAuthority !== null && explicitAuthority !== "none") {
4320
4425
  raiseError("Event bindings only allow init=none.");
@@ -4326,7 +4431,7 @@ function resolveInitialSyncPolicy(binding) {
4326
4431
  // property authority 検証(未宣言なら raiseError)に掛けてはならない。値の初期同期を
4327
4432
  // 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
4328
4433
  // 初期 apply で配線される)。
4329
- if (binding.propSegments[0] === "command") {
4434
+ if (binding.propSegments[0] === COMMAND_NAMESPACE) {
4330
4435
  return statePolicy("state", syncOn);
4331
4436
  }
4332
4437
  if (binding.bindingType !== "prop") {
@@ -5099,7 +5204,7 @@ class BindingSession {
5099
5204
  record.eventAttached = true;
5100
5205
  return;
5101
5206
  }
5102
- if (binding.propSegments[0] === "eventToken") {
5207
+ if (binding.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
5103
5208
  this.attachAfterDefinition(record, () => {
5104
5209
  if (attachEventTokenHandler(binding)) {
5105
5210
  addRecordTeardown(record, () => detachEventTokenHandler(binding));
@@ -5125,7 +5230,7 @@ class BindingSession {
5125
5230
  // isPossibleTwoWay の未定義 CE raiseError も踏まない)。
5126
5231
  if (config.enableDirectionalInitialSync
5127
5232
  && isPossibleTwoWay(binding.node, binding.propName)
5128
- && binding.propModifiers.indexOf("ro") === -1) {
5233
+ && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
5129
5234
  const removeObserver = addTwowayValueObserver(binding.node, binding.propName, (value) => {
5130
5235
  if (!this.isAlive(record, record.generation))
5131
5236
  return;
@@ -5544,7 +5649,8 @@ function applyChangeToCommand(binding, _context, newValue) {
5544
5649
  raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
5545
5650
  }
5546
5651
  if (!bindable.declaredCommands.has(methodName)) {
5547
- raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.`);
5652
+ // eventTokenHandler property 検証と対双の did-you-mean(設計 §3)。
5653
+ raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.${didYouMean(methodName, bindable.declaredCommands.keys())}`);
5548
5654
  }
5549
5655
  // ここまで来たら旧解除して新 subscribe に切り替える。
5550
5656
  if (existing) {
@@ -6204,7 +6310,7 @@ function compileRowPlan(fragmentInfo) {
6204
6310
  // command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
6205
6311
  // teardown / attach 分岐が要るため不適格
6206
6312
  const namespace = template.propSegments[0];
6207
- if (namespace === "command" || namespace === "eventToken") {
6313
+ if (namespace === COMMAND_NAMESPACE || namespace === EVENT_TOKEN_NAMESPACE) {
6208
6314
  return null;
6209
6315
  }
6210
6316
  if (bindingType === "text") {
@@ -7298,12 +7404,15 @@ function scheduleDeferredApply(binding, tagName) {
7298
7404
  }, reject);
7299
7405
  }
7300
7406
 
7301
- const applyChangeByFirstSegment = {
7302
- "class": applyChangeToClass,
7303
- "attr": applyChangeToAttribute,
7304
- "style": applyChangeToStyle,
7305
- "command": applyChangeToCommand,
7306
- };
7407
+ // キーは define.ts の namespace 語彙定数(manifest.syntax.bindingTypes.propNamespaces と
7408
+ // 同一の正本)。集合の一致は __tests__/manifest.test.ts の drift テストが強制するため
7409
+ // export する(manifest エントリは DOM 非依存でこのファイルを import できない)。
7410
+ const applyChangeByFirstSegment = Object.freeze({
7411
+ [CLASS_NAMESPACE]: applyChangeToClass,
7412
+ [ATTR_NAMESPACE]: applyChangeToAttribute,
7413
+ [STYLE_NAMESPACE]: applyChangeToStyle,
7414
+ [COMMAND_NAMESPACE]: applyChangeToCommand,
7415
+ });
7307
7416
  const applyChangeByBindingType = {
7308
7417
  "text": applyChangeToText,
7309
7418
  "for": applyChangeToFor,
@@ -8036,7 +8145,7 @@ async function buildBindings(root) {
8036
8145
  }
8037
8146
  }
8038
8147
 
8039
- var version = "1.27.0";
8148
+ var version = "1.28.0";
8040
8149
  var pkg = {
8041
8150
  version: version};
8042
8151
 
@@ -9636,7 +9745,7 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
9636
9745
  }
9637
9746
  for (const [name, handler] of Object.entries(declared)) {
9638
9747
  if (!eventTokenNames.has(name)) {
9639
- raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.`);
9748
+ raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.${didYouMean(name, eventTokenNames)}`);
9640
9749
  }
9641
9750
  if (typeof handler !== "function") {
9642
9751
  raiseError(`${STATE_ON_NAME} entry "${name}" must be a function.`);
@@ -10674,41 +10783,43 @@ function processWatchDeclaration(stateElement, state) {
10674
10783
  return null;
10675
10784
  }
10676
10785
  if (typeof declared !== "object" || declared === null) {
10677
- raiseError(`${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.`);
10786
+ // 非オブジェクト形は lint 側では候補ゼロ扱いで検出されないため LINT_HINT なし
10787
+ // (以下、lint が実際に検出する shape(非関数・$ 始まり・@ 越境・空セグメント)にだけ付ける)。
10788
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.`);
10678
10789
  }
10679
10790
  const entries = new Map();
10680
10791
  const paths = new Set();
10681
10792
  let order = 0;
10682
10793
  for (const [path, handler] of Object.entries(declared)) {
10683
10794
  if (typeof handler !== "function") {
10684
- raiseError(`${STATE_WATCH_NAME} entry "${path}" must be a function.`);
10795
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must be a function.${LINT_HINT}`);
10685
10796
  }
10686
10797
  if (path.length === 0) {
10687
- raiseError(`${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
10798
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
10688
10799
  }
10689
10800
  if (path.startsWith("$")) {
10690
- raiseError(`${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).`);
10801
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).${LINT_HINT}`);
10691
10802
  }
10692
10803
  // 越境 watch は不採用(設計 D8)。他 state のアドレスは発火対象にしないため、
10693
10804
  // `@stateName` 付きのパスは受け取った時点で落とす(黙って発火しないより良い)。
10694
10805
  if (path.includes(STATE_NAME_SEPARATOR)) {
10695
- raiseError(`${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.`);
10806
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.${LINT_HINT}`);
10696
10807
  }
10697
10808
  // Object.prototype の継承名は `path in state` 系の判定を汚すため一律拒否する
10698
10809
  // (processStreamsDeclaration と同じ防衛線)。
10699
10810
  if (path in Object.prototype) {
10700
- raiseError(`${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
10811
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
10701
10812
  }
10702
10813
  const pathInfo = getPathInfo(path);
10703
10814
  // 空セグメント("a..b" / 先頭・末尾の ".")は getPathInfo が黙って受理してしまうため、
10704
10815
  // ここで落とす。放置すると解決不能なアドレスを依存グラフへ登録することになる。
10705
10816
  for (const segment of pathInfo.segments) {
10706
10817
  if (segment.length === 0) {
10707
- raiseError(`${STATE_WATCH_NAME} entry "${path}" has an empty path segment.`);
10818
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" has an empty path segment.${LINT_HINT}`);
10708
10819
  }
10709
10820
  }
10710
10821
  if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
10711
- raiseError(`${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
10822
+ raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
10712
10823
  }
10713
10824
  entries.set(path, {
10714
10825
  path,
@@ -11171,6 +11282,20 @@ function getAllPropertyDescriptors(obj) {
11171
11282
  * 双方向バインド・spread・initialSync の bindable 判定が**警告なしで**丸ごと死ぬ。
11172
11283
  * 自前のファクトリが自前の reader に棄却される状態なので、生成前に落とす。
11173
11284
  */
11285
+ /**
11286
+ * did-you-mean の候補(エラーパス専用)。`$` 予約名と継承 `constructor` を除き、
11287
+ * `$bindables` には値プロパティだけ・`$commands` にはメソッドだけを提案する
11288
+ * (逆側を提案すると次は「is a method / is not a method」エラーに嵌まるため)。
11289
+ */
11290
+ function dccCandidateNames(descriptors, kind) {
11291
+ return Object.keys(descriptors).filter((key) => {
11292
+ if (key.startsWith("$") || key === "constructor") {
11293
+ return false;
11294
+ }
11295
+ const isMethod = typeof descriptors[key].value === "function";
11296
+ return kind === "method" ? isMethod : !isMethod;
11297
+ });
11298
+ }
11174
11299
  function readNameList(state, declarationName) {
11175
11300
  const declared = state[declarationName];
11176
11301
  if (typeof declared === "undefined") {
@@ -11223,7 +11348,7 @@ function processDccDeclarations(state) {
11223
11348
  streamBackedBindables.push(name);
11224
11349
  continue;
11225
11350
  }
11226
- raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.`);
11351
+ raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "value"))}`);
11227
11352
  }
11228
11353
  if (typeof descriptor.value === "function") {
11229
11354
  raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is a method. Declare it in ${STATE_COMMANDS_NAME} instead.`);
@@ -11232,7 +11357,7 @@ function processDccDeclarations(state) {
11232
11357
  for (const name of commands) {
11233
11358
  const descriptor = descriptors[name];
11234
11359
  if (typeof descriptor === "undefined") {
11235
- raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.`);
11360
+ raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "method"))}`);
11236
11361
  }
11237
11362
  if (typeof descriptor.value !== "function") {
11238
11363
  raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not a method. Declare it in ${STATE_BINDABLES_NAME} instead.`);
@@ -14722,6 +14847,27 @@ function getWcsManifest() {
14722
14847
  },
14723
14848
  // 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
14724
14849
  structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
14850
+ modifiers: {
14851
+ flags: MODIFIER_FLAGS,
14852
+ keyValue: MODIFIER_KEYS,
14853
+ eventNamePrefix: EVENT_PROP_PREFIX,
14854
+ },
14855
+ indexParam: {
14856
+ prefix: INDEX_PARAM_PREFIX,
14857
+ maxDepth: MAX_WILDCARD_DEPTH,
14858
+ },
14859
+ bindingTypes: {
14860
+ elseKeyword: ELSE_KEYWORD,
14861
+ spread: SPREAD_PROP,
14862
+ eventPropertyPrefix: EVENT_PROP_PREFIX,
14863
+ propNamespaces: {
14864
+ eventToken: EVENT_TOKEN_NAMESPACE,
14865
+ command: COMMAND_NAMESPACE,
14866
+ class: CLASS_NAMESPACE,
14867
+ attr: ATTR_NAMESPACE,
14868
+ style: STYLE_NAMESPACE,
14869
+ },
14870
+ },
14725
14871
  },
14726
14872
  // 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
14727
14873
  filters: Object.keys(outputBuiltinFilters),