clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -77,6 +77,7 @@ if (shotsDir) mkdirSync(shotsDir, { recursive: true })
|
|
|
77
77
|
// `productName` is resolved through the same expression portal-service.mjs evaluates, never typed: the
|
|
78
78
|
// sibling instruments' rule, and it holds here for the same reason.
|
|
79
79
|
const { reportIdentityFor } = await import('../driver/search-policy.mjs')
|
|
80
|
+
const { prepareReportForEmbed } = await import('../driver/portal-report.mjs')
|
|
80
81
|
|
|
81
82
|
const KEY = 'acme'
|
|
82
83
|
const NAME = 'Acme'
|
|
@@ -127,6 +128,19 @@ const DECKS = {
|
|
|
127
128
|
stdio: { command: 'node server.mjs', note: 'from this install', verify: 'it prints ready' } },
|
|
128
129
|
}
|
|
129
130
|
let deck = 'connected'
|
|
131
|
+
// WHICH DOCUMENT THE FRAME HOLDS. The stub below for every state about the header; for a finding's own
|
|
132
|
+
// button, a document put through the same preparation the portal serves a report with, so the press
|
|
133
|
+
// travels the real injected script to the real listener. Two cards at fixed places: a numbered finding,
|
|
134
|
+
// and an "Also considered" card, which carries the button and no number.
|
|
135
|
+
let reportDoc = 'stub'
|
|
136
|
+
const FINDING_DOC = () => prepareReportForEmbed(`<!doctype html><html><head><meta charset="utf-8"><title>report</title></head>
|
|
137
|
+
<body style="margin:0;font-family:sans-serif;background:#fff;height:400px">
|
|
138
|
+
<div class="card" id="c3" style="position:absolute;top:40px;left:20px"><div class="cardhead"><span class="fnum">3</span>
|
|
139
|
+
<button class="ask-fi no-print" style="width:240px;height:32px">\u2726 Ask AI about this finding</button></div></div>
|
|
140
|
+
<div class="card ruled" style="position:absolute;top:140px;left:20px"><div class="cardhead">
|
|
141
|
+
<button class="ask-fi no-print" style="width:240px;height:32px">\u2726 Ask AI about this finding</button></div></div>
|
|
142
|
+
<script>parent.postMessage({ source: 'cordillera-report', type: 'controls', commands: ['exportPDF'] }, '*');</script>
|
|
143
|
+
</body></html>`, {}).html
|
|
130
144
|
|
|
131
145
|
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.json': 'application/json' }
|
|
132
146
|
const json = (res, body) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)) }
|
|
@@ -143,6 +157,10 @@ const server = createServer((req, res) => {
|
|
|
143
157
|
// A real document, because a 404 in the frame changes what Result draws around it. Its CONTENT is not
|
|
144
158
|
// this file's subject — render-check.mjs owns the report's own layout. It announces the commands and
|
|
145
159
|
// the height a published report's bridge announces, so the header draws the Export menu a reader meets.
|
|
160
|
+
if (p.startsWith('/portal/report/') && reportDoc === 'finding') {
|
|
161
|
+
res.writeHead(200, { 'content-type': 'text/html' })
|
|
162
|
+
return res.end(FINDING_DOC())
|
|
163
|
+
}
|
|
146
164
|
if (p.startsWith('/portal/report/')) {
|
|
147
165
|
res.writeHead(200, { 'content-type': 'text/html' })
|
|
148
166
|
return res.end(`<!doctype html><meta charset="utf-8"><title>report</title>
|
|
@@ -409,6 +427,77 @@ await value('(() => { window.__pressContinue = true; return true })()')
|
|
|
409
427
|
const back = await value(CONTINUE)
|
|
410
428
|
const arrived = await value(PROBE)
|
|
411
429
|
|
|
430
|
+
// ── a finding's own button, pressed INSIDE the report ──
|
|
431
|
+
//
|
|
432
|
+
// A REAL CLICK AT THE BUTTON'S PLACE, not a message posted by this script: the document is sandboxed to a
|
|
433
|
+
// null origin and the point is the whole path — the injected script hears the press, posts it, and the
|
|
434
|
+
// report screen opens its control. The cards sit at fixed places in the document, so the frame's own
|
|
435
|
+
// position on the page is all that has to be measured.
|
|
436
|
+
const pressInFrame = async (top) => {
|
|
437
|
+
await value('(() => { window.scrollTo(0, 0); return true })()')
|
|
438
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
439
|
+
const at = await value(`(() => { const f = document.querySelector('iframe'); if (!f) return null; const r = f.getBoundingClientRect(); return { x: r.left, y: r.top } })()`)
|
|
440
|
+
if (!at) return false
|
|
441
|
+
const x = Math.round(at.x + 140), y = Math.round(at.y + top + 16)
|
|
442
|
+
for (const type of ['mousePressed', 'mouseReleased']) await cmd('Input.dispatchMouseEvent', { type, x, y, button: 'left', clickCount: 1 })
|
|
443
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
444
|
+
return true
|
|
445
|
+
}
|
|
446
|
+
const PANEL = `(() => {
|
|
447
|
+
const flat = (s) => (s || '').replace(/\\s+/g, ' ').trim();
|
|
448
|
+
const anchor = document.querySelector('[data-ask-ai]');
|
|
449
|
+
const float = anchor ? anchor.querySelector('.float') : null;
|
|
450
|
+
return { open: !!float, anchorState: anchor ? anchor.getAttribute('data-ask-ai') : null,
|
|
451
|
+
head: float && float.querySelector('.ask-ai-head') ? flat(float.querySelector('.ask-ai-head').textContent) : null,
|
|
452
|
+
text: float ? flat(float.innerText) : null,
|
|
453
|
+
checked: float ? [...float.querySelectorAll('[role="radio"]')].map(r => r.getAttribute('aria-checked')) : [] };
|
|
454
|
+
})()`
|
|
455
|
+
// THE BUTTON'S OWN OPEN STATE, AND THE CARET THAT TIES THE PANEL TO IT. Both are CSS, so both are read
|
|
456
|
+
// off the browser: a selector that never matched anything would still be in the stylesheet, and a
|
|
457
|
+
// source-text arm over it would pass on a panel a reader sees floating loose over the header.
|
|
458
|
+
const BUTTON = `(() => {
|
|
459
|
+
const b = document.querySelector('[data-ask-ai] > button');
|
|
460
|
+
if (!b) return null;
|
|
461
|
+
const s = getComputedStyle(b);
|
|
462
|
+
return { expanded: b.getAttribute('aria-expanded'), background: s.backgroundColor, border: s.borderTopColor };
|
|
463
|
+
})()`
|
|
464
|
+
const CARET = `(() => {
|
|
465
|
+
const f = document.querySelector('[data-ask-ai] .float');
|
|
466
|
+
if (!f) return null;
|
|
467
|
+
const edge = getComputedStyle(f, '::before'), fill = getComputedStyle(f, '::after'), panel = getComputedStyle(f);
|
|
468
|
+
return { edge: edge.borderBottomWidth, edgeColour: edge.borderBottomColor, panelBorder: panel.borderTopColor,
|
|
469
|
+
fill: fill.borderBottomWidth, fillColour: fill.borderBottomColor, panelFill: panel.backgroundColor, above: edge.top };
|
|
470
|
+
})()`
|
|
471
|
+
reportDoc = 'finding'
|
|
472
|
+
deck = 'connected'; await visit()
|
|
473
|
+
await value('(() => { window.__opened = []; window.open = (href) => { window.__opened.push(href); return null; }; return true })()')
|
|
474
|
+
const findingPressed = await pressInFrame(40)
|
|
475
|
+
const fromFinding = await value(PANEL)
|
|
476
|
+
await capture('ask-ai-from-finding')
|
|
477
|
+
const caret = await value(CARET)
|
|
478
|
+
const openButton = await value(BUTTON)
|
|
479
|
+
// PRESSED TWICE, THE SAME BUTTON SHUTS IT. A third press brings it back — which is also the state the
|
|
480
|
+
// questions below need, so this measurement leaves the deck exactly as it found it.
|
|
481
|
+
await pressInFrame(40)
|
|
482
|
+
const closedBySecondPress = await value(PANEL)
|
|
483
|
+
const shutButton = await value(BUTTON)
|
|
484
|
+
await pressInFrame(40)
|
|
485
|
+
const reopened = await value(PANEL)
|
|
486
|
+
const findingAsks = []
|
|
487
|
+
for (const i of QUESTIONS.keys()) {
|
|
488
|
+
if (i > 0) { await pressInFrame(40) }
|
|
489
|
+
findingAsks.push(await value(ASK(i)))
|
|
490
|
+
}
|
|
491
|
+
// The header's own button afterwards is about the whole report again.
|
|
492
|
+
const headerAfter = await value(`(async () => { document.querySelector('[data-ask-ai] > button').click(); await new Promise(r => setTimeout(r, 250)); return ${PANEL} })()`)
|
|
493
|
+
await value(`(async () => { document.querySelector('[data-ask-ai] > button').click(); await new Promise(r => setTimeout(r, 200)); return true })()`)
|
|
494
|
+
await pressInFrame(140)
|
|
495
|
+
const fromUnnumbered = await value(PANEL)
|
|
496
|
+
deck = 'neverBefore'; await visit()
|
|
497
|
+
await pressInFrame(40)
|
|
498
|
+
const findingNotConnected = await value(PANEL)
|
|
499
|
+
reportDoc = 'stub'
|
|
500
|
+
|
|
412
501
|
deck = 'cannotTell'; await visit(); const unknown = await value(PROBE)
|
|
413
502
|
deck = 'noConnector'; await visit(); const none = await value(PROBE)
|
|
414
503
|
deck = 'localOnly'; await visit(); const local = await value(PROBE)
|
|
@@ -526,6 +615,43 @@ ok(none.drawn === false, `no connector: a button was drawn that can do nothing
|
|
|
526
615
|
ok(local.drawn && local.opened, `local only: the case the old panel stranded is stranded again — ${show(local)}`)
|
|
527
616
|
ok(local.anchorState === 'ask' && local.radios.length === 4, `local only: ${show(local.anchorState)} with ${local.radios.length} question(s)`)
|
|
528
617
|
|
|
618
|
+
// ── a finding's own button: the press reaches the control, and the finding is named ──
|
|
619
|
+
const FINDING_REPORT = `finding 3 of ${REPORT}`
|
|
620
|
+
ok(findingPressed, 'a finding: the report frame was not on the page to press inside')
|
|
621
|
+
ok(fromFinding && fromFinding.open && fromFinding.anchorState === 'ask',
|
|
622
|
+
`a finding: pressing "Ask AI about this finding" inside the report opened nothing — ${show(fromFinding)}`)
|
|
623
|
+
ok(fromFinding && fromFinding.head === `${MARK} · finding 3 · ${PRODUCT_NAME} · searched ${ISO_DATE}`,
|
|
624
|
+
`a finding: the panel's first line reads ${show(fromFinding?.head)}, not the finding named beside the mark`)
|
|
625
|
+
ok(show(fromFinding?.checked) === show(['true', 'false', 'false', 'false']), `a finding: the first question is not the one selected — ${show(fromFinding)}`)
|
|
626
|
+
|
|
627
|
+
// ── the panel reads as this button's, and one button opens and shuts it ──
|
|
628
|
+
ok(caret && caret.edge === '7px' && caret.fill === '7px',
|
|
629
|
+
`the panel draws no caret, so it reads as chrome floating over the header rather than as this button's — ${show(caret)}`)
|
|
630
|
+
ok(caret && caret.edgeColour === caret.panelBorder && caret.fillColour === caret.panelFill,
|
|
631
|
+
`the caret is not the panel's own edge continuing — ${show(caret)}`)
|
|
632
|
+
ok(caret && caret.above.startsWith('-'), `the caret sits at ${show(caret?.above)}, not above the panel pointing at the button`)
|
|
633
|
+
ok(openButton && openButton.expanded === 'true', `with the panel open the button says expanded=${show(openButton?.expanded)}`)
|
|
634
|
+
ok(shutButton && shutButton.expanded === 'false', `with the panel shut the button says expanded=${show(shutButton?.expanded)}`)
|
|
635
|
+
ok(openButton && shutButton && openButton.background !== shutButton.background && openButton.border !== shutButton.border,
|
|
636
|
+
`the button is drawn the same open as shut, so nothing on the page says which control the panel belongs to — ${show({ open: openButton, shut: shutButton })}`)
|
|
637
|
+
ok(closedBySecondPress && closedBySecondPress.open === false,
|
|
638
|
+
`a finding pressed twice: the second press reopened the panel instead of closing it — ${show(closedBySecondPress)}`)
|
|
639
|
+
ok(reopened && reopened.open && reopened.head === `${MARK} · finding 3 · ${PRODUCT_NAME} · searched ${ISO_DATE}`,
|
|
640
|
+
`a finding pressed a third time: the panel did not come back on that finding — ${show(reopened)}`)
|
|
641
|
+
const FINDING_TYPED = [`Brief me on ${FINDING_REPORT}.`, `Explain the main risks in ${FINDING_REPORT}.`,
|
|
642
|
+
`What needs further investigation in ${FINDING_REPORT}?`, `How would narrower goods change the assessment in ${FINDING_REPORT}?`]
|
|
643
|
+
for (const [i, press] of findingAsks.entries()) {
|
|
644
|
+
const href = press?.opened?.[press.opened.length - 1] ?? ''
|
|
645
|
+
const typed = (() => { try { return new URL(href).searchParams.get('q') } catch { return null } })()
|
|
646
|
+
ok(typed === FINDING_TYPED[i], `a finding, question ${i + 1}: the assistant opens with ${show(typed)}, not ${show(FINDING_TYPED[i])}`)
|
|
647
|
+
}
|
|
648
|
+
ok(headerAfter && headerAfter.open && headerAfter.head === `${MARK} · ${PRODUCT_NAME} · searched ${ISO_DATE}`,
|
|
649
|
+
`after a finding, the header's own Ask AI still names the finding rather than the report — ${show(headerAfter)}`)
|
|
650
|
+
ok(fromUnnumbered && fromUnnumbered.open && fromUnnumbered.head === `${MARK} · ${PRODUCT_NAME} · searched ${ISO_DATE}`,
|
|
651
|
+
`a card with no number: its button opened nothing, or named a finding it does not have — ${show(fromUnnumbered)}`)
|
|
652
|
+
ok(findingNotConnected && findingNotConnected.open && findingNotConnected.anchorState === 'connect' && /Connect your AI first/.test(findingNotConnected.text ?? ''),
|
|
653
|
+
`a finding, not connected: the press did not open the unchanged connect panel — ${show(findingNotConnected)}`)
|
|
654
|
+
|
|
529
655
|
// ── NO ADDRESS AND NO COPY LINK, in any state that draws ──
|
|
530
656
|
for (const [name, r] of [['connected', connected], ['never connected', never], ['could not tell', unknown], ['local only', local]]) {
|
|
531
657
|
ok(!r.hasAddress, `${name}: a connector address is on the report again — ${show(r.text)}`)
|
|
@@ -537,5 +663,5 @@ if (fail.length) {
|
|
|
537
663
|
console.error(`ask-ai-render-check: ${fail.length} thing(s) a reader would meet`)
|
|
538
664
|
process.exit(1)
|
|
539
665
|
}
|
|
540
|
-
console.log(`ask-ai-render-check: the header, both panels and the unmeasured state render; all four questions reach the link typed in; Set it up leaves its note and Continue lands on the report with the panel open; no button where there is no connector`)
|
|
666
|
+
console.log(`ask-ai-render-check: the header, both panels and the unmeasured state render; the panel carries its caret and the button is drawn open, and a finding's own button opens and shuts it; all four questions reach the link typed in; Set it up leaves its note and Continue lands on the report with the panel open; no button where there is no connector`)
|
|
541
667
|
if (shotsDir) console.log(`ask-ai-render-check: shots in ${shotsDir}`)
|
|
@@ -28,18 +28,18 @@ import { join } from "node:path";
|
|
|
28
28
|
import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
|
|
29
29
|
import { tmpdir } from "node:os";
|
|
30
30
|
import { buildClaudeArgs, spawnEnv } from "../driver/engine/anthropic-agent.mjs";
|
|
31
|
-
import {
|
|
31
|
+
import { resolveEngineProgram } from "../driver/driver.config.mjs"; // — the program a run would spawn, found the way a run finds it
|
|
32
32
|
|
|
33
33
|
const arg = (f, d) => { const i = process.argv.indexOf(f); return i > 0 ? process.argv[i + 1] : d; };
|
|
34
34
|
const model = arg("--model", "claude-sonnet-5");
|
|
35
35
|
|
|
36
36
|
const root = mkdtempSync(join(tmpdir(), "authority-probe-"));
|
|
37
37
|
const skills = join(root, "skills"), runDir = join(root, "run");
|
|
38
|
-
mkdirSync(join(skills, "
|
|
38
|
+
mkdirSync(join(skills, "clearance-register"), { recursive: true });
|
|
39
39
|
ensureDriverDir(runDir);
|
|
40
|
-
writeFileSync(join(skills, "
|
|
41
|
-
const doctrine = join(skills, "
|
|
42
|
-
const existing = join(skills, "
|
|
40
|
+
writeFileSync(join(skills, "clearance-register", "SKILL.md"), "# doctrine\nThe method for this stage.\n");
|
|
41
|
+
const doctrine = join(skills, "clearance-register", "NOTES.md"); // the incident's shape: a NEW file in the doctrine tree
|
|
42
|
+
const existing = join(skills, "clearance-register", "SKILL.md"); // already on disk — the Edit target
|
|
43
43
|
const record = driverDir(runDir, "probe-note.json"); // the forensic record a seat must never author
|
|
44
44
|
const output = join(runDir, "findings.json"); // where this seat's own output belongs
|
|
45
45
|
|
|
@@ -73,7 +73,9 @@ console.log(`probe root: ${root}`);
|
|
|
73
73
|
console.log(`--add-dir roots: ${args.filter((a, i) => args[i - 1] === "--add-dir").join(", ")}`);
|
|
74
74
|
console.log(`--settings present: ${args.includes("--settings")}`);
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
// The program a run would spawn (driver.config.mjs resolveEngineProgram), so this probes the same copy.
|
|
77
|
+
const program = resolveEngineProgram("anthropic-agent");
|
|
78
|
+
const child = spawn(program.resolved ?? program.bin, [...args, "--include-hook-events"],
|
|
77
79
|
{ stdio: ["pipe", "pipe", "pipe"], cwd: runDir, env: spawnEnv() });
|
|
78
80
|
let out = "", err = "";
|
|
79
81
|
child.stdout.on("data", (d) => { out += d; });
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import "../shared/env-local.mjs"; // — FIRST: applies the CLEAROTRON_* translation before any module-top
|
|
29
29
|
// capture evaluates. Reads no `.env` here — that load is gated on isCliEntry(argv[1]).
|
|
30
30
|
import { readdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "node:fs";
|
|
31
|
-
import { join } from "node:path";
|
|
31
|
+
import { join } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
32
32
|
import { DRIVER_DIR, driverDir } from "../shared/driver-dir.mjs"; //
|
|
33
33
|
|
|
34
34
|
// NAMED, never guessed. This used to fall back to a hardcoded production workspace path, so a run
|
|
@@ -115,7 +115,7 @@ function main() {
|
|
|
115
115
|
const counts = {};
|
|
116
116
|
const rows = [];
|
|
117
117
|
for (const agent of AGENTS) {
|
|
118
|
-
const studio = join(WORKSPACE_ROOT, `workspace-${agent}
|
|
118
|
+
const studio = studioDirFor(join(WORKSPACE_ROOT, `workspace-${agent}`));
|
|
119
119
|
if (!existsSync(studio)) continue;
|
|
120
120
|
for (const runDir of findRunDirs(studio)) {
|
|
121
121
|
const r = backfillRun(runDir, { apply: APPLY });
|
|
@@ -37,11 +37,24 @@
|
|
|
37
37
|
// driver removes a conflict; it does not remove the check. That is the whole safety argument, and if
|
|
38
38
|
// that arm is ever weakened this file becomes unsafe with it.
|
|
39
39
|
//
|
|
40
|
-
// ── WHAT IT STILL REFUSES
|
|
40
|
+
// ── WHAT IT STILL REFUSES, AND HOW IT REFUSES ───────────────────────────────────────────────────────
|
|
41
41
|
//
|
|
42
42
|
// Both sides changing the SAME file's counts to DIFFERENT values is a real disagreement — two branches
|
|
43
43
|
// edited one test file — and it exits non-zero so git raises the conflict a human should see. A driver
|
|
44
44
|
// that silently picked a side there would be the "matches neither tree" failure in a new costume.
|
|
45
|
+
//
|
|
46
|
+
// AND IT WRITES MARKERS, BECAUSE EXITING WITHOUT WRITING IS NOT A REFUSAL. Until 2026-09-18 the refusal
|
|
47
|
+
// path wrote nothing at all, which leaves `%A` holding the OURS side untouched: a file with no conflict
|
|
48
|
+
// markers that parses as valid JSON, beside git's own "CONFLICT (content)" line. Both cheap checks a
|
|
49
|
+
// person runs on a conflicted file — does it have markers, does it parse — passed, so it was staged
|
|
50
|
+
// blind, and every entry the union had taken from THEIRS went with it. Measured that day on a real
|
|
51
|
+
// merge: the result was byte-identical to ours, and one test file's entry was simply gone. A census that
|
|
52
|
+
// under-counts is a population floor reading low, which is the one failure the census exists to prevent.
|
|
53
|
+
//
|
|
54
|
+
// So a refusal now writes a file that CANNOT be staged blind: real markers at column 0, the named
|
|
55
|
+
// disagreements above them, the UNION on the ours side — so a reader who resolves by taking it keeps
|
|
56
|
+
// every entry this driver could merge — and theirs whole on the other. It parses as nothing, which is
|
|
57
|
+
// the honest direction for a file nobody has resolved yet.
|
|
45
58
|
|
|
46
59
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
47
60
|
|
|
@@ -139,9 +152,27 @@ const merged = {
|
|
|
139
152
|
};
|
|
140
153
|
|
|
141
154
|
if (conflicts.length) {
|
|
155
|
+
const named = conflicts.slice(0, 10).map((c) => ` ${c}`);
|
|
156
|
+
const more = conflicts.length > named.length ? [` …and ${conflicts.length - named.length} more`] : [];
|
|
142
157
|
console.error(`census-merge-driver: ${conflicts.length} entr(y/ies) changed on BOTH sides — not ours to pick:`);
|
|
143
|
-
for (const
|
|
158
|
+
for (const line of [...named, ...more]) console.error(line);
|
|
144
159
|
console.error(" Resolve by hand, or re-run `node scripts/mint-suite-census.mjs --apply` on the merged tree.");
|
|
160
|
+
|
|
161
|
+
// THE UNION RIDES THE OURS SIDE, and that is the point of writing at all. Everything this driver COULD
|
|
162
|
+
// merge is in it — including every entry that exists only on theirs — so resolving by taking this side
|
|
163
|
+
// loses nothing but the disagreements, which are named above it and are the only thing a human is
|
|
164
|
+
// being asked about.
|
|
165
|
+
writeFileSync(oursPath, [
|
|
166
|
+
`census-merge-driver: ${conflicts.length} entr(y/ies) changed on BOTH sides and were NOT merged.`,
|
|
167
|
+
...named, ...more,
|
|
168
|
+
"Resolve, delete these lines and the markers, then run `node scripts/mint-suite-census.mjs --apply`.",
|
|
169
|
+
"<<<<<<< ours — the union of both sides, except the entries named above",
|
|
170
|
+
JSON.stringify(merged, null, 2),
|
|
171
|
+
"=======",
|
|
172
|
+
JSON.stringify(theirs, null, 2),
|
|
173
|
+
">>>>>>> theirs",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n"));
|
|
145
176
|
process.exit(1);
|
|
146
177
|
}
|
|
147
178
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sarl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
// WHICH BARE CITATIONS COULD CARRY A SYMBOL, AND WHICH ONES DISAGREE WITH THEIR OWN PROSE.
|
|
5
|
+
//
|
|
6
|
+
// node scripts/citation-anchor-report.mjs the report
|
|
7
|
+
// node scripts/citation-anchor-report.mjs --list every row, not the first ten of each kind
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THIS REPORTS AND DOES NOT REWRITE ────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// citation-line-check states its own blindness: it cannot see a citation pointing at the WRONG LIVE LINE
|
|
12
|
+
// while that line is real code, and it says the fix is to cite the symbol. The obvious next step is a
|
|
13
|
+
// codemod — the line is known and the file is known, so read the enclosing symbol out of the file and
|
|
14
|
+
// append it. Measured before building, that is the wrong move, and the measurement is worth keeping
|
|
15
|
+
// beside the thing it refutes.
|
|
16
|
+
//
|
|
17
|
+
// A codemod reads where the number CURRENTLY POINTS and writes that down. It cannot tell a citation that
|
|
18
|
+
// is right from one that has drifted, because both point somewhere, and appending the destination makes
|
|
19
|
+
// either self-consistent. Driven against the two citations a hand audit had already proved wrong:
|
|
20
|
+
//
|
|
21
|
+
// verify.mjs line 1539 would gain the name coverageEntryList — and the line is still
|
|
22
|
+
// `entries.push(e);` and the token it claims to site is minted three other places.
|
|
23
|
+
// stages.mjs line 844 would gain the name inputsForReference — and the line is still a comment,
|
|
24
|
+
// and that citation carries `[citation unverified]` today, which is a reader having
|
|
25
|
+
// doubted it. The rewrite strips the doubt and leaves a green tick.
|
|
26
|
+
//
|
|
27
|
+
// So the mechanism turns roughly a hundred unverifiable citations into verified-looking ones. This script
|
|
28
|
+
// produces the same information as a WORKLIST instead, and changes nothing.
|
|
29
|
+
//
|
|
30
|
+
// ── WHAT IT MEASURES, AND THE TWO RULES THAT ARE NOT OBVIOUS ────────────────
|
|
31
|
+
//
|
|
32
|
+
// THE ANCHOR IS THE TOP-LEVEL DECLARATION, NOT THE INNERMOST ONE. The innermost is the literal reading of
|
|
33
|
+
// "the symbol enclosing that line" and it is wrong here: measured over this tree it names a local or a
|
|
34
|
+
// one-to-three-letter binding 29 times — `e`, `hit`, `ob`, `one`, `out`, `known`, `unplaced` — against 2
|
|
35
|
+
// for the top-level rule, over the same 115 citations. An anchor nobody would have written by hand is not
|
|
36
|
+
// an anchor; it is the number wearing a name.
|
|
37
|
+
//
|
|
38
|
+
// A CITATION POINTING INSIDE A FUNCTION IT NAMES IS CORRECT, and the innermost rule reports it as a
|
|
39
|
+
// disagreement. Two of this report's first six "confirmed" rows were that bug rather than a bad citation:
|
|
40
|
+
// publish-inputs.mjs cites line 65 of mcp-server/lib/coverage.mjs and names `assertValidatorCoverage`, declared at
|
|
41
|
+
// 63, which is exactly right. The rule below is why they no longer appear.
|
|
42
|
+
import { readFileSync } from "node:fs";
|
|
43
|
+
import { join, dirname } from "node:path";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
import { corpusOf, citationsIn, indexByBasename } from "./citation-line-check.mjs";
|
|
46
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
47
|
+
|
|
48
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
49
|
+
|
|
50
|
+
// The gate's own matchers, restated here rather than imported because they are not exported: keeping them
|
|
51
|
+
// identical is the property, so any drift between the two is a defect in this file and not in the gate.
|
|
52
|
+
const ANY_DECL = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+[A-Za-z_$]/;
|
|
53
|
+
const NAME_OF = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/;
|
|
54
|
+
const INDENT_OF = (l) => (String(l).match(/^[ \t]*/) || [""])[0].length;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The top-level declaration whose span contains `line`, or null when the line is not in code at all.
|
|
58
|
+
* The span ends at the next top-level declaration, which is constructRange's sibling rule at indent 0.
|
|
59
|
+
* PURE.
|
|
60
|
+
*/
|
|
61
|
+
export function topLevelAnchor(lines, line) {
|
|
62
|
+
let best = null;
|
|
63
|
+
for (let d = 0; d < lines.length; d++) {
|
|
64
|
+
if (!ANY_DECL.test(lines[d]) || INDENT_OF(lines[d]) !== 0) continue;
|
|
65
|
+
const name = (lines[d].match(NAME_OF) || [])[1];
|
|
66
|
+
if (!name) continue;
|
|
67
|
+
let end = d + 1;
|
|
68
|
+
while (end < lines.length && !(ANY_DECL.test(lines[end]) && INDENT_OF(lines[end]) === 0)) end++;
|
|
69
|
+
if (line >= d + 1 && line <= end && (!best || d + 1 > best.declaredAt)) best = { name, declaredAt: d + 1, end };
|
|
70
|
+
}
|
|
71
|
+
return best;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The identifiers the citing line CLAIMS, which is not every word in it. Only a backticked span or a name
|
|
76
|
+
* shaped like code counts — camelCase, SCREAMING_SNAKE, or a `$`/`_` in it.
|
|
77
|
+
*
|
|
78
|
+
* MEASURED, NOT ASSUMED: taking every word put "spec", "one", "model", "skills" and "config" forward as
|
|
79
|
+
* identifier claims, because each is also declared somewhere in some cited file, and the drifted count
|
|
80
|
+
* went from a handful to 120. A report whose worklist is mostly English words is a worklist nobody reads.
|
|
81
|
+
* Longest first, so a suffix never shadows its owner.
|
|
82
|
+
*/
|
|
83
|
+
const namesIn = (sentence) => {
|
|
84
|
+
const s = String(sentence);
|
|
85
|
+
const ticked = [...s.matchAll(/`([A-Za-z_$][\w$]*)`/g)].map((m) => m[1]);
|
|
86
|
+
const shaped = [...s.matchAll(/[A-Za-z_$][\w$]{2,}/g)].map((m) => m[0])
|
|
87
|
+
.filter((n) => /[a-z][A-Z]/.test(n) || /^[A-Z0-9_]{3,}$/.test(n) || /[_$]/.test(n));
|
|
88
|
+
return [...new Set([...ticked, ...shaped])].sort((a, b) => b.length - a.length);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One row per bare citation whose target sits in code. PURE given its readers.
|
|
93
|
+
*
|
|
94
|
+
* `verdict` is one of:
|
|
95
|
+
* corroborated the citing prose names the anchor — safe to append, and the only class that is
|
|
96
|
+
* silent the prose names no identifier at all, so there is nothing to check the anchor against
|
|
97
|
+
* disagrees the prose names something else that is NOT declared in the cited file — weak evidence,
|
|
98
|
+
* because it is usually a caller or a type rather than a claim about this file
|
|
99
|
+
* drifted the prose names something DECLARED ELSEWHERE in the cited file, and the citation does
|
|
100
|
+
* not point inside it. That is the shape of a number that moved.
|
|
101
|
+
*/
|
|
102
|
+
export function anchorRows(citations, linesOf) {
|
|
103
|
+
const rows = [];
|
|
104
|
+
for (const c of citations) {
|
|
105
|
+
if (c.symbols?.length) continue; // already checkable
|
|
106
|
+
const target = linesOf(c.path ?? c.cited);
|
|
107
|
+
if (!target) continue;
|
|
108
|
+
const anchor = topLevelAnchor(target, c.start);
|
|
109
|
+
if (!anchor) { rows.push({ ...c, verdict: "no-code", anchor: null }); continue; }
|
|
110
|
+
const prose = namesIn(linesOf(c.from)?.[c.atLine - 1] ?? "");
|
|
111
|
+
if (prose.includes(anchor.name)) { rows.push({ ...c, verdict: "corroborated", anchor }); continue; }
|
|
112
|
+
const others = prose.filter((n) => n !== anchor.name);
|
|
113
|
+
if (!others.length) { rows.push({ ...c, verdict: "silent", anchor }); continue; }
|
|
114
|
+
let elsewhere = null;
|
|
115
|
+
for (const n of others) {
|
|
116
|
+
const decl = new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|const|let|var|class)\\s+${n.replace(/\$/g, "\\$")}\\b`);
|
|
117
|
+
for (let i = 0; i < target.length; i++) {
|
|
118
|
+
if (!decl.test(target[i])) continue;
|
|
119
|
+
// THE CITATION POINTING INSIDE WHAT IT NAMES IS CORRECT, not drifted — see the header.
|
|
120
|
+
const span = topLevelAnchor(target, i + 1);
|
|
121
|
+
if (span && c.start >= span.declaredAt && c.start <= span.end) { elsewhere = null; i = target.length; n; break; }
|
|
122
|
+
elsewhere = { name: n, declaredAt: i + 1 };
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (elsewhere) break;
|
|
126
|
+
}
|
|
127
|
+
rows.push({ ...c, verdict: elsewhere ? "drifted" : "disagrees", anchor, elsewhere });
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function main() {
|
|
133
|
+
const loaded = corpusOf(ROOT);
|
|
134
|
+
if (loaded == null) {
|
|
135
|
+
console.log("citation-anchor-report did not run — no tracked corpus. This is a SKIP, not a pass.");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
const { files, corpus } = loaded;
|
|
139
|
+
const text = new Map(corpus.map((c) => [c.file, c.text.split("\n")]));
|
|
140
|
+
const linesOf = (p) => {
|
|
141
|
+
if (text.has(p)) return text.get(p);
|
|
142
|
+
try { const l = readFileSync(join(ROOT, p), "utf8").split("\n"); text.set(p, l); return l; } catch { return null; }
|
|
143
|
+
};
|
|
144
|
+
const citations = citationsIn(corpus, indexByBasename(files));
|
|
145
|
+
const bare = citations.filter((c) => !c.symbols?.length);
|
|
146
|
+
const rows = anchorRows(citations, linesOf);
|
|
147
|
+
const by = (v) => rows.filter((r) => r.verdict === v);
|
|
148
|
+
const all = process.argv.includes("--list");
|
|
149
|
+
const show = (v) => (all ? by(v) : by(v).slice(0, 10));
|
|
150
|
+
|
|
151
|
+
console.log(`citation-anchor-report — ${citations.length} citation(s), ${citations.length - bare.length} already name a symbol, ${bare.length} BARE`);
|
|
152
|
+
console.log(` ${by("no-code").length} of the bare point at prose, comments, skill documents or data — there is NO symbol to cite,`);
|
|
153
|
+
console.log(" and the gate's own advice cannot be followed on them. That is a guard-design question, not a chore.");
|
|
154
|
+
console.log(` ${rows.length - by("no-code").length} point at a line inside code:`);
|
|
155
|
+
console.log(` corroborated ${by("corroborated").length} the citing prose names the enclosing symbol — appending it is safe`);
|
|
156
|
+
console.log(` drifted ${by("drifted").length} the prose names something declared ELSEWHERE in the cited file`);
|
|
157
|
+
console.log(` disagrees ${by("disagrees").length} the prose names something not declared there — usually a caller, weak evidence`);
|
|
158
|
+
console.log(` silent ${by("silent").length} the prose names nothing to check the anchor against`);
|
|
159
|
+
|
|
160
|
+
if (by("drifted").length) {
|
|
161
|
+
console.log("\nDRIFTED — read these first. Each names a symbol this file declares somewhere the citation does not point:");
|
|
162
|
+
for (const r of show("drifted")) {
|
|
163
|
+
console.log(` ${r.from}:${r.atLine}`);
|
|
164
|
+
console.log(` cites ${r.cited}:${r.start}, which is inside ${r.anchor.name}`);
|
|
165
|
+
console.log(` names ${r.elsewhere.name}, declared at ${r.elsewhere.declaredAt}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (by("corroborated").length) {
|
|
169
|
+
console.log("\nCORROBORATED — the anchor can be appended by hand with the prose as the check:");
|
|
170
|
+
for (const r of show("corroborated")) console.log(` ${r.from}:${r.atLine} ${r.cited}:${r.start} → ${r.anchor.name}`);
|
|
171
|
+
}
|
|
172
|
+
if (!all && (by("drifted").length > 10 || by("corroborated").length > 10)) console.log("\n (--list for every row)");
|
|
173
|
+
|
|
174
|
+
// A REPORT EXITS 0. Its `drifted` class is evidence for a person, not a decidable defect: the prose can
|
|
175
|
+
// name a symbol for a reason the citation is not about, and this file's own header records two rows it
|
|
176
|
+
// got wrong before the rule was fixed. A worklist that fails a build teaches people to silence it.
|
|
177
|
+
console.log("\nThis is a worklist, not a gate — it exits 0 on every finding. citation-line-check is the gate.");
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isEntrypoint(import.meta.url)) main();
|
package/scripts/dead-names.mjs
CHANGED
|
@@ -47,7 +47,7 @@ export const GUARD = "dead-names (#853)";
|
|
|
47
47
|
// agent identities for individuals at this firm, shipped in an operator script's default roster, in a
|
|
48
48
|
// systemd unit watching their queues, and in two test fixtures. That out-of-scope note ruled `alex`
|
|
49
49
|
// and `sam` INVENTED, which is why those stay; these two were never covered by that ruling.
|
|
50
|
-
export const DEAD_NAME_RE = /lorestar|lore_(pool|url|flags)|
|
|
50
|
+
export const DEAD_NAME_RE = /lorestar|lore_(pool|url|flags)|clearance_lore|lorectl|loreurl|lorecontrols|trademark-lore|\blore\b|\baughra|clawdi-(lisa|lidia)/i;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Where a true match is somebody else's word rather than our dead name. NAMED, with the reason, and
|
|
@@ -78,6 +78,43 @@ function published() {
|
|
|
78
78
|
return Array.isArray(versions) ? versions : [versions];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** How long to keep asking the registry to show a write it has already accepted. */
|
|
82
|
+
export const CONFIRM_BUDGET_MS = 90_000;
|
|
83
|
+
const CONFIRM_STEP_MS = 3_000;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Confirm writes the registry has ACCEPTED but may not be serving yet.
|
|
87
|
+
*
|
|
88
|
+
* WHY THIS IS A SECOND PASS AND NOT A READ AFTER EACH WRITE. `npm deprecate` returns when the registry
|
|
89
|
+
* has taken the write, not when every reader can see it. Reading back one line later asks a question
|
|
90
|
+
* the registry has not finished answering, and on 2026-09-17 that reported ELEVEN successful
|
|
91
|
+
* deprecations as failures — run 35219705516, where the messages were all in place minutes later and
|
|
92
|
+
* the job had already exited 1. The outside read was the truth and the job's red was not evidence.
|
|
93
|
+
*
|
|
94
|
+
* SO THE WRITES GO FIRST AND THE READING COMES AFTER, which also collapses a per-version read into one
|
|
95
|
+
* pass over what is left. Anything still unseen is asked for again until the budget runs out.
|
|
96
|
+
*
|
|
97
|
+
* A WRITE THAT WAS ACCEPTED AND IS NOT YET VISIBLE IS NOT A FAILED WRITE, and the caller is told so in
|
|
98
|
+
* those words rather than having the two folded together. `npm deprecate` exiting 0 is what says the
|
|
99
|
+
* write happened; this pass says whether the registry is serving it yet.
|
|
100
|
+
*/
|
|
101
|
+
export async function confirmWrites({ versions, read, sleep, now = () => Date.now(),
|
|
102
|
+
budgetMs = CONFIRM_BUDGET_MS, stepMs = CONFIRM_STEP_MS }) {
|
|
103
|
+
const pending = new Set(versions);
|
|
104
|
+
const confirmed = [];
|
|
105
|
+
const started = now();
|
|
106
|
+
while (pending.size) {
|
|
107
|
+
for (const v of [...pending]) {
|
|
108
|
+
let msg = null;
|
|
109
|
+
try { msg = read(v); } catch { msg = null; } // a read that threw is asked again, not judged
|
|
110
|
+
if (msg) { confirmed.push(v); pending.delete(v); }
|
|
111
|
+
}
|
|
112
|
+
if (!pending.size || now() - started >= budgetMs) break;
|
|
113
|
+
await sleep(stepMs);
|
|
114
|
+
}
|
|
115
|
+
return { confirmed, unconfirmed: [...pending], waitedMs: now() - started };
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
/** One version's deprecation message, read by its own exact spec. Null when it carries none. */
|
|
82
119
|
function deprecationOf(version) {
|
|
83
120
|
// `--json` on a field that is absent prints nothing at all, which JSON.parse refuses. An empty read is
|
|
@@ -89,7 +126,7 @@ function deprecationOf(version) {
|
|
|
89
126
|
return typeof v === "string" && v.trim() ? v : null;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
function main() {
|
|
129
|
+
async function main() {
|
|
93
130
|
const argv = process.argv.slice(2);
|
|
94
131
|
const at = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
95
132
|
const DRY = argv.includes("--dry-run");
|
|
@@ -115,6 +152,7 @@ function main() {
|
|
|
115
152
|
console.log(`deprecate-below: ${all.length} published version(s); ${candidates.length} below ${below}.`);
|
|
116
153
|
|
|
117
154
|
let done = 0, skipped = 0, failed = 0;
|
|
155
|
+
const wrote = [];
|
|
118
156
|
for (const v of candidates) {
|
|
119
157
|
let existing;
|
|
120
158
|
try { existing = deprecationOf(v); }
|
|
@@ -132,15 +170,35 @@ function main() {
|
|
|
132
170
|
failed += 1;
|
|
133
171
|
continue;
|
|
134
172
|
}
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
// ACCEPTED. Whether the registry is SERVING it yet is asked once, after every write, below.
|
|
174
|
+
console.log(` ${v}: deprecate accepted`);
|
|
175
|
+
wrote.push(v);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── NOW ASK WHETHER THE REGISTRY IS SERVING THEM ────────────────────────────────────────────────
|
|
179
|
+
let confirmed = [], unconfirmed = [], waitedMs = 0;
|
|
180
|
+
if (wrote.length && !DRY) {
|
|
181
|
+
({ confirmed, unconfirmed, waitedMs } = await confirmWrites({
|
|
182
|
+
versions: wrote,
|
|
183
|
+
read: (v) => deprecationOf(v),
|
|
184
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
185
|
+
}));
|
|
186
|
+
done += confirmed.length;
|
|
187
|
+
for (const v of confirmed) console.log(` ${v}: serving the message`);
|
|
188
|
+
for (const v of unconfirmed) {
|
|
189
|
+
console.error(` ${v}: WRITE ACCEPTED, not yet served after ${Math.round(waitedMs / 1000)}s — `
|
|
190
|
+
+ "the registry took it and is not showing it yet. Read the version from outside before treating "
|
|
191
|
+
+ "this as undone; re-running is safe and will report it as already deprecated.");
|
|
192
|
+
}
|
|
193
|
+
} else if (DRY) {
|
|
194
|
+
done += wrote.length;
|
|
140
195
|
}
|
|
141
196
|
|
|
142
|
-
console.log(`deprecate-below: ${done} deprecated, ${skipped} already carried a message,
|
|
197
|
+
console.log(`deprecate-below: ${done} deprecated, ${skipped} already carried a message, `
|
|
198
|
+
+ `${unconfirmed.length} accepted but not yet served, ${failed} failed.`);
|
|
199
|
+
// ONLY A FAILED WRITE IS A FAILURE. A write the registry accepted and has not caught up on is not a
|
|
200
|
+
// release gone wrong, and exiting 1 for it is what turned eleven successful deprecations into a red.
|
|
143
201
|
return failed ? 1 : 0;
|
|
144
202
|
}
|
|
145
203
|
|
|
146
|
-
if (process.argv[1] && process.argv[1].endsWith("deprecate-below.mjs")) process.exit(main());
|
|
204
|
+
if (process.argv[1] && process.argv[1].endsWith("deprecate-below.mjs")) process.exit(await main());
|
|
@@ -108,7 +108,7 @@ export function backlogFinding(backlog, dir, stuckAfterSec = STUCK_AFTER_SEC) {
|
|
|
108
108
|
* The queue directories a `.path` unit actually watches, with `%h` resolved.
|
|
109
109
|
*
|
|
110
110
|
* COMMENTED LINES ARE NOT WATCHES, and that matters here more than usual: the headless glob
|
|
111
|
-
* (`%h/
|
|
111
|
+
* (`%h/clearance-queue/*.json`) ships commented out, so a standalone deployment that never uncommented it
|
|
112
112
|
* has event-driven pickup dead on the one queue it uses. Reading the file without honouring `#` would
|
|
113
113
|
* report that box as watched.
|
|
114
114
|
*
|