clearotron 0.2.4 → 0.3.0-beta.1
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 +62 -38
- package/bin/brandowner.mjs +18 -169
- package/bin/clearotron.mjs +3 -1
- package/bin/connect.mjs +28 -19
- package/bin/disconnect.mjs +3 -3
- package/bin/example.mjs +7 -7
- package/bin/framework-preflight.mjs +49 -0
- package/bin/grant.mjs +151 -93
- package/bin/onboard.mjs +220 -138
- package/bin/start.mjs +146 -137
- package/bin/stop.mjs +2 -2
- package/bin/update.mjs +1 -1
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +2 -2
- package/docs/E2E.md +12 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +14 -13
- package/docs/SECURITY.md +23 -24
- package/docs/architecture/04-configuration-reference.md +12 -5
- package/docs/architecture/05-config-governance.md +7 -7
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +5 -0
- package/docs/configuration.md +118 -0
- package/docs/decisions/0004-documentation-structure.md +2 -2
- package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/cancel.mjs +27 -0
- package/driver/case-law-sources.mjs +3 -3
- package/driver/company-bundle.mjs +261 -0
- package/driver/compare.mjs +1 -1
- package/driver/compose-read.mjs +2 -2
- package/driver/config-inventory.mjs +2 -2
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +1 -1
- package/driver/declination-call.mjs +1 -1
- package/driver/deliver-trigger.sh +3 -3
- package/driver/dev-portal.mjs +3 -1
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-tool.mjs +1 -1
- package/driver/doc-constants.mjs +1 -1
- package/driver/drain-posture.mjs +2 -2
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +44 -9
- package/driver/effective-scope.mjs +30 -1
- package/driver/effort-model.mjs +6 -6
- package/driver/engine/CONTRACT.md +2 -2
- package/driver/engine/anthropic-agent.mjs +11 -11
- package/driver/engine/jx-turn.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +29 -4
- package/driver/engine/mcp/recording-server.mjs +73 -1
- package/driver/engine/openai-agent.mjs +1 -1
- package/driver/engine/probe.mjs +28 -4
- package/driver/enqueue-schema.mjs +23 -3
- package/driver/findings-model.mjs +2 -2
- package/driver/flag-snapshot.mjs +2 -2
- package/driver/floor-duty.mjs +2 -2
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/framework-preflight.mjs +143 -0
- package/driver/gateway.mjs +9 -1
- package/driver/hit-list.mjs +1 -1
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +1 -1
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/knockout-review-record.mjs +435 -0
- package/driver/order-probe.mjs +1 -1
- package/driver/outbox-backoff.mjs +2 -2
- package/driver/owner-use-check.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +105 -8
- package/driver/pipeline.mjs +81 -30
- package/driver/plain-register.mjs +77 -3
- package/driver/portal-access.mjs +141 -74
- package/driver/portal-config-view.mjs +59 -70
- package/driver/portal-report.mjs +4 -4
- package/driver/portal-service.mjs +348 -141
- package/driver/portal-upstream.mjs +105 -17
- package/driver/predelivery-lint.mjs +43 -18
- package/driver/product-rows.mjs +1 -1
- package/driver/products.mjs +1 -1
- package/driver/profile-page.html +30 -5
- package/driver/profile-service.mjs +197 -26
- package/driver/profiles.mjs +48 -1
- package/driver/publish/index.mjs +31 -13
- package/driver/publish/knockout.mjs +9 -5
- package/driver/publish/office-record-links.mjs +189 -0
- package/driver/publish/parse.mjs +3 -3
- package/driver/publish/publish-inputs.mjs +26 -0
- package/driver/publish/render-knockout.mjs +42 -42
- package/driver/publish/render.mjs +29 -4
- package/driver/publish/report-data.mjs +2 -2
- package/driver/publish/seed-pool.mjs +1 -1
- package/driver/publish/templates/report.css +8 -8
- package/driver/publish/xlsx.mjs +49 -7
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/recipe-service.mjs +1 -1
- package/driver/record-carry.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/reference-strip-signatures.mjs +1 -1
- package/driver/register-availability.mjs +4 -3
- package/driver/register-count.mjs +3 -3
- package/driver/register-records.mjs +1 -1
- package/driver/repair-composers.mjs +1 -1
- package/driver/repairs.mjs +3 -3
- package/driver/replay-archive.mjs +1 -1
- package/driver/report-card-record.mjs +1 -1
- package/driver/result-noun-fields.mjs +5 -0
- package/driver/roster-verdict.mjs +48 -5
- package/driver/run-activity.mjs +1 -1
- package/driver/run-requirements.mjs +18 -5
- package/driver/runner.mjs +24 -15
- package/driver/search-policy.mjs +8 -8
- package/driver/senior-rights.mjs +1 -1
- package/driver/skills/prelim-search/delivery-contract.md +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
- package/driver/stages-knockout.mjs +72 -6
- package/driver/stages.mjs +10 -10
- package/driver/suite-census.json +238 -70
- package/driver/synthesis-record.mjs +2 -2
- package/driver/systemd/clearotron-client-mcp.service +3 -3
- package/driver/systemd/clearotron-deploy.service +2 -2
- package/driver/systemd/clearotron-mcp-face.service +1 -1
- package/driver/systemd/clearotron-portal.service +3 -3
- package/driver/systemd/clearotron-worker.service +5 -5
- package/driver/systemd/install-census.mjs +1 -1
- package/driver/systemd/render-units.mjs +9 -9
- package/driver/terminal-clamp.mjs +1 -1
- package/driver/trigger-cap.mjs +18 -2
- package/driver/unit-inventory.mjs +8 -8
- package/driver/usage-ledger.mjs +5 -3
- package/driver/verify-knockout.mjs +7 -7
- package/driver/verify.mjs +5 -5
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/whatif-queue.mjs +3 -3
- package/driver/whatif-worker.mjs +2 -2
- package/examples/README.md +1 -1
- package/examples/grants.example.json +25 -24
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/http-server.mjs +3 -3
- package/mcp-server/key-socket.mjs +1 -1
- package/mcp-server/lib/audit-view.mjs +3 -3
- package/mcp-server/lib/brief.mjs +3 -3
- package/mcp-server/lib/driver.mjs +1 -1
- package/mcp-server/lib/events.mjs +1 -1
- package/mcp-server/lib/http-handler.mjs +2 -2
- package/mcp-server/lib/instructions.mjs +2 -2
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +6 -3
- package/mcp-server/lib/options.mjs +15 -4
- package/mcp-server/lib/plan.mjs +7 -6
- package/mcp-server/lib/runs.mjs +10 -0
- package/mcp-server/lib/whatif.mjs +5 -5
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/remote/client-mcp-apikey.service +1 -1
- package/mcp-server/remote/client-mcp.service +2 -2
- package/mcp-server/remote/trademark-artifacts-http.service +1 -1
- package/mcp-server/server.mjs +38 -24
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
- package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/lane-probe.mjs +9 -3
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/scripts/citation-drift-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +2 -2
- package/scripts/drive-env-check.mjs +1 -1
- package/scripts/e2e.mjs +5 -5
- package/scripts/env-audit.mjs +13 -1
- package/scripts/headless-page.mjs +5 -5
- package/scripts/live-surface-check.mjs +26 -2
- package/scripts/mint-names-in-force.mjs +19 -5
- package/scripts/mint-reference-strip-backlog.mjs +1 -1
- package/scripts/mint-suite-census.mjs +37 -10
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/preinstall-node-check.mjs +1 -1
- package/scripts/release-await-cut.mjs +3 -3
- package/scripts/release-cut-decision.mjs +1 -1
- package/scripts/release-dist-tag.mjs +1 -1
- package/scripts/release-install-check.mjs +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/scripts/release-publish-guard.mjs +1 -1
- package/scripts/release-version-pr-checks.mjs +2 -2
- package/scripts/release-version.mjs +61 -5
- package/scripts/render-brand-banner.mjs +1 -1
- package/scripts/render-check.mjs +2 -2
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-frame-check.mjs +1 -1
- package/scripts/report-screenshot.mjs +2 -2
- package/scripts/retire-bare-refs.mjs +1 -1
- package/scripts/revisit-render-check.mjs +1 -1
- package/scripts/score.mjs +1 -1
- package/scripts/strip-titles-and-attributions.mjs +389 -0
- package/scripts/strip-tracker-citations.mjs +122 -5
- package/scripts/test-run.mjs +4 -4
- package/scripts/third-party-notices.mjs +1 -1
- package/scripts/verify-publishable.mjs +1 -1
- package/shared/access-audience.mjs +2 -2
- package/shared/anon-overlay.mjs +1 -1
- package/shared/brand.mjs +15 -1
- package/shared/bundle-freshness.mjs +1 -1
- package/shared/bundle-rebuild.mjs +1 -1
- package/shared/checkout-move.mjs +2 -2
- package/shared/client-door.mjs +8 -8
- package/shared/connect-clients.mjs +7 -7
- package/shared/connector-signin-probe.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/env-local.mjs +5 -5
- package/shared/grants-edit.mjs +76 -0
- package/shared/install-auth.mjs +1 -1
- package/shared/listen.mjs +3 -3
- package/shared/mcp-challenge.mjs +1 -1
- package/shared/names-in-force.mjs +2 -1
- package/shared/onboarding-store.mjs +19 -2
- package/shared/reference-guard-classes.mjs +44 -2
- package/shared/register-selection.mjs +1 -1
- package/shared/scope.mjs +223 -56
- package/shared/secret-file.mjs +1 -1
- package/shared/server-units.mjs +1 -1
- package/shared/staff-domain.mjs +45 -78
- package/shared/summary-blocks.mjs +2 -2
- package/shared/systemd-failure.mjs +3 -3
- package/shared/tracked-files.mjs +1 -1
- package/shared/trigger-lane.mjs +1 -1
- package/shared/tty-style.mjs +1 -1
- package/shared/usage-block.mjs +1 -1
- package/shared/vacuous-pass.mjs +1 -1
- package/shared/verb-shim.mjs +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// office-record-links.mjs — the page a trade mark office publishes for one record, addressed from the
|
|
4
|
+
// numbers on the record the run fetched.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS. A register vendor with no per-record page of its own left every register finding with
|
|
7
|
+
// nothing a reader could open. The card showed the engine's handle, `/mark/<office>/<id>`, and the id in
|
|
8
|
+
// it is the vendor's, not the office's. Most offices do publish a page per record, keyed on their own
|
|
9
|
+
// application or registration number, and the fetched record carries that number. So the link is built
|
|
10
|
+
// here, in code, from a table of what each office's address takes: never by the model, and never from
|
|
11
|
+
// the handle.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS DOES NOT TOUCH. The model's own record URLs are still refused for such a vendor: its
|
|
14
|
+
// allow-list (record-origins.mjs) stays empty, and normalizeRecordLinks still strips a host it does not
|
|
15
|
+
// publish. This is a second source of links with one input, the fetched record, and it only ever emits
|
|
16
|
+
// an address from the table below.
|
|
17
|
+
//
|
|
18
|
+
// A NUMBER THAT DOES NOT FIT GETS NO LINK. Each entry states the form its address takes, and a number in
|
|
19
|
+
// any other form is cited, not linked: a guessed address on a legal deliverable is worse than an admitted
|
|
20
|
+
// absence. The only reshaping is what the office's own format defines: Swiss application serials are
|
|
21
|
+
// five digits, EU application numbers nine, French numbers carry FR.
|
|
22
|
+
//
|
|
23
|
+
// Each address was checked on 2026-09-10 against a real record, and each row says how.
|
|
24
|
+
|
|
25
|
+
const pad = (s, n) => String(s).padStart(n, "0");
|
|
26
|
+
const clean = (v) => (v == null ? "" : String(v).trim());
|
|
27
|
+
|
|
28
|
+
/** The first of `values` matching `re`, handed to `build`; null when none does. */
|
|
29
|
+
function addressFrom(values, re, build) {
|
|
30
|
+
for (const v of values) {
|
|
31
|
+
const m = re.exec(clean(v));
|
|
32
|
+
if (m) return build(m);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// An international registration, at WIPO's Madrid Monitor. Opened a real record. WIPO has said it will
|
|
38
|
+
// retire Madrid Monitor once eMadrid covers everything it does, and has given no date.
|
|
39
|
+
const madridMonitor = (values) => addressFrom(values, /^0*([1-9]\d{4,6})$/, (m) => ({
|
|
40
|
+
number: m[1], href: `https://www3.wipo.int/madrid/monitor/en/showData.jsp?ID=ROM.${m[1]}` }));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One entry per office code the handle carries (`/mark/<office>/...`, lower case). `address(rec)` returns
|
|
44
|
+
* `{ number, href }` when the record's number is in the form the office's page takes, and null when it is
|
|
45
|
+
* not. An office with `address: null` publishes no page for a single record.
|
|
46
|
+
*/
|
|
47
|
+
export const OFFICE_RECORD_PAGES = Object.freeze({
|
|
48
|
+
// Opened a real record. Australia reuses the trade mark number as the registration number.
|
|
49
|
+
au: { name: "Australia", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(\d{1,8})$/, (m) => ({
|
|
50
|
+
number: m[1], href: `https://search.ipaustralia.gov.au/trademarks/search/view/${m[1]}` })) },
|
|
51
|
+
// Opened a real record. `-00` is the extension CIPO gives the base application.
|
|
52
|
+
ca: { name: "Canada", address: (r) => addressFrom([r.applicationNumber], /^([1-9]\d{0,6})(?:-(\d{2}))?$/, (m) => ({
|
|
53
|
+
number: m[2] ? `${m[1]}-${m[2]}` : m[1], href: `https://ised-isde.canada.ca/cipo/trademark-search/${m[1]}-${m[2] ?? "00"}?lang=eng` })) },
|
|
54
|
+
// Rendered in a browser: a real number draws the record, and one that does not exist draws the bare
|
|
55
|
+
// search page. National marks only; a Madrid designation of Switzerland goes to WIPO, below.
|
|
56
|
+
ch: { name: "Switzerland", address: (r) => addressFrom([r.applicationNumber], /^(\d{1,5})\/(\d{4})$/, (m) => {
|
|
57
|
+
const n = `${pad(m[1], 5)}/${m[2]}`;
|
|
58
|
+
return { number: n, href: `https://www.swissreg.ch/database-client/register/detail?type=trademark&lang=en&no=${encodeURIComponent(n)}` };
|
|
59
|
+
}) },
|
|
60
|
+
// The address the EUIPO provider already links (providers/euipo/src/row.js).
|
|
61
|
+
eu: { name: "European Union", address: (r) => addressFrom([r.applicationNumber], /^(\d{1,9})$/, (m) => {
|
|
62
|
+
const n = pad(m[1], 9);
|
|
63
|
+
return { number: n, href: `https://euipo.europa.eu/eSearch/#details/trademarks/${n}` };
|
|
64
|
+
}) },
|
|
65
|
+
// INPI's own record pages at this address are indexed; INPI blocks automated fetches, so none was
|
|
66
|
+
// opened from a server. The vendor keeps INPI's FR prefix on the number.
|
|
67
|
+
fr: { name: "France", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(?:FR)?(\d{7})$/i, (m) => ({
|
|
68
|
+
number: `FR${m[1]}`, href: `https://data.inpi.fr/marques/FR${m[1]}` })) },
|
|
69
|
+
// The office's record address as indexed; it challenges automated fetches, so none was opened from a
|
|
70
|
+
// server. UK numbers are UK plus eleven digits (UK000..., UK008..., UK009...).
|
|
71
|
+
gb: { name: "United Kingdom", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(UK\d{11})$/i, (m) => ({
|
|
72
|
+
number: m[1].toUpperCase(), href: `https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/${m[1].toUpperCase()}` })) },
|
|
73
|
+
// The register's own search opens records at exactly this address: the application number, then the
|
|
74
|
+
// registration number when there is one. Opened a real record. The older search.patentstyret.no
|
|
75
|
+
// addresses now answer 410 Gone.
|
|
76
|
+
no: { name: "Norway", address: (r) => addressFrom([r.applicationNumber], /^((?:19|20)\d{7})$/, (m) => {
|
|
77
|
+
const reg = /^\d{1,7}$/.test(clean(r.registrationNumber)) ? clean(r.registrationNumber) : null;
|
|
78
|
+
return { number: m[1], href: `https://services.patentstyret.no/search-details/trademark/${m[1]}${reg ? `-${reg}` : ""}?lang=en` };
|
|
79
|
+
}) },
|
|
80
|
+
// PRV's new search, in beta since 2026-01-26. Rendered in a browser: a real number draws the record,
|
|
81
|
+
// and one that does not exist draws the bare search page.
|
|
82
|
+
se: { name: "Sweden", address: (r) => addressFrom([r.applicationNumber], /^(\d{4}-\d{5})$/, (m) => ({
|
|
83
|
+
number: m[1], href: `https://search.prv.se/#/trademark/${m[1]}` })) },
|
|
84
|
+
// IPOS Digital Hub shows a record only inside a search session: there is no address for one record.
|
|
85
|
+
sg: { name: "Singapore", address: null },
|
|
86
|
+
// The address the USPTO provider already links. The number is the serial.
|
|
87
|
+
us: { name: "United States", address: (r) => addressFrom([r.applicationNumber], /^(\d{8})$/, (m) => ({
|
|
88
|
+
number: m[1], href: `https://tsdr.uspto.gov/#caseNumber=${m[1]}&caseType=SERIAL_NO&searchType=statusSearch` })) },
|
|
89
|
+
wo: { name: "WIPO (Madrid)", address: (r) => madridMonitor([r.irNumber, r.registrationNumber, r.applicationNumber]) },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const handleOffice = (uri) => (/^\/mark\/([a-z]{2,4})\//i.exec(clean(uri))?.[1] ?? "").toLowerCase();
|
|
93
|
+
|
|
94
|
+
// A number that already carries its office's letters (FR4123456, UK00003456789) stands alone; a bare
|
|
95
|
+
// one is shown with its office (CH 08133/2025), so the label always says whose number it is.
|
|
96
|
+
const labelOf = (office, number) => (number ? (/^[a-z]{2}/i.test(number) ? number : `${office.toUpperCase()} ${number}`) : null);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The office's page for one fetched record. `{ office, label, href, reason: null }` when the office
|
|
100
|
+
* publishes a page and the number is in the form its address takes. Otherwise `href` is null and
|
|
101
|
+
* `reason` says why: `no-page` (the office publishes none), `unaddressable` (the number is not in the
|
|
102
|
+
* form the address takes, or is missing), `unknown-office` (this table holds no address for the office).
|
|
103
|
+
*
|
|
104
|
+
* A MADRID DESIGNATION IS ADDRESSED AT WIPO, whatever office it sits under. On a designation the
|
|
105
|
+
* vendor's registration number is the IR number, so a national address built from it opens the wrong
|
|
106
|
+
* record or none.
|
|
107
|
+
*/
|
|
108
|
+
export function officeRecordLink(rec, uri = "") {
|
|
109
|
+
if (!rec || typeof rec !== "object") return null;
|
|
110
|
+
const designation = clean(rec.filingRoute) === "madrid_designation";
|
|
111
|
+
const office = designation ? "wo" : (clean(rec.office).toLowerCase() || handleOffice(uri));
|
|
112
|
+
const page = OFFICE_RECORD_PAGES[office];
|
|
113
|
+
const own = designation ? clean(rec.irNumber) : clean(rec.applicationNumber) || clean(rec.registrationNumber);
|
|
114
|
+
const unlinked = (reason) => ({ office, label: labelOf(office, own), href: null, reason });
|
|
115
|
+
if (!page) return unlinked("unknown-office");
|
|
116
|
+
if (!page.address) return unlinked("no-page");
|
|
117
|
+
const a = designation ? madridMonitor([rec.irNumber]) : page.address(rec);
|
|
118
|
+
return a ? { office, label: labelOf(office, a.number), href: a.href, reason: null } : unlinked("unaddressable");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The registers whose records carry no page of the vendor's own, so the office's page is the link. */
|
|
122
|
+
export const VENDORS_WITHOUT_RECORD_PAGES = Object.freeze(["signa"]);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Every registration the findings cite, keyed by its lower-cased uri, for a run whose register is
|
|
126
|
+
* `provider`. Returns null for any other register, so those runs render exactly as before: their records
|
|
127
|
+
* carry links of their own. `tally` is what publish records, per office, so that a register whose numbers
|
|
128
|
+
* never fit shows up as a count instead of as a report that quietly looks the way it always did.
|
|
129
|
+
*/
|
|
130
|
+
export function recordLinksFor(findings, recordsByUri, provider) {
|
|
131
|
+
if (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(provider).toLowerCase())) return null;
|
|
132
|
+
const byUri = new Map();
|
|
133
|
+
const tally = { linked: {}, cited: {}, notRetrieved: 0 };
|
|
134
|
+
const bump = (o, k) => { o[k] = (o[k] ?? 0) + 1; };
|
|
135
|
+
for (const f of Array.isArray(findings) ? findings : []) {
|
|
136
|
+
for (const r of f?.owner?.registrations ?? []) {
|
|
137
|
+
const key = clean(r?.uri).toLowerCase();
|
|
138
|
+
if (!key || byUri.has(key)) continue;
|
|
139
|
+
const rec = recordsByUri instanceof Map ? recordsByUri.get(key) : null;
|
|
140
|
+
if (!rec) { byUri.set(key, null); tally.notRetrieved += 1; continue; }
|
|
141
|
+
const link = officeRecordLink(rec, key);
|
|
142
|
+
byUri.set(key, link);
|
|
143
|
+
if (link.href) bump(tally.linked, link.office);
|
|
144
|
+
else bump((tally.cited[link.office] ??= {}), link.reason);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
148
|
+
const linked = Object.entries(tally.linked).map(([o, n]) => `${o} ${n}`).join(", ");
|
|
149
|
+
const cited = Object.entries(tally.cited).flatMap(([o, rs]) => Object.entries(rs).map(([r, n]) => `${o} ${n} ${r}`)).join(", ");
|
|
150
|
+
const summary = `linked ${sum(Object.values(tally.linked))}${linked ? ` (${linked})` : ""}`
|
|
151
|
+
+ ` · cited by number ${sum(Object.values(tally.cited).flatMap((rs) => Object.values(rs)))}${cited ? ` (${cited})` : ""}`
|
|
152
|
+
+ ` · record not retrieved ${tally.notRetrieved}`;
|
|
153
|
+
return { byUri, tally, summary };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const nameOf = (office) => OFFICE_RECORD_PAGES[office]?.name ?? office.toUpperCase();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The report's once-per-office account of the registrations it cites by number rather than links.
|
|
160
|
+
*
|
|
161
|
+
* NO COUNT. The cards list only some findings' registrations (an off-field finding lists none), so a
|
|
162
|
+
* number here could name more registrations than the reader can find on the page. The workbook lists
|
|
163
|
+
* them all, each with its reason.
|
|
164
|
+
*/
|
|
165
|
+
export function officeReasonSentences(byUri) {
|
|
166
|
+
const keys = new Set();
|
|
167
|
+
for (const l of byUri instanceof Map ? byUri.values() : []) if (l && !l.href) keys.add(`${l.office} ${l.reason}`);
|
|
168
|
+
return [...keys].sort().map((k) => {
|
|
169
|
+
const [office, reason] = k.split(" ");
|
|
170
|
+
const name = nameOf(office);
|
|
171
|
+
if (reason === "no-page") return `${name}: the register publishes no page for a single record, so its registrations are cited by number.`;
|
|
172
|
+
if (reason === "unknown-office") return `${name}: we hold no page address for this register, so its registrations are cited by number.`;
|
|
173
|
+
return `${name}: registrations whose numbers are not in the form the register's page address takes are cited by number, not linked.`;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The workbook's Link cell for a finding: its first linked registration, else the first stated reason. */
|
|
178
|
+
export function linkCellFor(registrations, byUri) {
|
|
179
|
+
if (!(byUri instanceof Map)) return "";
|
|
180
|
+
const links = (registrations ?? []).map((r) => byUri.get(clean(r?.uri).toLowerCase())).filter(Boolean);
|
|
181
|
+
const linked = links.find((l) => l.href);
|
|
182
|
+
if (linked) return linked.href;
|
|
183
|
+
const first = links[0];
|
|
184
|
+
if (!first) return "";
|
|
185
|
+
const name = nameOf(first.office);
|
|
186
|
+
if (first.reason === "no-page") return `No page for a single record at this register (${name}); cited by number`;
|
|
187
|
+
if (first.reason === "unknown-office") return `No page address held for this register (${name}); cited by number`;
|
|
188
|
+
return `Number not in the form this register's page address takes (${name}); cited by number`;
|
|
189
|
+
}
|
package/driver/publish/parse.mjs
CHANGED
|
@@ -243,7 +243,7 @@ export function stripTelemetry(md) {
|
|
|
243
243
|
// packaged MCP and any future consumer get it from one definition.
|
|
244
244
|
//
|
|
245
245
|
// WHY NOT RE-WORD THE BUILDER. The audit workbook and the internal surfaces keep the engine's vocabulary
|
|
246
|
-
// by ruling (
|
|
246
|
+
// by ruling ("Out of scope"), and `resolution` is the audit trail's own record of which finding a
|
|
247
247
|
// claim resolved to. Changing what buildAuditMd stamps would take the word off an internal surface that is
|
|
248
248
|
// entitled to it. The boundary is where the audience changes, so the boundary is where the cut belongs —
|
|
249
249
|
// the same shape as scrubCards' `rated_under` → ratedUnderForClient, which is the precedent in that file.
|
|
@@ -256,7 +256,7 @@ export function stripTelemetry(md) {
|
|
|
256
256
|
// Each rule is ANCHORED to one position in a grammar THIS ENGINE writes: the disposition token at the head
|
|
257
257
|
// of `resolution`, and the disposition token inside the `(finding #N, …)` parenthetical the contradiction
|
|
258
258
|
// line builds. A mark named ADVERSARIAL survives both — it is not at position 0 of a resolution line, and
|
|
259
|
-
// it is not inside a finding-reference parenthetical. That is the
|
|
259
|
+
// it is not inside a finding-reference parenthetical. That is the lesson applied: match a POSITION in
|
|
260
260
|
// a grammar we own, never a word anywhere in a string we do not.
|
|
261
261
|
//
|
|
262
262
|
// The enum is IMPORTED (top of file), never retyped: a sixth disposition added to findings-model must not
|
|
@@ -292,7 +292,7 @@ export function contradictionResolutionForClient(v) {
|
|
|
292
292
|
|
|
293
293
|
// ── ENGINE_PLAIN AND plainify ARE DELETED ────────────────────────────────────────────────────
|
|
294
294
|
//
|
|
295
|
-
// doc-52 put nineteen find-and-replace rules on the rendered client surface.
|
|
295
|
+
// doc-52 put nineteen find-and-replace rules on the rendered client surface. This is what that cost:
|
|
296
296
|
// `axis` -> `group` turned "AXIS Bank filed in class 36" into "group Bank filed in class 36" — a report
|
|
297
297
|
// naming a mark that does not exist, inside the report that clears it. AXIS and SLICE are both live
|
|
298
298
|
// trademarks. The ban list and the trademark register overlap, and this engine exists to search the
|
|
@@ -167,3 +167,29 @@ export function readStore(base, name, { path: override = null } = {}) {
|
|
|
167
167
|
// the divergence between those two is exactly what was.
|
|
168
168
|
export const requiredAbsent = (names = []) =>
|
|
169
169
|
(Array.isArray(names) ? names : []).filter((n) => PUBLISH_INPUTS[n] === "required");
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* WHY AN ABSENCE DID NOT HOLD THE RELEASE — the other half of `requiredAbsent`, and the half that was
|
|
173
|
+
* missing from every receipt.
|
|
174
|
+
*
|
|
175
|
+
* A delivered run's record read `released: true`, `reasons: []`, and `inputsAbsent` naming a store that
|
|
176
|
+
* was not there. All three were correct: every store above is declared `optional`, deliberately, so that
|
|
177
|
+
* re-rendering an archived run cannot rewrite the released status of delivered client work. But the
|
|
178
|
+
* reason lived in the table above and nowhere else, so a reader holding the receipt could see only a gate
|
|
179
|
+
* that had released while recording something. This states it where the gate states everything else.
|
|
180
|
+
*
|
|
181
|
+
* It EXPLAINS and decides nothing: `released` is `reasons.length === 0` and stays so. A store the table
|
|
182
|
+
* does not name reads `undeclared` rather than being given a ruling it never got.
|
|
183
|
+
*
|
|
184
|
+
* PURE, and it lives beside the table it reads so the two cannot drift.
|
|
185
|
+
*/
|
|
186
|
+
export const nonClosingAbsences = (names = []) =>
|
|
187
|
+
(Array.isArray(names) ? names : [])
|
|
188
|
+
.filter((n) => PUBLISH_INPUTS[n] !== "required")
|
|
189
|
+
.map((n) => ({
|
|
190
|
+
input: n,
|
|
191
|
+
declared: PUBLISH_INPUTS[n] ?? "undeclared",
|
|
192
|
+
why: PUBLISH_INPUTS[n] === "optional"
|
|
193
|
+
? "declared optional: a run that legitimately has no such store, and every archived run, must still publish"
|
|
194
|
+
: "not a declared publish input — the absence is recorded and nothing gates on it",
|
|
195
|
+
}));
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// ONE report. There is no client variant and no internal variant, and that is
|
|
25
25
|
// still the rule — it is what removes the whole class of "the wrong link got sent".
|
|
26
26
|
//
|
|
27
|
-
// WHAT CHANGED, 2026-09-07 (
|
|
27
|
+
// WHAT CHANGED, 2026-09-07 (ruling): the one report now CARRIES the reviewer's
|
|
28
28
|
// notes rather than routing them to the audit workbook alone. The earlier reading of "one report" was
|
|
29
29
|
// that internal working material is simply not part of it; the ruling is that there is one report and the
|
|
30
30
|
// person who ran Clearotron reads it, so holding material back "just adds confusion for where data is
|
|
@@ -87,9 +87,9 @@ export function inlineMd(s) {
|
|
|
87
87
|
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<i>$2</i>')
|
|
88
88
|
.replace(/`([^`]+)`/g, '<span class="mono">$1</span>');
|
|
89
89
|
}
|
|
90
|
-
//
|
|
90
|
+
// BLOCKS, NOT ONLY PARAGRAPHS.
|
|
91
91
|
//
|
|
92
|
-
// The writer emits sub-headers and bullets inside the summary now (
|
|
92
|
+
// The writer emits sub-headers and bullets inside the summary now (ruling 2026-08-31: "keep the
|
|
93
93
|
// length, add the structure"). This function read exactly two shapes — a blank line as a paragraph
|
|
94
94
|
// break, a single newline as a wrap — so a `## CORAL FREEZE` line reached the client as those literal
|
|
95
95
|
// characters, and a `- point` list as one run of text with hyphens in it. The structure the ruling asks
|
|
@@ -318,8 +318,8 @@ function glanceSection(marks, framework, registerCounts) {
|
|
|
318
318
|
// There is ONE figure per predicate per row, at ONE scope, and the table says which. A second
|
|
319
319
|
// all-classes figure alongside a class-scoped one would be a second count — one more provider call per
|
|
320
320
|
// mark per predicate, billable on Corsearch — and that is a spend the owner has not ruled on. Raised as
|
|
321
|
-
// a follow-up rather than assumed here
|
|
322
|
-
// ── TERRITORIES IN WORDS, NEVER CODES
|
|
321
|
+
// a follow-up rather than assumed here.
|
|
322
|
+
// ── TERRITORIES IN WORDS, NEVER CODES ───────────────────────────────────────────────────────────────
|
|
323
323
|
//
|
|
324
324
|
// The line read "territories: EM, US, WO". On a worldwide run the same line printed every register code
|
|
325
325
|
// the provider offers — roughly two hundred, AD through ZZ, internal groupings among them — which the
|
|
@@ -373,7 +373,7 @@ function listWords(items) {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
|
-
* The counted scope, in one sentence
|
|
376
|
+
* The counted scope, in one sentence. Three shapes, and the shape is chosen by
|
|
377
377
|
* how many registers were counted rather than by which they were:
|
|
378
378
|
*
|
|
379
379
|
* every register the provider offers -> "Counted worldwide, 190 registers, on Clarivate Compumark."
|
|
@@ -405,7 +405,7 @@ const COUNTS_READER_LINE = 'Counts include live, pending and dead filings. '
|
|
|
405
405
|
+ 'A count is not a conflict; the cards above say which filings matter.';
|
|
406
406
|
|
|
407
407
|
function countsSection(marks, registerCounts) {
|
|
408
|
-
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER
|
|
408
|
+
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER ──────────────────────────────────────────────────
|
|
409
409
|
//
|
|
410
410
|
// The 70-word count-basis paragraph existed because three one-word headers — Identical, Containing,
|
|
411
411
|
// Close variations — did not say what they counted, so the definition had to go somewhere. Put it in
|
|
@@ -451,7 +451,7 @@ function countsSection(marks, registerCounts) {
|
|
|
451
451
|
return `<tr><td><b>${esc(m.name)}</b><br><span class="ko-classes">${esc(scope)}</span></td>${cells}</tr>`
|
|
452
452
|
+ (forms ? `<tr class="ko-forms"><td colspan="${COUNT_PREDICATES.length + 1}">${esc(forms)}</td></tr>` : '');
|
|
453
453
|
}).join('');
|
|
454
|
-
// COUNT_BASIS IS OFF THE PAGE, NOT OUT OF THE RECORD
|
|
454
|
+
// COUNT_BASIS IS OFF THE PAGE, NOT OUT OF THE RECORD. It is still written to
|
|
455
455
|
// report-data.json (knockoutReportData -> registerCountBasis) and still on the workbook, which is where
|
|
456
456
|
// 331 says it belongs; what it stops doing is printing twice on a page whose headers now say the same
|
|
457
457
|
// thing in three words each.
|
|
@@ -461,7 +461,7 @@ function countsSection(marks, registerCounts) {
|
|
|
461
461
|
</div>`;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
//
|
|
464
|
+
// The filings behind the narrow numbers, as a compact appendix under the counts.
|
|
465
465
|
//
|
|
466
466
|
// FIRST N ROWS, then the workbook. The full list is capped at 100 per name and a hundred rows of
|
|
467
467
|
// register data on a triage document would bury the page it is an appendix to; the workbook is where a
|
|
@@ -474,7 +474,7 @@ function countsSection(marks, registerCounts) {
|
|
|
474
474
|
// of them is a finding. recordsLine owns all three wordings so the report, the workbook and
|
|
475
475
|
// report-data.json cannot phrase them differently.
|
|
476
476
|
/**
|
|
477
|
-
* MATERIALITY, FOR BOTH REGISTER SURFACES
|
|
477
|
+
* MATERIALITY, FOR BOTH REGISTER SURFACES.
|
|
478
478
|
*
|
|
479
479
|
* Two caps decide which filings a client sees — the appendix table and the promoted cards — and both
|
|
480
480
|
* took the provider's arrival order. Measured on a delivered four-mark run: one name fetched 23 records,
|
|
@@ -591,7 +591,7 @@ function filingsSection(marks, registerRecords) {
|
|
|
591
591
|
return `<div class="ko-row"><div><p class="ko-name">${esc(m.name)}</p></div>
|
|
592
592
|
<div><p class="ko-bul">${esc(line ?? 'No filings listing was recorded for this name.')}</p></div></div>`;
|
|
593
593
|
}
|
|
594
|
-
// RANKED, AND DELIBERATELY NOT FILTERED
|
|
594
|
+
// RANKED, AND DELIBERATELY NOT FILTERED. This table is the evidence behind a
|
|
595
595
|
// COUNT: dropping a cancelled or out-of-class row would leave a reader unable to reconcile the table
|
|
596
596
|
// with the number above it, and "we did not show you that one" is a different promise from "it is
|
|
597
597
|
// not material". The cards promote and therefore filter; the appendix evidences and therefore ranks.
|
|
@@ -660,7 +660,7 @@ function tierAbsenceLine(registerCounts, probeRan) {
|
|
|
660
660
|
// nicely, but portal-ui/src/contract/reportFrame.ts refuses a mark from the document side on purpose,
|
|
661
661
|
// and the composite key IS a mark name. The server reads `ref` back off disk once it has resolved.
|
|
662
662
|
/**
|
|
663
|
-
* THE CARD'S DETAIL, FOLDED
|
|
663
|
+
* THE CARD'S DETAIL, FOLDED.
|
|
664
664
|
*
|
|
665
665
|
* Visible on a card: the reference, the name, the band chip, the source chip, the one-sentence read and
|
|
666
666
|
* the evidence links. Folded: the paragraph that argues the band, and on a register card the use-check
|
|
@@ -685,8 +685,8 @@ const WHY_BAND_LABEL = 'Why this band';
|
|
|
685
685
|
function findingBlock(v, framework, markIndex) {
|
|
686
686
|
const meta = [v.type, v.owner].filter(Boolean).map((s) => esc(s)).join(' · ');
|
|
687
687
|
const ev = v.evidence.map((u) => linkOrText(u)).join(' · ');
|
|
688
|
-
// THE CLEARANCE REPORT'S CARD, AND THE KNOCKOUT'S LOCATOR — both classes, deliberately
|
|
689
|
-
//
|
|
688
|
+
// THE CLEARANCE REPORT'S CARD, AND THE KNOCKOUT'S LOCATOR — both classes, deliberately. report.css
|
|
689
|
+
// is already inlined here and already defines .card, .top, .rail, .cardhead,
|
|
690
690
|
// .fnum and .who; this uses them rather than a parallel set, so a client who has read one report
|
|
691
691
|
// recognises the other.
|
|
692
692
|
//
|
|
@@ -808,9 +808,9 @@ const NOT_WEIGHED = NOT_WEIGHED_LINE;
|
|
|
808
808
|
/**
|
|
809
809
|
* The register card's basis line, which cannot be written without knowing whether the card is rated.
|
|
810
810
|
*
|
|
811
|
-
* THE TWO RULINGS MEET HERE AND THEY ALMOST CONTRADICTED EACH OTHER ON A CLIENT PAGE.
|
|
812
|
-
*
|
|
813
|
-
*
|
|
811
|
+
* THE TWO RULINGS MEET HERE AND THEY ALMOST CONTRADICTED EACH OTHER ON A CLIENT PAGE. One ruling
|
|
812
|
+
* retired a code-owned placeholder because a rating nobody performed must not appear; the other then
|
|
813
|
+
* ruled that the rater's OWN band for a promoted filing must appear, because a filing
|
|
814
814
|
* the rater weighed into the verdict was rendering as though it had not been. Both hold, and they hold
|
|
815
815
|
* over different cards.
|
|
816
816
|
*
|
|
@@ -844,7 +844,7 @@ function basisLine(band, read) {
|
|
|
844
844
|
* Neither test is a judgment about the filing. Whether any of it blocks anything is lawyer work this
|
|
845
845
|
* product does not do — the same line register-records.mjs rule 3 has held since the lane shipped.
|
|
846
846
|
*/
|
|
847
|
-
// EXPORTED so the pipeline can ask the same question the page asks
|
|
847
|
+
// EXPORTED so the pipeline can ask the same question the page asks. The owner
|
|
848
848
|
// use-check runs on PROMOTED filings only, and "promoted" has to mean here what it means on the report —
|
|
849
849
|
// a second predicate would let the engine search an owner whose filing the report never shows, or skip
|
|
850
850
|
// one it does.
|
|
@@ -864,7 +864,7 @@ export function promotableRecords(entry, mark) {
|
|
|
864
864
|
/** One sentence of what the filing IS, composed from the register's own fields. No adjective that the
|
|
865
865
|
* record did not supply, and every absent field says it is absent rather than rendering as a blank —
|
|
866
866
|
* a blank cell and an unstated field read alike to a human and only one is honest. */
|
|
867
|
-
// ── THE OFFICE, NOT THE VENDOR
|
|
867
|
+
// ── THE OFFICE, NOT THE VENDOR ──────────────────────────────────────────────────────────────────────
|
|
868
868
|
//
|
|
869
869
|
// The sentence read "A filing of the identical name on Clarivate Compumark — held by ..., status
|
|
870
870
|
// PENDING, classes 9, 38, 45, territory us." Clarivate Compumark is the search vendor; the filing is at
|
|
@@ -926,7 +926,7 @@ function registerStatement(r, provider) {
|
|
|
926
926
|
* which the card links.
|
|
927
927
|
*/
|
|
928
928
|
/**
|
|
929
|
-
* WHICH OF A MARK'S REGISTER CARDS THE PAGE DRAWS
|
|
929
|
+
* WHICH OF A MARK'S REGISTER CARDS THE PAGE DRAWS.
|
|
930
930
|
*
|
|
931
931
|
* THIS FILTERS THE PAGE, NEVER THE RECORD, and that separation is the whole point. 331 rejects dropping
|
|
932
932
|
* anything from report-data.json or the workbook to make the page shorter, so `registerCardViews` still
|
|
@@ -951,7 +951,7 @@ function registerStatement(r, provider) {
|
|
|
951
951
|
* is kept, because "the rater said something, and the safe reading of something we cannot rank is that
|
|
952
952
|
* it is worth pointing at". An absent band is strictly LESS information than an unrankable one, so if
|
|
953
953
|
* unknown keeps, absent keeps. Dropping it would be this renderer deciding a filing is immaterial on
|
|
954
|
-
* the rater's behalf — the same claim
|
|
954
|
+
* the rater's behalf — the same claim a later ruling removed from these cards for the same reason.
|
|
955
955
|
*
|
|
956
956
|
* A run that carries no bands at all still renders every card it was delivered with, which is what
|
|
957
957
|
* clause F asks for; it now falls out of the rule instead of being carved around it.
|
|
@@ -961,7 +961,7 @@ function registerCardsOnPage(cards, mark, framework) {
|
|
|
961
961
|
}
|
|
962
962
|
|
|
963
963
|
/**
|
|
964
|
-
* DID THE RATER PUT THIS FILING ON THE LADDER'S LOWEST RUNG?
|
|
964
|
+
* DID THE RATER PUT THIS FILING ON THE LADDER'S LOWEST RUNG?
|
|
965
965
|
*
|
|
966
966
|
* The ladder runs worst-first, so the lowest rung is the LAST entry. A filing the rater put there is one
|
|
967
967
|
* the rater called manageable, and 331 keeps those in the filings table rather than on a card: three
|
|
@@ -1017,7 +1017,7 @@ function registerCardViews(mark, framework, registerRecords) {
|
|
|
1017
1017
|
/**
|
|
1018
1018
|
* ONE promoted filing, in findingBlock's own anatomy — the SAME anatomy, now, not a parallel one.
|
|
1019
1019
|
*
|
|
1020
|
-
*
|
|
1020
|
+
* RULING: one combined card, the same head as every other conflict, tagged by
|
|
1021
1021
|
* SOURCE. "Weighed" and "not weighed" are not terms we use with a client, so the band slot's neutral word
|
|
1022
1022
|
* is gone; what tells the two apart on the page is where the finding came from, which is the vocabulary
|
|
1023
1023
|
* the clearance reports already use for exactly this.
|
|
@@ -1025,7 +1025,7 @@ function registerCardViews(mark, framework, registerRecords) {
|
|
|
1025
1025
|
* THE BAND CHIP IS DRAWN ONLY WHEN THE RATER SENT ONE, and that condition is the whole rule. The old note
|
|
1026
1026
|
* here said "no band chip and no `bandStop`, ever: a rung of the run's ladder would be a rating nobody
|
|
1027
1027
|
* performed" — which was true for as long as nothing could carry the rater's rating of an individual
|
|
1028
|
-
* filing. `registerReads[].band` now can
|
|
1028
|
+
* filing. `registerReads[].band` now can, so the objection is answered where it was
|
|
1029
1029
|
* raised: with a band, the rating WAS performed, by the seat, in the framework's own words, and the chip
|
|
1030
1030
|
* states it. With no band the old rule stands unchanged — --faint rail, no chip, nothing claimed — and
|
|
1031
1031
|
* that is still what every archived run renders.
|
|
@@ -1059,7 +1059,7 @@ function sourceChips(v) {
|
|
|
1059
1059
|
|
|
1060
1060
|
// ── THE REVIEWER'S NOTES, ON THE REPORT ─────────────────────────────────────────────────
|
|
1061
1061
|
//
|
|
1062
|
-
//
|
|
1062
|
+
// RULING, 2026-09-07: there is one report and the person who ran Clearotron reads it — keeping
|
|
1063
1063
|
// this material off the page "just adds confusion for where data is lost". This replaces the earlier
|
|
1064
1064
|
// split under which the notes went to the audit workbook alone (the header of this file and
|
|
1065
1065
|
// publish/knockout.mjs carried that rule; both now say what replaced it). The workbook keeps them too:
|
|
@@ -1071,7 +1071,7 @@ function sourceChips(v) {
|
|
|
1071
1071
|
// findings about the mark, which is the one way this ruling could produce a worse document.
|
|
1072
1072
|
const REVIEWER_NOTES_LEGEND = 'Purple notes are for the reviewing lawyer. Remove them before this goes to the client.';
|
|
1073
1073
|
|
|
1074
|
-
// The clearance lane's own label, copied rather than re-worded
|
|
1074
|
+
// The clearance lane's own label, copied rather than re-worded. One spelling across
|
|
1075
1075
|
// both products is the point: a reader who has seen it on a clearance report knows what it means here.
|
|
1076
1076
|
const USE_CHECK_LABEL = 'Use-check source:';
|
|
1077
1077
|
|
|
@@ -1087,7 +1087,7 @@ function ownerCheckFor(ownerChecks, recordId) {
|
|
|
1087
1087
|
// needs the same answer: the page files each note, and the reviewer warns a writer whose note will file
|
|
1088
1088
|
// the way they did not intend. One reader, so the two can never disagree.
|
|
1089
1089
|
/**
|
|
1090
|
-
* "About this request" — what was asked, and any flag on the asking
|
|
1090
|
+
* "About this request" — what was asked, and any flag on the asking.
|
|
1091
1091
|
*
|
|
1092
1092
|
* WHAT WAS ASKED IS CODE-OWNED. It is read off the run's own frozen instructed scope, never off model
|
|
1093
1093
|
* prose, so the line states the request the run was given rather than a paraphrase of it. An archived run
|
|
@@ -1143,7 +1143,7 @@ function registerFindingBlock(v, markIndex, reads = null, framework = null, owne
|
|
|
1143
1143
|
// colour an unknown word as though it were rated.
|
|
1144
1144
|
const band = row?.band || null;
|
|
1145
1145
|
const stop = band ? bandStop(framework, band) : null;
|
|
1146
|
-
// ── WHERE THE OWNER'S TRADE WAS LOOKED UP
|
|
1146
|
+
// ── WHERE THE OWNER'S TRADE WAS LOOKED UP ──────────────────────────────────────────────────────
|
|
1147
1147
|
//
|
|
1148
1148
|
// The source is the DRIVER'S, joined by this filing's own recordId — never a URL the seat typed. That
|
|
1149
1149
|
// is the whole reason a reader can trust it: the row exists because the driver made the call, so
|
|
@@ -1288,7 +1288,7 @@ function coverageClause(mark, registerCounts, probeRan) {
|
|
|
1288
1288
|
// under a collapsed "Full narrative" — the reader who wants the long form still has it, one click away,
|
|
1289
1289
|
// and no sentence the stage wrote is dropped on the floor.
|
|
1290
1290
|
/**
|
|
1291
|
-
* THE LABELS, IN THE READER'S WORDS
|
|
1291
|
+
* THE LABELS, IN THE READER'S WORDS.
|
|
1292
1292
|
*
|
|
1293
1293
|
* The old pair was "What holds it there" and "What would move it", 9.5px grey capitals. The owner read
|
|
1294
1294
|
* the first one and asked "what holds what?" — so the fix is not a synonym in the same style, it is a
|
|
@@ -1361,7 +1361,7 @@ function readBlock(m, framework) {
|
|
|
1361
1361
|
counter.length ? `<div class="ko-counter"><span class="ko-lbl2">${esc(counterLabel(framework, band))}</span><ul class="ko-bul">${
|
|
1362
1362
|
counter.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
|
|
1363
1363
|
m.mitigation ? `<div class="ko-mitig"><span class="ko-lbl2">What would lower the risk</span><p>${inlineMd(m.mitigation)}</p></div>` : '',
|
|
1364
|
-
// ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD
|
|
1364
|
+
// ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD ────────────────────────────────────────────
|
|
1365
1365
|
//
|
|
1366
1366
|
// The model writes both. `bullets` (five) say in other words what `factors` (four) already say
|
|
1367
1367
|
// visible above; `assessment` is the long-form read under its own four headings, and until now the
|
|
@@ -1509,7 +1509,7 @@ const CAVEAT_LEAD = 'This screen also carries the following limits:';
|
|
|
1509
1509
|
export function renderKnockoutHtml(findings, framework, {
|
|
1510
1510
|
runId, overall, issued = null, auditFile = null, probeRan = false, registerCounts = null,
|
|
1511
1511
|
registerRecords = null,
|
|
1512
|
-
// The driver's own record of the owner lookups it ran
|
|
1512
|
+
// The driver's own record of the owner lookups it ran. Defaults to [] so an
|
|
1513
1513
|
// archived run that predates the lane renders exactly as it was delivered.
|
|
1514
1514
|
ownerChecks = [],
|
|
1515
1515
|
identity = null, matter = null, homeHref = null, chromeHref = null, depthNote = null,
|
|
@@ -1517,7 +1517,7 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1517
1517
|
// instructed to do rather than what its artifacts happen to show. Defaults to null, and an archived
|
|
1518
1518
|
// run that carries none keeps the artifact-derived sentence it was delivered with.
|
|
1519
1519
|
searchPolicy = null,
|
|
1520
|
-
// — the run's own frozen instructed scope
|
|
1520
|
+
// — the run's own frozen instructed scope: the goods, classes and
|
|
1521
1521
|
// territories the requester asked for, in the requester's words. Defaults to null, and an archived run
|
|
1522
1522
|
// that carries no such sidecar renders "About this request" from its request-level notes alone, or not
|
|
1523
1523
|
// at all when it has none. Never derived from model prose — this is what was ASKED, not what was said
|
|
@@ -1549,7 +1549,7 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1549
1549
|
const allViews = marks.flatMap((m) => knockoutFindingViews(m, { manifest: framework }));
|
|
1550
1550
|
const citedFindings = allViews.filter((v) => v.evidence.length).length;
|
|
1551
1551
|
const uncitedFindings = allViews.length - citedFindings;
|
|
1552
|
-
// ── A ONE-NAME PAGE HAS NO INDEX
|
|
1552
|
+
// ── A ONE-NAME PAGE HAS NO INDEX ────────────────────────────────────────────────────────────────
|
|
1553
1553
|
//
|
|
1554
1554
|
// The glance row exists to let a reader of a BATCH find the name they care about. With one name there
|
|
1555
1555
|
// is nothing to index, and the row printed that name, its band and its classes for the first of three
|
|
@@ -1563,7 +1563,7 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1563
1563
|
const counts = hasCounts
|
|
1564
1564
|
? countsSection(marks, registerCounts)
|
|
1565
1565
|
: `<div class="panel"><p class="ko-tier">${esc(tierLine)}</p></div>`;
|
|
1566
|
-
// ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED
|
|
1566
|
+
// ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED ────────────────────────
|
|
1567
1567
|
//
|
|
1568
1568
|
// The four model-written caveats on the measured run overlap the fixed text completely — 331 item 9,
|
|
1569
1569
|
// and 333 rule 4 ("say it once") fixes it at the source for runs written under that doctrine. This is
|
|
@@ -1631,7 +1631,7 @@ ${filings}`
|
|
|
1631
1631
|
</div>
|
|
1632
1632
|
<div class="fab-stack">${themeButton()}</div>
|
|
1633
1633
|
<script>
|
|
1634
|
-
/* THE DOCUMENT'S OWN CONTROLS, and they are GLOBAL on purpose
|
|
1634
|
+
/* THE DOCUMENT'S OWN CONTROLS, and they are GLOBAL on purpose.
|
|
1635
1635
|
The portal frames this document and drives it by postMessage. The bridge injected at serve time looks
|
|
1636
1636
|
up exportPDF, pickAll and openAll BY NAME and replies "this report has no <verb>" when a name is not
|
|
1637
1637
|
there. Every one of these lived inside an IIFE, so a knockout answered that to all three and every
|
|
@@ -1664,7 +1664,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1664
1664
|
${filingsSec}
|
|
1665
1665
|
|
|
1666
1666
|
<div class="sec"><span class="num">${num()}</span><h2>Scope & what we didn't search</h2><span class="note">what this screen is</span></div>
|
|
1667
|
-
<!-- COLLAPSED, like the clearance report's
|
|
1667
|
+
<!-- COLLAPSED, like the clearance report's. Both documents already inline the
|
|
1668
1668
|
same report.css, and details.scope is its vocabulary — this section was the one place the two
|
|
1669
1669
|
lanes presented the same thing differently, with the knockout's open panel pushing the filings
|
|
1670
1670
|
table and the footer down the page on every read. What is inside is unchanged; a reader who
|
|
@@ -1674,10 +1674,10 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1674
1674
|
<details class="scope"><summary>Scope & what we didn't search</summary>
|
|
1675
1675
|
<div class="panel drillbody">
|
|
1676
1676
|
${(() => {
|
|
1677
|
-
// FROM THE POLICY WHERE THERE IS ONE
|
|
1677
|
+
// FROM THE POLICY WHERE THERE IS ONE. The fallback is not a tidy default: an
|
|
1678
1678
|
// archived run froze no policy, and re-rendering it must not invent a scope claim about a run
|
|
1679
1679
|
// nobody can now ask. It keeps the sentence it was delivered with.
|
|
1680
|
-
// ── THE SCOPE BLOCK, WHOLE
|
|
1680
|
+
// ── THE SCOPE BLOCK, WHOLE ────────────────────────────────────────────────────────────────────
|
|
1681
1681
|
//
|
|
1682
1682
|
// The composed lines said one thing five times in 362 words: "not a clearance" three times,
|
|
1683
1683
|
// "proceeds to clearance" twice, and the count-basis sentence a second time after the counts table
|
|
@@ -1771,7 +1771,7 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1771
1771
|
classesDriving: m.classesDriving ?? [],
|
|
1772
1772
|
degraded: Boolean(m.degraded),
|
|
1773
1773
|
points: m.bullets ?? [],
|
|
1774
|
-
// ── THE ASSESSMENT THE RUN ALREADY WROTE
|
|
1774
|
+
// ── THE ASSESSMENT THE RUN ALREADY WROTE ──────────────────────────────────────────────────
|
|
1775
1775
|
//
|
|
1776
1776
|
// Seven keys the assess stage writes into knockout-findings.json reached this file as nothing.
|
|
1777
1777
|
// They were not withheld by a rule — no code decided against them; this projection is an explicit
|
|
@@ -1828,7 +1828,7 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1828
1828
|
// `shape: 'register'` is the load-bearing field: a consumer can tell a weighed conflict from a
|
|
1829
1829
|
// pointed-at filing without parsing prose.
|
|
1830
1830
|
//
|
|
1831
|
-
// `band` AND `basis` NOW CARRY THE RATER'S READ WHEN IT SENT ONE
|
|
1831
|
+
// `band` AND `basis` NOW CARRY THE RATER'S READ WHEN IT SENT ONE. Before
|
|
1832
1832
|
// this, both were constants — `band: null` and the not-weighed sentence — on every filing of
|
|
1833
1833
|
// every run, including runs where the seat had written a full read of that exact filing and the
|
|
1834
1834
|
// HTML card was already printing it. The page and this file disagreed, and this file is the one
|
|
@@ -1842,8 +1842,8 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1842
1842
|
.find((x) => String(x?.recordId ?? '').trim() && String(x?.recordId ?? '').trim() === String(v.record?.recordId ?? '').trim());
|
|
1843
1843
|
const read = String(row?.read ?? '').trim() || null;
|
|
1844
1844
|
const band = String(row?.band ?? '').trim() || null;
|
|
1845
|
-
// The driver's own record of where this filing's proprietor was looked up
|
|
1846
|
-
//
|
|
1845
|
+
// The driver's own record of where this filing's proprietor was looked up, joined by
|
|
1846
|
+
// recordId. `null` where the run owed no check — the same three states the
|
|
1847
1847
|
// card has, so a consumer can tell "searched and found nothing" from "never searched".
|
|
1848
1848
|
const oc = ownerCheckFor(ownerChecks, v.record?.recordId);
|
|
1849
1849
|
return {
|