@wcstack/devtools 1.33.0 → 2.1.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
@@ -11,7 +11,7 @@
11
11
  /** グローバル registry のプロパティ名 */
12
12
  const DEVTOOLS_HOOK_GLOBAL = "__WCSTACK_DEVTOOLS_HOOK__";
13
13
  /** プロトコル版。additive change では上げない(protocol §2) */
14
- const DEVTOOLS_PROTOCOL_VERSION = 1;
14
+ const DEVTOOLS_PROTOCOL_VERSION = 2;
15
15
 
16
16
  /**
17
17
  * protocol/registry.ts
@@ -210,36 +210,44 @@ function formatArgs(args) {
210
210
  * 台帳はすべて devtools 側に置く(protocol 原則 2)。disconnect で
211
211
  * sources / roster / wiring をクリアし、残留参照を持たない。
212
212
  */
213
- /** 予約 state 名 prefix(protocol §5)。この prefix の要素・イベントは常に除外 */
214
- const RESERVED_STATE_NAME_PREFIX = "wcs-devtools";
215
213
  const DEFAULT_TIMELINE_CAPACITY = 500;
216
214
  /** 台帳キーの区切り文字(state 名・パスに現れ得ない NUL)。
217
215
  * エスケープ表記でなく生成式にしているのは、ツーリングがソース中の
218
216
  * `エスケープ列` リテラルを生バイトへ壊す事故が実際に起きたため。 */
219
217
  const NUL = String.fromCharCode(0);
220
- function pathKeyOf(stateName, path) {
221
- return stateName + NUL + path;
218
+ function pathKeyOf(path) {
219
+ return path;
222
220
  }
223
- function tokenKeyOf(stateName, kind, name) {
224
- return stateName + NUL + kind + NUL + name;
221
+ function tokenKeyOf(kind, name) {
222
+ return kind + NUL + name;
225
223
  }
226
- /** 宣言(stateName+path+propName)単位の attach キー。NUL 区切りは pathKeyOf と同じ理由 */
227
- function attachKeyOf(stateName, path, propName) {
228
- return pathKeyOf(stateName, path) + NUL + propName;
224
+ /** 宣言(path+propName)単位の attach キー。NUL 区切りは tokenKeyOf と同じ理由 */
225
+ function attachKeyOf(path, propName) {
226
+ return path + NUL + propName;
227
+ }
228
+ /** roster 表示用のルート由来ラベル(v2: name 次元が無いのでルートで識別する) */
229
+ function rootLabelOf(rootNode) {
230
+ if (typeof ShadowRoot !== "undefined" && rootNode instanceof ShadowRoot) {
231
+ return `<${rootNode.host.tagName.toLowerCase()}>`;
232
+ }
233
+ if (rootNode.nodeType === 9 /* DOCUMENT_NODE */) {
234
+ return "document";
235
+ }
236
+ return rootNode.nodeName.toLowerCase();
229
237
  }
230
238
  /** token カバレッジ 1 行の判定(§4: 空撃ちのみ / 一部空撃ち / 正常 / 未発火) */
231
- function tokenCoverageOf(stateName, kind, name, stat) {
239
+ function tokenCoverageOf(kind, name, stat) {
232
240
  if (stat === undefined) {
233
- return { stateName, kind, name, status: "never", count: 0, note: null };
241
+ return { kind, name, status: "never", count: 0, note: null };
234
242
  }
235
243
  if (stat.zeroSubscriberCount === stat.count) {
236
244
  return {
237
- stateName, kind, name, status: "emitted-unheard", count: stat.count,
245
+ kind, name, status: "emitted-unheard", count: stat.count,
238
246
  note: `all ${stat.count} emit(s) had 0 subscribers`,
239
247
  };
240
248
  }
241
249
  return {
242
- stateName, kind, name, status: "emitted", count: stat.count,
250
+ kind, name, status: "emitted", count: stat.count,
243
251
  note: stat.zeroSubscriberCount > 0
244
252
  ? `${stat.zeroSubscriberCount}/${stat.count} emit(s) had 0 subscribers`
245
253
  : null,
@@ -247,7 +255,6 @@ function tokenCoverageOf(stateName, kind, name, stat) {
247
255
  }
248
256
  class DevtoolsCore {
249
257
  _timelineCapacity;
250
- _hiddenStateNames;
251
258
  _removeListener = null;
252
259
  _sources = new Map();
253
260
  _roster = new Map();
@@ -259,10 +266,14 @@ class DevtoolsCore {
259
266
  _changeListeners = new Set();
260
267
  /** 観測開始時刻(connect 時の performance.now。未接続は null)。 */
261
268
  _observingSince = null;
262
- /** watch 発火回数(stateName + NUL + path → count)。カバレッジの実測面。 */
263
- _watchFiredCounts = new Map();
264
- /** token emit 実測(stateName + NUL + kind + NUL + name → 回数と空撃ち回数)。 */
265
- _tokenEmitCounts = new Map();
269
+ /** watch 発火実測(path → ツリー別区分列)。カバレッジの実測面。
270
+ * protocol v2 追補で payload がツリー識別(stateElement)を持つようになり、
271
+ * 複数ツリーが同名 watch パスを宣言するページでもツリー別に数えられる。
272
+ * 識別の無い旧 payload は null 区分に積み、どのツリーの照会にも合算で残す
273
+ * (slice 29 non-blocking 記録の解消)。 */
274
+ _watchFiredStats = new Map();
275
+ /** token emit 実測(kind + NUL + name → ツリー別区分列)。上の追補注記と同じ。 */
276
+ _tokenEmitStats = new Map();
266
277
  /** 観測開始以降に一度でも attach を観測した宣言キー(attachKeyOf)。
267
278
  * live 配線台帳は WeakRef pruning / binding-removed で縮むため、binding
268
279
  * カバレッジの attached 判定はこちらで行う(瞬間値で判定すると行の
@@ -270,7 +281,6 @@ class DevtoolsCore {
270
281
  _everAttachedKeys = new Set();
271
282
  constructor(options) {
272
283
  this._timelineCapacity = options?.timelineCapacity ?? DEFAULT_TIMELINE_CAPACITY;
273
- this._hiddenStateNames = new Set(options?.hiddenStateNames ?? []);
274
284
  }
275
285
  get connected() {
276
286
  return this._removeListener !== null;
@@ -281,13 +291,6 @@ class DevtoolsCore {
281
291
  set paused(value) {
282
292
  this._paused = value;
283
293
  }
284
- /** 表示から除外する state 名か(予約 prefix + hiddenStateNames、protocol §5) */
285
- isHiddenStateName(name) {
286
- if (name === null) {
287
- return false;
288
- }
289
- return name.startsWith(RESERVED_STATE_NAME_PREFIX) || this._hiddenStateNames.has(name);
290
- }
291
294
  connect() {
292
295
  if (this._removeListener !== null) {
293
296
  return;
@@ -325,8 +328,8 @@ class DevtoolsCore {
325
328
  this._wiringByPathKey.clear();
326
329
  this._wiringEntryByBinding = new WeakMap();
327
330
  this._observingSince = null;
328
- this._watchFiredCounts.clear();
329
- this._tokenEmitCounts.clear();
331
+ this._watchFiredStats.clear();
332
+ this._tokenEmitStats.clear();
330
333
  this._everAttachedKeys.clear();
331
334
  this._notify("sources");
332
335
  this._notify("roster");
@@ -367,13 +370,22 @@ class DevtoolsCore {
367
370
  this._timeline = [];
368
371
  this._notify("timeline");
369
372
  }
370
- /** 指定パスに束縛された配線(生存している binding のみ) */
371
- getWiringForPath(stateName, path) {
372
- const set = this._wiringByPathKey.get(pathKeyOf(stateName, path));
373
+ /**
374
+ * 指定パスに束縛された配線(生存している binding のみ)。
375
+ * stateElement を渡すとそのツリーの配線だけに絞る(v2 は名前次元が無いため、
376
+ * 複数ツリーの同名パスは要素同一性で区別する)。stateElement を持たない
377
+ * 旧 payload 由来の entry は絞り込みでも残す(欠測を欠落にしない)。
378
+ */
379
+ getWiringForPath(path, stateElement) {
380
+ const set = this._wiringByPathKey.get(pathKeyOf(path));
373
381
  if (set === undefined) {
374
382
  return [];
375
383
  }
376
- return this._collectAlive(set);
384
+ const alive = this._collectAlive(set);
385
+ if (stateElement === undefined) {
386
+ return alive;
387
+ }
388
+ return alive.filter((entry) => entry.stateElementRef === null || entry.stateElementRef.deref() === stateElement);
377
389
  }
378
390
  /** 全配線のスナップショット(生存している binding のみ) */
379
391
  getAllWiring() {
@@ -421,10 +433,7 @@ class DevtoolsCore {
421
433
  }
422
434
  for (const root of roots) {
423
435
  for (const declared of source.getDeclaredBindings(root)) {
424
- if (this.isHiddenStateName(declared.stateName)) {
425
- continue;
426
- }
427
- const key = [declared.stateName, declared.statePathName, declared.propName,
436
+ const key = [declared.statePathName, declared.propName,
428
437
  declared.bindingType, declared.origin, declared.raw].join(NUL);
429
438
  if (seen.has(key)) {
430
439
  continue;
@@ -440,16 +449,26 @@ class DevtoolsCore {
440
449
  * 配線カバレッジ(§4): 宣言面(watchPaths / token 宣言 / canonical declared)と
441
450
  * 実測面(watch-fired・token-emit の台帳・live 配線台帳)の突合。
442
451
  * 「観測開始以降」の実測であることは observingSince を UI が常時表示して明示する。
452
+ *
453
+ * watch / token の実測は各行の属するツリー(roster entry の element)でスコープする
454
+ * (protocol v2 追補 — 複数ツリーの同名宣言を合算しない)。ツリー識別の無い
455
+ * 旧 payload 由来の実測はどの行にも合算で残す(欠測を欠落にしない)。
456
+ * stateElement を渡すとそのツリーの宣言行だけに絞る(getWiringForPath と同じ流儀)。
457
+ * binding 節(canonical declared × ever-attach)は宣言・実測ともツリー識別を
458
+ * 持たないため絞り込みの対象外。
443
459
  */
444
- getCoverageReport() {
460
+ getCoverageReport(stateElement) {
445
461
  const out = [];
446
462
  for (const entry of this.getRoster()) {
447
463
  const summary = entry.summary;
448
- // --- watch: 宣言(watchPaths)× 実測(watch-fired 台帳) ---
464
+ if (stateElement !== undefined && summary.element !== stateElement) {
465
+ continue;
466
+ }
467
+ // --- watch: 宣言(watchPaths)× 実測(watch-fired 台帳・自ツリー + 旧 payload 合算) ---
449
468
  for (const path of summary.watchPaths ?? []) {
450
- const count = this._watchFiredCounts.get(pathKeyOf(entry.name, path)) ?? 0;
469
+ const count = this._measuredFor(this._watchFiredStats, pathKeyOf(path), summary.element)?.count ?? 0;
451
470
  if (count > 0) {
452
- out.push({ stateName: entry.name, kind: "watch", name: path, status: "fired", count, note: null });
471
+ out.push({ kind: "watch", name: path, status: "fired", count, note: null });
453
472
  continue;
454
473
  }
455
474
  // ワイルドカード行 watch に**リスト書き込み**が届く前提 = 各 `.*` 階層の
@@ -473,23 +492,23 @@ class DevtoolsCore {
473
492
  }
474
493
  if (missingList !== null) {
475
494
  out.push({
476
- stateName: entry.name, kind: "watch", name: path, status: "prerequisite-missing", count: 0,
495
+ kind: "watch", name: path, status: "prerequisite-missing", count: 0,
477
496
  note: keyedKnown
478
497
  ? `list "${missingList}" has no for binding and no $listKeys declaration — list writes never reach its rows (only explicit-index writes can fire this watch)`
479
498
  : `no for binding observed for list "${missingList}" (a $listKeys declaration would still let list writes fire it)`,
480
499
  });
481
500
  continue;
482
501
  }
483
- out.push({ stateName: entry.name, kind: "watch", name: path, status: "never", count: 0, note: null });
502
+ out.push({ kind: "watch", name: path, status: "never", count: 0, note: null });
484
503
  }
485
504
  // --- token: 宣言(commandTokenNames / eventTokenNames)× 実測(emit 台帳)。
486
505
  // subscriberCount 0 = 空撃ち(§4): 全 emit が空撃ちなら emitted-unheard として
487
506
  // 警告する(emit 側だけ配線されて受け手が一つも居ない構成ミスの検出)。 ---
488
507
  for (const name of summary.commandTokenNames) {
489
- out.push(tokenCoverageOf(entry.name, "command", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "command", name))));
508
+ out.push(tokenCoverageOf("command", name, this._measuredFor(this._tokenEmitStats, tokenKeyOf("command", name), summary.element)));
490
509
  }
491
510
  for (const name of summary.eventTokenNames) {
492
- out.push(tokenCoverageOf(entry.name, "eventToken", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "event", name))));
511
+ out.push(tokenCoverageOf("eventToken", name, this._measuredFor(this._tokenEmitStats, tokenKeyOf("event", name), summary.element)));
493
512
  }
494
513
  }
495
514
  // --- binding: canonical declared(宣言)× live 配線台帳(実測)。canonical が
@@ -504,7 +523,7 @@ class DevtoolsCore {
504
523
  continue;
505
524
  if (decl.propName.startsWith("eventToken."))
506
525
  continue;
507
- const key = attachKeyOf(decl.stateName, decl.statePathName, decl.propName);
526
+ const key = attachKeyOf(decl.statePathName, decl.propName);
508
527
  if (seen.has(key))
509
528
  continue;
510
529
  seen.add(key);
@@ -512,7 +531,7 @@ class DevtoolsCore {
512
531
  // 破棄や GC(WeakRef pruning)で縮むため、瞬間値だと逆戻りする。
513
532
  const attached = this._everAttachedKeys.has(key);
514
533
  out.push({
515
- stateName: decl.stateName, kind: "binding",
534
+ kind: "binding",
516
535
  name: `${decl.propName} ← ${decl.statePathName}`,
517
536
  status: attached ? "attached" : "never-attached",
518
537
  count: attached ? 1 : 0,
@@ -528,21 +547,33 @@ class DevtoolsCore {
528
547
  if (source === undefined || typeof source.keys !== "function") {
529
548
  return [];
530
549
  }
531
- return source.keys(entry.name, entry.rootNode);
550
+ return source.keys(entry.rootNode);
551
+ }
552
+ /**
553
+ * roster entry のツリーに載るマウント記録(overlays — protocol v2・D20 の可視化)。
554
+ * overlays を実装しないランタイム(v2 より前)では null — UI はセクションごと
555
+ * 出さない(後方互換)。マウントが無いツリーは空配列。
556
+ */
557
+ overlaysOf(entry) {
558
+ const source = this._sources.get(entry.sourceId);
559
+ if (source === undefined || typeof source.overlays !== "function") {
560
+ return null;
561
+ }
562
+ return source.overlays(entry.rootNode);
532
563
  }
533
564
  readValue(entry, path, indexes) {
534
565
  const source = this._sources.get(entry.sourceId);
535
566
  if (source === undefined) {
536
567
  return undefined;
537
568
  }
538
- return source.read(entry.name, entry.rootNode, path, indexes);
569
+ return source.read(entry.rootNode, path, indexes);
539
570
  }
540
571
  writeValue(entry, path, value, indexes) {
541
572
  const source = this._sources.get(entry.sourceId);
542
573
  if (source === undefined) {
543
574
  return;
544
575
  }
545
- source.write(entry.name, entry.rootNode, path, value, indexes);
576
+ source.write(entry.rootNode, path, value, indexes);
546
577
  }
547
578
  // --- internal ---
548
579
  _notify(kind) {
@@ -552,19 +583,74 @@ class DevtoolsCore {
552
583
  }
553
584
  _refreshRosterOf(source) {
554
585
  const entries = [];
586
+ const labelCounts = new Map();
555
587
  for (const summary of source.getStateElements()) {
556
- if (this.isHiddenStateName(summary.name)) {
557
- continue;
558
- }
588
+ const base = rootLabelOf(summary.rootNode);
589
+ const seen = (labelCounts.get(base) ?? 0) + 1;
590
+ labelCounts.set(base, seen);
559
591
  entries.push({
560
592
  sourceId: source.id,
561
- name: summary.name,
593
+ label: seen === 1 ? base : `${base}#${seen}`,
562
594
  rootNode: summary.rootNode,
563
595
  summary,
564
596
  });
565
597
  }
566
598
  this._roster.set(source.id, entries);
567
599
  }
600
+ /**
601
+ * key の実測台帳から stateElement 区分の stat を探すか作る(ingest 用)。
602
+ * payload の stateElement がオブジェクトでない(旧 payload / 直接生成 token)
603
+ * ときは null 区分 = 全ツリー合算面に積む。
604
+ */
605
+ _statFor(map, key, stateElement) {
606
+ let entries = map.get(key);
607
+ if (entries === undefined) {
608
+ entries = [];
609
+ map.set(key, entries);
610
+ }
611
+ const element = typeof stateElement === "object" && stateElement !== null ? stateElement : null;
612
+ for (const entry of entries) {
613
+ if (element === null ? entry.stateElementRef === null : entry.stateElementRef?.deref() === element) {
614
+ return entry;
615
+ }
616
+ }
617
+ const created = {
618
+ stateElementRef: element === null ? null : new WeakRef(element),
619
+ count: 0,
620
+ zeroSubscriberCount: 0,
621
+ };
622
+ entries.push(created);
623
+ return created;
624
+ }
625
+ /**
626
+ * key の実測を element のツリーへスコープして集計する(照会用)。
627
+ * null 区分(ツリー識別の無い旧 payload)は常に合算に含める。GC 済みツリーの
628
+ * 区分は遅延剪定(wiring 台帳の WeakRef pruning と同じ)。実測ゼロは undefined
629
+ * (= 宣言だけあって一度も観測していない)に畳む。
630
+ */
631
+ _measuredFor(map, key, element) {
632
+ const entries = map.get(key);
633
+ if (entries === undefined) {
634
+ return undefined;
635
+ }
636
+ let count = 0;
637
+ let zeroSubscriberCount = 0;
638
+ for (let i = entries.length - 1; i >= 0; i--) {
639
+ const ref = entries[i].stateElementRef;
640
+ if (ref !== null && ref.deref() === undefined) {
641
+ entries.splice(i, 1);
642
+ continue;
643
+ }
644
+ if (ref === null || ref.deref() === element) {
645
+ count += entries[i].count;
646
+ zeroSubscriberCount += entries[i].zeroSubscriberCount;
647
+ }
648
+ }
649
+ if (entries.length === 0) {
650
+ map.delete(key);
651
+ }
652
+ return count === 0 ? undefined : { count, zeroSubscriberCount };
653
+ }
568
654
  _collectAlive(set) {
569
655
  const alive = [];
570
656
  for (const entry of set) {
@@ -600,9 +686,6 @@ class DevtoolsCore {
600
686
  _ingest(sourceId, event) {
601
687
  switch (event.type) {
602
688
  case "state:element-registered": {
603
- if (this.isHiddenStateName(event.name)) {
604
- return;
605
- }
606
689
  const source = this._sources.get(sourceId);
607
690
  if (source !== undefined) {
608
691
  this._refreshRosterOf(source);
@@ -611,17 +694,13 @@ class DevtoolsCore {
611
694
  this._appendTimeline({
612
695
  sourceId,
613
696
  kind: "element-registered",
614
- stateName: event.name,
615
- label: event.name,
697
+ label: rootLabelOf(event.rootNode),
616
698
  detail: "",
617
699
  subscriberCount: null,
618
700
  });
619
701
  return;
620
702
  }
621
703
  case "state:element-unregistered": {
622
- if (this.isHiddenStateName(event.name)) {
623
- return;
624
- }
625
704
  const source = this._sources.get(sourceId);
626
705
  if (source !== undefined) {
627
706
  this._refreshRosterOf(source);
@@ -630,25 +709,19 @@ class DevtoolsCore {
630
709
  this._appendTimeline({
631
710
  sourceId,
632
711
  kind: "element-unregistered",
633
- stateName: event.name,
634
- label: event.name,
712
+ label: rootLabelOf(event.rootNode),
635
713
  detail: "",
636
714
  subscriberCount: null,
637
715
  });
638
716
  return;
639
717
  }
640
718
  case "state:write": {
641
- const stateName = event.absoluteAddress.absolutePathInfo.stateName;
642
- if (this.isHiddenStateName(stateName)) {
643
- return;
644
- }
645
719
  const detail = event.hasOldValue
646
720
  ? `${formatValue(event.value)} (was ${formatValue(event.oldValue)})`
647
721
  : formatValue(event.value);
648
722
  this._appendTimeline({
649
723
  sourceId,
650
724
  kind: "write",
651
- stateName,
652
725
  label: this._labelOf(event.absoluteAddress),
653
726
  detail,
654
727
  subscriberCount: null,
@@ -659,9 +732,6 @@ class DevtoolsCore {
659
732
  const labels = [];
660
733
  let total = 0;
661
734
  for (const address of event.addresses) {
662
- if (this.isHiddenStateName(address.absolutePathInfo.stateName)) {
663
- continue;
664
- }
665
735
  total++;
666
736
  if (labels.length < 3) {
667
737
  labels.push(this._labelOf(address));
@@ -674,7 +744,6 @@ class DevtoolsCore {
674
744
  this._appendTimeline({
675
745
  sourceId,
676
746
  kind: "batch",
677
- stateName: null,
678
747
  label: `${total} address${total === 1 ? "" : "es"}`,
679
748
  detail: `${labels.join(", ")}${rest}`,
680
749
  subscriberCount: null,
@@ -682,29 +751,28 @@ class DevtoolsCore {
682
751
  return;
683
752
  }
684
753
  case "state:binding-added": {
685
- const stateName = event.absoluteAddress.absolutePathInfo.stateName;
686
- if (this.isHiddenStateName(stateName)) {
687
- return;
688
- }
689
754
  const path = event.absoluteAddress.absolutePathInfo.pathInfo.path;
690
- const key = pathKeyOf(stateName, path);
755
+ const key = pathKeyOf(path);
691
756
  let set = this._wiringByPathKey.get(key);
692
757
  if (set === undefined) {
693
758
  set = new Set();
694
759
  this._wiringByPathKey.set(key, set);
695
760
  }
761
+ const stateElement = event.absoluteAddress.absolutePathInfo.stateElement;
696
762
  const entry = {
697
763
  sourceId,
698
- stateName,
699
764
  path,
700
765
  propName: event.binding.propName,
701
766
  bindingType: event.binding.bindingType,
702
767
  bindingRef: new WeakRef(event.binding),
768
+ stateElementRef: typeof stateElement === "object" && stateElement !== null
769
+ ? new WeakRef(stateElement)
770
+ : null,
703
771
  };
704
772
  set.add(entry);
705
773
  this._wiringEntryByBinding.set(event.binding, entry);
706
774
  // カバレッジの実測面(B2): attach の観測を ever 台帳へ積む(remove では消さない)
707
- this._everAttachedKeys.add(attachKeyOf(stateName, path, event.binding.propName));
775
+ this._everAttachedKeys.add(attachKeyOf(path, event.binding.propName));
708
776
  this._notify("wiring");
709
777
  this._notify("coverage");
710
778
  return;
@@ -715,7 +783,7 @@ class DevtoolsCore {
715
783
  return;
716
784
  }
717
785
  this._wiringEntryByBinding.delete(event.binding);
718
- const key = pathKeyOf(entry.stateName, entry.path);
786
+ const key = pathKeyOf(entry.path);
719
787
  const set = this._wiringByPathKey.get(key);
720
788
  if (set !== undefined) {
721
789
  set.delete(entry);
@@ -727,41 +795,26 @@ class DevtoolsCore {
727
795
  return;
728
796
  }
729
797
  case "state:binding-cleared": {
730
- const stateName = event.absoluteAddress.absolutePathInfo.stateName;
731
798
  const path = event.absoluteAddress.absolutePathInfo.pathInfo.path;
732
- const key = pathKeyOf(stateName, path);
799
+ const key = pathKeyOf(path);
733
800
  if (this._wiringByPathKey.delete(key)) {
734
801
  this._notify("wiring");
735
802
  }
736
803
  return;
737
804
  }
738
805
  case "state:token-emit": {
739
- if (this.isHiddenStateName(event.stateName)) {
740
- return;
741
- }
742
- // カバレッジの実測面: emit 回数と空撃ち回数を台帳に積む(§4)。
743
- // stateName が null の emit はどの宣言とも突合できないため積まない。
744
- if (event.stateName !== null) {
745
- const emitKey = tokenKeyOf(event.stateName, event.kind, event.tokenName);
746
- const stat = this._tokenEmitCounts.get(emitKey);
747
- if (stat === undefined) {
748
- this._tokenEmitCounts.set(emitKey, {
749
- count: 1,
750
- zeroSubscriberCount: event.subscriberCount === 0 ? 1 : 0,
751
- });
752
- }
753
- else {
754
- stat.count += 1;
755
- if (event.subscriberCount === 0) {
756
- stat.zeroSubscriberCount += 1;
757
- }
758
- }
759
- this._notify("coverage");
806
+ // カバレッジの実測面: emit 回数と空撃ち回数を、発火元ツリー
807
+ // (payload の stateElement — protocol v2 追補)別の区分に積む(§4)。
808
+ // 識別の無い旧 payload は null 区分 = 全ツリー合算面へ。
809
+ const stat = this._statFor(this._tokenEmitStats, tokenKeyOf(event.kind, event.tokenName), event.stateElement);
810
+ stat.count += 1;
811
+ if (event.subscriberCount === 0) {
812
+ stat.zeroSubscriberCount += 1;
760
813
  }
814
+ this._notify("coverage");
761
815
  this._appendTimeline({
762
816
  sourceId,
763
817
  kind: event.kind,
764
- stateName: event.stateName,
765
818
  label: event.tokenName,
766
819
  detail: formatArgs(event.args),
767
820
  subscriberCount: event.subscriberCount,
@@ -769,26 +822,19 @@ class DevtoolsCore {
769
822
  return;
770
823
  }
771
824
  case "state:watch-fired": {
772
- if (this.isHiddenStateName(event.stateName)) {
773
- return;
774
- }
775
825
  // timeline 行にはしない(発火は高頻度になりうる活動でありカバレッジが受け皿)。
776
- const firedKey = pathKeyOf(event.stateName, event.path);
777
- this._watchFiredCounts.set(firedKey, (this._watchFiredCounts.get(firedKey) ?? 0) + 1);
826
+ // 発火元ツリー(payload の stateElement — protocol v2 追補)別に数える。
827
+ this._statFor(this._watchFiredStats, pathKeyOf(event.path), event.stateElement).count += 1;
778
828
  this._notify("coverage");
779
829
  return;
780
830
  }
781
831
  case "state:watch-error": {
782
- if (this.isHiddenStateName(event.stateName)) {
783
- return;
784
- }
785
832
  // ランタイム側は例外を握って drain を守るため、ここに出ないと失敗が
786
833
  // どこにも現れない(console を見ていない限り)。phase を detail の先頭に
787
834
  // 置くのは、getter の評価失敗とハンドラの失敗で直し方が違うため。
788
835
  this._appendTimeline({
789
836
  sourceId,
790
837
  kind: "watch-error",
791
- stateName: event.stateName,
792
838
  label: event.path,
793
839
  detail: `${event.phase}: ${formatError(event.error)}`,
794
840
  subscriberCount: null,
@@ -799,9 +845,7 @@ class DevtoolsCore {
799
845
  this._appendTimeline({
800
846
  sourceId,
801
847
  kind: "watch-chain-limit",
802
- stateName: null,
803
- // 打ち切りはバッチ単位で state 名を持たない(複数 state のアドレスが
804
- // 載りうる)ため、hidden 判定はここでは行わない。
848
+ // 打ち切りはバッチ単位(複数ツリーのアドレスが載りうる)— 行の主語は深さ上限
805
849
  label: `depth > ${event.maxDepth}`,
806
850
  detail: event.paths.join(", "),
807
851
  subscriberCount: null,
@@ -809,15 +853,11 @@ class DevtoolsCore {
809
853
  return;
810
854
  }
811
855
  case "state:path-unresolved": {
812
- if (this.isHiddenStateName(event.stateName)) {
813
- return;
814
- }
815
856
  // ランタイムは warn で続行する(既存ページを止めないため)。lint を
816
857
  // 走らせていない相手には、ここが「配線が死んでいる」唯一の可視面になる。
817
858
  this._appendTimeline({
818
859
  sourceId,
819
860
  kind: "path-unresolved",
820
- stateName: event.stateName,
821
861
  label: event.path,
822
862
  detail: `${event.source}: "${event.missingSegment}" is not declared`,
823
863
  subscriberCount: null,
@@ -825,15 +865,11 @@ class DevtoolsCore {
825
865
  return;
826
866
  }
827
867
  case "state:binding-apply-error": {
828
- if (this.isHiddenStateName(event.stateName)) {
829
- return;
830
- }
831
868
  // 隔離された適用失敗。bindingType を detail 先頭に置くのは、構造
832
869
  // (for / if)の失敗と値の失敗で影響範囲が違うため。
833
870
  this._appendTimeline({
834
871
  sourceId,
835
872
  kind: "binding-apply-error",
836
- stateName: event.stateName,
837
873
  label: event.path,
838
874
  detail: `${event.bindingType}: ${formatError(event.error)}`,
839
875
  subscriberCount: null,
@@ -841,12 +877,10 @@ class DevtoolsCore {
841
877
  return;
842
878
  }
843
879
  case "propagation:suppressed": {
844
- // two-way エコーの辺単位抑止。state 名を持たない(node+member が主語)
845
- // ため hidden 判定はここでは行わない。
880
+ // two-way エコーの辺単位抑止(主語は node + member — パスではない)
846
881
  this._appendTimeline({
847
882
  sourceId,
848
883
  kind: "propagation-suppressed",
849
- stateName: null,
850
884
  label: event.member,
851
885
  detail: `${event.reason} (tx ${event.transactionId}, edge ${event.edgeId})`,
852
886
  subscriberCount: null,
@@ -854,14 +888,9 @@ class DevtoolsCore {
854
888
  return;
855
889
  }
856
890
  case "propagation:coalesced": {
857
- const stateName = event.absoluteAddress.absolutePathInfo.stateName;
858
- if (this.isHiddenStateName(stateName)) {
859
- return;
860
- }
861
891
  this._appendTimeline({
862
892
  sourceId,
863
893
  kind: "propagation-coalesced",
864
- stateName,
865
894
  label: this._labelOf(event.absoluteAddress),
866
895
  detail: `tx ${event.droppedTransactionId} dropped (winner tx ${event.winnerTransactionId})`,
867
896
  subscriberCount: null,
@@ -869,14 +898,9 @@ class DevtoolsCore {
869
898
  return;
870
899
  }
871
900
  case "propagation:hop-limit": {
872
- const stateName = event.absoluteAddress.absolutePathInfo.stateName;
873
- if (this.isHiddenStateName(stateName)) {
874
- return;
875
- }
876
901
  this._appendTimeline({
877
902
  sourceId,
878
903
  kind: "propagation-hop-limit",
879
- stateName,
880
904
  label: this._labelOf(event.absoluteAddress),
881
905
  detail: `hop ${event.hop} (tx ${event.transactionId})`,
882
906
  subscriberCount: null,
@@ -894,7 +918,6 @@ class DevtoolsCore {
894
918
  this._appendTimeline({
895
919
  sourceId,
896
920
  kind: "contract-drift",
897
- stateName: null,
898
921
  label: event.tag,
899
922
  detail: `${event.reason}${memberPart}${eventPart}`,
900
923
  subscriberCount: null,
@@ -922,7 +945,7 @@ class DevtoolsCore {
922
945
  * 「宣言レベルの配線ビュー」を組む。ライブ台帳と違い binding 実体・
923
946
  * 接続状態は分からない(UI では "declared" バッジで区別する)。
924
947
  *
925
- * パースは表示目的の簡易版(`prop[#mod]: path[@state][|filters]` を
948
+ * パースは表示目的の簡易版(`prop[#mod]: path[|filters]` を
926
949
  * `;` 区切りで分解するだけ)。正確なセマンティクスの正本は
927
950
  * @wcstack/state の bindTextParser であり、ここでは追随しない。
928
951
  */
@@ -943,18 +966,13 @@ function parseEntry(element, raw, origin) {
943
966
  return null;
944
967
  }
945
968
  const [pathPart, ...filterParts] = rhs.split("|").map((part) => part.trim());
946
- let path = pathPart;
947
- let stateName = "default";
948
- const at = pathPart.lastIndexOf("@");
949
- if (at > 0) {
950
- path = pathPart.slice(0, at).trim();
951
- stateName = pathPart.slice(at + 1).trim();
952
- }
969
+ // v2: `@name` セレクタは撤去済み(ランタイムは parse error にする)。
970
+ // 簡易パーサはパスをそのまま流す — 壊れた宣言の可視化はランタイム側の役目。
971
+ const path = pathPart;
953
972
  return {
954
973
  element,
955
974
  propName,
956
975
  path,
957
- stateName,
958
976
  filters: filterParts.filter((part) => part.length > 0),
959
977
  origin,
960
978
  raw: trimmed,
@@ -1108,6 +1126,11 @@ header .spacer { flex: 1; }
1108
1126
  .notice { color: #efe3a0; padding: 2px 0 6px; }
1109
1127
  .notice button { font: inherit; margin-left: 6px; cursor: pointer; }
1110
1128
  .wiring-row .detail { color: #6f88a3; }
1129
+ .overlays-heading { margin-top: 10px; }
1130
+ .overlay-row { padding: 1px 0; white-space: nowrap; }
1131
+ .overlay-row .prop { color: #b7f0c0; }
1132
+ .overlay-row .path { color: #9fd0ff; }
1133
+ .overlay-row .detail { color: #6f88a3; }
1111
1134
  .wiring-tabs { padding: 0 0 4px; }
1112
1135
  .wiring-tabs button { font: inherit; cursor: pointer; margin-right: 4px; opacity: 0.6; }
1113
1136
  .wiring-tabs button.active { opacity: 1; font-weight: bold; }
@@ -1178,13 +1201,8 @@ class WcsDevtools extends HTMLElement {
1178
1201
  this._buildShadow();
1179
1202
  }
1180
1203
  const capacity = Number(this.getAttribute("buffer") ?? "");
1181
- const hidden = (this.getAttribute("hidden-states") ?? "")
1182
- .split(",")
1183
- .map((name) => name.trim())
1184
- .filter((name) => name.length > 0);
1185
1204
  this._core = new DevtoolsCore({
1186
1205
  timelineCapacity: Number.isFinite(capacity) && capacity > 0 ? capacity : undefined,
1187
- hiddenStateNames: hidden,
1188
1206
  });
1189
1207
  this._removeCoreListener = this._core.onChange((kind) => {
1190
1208
  if (kind === "roster" || kind === "sources") {
@@ -1461,7 +1479,7 @@ class WcsDevtools extends HTMLElement {
1461
1479
  }
1462
1480
  }
1463
1481
  _rosterKey(entry) {
1464
- return `${entry.sourceId}:${entry.name}`;
1482
+ return `${entry.sourceId}:${entry.label}`;
1465
1483
  }
1466
1484
  _selectedRoster() {
1467
1485
  const core = this._core;
@@ -1481,7 +1499,7 @@ class WcsDevtools extends HTMLElement {
1481
1499
  select.replaceChildren(...roster.map((entry) => {
1482
1500
  const option = document.createElement("option");
1483
1501
  option.value = this._rosterKey(entry);
1484
- option.textContent = `${entry.name} (${entry.sourceId.slice(0, 12)})`;
1502
+ option.textContent = `${entry.label} (${entry.sourceId.slice(0, 12)})`;
1485
1503
  option.selected = selected !== null && this._rosterKey(entry) === this._rosterKey(selected);
1486
1504
  return option;
1487
1505
  }));
@@ -1494,11 +1512,73 @@ class WcsDevtools extends HTMLElement {
1494
1512
  const keys = core.keysOf(selected);
1495
1513
  if (keys.length === 0) {
1496
1514
  body.append(this._emptyRow("no readable keys (runtime without keys() API?)"));
1515
+ // keys が読めないランタイムでも overlays は独立に読める可能性があるため出す
1516
+ this._renderOverlaysSection(body, selected);
1497
1517
  return;
1498
1518
  }
1499
1519
  for (const key of keys) {
1500
1520
  this._renderTreeNode(body, selected, { path: key, indexes: [] }, key, 0);
1501
1521
  }
1522
+ this._renderOverlaysSection(body, selected);
1523
+ }
1524
+ /**
1525
+ * 選択ツリーのマウント記録セクション(overlays — protocol v2・D20 の可視化)。
1526
+ * マウントの私有キー・getter はオーバーレイ専用アドレス空間(マーカー `#m<id>`)に
1527
+ * 住み、上の状態ツリー(keys/read)には現れないため、ここが唯一の可視面。
1528
+ * overlays 未提供の旧ランタイム(null)ではセクションごと出さない(後方互換)。
1529
+ * マウント無し(空配列)も見出しだけ残さない。pull は State ペインの再描画
1530
+ * (roster / sources 変更・ツリー切替)にそのまま乗る — 専用 polling は足さない
1531
+ * (観測者効果の排除、devtools-tag-design.md の rAF 合流と同じ姿勢)。
1532
+ */
1533
+ _renderOverlaysSection(body, entry) {
1534
+ const overlays = this._core.overlaysOf(entry);
1535
+ if (overlays === null || overlays.length === 0) {
1536
+ return;
1537
+ }
1538
+ const heading = document.createElement("h3");
1539
+ heading.className = "overlays-heading";
1540
+ heading.textContent = `Overlays (${overlays.length} mount${overlays.length === 1 ? "" : "s"})`;
1541
+ body.append(heading);
1542
+ for (const overlay of overlays) {
1543
+ body.append(this._overlayRow(overlay));
1544
+ }
1545
+ }
1546
+ /** マウント記録 1 件の描画(マーカー・コンポーネント・マウント表・私有面)。 */
1547
+ _overlayRow(overlay) {
1548
+ const row = document.createElement("div");
1549
+ row.className = "overlay-row";
1550
+ const marker = document.createElement("span");
1551
+ marker.className = "badge-tag";
1552
+ marker.textContent = overlay.marker;
1553
+ const tag = document.createElement("span");
1554
+ tag.className = "prop";
1555
+ tag.textContent = `<${overlay.componentTag}>`;
1556
+ const prop = document.createElement("span");
1557
+ prop.className = "path";
1558
+ // stateProp はホスト側の `state[.sub]:` 宣言の書き手語彙
1559
+ prop.textContent = overlay.stateProp;
1560
+ row.append(marker, document.createTextNode(" "), tag, document.createTextNode(" "), prop);
1561
+ const details = [];
1562
+ for (const entry of overlay.mountTable) {
1563
+ // 内側接頭辞が空 = ルートエントリ。外側パスへの対応で読ませる
1564
+ details.push(`${entry.inner === "" ? "(root)" : entry.inner} → ${entry.outer}`);
1565
+ }
1566
+ if (overlay.delta > 0) {
1567
+ details.push(`Δ${overlay.delta}`);
1568
+ }
1569
+ if (overlay.privateKeys.length > 0) {
1570
+ details.push(`private: ${overlay.privateKeys.join(", ")}`);
1571
+ }
1572
+ if (overlay.getterKeys.length > 0) {
1573
+ details.push(`getters: ${overlay.getterKeys.join(", ")}`);
1574
+ }
1575
+ if (details.length > 0) {
1576
+ const detail = document.createElement("span");
1577
+ detail.className = "detail";
1578
+ detail.textContent = ` ${details.join(" · ")}`;
1579
+ row.append(detail);
1580
+ }
1581
+ return row;
1502
1582
  }
1503
1583
  _renderTreeNode(container, entry, ref, label, depth) {
1504
1584
  const core = this._core;
@@ -1632,8 +1712,9 @@ class WcsDevtools extends HTMLElement {
1632
1712
  }
1633
1713
  else if (this._selectedPath !== null) {
1634
1714
  const selected = this._selectedRoster();
1715
+ // 選択中のツリーでスコープ(複数ツリーの同名パスを混ぜない)
1635
1716
  entries =
1636
- selected !== null ? core.getWiringForPath(selected.name, this._selectedPath) : [];
1717
+ selected !== null ? core.getWiringForPath(this._selectedPath, selected.summary.element) : [];
1637
1718
  contextLabel = this._selectedPath;
1638
1719
  }
1639
1720
  else {
@@ -1701,7 +1782,10 @@ class WcsDevtools extends HTMLElement {
1701
1782
  ? "not observing"
1702
1783
  : `observing since ${sinceLabel} (declared vs measured after this point)`;
1703
1784
  body.append(info);
1704
- const report = core.getCoverageReport();
1785
+ // 選択中のツリーでスコープ(複数ツリーの同名 watch パス / token 名を混ぜない —
1786
+ // wiring のパス文脈と同じ流儀。protocol v2 追補のツリー識別が実測面を分ける)
1787
+ const selected = this._selectedRoster();
1788
+ const report = core.getCoverageReport(selected?.summary.element);
1705
1789
  if (report.length === 0) {
1706
1790
  body.append(this._emptyRow("nothing declared to cover"));
1707
1791
  return;
@@ -1726,7 +1810,7 @@ class WcsDevtools extends HTMLElement {
1726
1810
  }
1727
1811
  const label = document.createElement("span");
1728
1812
  label.className = "path";
1729
- label.textContent = ` ${entry.name}@${entry.stateName} `;
1813
+ label.textContent = ` ${entry.name} `;
1730
1814
  row.append(kind, document.createTextNode(" "), label, status);
1731
1815
  if (entry.note !== null) {
1732
1816
  // note はホバー頼み(title)にせず常時表示する — 前提未成立の理由は
@@ -1752,7 +1836,7 @@ class WcsDevtools extends HTMLElement {
1752
1836
  const path = document.createElement("span");
1753
1837
  path.className = "path";
1754
1838
  const filters = entry.outFilters.map((f) => ` | ${f.filterName}(${f.args.join(",")})`).join("");
1755
- path.textContent = `${entry.statePathName}@${entry.stateName}${filters}`;
1839
+ path.textContent = `${entry.statePathName}${filters}`;
1756
1840
  row.append(type, document.createTextNode(" "), prop, document.createTextNode(" ← "), path);
1757
1841
  if (entry.node !== null) {
1758
1842
  const target = entry.node;
@@ -1781,7 +1865,7 @@ class WcsDevtools extends HTMLElement {
1781
1865
  const arrow = document.createTextNode(" ← ");
1782
1866
  const path = document.createElement("span");
1783
1867
  path.className = "path";
1784
- path.textContent = `${entry.path}@${entry.stateName}`;
1868
+ path.textContent = entry.path;
1785
1869
  const type = document.createElement("span");
1786
1870
  type.className = "badge-tag";
1787
1871
  type.textContent = entry.bindingType;
@@ -1805,7 +1889,7 @@ class WcsDevtools extends HTMLElement {
1805
1889
  prop.textContent = entry.propName;
1806
1890
  const path = document.createElement("span");
1807
1891
  path.className = "path";
1808
- path.textContent = `${entry.path}@${entry.stateName}`;
1892
+ path.textContent = entry.path;
1809
1893
  row.append(type, document.createTextNode(" "), prop, document.createTextNode(" ← "), path);
1810
1894
  row.addEventListener("click", () => {
1811
1895
  this._highlightNodes([entry.element]);
@@ -1870,8 +1954,7 @@ class WcsDevtools extends HTMLElement {
1870
1954
  }
1871
1955
  const label = document.createElement("span");
1872
1956
  label.className = "label";
1873
- const stateName = entry.stateName !== null ? `@${entry.stateName}` : "";
1874
- label.textContent = ` ${entry.label}${stateName} `;
1957
+ label.textContent = ` ${entry.label} `;
1875
1958
  const detail = document.createElement("span");
1876
1959
  detail.className = "detail";
1877
1960
  detail.textContent = entry.detail;
@@ -1888,7 +1971,7 @@ class WcsDevtools extends HTMLElement {
1888
1971
  _highlightPath(entry, path) {
1889
1972
  const core = this._core;
1890
1973
  const nodes = [];
1891
- for (const wiring of core.getWiringForPath(entry.name, path)) {
1974
+ for (const wiring of core.getWiringForPath(path, entry.summary.element)) {
1892
1975
  const binding = wiring.bindingRef.deref();
1893
1976
  if (binding !== undefined) {
1894
1977
  nodes.push(binding.node, binding.replaceNode);
@@ -1954,5 +2037,5 @@ function bootstrapDevtools() {
1954
2037
  }
1955
2038
  }
1956
2039
 
1957
- export { DEVTOOLS_HOOK_GLOBAL, DEVTOOLS_PROTOCOL_VERSION, DevtoolsCore, RESERVED_STATE_NAME_PREFIX, WcsDevtools, bootstrapDevtools, formatArgs, formatValue, getOrCreateHookRegistry, scanDeclaredBindings };
2040
+ export { DEVTOOLS_HOOK_GLOBAL, DEVTOOLS_PROTOCOL_VERSION, DevtoolsCore, WcsDevtools, bootstrapDevtools, formatArgs, formatValue, getOrCreateHookRegistry, scanDeclaredBindings };
1958
2041
  //# sourceMappingURL=index.esm.js.map