clearotron 0.3.2-beta.11 → 0.3.2-beta.12
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 +1 -1
- package/bin/clearotron.mjs +14 -0
- package/bin/connect.mjs +68 -3
- package/bin/key.mjs +6 -1
- package/bin/onboard.mjs +7 -1
- package/bin/passphrase.mjs +4 -2
- package/bin/start.mjs +12 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +16 -0
- package/driver/contract-vocabulary.mjs +5 -5
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/supplemental.mjs +22 -5
- package/driver/knockout-next-step.mjs +72 -0
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +26 -0
- package/driver/portal-report.mjs +21 -2
- package/driver/portal-service.mjs +8 -8
- package/driver/publish/render-knockout.mjs +14 -9
- package/driver/register-availability.mjs +1 -1
- package/driver/register-plan.mjs +81 -11
- package/driver/result-noun-fields.mjs +3 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/suite-census.json +47 -17
- package/driver/verify-knockout.mjs +0 -27
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
- package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/ask-ai-render-check.mjs +26 -1
- package/scripts/dead-names.mjs +17 -16
- package/scripts/env-audit.mjs +6 -0
- package/scripts/repo-writes.mjs +42 -0
- package/scripts/report-sections-render-check.mjs +245 -0
- package/scripts/report-theme-render-check.mjs +31 -3
- package/scripts/settings-render-check.mjs +10 -7
- package/scripts/test-run.mjs +63 -4
- package/shared/client-door.mjs +20 -0
- package/shared/parent-watch.mjs +33 -0
- package/shared/running-start.mjs +14 -3
- package/shared/scope.mjs +22 -3
|
@@ -18201,11 +18201,15 @@ function readFrameSections(data, sameSource) {
|
|
|
18201
18201
|
const out = [];
|
|
18202
18202
|
for (const raw of msg.sections) {
|
|
18203
18203
|
if (typeof raw !== "object" || raw === null) continue;
|
|
18204
|
-
const { id, label } = raw;
|
|
18204
|
+
const { id, label, top } = raw;
|
|
18205
18205
|
if (typeof id !== "string" || typeof label !== "string") continue;
|
|
18206
18206
|
const trimmed = label.trim().slice(0, 40);
|
|
18207
18207
|
if (!id.trim() || !trimmed) continue;
|
|
18208
|
-
out.push({
|
|
18208
|
+
out.push(typeof top === "number" && Number.isFinite(top) && top >= 0 ? {
|
|
18209
|
+
id,
|
|
18210
|
+
label: trimmed,
|
|
18211
|
+
top: Math.round(top)
|
|
18212
|
+
} : {
|
|
18209
18213
|
id,
|
|
18210
18214
|
label: trimmed
|
|
18211
18215
|
});
|
|
@@ -18213,6 +18217,16 @@ function readFrameSections(data, sameSource) {
|
|
|
18213
18217
|
}
|
|
18214
18218
|
return out;
|
|
18215
18219
|
}
|
|
18220
|
+
function sectionsReached(sections, line, where) {
|
|
18221
|
+
if (where.atTop) return 0;
|
|
18222
|
+
if (where.atFoot) return sections.length;
|
|
18223
|
+
let n = 0;
|
|
18224
|
+
for (const s of sections) {
|
|
18225
|
+
if (typeof s.top !== "number" || s.top > line) break;
|
|
18226
|
+
n += 1;
|
|
18227
|
+
}
|
|
18228
|
+
return n;
|
|
18229
|
+
}
|
|
18216
18230
|
function readFrameTheme(data, sameSource) {
|
|
18217
18231
|
if (!sameSource) return null;
|
|
18218
18232
|
if (typeof data !== "object" || data === null) return null;
|
|
@@ -18243,6 +18257,8 @@ function usePortalTheme() {
|
|
|
18243
18257
|
}, []);
|
|
18244
18258
|
return theme;
|
|
18245
18259
|
}
|
|
18260
|
+
var TOPBAR_PX = 56;
|
|
18261
|
+
var LANDING_PX = 10;
|
|
18246
18262
|
function useReportFrame() {
|
|
18247
18263
|
const ref = (0, import_react.useRef)(null);
|
|
18248
18264
|
const [height, setHeight] = (0, import_react.useState)(FIRST_PAINT);
|
|
@@ -18276,7 +18292,7 @@ function useReportFrame() {
|
|
|
18276
18292
|
if (jump !== null) {
|
|
18277
18293
|
if (frame) {
|
|
18278
18294
|
const head = document.querySelector(".report-head");
|
|
18279
|
-
const chrome =
|
|
18295
|
+
const chrome = TOPBAR_PX + (head instanceof HTMLElement ? head.offsetHeight : 48) + LANDING_PX;
|
|
18280
18296
|
const y = frame.getBoundingClientRect().top + window.scrollY + jump - chrome;
|
|
18281
18297
|
window.scrollTo({
|
|
18282
18298
|
top: Math.max(0, y),
|
|
@@ -18478,10 +18494,45 @@ function Result({ ctx, runId, markSlug }) {
|
|
|
18478
18494
|
const run = (0, import_react.useMemo)(() => runs.find((r) => r.runId === runId) ?? null, [runs, runId]);
|
|
18479
18495
|
const reads = (0, import_react.useMemo)(() => run ? readsFor(runs, run) : [], [runs, run]);
|
|
18480
18496
|
const frame = useReportFrame();
|
|
18481
|
-
const [
|
|
18497
|
+
const [reached, setReached] = (0, import_react.useState)(0);
|
|
18498
|
+
const sectionList = frame.sections;
|
|
18499
|
+
const frameRef = frame.ref;
|
|
18482
18500
|
(0, import_react.useEffect)(() => {
|
|
18483
|
-
|
|
18484
|
-
|
|
18501
|
+
if (!sectionList || sectionList.length < 2) {
|
|
18502
|
+
setReached(0);
|
|
18503
|
+
return;
|
|
18504
|
+
}
|
|
18505
|
+
let queued = false;
|
|
18506
|
+
const measure = () => {
|
|
18507
|
+
queued = false;
|
|
18508
|
+
const el = frameRef.current;
|
|
18509
|
+
if (!el) return;
|
|
18510
|
+
const head = document.querySelector(".report-head");
|
|
18511
|
+
const lineOnScreen = (head instanceof HTMLElement ? head.getBoundingClientRect().bottom : TOPBAR_PX) + LANDING_PX + 2;
|
|
18512
|
+
const where = {
|
|
18513
|
+
atTop: document.documentElement.scrollHeight > window.innerHeight + 2 && window.scrollY <= 0,
|
|
18514
|
+
atFoot: window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2
|
|
18515
|
+
};
|
|
18516
|
+
setReached(sectionsReached(sectionList, lineOnScreen - el.getBoundingClientRect().top, where));
|
|
18517
|
+
};
|
|
18518
|
+
const later = () => {
|
|
18519
|
+
if (!queued) {
|
|
18520
|
+
queued = true;
|
|
18521
|
+
requestAnimationFrame(measure);
|
|
18522
|
+
}
|
|
18523
|
+
};
|
|
18524
|
+
measure();
|
|
18525
|
+
window.addEventListener("scroll", later, { passive: true });
|
|
18526
|
+
window.addEventListener("resize", later);
|
|
18527
|
+
return () => {
|
|
18528
|
+
window.removeEventListener("scroll", later);
|
|
18529
|
+
window.removeEventListener("resize", later);
|
|
18530
|
+
};
|
|
18531
|
+
}, [
|
|
18532
|
+
sectionList,
|
|
18533
|
+
frame.height,
|
|
18534
|
+
frameRef
|
|
18535
|
+
]);
|
|
18485
18536
|
const grouped = run !== null && showsAssessment(run, markSlug);
|
|
18486
18537
|
const { result: summaryResult } = useLoad(() => grouped ? api.runSummary(runId) : Promise.resolve({ kind: "notFound" }), [runId, grouped]);
|
|
18487
18538
|
const assessment = summaryResult?.kind === "ok" ? summaryResult.value : [];
|
|
@@ -18612,70 +18663,60 @@ function Result({ ctx, runId, markSlug }) {
|
|
|
18612
18663
|
]
|
|
18613
18664
|
}), hasThread(reads) || (frame.sections?.length ?? 0) > 1 ? (0, import_jsx_runtime.jsxs)("div", {
|
|
18614
18665
|
className: "report-nav",
|
|
18615
|
-
children: [
|
|
18616
|
-
|
|
18617
|
-
|
|
18618
|
-
|
|
18619
|
-
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18666
|
+
children: [hasThread(reads) ? (0, import_jsx_runtime.jsxs)("div", {
|
|
18667
|
+
style: {
|
|
18668
|
+
display: "flex",
|
|
18669
|
+
gap: 8,
|
|
18670
|
+
flexWrap: "wrap",
|
|
18671
|
+
alignItems: "center"
|
|
18672
|
+
},
|
|
18673
|
+
children: [(0, import_jsx_runtime.jsx)("span", {
|
|
18674
|
+
className: "eyebrow",
|
|
18675
|
+
style: { alignSelf: "center" },
|
|
18676
|
+
children: "Reads"
|
|
18677
|
+
}), reads.map((r) => {
|
|
18678
|
+
const active = r.runId === run.runId;
|
|
18679
|
+
return (0, import_jsx_runtime.jsxs)("button", {
|
|
18680
|
+
type: "button",
|
|
18681
|
+
className: "pill",
|
|
18682
|
+
"aria-current": active ? "true" : void 0,
|
|
18683
|
+
onClick: () => ctx.go(resultPath(r.runId)),
|
|
18684
|
+
style: {
|
|
18685
|
+
cursor: "pointer",
|
|
18686
|
+
display: "inline-flex",
|
|
18687
|
+
alignItems: "center",
|
|
18688
|
+
gap: 7,
|
|
18689
|
+
background: active ? "var(--accent-wash)" : "var(--surface-sunken)",
|
|
18690
|
+
borderColor: active ? "var(--accent)" : "var(--border-hairline)",
|
|
18691
|
+
color: active ? "var(--text-accent)" : "var(--text-muted)"
|
|
18692
|
+
},
|
|
18693
|
+
children: [r.tone ? (0, import_jsx_runtime.jsx)("span", {
|
|
18694
|
+
className: "dot",
|
|
18634
18695
|
style: {
|
|
18635
|
-
|
|
18636
|
-
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
|
|
18643
|
-
|
|
18644
|
-
|
|
18645
|
-
|
|
18646
|
-
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
|
|
18650
|
-
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18657
|
-
})
|
|
18658
|
-
|
|
18659
|
-
className: "report-sections",
|
|
18660
|
-
"aria-label": "Sections of this report",
|
|
18661
|
-
children: (frame.sections ?? []).map((sec, i) => {
|
|
18662
|
-
const now = atSection === null ? i === 0 : atSection === sec.id;
|
|
18663
|
-
return (0, import_jsx_runtime.jsxs)("button", {
|
|
18664
|
-
type: "button",
|
|
18665
|
-
className: "report-section",
|
|
18666
|
-
"aria-current": now ? "true" : void 0,
|
|
18667
|
-
onClick: () => {
|
|
18668
|
-
setAtSection(sec.id);
|
|
18669
|
-
frame.send("section", sec.id);
|
|
18670
|
-
},
|
|
18671
|
-
children: [(0, import_jsx_runtime.jsx)("span", {
|
|
18672
|
-
className: "report-section-dot",
|
|
18673
|
-
"aria-hidden": "true"
|
|
18674
|
-
}), sec.label]
|
|
18675
|
-
}, sec.id);
|
|
18676
|
-
})
|
|
18677
|
-
}) : null
|
|
18678
|
-
]
|
|
18696
|
+
background: `var(--tone-${r.tone})`,
|
|
18697
|
+
width: 7,
|
|
18698
|
+
height: 7
|
|
18699
|
+
}
|
|
18700
|
+
}) : null, readLabel(r)]
|
|
18701
|
+
}, r.runId);
|
|
18702
|
+
})]
|
|
18703
|
+
}) : null, (frame.sections?.length ?? 0) > 1 ? (0, import_jsx_runtime.jsx)("nav", {
|
|
18704
|
+
className: "report-sections",
|
|
18705
|
+
"aria-label": "Sections of this report",
|
|
18706
|
+
children: (frame.sections ?? []).map((sec, i) => {
|
|
18707
|
+
return (0, import_jsx_runtime.jsxs)("button", {
|
|
18708
|
+
type: "button",
|
|
18709
|
+
className: "report-section",
|
|
18710
|
+
"data-reached": i < reached ? "true" : void 0,
|
|
18711
|
+
"aria-current": i === reached - 1 ? "true" : void 0,
|
|
18712
|
+
onClick: () => frame.send("section", sec.id),
|
|
18713
|
+
children: [(0, import_jsx_runtime.jsx)("span", {
|
|
18714
|
+
className: "report-section-dot",
|
|
18715
|
+
"aria-hidden": "true"
|
|
18716
|
+
}), sec.label]
|
|
18717
|
+
}, sec.id);
|
|
18718
|
+
})
|
|
18719
|
+
}) : null]
|
|
18679
18720
|
}) : null]
|
|
18680
18721
|
}),
|
|
18681
18722
|
frame.failed ? (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-w8GFZftk.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="/portal/assets/index-7Lq-dXDV.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
|
56
56
|
<div id="root"></div>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.12",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.12",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -133,6 +133,10 @@ let deck = 'connected'
|
|
|
133
133
|
// travels the real injected script to the real listener. Two cards at fixed places: a numbered finding,
|
|
134
134
|
// and an "Also considered" card, which carries the button and no number.
|
|
135
135
|
let reportDoc = 'stub'
|
|
136
|
+
// A SLOW RUNNER, ON DEMAND. The finding's document is held back this long before it is served, which is
|
|
137
|
+
// how a CI runner that loaded it late is reproduced: the press below has to wait for the document, not
|
|
138
|
+
// for a fixed time. Zero, the default, serves it at once.
|
|
139
|
+
const FRAME_DELAY_MS = Number(process.env.ASK_AI_CHECK_FRAME_DELAY_MS ?? 0) || 0
|
|
136
140
|
const FINDING_DOC = () => prepareReportForEmbed(`<!doctype html><html><head><meta charset="utf-8"><title>report</title></head>
|
|
137
141
|
<body style="margin:0;font-family:sans-serif;background:#fff;height:400px">
|
|
138
142
|
<div class="card" id="c3" style="position:absolute;top:40px;left:20px"><div class="cardhead"><span class="fnum">3</span>
|
|
@@ -159,7 +163,8 @@ const server = createServer((req, res) => {
|
|
|
159
163
|
// the height a published report's bridge announces, so the header draws the Export menu a reader meets.
|
|
160
164
|
if (p.startsWith('/portal/report/') && reportDoc === 'finding') {
|
|
161
165
|
res.writeHead(200, { 'content-type': 'text/html' })
|
|
162
|
-
|
|
166
|
+
const doc = FINDING_DOC()
|
|
167
|
+
return FRAME_DELAY_MS ? setTimeout(() => res.end(doc), FRAME_DELAY_MS) : res.end(doc)
|
|
163
168
|
}
|
|
164
169
|
if (p.startsWith('/portal/report/')) {
|
|
165
170
|
res.writeHead(200, { 'content-type': 'text/html' })
|
|
@@ -433,9 +438,29 @@ const arrived = await value(PROBE)
|
|
|
433
438
|
// null origin and the point is the whole path — the injected script hears the press, posts it, and the
|
|
434
439
|
// report screen opens its control. The cards sit at fixed places in the document, so the frame's own
|
|
435
440
|
// position on the page is all that has to be measured.
|
|
441
|
+
// READY, NOT A FIXED WAIT. The press lands on a finding's button, and it does something only once the
|
|
442
|
+
// document in the frame has loaded and its bridge is listening. A fixed pause after navigating was not
|
|
443
|
+
// always that on a CI runner: the press arrived first and opened nothing (measured on beta-12, 2026-09-19,
|
|
444
|
+
// with no change to the portal or the bridge since a green run). The bridge posts the document's height
|
|
445
|
+
// from the same script that listens for the press, so a frame that has left its first-paint height has a
|
|
446
|
+
// listener. The Ask AI control must be on the header too, since the press opens it. Both are waited for,
|
|
447
|
+
// and the frame's place must hold still across two reads, so the press is aimed where the button is.
|
|
448
|
+
const FIRST_PAINT = Number(/export const FIRST_PAINT = (\d+)/.exec(readFileSync(join(DIST, '..', 'src', 'contract', 'reportFrame.ts'), 'utf8'))?.[1] ?? NaN)
|
|
449
|
+
const frameReady = async () => {
|
|
450
|
+
let last = null
|
|
451
|
+
for (let i = 0; i < 200; i++) {
|
|
452
|
+
const now = await value(`(() => { const f = document.querySelector('iframe'); if (!f || !document.querySelector('[data-ask-ai] > button')) return null;
|
|
453
|
+
const r = f.getBoundingClientRect(); return Math.round(r.top) + ':' + Math.round(r.height) })()`)
|
|
454
|
+
if (now && now === last && !now.endsWith(`:${FIRST_PAINT}`)) return true
|
|
455
|
+
last = now
|
|
456
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
457
|
+
}
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
436
460
|
const pressInFrame = async (top) => {
|
|
437
461
|
await value('(() => { window.scrollTo(0, 0); return true })()')
|
|
438
462
|
await new Promise((r) => setTimeout(r, 200))
|
|
463
|
+
if (!(await frameReady())) return false
|
|
439
464
|
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
465
|
if (!at) return false
|
|
441
466
|
const x = Math.round(at.x + 140), y = Math.round(at.y + top + 16)
|
package/scripts/dead-names.mjs
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// node scripts/dead-names.mjs report; exit 1 if a dead name is back
|
|
7
7
|
// node scripts/dead-names.mjs --json the same, as JSON
|
|
8
8
|
//
|
|
9
|
-
// ruled Lore and Aughra dead names that may appear nowhere, and the purge is done: the issue's
|
|
10
|
-
// own pattern returned 58 files when it was written and
|
|
9
|
+
// The owner ruled Lore and Aughra dead names that may appear nowhere, and the purge is done: the issue's
|
|
10
|
+
// own pattern returned 58 files when it was written and ONE when this guard landed, a dictionary snippet
|
|
11
11
|
// about hunting inside captured search results. What does not exist is anything to keep it that way. A
|
|
12
12
|
// name published once stays published, so the cost of a re-introduction is not a revert — it is a name
|
|
13
13
|
// in the history of a repository that is about to be cut in public.
|
|
@@ -42,37 +42,38 @@ export const GUARD = "dead-names (#853)";
|
|
|
42
42
|
// the issue names as deployed surfaces, and `_` is a word character, so a trailing `\b` would miss the
|
|
43
43
|
// environment variable while catching the prose. No English word begins with those six letters, so the
|
|
44
44
|
// leading boundary is all the protection the arm needs — measured: zero hits in the tree either way.
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
export const DEAD_NAME_RE = /lorestar|lore_(pool|url|flags)|clearance_lore|lorectl|loreurl|lorecontrols|trademark-lore|\blore\b|\baughra
|
|
45
|
+
// A PER-PERSON IDENTITY IS THE SAME KIND OF NAME, AND IT IS NOT WRITTEN HERE. An agent identity named
|
|
46
|
+
// after a real person must never reappear either, and the mechanism is identical. But a public guard
|
|
47
|
+
// that spells out a person's name publishes the name it exists to keep out, so no such arm is written
|
|
48
|
+
// into this file: `deadNameHits` takes its pattern as an argument, and the test drives the mechanism
|
|
49
|
+
// with an invented identity. The names the tests give agents and requesters are invented too.
|
|
50
|
+
export const DEAD_NAME_RE = /lorestar|lore_(pool|url|flags)|clearance_lore|lorectl|loreurl|lorecontrols|trademark-lore|\blore\b|\baughra/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
|
|
54
54
|
* scoped to the file — never a blanket pattern, because a blanket would re-admit the name everywhere.
|
|
55
|
+
* The first exemption was a dictionary snippet in the demo's captured common-law results; the demo was
|
|
56
|
+
* re-captured without it, so that exemption was deleted rather than carried.
|
|
55
57
|
*/
|
|
56
58
|
export const EXEMPTIONS = [
|
|
57
59
|
{
|
|
58
|
-
file: "
|
|
59
|
-
why: "
|
|
60
|
-
+ "
|
|
61
|
-
+ "
|
|
62
|
-
+ "satisfy a lint, which is the one thing a receipt may never be.",
|
|
60
|
+
file: "driver/wordlists/en.txt",
|
|
61
|
+
why: "the engine's list of ordinary English words, the dictionary of the one-letter neighbourhood "
|
|
62
|
+
+ "filter. `lore` is an ordinary English word and sits on it. The list only ever removes queries, "
|
|
63
|
+
+ "so taking the word out would change which searches a run makes, to satisfy a lint.",
|
|
63
64
|
},
|
|
64
65
|
];
|
|
65
66
|
|
|
66
67
|
const exemptionFor = (file) => EXEMPTIONS.find((e) => e.file === file) ?? null;
|
|
67
68
|
|
|
68
|
-
/** Every dead-name hit in a corpus of {file, text}. PURE. */
|
|
69
|
-
export function deadNameHits(corpus) {
|
|
69
|
+
/** Every dead-name hit in a corpus of {file, text}, against DEAD_NAME_RE unless a pattern is given. PURE. */
|
|
70
|
+
export function deadNameHits(corpus, pattern = DEAD_NAME_RE) {
|
|
70
71
|
const out = [];
|
|
71
72
|
for (const { file, text } of corpus ?? []) {
|
|
72
73
|
if (exemptionFor(file)) continue;
|
|
73
74
|
const lines = String(text ?? "").split("\n");
|
|
74
75
|
for (let i = 0; i < lines.length; i++) {
|
|
75
|
-
if (
|
|
76
|
+
if (pattern.test(lines[i])) out.push({ file, line: i + 1, text: lines[i].trim().slice(0, 160) });
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
return out;
|
package/scripts/env-audit.mjs
CHANGED
|
@@ -310,6 +310,12 @@ export function isProductFile(rel) {
|
|
|
310
310
|
if (/\.test\.mjs$/.test(rel)) return false;
|
|
311
311
|
if (/^e2e\//.test(rel)) return false;
|
|
312
312
|
if (/^scripts\/(e2e|test-run|ci-)/.test(rel)) return false;
|
|
313
|
+
// THE BROWSER CHECKS. CI and a developer run them against a fixture portal; no install runs one. A switch
|
|
314
|
+
// one of them reads, such as the delay that reproduces a slow runner, belongs to the check, and a row
|
|
315
|
+
// for it in `.env.example` would hand an operator a setting that changes nothing they run. Measured
|
|
316
|
+
// when this was added: HOME, PATH, TMPDIR and that delay were all the checks read, and the first three
|
|
317
|
+
// stay product, because shipping code reads them too.
|
|
318
|
+
if (/^scripts\/[a-z0-9-]+-render-check\.mjs$/.test(rel)) return false;
|
|
313
319
|
return true;
|
|
314
320
|
}
|
|
315
321
|
|
package/scripts/repo-writes.mjs
CHANGED
|
@@ -119,3 +119,45 @@ export function explainRepoWrites(rows) {
|
|
|
119
119
|
" touching the tree to tell the two apart.)",
|
|
120
120
|
];
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
// ── the home the run executes as ────────────────────────────────────────────────────────────────────
|
|
124
|
+
//
|
|
125
|
+
// THE SAME RULE, ONE FOLDER OUT. The suite is documented as offline with no side effects, and a fresh
|
|
126
|
+
// clone's run left `~/trademark/telemetry/trademark-mcp-access.jsonl` behind (measured 2026-09-19, one
|
|
127
|
+
// line per run): a test spawned the stdio server with the real home, and `doctor` on that machine then
|
|
128
|
+
// reported the client door's access log as being written. These are the product's own folders under a
|
|
129
|
+
// home: the pool, workspace, queue and telemetry, and the settings, records and revocation list. A run
|
|
130
|
+
// leaves them as it found them. The rest of a home — npm's cache, a browser's profile — belongs to the
|
|
131
|
+
// tools a run uses, and is theirs to write.
|
|
132
|
+
|
|
133
|
+
/** The product's own folders under a home, relative to it. */
|
|
134
|
+
export const HOME_DATA = Object.freeze(["trademark", join(".config", "clearotron")]);
|
|
135
|
+
|
|
136
|
+
/** Every path under the home's product folders, stamped. A folder that does not exist is not walked. */
|
|
137
|
+
export function snapshotHome(home) {
|
|
138
|
+
const out = new Map();
|
|
139
|
+
for (const rel of HOME_DATA) {
|
|
140
|
+
const p = join(resolve(home), rel);
|
|
141
|
+
let isDir = false;
|
|
142
|
+
try { isDir = lstatSync(p).isDirectory(); } catch { continue; }
|
|
143
|
+
if (!isDir) { out.set(p, "not-a-directory"); continue; }
|
|
144
|
+
out.set(p, "dir");
|
|
145
|
+
for (const [k, v] of snapshotRepo(p)) out.set(k, v);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What a reader is told when a run wrote under the home's product folders. */
|
|
151
|
+
export function explainHomeWrites(rows, home) {
|
|
152
|
+
return [
|
|
153
|
+
"",
|
|
154
|
+
`[test-run] THIS RUN WROTE UNDER THE HOME IT RAN AS (${home}), in the product's own folders:`,
|
|
155
|
+
...rows,
|
|
156
|
+
"",
|
|
157
|
+
" A test that starts a product command hands it a HOME under its own temp directory. Without one, the",
|
|
158
|
+
" command reads and writes the pool, queue, telemetry and settings of whoever runs the suite.",
|
|
159
|
+
"",
|
|
160
|
+
" (A service of this account writing there while the run was in flight prints this too. Run again",
|
|
161
|
+
" with HOME set to an empty temp directory to tell the two apart.)",
|
|
162
|
+
];
|
|
163
|
+
}
|