@wcstack/devtools 1.28.0 → 1.30.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/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.esm.js +335 -7
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,42 @@ interface IStateElementSummaryLike {
|
|
|
60
60
|
readonly eventTokenNames: ReadonlySet<string>;
|
|
61
61
|
readonly staticDependency: ReadonlyMap<string, readonly string[]>;
|
|
62
62
|
readonly dynamicDependency: ReadonlyMap<string, readonly string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* `$watch` の宣言パス集合(protocol v1 追補・配線カバレッジの宣言面)。
|
|
65
|
+
* 旧ランタイムにはフィールド自体が無いため optional。宣言なしは null。
|
|
66
|
+
*/
|
|
67
|
+
readonly watchPaths?: ReadonlySet<string> | null;
|
|
68
|
+
/**
|
|
69
|
+
* `$listKeys` で宣言されたリストパス集合(protocol v1 追補)。ワイルドカード行
|
|
70
|
+
* watch に**リスト書き込み**が届く前提は「for バインド(paths.list)or
|
|
71
|
+
* $listKeys 宣言」なので、前提判定の正確化に paths.list と対で使う
|
|
72
|
+
*(明示 index 書き込みは前提に依らず発火し得る)。旧ランタイムにはフィールド自体が
|
|
73
|
+
* 無いため optional(undefined = $listKeys 側が観測不能)。宣言なしは null。
|
|
74
|
+
*/
|
|
75
|
+
readonly keyedListPaths?: ReadonlySet<string> | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 宣言レベルのバインディング 1 件(getDeclaredBindings の要素・protocol v1 追補)。
|
|
79
|
+
* ランタイム正本パーサの結果が構造的に流れる。宣言タプルで dedupe 済みの
|
|
80
|
+
* 「宣言の集合」であり、レンダリング行数に比例したインスタンス列ではない。
|
|
81
|
+
*/
|
|
82
|
+
interface IDeclaredBindingLike {
|
|
83
|
+
/** 代表ノード(fragment 由来 = 構造テンプレート内部は null)。 */
|
|
84
|
+
readonly node: Node | null;
|
|
85
|
+
readonly propName: string;
|
|
86
|
+
readonly statePathName: string;
|
|
87
|
+
readonly stateName: string;
|
|
88
|
+
readonly bindingType: string;
|
|
89
|
+
readonly inFilters: readonly {
|
|
90
|
+
readonly filterName: string;
|
|
91
|
+
readonly args: readonly string[];
|
|
92
|
+
}[];
|
|
93
|
+
readonly outFilters: readonly {
|
|
94
|
+
readonly filterName: string;
|
|
95
|
+
readonly args: readonly string[];
|
|
96
|
+
}[];
|
|
97
|
+
readonly origin: "attribute" | "comment" | "fragment";
|
|
98
|
+
readonly raw: string;
|
|
63
99
|
}
|
|
64
100
|
type DevtoolsEventLike = {
|
|
65
101
|
readonly type: "state:element-registered";
|
|
@@ -108,6 +144,10 @@ type DevtoolsEventLike = {
|
|
|
108
144
|
readonly type: "state:watch-chain-limit";
|
|
109
145
|
readonly maxDepth: number;
|
|
110
146
|
readonly paths: readonly string[];
|
|
147
|
+
} | {
|
|
148
|
+
readonly type: "state:watch-fired";
|
|
149
|
+
readonly stateName: string;
|
|
150
|
+
readonly path: string;
|
|
111
151
|
} | {
|
|
112
152
|
readonly type: "propagation:suppressed";
|
|
113
153
|
readonly reason: "confirmation" | "visited-edge";
|
|
@@ -150,6 +190,11 @@ interface IDevtoolsSourceLike {
|
|
|
150
190
|
keys?(name: string, rootNode: Node): string[];
|
|
151
191
|
read(name: string, rootNode: Node, path: string, indexes?: number[]): unknown;
|
|
152
192
|
write(name: string, rootNode: Node, path: string, value: unknown, indexes?: number[]): void;
|
|
193
|
+
/**
|
|
194
|
+
* protocol v1 追補 API(optional 扱いで呼ぶ)。ランタイム正本パーサによる
|
|
195
|
+
* 宣言レベルバインディングの集合(declaredScan の簡易パーサを置き換える正本)。
|
|
196
|
+
*/
|
|
197
|
+
getDeclaredBindings?(rootNode: Node): IDeclaredBindingLike[];
|
|
153
198
|
_setSink(sink: DevtoolsSinkLike | null): void;
|
|
154
199
|
}
|
|
155
200
|
interface IDevtoolsListenerLike {
|
|
@@ -207,8 +252,28 @@ interface IWiringEntry {
|
|
|
207
252
|
readonly bindingType: string;
|
|
208
253
|
readonly bindingRef: WeakRef<IBindingLike>;
|
|
209
254
|
}
|
|
210
|
-
type CoreChangeKind = "sources" | "roster" | "wiring" | "timeline";
|
|
255
|
+
type CoreChangeKind = "sources" | "roster" | "wiring" | "timeline" | "coverage";
|
|
211
256
|
type CoreChangeListener = (kind: CoreChangeKind) => void;
|
|
257
|
+
/**
|
|
258
|
+
* 配線カバレッジ 1 行(static-wiring-dx-design.md §4 — 宣言 × 実測の突合)。
|
|
259
|
+
* - watch: `fired`(count 回)/ `never` / `prerequisite-missing`
|
|
260
|
+
* (ワイルドカード行 watch は「for バインド or $listKeys 宣言」が無いと
|
|
261
|
+
* リスト書き込みが行へ届かない — 「未発火」と区別しないと誤警告になる)
|
|
262
|
+
* - command / eventToken: `emitted`(count 回)/ `never` /
|
|
263
|
+
* `emitted-unheard`(全 emit が subscriberCount 0 = 空撃ち。§4 の突合対象)
|
|
264
|
+
* - binding: canonical declared がある場合のみ。`attached` / `never-attached`
|
|
265
|
+
* (attached は「観測開始以降に一度でも attach された」— live 台帳は WeakRef
|
|
266
|
+
* pruning で縮むため、瞬間値で判定すると attached が never-attached へ戻る)
|
|
267
|
+
*/
|
|
268
|
+
type CoverageStatus = "fired" | "emitted" | "emitted-unheard" | "attached" | "never" | "prerequisite-missing" | "never-attached";
|
|
269
|
+
interface ICoverageEntry {
|
|
270
|
+
readonly stateName: string;
|
|
271
|
+
readonly kind: "watch" | "command" | "eventToken" | "binding";
|
|
272
|
+
readonly name: string;
|
|
273
|
+
readonly status: CoverageStatus;
|
|
274
|
+
readonly count: number;
|
|
275
|
+
readonly note: string | null;
|
|
276
|
+
}
|
|
212
277
|
interface IDevtoolsCoreOptions {
|
|
213
278
|
/** タイムライン ring buffer 件数(既定 500) */
|
|
214
279
|
timelineCapacity?: number;
|
|
@@ -227,6 +292,17 @@ declare class DevtoolsCore {
|
|
|
227
292
|
private _seq;
|
|
228
293
|
private _paused;
|
|
229
294
|
private _changeListeners;
|
|
295
|
+
/** 観測開始時刻(connect 時の performance.now。未接続は null)。 */
|
|
296
|
+
private _observingSince;
|
|
297
|
+
/** watch 発火回数(stateName + NUL + path → count)。カバレッジの実測面。 */
|
|
298
|
+
private _watchFiredCounts;
|
|
299
|
+
/** token emit 実測(stateName + NUL + kind + NUL + name → 回数と空撃ち回数)。 */
|
|
300
|
+
private _tokenEmitCounts;
|
|
301
|
+
/** 観測開始以降に一度でも attach を観測した宣言キー(attachKeyOf)。
|
|
302
|
+
* live 配線台帳は WeakRef pruning / binding-removed で縮むため、binding
|
|
303
|
+
* カバレッジの attached 判定はこちらで行う(瞬間値で判定すると行の
|
|
304
|
+
* 破棄・GC のたびに attached が never-attached へ逆戻りする)。 */
|
|
305
|
+
private _everAttachedKeys;
|
|
230
306
|
constructor(options?: IDevtoolsCoreOptions);
|
|
231
307
|
get connected(): boolean;
|
|
232
308
|
get paused(): boolean;
|
|
@@ -236,6 +312,8 @@ declare class DevtoolsCore {
|
|
|
236
312
|
connect(): void;
|
|
237
313
|
/** 購読解除 + 台帳クリア(タイムラインは保持。protocol §7-2 の残留ゼロ) */
|
|
238
314
|
disconnect(): void;
|
|
315
|
+
/** 観測開始時刻(performance.now 基準。未接続は null)。 */
|
|
316
|
+
get observingSince(): number | null;
|
|
239
317
|
onChange(listener: CoreChangeListener): () => void;
|
|
240
318
|
getSources(): IDevtoolsSourceLike[];
|
|
241
319
|
getRoster(): IRosterEntry[];
|
|
@@ -249,6 +327,19 @@ declare class DevtoolsCore {
|
|
|
249
327
|
getAllWiring(): IWiringEntry[];
|
|
250
328
|
/** 指定ノード(またはその子孫のバインドノード)に載る配線 */
|
|
251
329
|
getWiringForNode(node: Node): IWiringEntry[];
|
|
330
|
+
/**
|
|
331
|
+
* ランタイム正本パーサによる宣言バインディング集合(protocol v1 追補)。
|
|
332
|
+
* getDeclaredBindings を実装した source が 1 つも無ければ null(消費側は
|
|
333
|
+
* declaredScan の簡易パーサへフォールバックする)。root は roster の rootNode
|
|
334
|
+
* を source ごとに重複排除して渡す。
|
|
335
|
+
*/
|
|
336
|
+
getCanonicalDeclared(): IDeclaredBindingLike[] | null;
|
|
337
|
+
/**
|
|
338
|
+
* 配線カバレッジ(§4): 宣言面(watchPaths / token 宣言 / canonical declared)と
|
|
339
|
+
* 実測面(watch-fired・token-emit の台帳・live 配線台帳)の突合。
|
|
340
|
+
* 「観測開始以降」の実測であることは observingSince を UI が常時表示して明示する。
|
|
341
|
+
*/
|
|
342
|
+
getCoverageReport(): ICoverageEntry[];
|
|
252
343
|
/** roster entry の state からトップレベルキーを列挙(keys 未実装ランタイムは空) */
|
|
253
344
|
keysOf(entry: IRosterEntry): string[];
|
|
254
345
|
readValue(entry: IRosterEntry, path: string, indexes?: number[]): unknown;
|
|
@@ -284,6 +375,8 @@ declare class WcsDevtools extends HTMLElement {
|
|
|
284
375
|
private _badge;
|
|
285
376
|
private _stateSelect;
|
|
286
377
|
private _paneElements;
|
|
378
|
+
/** Wiring ペインの表示モード(配線一覧 / カバレッジ突合)。 */
|
|
379
|
+
private _wiringView;
|
|
287
380
|
private _highlightLayer;
|
|
288
381
|
private _dirtyPanes;
|
|
289
382
|
private _renderScheduled;
|
|
@@ -318,6 +411,14 @@ declare class WcsDevtools extends HTMLElement {
|
|
|
318
411
|
private _renderTreeNode;
|
|
319
412
|
private _beginEdit;
|
|
320
413
|
private _renderWiringPane;
|
|
414
|
+
/**
|
|
415
|
+
* カバレッジビュー: 宣言(watchPaths / token 宣言 / canonical declared)×
|
|
416
|
+
* 実測(発火・emit・attach)の突合表。観測開始以降の実測であることを常時明示する
|
|
417
|
+
* (protocol §6 — 台帳の過去は再構成できない)。
|
|
418
|
+
*/
|
|
419
|
+
private _renderCoverageView;
|
|
420
|
+
/** canonical declared(正本パーサ由来・宣言集合)の 1 行。 */
|
|
421
|
+
private _canonicalDeclaredRow;
|
|
321
422
|
private _scanRootOf;
|
|
322
423
|
private _wiringRow;
|
|
323
424
|
private _declaredRow;
|
package/dist/index.esm.js
CHANGED
|
@@ -213,8 +213,37 @@ function formatArgs(args) {
|
|
|
213
213
|
/** 予約 state 名 prefix(protocol §5)。この prefix の要素・イベントは常に除外 */
|
|
214
214
|
const RESERVED_STATE_NAME_PREFIX = "wcs-devtools";
|
|
215
215
|
const DEFAULT_TIMELINE_CAPACITY = 500;
|
|
216
|
+
/** 台帳キーの区切り文字(state 名・パスに現れ得ない NUL)。
|
|
217
|
+
* エスケープ表記でなく生成式にしているのは、ツーリングがソース中の
|
|
218
|
+
* `エスケープ列` リテラルを生バイトへ壊す事故が実際に起きたため。 */
|
|
219
|
+
const NUL = String.fromCharCode(0);
|
|
216
220
|
function pathKeyOf(stateName, path) {
|
|
217
|
-
return stateName +
|
|
221
|
+
return stateName + NUL + path;
|
|
222
|
+
}
|
|
223
|
+
function tokenKeyOf(stateName, kind, name) {
|
|
224
|
+
return stateName + NUL + kind + NUL + name;
|
|
225
|
+
}
|
|
226
|
+
/** 宣言(stateName+path+propName)単位の attach キー。NUL 区切りは pathKeyOf と同じ理由 */
|
|
227
|
+
function attachKeyOf(stateName, path, propName) {
|
|
228
|
+
return pathKeyOf(stateName, path) + NUL + propName;
|
|
229
|
+
}
|
|
230
|
+
/** token カバレッジ 1 行の判定(§4: 空撃ちのみ / 一部空撃ち / 正常 / 未発火) */
|
|
231
|
+
function tokenCoverageOf(stateName, kind, name, stat) {
|
|
232
|
+
if (stat === undefined) {
|
|
233
|
+
return { stateName, kind, name, status: "never", count: 0, note: null };
|
|
234
|
+
}
|
|
235
|
+
if (stat.zeroSubscriberCount === stat.count) {
|
|
236
|
+
return {
|
|
237
|
+
stateName, kind, name, status: "emitted-unheard", count: stat.count,
|
|
238
|
+
note: `all ${stat.count} emit(s) had 0 subscribers`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
stateName, kind, name, status: "emitted", count: stat.count,
|
|
243
|
+
note: stat.zeroSubscriberCount > 0
|
|
244
|
+
? `${stat.zeroSubscriberCount}/${stat.count} emit(s) had 0 subscribers`
|
|
245
|
+
: null,
|
|
246
|
+
};
|
|
218
247
|
}
|
|
219
248
|
class DevtoolsCore {
|
|
220
249
|
_timelineCapacity;
|
|
@@ -228,6 +257,17 @@ class DevtoolsCore {
|
|
|
228
257
|
_seq = 0;
|
|
229
258
|
_paused = false;
|
|
230
259
|
_changeListeners = new Set();
|
|
260
|
+
/** 観測開始時刻(connect 時の performance.now。未接続は null)。 */
|
|
261
|
+
_observingSince = null;
|
|
262
|
+
/** watch 発火回数(stateName + NUL + path → count)。カバレッジの実測面。 */
|
|
263
|
+
_watchFiredCounts = new Map();
|
|
264
|
+
/** token emit 実測(stateName + NUL + kind + NUL + name → 回数と空撃ち回数)。 */
|
|
265
|
+
_tokenEmitCounts = new Map();
|
|
266
|
+
/** 観測開始以降に一度でも attach を観測した宣言キー(attachKeyOf)。
|
|
267
|
+
* live 配線台帳は WeakRef pruning / binding-removed で縮むため、binding
|
|
268
|
+
* カバレッジの attached 判定はこちらで行う(瞬間値で判定すると行の
|
|
269
|
+
* 破棄・GC のたびに attached が never-attached へ逆戻りする)。 */
|
|
270
|
+
_everAttachedKeys = new Set();
|
|
231
271
|
constructor(options) {
|
|
232
272
|
this._timelineCapacity = options?.timelineCapacity ?? DEFAULT_TIMELINE_CAPACITY;
|
|
233
273
|
this._hiddenStateNames = new Set(options?.hiddenStateNames ?? []);
|
|
@@ -252,6 +292,9 @@ class DevtoolsCore {
|
|
|
252
292
|
if (this._removeListener !== null) {
|
|
253
293
|
return;
|
|
254
294
|
}
|
|
295
|
+
// カバレッジは「観測開始以降」の実測。台帳の過去は再構成できない(protocol §6)
|
|
296
|
+
// ため、UI はこの時刻を常時表示して非対称を明示する。
|
|
297
|
+
this._observingSince = performance.now();
|
|
255
298
|
const registry = getOrCreateHookRegistry();
|
|
256
299
|
this._removeListener = registry.addListener({
|
|
257
300
|
onSourceRegistered: (source) => {
|
|
@@ -281,9 +324,18 @@ class DevtoolsCore {
|
|
|
281
324
|
this._roster.clear();
|
|
282
325
|
this._wiringByPathKey.clear();
|
|
283
326
|
this._wiringEntryByBinding = new WeakMap();
|
|
327
|
+
this._observingSince = null;
|
|
328
|
+
this._watchFiredCounts.clear();
|
|
329
|
+
this._tokenEmitCounts.clear();
|
|
330
|
+
this._everAttachedKeys.clear();
|
|
284
331
|
this._notify("sources");
|
|
285
332
|
this._notify("roster");
|
|
286
333
|
this._notify("wiring");
|
|
334
|
+
this._notify("coverage");
|
|
335
|
+
}
|
|
336
|
+
/** 観測開始時刻(performance.now 基準。未接続は null)。 */
|
|
337
|
+
get observingSince() {
|
|
338
|
+
return this._observingSince;
|
|
287
339
|
}
|
|
288
340
|
onChange(listener) {
|
|
289
341
|
this._changeListeners.add(listener);
|
|
@@ -347,6 +399,129 @@ class DevtoolsCore {
|
|
|
347
399
|
}
|
|
348
400
|
return result;
|
|
349
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* ランタイム正本パーサによる宣言バインディング集合(protocol v1 追補)。
|
|
404
|
+
* getDeclaredBindings を実装した source が 1 つも無ければ null(消費側は
|
|
405
|
+
* declaredScan の簡易パーサへフォールバックする)。root は roster の rootNode
|
|
406
|
+
* を source ごとに重複排除して渡す。
|
|
407
|
+
*/
|
|
408
|
+
getCanonicalDeclared() {
|
|
409
|
+
let supported = false;
|
|
410
|
+
const out = [];
|
|
411
|
+
// source をまたぐ重複(同じ root を複数 source が走査した場合)を宣言タプルで排除
|
|
412
|
+
const seen = new Set();
|
|
413
|
+
for (const [sourceId, source] of this._sources) {
|
|
414
|
+
if (typeof source.getDeclaredBindings !== "function") {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
supported = true;
|
|
418
|
+
const roots = new Set();
|
|
419
|
+
for (const entry of this._roster.get(sourceId) ?? []) {
|
|
420
|
+
roots.add(entry.rootNode);
|
|
421
|
+
}
|
|
422
|
+
for (const root of roots) {
|
|
423
|
+
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,
|
|
428
|
+
declared.bindingType, declared.origin, declared.raw].join(NUL);
|
|
429
|
+
if (seen.has(key)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
seen.add(key);
|
|
433
|
+
out.push(declared);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return supported ? out : null;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 配線カバレッジ(§4): 宣言面(watchPaths / token 宣言 / canonical declared)と
|
|
441
|
+
* 実測面(watch-fired・token-emit の台帳・live 配線台帳)の突合。
|
|
442
|
+
* 「観測開始以降」の実測であることは observingSince を UI が常時表示して明示する。
|
|
443
|
+
*/
|
|
444
|
+
getCoverageReport() {
|
|
445
|
+
const out = [];
|
|
446
|
+
for (const entry of this.getRoster()) {
|
|
447
|
+
const summary = entry.summary;
|
|
448
|
+
// --- watch: 宣言(watchPaths)× 実測(watch-fired 台帳) ---
|
|
449
|
+
for (const path of summary.watchPaths ?? []) {
|
|
450
|
+
const count = this._watchFiredCounts.get(pathKeyOf(entry.name, path)) ?? 0;
|
|
451
|
+
if (count > 0) {
|
|
452
|
+
out.push({ stateName: entry.name, kind: "watch", name: path, status: "fired", count, note: null });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
// ワイルドカード行 watch に**リスト書き込み**が届く前提 = 各 `.*` 階層の
|
|
456
|
+
// リストが「for バインド(paths.list)or `$listKeys` 宣言(keyedListPaths)」
|
|
457
|
+
// されていること(watch 設計 §6-3)。未成立は「未発火」と区別する。
|
|
458
|
+
// 前提はリスト置換経路に限る主張 — 明示 index 書き込み(`$resolve` /
|
|
459
|
+
// `items.0.price` 代入。$getAll で listIndex 台帳が生えた後)は前提に
|
|
460
|
+
// 依らず発火し得るため、「一度も発火しない」とは断定できない(実測済み)。
|
|
461
|
+
// keyedListPaths は protocol v1 追補 — フィールドを持たない旧ランタイム
|
|
462
|
+
// では $listKeys 側が観測できないため、note はさらに観測形に落とす 2 段。
|
|
463
|
+
const keyedKnown = summary.keyedListPaths !== undefined;
|
|
464
|
+
let missingList = null;
|
|
465
|
+
for (let star = path.indexOf(".*"); star !== -1; star = path.indexOf(".*", star + 2)) {
|
|
466
|
+
const listPath = path.slice(0, star);
|
|
467
|
+
const satisfied = summary.paths.list.has(listPath)
|
|
468
|
+
|| (summary.keyedListPaths?.has(listPath) ?? false);
|
|
469
|
+
if (!satisfied) {
|
|
470
|
+
missingList = listPath;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (missingList !== null) {
|
|
475
|
+
out.push({
|
|
476
|
+
stateName: entry.name, kind: "watch", name: path, status: "prerequisite-missing", count: 0,
|
|
477
|
+
note: keyedKnown
|
|
478
|
+
? `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
|
+
: `no for binding observed for list "${missingList}" (a $listKeys declaration would still let list writes fire it)`,
|
|
480
|
+
});
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
out.push({ stateName: entry.name, kind: "watch", name: path, status: "never", count: 0, note: null });
|
|
484
|
+
}
|
|
485
|
+
// --- token: 宣言(commandTokenNames / eventTokenNames)× 実測(emit 台帳)。
|
|
486
|
+
// subscriberCount 0 = 空撃ち(§4): 全 emit が空撃ちなら emitted-unheard として
|
|
487
|
+
// 警告する(emit 側だけ配線されて受け手が一つも居ない構成ミスの検出)。 ---
|
|
488
|
+
for (const name of summary.commandTokenNames) {
|
|
489
|
+
out.push(tokenCoverageOf(entry.name, "command", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "command", name))));
|
|
490
|
+
}
|
|
491
|
+
for (const name of summary.eventTokenNames) {
|
|
492
|
+
out.push(tokenCoverageOf(entry.name, "eventToken", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "event", name))));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// --- binding: canonical declared(宣言)× live 配線台帳(実測)。canonical が
|
|
496
|
+
// 取れないランタイムではこの節を出さない(declaredScan は集合でないため突合しない)。
|
|
497
|
+
const declared = this.getCanonicalDeclared();
|
|
498
|
+
if (declared !== null) {
|
|
499
|
+
const seen = new Set();
|
|
500
|
+
for (const decl of declared) {
|
|
501
|
+
// 構造ディレクティブは行の実体化そのもの・eventToken は token 節が担当
|
|
502
|
+
if (decl.bindingType === "for" || decl.bindingType === "if"
|
|
503
|
+
|| decl.bindingType === "elseif" || decl.bindingType === "else")
|
|
504
|
+
continue;
|
|
505
|
+
if (decl.propName.startsWith("eventToken."))
|
|
506
|
+
continue;
|
|
507
|
+
const key = attachKeyOf(decl.stateName, decl.statePathName, decl.propName);
|
|
508
|
+
if (seen.has(key))
|
|
509
|
+
continue;
|
|
510
|
+
seen.add(key);
|
|
511
|
+
// 「一度でも attach を観測したか」で判定(ever 台帳)。live 台帳は行の
|
|
512
|
+
// 破棄や GC(WeakRef pruning)で縮むため、瞬間値だと逆戻りする。
|
|
513
|
+
const attached = this._everAttachedKeys.has(key);
|
|
514
|
+
out.push({
|
|
515
|
+
stateName: decl.stateName, kind: "binding",
|
|
516
|
+
name: `${decl.propName} ← ${decl.statePathName}`,
|
|
517
|
+
status: attached ? "attached" : "never-attached",
|
|
518
|
+
count: attached ? 1 : 0,
|
|
519
|
+
note: decl.origin === "fragment" ? "template interior (attaches when rows materialize)" : null,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
350
525
|
/** roster entry の state からトップレベルキーを列挙(keys 未実装ランタイムは空) */
|
|
351
526
|
keysOf(entry) {
|
|
352
527
|
const source = this._sources.get(entry.sourceId);
|
|
@@ -528,7 +703,10 @@ class DevtoolsCore {
|
|
|
528
703
|
};
|
|
529
704
|
set.add(entry);
|
|
530
705
|
this._wiringEntryByBinding.set(event.binding, entry);
|
|
706
|
+
// カバレッジの実測面(B2): attach の観測を ever 台帳へ積む(remove では消さない)
|
|
707
|
+
this._everAttachedKeys.add(attachKeyOf(stateName, path, event.binding.propName));
|
|
531
708
|
this._notify("wiring");
|
|
709
|
+
this._notify("coverage");
|
|
532
710
|
return;
|
|
533
711
|
}
|
|
534
712
|
case "state:binding-removed": {
|
|
@@ -561,6 +739,25 @@ class DevtoolsCore {
|
|
|
561
739
|
if (this.isHiddenStateName(event.stateName)) {
|
|
562
740
|
return;
|
|
563
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");
|
|
760
|
+
}
|
|
564
761
|
this._appendTimeline({
|
|
565
762
|
sourceId,
|
|
566
763
|
kind: event.kind,
|
|
@@ -571,6 +768,16 @@ class DevtoolsCore {
|
|
|
571
768
|
});
|
|
572
769
|
return;
|
|
573
770
|
}
|
|
771
|
+
case "state:watch-fired": {
|
|
772
|
+
if (this.isHiddenStateName(event.stateName)) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// timeline 行にはしない(発火は高頻度になりうる活動でありカバレッジが受け皿)。
|
|
776
|
+
const firedKey = pathKeyOf(event.stateName, event.path);
|
|
777
|
+
this._watchFiredCounts.set(firedKey, (this._watchFiredCounts.get(firedKey) ?? 0) + 1);
|
|
778
|
+
this._notify("coverage");
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
574
781
|
case "state:watch-error": {
|
|
575
782
|
if (this.isHiddenStateName(event.stateName)) {
|
|
576
783
|
return;
|
|
@@ -868,6 +1075,10 @@ header .spacer { flex: 1; }
|
|
|
868
1075
|
.empty { color: #6f88a3; font-style: italic; padding: 4px 0; }
|
|
869
1076
|
.notice { color: #efe3a0; padding: 2px 0 6px; }
|
|
870
1077
|
.notice button { font: inherit; margin-left: 6px; cursor: pointer; }
|
|
1078
|
+
.wiring-row .detail { color: #6f88a3; }
|
|
1079
|
+
.wiring-tabs { padding: 0 0 4px; }
|
|
1080
|
+
.wiring-tabs button { font: inherit; cursor: pointer; margin-right: 4px; opacity: 0.6; }
|
|
1081
|
+
.wiring-tabs button.active { opacity: 1; font-weight: bold; }
|
|
871
1082
|
.hl-box {
|
|
872
1083
|
position: fixed;
|
|
873
1084
|
pointer-events: none;
|
|
@@ -911,6 +1122,8 @@ class WcsDevtools extends HTMLElement {
|
|
|
911
1122
|
_badge = null;
|
|
912
1123
|
_stateSelect = null;
|
|
913
1124
|
_paneElements = {};
|
|
1125
|
+
/** Wiring ペインの表示モード(配線一覧 / カバレッジ突合)。 */
|
|
1126
|
+
_wiringView = "wiring";
|
|
914
1127
|
_highlightLayer = null;
|
|
915
1128
|
_dirtyPanes = new Set();
|
|
916
1129
|
_renderScheduled = false;
|
|
@@ -949,6 +1162,12 @@ class WcsDevtools extends HTMLElement {
|
|
|
949
1162
|
else if (kind === "wiring") {
|
|
950
1163
|
this._markDirty("wiring");
|
|
951
1164
|
}
|
|
1165
|
+
else if (kind === "coverage") {
|
|
1166
|
+
// カバレッジは Wiring ペインのタブとして描画される
|
|
1167
|
+
if (this._wiringView === "coverage") {
|
|
1168
|
+
this._markDirty("wiring");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
952
1171
|
else {
|
|
953
1172
|
this._markDirty("timeline");
|
|
954
1173
|
}
|
|
@@ -1351,6 +1570,26 @@ class WcsDevtools extends HTMLElement {
|
|
|
1351
1570
|
const core = this._core;
|
|
1352
1571
|
const body = this._paneElements["wiring"];
|
|
1353
1572
|
body.replaceChildren();
|
|
1573
|
+
// 配線一覧 / カバレッジのビュートグル(devtools-tag-design.md §3.2 / 設計 §4)
|
|
1574
|
+
const tabs = document.createElement("div");
|
|
1575
|
+
tabs.className = "wiring-tabs";
|
|
1576
|
+
for (const [view, label] of [["wiring", "wiring"], ["coverage", "coverage"]]) {
|
|
1577
|
+
const button = document.createElement("button");
|
|
1578
|
+
button.textContent = label;
|
|
1579
|
+
if (this._wiringView === view) {
|
|
1580
|
+
button.classList.add("active");
|
|
1581
|
+
}
|
|
1582
|
+
button.addEventListener("click", () => {
|
|
1583
|
+
this._wiringView = view;
|
|
1584
|
+
this._markDirty("wiring");
|
|
1585
|
+
});
|
|
1586
|
+
tabs.append(button);
|
|
1587
|
+
}
|
|
1588
|
+
body.append(tabs);
|
|
1589
|
+
if (this._wiringView === "coverage") {
|
|
1590
|
+
this._renderCoverageView(body);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1354
1593
|
let entries;
|
|
1355
1594
|
let contextLabel;
|
|
1356
1595
|
if (this._pickedNode !== null) {
|
|
@@ -1378,10 +1617,13 @@ class WcsDevtools extends HTMLElement {
|
|
|
1378
1617
|
}
|
|
1379
1618
|
return;
|
|
1380
1619
|
}
|
|
1381
|
-
// ライブ台帳が空 → declared ビューへフォールバック(protocol §6
|
|
1620
|
+
// ライブ台帳が空 → declared ビューへフォールバック(protocol §6)。
|
|
1621
|
+
// ランタイムが getDeclaredBindings を実装していれば正本パーサの宣言集合を使い、
|
|
1622
|
+
// 旧ランタイムだけ簡易パーサ(declaredScan — bindTextParser 非追随)に落ちる。
|
|
1623
|
+
const canonical = core.getCanonicalDeclared();
|
|
1382
1624
|
const selected = this._selectedRoster();
|
|
1383
|
-
const
|
|
1384
|
-
if (
|
|
1625
|
+
const legacy = canonical === null && selected !== null ? scanDeclaredBindings(this._scanRootOf(selected)) : [];
|
|
1626
|
+
if ((canonical?.length ?? 0) === 0 && legacy.length === 0) {
|
|
1385
1627
|
body.append(this._emptyRow("no bindings observed"));
|
|
1386
1628
|
return;
|
|
1387
1629
|
}
|
|
@@ -1389,7 +1631,7 @@ class WcsDevtools extends HTMLElement {
|
|
|
1389
1631
|
notice.className = "notice";
|
|
1390
1632
|
const tag = document.createElement("span");
|
|
1391
1633
|
tag.className = "badge-tag declared";
|
|
1392
|
-
tag.textContent = "declared";
|
|
1634
|
+
tag.textContent = canonical !== null ? "declared (canonical)" : "declared";
|
|
1393
1635
|
notice.append(tag, document.createTextNode(" attached late — reload to capture live bindings "));
|
|
1394
1636
|
const reload = document.createElement("button");
|
|
1395
1637
|
reload.textContent = "reload";
|
|
@@ -1398,9 +1640,95 @@ class WcsDevtools extends HTMLElement {
|
|
|
1398
1640
|
});
|
|
1399
1641
|
notice.append(reload);
|
|
1400
1642
|
body.append(notice);
|
|
1401
|
-
|
|
1402
|
-
|
|
1643
|
+
if (canonical !== null) {
|
|
1644
|
+
for (const entry of canonical) {
|
|
1645
|
+
body.append(this._canonicalDeclaredRow(entry));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
for (const entry of legacy) {
|
|
1650
|
+
body.append(this._declaredRow(entry));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* カバレッジビュー: 宣言(watchPaths / token 宣言 / canonical declared)×
|
|
1656
|
+
* 実測(発火・emit・attach)の突合表。観測開始以降の実測であることを常時明示する
|
|
1657
|
+
* (protocol §6 — 台帳の過去は再構成できない)。
|
|
1658
|
+
*/
|
|
1659
|
+
_renderCoverageView(body) {
|
|
1660
|
+
const core = this._core;
|
|
1661
|
+
const since = core.observingSince;
|
|
1662
|
+
const info = document.createElement("div");
|
|
1663
|
+
// 壁時計表示(performance.now 原点からの換算)。「ページ内経過秒」だと
|
|
1664
|
+
// devtools を後から開いたときに観測開始点として読めない。
|
|
1665
|
+
const sinceLabel = since === null
|
|
1666
|
+
? null
|
|
1667
|
+
: new Date(Date.now() - (performance.now() - since)).toLocaleTimeString();
|
|
1668
|
+
info.textContent = sinceLabel === null
|
|
1669
|
+
? "not observing"
|
|
1670
|
+
: `observing since ${sinceLabel} (declared vs measured after this point)`;
|
|
1671
|
+
body.append(info);
|
|
1672
|
+
const report = core.getCoverageReport();
|
|
1673
|
+
if (report.length === 0) {
|
|
1674
|
+
body.append(this._emptyRow("nothing declared to cover"));
|
|
1675
|
+
return;
|
|
1403
1676
|
}
|
|
1677
|
+
for (const entry of report) {
|
|
1678
|
+
const row = document.createElement("div");
|
|
1679
|
+
row.className = "wiring-row";
|
|
1680
|
+
const kind = document.createElement("span");
|
|
1681
|
+
kind.className = "badge-tag";
|
|
1682
|
+
kind.textContent = entry.kind;
|
|
1683
|
+
const status = document.createElement("span");
|
|
1684
|
+
status.className = "badge-tag";
|
|
1685
|
+
status.textContent = entry.count > 1 ? `${entry.status} ×${entry.count}` : entry.status;
|
|
1686
|
+
if (entry.status === "never" || entry.status === "never-attached"
|
|
1687
|
+
|| entry.status === "emitted-unheard") {
|
|
1688
|
+
// 「宣言したのに一度も効いていない」= このビューの主役。空撃ちも同列の warn
|
|
1689
|
+
status.classList.add("warn");
|
|
1690
|
+
}
|
|
1691
|
+
else if (entry.status === "prerequisite-missing") {
|
|
1692
|
+
// 発火前提が未成立(未発火とは別問題)— declared 系の淡色で区別
|
|
1693
|
+
status.classList.add("declared");
|
|
1694
|
+
}
|
|
1695
|
+
const label = document.createElement("span");
|
|
1696
|
+
label.className = "path";
|
|
1697
|
+
label.textContent = ` ${entry.name}@${entry.stateName} `;
|
|
1698
|
+
row.append(kind, document.createTextNode(" "), label, status);
|
|
1699
|
+
if (entry.note !== null) {
|
|
1700
|
+
// note はホバー頼み(title)にせず常時表示する — 前提未成立の理由は
|
|
1701
|
+
// このビューの本文情報で、ツールチップだと存在に気づけない
|
|
1702
|
+
const note = document.createElement("span");
|
|
1703
|
+
note.className = "detail";
|
|
1704
|
+
note.textContent = ` ${entry.note}`;
|
|
1705
|
+
row.append(note);
|
|
1706
|
+
}
|
|
1707
|
+
body.append(row);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/** canonical declared(正本パーサ由来・宣言集合)の 1 行。 */
|
|
1711
|
+
_canonicalDeclaredRow(entry) {
|
|
1712
|
+
const row = document.createElement("div");
|
|
1713
|
+
row.className = "wiring-row";
|
|
1714
|
+
const type = document.createElement("span");
|
|
1715
|
+
type.className = "badge-tag declared";
|
|
1716
|
+
type.textContent = entry.origin;
|
|
1717
|
+
const prop = document.createElement("span");
|
|
1718
|
+
prop.className = "prop";
|
|
1719
|
+
prop.textContent = entry.propName;
|
|
1720
|
+
const path = document.createElement("span");
|
|
1721
|
+
path.className = "path";
|
|
1722
|
+
const filters = entry.outFilters.map((f) => ` | ${f.filterName}(${f.args.join(",")})`).join("");
|
|
1723
|
+
path.textContent = `${entry.statePathName}@${entry.stateName}${filters}`;
|
|
1724
|
+
row.append(type, document.createTextNode(" "), prop, document.createTextNode(" ← "), path);
|
|
1725
|
+
if (entry.node !== null) {
|
|
1726
|
+
const target = entry.node;
|
|
1727
|
+
row.addEventListener("click", () => {
|
|
1728
|
+
this._highlightNodes([target]);
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
return row;
|
|
1404
1732
|
}
|
|
1405
1733
|
_scanRootOf(entry) {
|
|
1406
1734
|
// instanceof はテスト環境(happy-dom)の Document 実体と一致しないことがあるため
|