clearotron 0.2.2 → 0.2.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 (118) hide show
  1. package/.env.example +28 -0
  2. package/.nvmrc +1 -1
  3. package/INSTALL.md +13 -11
  4. package/README.md +11 -16
  5. package/THIRD-PARTY-NOTICES.md +1 -1
  6. package/bin/clearotron.mjs +22 -0
  7. package/bin/example.mjs +4 -0
  8. package/bin/onboard.mjs +195 -19
  9. package/bin/start.mjs +154 -8
  10. package/build-info.json +2 -2
  11. package/demo/full-country-search/run/_driver/framework.json +1 -1
  12. package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
  13. package/demo/knockout-search/run/_driver/framework.json +1 -1
  14. package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
  15. package/docs/E2E.md +5 -5
  16. package/docs/PORTAL.md +2 -2
  17. package/docs/README.md +5 -4
  18. package/docs/SECURITY.md +17 -0
  19. package/docs/architecture/04-configuration-reference.md +2 -0
  20. package/docs/architecture/05-config-governance.md +14 -0
  21. package/docs/architecture/05-customer-profiles.md +2 -2
  22. package/docs/configuration.md +4 -3
  23. package/driver/CHANGELOG.md +56 -0
  24. package/driver/common-law-receipts.mjs +1 -1
  25. package/driver/config-staleness.mjs +1 -1
  26. package/driver/dev-portal.mjs +2 -2
  27. package/driver/driver.config.mjs +1 -1
  28. package/driver/engine/cli-version.mjs +103 -0
  29. package/driver/enqueue-schema.mjs +8 -8
  30. package/driver/feedback-store.mjs +1 -1
  31. package/driver/findings-model.mjs +52 -2
  32. package/driver/flag-snapshot.mjs +20 -0
  33. package/driver/framework.mjs +1 -1
  34. package/driver/gateway.mjs +18 -1
  35. package/driver/package.json +3 -3
  36. package/driver/party-facts.mjs +2 -2
  37. package/driver/phase0.mjs +1 -1
  38. package/driver/pipeline.mjs +5 -5
  39. package/driver/plain-register.mjs +120 -0
  40. package/driver/portal-access.mjs +1 -1
  41. package/driver/portal-config-view.mjs +40 -1
  42. package/driver/portal-families.mjs +1 -1
  43. package/driver/portal-report.mjs +1 -1
  44. package/driver/portal-service.mjs +26 -7
  45. package/driver/portal-upstream.mjs +2 -2
  46. package/driver/predelivery-lint.mjs +121 -1
  47. package/driver/profile-page.html +10 -2
  48. package/driver/profile-service.mjs +4 -4
  49. package/driver/profiles/README.md +3 -3
  50. package/driver/profiles.mjs +144 -12
  51. package/driver/publish/index.mjs +2 -2
  52. package/driver/publish/knockout.mjs +7 -1
  53. package/driver/publish/render-knockout.mjs +453 -118
  54. package/driver/publish/render.mjs +2 -2
  55. package/driver/reference-strip-signatures.mjs +20 -0
  56. package/driver/search-policy.mjs +2 -2
  57. package/driver/skills/blind-frame/SKILL.md +1 -1
  58. package/driver/skills/knockout-assess/SKILL.md +84 -0
  59. package/driver/skills/matter-frame/SKILL.md +6 -6
  60. package/driver/skills/matter-frame/watchlist-reference.md +2 -2
  61. package/driver/skills/prelim-common-law/SKILL.md +1 -1
  62. package/driver/skills/prelim-search/SKILL.md +1 -1
  63. package/driver/skills/prelim-search/report-prose.md +82 -0
  64. package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
  65. package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
  66. package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
  67. package/driver/skills/prelim-search/synthesis-rules.md +1 -1
  68. package/driver/skills/prelim-variants/SKILL.md +3 -3
  69. package/driver/stages.mjs +4 -4
  70. package/driver/suite-census.json +134 -38
  71. package/driver/unit-inventory.mjs +184 -27
  72. package/driver/verify.mjs +1 -1
  73. package/driver/whatif-memo-run.mjs +26 -4
  74. package/driver/whatif-memo.mjs +19 -1
  75. package/examples/README.md +2 -2
  76. package/examples/grants.example.json +6 -7
  77. package/mcp-server/CHANGELOG.md +2 -0
  78. package/mcp-server/lib/scrub.mjs +1 -1
  79. package/mcp-server/lib/whatif.mjs +2 -2
  80. package/mcp-server/package.json +1 -1
  81. package/mcp-server/packs/ops/EXAMPLES.md +2 -2
  82. package/mcp-server/server.mjs +1 -1
  83. package/package.json +28 -10
  84. package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
  85. package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-CEYvXdqH.js} +226 -49
  86. package/portal-ui/dist/index.html +2 -2
  87. package/portal-ui/package.json +1 -1
  88. package/providers/_shared/term-shape.mjs +1 -1
  89. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  90. package/providers/oauth-mcp-bridge/package.json +1 -1
  91. package/providers/uspto-local/src/index-store.js +2 -2
  92. package/scripts/citation-line-check.mjs +121 -2
  93. package/scripts/env-classify.mjs +67 -3
  94. package/scripts/live-surface-check.mjs +38 -1
  95. package/scripts/nightly-notice.mjs +0 -2
  96. package/scripts/preinstall-node-check.mjs +24 -0
  97. package/scripts/strip-tracker-citations.mjs +29 -6
  98. package/scripts/test-run.mjs +16 -0
  99. package/shared/anon-overlay.mjs +3 -3
  100. package/shared/customer-store.mjs +1 -1
  101. package/shared/invocation.mjs +28 -0
  102. package/shared/names-in-force.mjs +2 -0
  103. package/shared/node-floor.mjs +97 -0
  104. package/shared/staff-domain.mjs +154 -0
  105. package/shared/store-in-repo.mjs +23 -2
  106. package/skills/clearotron-ops/SKILL.md +1 -1
  107. package/driver/recipes/README.md +0 -49
  108. package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
  109. package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
  110. package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
  111. package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
  112. package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
  113. package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
  114. package/scripts/ai-page-render-check.mjs +0 -532
  115. package/scripts/clearances-render-check.mjs +0 -480
  116. package/scripts/composer-render-check.mjs +0 -1027
  117. package/scripts/home-render-check.mjs +0 -526
  118. package/scripts/portal-lifecycle-check.mjs +0 -639
@@ -1,532 +0,0 @@
1
- // SPDX-License-Identifier: AGPL-3.0-only
2
- // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- //
4
- // ai-page-render-check.mjs — the "Use your AI" page, drawn in a real browser, in the states a reader
5
- // actually meets (tracker issues 1938, 1976).
6
- //
7
- // node scripts/ai-page-render-check.mjs
8
- //
9
- // WHY A BROWSER AND NOT A STRING TEST. The owner pressed all four Connect buttons on the test portal and
10
- // said "none of them do anything". Nothing was broken in any file: each piece was correct and the fault
11
- // was the RELATION between them at run time. portal-ui carries no DOM, so this is where the question
12
- // "does a press leave a reader looking at anything" can be asked at all.
13
- //
14
- // WHAT THIS FILE ASSERTS SINCE THE RUTHLESS CUT (owner ruling, 2026-08-31). The page renders what
15
- // `/portal/api/mcp-access` hands it and derives nothing, so the browser questions are relational:
16
- //
17
- // · a button renders EXACTLY for each assistant this deployment serves — no silent drop, no button
18
- // over an assistant that cannot work (that is now a sentence with a reason and a remedy)
19
- // · nothing is expanded on arrival, and none of the six banned words is on the arriving page —
20
- // including the strings the SERVER sends, which is the side a source test of the page cannot see
21
- // · a press on a local-command assistant shows that assistant's own line and NEVER talks to the
22
- // key-minting endpoint
23
- // · a press on an address assistant mints once, and the credential reaches the DOM in exactly one
24
- // world: the browser refused the clipboard and the page said so, once, with "will not be shown
25
- // again". If the clipboard took it, the page says "Copied" and the credential is nowhere.
26
- //
27
- // THE FIXTURES HAVE ONE AUTHOR. Each deck's `/portal/api/mcp-access` body is composed through
28
- // `shared/connect-clients.mjs` and `shared/stdio-connect.mjs` — the same resolvers the real handler
29
- // calls — with only the deployment facts (role, door address) varying per deck. Hand-written offer rows
30
- // here would be a second author of the wire shape, which is exactly what this check exists to catch:
31
- // its previous fixtures served the pre-1976 `{enabled, url, keyUrl}` shape long after the server had
32
- // moved to `offers`, and every deck failed with "the Connect buttons did not render". The handler's own
33
- // output shape is pinned by driver/test/portal-service.test.mjs; the composition below follows it.
34
- //
35
- // THE DECKS are the two axes the resolver actually answers to — who is looking (staff get this box's
36
- // stdio routes, clients never do) and whether the client door is standing:
37
- //
38
- // local-staff a fresh local install, the reader is the operator. The stdio assistants are live
39
- // with no door at all; everything needing an address says why it cannot work here.
40
- // unwired-client a client on a box with no door. NOTHING is connectable — the page must say so
41
- // per assistant, with a reason, and render no button. Zero buttons is the pass.
42
- // wired-client the hosted shape: address assistants live, stdio ones honestly absent.
43
- // wired-staff everything on offer at once — the widest bijection.
44
- import { navigateOrRefuse } from './headless-page.mjs' // Page.navigate returns an errorText, and nothing read it
45
- import { createServer } from 'node:http'
46
- import { reapOnExit } from "../shared/reap-on-exit.mjs"; // — a detached group dies with this script
47
- import { readFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs'
48
- import { join, extname, dirname } from 'node:path'
49
- import { fileURLToPath } from 'node:url'
50
- import { spawn } from 'node:child_process'
51
- import { tmpdir } from 'node:os'
52
- import { connectOffers, offersForWire } from '../shared/connect-clients.mjs'
53
- import { stdioConnectOffer, stdioConnectFor, STDIO_SHAPES } from '../shared/stdio-connect.mjs'
54
-
55
- const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
56
- const DIST = join(REPO, 'portal-ui', 'dist')
57
- if (!existsSync(join(DIST, 'index.html'))) {
58
- console.error('ai-page-render-check: portal-ui/dist is not built. Run `npm run build:ui` first.')
59
- process.exit(2)
60
- }
61
- const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.json': 'application/json' }
62
-
63
- const ADDRESS = 'https://mcp.test.invalid/mcp'
64
- // What the mint hands back. Recognisable and inert — the check greps the page for these exact strings,
65
- // so they must be values no real deployment could produce.
66
- const MINTED = { address: 'https://mcp.test.invalid/mcp', key: 'fixture-credential-1976-never-real' }
67
-
68
- const STATES = {
69
- 'local-staff': { role: 'staff', url: null },
70
- 'unwired-client': { role: 'client', url: null },
71
- 'wired-client': { role: 'client', url: ADDRESS },
72
- 'wired-staff': { role: 'staff', url: ADDRESS },
73
- }
74
-
75
- /**
76
- * The mcp-access body for one deck, composed the way driver/portal-service.mjs composes it: staff get
77
- * this install's stdio routes, offers resolve through the one table, `steps` is dropped on the way out.
78
- */
79
- function accessFor({ role, url }) {
80
- const stdio = role === 'staff' ? stdioConnectOffer({ workDir: null }) : null
81
- const offers = connectOffers({
82
- stdioRoutes: stdio
83
- ? Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) => [shape, stdioConnectFor(shape, { workDir: null })]))
84
- : {},
85
- publicAddress: url,
86
- operator: 'counsel@zephyr.com',
87
- })
88
- return {
89
- url, keyUrl: null, email: 'counsel@zephyr.com', enabled: !!url, stdio,
90
- // THE ROUTE'S OWN MAPPING, not a copy of it.
91
- offers: offersForWire(offers),
92
- }
93
- }
94
-
95
- let current = 'local-staff'
96
- let mints = 0 // POSTs to /portal/api/connect-key since the last reset — a local press must never mint
97
-
98
- const server = createServer((req, res) => {
99
- const path = new URL(req.url, 'http://localhost').pathname
100
- const json = (o) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)) }
101
- const s = STATES[current]
102
- if (path === '/portal/api/me') {
103
- return json({ role: s.role, email: 'counsel@zephyr.com', accounts: s.role === 'staff' ? [] : ['zephyr'],
104
- allAccounts: s.role === 'staff', accountNames: { zephyr: 'Zephyr Beverages' } })
105
- }
106
- if (path === '/portal/api/mcp-access') return json(accessFor(s))
107
- if (path === '/portal/api/connect-key' && req.method === 'POST') { mints++; return json(MINTED) }
108
- if (path === '/portal/admin/roster') return json({ customers: [{ key: 'zephyr', name: 'Zephyr Beverages' }] })
109
- if (path.startsWith('/portal/api/') || path.startsWith('/portal/admin/')) return json({})
110
- const file = path === '/' || (path.startsWith('/portal') && !path.includes('.')) ? '/index.html' : path.replace(/^\/portal/, '')
111
- const full = join(DIST, file)
112
- if (!existsSync(full)) { res.writeHead(404); res.end('no'); return }
113
- res.writeHead(200, { 'content-type': MIME[extname(full)] ?? 'application/octet-stream' })
114
- res.end(readFileSync(full))
115
- })
116
- await new Promise((r) => server.listen(0, '127.0.0.1', r))
117
- const origin = `http://127.0.0.1:${server.address().port}`
118
-
119
- const userDir = mkdtempSync(join(tmpdir(), 'ai-page-check-'))
120
- const chrome = spawn('google-chrome', [
121
- '--headless=new', '--disable-gpu', '--no-sandbox', '--hide-scrollbars',
122
- `--user-data-dir=${userDir}`, '--window-size=1280,1000', '--remote-debugging-port=0', 'about:blank',
123
- ], { stdio: ['ignore', 'pipe', 'pipe'], detached: true })
124
- // — and the group dies with THIS script, on every exit it can observe.
125
- // The teardown below runs on the paths somebody wrote a branch for; a cancelled CI job (SIGTERM),
126
- // a Ctrl-C, or a throw elsewhere in this file are not among them — and that is where the measured
127
- // eighty-eight-minute orphan came from.
128
- reapOnExit(chrome);
129
-
130
- let devtools = ''
131
- const wsUrl = await new Promise((resolve, reject) => {
132
- const t = setTimeout(() => reject(new Error(`chrome did not report a devtools endpoint within 60s. Its stderr was:\n${devtools || '(nothing)'}`)), 60000)
133
- chrome.stderr.on('data', (c) => {
134
- devtools += c
135
- const m = devtools.match(/ws:\/\/[^\s]+/)
136
- if (m) { clearTimeout(t); resolve(m[0]) }
137
- })
138
- })
139
- const ws = new WebSocket(wsUrl)
140
- let id = 0
141
- const pending = new Map()
142
- ws.addEventListener('message', (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id) } })
143
- await new Promise((r) => ws.addEventListener('open', r))
144
- const send = (method, params = {}) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method, params })) })
145
- const { result: targets } = await send('Target.getTargets')
146
- const page = targets.targetInfos.find((t) => t.type === 'page')
147
- const { result: sess } = await send('Target.attachToTarget', { targetId: page.targetId, flatten: true })
148
- const sessionId = sess.sessionId
149
-
150
- const cmd = (method, params = {}) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, sessionId, method, params })) })
151
-
152
- // GRANT THE CLIPBOARD, RATHER THAN MEASURING THE DEGRADED PATH FOREVER.
153
- //
154
- // This was written asserting that headless Chrome grants clipboard writes here. It does not — measured,
155
- // after the first run came back with every `copied` state false and every panel empty. Left alone, the
156
- // instrument would have exercised only the browser-refused branch: the exception, on every deck, while
157
- // claiming to check the page a reader actually meets.
158
- //
159
- // So the permission is granted explicitly and the ordinary path is what gets driven. The refused branch
160
- // still has arms; it simply is not the only world this file can see.
161
- await cmd('Browser.grantPermissions', {
162
- origin,
163
- permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
164
- }).catch(() => {});
165
- // AND THE DOCUMENT MUST BE FOCUSED. A clipboard write needs a focused document as well as the
166
- // permission, which is why granting alone left every press on the refused path. Found independently by
167
- // role-e2e driving this page and by this file's first run coming back with every `copied` false.
168
- await cmd('Page.bringToFront').catch(() => {});
169
- const evalIn = async (expr) => (await cmd('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result?.result?.value
170
-
171
- // ── The probes ───────────────────────────────────────────────────────────────────────────────────
172
- //
173
- // Everything is asked of `.screen` — the page's own root — so the shell's navigation cannot answer for
174
- // the page. `key` below is the minted fixture credential, and the arrival probe must never find it.
175
- //
176
- // THE SIX WORDS are scanned on ARRIVAL only, deliberately. The one sanctioned technical artifact is the
177
- // line a press hands over (`claude mcp add …`, a config block naming the server path); it appears only
178
- // inside a pressed-open expansion, and banning its vocabulary would ban the feature. What the ruling
179
- // forbids is the page ASKING the reader to understand those words before anything is pressed.
180
- const arrivalProbe = (expect) => `(async () => {
181
- const EXPECT = ${JSON.stringify(expect)}
182
- const flat = (s) => (s ?? '').replace(/\\s+/g, ' ').trim()
183
- const screen = document.querySelector('.screen')
184
- if (!screen) return { fatal: 'the page did not render a .screen at all' }
185
- const text = flat(screen.innerText)
186
- const dests = [...screen.querySelectorAll('button.ai-dest')]
187
- return {
188
- // ANTI-VACUITY FIRST, and it is not decoration. Every assertion below this line is about something
189
- // being ABSENT — no banned word, no unserved row, no credential, nothing expanded — and a deck that
190
- // failed to render satisfies all of them perfectly. The length is asserted before the absences.
191
- renderedChars: text.length,
192
- destNames: dests.map((d) => flat(d.querySelector('.ai-dest-name')?.childNodes[0]?.textContent ?? '')),
193
- destCount: dests.length,
194
- // RULING 3: an unserved row does not render for a client at all. Checked by the OPERATOR-ONLY
195
- // wording rather than by name, because one product legitimately appears on two routes and a
196
- // name-based test could not tell a served Claude from an unserved one.
197
- operatorWordingOnPage: /incomplete|install it again/i.test(text),
198
- questionShown: !!screen.querySelector('.ai-where-q'),
199
- // TWO CONTROLS IN ONE GROUP MUST BE TELLABLE APART. Every other check on this page asks about ROWS,
200
- // and the rows are correct for whichever segment happens to be selected — so a group offering the
201
- // same words twice passes everything and is unreadable to a person. Suggested by role-e2e after
202
- // driving the staff decks and finding exactly that.
203
- segLabels: [...screen.querySelectorAll('.ai-seg-btn')].map((b) => flat(b.innerText)),
204
- // Nothing is expanded on arrival: the slot holds its empty line, not a panel.
205
- panelsOnArrival: [...screen.querySelectorAll('.ai-panel')].length,
206
- slotPresent: !!screen.querySelector('.ai-slot'),
207
- // THE SLOT'S HEIGHT BEFORE ANYTHING IS SELECTED, which is the only thing that makes "reserved" mean
208
- // anything. Asserting that nothing above the panel moves is VACUOUS on this layout — the panel is
209
- // the last element, so nothing above it can move whether or not space was reserved. Measured after
210
- // planting min-height:0 and watching the whole battery stay green.
211
- slotHeightOnArrival: Math.round(screen.querySelector('.ai-slot')?.getBoundingClientRect().height ?? 0),
212
- expandedPres: [...screen.querySelectorAll('pre')].length,
213
- bannedLines: screen.innerText.split('\\n').map(flat).filter((l) => /\\b(MCP|connector|token|scope|address|key)\\b/i.test(l)),
214
- keyOnPage: text.includes(EXPECT.key),
215
- helpLink: !!screen.querySelector('a[href="/portal/connect-help"]'),
216
- heading: text.includes('Use your own AI'),
217
- }
218
- })()`
219
-
220
- // BY ID, NOT BY NAME. The cowork/claude merge means one product legitimately appears on two routes
221
- // under one name — "Claude · app, web, and Cowork" and "Claude · app, on this computer" — so a
222
- // name-based lookup silently picks whichever came first. It picked the stdio row on the first run while
223
- // the arm believed it was pressing the web one.
224
- const readProbe = (id, expect) => `(async () => {
225
- const EXPECT = ${JSON.stringify(expect)}
226
- const flat = (s) => (s ?? '').replace(/\\s+/g, ' ').trim()
227
- const screen = document.querySelector('.screen')
228
- const rows = [...screen.querySelectorAll('button.ai-dest')]
229
- const row = rows.find((d) => d.getAttribute('data-id') === ${JSON.stringify(id)})
230
- if (!row) return { fatal: 'the pressed row vanished: ' + ${JSON.stringify(id)} }
231
- const movedAfter = [...screen.querySelectorAll('h1, h2, .ai-can li, button.ai-dest')]
232
- .map((el) => Math.round(el.getBoundingClientRect().top))
233
- const panel = screen.querySelector('.ai-panel')
234
- const steps = [...screen.querySelectorAll('.ai-panel-steps li')].map((li) => flat(li.innerText))
235
- return {
236
- movedAfter,
237
- panelFor: panel?.getAttribute('data-for') ?? null,
238
- panelCount: [...screen.querySelectorAll('.ai-panel')].length,
239
- copiedOnPressed: row.hasAttribute('data-copied'),
240
- copiedElsewhere: rows.filter((d) => d !== row && d.hasAttribute('data-copied')).length,
241
- selectedElsewhere: rows.filter((d) => d !== row && d.hasAttribute('data-selected')).length,
242
- steps,
243
- stepsPointingNowhere: steps.filter((l) => /\\bbelow\\b/.test(l)),
244
- stampShown: !!screen.querySelector('.ai-panel-stamp'),
245
- stampDated: /Checked \\d{4}-\\d{2}-\\d{2}/.test(flat(screen.querySelector('.ai-panel-stamp')?.innerText ?? '')),
246
- landedShown: /On your clipboard now/.test(flat(panel?.innerText ?? '')),
247
- keyAnywhere: flat(screen.innerText).includes(EXPECT.key),
248
- maskedShown: /••••/.test(flat(panel?.innerText ?? '')),
249
- refusedPath: /will not be shown again/.test(flat(screen.innerText)),
250
- }
251
- })()`
252
-
253
- const pressProbe = (id, name, expect) => `(async () => {
254
- const EXPECT = ${JSON.stringify(expect)}
255
- const flat = (s) => (s ?? '').replace(/\\s+/g, ' ').trim()
256
- const screen = document.querySelector('.screen')
257
- const rows = [...screen.querySelectorAll('button.ai-dest')]
258
- const row = rows.find((d) => d.getAttribute('data-id') === ${JSON.stringify(id)})
259
- if (!row) return { fatal: 'no destination row with id ' + ${JSON.stringify(id)} + ' (rows: ' + rows.map((d) => d.getAttribute('data-id')).join(', ') + ')' }
260
-
261
- // THE STRUCTURAL CLAIM IS A MEASUREMENT, NOT AN OPINION. The owner met the old page as "new links
262
- // open and move shit around", and single-select plus a reserved slot is the mechanism that answers it.
263
- // So the position of everything above the slot is recorded BEFORE the press and compared after.
264
- const above = [...screen.querySelectorAll('h1, h2, .ai-can li, button.ai-dest')]
265
- .map((el) => Math.round(el.getBoundingClientRect().top))
266
-
267
- // NO row.click() HERE. A scripted click carries no USER ACTIVATION, and navigator.clipboard requires
268
- // it — so a synthetic press always took the browser-refused branch and this file was measuring the
269
- // exception on every deck while claiming to check the page a reader meets. Granting the permission was
270
- // not enough; activation is a separate gate. The caller dispatches a real mouse press at these
271
- // coordinates through Input.dispatchMouseEvent, and this probe is split around it.
272
- const box = row.getBoundingClientRect()
273
- window.__aiPress = { x: Math.round(box.left + box.width / 2), y: Math.round(box.top + box.height / 2) }
274
- return { locate: true, x: window.__aiPress.x, y: window.__aiPress.y, above }
275
-
276
- const movedAfter = [...screen.querySelectorAll('h1, h2, .ai-can li, button.ai-dest')]
277
- .map((el) => Math.round(el.getBoundingClientRect().top))
278
- const shifted = above.length === movedAfter.length
279
- ? above.filter((t, i) => Math.abs(t - movedAfter[i]) > 1).length
280
- : -1
281
-
282
- const panel = screen.querySelector('.ai-panel')
283
- const steps = [...screen.querySelectorAll('.ai-panel-steps li')].map((li) => flat(li.innerText))
284
- return {
285
- shifted,
286
- panelFor: panel?.getAttribute('data-for') ?? null,
287
- panelCount: [...screen.querySelectorAll('.ai-panel')].length,
288
- // A press marks the row it was made on and no other.
289
- copiedOnPressed: row.hasAttribute('data-copied'),
290
- copiedElsewhere: rows.filter((d) => d !== row && d.hasAttribute('data-copied')).length,
291
- selectedElsewhere: rows.filter((d) => d !== row && d.hasAttribute('data-selected')).length,
292
- steps,
293
- // A STEP MAY NOT POINT AT SOMETHING THAT IS NOT THERE. These steps were written for a surface that
294
- // rendered the address and the credential underneath them; this page renders neither. A step saying
295
- // "below" is an instruction to look at nothing. The word boundary is DOUBLE-escaped deliberately:
296
- // this probe is a template literal, so a single backslash-b is the BACKSPACE escape and the pattern
297
- // would silently match nothing — which is how it was written first.
298
- stepsPointingNowhere: steps.filter((l) => /\\bbelow\\b/.test(l)),
299
- stampShown: !!screen.querySelector('.ai-panel-stamp'),
300
- stampDated: /Checked \\d{4}-\\d{2}-\\d{2}/.test(flat(screen.querySelector('.ai-panel-stamp')?.innerText ?? '')),
301
- landedShown: /On your clipboard now/.test(flat(panel?.innerText ?? '')),
302
- // THE CREDENTIAL IS IN THE DOM ONLY ON THE CLIPBOARD-REFUSED PATH. Headless Chrome grants the
303
- // clipboard here, so this must be false on every deck; the masked proof line is not the credential.
304
- keyAnywhere: flat(screen.innerText).includes(EXPECT.key),
305
- maskedShown: /••••/.test(flat(panel?.innerText ?? '')),
306
- }
307
- })()`
308
-
309
- // The segment labels, as the page renders them. MIRRORED, and pinned by the arm at the end of this file
310
- // so the two cannot drift silently — a stale label here would make the route switch a no-op and every
311
- // address press would quietly test the wrong row.
312
- const WHERE_LABEL = { disk: 'On this computer', either: 'On this computer', 'public-http': 'Somewhere else' }
313
-
314
- let failures = 0
315
- const ok = (cond, msg) => { if (!cond) { failures++; console.error(` ✗ ${msg}`) } else console.log(` ✓ ${msg}`) }
316
- const sameMembers = (a, b) => a.length === b.length && [...a].sort().every((v, i) => v === [...b].sort()[i])
317
-
318
- // Across all decks, both press kinds must actually run — a battery whose press arms all skipped would
319
- // be green about nothing, which is this repository's oldest trap.
320
- let commandPresses = 0
321
- let addressPresses = 0
322
- // HOW MANY PRESSES ACTUALLY REACHED THE CLIPBOARD, across every deck.
323
- //
324
- // This job can go green while asserting only the EXCEPTION, and that is not hypothetical — it is how
325
- // this file behaved until the press was made a real mouse event. Lose the clipboard and every press
326
- // quietly takes the browser-refused branch, where showing the credential is CORRECT by design; the
327
- // battery then passes forever while never once checking the page a reader actually meets.
328
- //
329
- // WHY IT FAILED IS NOT ESTABLISHED, AND SAYING SO IS THE POINT OF THIS PARAGRAPH.
330
- //
331
- // The first run of this rebuilt battery came back with every `copied` false and every panel empty. Three
332
- // things were then changed together — a granted clipboard permission, a focused document, and a real
333
- // Input.dispatchMouseEvent press instead of a scripted click — and it passed. Two confident explanations
334
- // were written here in turn, and PLANTING KILLED BOTH: removing Page.bringToFront changes nothing,
335
- // removing Browser.grantPermissions changes nothing, and reverting to a scripted `.click()` changes
336
- // nothing. Four presses reach the clipboard in all three.
337
- //
338
- // So the cause is unknown. It may have been a fourth thing that moved in the same edit, or something
339
- // about the first run's profile. All three preconditions stay, because none costs anything and a runner
340
- // is not this box — but none of them is written down here as the fix, because that would be a cause
341
- // asserted from no observation, which is the defect this whole file exists to catch.
342
- //
343
- // So the successful path is counted, and zero is an ENVIRONMENT failure rather than a page regression.
344
- // Named that way because the symptom is indistinguishable from a pass and the cause is not in the diff.
345
- let clipboardReached = 0
346
- let clipboardRefused = 0
347
-
348
- const out = {}
349
- for (const state of Object.keys(STATES)) {
350
- current = state
351
- mints = 0
352
- await cmd('Page.navigate', { url: 'about:blank' })
353
- await new Promise((r) => setTimeout(r, 150))
354
- await navigateOrRefuse(cmd, `${origin}/portal/ai`, { what: 'ai-page-render-check' })
355
- await new Promise((r) => setTimeout(r, 1400))
356
-
357
- const access = accessFor(STATES[state])
358
- const served = access.offers.filter((o) => o.served)
359
- const absent = access.offers.filter((o) => !o.served)
360
-
361
- const r = await evalIn(arrivalProbe({ absent: absent.map((a) => ({ name: a.name, reason: a.reason })), key: MINTED.key }))
362
- out[state] = { arrival: r }
363
- console.log(`\n${state}:`)
364
- if (r?.fatal) { failures++; console.error(` ✗ ${r.fatal}`); continue }
365
-
366
- // ANTI-VACUITY BEFORE EVERY ABSENCE. Most of what follows asserts that something is NOT on the page,
367
- // and a deck that rendered nothing satisfies all of it. Four of us hit exactly this shape today on
368
- // four different surfaces, so it is checked first and by measurement rather than by eye.
369
- ok(r.heading, 'the page rendered at all')
370
- ok(r.renderedChars > 400, `the deck rendered real text (${r.renderedChars} chars) — every absence below is worthless without this`)
371
-
372
- // ── THE ASSERTION THAT REPLACED "A BUTTON PER ASSISTANT" ────────────────────────────────────────
373
- // That one died with the button grid, and it is replaced rather than deleted (the brief's own
374
- // instruction). What survives is the property it protected: the page shows exactly what the resolver
375
- // says this deployment serves — no silent drop, and nothing invented.
376
- //
377
- // Grouped by route now, so the arriving deck shows the DEFAULT route's rows: the one that needs
378
- // nothing leads where the deployment has it.
379
- // `either` is not a place — a generic row accepts both, so it belongs to whichever the reader picks.
380
- // Mirrors the page's own placesOf/servesPlace, and the label arm below is what keeps the two honest.
381
- const routes = [...new Set(served.map((o) => o.route ?? 'public-http'))].filter((r) => r !== 'either')
382
- const places = routes.length ? routes : (served.length ? ['disk'] : [])
383
- const lead = places.includes('disk') ? 'disk' : places[0]
384
- const onLead = served.filter((o) => (o.route ?? 'public-http') === lead || (o.route ?? '') === 'either')
385
- ok(sameMembers(r.destNames, onLead.map((o) => o.name)),
386
- `a destination renders EXACTLY for each assistant this deck serves on the leading route `
387
- + `(saw ${JSON.stringify(r.destNames)}; the resolver says ${JSON.stringify(onLead.map((o) => o.name))})`)
388
-
389
- // ── RULING 3, AND IT IS THE HIGHEST-VALUE LINE IN THIS FILE ─────────────────────────────────────
390
- // On a healthy hosted install three rows resolve unserved carrying "this copy of the software is
391
- // incomplete… whoever installed it will need to install it again". That is the operator's case reused
392
- // for a reader with no shell, and a paying client read it as their software being broken. It must not
393
- // reach a client by any surface, so this checks the WORDING rather than the row: one product appears
394
- // on two routes, and a name-based check could not tell a served Claude from an unserved one.
395
- ok(!r.operatorWordingOnPage,
396
- 'the operator-only "incomplete / install it again" wording stays off a reader\'s page '
397
- + `(present: ${r.operatorWordingOnPage}) — a client reading it was the defect the redesign retires`)
398
- ok(r.destCount === onLead.length,
399
- `no extra row rendered beyond what the resolver serves (${r.destCount} rendered, ${onLead.length} served)`)
400
-
401
- // ── THE PAGE ASKS AS MANY QUESTIONS AS THE DEPLOYMENT LEAVES OPEN ───────────────────────────────
402
- ok(r.questionShown === (places.length > 1),
403
- `the "where does your assistant run?" question renders iff the served offers span more than one `
404
- + `place (places: ${JSON.stringify(places)}, question shown: ${r.questionShown})`)
405
- // AND ITS ANSWERS MUST BE DISTINGUISHABLE. A group answering one question with the same words twice is
406
- // unreadable to a person while every row-level check passes, because the rows are right for whichever
407
- // one is selected. This is "a name is not an identifier" applied to what a reader sees.
408
- ok(new Set(r.segLabels).size === r.segLabels.length,
409
- `the answers to one question are distinct (saw ${JSON.stringify(r.segLabels)})`)
410
- ok(r.segLabels.length === (places.length > 1 ? places.length : 0),
411
- `one control per place, and none for a place that does not exist (${r.segLabels.length} control(s), ${places.length} place(s))`)
412
-
413
- ok(r.panelsOnArrival === 0, `nothing is expanded on arrival (saw ${r.panelsOnArrival} panel(s))`)
414
- // The slot exists only where there is something to select. A deployment serving nothing renders one
415
- // sentence and no control, so requiring a slot there would fail the honest state.
416
- ok(r.slotPresent === (served.length > 0),
417
- `the reserved slot renders iff this deck has something to pick (served: ${served.length}, slot: ${r.slotPresent})`)
418
- // AND IT RESERVES SPACE WHILE EMPTY. This is the assertion that actually holds the design: a slot with
419
- // no height is not reserved, it just happens to sit below everything. 120px is well under the panel's
420
- // real size and well above an empty box, so it fails on a removed reservation and not on a reflow.
421
- if (r.slotPresent) {
422
- ok(r.slotHeightOnArrival >= 120,
423
- `the slot reserves space before anything is picked (${r.slotHeightOnArrival}px) — without it the `
424
- + 'page grows under the reader when they select, which is the reflow this redesign removes')
425
- }
426
- ok(r.expandedPres === 0, `nothing is expanded on arrival (saw ${r.expandedPres} open block(s))`)
427
- ok(r.bannedLines.length === 0,
428
- `none of the six banned words reaches the arriving reader — from either side of the wire (saw ${JSON.stringify(r.bannedLines)})`)
429
- ok(!r.keyOnPage, 'no credential is on the page before any press')
430
- ok(r.helpLink, 'the one link to the by-hand instructions is on the page')
431
-
432
- // ── A PRESS, AND THE STRUCTURAL CLAIM THAT COMES WITH IT ───────────────────────────────────────
433
- //
434
- // Every press on every deck asserts the same three things, because they are the rebuild's whole
435
- // promise: nothing above the panel moves, the pressed row is the confirmation, and no other row
436
- // claims to be. The route-specific arms follow.
437
- const pressArm = async (offer, kind) => {
438
- const before = mints
439
- // THE ROW MAY NOT BE ON THE ROUTE THE DECK IS SHOWING. Grouping means only the leading route's rows
440
- // render, so an address offer on a staff deck sits behind the question until it is answered. Pressing
441
- // the segment first is not a workaround — it is the path a staff reader takes, and it exercises the
442
- // question this page asks.
443
- await evalIn(`(async () => {
444
- const want = ${JSON.stringify(WHERE_LABEL[offer.route ?? 'public-http'] ?? 'Somewhere else')}
445
- const seg = [...document.querySelectorAll('.ai-seg-btn')].find((b) => b.innerText.trim() === want)
446
- if (seg && !seg.hasAttribute('data-on')) { seg.click(); await new Promise((r) => setTimeout(r, 250)) }
447
- return true
448
- })()`)
449
- const loc = await evalIn(pressProbe(offer.id, offer.name, { command: offer.command, key: MINTED.key, address: MINTED.address }))
450
- if (loc?.fatal) { failures++; console.error(` ✗ ${loc.fatal}`); return }
451
-
452
- // A REAL MOUSE PRESS, so the page gets user activation and the clipboard is actually reachable.
453
- for (const type of ['mousePressed', 'mouseReleased']) {
454
- await cmd('Input.dispatchMouseEvent', { type, x: loc.x, y: loc.y, button: 'left', clickCount: 1 })
455
- }
456
- await new Promise((r) => setTimeout(r, 800))
457
-
458
- const p = await evalIn(readProbe(offer.id, { key: MINTED.key }))
459
- out[state][`press ${offer.name}`] = p
460
- if (p?.fatal) { failures++; console.error(` ✗ ${p.fatal}`); return }
461
- p.shifted = loc.above.length === p.movedAfter.length
462
- ? loc.above.filter((t, i) => Math.abs(t - p.movedAfter[i]) > 1).length
463
- : -1
464
-
465
- // THE CLAIM THE OWNER MADE THE COMPLAINT ABOUT — "new links open and move shit around" — measured
466
- // as pixels rather than asserted as a design intent. Every heading, bullet and destination row is
467
- // recorded before the press and compared after; a single one moving is a failure.
468
- ok(p.shifted === 0,
469
- `nothing above the panel moved when "${offer.name}" was pressed (${p.shifted} element(s) shifted)`);
470
- ok(p.panelCount <= 1, `at most one panel is open at a time (saw ${p.panelCount}) — single-select is the mechanism`)
471
- ok(p.panelFor === offer.id, `the panel that opened is the one pressed (saw ${p.panelFor}, expected ${offer.id})`)
472
-
473
- // The pressed control IS the confirmation, and only the pressed one.
474
- if (p.refusedPath) clipboardRefused += 1; else clipboardReached += 1
475
- ok(p.copiedOnPressed,
476
- `the pressed row IS the confirmation for "${offer.name}" (marked: ${p.copiedOnPressed}, refused-clipboard path: ${p.refusedPath})`)
477
- ok(p.copiedElsewhere === 0, `and no other row claims to have been copied (${p.copiedElsewhere} did)`)
478
- ok(p.selectedElsewhere === 0, `and no other row is still selected (${p.selectedElsewhere} were) — the list is single-select`)
479
-
480
- ok(p.steps.length > 0, `pressing "${offer.name}" shows that assistant's own steps`)
481
- ok(p.stepsPointingNowhere.length === 0,
482
- `no step points the reader "below" at something this page does not render: ${JSON.stringify(p.stepsPointingNowhere)}`)
483
-
484
- // A DRIVEN ROW CARRIES A DATE AND AN UNDRIVEN ONE MUST NOT. A stamp defaulted onto steps nobody
485
- // opened would be this product's own defect class wearing the costume of evidence.
486
- ok(p.stampShown, `the panel for "${offer.name}" says whether anybody has driven these steps`)
487
- ok(p.stampDated === Boolean(offer.verifiedOn),
488
- `the dated stamp is shown iff the resolver's row was driven (row says ${JSON.stringify(offer.verifiedOn ?? null)}, panel dated: ${p.stampDated})`)
489
-
490
- if (kind === 'local') {
491
- ok(mints === before, `the local route needs no server, but ${mints - before} mint(s) were made`)
492
- ok(!p.keyAnywhere, 'no credential is anywhere on the page after a local press')
493
- } else {
494
- ok(mints === before + 1, `pressing "${offer.name}" mints exactly once (saw ${mints - before})`)
495
- ok(p.landedShown, 'the panel says what landed on the clipboard, so the press has visible proof')
496
- // THE CREDENTIAL IS IN THE DOM ONLY ON THE CLIPBOARD-REFUSED PATH. Headless Chrome grants the
497
- // clipboard, so the refused path is unreachable here and the credential must be absent — while the
498
- // MASKED proof line is present. Both are asserted: "no key" alone would pass a panel that showed
499
- // nothing at all, which is the shape this file has been bitten by before.
500
- ok(!p.keyAnywhere, 'the clipboard took the credential and it is nowhere on the page')
501
- ok(p.maskedShown, 'and it is MASKED — proof of the copy, never the credential itself')
502
- }
503
- }
504
-
505
- const local = served.find((o) => o.command)
506
- if (local) { commandPresses++; await pressArm(local, 'local') }
507
-
508
- const addressed = served.find((o) => !o.command)
509
- if (addressed) { addressPresses++; await pressArm(addressed, 'address') }
510
- }
511
-
512
-
513
- // THE ANTI-VACUITY GUARD FOR THE WHOLE BATTERY. Raised by role-e2e as the symptom to watch for once CI
514
- // began running this job: "if it ever goes green with every `copied` false, that is the symptom, not a
515
- // page regression." Written as a check rather than left as a caution, because a caution in a message is
516
- // not read by whoever meets the green job eighteen months from now.
517
- ok(clipboardReached > 0,
518
- `at least one press reached the clipboard (${clipboardReached} reached, ${clipboardRefused} refused). `
519
- + 'Zero means this browser stopped granting the clipboard, and the battery has then been asserting '
520
- + 'the refused fallback on every deck while looking exactly like a pass. That is an environment '
521
- + 'failure rather than a page regression, and the cause has never been pinned down — see the note by '
522
- + 'clipboardReached, which records three plants that each failed to reproduce it.')
523
-
524
- ok(commandPresses > 0, `at least one deck exercised the local-command press (saw ${commandPresses})`)
525
- ok(addressPresses > 0, `at least one deck exercised the address press (saw ${addressPresses})`)
526
-
527
- console.log(`\n${JSON.stringify(out, null, 2)}`)
528
- try { process.kill(-chrome.pid, 'SIGKILL') } catch { /* already gone */ }
529
- server.close()
530
- rmSync(userDir, { recursive: true, force: true })
531
- console.log(failures ? `\n${failures} failed` : `\nthe AI page draws correctly in all ${Object.keys(STATES).length} states`)
532
- process.exit(failures ? 1 : 0)