clearotron 0.3.0-beta.1 → 0.3.0-beta.2
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 +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +12 -10
- package/README.md +3 -2
- package/bin/brandowner.mjs +94 -1
- package/bin/onboard.mjs +156 -55
- package/bin/passphrase.mjs +23 -4
- package/bin/start.mjs +177 -38
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +5 -2
- package/docs/architecture/05-config-governance.md +3 -2
- package/driver/CHANGELOG.md +95 -0
- package/driver/demo-posture.mjs +59 -8
- package/driver/driver.config.mjs +36 -7
- package/driver/engine/child-record.mjs +93 -0
- package/driver/findings-model.mjs +6 -1
- package/driver/knockout-assess-record.mjs +6 -3
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +98 -3
- package/driver/portal-service.mjs +65 -29
- package/driver/publish/knockout.mjs +12 -1
- package/driver/publish/office-record-links.mjs +61 -10
- package/driver/publish/render-knockout.mjs +31 -11
- package/driver/publish/xlsx.mjs +33 -1
- package/driver/register-records.mjs +6 -0
- package/driver/run-requirements.mjs +24 -1
- package/driver/runner.mjs +10 -5
- package/driver/skills/knockout-assess/SKILL.md +1 -1
- package/driver/suite-census.json +174 -30
- package/driver/unit-inventory.mjs +109 -5
- package/driver/updater-identity.mjs +178 -0
- package/driver/usage-ledger.mjs +5 -5
- package/mcp-server/CHANGELOG.md +6 -0
- package/mcp-server/http-server.mjs +16 -13
- package/mcp-server/lib/driver.mjs +7 -0
- package/mcp-server/lib/knockout.mjs +14 -2
- package/mcp-server/lib/ops.mjs +38 -16
- package/mcp-server/package.json +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CWTHP0sH.js → index-CcFjgM78.js} +32 -17
- package/portal-ui/dist/assets/{index-KpytsmNH.css → index-CsCuPshD.css} +7 -2
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +3 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/scripts/changelog-plain-language.mjs +7 -30
- package/scripts/e2e.mjs +13 -2
- package/scripts/env-audit.mjs +10 -3
- package/scripts/env-classify.mjs +205 -12
- package/scripts/live-surface-check.mjs +74 -2
- package/scripts/plain-language-rules.mjs +103 -0
- package/scripts/release-note-required.mjs +118 -24
- package/scripts/release-notes-lint.mjs +22 -43
- package/scripts/release-version.mjs +59 -0
- package/scripts/revisit-render-check.mjs +6 -2
- package/scripts/text-difference.mjs +22 -0
- package/shared/env-local.mjs +25 -2
- package/shared/names-in-force.mjs +1 -0
- package/shared/reap-on-exit.mjs +27 -14
- package/shared/store-in-repo.mjs +147 -0
- package/shared/withheld-paths-access.mjs +6 -6
- package/shared/yes-no-echo.mjs +35 -0
|
@@ -131,7 +131,6 @@ export function recordLinksFor(findings, recordsByUri, provider) {
|
|
|
131
131
|
if (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(provider).toLowerCase())) return null;
|
|
132
132
|
const byUri = new Map();
|
|
133
133
|
const tally = { linked: {}, cited: {}, notRetrieved: 0 };
|
|
134
|
-
const bump = (o, k) => { o[k] = (o[k] ?? 0) + 1; };
|
|
135
134
|
for (const f of Array.isArray(findings) ? findings : []) {
|
|
136
135
|
for (const r of f?.owner?.registrations ?? []) {
|
|
137
136
|
const key = clean(r?.uri).toLowerCase();
|
|
@@ -140,17 +139,26 @@ export function recordLinksFor(findings, recordsByUri, provider) {
|
|
|
140
139
|
if (!rec) { byUri.set(key, null); tally.notRetrieved += 1; continue; }
|
|
141
140
|
const link = officeRecordLink(rec, key);
|
|
142
141
|
byUri.set(key, link);
|
|
143
|
-
|
|
144
|
-
else bump((tally.cited[link.office] ??= {}), link.reason);
|
|
142
|
+
tallyLink(tally, link);
|
|
145
143
|
}
|
|
146
144
|
}
|
|
145
|
+
return { byUri, tally, summary: summaryOf(tally, `record not retrieved ${tally.notRetrieved}`) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const bump = (o, k) => { o[k] = (o[k] ?? 0) + 1; };
|
|
149
|
+
function tallyLink(tally, link) {
|
|
150
|
+
if (link.href) bump(tally.linked, link.office);
|
|
151
|
+
else bump((tally.cited[link.office] ??= {}), link.reason);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The log line for a tally: how many were linked and how many cited by number, per office and reason. */
|
|
155
|
+
function summaryOf(tally, tail) {
|
|
147
156
|
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
148
157
|
const linked = Object.entries(tally.linked).map(([o, n]) => `${o} ${n}`).join(", ");
|
|
149
158
|
const cited = Object.entries(tally.cited).flatMap(([o, rs]) => Object.entries(rs).map(([r, n]) => `${o} ${n} ${r}`)).join(", ");
|
|
150
|
-
|
|
159
|
+
return `linked ${sum(Object.values(tally.linked))}${linked ? ` (${linked})` : ""}`
|
|
151
160
|
+ ` · cited by number ${sum(Object.values(tally.cited).flatMap((rs) => Object.values(rs)))}${cited ? ` (${cited})` : ""}`
|
|
152
|
-
+ ` ·
|
|
153
|
-
return { byUri, tally, summary };
|
|
161
|
+
+ ` · ${tail}`;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
const nameOf = (office) => OFFICE_RECORD_PAGES[office]?.name ?? office.toUpperCase();
|
|
@@ -174,6 +182,26 @@ export function officeReasonSentences(byUri) {
|
|
|
174
182
|
});
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
/**
|
|
186
|
+
* What a linked registration number opens, when one is linked, then the reasons per office, as the
|
|
187
|
+
* knockout says them under its listed filings. They are the sentences the clearance says in Scope; its
|
|
188
|
+
* renderer is frozen (render-frozen.test.mjs) and keeps its own copy of the first.
|
|
189
|
+
*/
|
|
190
|
+
export function officeLinkSentences(byUri) {
|
|
191
|
+
if (!(byUri instanceof Map)) return [];
|
|
192
|
+
const linked = [...byUri.values()].some((l) => l?.href);
|
|
193
|
+
return [...(linked ? ["A registration number shown as a link opens the office’s own page for that record."] : []),
|
|
194
|
+
...officeReasonSentences(byUri)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Why a registration is cited by number rather than linked, in the workbook's words. */
|
|
198
|
+
export function reasonCellFor(link) {
|
|
199
|
+
const name = nameOf(link.office);
|
|
200
|
+
if (link.reason === "no-page") return `No page for a single record at this register (${name}); cited by number`;
|
|
201
|
+
if (link.reason === "unknown-office") return `No page address held for this register (${name}); cited by number`;
|
|
202
|
+
return `Number not in the form this register's page address takes (${name}); cited by number`;
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
/** The workbook's Link cell for a finding: its first linked registration, else the first stated reason. */
|
|
178
206
|
export function linkCellFor(registrations, byUri) {
|
|
179
207
|
if (!(byUri instanceof Map)) return "";
|
|
@@ -182,8 +210,31 @@ export function linkCellFor(registrations, byUri) {
|
|
|
182
210
|
if (linked) return linked.href;
|
|
183
211
|
const first = links[0];
|
|
184
212
|
if (!first) return "";
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
213
|
+
return reasonCellFor(first);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A knockout's listed filings, each addressed at its office's page, for a listing taken on a register
|
|
218
|
+
* with no record pages of its own. `doc` is register-records.json, and each filing that carries an office
|
|
219
|
+
* number gains an `officeLink` in place, as normalizeRegisterRecordLinks rewrites links in place, so the
|
|
220
|
+
* report, the workbook and report-data.json all state the same link. Returns null for any other
|
|
221
|
+
* register, whose filings render exactly as before.
|
|
222
|
+
*
|
|
223
|
+
* A FILING WITH NO OFFICE NUMBER IS LEFT AS IT WAS, and shows as it always did. Every listing taken
|
|
224
|
+
* before the numbers were kept is that case, so an archived knockout republishes unchanged. An
|
|
225
|
+
* `officeLink` found already on the sidecar is dropped first: the link is set here, from the numbers, and
|
|
226
|
+
* nowhere else.
|
|
227
|
+
*/
|
|
228
|
+
export function addressListedFilings(doc) {
|
|
229
|
+
const records = (doc?.marks ?? []).flatMap((m) => m?.records ?? []).filter((r) => r && typeof r === "object");
|
|
230
|
+
for (const r of records) delete r.officeLink;
|
|
231
|
+
if (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(doc?.provider).toLowerCase())) return null;
|
|
232
|
+
const tally = { linked: {}, cited: {}, noNumber: 0 };
|
|
233
|
+
for (const r of records) {
|
|
234
|
+
const link = officeRecordLink(r, r.recordId);
|
|
235
|
+
if (!link?.label) { tally.noNumber += 1; continue; }
|
|
236
|
+
r.officeLink = link;
|
|
237
|
+
tallyLink(tally, link);
|
|
238
|
+
}
|
|
239
|
+
return { tally, summary: summaryOf(tally, `no office number ${tally.noNumber}`) };
|
|
189
240
|
}
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { SUMMARY_BLOCK_LINE, parseSummaryBlocks } from '../../shared/summary-blocks.mjs';
|
|
46
46
|
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine } from '../register-count.mjs';
|
|
47
47
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
48
|
+
import { officeLinkSentences } from './office-record-links.mjs';
|
|
48
49
|
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
49
50
|
import { demoBannerHtml } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
50
51
|
// — the two facts the register card is allowed to read off a raw record, and NEITHER is minted
|
|
@@ -78,6 +79,16 @@ const isHttpUrl = (u) => /^https?:\/\//i.test(String(u ?? '').trim());
|
|
|
78
79
|
// beside it — noopener severs window.opener, noreferrer withholds the run's URL from the evidence host.
|
|
79
80
|
const linkOrText = (u) => (isHttpUrl(u) ? `<a href="${escAttr(u)}" target="_blank" rel="noopener noreferrer">${esc(u)}</a>` : esc(u));
|
|
80
81
|
|
|
82
|
+
// A listed filing's record as its office publishes it, where the run's register has no page of its own:
|
|
83
|
+
// publish sets `officeLink` from the filing's own numbers (office-record-links.mjs). The label is the
|
|
84
|
+
// office and the number, linked where the office has a page. The handle names the vendor's record, not
|
|
85
|
+
// the office's, and a reader can look it up nowhere. A filing with no number shows `fallback`, as before.
|
|
86
|
+
const officeRecordCell = (r, fallback) => {
|
|
87
|
+
const l = r?.officeLink;
|
|
88
|
+
if (!l?.label) return fallback;
|
|
89
|
+
return l.href ? `<a href="${escAttr(l.href)}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a>` : esc(l.label);
|
|
90
|
+
};
|
|
91
|
+
|
|
81
92
|
// Minimal inline markdown, escaped FIRST. The old renderer ran esc() and nothing else, so a batch
|
|
82
93
|
// summary reading `BRIMSTONE rates **High (Classes 9, 28, 41)**.` rendered its asterisks verbatim —
|
|
83
94
|
// the model writes markdown because every other surface it feeds renders markdown.
|
|
@@ -189,7 +200,6 @@ const KO_CSS = `
|
|
|
189
200
|
/* The reviewer-notes legend. NOT .ko-legend — that name is taken by the framework attribution row
|
|
190
201
|
above, and reusing it would restyle the caption. It names the purple convention report.css already
|
|
191
202
|
draws for .internal, so the colour is stated once and never re-specified. */
|
|
192
|
-
.ko-refnote{margin:0;padding:11px 24px 0;font-size:12px;color:#6a2b6e;font-style:italic}
|
|
193
203
|
/* The notes sit inside a mark's column, so the shared .internal block's bullets keep the column's
|
|
194
204
|
own list indent and do not fight the paragraph above them. */
|
|
195
205
|
.ko-row .internal .ko-bul{margin:0 0 4px}
|
|
@@ -603,7 +613,7 @@ function filingsSection(marks, registerRecords) {
|
|
|
603
613
|
<td>${esc(r.status ?? '—')}</td>
|
|
604
614
|
<td>${esc((r.classes ?? []).join(', ') || '—')}</td>
|
|
605
615
|
<td>${esc(r.territory ?? '—')}</td>
|
|
606
|
-
<td class="ko-findev">${r.url ? linkOrText(r.url) : esc(r.recordId ?? '—')}</td>
|
|
616
|
+
<td class="ko-findev">${officeRecordCell(r, r.url ? linkOrText(r.url) : esc(r.recordId ?? '—'))}</td>
|
|
607
617
|
</tr>`).join('');
|
|
608
618
|
// "First N" WAS TRUE AND IS NOT ANY MORE, and saying it of a ranked table would be worse than the
|
|
609
619
|
// cap: it invites the reader to take the sample for the top of the provider's list. Say the count
|
|
@@ -620,7 +630,15 @@ function filingsSection(marks, registerRecords) {
|
|
|
620
630
|
<p class="ko-basis">${esc(line ?? '')}${esc(more)}</p>
|
|
621
631
|
</div>`;
|
|
622
632
|
}).join('');
|
|
623
|
-
return `<div class="panel ko-glance">${blocks}<p class="ko-basis">${esc(RECORD_BASIS)}</p></div>`;
|
|
633
|
+
return `<div class="panel ko-glance">${blocks}<p class="ko-basis">${esc(RECORD_BASIS)}${officeLinkNote(registerRecords)}</p></div>`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Once, under the filings: what a linked number opens and, per office, why the rest are cited by number.
|
|
637
|
+
// The clearance's own sentences, said once for the reason it says them once: a note repeated beside every
|
|
638
|
+
// filing reads as a broken report. Empty unless a filing carries an office number.
|
|
639
|
+
function officeLinkNote(registerRecords) {
|
|
640
|
+
const links = (registerRecords?.marks ?? []).flatMap((m) => m?.records ?? []).map((r) => r?.officeLink).filter(Boolean);
|
|
641
|
+
return officeLinkSentences(new Map(links.map((l, i) => [i, l]))).map((s) => ` ${esc(s)}`).join('');
|
|
624
642
|
}
|
|
625
643
|
|
|
626
644
|
// The tier-absence line ( part 2). A knockout whose product bought no register step used to render
|
|
@@ -1069,7 +1087,6 @@ function sourceChips(v) {
|
|
|
1069
1087
|
// already defines — the same one the clearance report uses for this material — so a reader can see whose
|
|
1070
1088
|
// voice a line is in. Merging them into the client-voiced body would make the reviewer's asides read as
|
|
1071
1089
|
// findings about the mark, which is the one way this ruling could produce a worse document.
|
|
1072
|
-
const REVIEWER_NOTES_LEGEND = 'Purple notes are for the reviewing lawyer. Remove them before this goes to the client.';
|
|
1073
1090
|
|
|
1074
1091
|
// The clearance lane's own label, copied rather than re-worded. One spelling across
|
|
1075
1092
|
// both products is the point: a reader who has seen it on a clearance report knows what it means here.
|
|
@@ -1155,7 +1172,7 @@ function registerFindingBlock(v, markIndex, reads = null, framework = null, owne
|
|
|
1155
1172
|
const useCheckSource = useCheck?.source ?? null;
|
|
1156
1173
|
const meta = [r.owner ? esc(r.owner) : 'proprietor not stated', r.territory ? esc(r.territory) : null]
|
|
1157
1174
|
.filter(Boolean).join(' · ');
|
|
1158
|
-
const receipt = isHttpUrl(r.url) ? linkOrText(r.url) : esc(r.recordId ?? 'no record address supplied');
|
|
1175
|
+
const receipt = officeRecordCell(r, isHttpUrl(r.url) ? linkOrText(r.url) : esc(r.recordId ?? 'no record address supplied'));
|
|
1159
1176
|
return `<div class="card ko-find" data-ko-mark="${Number(markIndex)}" data-ko-ord="${Number(v.ordinal)}">
|
|
1160
1177
|
<div class="top">
|
|
1161
1178
|
<div class="rail" style="background:var(${stop ?? '--faint'})"></div>
|
|
@@ -1425,12 +1442,12 @@ function analysisSection(marks, framework, { registerCounts = null, probeRan = f
|
|
|
1425
1442
|
</div>
|
|
1426
1443
|
</div>`;
|
|
1427
1444
|
}).join('');
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
return `<div class="panel ko-glance">${
|
|
1445
|
+
// NO LEGEND. A standing line saying "remove these before this goes to the client" was an instruction
|
|
1446
|
+
// to the reader printed on the document itself, and the owner ruled it out: a report that explains its
|
|
1447
|
+
// own conventions in a caveat is telling the reader how to handle it rather than what was found. The
|
|
1448
|
+
// notes and their "For the reviewing lawyer" label stay — that label names a reader, which is a fact
|
|
1449
|
+
// about the note; the legend told somebody what to do about it, which is not.
|
|
1450
|
+
return `<div class="panel ko-glance">${cards}</div>`;
|
|
1434
1451
|
}
|
|
1435
1452
|
|
|
1436
1453
|
// The method line names EXACTLY what ran, and its wording is doctrine — with counts on the page, "register
|
|
@@ -1885,6 +1902,9 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1885
1902
|
mark: r.mark, owner: r.owner, status: r.status, classes: r.classes ?? [],
|
|
1886
1903
|
territory: r.territory, matchedForm: r.matchedForm, matchedBasis: r.matchedBasis,
|
|
1887
1904
|
recordId: r.recordId, url: r.url,
|
|
1905
|
+
// The office's page for the filing, or its office and number and why it is not a link: the
|
|
1906
|
+
// report's own cell, as data. Only where the run's register has no record pages of its own.
|
|
1907
|
+
...(r.officeLink ? { officeRecord: { label: r.officeLink.label, href: r.officeLink.href, reason: r.officeLink.reason } } : {}),
|
|
1888
1908
|
})),
|
|
1889
1909
|
// Which searches did NOT answer, by name. A consumer that lists only `records` would report
|
|
1890
1910
|
// a partial listing as a complete one.
|
package/driver/publish/xlsx.mjs
CHANGED
|
@@ -283,7 +283,7 @@ export function searchRows(auditParsed, { findings = [], joinedTerms = null, reg
|
|
|
283
283
|
|
|
284
284
|
const rows = [];
|
|
285
285
|
let i = 0;
|
|
286
|
-
const push = r => rows.push({ '#': ++i, ...r });
|
|
286
|
+
const push = r => rows.push({ '#': ++i, ...r, Result: readerWords(r.Result), Note: readerWords(r.Note) }); // plain words, after each branch below has classified its row from the engine's own
|
|
287
287
|
|
|
288
288
|
if (reg.length) {
|
|
289
289
|
push({ 'Search term / variant': 'REGISTER — vendor register, worldwide', Scope: '', Result: '', Outcome: '', Note: '', _section: true });
|
|
@@ -640,6 +640,38 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
|
|
|
640
640
|
};
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
// READER_WORDS — the plain word a reader of "What was searched" gets in place of each engine word BANNED
|
|
644
|
+
// names. That sheet's Result and Note cells carry the search log's own prose, model-authored and already
|
|
645
|
+
// written on every archived run, so the words are put right where each row is written (`push` in
|
|
646
|
+
// `searchRows()`) instead of being left to the advisory gate. The gate still runs; a hit on this sheet is
|
|
647
|
+
// now a name, or a word this table is missing.
|
|
648
|
+
//
|
|
649
|
+
// Lower case only. In these cells a capitalised or upper-case word is a name — a mark, an owner, a
|
|
650
|
+
// platform — and a rewritten name misstates what was searched, so names stay as written. HTTP keeps to
|
|
651
|
+
// that rule with one exception, which is never a name: before a status code it goes in any case, so
|
|
652
|
+
// "provider-rejected (HTTP 429)" reads "provider-rejected (429)" while a mark called HTTP HOUSE keeps its
|
|
653
|
+
// name. A web address loses its scheme in any case and keeps its host and path. has_more is plainNote's.
|
|
654
|
+
//
|
|
655
|
+
// Kept down here, below every line the rest of the tree cites in this file by number.
|
|
656
|
+
export const READER_WORDS = Object.freeze({
|
|
657
|
+
receipt: 'record', receipts: 'records',
|
|
658
|
+
lint: 'check', tripwire: 'check', tripwires: 'checks',
|
|
659
|
+
composite: 'combined', scatter: 'chart', cache: 'stored copy',
|
|
660
|
+
quadrant: 'position', quadrants: 'positions', coordinate: 'position', coordinates: 'positions',
|
|
661
|
+
meter: 'measure', meters: 'measures',
|
|
662
|
+
});
|
|
663
|
+
const READER_WORD = new RegExp(`\\b(?:${Object.keys(READER_WORDS).join('|')})\\b`, 'gi');
|
|
664
|
+
|
|
665
|
+
function readerWords(s) {
|
|
666
|
+
return String(s ?? '')
|
|
667
|
+
.replace(/\bhttps?:\/\//gi, '')
|
|
668
|
+
.replace(/\bhttps?\s+(?=\d{3}\b)/gi, '')
|
|
669
|
+
.replace(/\bhttps?\b\s*/g, '')
|
|
670
|
+
.replace(READER_WORD, (w) => (w === w.toLowerCase() ? READER_WORDS[w] : w))
|
|
671
|
+
.replace(/\s+/g, ' ')
|
|
672
|
+
.trim();
|
|
673
|
+
}
|
|
674
|
+
|
|
643
675
|
/**
|
|
644
676
|
* validateAudit — the build gate. Reads the assembled workbook back and asserts the spec's MUST-BE-TRUE list.
|
|
645
677
|
* Returns { ok, violations[] }. Exported so tests assert the same contract the build enforces.
|
|
@@ -143,6 +143,12 @@ function toRecord(row, { term, basis, provider }) {
|
|
|
143
143
|
territory,
|
|
144
144
|
applicationDate: row?.application_date ?? null,
|
|
145
145
|
registrationDate: row?.registration_date ?? null,
|
|
146
|
+
// The office's own numbers, where the provider hands them over. On a register with no record pages of
|
|
147
|
+
// its own, publish addresses the office's page for the filing from these (office-record-links.mjs).
|
|
148
|
+
applicationNumber: row?.application_number ?? null,
|
|
149
|
+
registrationNumber: row?.registration_number ?? null,
|
|
150
|
+
irNumber: row?.ir_number ?? null,
|
|
151
|
+
filingRoute: row?.filing_route ?? null,
|
|
146
152
|
// WHICH QUESTION FOUND IT. Without this a reader cannot tell a filing on the name from a filing on
|
|
147
153
|
// a generated variant, and the two mean very different things to the person deciding on the name.
|
|
148
154
|
matchedForm: term,
|
|
@@ -186,7 +186,7 @@ export function missingRequirements(env = {}, tables = {}) {
|
|
|
186
186
|
*
|
|
187
187
|
* Returns null when nothing blocks, so a caller cannot mistake "configured" for "could not look".
|
|
188
188
|
*/
|
|
189
|
-
export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFile = null } = {}) {
|
|
189
|
+
export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFile = null, startFile = null } = {}) {
|
|
190
190
|
const missing = missingRequirements(env, tables).atOrder;
|
|
191
191
|
if (!missing.length) return null;
|
|
192
192
|
const names = missing.map((r) => r.name);
|
|
@@ -197,12 +197,21 @@ export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFi
|
|
|
197
197
|
// was told to create a file nothing on that box reads — and the one actually read went unnamed.
|
|
198
198
|
// `readFile` is the file THIS process loaded at start; it is null under a unit, where the unit's own
|
|
199
199
|
// EnvironmentFile is the only configuration and `envFile` alone is the true answer.
|
|
200
|
+
//
|
|
201
|
+
// AND A RUNNER THAT `clearotron start` STARTED READ NO FILE EITHER. The supervisor read the install's own
|
|
202
|
+
// file and handed its values down with CLEAROTRON_NO_ENV_FILE=1, so `readFile` is null there exactly as
|
|
203
|
+
// it is under a unit, and this sentence named the units' file: on a box with no units, a file that does
|
|
204
|
+
// not exist, while the file the values came from went unnamed. Measured on a fresh install, 2026-09-10.
|
|
205
|
+
// `startFile` is the file that supervisor read. It reaches the runner as a command-line flag that a unit's
|
|
206
|
+
// fixed ExecStart never carries, so it is set exactly when `clearotron start` is the parent.
|
|
200
207
|
const cmd = "`clearotron install` in a terminal, which writes them for you";
|
|
201
208
|
const both = readFile && envFile && readFile !== envFile;
|
|
202
209
|
const one = readFile || envFile;
|
|
203
210
|
const where = both
|
|
204
211
|
? ` Set them in either of these — both reach a run:\n ${readFile} — the file this runner read when it started\n`
|
|
205
212
|
+ ` ${envFile} — the file background units read\n then restart, or run ${cmd}.`
|
|
213
|
+
: !readFile && startFile
|
|
214
|
+
? ` Set them in ${startFile} — the file \`clearotron start\` read when it started this runner — then restart it, or run ${cmd}.`
|
|
206
215
|
: one
|
|
207
216
|
? ` Set them in ${one} and restart, or run ${cmd}.`
|
|
208
217
|
: " Run `clearotron install` in a terminal to configure them, or set them in the file this install's units read.";
|
|
@@ -217,3 +226,17 @@ export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFi
|
|
|
217
226
|
+ "nothing has been charged. Please contact whoever administers it.",
|
|
218
227
|
};
|
|
219
228
|
}
|
|
229
|
+
|
|
230
|
+
// THE ENV FILE `clearotron start` READ, when it started this runner: the one file a refusal can honestly
|
|
231
|
+
// name for a runner that read none itself (`startFile` above). A command-line flag rather than a variable,
|
|
232
|
+
// for two reasons. A unit's ExecStart is fixed at `--watch` and never carries it, so a runner holding it
|
|
233
|
+
// was started by that command. And a variable read by product code belongs in the environment catalogue,
|
|
234
|
+
// which describes settings an operator makes, and this is not one. `bin/start.mjs` puts it on the
|
|
235
|
+
// worker's command line, and `driver/runner.mjs` reads it off its own.
|
|
236
|
+
export const START_ENV_FILE_FLAG = "--start-env-file=";
|
|
237
|
+
|
|
238
|
+
/** The path `argv` carries in `--start-env-file=<path>`, or null when it carries none or an empty one. PURE. */
|
|
239
|
+
export function startEnvFileOf(argv = []) {
|
|
240
|
+
const flag = argv.find((t) => String(t).startsWith(START_ENV_FILE_FLAG));
|
|
241
|
+
return flag ? flag.slice(START_ENV_FILE_FLAG.length) || null : null;
|
|
242
|
+
}
|
package/driver/runner.mjs
CHANGED
|
@@ -27,8 +27,8 @@ import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // —
|
|
|
27
27
|
// claim-sidecar names, because a harness check retyped four of them from memory and false-alarmed on the
|
|
28
28
|
// other nine. Behaviour here is unchanged: the same object and the same three suffixes, sourced.
|
|
29
29
|
import { isLiveQueueMarker, PROSE_PARTS, CLAIM_SIDECAR_SUFFIXES, TERMINAL_QUEUE_SUFFIXES } from "./queue-markers.mjs";
|
|
30
|
-
import { matterLedgerPath } from "./usage-ledger.mjs"; // ONE ledger-path calculation, shared with the portal pre-check
|
|
31
|
-
import { orderTimeRefusal } from "./run-requirements.mjs"; // one authority for what a run needs, and when it is asked for
|
|
30
|
+
import { matterLedgerPath, DEFAULT_CLIENT_DAILY_RUNS } from "./usage-ledger.mjs"; // ONE ledger-path calculation, shared with the portal pre-check
|
|
31
|
+
import { orderTimeRefusal, START_ENV_FILE_FLAG, startEnvFileOf } from "./run-requirements.mjs"; // one authority for what a run needs, and when it is asked for
|
|
32
32
|
import { unitEnvPath, envFileRead } from "../shared/env-local.mjs"; // the file the units read, named by its one author
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { config, preflightDeploymentUrls } from "./driver.config.mjs";
|
|
@@ -235,7 +235,11 @@ function recordMatter(qdir, entry) {
|
|
|
235
235
|
// which reads as the product being broken rather than as a fair-use wall. The risk this exists to stop is
|
|
236
236
|
// unbounded spend by an unconfigured account, and 20 stops that just as completely. It is the DEFAULT, so
|
|
237
237
|
// any account that needs a different number says so in its own runCaps block.
|
|
238
|
-
|
|
238
|
+
//
|
|
239
|
+
// ONE DECLARATION. The number lives in usage-ledger.mjs, the leaf a request path imports without pulling
|
|
240
|
+
// the runner in, and is re-exported here for the gate and its tests. Two declarations held equal by a
|
|
241
|
+
// test is how the screen comes to advertise one allowance while the gate enforces another.
|
|
242
|
+
export { DEFAULT_CLIENT_DAILY_RUNS };
|
|
239
243
|
|
|
240
244
|
export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, now = Date.now(), clientRun = false, organisation = null }) {
|
|
241
245
|
// GENERIC IS CAPPED LIKE ANY COMPANY, one lane per organisation (ruling 2026-09-10: "every
|
|
@@ -768,6 +772,7 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
|
|
|
768
772
|
// WHAT IT MUST NEVER DO is what F41 did: fail at the first stage and tell the client "Clearotron has
|
|
769
773
|
// been notified" on a box with no outbox. `failAtIntake` writes the honest refusal and the run never
|
|
770
774
|
// starts, so nothing is spent and nothing is promised.
|
|
775
|
+
|
|
771
776
|
let __runTables = null;
|
|
772
777
|
async function runTables() {
|
|
773
778
|
// AT CALL TIME, never a static import. `driver/run-requirements.mjs`'s header states the reason and it
|
|
@@ -856,7 +861,7 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
|
|
|
856
861
|
}
|
|
857
862
|
// ── IS THIS BOX CONFIGURED TO SEARCH AT ALL? See the header above claimAndPrep.
|
|
858
863
|
{
|
|
859
|
-
const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath(), readFile: envFileRead() });
|
|
864
|
+
const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath(), readFile: envFileRead(), startFile: startEnvFileOf(process.argv) });
|
|
860
865
|
if (refusal) {
|
|
861
866
|
note(`[runner] ${base} REFUSED at order time — this install is not configured to run a search: ${refusal.names.join(", ")}`);
|
|
862
867
|
await failAtIntake(procPath, qdir, base, agentId, job,
|
|
@@ -2404,7 +2409,7 @@ if (isEntrypoint(import.meta.url)) {
|
|
|
2404
2409
|
// the exact silence this whole change exists to remove. (pipeline.mjs's RETIRED_FLAGS table makes the same
|
|
2405
2410
|
// argument about a switch that degrades quietly into whatever the remaining arguments mean.)
|
|
2406
2411
|
const argv = process.argv.slice(2);
|
|
2407
|
-
const unknown = argv.filter((t) => t !== "--watch");
|
|
2412
|
+
const unknown = argv.filter((t) => t !== "--watch" && !t.startsWith(START_ENV_FILE_FLAG)); // the second is start's own, see startEnvFileOf
|
|
2408
2413
|
if (unknown.length) {
|
|
2409
2414
|
console.error(`error: unknown argument ${unknown[0]}\nusage: node runner.mjs [--watch]\n --watch keep polling instead of draining once — the substitute for the systemd .path/.timer units\n on a machine that has none. Without it this drains the queue once and exits, which is what\n systemd invokes.`);
|
|
2410
2415
|
process.exit(2);
|
|
@@ -310,7 +310,7 @@ line, which describes the card and claims nothing about the rating. So:
|
|
|
310
310
|
"degraded": null } ] }
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
**The finding record — closed keys, all
|
|
313
|
+
**The finding record — closed keys, all nine, no others.** A key this list does not name is refused
|
|
314
314
|
(the validator is `findings-model.mjs validateKnockoutFinding`, and it runs at the chunk and again on
|
|
315
315
|
the merged artifact):
|
|
316
316
|
|