@wcstack/devtools 1.27.0 → 1.29.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 +126 -2
- package/dist/index.esm.js +402 -7
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
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,120 @@ 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 は各 `.*` 階層のリストが `for` バインドされている
|
|
456
|
+
// (= paths.list に載っている)ことが発火前提(watch 設計 §6-3)。未成立は
|
|
457
|
+
// 「未発火」と区別する。ただし paths.list は for 由来のみで `$listKeys`
|
|
458
|
+
// 宣言はここから見えないため、断定でなく「for が観測できない」と報告する。
|
|
459
|
+
let missingList = null;
|
|
460
|
+
for (let star = path.indexOf(".*"); star !== -1; star = path.indexOf(".*", star + 2)) {
|
|
461
|
+
const listPath = path.slice(0, star);
|
|
462
|
+
if (!summary.paths.list.has(listPath)) {
|
|
463
|
+
missingList = listPath;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (missingList !== null) {
|
|
468
|
+
out.push({
|
|
469
|
+
stateName: entry.name, kind: "watch", name: path, status: "prerequisite-missing", count: 0,
|
|
470
|
+
note: `no for binding observed for list "${missingList}" (a $listKeys declaration would still let it fire)`,
|
|
471
|
+
});
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
out.push({ stateName: entry.name, kind: "watch", name: path, status: "never", count: 0, note: null });
|
|
475
|
+
}
|
|
476
|
+
// --- token: 宣言(commandTokenNames / eventTokenNames)× 実測(emit 台帳)。
|
|
477
|
+
// subscriberCount 0 = 空撃ち(§4): 全 emit が空撃ちなら emitted-unheard として
|
|
478
|
+
// 警告する(emit 側だけ配線されて受け手が一つも居ない構成ミスの検出)。 ---
|
|
479
|
+
for (const name of summary.commandTokenNames) {
|
|
480
|
+
out.push(tokenCoverageOf(entry.name, "command", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "command", name))));
|
|
481
|
+
}
|
|
482
|
+
for (const name of summary.eventTokenNames) {
|
|
483
|
+
out.push(tokenCoverageOf(entry.name, "eventToken", name, this._tokenEmitCounts.get(tokenKeyOf(entry.name, "event", name))));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// --- binding: canonical declared(宣言)× live 配線台帳(実測)。canonical が
|
|
487
|
+
// 取れないランタイムではこの節を出さない(declaredScan は集合でないため突合しない)。
|
|
488
|
+
const declared = this.getCanonicalDeclared();
|
|
489
|
+
if (declared !== null) {
|
|
490
|
+
const seen = new Set();
|
|
491
|
+
for (const decl of declared) {
|
|
492
|
+
// 構造ディレクティブは行の実体化そのもの・eventToken は token 節が担当
|
|
493
|
+
if (decl.bindingType === "for" || decl.bindingType === "if"
|
|
494
|
+
|| decl.bindingType === "elseif" || decl.bindingType === "else")
|
|
495
|
+
continue;
|
|
496
|
+
if (decl.propName.startsWith("eventToken."))
|
|
497
|
+
continue;
|
|
498
|
+
const key = attachKeyOf(decl.stateName, decl.statePathName, decl.propName);
|
|
499
|
+
if (seen.has(key))
|
|
500
|
+
continue;
|
|
501
|
+
seen.add(key);
|
|
502
|
+
// 「一度でも attach を観測したか」で判定(ever 台帳)。live 台帳は行の
|
|
503
|
+
// 破棄や GC(WeakRef pruning)で縮むため、瞬間値だと逆戻りする。
|
|
504
|
+
const attached = this._everAttachedKeys.has(key);
|
|
505
|
+
out.push({
|
|
506
|
+
stateName: decl.stateName, kind: "binding",
|
|
507
|
+
name: `${decl.propName} ← ${decl.statePathName}`,
|
|
508
|
+
status: attached ? "attached" : "never-attached",
|
|
509
|
+
count: attached ? 1 : 0,
|
|
510
|
+
note: decl.origin === "fragment" ? "template interior (attaches when rows materialize)" : null,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
350
516
|
/** roster entry の state からトップレベルキーを列挙(keys 未実装ランタイムは空) */
|
|
351
517
|
keysOf(entry) {
|
|
352
518
|
const source = this._sources.get(entry.sourceId);
|
|
@@ -528,7 +694,10 @@ class DevtoolsCore {
|
|
|
528
694
|
};
|
|
529
695
|
set.add(entry);
|
|
530
696
|
this._wiringEntryByBinding.set(event.binding, entry);
|
|
697
|
+
// カバレッジの実測面(B2): attach の観測を ever 台帳へ積む(remove では消さない)
|
|
698
|
+
this._everAttachedKeys.add(attachKeyOf(stateName, path, event.binding.propName));
|
|
531
699
|
this._notify("wiring");
|
|
700
|
+
this._notify("coverage");
|
|
532
701
|
return;
|
|
533
702
|
}
|
|
534
703
|
case "state:binding-removed": {
|
|
@@ -561,6 +730,25 @@ class DevtoolsCore {
|
|
|
561
730
|
if (this.isHiddenStateName(event.stateName)) {
|
|
562
731
|
return;
|
|
563
732
|
}
|
|
733
|
+
// カバレッジの実測面: emit 回数と空撃ち回数を台帳に積む(§4)。
|
|
734
|
+
// stateName が null の emit はどの宣言とも突合できないため積まない。
|
|
735
|
+
if (event.stateName !== null) {
|
|
736
|
+
const emitKey = tokenKeyOf(event.stateName, event.kind, event.tokenName);
|
|
737
|
+
const stat = this._tokenEmitCounts.get(emitKey);
|
|
738
|
+
if (stat === undefined) {
|
|
739
|
+
this._tokenEmitCounts.set(emitKey, {
|
|
740
|
+
count: 1,
|
|
741
|
+
zeroSubscriberCount: event.subscriberCount === 0 ? 1 : 0,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
stat.count += 1;
|
|
746
|
+
if (event.subscriberCount === 0) {
|
|
747
|
+
stat.zeroSubscriberCount += 1;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
this._notify("coverage");
|
|
751
|
+
}
|
|
564
752
|
this._appendTimeline({
|
|
565
753
|
sourceId,
|
|
566
754
|
kind: event.kind,
|
|
@@ -571,6 +759,16 @@ class DevtoolsCore {
|
|
|
571
759
|
});
|
|
572
760
|
return;
|
|
573
761
|
}
|
|
762
|
+
case "state:watch-fired": {
|
|
763
|
+
if (this.isHiddenStateName(event.stateName)) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// timeline 行にはしない(発火は高頻度になりうる活動でありカバレッジが受け皿)。
|
|
767
|
+
const firedKey = pathKeyOf(event.stateName, event.path);
|
|
768
|
+
this._watchFiredCounts.set(firedKey, (this._watchFiredCounts.get(firedKey) ?? 0) + 1);
|
|
769
|
+
this._notify("coverage");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
574
772
|
case "state:watch-error": {
|
|
575
773
|
if (this.isHiddenStateName(event.stateName)) {
|
|
576
774
|
return;
|
|
@@ -601,6 +799,74 @@ class DevtoolsCore {
|
|
|
601
799
|
});
|
|
602
800
|
return;
|
|
603
801
|
}
|
|
802
|
+
case "propagation:suppressed": {
|
|
803
|
+
// two-way エコーの辺単位抑止。state 名を持たない(node+member が主語)
|
|
804
|
+
// ため hidden 判定はここでは行わない。
|
|
805
|
+
this._appendTimeline({
|
|
806
|
+
sourceId,
|
|
807
|
+
kind: "propagation-suppressed",
|
|
808
|
+
stateName: null,
|
|
809
|
+
label: event.member,
|
|
810
|
+
detail: `${event.reason} (tx ${event.transactionId}, edge ${event.edgeId})`,
|
|
811
|
+
subscriberCount: null,
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
case "propagation:coalesced": {
|
|
816
|
+
const stateName = event.absoluteAddress.absolutePathInfo.stateName;
|
|
817
|
+
if (this.isHiddenStateName(stateName)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this._appendTimeline({
|
|
821
|
+
sourceId,
|
|
822
|
+
kind: "propagation-coalesced",
|
|
823
|
+
stateName,
|
|
824
|
+
label: this._labelOf(event.absoluteAddress),
|
|
825
|
+
detail: `tx ${event.droppedTransactionId} dropped (winner tx ${event.winnerTransactionId})`,
|
|
826
|
+
subscriberCount: null,
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
case "propagation:hop-limit": {
|
|
831
|
+
const stateName = event.absoluteAddress.absolutePathInfo.stateName;
|
|
832
|
+
if (this.isHiddenStateName(stateName)) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
this._appendTimeline({
|
|
836
|
+
sourceId,
|
|
837
|
+
kind: "propagation-hop-limit",
|
|
838
|
+
stateName,
|
|
839
|
+
label: this._labelOf(event.absoluteAddress),
|
|
840
|
+
detail: `hop ${event.hop} (tx ${event.transactionId})`,
|
|
841
|
+
subscriberCount: null,
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
case "contract:drift": {
|
|
846
|
+
// sidecar と live wcBindable の乖離。live が正本(wcstack-manifest-schema.md)。
|
|
847
|
+
// sidecarEvent / liveEvent は型上 optional(reason と結合されていない構造的
|
|
848
|
+
// 型付け)のため、欠落 payload でも "undefined" を表示しない防御を入れる。
|
|
849
|
+
const memberPart = event.member !== undefined ? `: ${event.member}` : "";
|
|
850
|
+
const eventPart = event.reason === "event-mismatch"
|
|
851
|
+
? ` (sidecar ${event.sidecarEvent ?? "?"} / live ${event.liveEvent ?? "?"})`
|
|
852
|
+
: "";
|
|
853
|
+
this._appendTimeline({
|
|
854
|
+
sourceId,
|
|
855
|
+
kind: "contract-drift",
|
|
856
|
+
stateName: null,
|
|
857
|
+
label: event.tag,
|
|
858
|
+
detail: `${event.reason}${memberPart}${eventPart}`,
|
|
859
|
+
subscriberCount: null,
|
|
860
|
+
});
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
case "contract:manifest-read":
|
|
864
|
+
case "contract:unsupported-extension": {
|
|
865
|
+
// 情報イベント。contract analyzer の戻り値 API から取得でき、timeline は
|
|
866
|
+
// 活動ログに絞る(static-wiring-dx-design.md §6 の行 4 種)。union に
|
|
867
|
+
// 載せることで「型に無いイベントを黙って捨てる」状態だけを解消する。
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
604
870
|
}
|
|
605
871
|
}
|
|
606
872
|
}
|
|
@@ -800,6 +1066,10 @@ header .spacer { flex: 1; }
|
|
|
800
1066
|
.empty { color: #6f88a3; font-style: italic; padding: 4px 0; }
|
|
801
1067
|
.notice { color: #efe3a0; padding: 2px 0 6px; }
|
|
802
1068
|
.notice button { font: inherit; margin-left: 6px; cursor: pointer; }
|
|
1069
|
+
.wiring-row .detail { color: #6f88a3; }
|
|
1070
|
+
.wiring-tabs { padding: 0 0 4px; }
|
|
1071
|
+
.wiring-tabs button { font: inherit; cursor: pointer; margin-right: 4px; opacity: 0.6; }
|
|
1072
|
+
.wiring-tabs button.active { opacity: 1; font-weight: bold; }
|
|
803
1073
|
.hl-box {
|
|
804
1074
|
position: fixed;
|
|
805
1075
|
pointer-events: none;
|
|
@@ -843,6 +1113,8 @@ class WcsDevtools extends HTMLElement {
|
|
|
843
1113
|
_badge = null;
|
|
844
1114
|
_stateSelect = null;
|
|
845
1115
|
_paneElements = {};
|
|
1116
|
+
/** Wiring ペインの表示モード(配線一覧 / カバレッジ突合)。 */
|
|
1117
|
+
_wiringView = "wiring";
|
|
846
1118
|
_highlightLayer = null;
|
|
847
1119
|
_dirtyPanes = new Set();
|
|
848
1120
|
_renderScheduled = false;
|
|
@@ -881,6 +1153,12 @@ class WcsDevtools extends HTMLElement {
|
|
|
881
1153
|
else if (kind === "wiring") {
|
|
882
1154
|
this._markDirty("wiring");
|
|
883
1155
|
}
|
|
1156
|
+
else if (kind === "coverage") {
|
|
1157
|
+
// カバレッジは Wiring ペインのタブとして描画される
|
|
1158
|
+
if (this._wiringView === "coverage") {
|
|
1159
|
+
this._markDirty("wiring");
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
884
1162
|
else {
|
|
885
1163
|
this._markDirty("timeline");
|
|
886
1164
|
}
|
|
@@ -1283,6 +1561,26 @@ class WcsDevtools extends HTMLElement {
|
|
|
1283
1561
|
const core = this._core;
|
|
1284
1562
|
const body = this._paneElements["wiring"];
|
|
1285
1563
|
body.replaceChildren();
|
|
1564
|
+
// 配線一覧 / カバレッジのビュートグル(devtools-tag-design.md §3.2 / 設計 §4)
|
|
1565
|
+
const tabs = document.createElement("div");
|
|
1566
|
+
tabs.className = "wiring-tabs";
|
|
1567
|
+
for (const [view, label] of [["wiring", "wiring"], ["coverage", "coverage"]]) {
|
|
1568
|
+
const button = document.createElement("button");
|
|
1569
|
+
button.textContent = label;
|
|
1570
|
+
if (this._wiringView === view) {
|
|
1571
|
+
button.classList.add("active");
|
|
1572
|
+
}
|
|
1573
|
+
button.addEventListener("click", () => {
|
|
1574
|
+
this._wiringView = view;
|
|
1575
|
+
this._markDirty("wiring");
|
|
1576
|
+
});
|
|
1577
|
+
tabs.append(button);
|
|
1578
|
+
}
|
|
1579
|
+
body.append(tabs);
|
|
1580
|
+
if (this._wiringView === "coverage") {
|
|
1581
|
+
this._renderCoverageView(body);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1286
1584
|
let entries;
|
|
1287
1585
|
let contextLabel;
|
|
1288
1586
|
if (this._pickedNode !== null) {
|
|
@@ -1310,10 +1608,13 @@ class WcsDevtools extends HTMLElement {
|
|
|
1310
1608
|
}
|
|
1311
1609
|
return;
|
|
1312
1610
|
}
|
|
1313
|
-
// ライブ台帳が空 → declared ビューへフォールバック(protocol §6
|
|
1611
|
+
// ライブ台帳が空 → declared ビューへフォールバック(protocol §6)。
|
|
1612
|
+
// ランタイムが getDeclaredBindings を実装していれば正本パーサの宣言集合を使い、
|
|
1613
|
+
// 旧ランタイムだけ簡易パーサ(declaredScan — bindTextParser 非追随)に落ちる。
|
|
1614
|
+
const canonical = core.getCanonicalDeclared();
|
|
1314
1615
|
const selected = this._selectedRoster();
|
|
1315
|
-
const
|
|
1316
|
-
if (
|
|
1616
|
+
const legacy = canonical === null && selected !== null ? scanDeclaredBindings(this._scanRootOf(selected)) : [];
|
|
1617
|
+
if ((canonical?.length ?? 0) === 0 && legacy.length === 0) {
|
|
1317
1618
|
body.append(this._emptyRow("no bindings observed"));
|
|
1318
1619
|
return;
|
|
1319
1620
|
}
|
|
@@ -1321,7 +1622,7 @@ class WcsDevtools extends HTMLElement {
|
|
|
1321
1622
|
notice.className = "notice";
|
|
1322
1623
|
const tag = document.createElement("span");
|
|
1323
1624
|
tag.className = "badge-tag declared";
|
|
1324
|
-
tag.textContent = "declared";
|
|
1625
|
+
tag.textContent = canonical !== null ? "declared (canonical)" : "declared";
|
|
1325
1626
|
notice.append(tag, document.createTextNode(" attached late — reload to capture live bindings "));
|
|
1326
1627
|
const reload = document.createElement("button");
|
|
1327
1628
|
reload.textContent = "reload";
|
|
@@ -1330,10 +1631,96 @@ class WcsDevtools extends HTMLElement {
|
|
|
1330
1631
|
});
|
|
1331
1632
|
notice.append(reload);
|
|
1332
1633
|
body.append(notice);
|
|
1333
|
-
|
|
1334
|
-
|
|
1634
|
+
if (canonical !== null) {
|
|
1635
|
+
for (const entry of canonical) {
|
|
1636
|
+
body.append(this._canonicalDeclaredRow(entry));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
for (const entry of legacy) {
|
|
1641
|
+
body.append(this._declaredRow(entry));
|
|
1642
|
+
}
|
|
1335
1643
|
}
|
|
1336
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* カバレッジビュー: 宣言(watchPaths / token 宣言 / canonical declared)×
|
|
1647
|
+
* 実測(発火・emit・attach)の突合表。観測開始以降の実測であることを常時明示する
|
|
1648
|
+
* (protocol §6 — 台帳の過去は再構成できない)。
|
|
1649
|
+
*/
|
|
1650
|
+
_renderCoverageView(body) {
|
|
1651
|
+
const core = this._core;
|
|
1652
|
+
const since = core.observingSince;
|
|
1653
|
+
const info = document.createElement("div");
|
|
1654
|
+
// 壁時計表示(performance.now 原点からの換算)。「ページ内経過秒」だと
|
|
1655
|
+
// devtools を後から開いたときに観測開始点として読めない。
|
|
1656
|
+
const sinceLabel = since === null
|
|
1657
|
+
? null
|
|
1658
|
+
: new Date(Date.now() - (performance.now() - since)).toLocaleTimeString();
|
|
1659
|
+
info.textContent = sinceLabel === null
|
|
1660
|
+
? "not observing"
|
|
1661
|
+
: `observing since ${sinceLabel} (declared vs measured after this point)`;
|
|
1662
|
+
body.append(info);
|
|
1663
|
+
const report = core.getCoverageReport();
|
|
1664
|
+
if (report.length === 0) {
|
|
1665
|
+
body.append(this._emptyRow("nothing declared to cover"));
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
for (const entry of report) {
|
|
1669
|
+
const row = document.createElement("div");
|
|
1670
|
+
row.className = "wiring-row";
|
|
1671
|
+
const kind = document.createElement("span");
|
|
1672
|
+
kind.className = "badge-tag";
|
|
1673
|
+
kind.textContent = entry.kind;
|
|
1674
|
+
const status = document.createElement("span");
|
|
1675
|
+
status.className = "badge-tag";
|
|
1676
|
+
status.textContent = entry.count > 1 ? `${entry.status} ×${entry.count}` : entry.status;
|
|
1677
|
+
if (entry.status === "never" || entry.status === "never-attached"
|
|
1678
|
+
|| entry.status === "emitted-unheard") {
|
|
1679
|
+
// 「宣言したのに一度も効いていない」= このビューの主役。空撃ちも同列の warn
|
|
1680
|
+
status.classList.add("warn");
|
|
1681
|
+
}
|
|
1682
|
+
else if (entry.status === "prerequisite-missing") {
|
|
1683
|
+
// 発火前提が未成立(未発火とは別問題)— declared 系の淡色で区別
|
|
1684
|
+
status.classList.add("declared");
|
|
1685
|
+
}
|
|
1686
|
+
const label = document.createElement("span");
|
|
1687
|
+
label.className = "path";
|
|
1688
|
+
label.textContent = ` ${entry.name}@${entry.stateName} `;
|
|
1689
|
+
row.append(kind, document.createTextNode(" "), label, status);
|
|
1690
|
+
if (entry.note !== null) {
|
|
1691
|
+
// note はホバー頼み(title)にせず常時表示する — 前提未成立の理由は
|
|
1692
|
+
// このビューの本文情報で、ツールチップだと存在に気づけない
|
|
1693
|
+
const note = document.createElement("span");
|
|
1694
|
+
note.className = "detail";
|
|
1695
|
+
note.textContent = ` ${entry.note}`;
|
|
1696
|
+
row.append(note);
|
|
1697
|
+
}
|
|
1698
|
+
body.append(row);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/** canonical declared(正本パーサ由来・宣言集合)の 1 行。 */
|
|
1702
|
+
_canonicalDeclaredRow(entry) {
|
|
1703
|
+
const row = document.createElement("div");
|
|
1704
|
+
row.className = "wiring-row";
|
|
1705
|
+
const type = document.createElement("span");
|
|
1706
|
+
type.className = "badge-tag declared";
|
|
1707
|
+
type.textContent = entry.origin;
|
|
1708
|
+
const prop = document.createElement("span");
|
|
1709
|
+
prop.className = "prop";
|
|
1710
|
+
prop.textContent = entry.propName;
|
|
1711
|
+
const path = document.createElement("span");
|
|
1712
|
+
path.className = "path";
|
|
1713
|
+
const filters = entry.outFilters.map((f) => ` | ${f.filterName}(${f.args.join(",")})`).join("");
|
|
1714
|
+
path.textContent = `${entry.statePathName}@${entry.stateName}${filters}`;
|
|
1715
|
+
row.append(type, document.createTextNode(" "), prop, document.createTextNode(" ← "), path);
|
|
1716
|
+
if (entry.node !== null) {
|
|
1717
|
+
const target = entry.node;
|
|
1718
|
+
row.addEventListener("click", () => {
|
|
1719
|
+
this._highlightNodes([target]);
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return row;
|
|
1723
|
+
}
|
|
1337
1724
|
_scanRootOf(entry) {
|
|
1338
1725
|
// instanceof はテスト環境(happy-dom)の Document 実体と一致しないことがあるため
|
|
1339
1726
|
// nodeType で判定する(DOCUMENT_FRAGMENT_NODE は ShadowRoot を含む)
|
|
@@ -1424,6 +1811,14 @@ class WcsDevtools extends HTMLElement {
|
|
|
1424
1811
|
? "a $watch threw; the runtime isolated it (console.error only)"
|
|
1425
1812
|
: "a $watch write chain hit the depth limit and was cut off";
|
|
1426
1813
|
}
|
|
1814
|
+
// 伝播の打ち切りと契約 drift も「黙って起きる異常」なので warn に乗せる。
|
|
1815
|
+
// suppressed / coalesced は定常動作(エコー抑止・合流)のため通常表示。
|
|
1816
|
+
if (entry.kind === "propagation-hop-limit" || entry.kind === "contract-drift") {
|
|
1817
|
+
kind.classList.add("warn");
|
|
1818
|
+
kind.title = entry.kind === "propagation-hop-limit"
|
|
1819
|
+
? "a two-way propagation chain hit the hop limit and was cut off"
|
|
1820
|
+
: "sidecar manifest drifted from the live wcBindable declaration (live wins)";
|
|
1821
|
+
}
|
|
1427
1822
|
const label = document.createElement("span");
|
|
1428
1823
|
label.className = "label";
|
|
1429
1824
|
const stateName = entry.stateName !== null ? `@${entry.stateName}` : "";
|