@wcstack/devtools 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -72,6 +72,29 @@ interface IStateElementSummaryLike {
72
72
  */
73
73
  readonly keyedListPaths?: ReadonlySet<string> | null;
74
74
  }
75
+ /**
76
+ * マウント記録 1 件の要約(overlays の要素 — protocol v2・D20 の可視化)。
77
+ * マウントの私有キー・getter はオーバーレイ専用アドレス空間(マーカー `#m<id>`)に
78
+ * 住み、状態ツリーの keys/read には現れないため、これが唯一の可視面になる。
79
+ */
80
+ interface IMountOverlaySummaryLike {
81
+ /** D20 の予約セグメント(`#m<id>`) */
82
+ readonly marker: string;
83
+ /** マウントされたコンポーネントのタグ名(小文字) */
84
+ readonly componentTag: string;
85
+ readonly stateProp: string;
86
+ /** マウント表: 内側接頭辞(空 = ルートエントリ)→ 外側パス */
87
+ readonly mountTable: readonly {
88
+ readonly inner: string;
89
+ readonly outer: string;
90
+ }[];
91
+ /** `$n` 補正の Δ(ルート接頭辞のワイルドカード数) */
92
+ readonly delta: number;
93
+ /** 私有キー(オーバーレイ空間に住む own data key) */
94
+ readonly privateKeys: readonly string[];
95
+ /** マーカーパスに載る getter のキー */
96
+ readonly getterKeys: readonly string[];
97
+ }
75
98
  /**
76
99
  * 宣言レベルのバインディング 1 件(getDeclaredBindings の要素・protocol v1 追補)。
77
100
  * ランタイム正本パーサの結果が構造的に流れる。宣言タプルで dedupe 済みの
@@ -128,6 +151,12 @@ type DevtoolsEventLike = {
128
151
  readonly tokenName: string;
129
152
  readonly args: readonly unknown[];
130
153
  readonly subscriberCount: number;
154
+ /**
155
+ * 発火元ツリーの state 要素(protocol v2 追補 2026-09-05・additive)。
156
+ * 旧ランタイム・registry 非経由の token の payload には無い(optional)—
157
+ * 無い emit の実測はツリー別に分けられないため、全ツリーの照会へ合算で残す。
158
+ */
159
+ readonly stateElement?: unknown;
131
160
  } | {
132
161
  readonly type: "state:watch-error";
133
162
  readonly phase: "prime" | "evaluate" | "handler";
@@ -140,6 +169,13 @@ type DevtoolsEventLike = {
140
169
  } | {
141
170
  readonly type: "state:watch-fired";
142
171
  readonly path: string;
172
+ /**
173
+ * 発火元ツリーの state 要素(protocol v2 追補 2026-09-05・additive)。
174
+ * 複数ツリーが同名 watch パスを宣言するページで実測台帳をツリー別に分ける。
175
+ * 旧ランタイムの payload には無い(optional)— 無い発火は全ツリーの照会へ
176
+ * 合算で残す。
177
+ */
178
+ readonly stateElement?: unknown;
143
179
  } | {
144
180
  readonly type: "state:path-unresolved";
145
181
  readonly source: "binding" | "watch";
@@ -190,6 +226,12 @@ interface IDevtoolsSourceLike {
190
226
  getStateElements(): IStateElementSummaryLike[];
191
227
  /** protocol v1 追補 API。古いランタイムには無い可能性があるため optional 扱いで呼ぶ */
192
228
  keys?(rootNode: Node): string[];
229
+ /**
230
+ * protocol v2 API(optional 扱いで呼ぶ)。rootNode のツリーに載っている
231
+ * マウント記録の列挙(D20 の可視化)。マウントが無ければ空配列。
232
+ * v2 より前のランタイムには無いため、無ければ UI はセクションごと出さない。
233
+ */
234
+ overlays?(rootNode: Node): IMountOverlaySummaryLike[];
193
235
  read(rootNode: Node, path: string, indexes?: number[]): unknown;
194
236
  write(rootNode: Node, path: string, value: unknown, indexes?: number[]): void;
195
237
  /**
@@ -295,13 +337,14 @@ declare class DevtoolsCore {
295
337
  private _changeListeners;
296
338
  /** 観測開始時刻(connect 時の performance.now。未接続は null)。 */
297
339
  private _observingSince;
298
- /** watch 発火回数(path → count)。カバレッジの実測面。
299
- * 既知の限界: `state:watch-fired` / `state:token-emit` の payload はツリー識別を
300
- * 持たないため、複数ツリーが同名の watch パス / token 名を宣言するページでは
301
- * 実測が合算される(ツリー別に分けるにはプロトコル追補が要る — slice 29 記録)。 */
302
- private _watchFiredCounts;
303
- /** token emit 実測(kind + NUL + name → 回数と空撃ち回数)。上の限界注記と同じ。 */
304
- private _tokenEmitCounts;
340
+ /** watch 発火実測(path → ツリー別区分列)。カバレッジの実測面。
341
+ * protocol v2 追補で payload がツリー識別(stateElement)を持つようになり、
342
+ * 複数ツリーが同名 watch パスを宣言するページでもツリー別に数えられる。
343
+ * 識別の無い旧 payload は null 区分に積み、どのツリーの照会にも合算で残す
344
+ * (slice 29 non-blocking 記録の解消)。 */
345
+ private _watchFiredStats;
346
+ /** token emit 実測(kind + NUL + name → ツリー別区分列)。上の追補注記と同じ。 */
347
+ private _tokenEmitStats;
305
348
  /** 観測開始以降に一度でも attach を観測した宣言キー(attachKeyOf)。
306
349
  * live 配線台帳は WeakRef pruning / binding-removed で縮むため、binding
307
350
  * カバレッジの attached 判定はこちらで行う(瞬間値で判定すると行の
@@ -345,14 +388,40 @@ declare class DevtoolsCore {
345
388
  * 配線カバレッジ(§4): 宣言面(watchPaths / token 宣言 / canonical declared)と
346
389
  * 実測面(watch-fired・token-emit の台帳・live 配線台帳)の突合。
347
390
  * 「観測開始以降」の実測であることは observingSince を UI が常時表示して明示する。
391
+ *
392
+ * watch / token の実測は各行の属するツリー(roster entry の element)でスコープする
393
+ * (protocol v2 追補 — 複数ツリーの同名宣言を合算しない)。ツリー識別の無い
394
+ * 旧 payload 由来の実測はどの行にも合算で残す(欠測を欠落にしない)。
395
+ * stateElement を渡すとそのツリーの宣言行だけに絞る(getWiringForPath と同じ流儀)。
396
+ * binding 節(canonical declared × ever-attach)は宣言・実測ともツリー識別を
397
+ * 持たないため絞り込みの対象外。
348
398
  */
349
- getCoverageReport(): ICoverageEntry[];
399
+ getCoverageReport(stateElement?: unknown): ICoverageEntry[];
350
400
  /** roster entry の state からトップレベルキーを列挙(keys 未実装ランタイムは空) */
351
401
  keysOf(entry: IRosterEntry): string[];
402
+ /**
403
+ * roster entry のツリーに載るマウント記録(overlays — protocol v2・D20 の可視化)。
404
+ * overlays を実装しないランタイム(v2 より前)では null — UI はセクションごと
405
+ * 出さない(後方互換)。マウントが無いツリーは空配列。
406
+ */
407
+ overlaysOf(entry: IRosterEntry): IMountOverlaySummaryLike[] | null;
352
408
  readValue(entry: IRosterEntry, path: string, indexes?: number[]): unknown;
353
409
  writeValue(entry: IRosterEntry, path: string, value: unknown, indexes?: number[]): void;
354
410
  private _notify;
355
411
  private _refreshRosterOf;
412
+ /**
413
+ * key の実測台帳から stateElement 区分の stat を探すか作る(ingest 用)。
414
+ * payload の stateElement がオブジェクトでない(旧 payload / 直接生成 token)
415
+ * ときは null 区分 = 全ツリー合算面に積む。
416
+ */
417
+ private _statFor;
418
+ /**
419
+ * key の実測を element のツリーへスコープして集計する(照会用)。
420
+ * null 区分(ツリー識別の無い旧 payload)は常に合算に含める。GC 済みツリーの
421
+ * 区分は遅延剪定(wiring 台帳の WeakRef pruning と同じ)。実測ゼロは undefined
422
+ * (= 宣言だけあって一度も観測していない)に畳む。
423
+ */
424
+ private _measuredFor;
356
425
  private _collectAlive;
357
426
  private _appendTimeline;
358
427
  private _labelOf;
@@ -415,6 +484,18 @@ declare class WcsDevtools extends HTMLElement {
415
484
  private _rosterKey;
416
485
  private _selectedRoster;
417
486
  private _renderStatePane;
487
+ /**
488
+ * 選択ツリーのマウント記録セクション(overlays — protocol v2・D20 の可視化)。
489
+ * マウントの私有キー・getter はオーバーレイ専用アドレス空間(マーカー `#m<id>`)に
490
+ * 住み、上の状態ツリー(keys/read)には現れないため、ここが唯一の可視面。
491
+ * overlays 未提供の旧ランタイム(null)ではセクションごと出さない(後方互換)。
492
+ * マウント無し(空配列)も見出しだけ残さない。pull は State ペインの再描画
493
+ * (roster / sources 変更・ツリー切替)にそのまま乗る — 専用 polling は足さない
494
+ * (観測者効果の排除、devtools-tag-design.md の rAF 合流と同じ姿勢)。
495
+ */
496
+ private _renderOverlaysSection;
497
+ /** マウント記録 1 件の描画(マーカー・コンポーネント・マウント表・私有面)。 */
498
+ private _overlayRow;
418
499
  private _renderTreeNode;
419
500
  private _beginEdit;
420
501
  private _renderWiringPane;
package/dist/index.esm.js CHANGED
@@ -266,13 +266,14 @@ class DevtoolsCore {
266
266
  _changeListeners = new Set();
267
267
  /** 観測開始時刻(connect 時の performance.now。未接続は null)。 */
268
268
  _observingSince = null;
269
- /** watch 発火回数(path → count)。カバレッジの実測面。
270
- * 既知の限界: `state:watch-fired` / `state:token-emit` の payload はツリー識別を
271
- * 持たないため、複数ツリーが同名の watch パス / token 名を宣言するページでは
272
- * 実測が合算される(ツリー別に分けるにはプロトコル追補が要る — slice 29 記録)。 */
273
- _watchFiredCounts = new Map();
274
- /** token emit 実測(kind + NUL + name → 回数と空撃ち回数)。上の限界注記と同じ。 */
275
- _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();
276
277
  /** 観測開始以降に一度でも attach を観測した宣言キー(attachKeyOf)。
277
278
  * live 配線台帳は WeakRef pruning / binding-removed で縮むため、binding
278
279
  * カバレッジの attached 判定はこちらで行う(瞬間値で判定すると行の
@@ -327,8 +328,8 @@ class DevtoolsCore {
327
328
  this._wiringByPathKey.clear();
328
329
  this._wiringEntryByBinding = new WeakMap();
329
330
  this._observingSince = null;
330
- this._watchFiredCounts.clear();
331
- this._tokenEmitCounts.clear();
331
+ this._watchFiredStats.clear();
332
+ this._tokenEmitStats.clear();
332
333
  this._everAttachedKeys.clear();
333
334
  this._notify("sources");
334
335
  this._notify("roster");
@@ -448,14 +449,24 @@ class DevtoolsCore {
448
449
  * 配線カバレッジ(§4): 宣言面(watchPaths / token 宣言 / canonical declared)と
449
450
  * 実測面(watch-fired・token-emit の台帳・live 配線台帳)の突合。
450
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
+ * 持たないため絞り込みの対象外。
451
459
  */
452
- getCoverageReport() {
460
+ getCoverageReport(stateElement) {
453
461
  const out = [];
454
462
  for (const entry of this.getRoster()) {
455
463
  const summary = entry.summary;
456
- // --- watch: 宣言(watchPaths)× 実測(watch-fired 台帳) ---
464
+ if (stateElement !== undefined && summary.element !== stateElement) {
465
+ continue;
466
+ }
467
+ // --- watch: 宣言(watchPaths)× 実測(watch-fired 台帳・自ツリー + 旧 payload 合算) ---
457
468
  for (const path of summary.watchPaths ?? []) {
458
- const count = this._watchFiredCounts.get(pathKeyOf(path)) ?? 0;
469
+ const count = this._measuredFor(this._watchFiredStats, pathKeyOf(path), summary.element)?.count ?? 0;
459
470
  if (count > 0) {
460
471
  out.push({ kind: "watch", name: path, status: "fired", count, note: null });
461
472
  continue;
@@ -494,10 +505,10 @@ class DevtoolsCore {
494
505
  // subscriberCount 0 = 空撃ち(§4): 全 emit が空撃ちなら emitted-unheard として
495
506
  // 警告する(emit 側だけ配線されて受け手が一つも居ない構成ミスの検出)。 ---
496
507
  for (const name of summary.commandTokenNames) {
497
- out.push(tokenCoverageOf("command", name, this._tokenEmitCounts.get(tokenKeyOf("command", name))));
508
+ out.push(tokenCoverageOf("command", name, this._measuredFor(this._tokenEmitStats, tokenKeyOf("command", name), summary.element)));
498
509
  }
499
510
  for (const name of summary.eventTokenNames) {
500
- out.push(tokenCoverageOf("eventToken", name, this._tokenEmitCounts.get(tokenKeyOf("event", name))));
511
+ out.push(tokenCoverageOf("eventToken", name, this._measuredFor(this._tokenEmitStats, tokenKeyOf("event", name), summary.element)));
501
512
  }
502
513
  }
503
514
  // --- binding: canonical declared(宣言)× live 配線台帳(実測)。canonical が
@@ -538,6 +549,18 @@ class DevtoolsCore {
538
549
  }
539
550
  return source.keys(entry.rootNode);
540
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);
563
+ }
541
564
  readValue(entry, path, indexes) {
542
565
  const source = this._sources.get(entry.sourceId);
543
566
  if (source === undefined) {
@@ -574,6 +597,60 @@ class DevtoolsCore {
574
597
  }
575
598
  this._roster.set(source.id, entries);
576
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
+ }
577
654
  _collectAlive(set) {
578
655
  const alive = [];
579
656
  for (const entry of set) {
@@ -726,20 +803,13 @@ class DevtoolsCore {
726
803
  return;
727
804
  }
728
805
  case "state:token-emit": {
729
- // カバレッジの実測面: emit 回数と空撃ち回数を台帳に積む(§4)。
730
- const emitKey = tokenKeyOf(event.kind, event.tokenName);
731
- const stat = this._tokenEmitCounts.get(emitKey);
732
- if (stat === undefined) {
733
- this._tokenEmitCounts.set(emitKey, {
734
- count: 1,
735
- zeroSubscriberCount: event.subscriberCount === 0 ? 1 : 0,
736
- });
737
- }
738
- else {
739
- stat.count += 1;
740
- if (event.subscriberCount === 0) {
741
- stat.zeroSubscriberCount += 1;
742
- }
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;
743
813
  }
744
814
  this._notify("coverage");
745
815
  this._appendTimeline({
@@ -753,8 +823,8 @@ class DevtoolsCore {
753
823
  }
754
824
  case "state:watch-fired": {
755
825
  // timeline 行にはしない(発火は高頻度になりうる活動でありカバレッジが受け皿)。
756
- const firedKey = pathKeyOf(event.path);
757
- 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;
758
828
  this._notify("coverage");
759
829
  return;
760
830
  }
@@ -1056,6 +1126,11 @@ header .spacer { flex: 1; }
1056
1126
  .notice { color: #efe3a0; padding: 2px 0 6px; }
1057
1127
  .notice button { font: inherit; margin-left: 6px; cursor: pointer; }
1058
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; }
1059
1134
  .wiring-tabs { padding: 0 0 4px; }
1060
1135
  .wiring-tabs button { font: inherit; cursor: pointer; margin-right: 4px; opacity: 0.6; }
1061
1136
  .wiring-tabs button.active { opacity: 1; font-weight: bold; }
@@ -1437,11 +1512,73 @@ class WcsDevtools extends HTMLElement {
1437
1512
  const keys = core.keysOf(selected);
1438
1513
  if (keys.length === 0) {
1439
1514
  body.append(this._emptyRow("no readable keys (runtime without keys() API?)"));
1515
+ // keys が読めないランタイムでも overlays は独立に読める可能性があるため出す
1516
+ this._renderOverlaysSection(body, selected);
1440
1517
  return;
1441
1518
  }
1442
1519
  for (const key of keys) {
1443
1520
  this._renderTreeNode(body, selected, { path: key, indexes: [] }, key, 0);
1444
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;
1445
1582
  }
1446
1583
  _renderTreeNode(container, entry, ref, label, depth) {
1447
1584
  const core = this._core;
@@ -1645,7 +1782,10 @@ class WcsDevtools extends HTMLElement {
1645
1782
  ? "not observing"
1646
1783
  : `observing since ${sinceLabel} (declared vs measured after this point)`;
1647
1784
  body.append(info);
1648
- const report = core.getCoverageReport();
1785
+ // 選択中のツリーでスコープ(複数ツリーの同名 watch パス / token 名を混ぜない —
1786
+ // wiring のパス文脈と同じ流儀。protocol v2 追補のツリー識別が実測面を分ける)
1787
+ const selected = this._selectedRoster();
1788
+ const report = core.getCoverageReport(selected?.summary.element);
1649
1789
  if (report.length === 0) {
1650
1790
  body.append(this._emptyRow("nothing declared to cover"));
1651
1791
  return;