@sleep2agi/agent-network-dashboard 0.5.3-preview.8 → 0.5.3

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.
Files changed (178) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +32 -32
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +2 -2
  14. package/.next/server/app/_not-found.rsc +12 -12
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/admin.html +2 -2
  23. package/.next/server/app/admin.rsc +14 -14
  24. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  25. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  26. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +14 -14
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  33. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  34. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  36. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/login.html +2 -2
  39. package/.next/server/app/login.rsc +14 -14
  40. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  41. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  42. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  45. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  46. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/logs.html +2 -2
  48. package/.next/server/app/logs.rsc +14 -14
  49. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  50. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  51. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  54. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  55. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/messages.html +2 -2
  57. package/.next/server/app/messages.rsc +14 -14
  58. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  59. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  60. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  64. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/node.html +2 -2
  66. package/.next/server/app/node.rsc +14 -14
  67. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  68. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  69. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  72. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  73. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/nodes.html +2 -2
  75. package/.next/server/app/nodes.rsc +14 -14
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs.html +2 -2
  85. package/.next/server/app/server-logs.rsc +14 -14
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  92. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/settings/networks.html +2 -2
  94. package/.next/server/app/settings/networks.rsc +14 -14
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  98. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  102. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens.html +2 -2
  105. package/.next/server/app/settings/tokens.rsc +14 -14
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  109. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +14 -14
  115. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  116. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  117. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  120. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  122. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks.html +2 -2
  124. package/.next/server/app/tasks.rsc +14 -14
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  131. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  132. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  134. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  141. package/.next/server/middleware-build-manifest.js +3 -3
  142. package/.next/server/pages/404.html +2 -2
  143. package/.next/server/pages/500.html +1 -1
  144. package/.next/static/chunks/0fuba20p57-zo.js +1 -0
  145. package/.next/static/chunks/{0hndl9yzpqajt.css → 0m.1mvl~t.avc.css} +1 -1
  146. package/.next/static/chunks/{0200cxbmk961-.js → 0ph1in3421~o-.js} +1 -1
  147. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  148. package/.next/static/chunks/10cj-86disers.js +1 -0
  149. package/.next/static/chunks/158h2g7f-nxbt.js +4 -0
  150. package/.next/trace +2 -2
  151. package/.next/trace-build +1 -1
  152. package/app/components/ServersDrawer.tsx +16 -3
  153. package/app/components/TopoGraph.tsx +453 -23
  154. package/app/globals.css +23 -0
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-alias-glow-test.mjs +121 -0
  158. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  159. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  160. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  161. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  162. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  163. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  164. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  165. package/scripts/topo-hub-idle-breath-test.mjs +109 -0
  166. package/scripts/topo-hub-recede-test.mjs +124 -0
  167. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  168. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  169. package/scripts/topo-orphan-label-italic-test.mjs +90 -0
  170. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  171. package/scripts/topo-recent-hot-pulse-test.mjs +102 -0
  172. package/scripts/topo-vendor-activelinks-press-test.mjs +100 -0
  173. package/.next/static/chunks/07fkn2-8-6ejj.js +0 -4
  174. package/.next/static/chunks/0nvcaq5be21x_.js +0 -1
  175. package/.next/static/chunks/0~r4zc7qlx96l.js +0 -1
  176. /package/.next/static/{iEkfox1TCVhnJmt_k5SoD → tQ1EtcEiS7ikgAoAyZWm7}/_buildManifest.js +0 -0
  177. /package/.next/static/{iEkfox1TCVhnJmt_k5SoD → tQ1EtcEiS7ikgAoAyZWm7}/_clientMiddlewareManifest.js +0 -0
  178. /package/.next/static/{iEkfox1TCVhnJmt_k5SoD → tQ1EtcEiS7ikgAoAyZWm7}/_ssgManifest.js +0 -0
@@ -0,0 +1,124 @@
1
+ /* Round 507 verification: hub-center workingCount digit fades to
2
+ * opacity 0.85 when ANY non-hub canvas surface is hovered. Lifts back
3
+ * to 1.0 on un-hover or when hovering the hub itself.
4
+ *
5
+ * Test phases:
6
+ * 1. rest (no hover) — opacity 1, data-topo-hub-recede='false'
7
+ * 2. node hover (synthetic pointerenter on g[data-node]) —
8
+ * opacity 0.85, data-topo-hub-recede='true'
9
+ * 3. release hover — back to opacity 1, recede='false'
10
+ *
11
+ * Source-side regex confirms gate + style + attr wired.
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
17
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
18
+
19
+ const browser = await chromium.launch({ headless: true });
20
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript(() => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'ring');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ } catch {}
28
+ });
29
+ await ctx.route('**/api/hub/status*', async (route) => {
30
+ const r = await route.fetch();
31
+ const b = await r.json();
32
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
33
+ const mk = (alias, status) => ({
34
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
35
+ network_id: nid, project_dir: null,
36
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
37
+ });
38
+ await route.fulfill({ response: r, json: { ...b, sessions: [
39
+ mk('alpha·a1', 'working'),
40
+ mk('alpha·a2', 'idle'),
41
+ ] } });
42
+ });
43
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
44
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
45
+ const page = await ctx.newPage();
46
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
47
+ await page.waitForSelector('[data-topo-hub-working-count-glow]', { timeout: 15000 });
48
+ await page.waitForTimeout(1500);
49
+
50
+ // Phase 1: rest state
51
+ const rest = await page.evaluate(() => {
52
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
53
+ if (!el) return null;
54
+ return {
55
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
56
+ opacity: window.getComputedStyle(el).opacity,
57
+ };
58
+ });
59
+
60
+ // Phase 2: hover a node via R488 banked synthetic dispatch
61
+ await page.evaluate(() => {
62
+ const g = document.querySelector('g[data-node]');
63
+ if (!g) return;
64
+ const target = g.querySelector('circle, image, rect') || g;
65
+ ['pointerenter', 'pointerover', 'mouseenter', 'mouseover'].forEach((t) => {
66
+ target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
67
+ });
68
+ });
69
+ await page.waitForTimeout(500);
70
+ const hover = await page.evaluate(() => {
71
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
72
+ if (!el) return null;
73
+ return {
74
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
75
+ opacity: window.getComputedStyle(el).opacity,
76
+ };
77
+ });
78
+
79
+ // Phase 3: release hover
80
+ await page.evaluate(() => {
81
+ const g = document.querySelector('g[data-node]');
82
+ if (!g) return;
83
+ const target = g.querySelector('circle, image, rect') || g;
84
+ ['pointerleave', 'pointerout', 'mouseleave', 'mouseout'].forEach((t) => {
85
+ target.dispatchEvent(new Event(t, { bubbles: true, cancelable: true }));
86
+ });
87
+ });
88
+ await page.waitForTimeout(500);
89
+ const release = await page.evaluate(() => {
90
+ const el = document.querySelector('[data-topo-hub-working-count-glow]');
91
+ if (!el) return null;
92
+ return {
93
+ recede_attr: el.getAttribute('data-topo-hub-recede'),
94
+ opacity: window.getComputedStyle(el).opacity,
95
+ };
96
+ });
97
+
98
+ await browser.close();
99
+
100
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
101
+ const sourceGateWired = /\(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel \|\|\s*hoveredStatus \|\| hoveredVendor\) && !hoveredHub/.test(src);
102
+ const sourceOpacity = /opacity: \(hoveredAlias \|\| hoveredEdgeKey \|\| hoveredGroupLabel/.test(src);
103
+ const sourceAttr = /data-topo-hub-recede=\{/.test(src);
104
+
105
+ // computed opacity comes back as a number-string ('1', '0.85')
106
+ const approxEq = (a, b) => Math.abs(parseFloat(a) - b) < 0.01;
107
+
108
+ const results = {
109
+ rest_recede_false: rest && rest.recede_attr === 'false',
110
+ rest_opacity_1: rest && approxEq(rest.opacity, 1),
111
+ hover_recede_true: hover && hover.recede_attr === 'true',
112
+ hover_opacity_85: hover && approxEq(hover.opacity, 0.85),
113
+ release_recede_false: release && release.recede_attr === 'false',
114
+ release_opacity_1: release && approxEq(release.opacity, 1),
115
+ source_gate_wired: sourceGateWired,
116
+ source_opacity_wired: sourceOpacity,
117
+ source_attr_wired: sourceAttr,
118
+ };
119
+ const ok = Object.values(results).every(Boolean);
120
+ console.log(`${ok ? '✅' : '❌'} R507 hub-recede:`, JSON.stringify(results),
121
+ '\n rest:', JSON.stringify(rest),
122
+ '\n hover:', JSON.stringify(hover),
123
+ '\n release:', JSON.stringify(release));
124
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 503 verification: orphan band rect (data-group-box-orphan="true")
2
+ * uses strokeDasharray='3 6' at rest, while prefix-group bands keep
3
+ * '6 6'. Pinned/hovered orphan still gets 'none' (solid stroke) — the
4
+ * differentiation lives ONLY in the rest state.
5
+ *
6
+ * Category-differentiation family 2nd anchor:
7
+ * R499 orphan label fontStyle: italic (1st anchor, R499)
8
+ * R503 orphan rect '3 6' dash pattern (2nd anchor, this round)
9
+ *
10
+ * Fixture: 2 prefix groups (alpha×3, beta×2) + 3 orphans (zeta/omega/lonely)
11
+ * — same shape as R499 test. Drives #150 orphan-band creation with
12
+ * prefix groups for differential assertion.
13
+ */
14
+ import { chromium } from 'playwright';
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
18
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
19
+
20
+ const browser = await chromium.launch({ headless: true });
21
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ localStorage.setItem('anet-topo-layout', 'grid');
27
+ sessionStorage.setItem('anet_v3_auth', '1');
28
+ } catch {}
29
+ });
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'), mk('alpha·3', 'idle'),
41
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
42
+ mk('zeta', 'idle'),
43
+ mk('omega', 'idle'),
44
+ mk('lonely', 'idle'),
45
+ ] } });
46
+ });
47
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
48
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
49
+ const page = await ctx.newPage();
50
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
51
+ await page.waitForSelector('[data-group-box-orphan]', { timeout: 15000 });
52
+ await page.waitForTimeout(1500);
53
+
54
+ const rects = await page.evaluate(() => {
55
+ const els = Array.from(document.querySelectorAll('[data-group-box-orphan]'));
56
+ return els.map((el) => {
57
+ const cs = window.getComputedStyle(el);
58
+ return {
59
+ orphan_attr: el.getAttribute('data-group-box-orphan'),
60
+ dasharray: el.getAttribute('stroke-dasharray') || cs.strokeDasharray || '',
61
+ };
62
+ });
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceTernary = /\(isPinned \|\| isHovered\) \? 'none' :\s*box\.isOrphan \? '3 6' : '6 6'/.test(src);
69
+ const sourceAttr = /data-group-box-orphan=\{box\.isOrphan \? 'true' : 'false'\}/.test(src);
70
+
71
+ const orphanRect = rects.find((r) => r.orphan_attr === 'true');
72
+ const prefixRects = rects.filter((r) => r.orphan_attr === 'false');
73
+
74
+ // Computed strokeDasharray comes back as 'px-suffixed' or raw — normalize
75
+ const normalize = (s) => (s || '').replace(/px/g, '').replace(/,\s*/g, ' ').trim();
76
+
77
+ const results = {
78
+ rects_found: rects.length >= 2,
79
+ orphan_rect_present: !!orphanRect,
80
+ orphan_dash_3_6: orphanRect && (normalize(orphanRect.dasharray) === '3 6' || normalize(orphanRect.dasharray) === '3 6'),
81
+ prefix_rects_present: prefixRects.length >= 1,
82
+ prefix_dash_6_6: prefixRects.length > 0 && prefixRects.every((r) => normalize(r.dasharray) === '6 6'),
83
+ source_ternary_wired: sourceTernary,
84
+ source_attr_wired: sourceAttr,
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} R503 orphan box dash:`, JSON.stringify(results),
88
+ '\n rects:', JSON.stringify(rects));
89
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,91 @@
1
+ /* Round 506 verification: orphan band rect rest-state fillOpacity
2
+ * drops slightly below prefix-group rest (0.025/0.045 → 0.015/0.028).
3
+ * Category-differentiation family 3rd anchor.
4
+ *
5
+ * R499 orphan label fontStyle italic (1st anchor)
6
+ * R503 orphan rect '3 6' dasharray (2nd anchor)
7
+ * R506 orphan rect lower fillOpacity (3rd anchor)
8
+ *
9
+ * Pin and hover branches UNCHANGED (full inspection affordance).
10
+ * Differentiation lives ONLY in the rest state.
11
+ *
12
+ * Fixture: 2 prefix groups (alpha×3 + beta×2) + 3 orphans (zeta/omega/
13
+ * lonely) — same shape as R499/R503 tests for cross-round consistency.
14
+ * Cyber theme drives the 0.045 (prefix) vs 0.028 (orphan) comparison.
15
+ */
16
+ import { chromium } from 'playwright';
17
+ import { readFileSync } from 'node:fs';
18
+
19
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
20
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
21
+
22
+ const browser = await chromium.launch({ headless: true });
23
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
24
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
25
+ await ctx.addInitScript(() => {
26
+ try {
27
+ localStorage.setItem('anet-theme', 'cyber');
28
+ localStorage.setItem('anet-topo-layout', 'grid');
29
+ sessionStorage.setItem('anet_v3_auth', '1');
30
+ } catch {}
31
+ });
32
+ await ctx.route('**/api/hub/status*', async (route) => {
33
+ const r = await route.fetch();
34
+ const b = await r.json();
35
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
36
+ const mk = (alias, status) => ({
37
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
38
+ network_id: nid, project_dir: null,
39
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
40
+ });
41
+ await route.fulfill({ response: r, json: { ...b, sessions: [
42
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'), mk('alpha·3', 'idle'),
43
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
44
+ mk('zeta', 'idle'),
45
+ mk('omega', 'idle'),
46
+ mk('lonely', 'idle'),
47
+ ] } });
48
+ });
49
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
50
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
53
+ await page.waitForSelector('[data-group-box-orphan]', { timeout: 15000 });
54
+ await page.waitForTimeout(1500);
55
+
56
+ const rects = await page.evaluate(() => {
57
+ const els = Array.from(document.querySelectorAll('[data-group-box-orphan]'));
58
+ return els.map((el) => ({
59
+ orphan_attr: el.getAttribute('data-group-box-orphan'),
60
+ fill_opacity_attr: el.getAttribute('data-group-box-fill-opacity'),
61
+ fill_opacity_dom: el.getAttribute('fill-opacity'),
62
+ }));
63
+ });
64
+
65
+ await browser.close();
66
+
67
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
68
+ const sourceTernary = /box\.isOrphan \? \(isLight \? 0\.015 : 0\.028\)\s*:\s*\(isLight \? 0\.025 : 0\.045\)/.test(src);
69
+ const sourceAttr = /data-group-box-fill-opacity=\{[\s\S]*?box\.isOrphan \? \(isLight \? 0\.015 : 0\.028\)/.test(src);
70
+
71
+ const orphanRect = rects.find((r) => r.orphan_attr === 'true');
72
+ const prefixRects = rects.filter((r) => r.orphan_attr === 'false');
73
+
74
+ // Cyber theme: prefix = 0.045, orphan = 0.028
75
+ const approxEq = (a, b) => Math.abs(parseFloat(a) - b) < 1e-9;
76
+
77
+ const results = {
78
+ rects_found: rects.length >= 2,
79
+ orphan_rect_present: !!orphanRect,
80
+ orphan_fill_028: orphanRect && approxEq(orphanRect.fill_opacity_attr, 0.028),
81
+ orphan_dom_fill_028: orphanRect && approxEq(orphanRect.fill_opacity_dom, 0.028),
82
+ prefix_rects_present: prefixRects.length >= 1,
83
+ prefix_fill_045: prefixRects.length > 0 && prefixRects.every((r) => approxEq(r.fill_opacity_attr, 0.045)),
84
+ prefix_dom_fill_045: prefixRects.length > 0 && prefixRects.every((r) => approxEq(r.fill_opacity_dom, 0.045)),
85
+ source_ternary_wired: sourceTernary,
86
+ source_attr_wired: sourceAttr,
87
+ };
88
+ const ok = Object.values(results).every(Boolean);
89
+ console.log(`${ok ? '✅' : '❌'} R506 orphan fill-opacity:`, JSON.stringify(results),
90
+ '\n rects:', JSON.stringify(rects));
91
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,90 @@
1
+ /* Round 499 verification: orphan band "其他" label gets fontStyle:
2
+ * italic, while prefix-group labels (alpha/beta/etc.) stay roman.
3
+ * Pure typography differentiation — no opacity / weight / fill / bbox
4
+ * change. data-group-label-orphan attr surfaces the gate for tests.
5
+ *
6
+ * Fixture: 2 prefix groups (alpha×3, beta×2) + 3 orphans (zeta/omega/
7
+ * lonely) — forces #150 orphan-band creation with at least 1 orphan
8
+ * and ≥ 1 prefix-group for differential assertion.
9
+ */
10
+ import { chromium } from 'playwright';
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
14
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
15
+
16
+ const browser = await chromium.launch({ headless: true });
17
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
18
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
19
+ await ctx.addInitScript(() => {
20
+ try {
21
+ localStorage.setItem('anet-theme', 'cyber');
22
+ // grid layout required for groupBoxes to render
23
+ localStorage.setItem('anet-topo-layout', 'grid');
24
+ sessionStorage.setItem('anet_v3_auth', '1');
25
+ } catch {}
26
+ });
27
+ await ctx.route('**/api/hub/status*', async (route) => {
28
+ const r = await route.fetch();
29
+ const b = await r.json();
30
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
31
+ const mk = (alias, status) => ({
32
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
33
+ network_id: nid, project_dir: null,
34
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
35
+ });
36
+ await route.fulfill({ response: r, json: { ...b, sessions: [
37
+ mk('alpha·1', 'working'), mk('alpha·2', 'idle'), mk('alpha·3', 'idle'),
38
+ mk('beta·1', 'working'), mk('beta·2', 'idle'),
39
+ mk('zeta', 'idle'),
40
+ mk('omega', 'idle'),
41
+ mk('lonely', 'idle'),
42
+ ] } });
43
+ });
44
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
45
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
46
+ const page = await ctx.newPage();
47
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
48
+ await page.waitForSelector('[data-group-label]', { timeout: 15000 });
49
+ await page.waitForTimeout(1500);
50
+
51
+ const labels = await page.evaluate(() => {
52
+ const els = Array.from(document.querySelectorAll('[data-group-label]'));
53
+ return els.map((el) => {
54
+ const cs = window.getComputedStyle(el);
55
+ return {
56
+ key: el.getAttribute('data-group-label'),
57
+ orphan_attr:el.getAttribute('data-group-label-orphan'),
58
+ font_style: cs.fontStyle,
59
+ };
60
+ });
61
+ });
62
+
63
+ await browser.close();
64
+
65
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
66
+ const sourceTypeWired = /groupBoxes: \[\] as \{ key: string; isOrphan\?: boolean;/.test(src);
67
+ const sourceFieldWired = /isOrphan: !!band\.isOrphan,/.test(src);
68
+ const sourceStyleWired = /fontStyle: box\.isOrphan \? 'italic' : undefined/.test(src);
69
+ const sourceAttrWired = /data-group-label-orphan=\{box\.isOrphan \? 'true' : 'false'\}/.test(src);
70
+
71
+ const orphanLabel = labels.find((l) => l.key === '其他');
72
+ const prefixLabels = labels.filter((l) => l.key !== '其他' && l.key !== '');
73
+
74
+ const results = {
75
+ found_labels: labels.length >= 2,
76
+ orphan_label_present: !!orphanLabel,
77
+ orphan_attr_true: orphanLabel && orphanLabel.orphan_attr === 'true',
78
+ orphan_font_italic: orphanLabel && orphanLabel.font_style === 'italic',
79
+ prefix_labels_present: prefixLabels.length >= 1,
80
+ prefix_attrs_false: prefixLabels.length > 0 && prefixLabels.every((p) => p.orphan_attr === 'false'),
81
+ prefix_font_normal: prefixLabels.length > 0 && prefixLabels.every((p) => p.font_style !== 'italic'),
82
+ source_type_wired: sourceTypeWired,
83
+ source_field_wired: sourceFieldWired,
84
+ source_style_wired: sourceStyleWired,
85
+ source_attr_wired: sourceAttrWired,
86
+ };
87
+ const ok = Object.values(results).every(Boolean);
88
+ console.log(`${ok ? '✅' : '❌'} R499 orphan label italic:`, JSON.stringify(results),
89
+ '\n labels:', JSON.stringify(labels));
90
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,89 @@
1
+ /* Round 504 verification: root svg surfaces `data-topo-pinned-aspect`
2
+ * categorical attribute. Sibling to R488's `data-topo-hovered-alias`
3
+ * (hover identity); R504 is the pin axis identity. Reflects WHICH
4
+ * pin state is active — paired with R467's any-pinned boolean.
5
+ *
6
+ * Tier values:
7
+ * none, status, group, vendor, edge, multi
8
+ *
9
+ * Test scenarios — seed pins via localStorage to reach each tier
10
+ * deterministically.
11
+ */
12
+ import { chromium } from 'playwright';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
16
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
17
+
18
+ async function probe({ pins }) {
19
+ const browser = await chromium.launch({ headless: true });
20
+ const ctx = await browser.newContext({ viewport: { width: 1500, height: 1200 } });
21
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
22
+ await ctx.addInitScript((p) => {
23
+ try {
24
+ localStorage.setItem('anet-theme', 'cyber');
25
+ localStorage.setItem('anet-topo-layout', 'ring');
26
+ sessionStorage.setItem('anet_v3_auth', '1');
27
+ // Pins persist via sessionStorage (not localStorage) — verified
28
+ // by grep of TopoGraph.tsx useState initializers at lines
29
+ // 1033/1148/1187. pinnedEdgeKey starts null (no persistence;
30
+ // edge pin only reachable via click, skipped in this test).
31
+ if (p.status) sessionStorage.setItem('anet-topo-pinned-status', p.status);
32
+ if (p.group) sessionStorage.setItem('anet-topo-pinned-group', p.group);
33
+ if (p.vendor) sessionStorage.setItem('anet-topo-pinned-vendor', p.vendor);
34
+ } catch {}
35
+ }, pins);
36
+ await ctx.route('**/api/hub/status*', async (route) => {
37
+ const r = await route.fetch();
38
+ const b = await r.json();
39
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
40
+ const mk = (alias, status) => ({
41
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
42
+ network_id: nid, project_dir: null,
43
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
44
+ });
45
+ await route.fulfill({ response: r, json: { ...b, sessions: [
46
+ mk('alpha·1', 'working'),
47
+ mk('alpha·2', 'idle'),
48
+ ] } });
49
+ });
50
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: [] } }));
51
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
52
+ const page = await ctx.newPage();
53
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
54
+ await page.waitForSelector('svg[data-topo-pinned-aspect]', { timeout: 15000 });
55
+ await page.waitForTimeout(2800); // settle for localStorage-restore useEffect
56
+ const aspect = await page.evaluate(() => {
57
+ return document.querySelector('svg[viewBox="0 0 1000 680"]')?.getAttribute('data-topo-pinned-aspect');
58
+ });
59
+ await browser.close();
60
+ return aspect;
61
+ }
62
+
63
+ const none = await probe({ pins: {} });
64
+ const status = await probe({ pins: { status: 'working' } });
65
+ // Note: 'alpha' for group depends on actual alias-prefix derivation; valid
66
+ // because we seeded alpha·1 + alpha·2 (both have prefix 'alpha·')
67
+ const group = await probe({ pins: { group: 'alpha·' } });
68
+ // vendor uses initial letter — Anthropic = 'A' (from vendorIdentity.ts line 69)
69
+ // claude-opus-4 model resolves to Anthropic vendor; stale-purge keeps 'A' valid
70
+ const vendor = await probe({ pins: { vendor: 'A' } });
71
+ // edge needs from→to format; skip — edge pin keys are complex
72
+ // Multi: status + group together
73
+ const multi = await probe({ pins: { status: 'working', group: 'alpha·' } });
74
+
75
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
76
+ const sourceWired = /data-topo-pinned-aspect=\{\(\(\) => \{[\s\S]*?aspects\.push\('status'\)[\s\S]*?aspects\.push\('group'\)[\s\S]*?aspects\.push\('vendor'\)[\s\S]*?aspects\.push\('edge'\)/.test(src);
77
+
78
+ const results = {
79
+ none_resolves: none === 'none',
80
+ status_resolves: status === 'status',
81
+ group_resolves: group === 'group',
82
+ vendor_resolves: vendor === 'vendor',
83
+ multi_resolves: multi === 'multi',
84
+ source_wired: sourceWired,
85
+ };
86
+ const ok = Object.values(results).every(Boolean);
87
+ console.log(`${ok ? '✅' : '❌'} R504 pinned-aspect:`, JSON.stringify(results),
88
+ '\n raw:', { none, status, group, vendor, multi });
89
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,102 @@
1
+ /* Round 498 verification: recent-signal row hot-count tspan gains a
2
+ * 3s opacity pulse (0.85↔1.0) when count >= 10 (isHot && !reducedMotion).
3
+ * Adds the 呼吸感 theme's second anchor after R497 hub idle-breath —
4
+ * gentle motion attracts glance to high-traffic lanes.
5
+ *
6
+ * Test scenarios — needs >= 10 messages between two aliases to trigger
7
+ * the hot threshold AND the recent-signal panel to render. Uses a
8
+ * messages fixture with 10 messages from alpha·a1 → alpha·a2.
9
+ */
10
+ import { chromium } from 'playwright';
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ const TOKEN = JSON.parse(readFileSync('/home/vansin/.anet/config.json', 'utf8')).token;
14
+ const fresh = new Date(Date.now() - 60 * 1000).toISOString();
15
+
16
+ async function probe({ reducedMotion }) {
17
+ const browser = await chromium.launch({ headless: true });
18
+ const ctx = await browser.newContext({
19
+ viewport: { width: 1500, height: 1200 },
20
+ reducedMotion: reducedMotion ? 'reduce' : 'no-preference',
21
+ });
22
+ await ctx.addCookies([{ name: 'anet_dashboard_session', value: `v3:${TOKEN}`, domain: '127.0.0.1', path: '/' }]);
23
+ await ctx.addInitScript(() => {
24
+ try {
25
+ localStorage.setItem('anet-theme', 'cyber');
26
+ localStorage.setItem('anet-topo-layout', 'ring');
27
+ sessionStorage.setItem('anet_v3_auth', '1');
28
+ } catch {}
29
+ });
30
+ await ctx.route('**/api/hub/status*', async (route) => {
31
+ const r = await route.fetch();
32
+ const b = await r.json();
33
+ const nid = (b.sessions || [])[0]?.network_id || 'default';
34
+ const mk = (alias, status) => ({
35
+ alias, status, model: 'claude-opus-4', runtime: 'claude-code-cli',
36
+ network_id: nid, project_dir: null,
37
+ created_at: fresh, updated_at: fresh, last_seen_at: fresh,
38
+ });
39
+ await route.fulfill({ response: r, json: { ...b, sessions: [
40
+ mk('alpha·a1', 'working'),
41
+ mk('alpha·a2', 'idle'),
42
+ ] } });
43
+ });
44
+ // 12 messages → count >= 10 → isHot true
45
+ const msgs = Array.from({ length: 12 }, (_, i) => ({
46
+ id: `m${i}`, from_alias: 'alpha·a1', to_alias: 'alpha·a2',
47
+ content: 'test', created_at: fresh,
48
+ }));
49
+ await ctx.route('**/api/hub/messages*', (r) => r.fulfill({ json: { messages: msgs } }));
50
+ await ctx.route('**/api/hub/tasks*', (r) => r.fulfill({ json: { tasks: [] } }));
51
+ const page = await ctx.newPage();
52
+ await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
53
+ await page.waitForTimeout(2000);
54
+
55
+ const result = await page.evaluate(() => {
56
+ const tspans = Array.from(document.querySelectorAll('[data-recent-row-count]'));
57
+ const hotTspan = tspans.find((t) => t.getAttribute('data-recent-row-count-hot') === 'true');
58
+ if (!hotTspan) return { found: false, totalTspans: tspans.length };
59
+ const cs = window.getComputedStyle(hotTspan);
60
+ return {
61
+ found: true,
62
+ class_has_pulse: /anet-recent-hot-pulse/.test(hotTspan.getAttribute('class') || ''),
63
+ pulse_attr: hotTspan.getAttribute('data-recent-row-count-hot-pulse'),
64
+ animation_name: cs.animationName,
65
+ animation_duration: cs.animationDuration,
66
+ };
67
+ });
68
+ await browser.close();
69
+ return result;
70
+ }
71
+
72
+ const motion = await probe({ reducedMotion: false });
73
+ const a11y = await probe({ reducedMotion: true });
74
+
75
+ const src = readFileSync('/home/vansin/agent-network-dashboard/app/components/TopoGraph.tsx', 'utf8');
76
+ const css = readFileSync('/home/vansin/agent-network-dashboard/app/globals.css', 'utf8');
77
+ const sourceClassWired = /className=\{isHot && !reducedMotion \? 'anet-recent-hot-pulse' : undefined\}/.test(src);
78
+ const sourceAttrWired = /data-recent-row-count-hot-pulse=\{isHot && !reducedMotion \? 'true' : 'false'\}/.test(src);
79
+ const cssKeyframeWired = /@keyframes anet-recent-hot-pulse-kf\s*\{[\s\S]*?opacity:\s*0\.85[\s\S]*?opacity:\s*1[\s\S]*?\}/.test(css);
80
+ const cssClassWired = /\.anet-recent-hot-pulse\s*\{\s*animation:\s*anet-recent-hot-pulse-kf 3s ease-in-out infinite/.test(css);
81
+
82
+ const results = {
83
+ // Motion fixture: tspan found + class applied + attr='true' + animation runs
84
+ motion_hot_found: motion.found,
85
+ motion_class_pulse: motion.found && motion.class_has_pulse,
86
+ motion_attr_true: motion.found && motion.pulse_attr === 'true',
87
+ motion_anim_name: motion.found && motion.animation_name === 'anet-recent-hot-pulse-kf',
88
+ // a11y fixture: class still absent (component-side gate) — attr='false'
89
+ a11y_hot_found: a11y.found,
90
+ a11y_no_class: a11y.found && !a11y.class_has_pulse,
91
+ a11y_attr_false: a11y.found && a11y.pulse_attr === 'false',
92
+ // Source/CSS
93
+ source_class_wired: sourceClassWired,
94
+ source_attr_wired: sourceAttrWired,
95
+ css_keyframe_wired: cssKeyframeWired,
96
+ css_class_wired: cssClassWired,
97
+ };
98
+ const ok = Object.values(results).every(Boolean);
99
+ console.log(`${ok ? '✅' : '❌'} R498 recent-row hot pulse:`, JSON.stringify(results),
100
+ '\n motion:', JSON.stringify(motion),
101
+ '\n a11y: ', JSON.stringify(a11y));
102
+ process.exit(ok ? 0 : 1);