clearotron 0.3.2-beta.8 → 0.3.2-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -3
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +6 -6
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/start.mjs +18 -4
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +6 -3
- package/docs/architecture/05-config-governance.md +6 -1
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +48 -0
- package/driver/README.md +3 -3
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -3
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +19 -19
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +24 -17
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +95 -45
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +5 -5
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +5 -2
- package/driver/flag-snapshot.mjs +6 -3
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +12 -6
- package/driver/jx-lanes.mjs +2 -2
- package/driver/jx-units.mjs +1 -1
- package/driver/jx.mjs +30 -2
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/named-band.mjs +1 -33
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +206 -68
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-report.mjs +92 -5
- package/driver/portal-service.mjs +34 -8
- package/driver/portal-upstream.mjs +1 -1
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +110 -5
- package/driver/publish/knockout.mjs +29 -4
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +115 -24
- package/driver/publish/render.mjs +153 -34
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +60 -3
- package/driver/publish/xlsx.mjs +8 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +67 -0
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +19 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/run-requirements.mjs +3 -3
- package/driver/runner.mjs +2 -2
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +53 -53
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +218 -92
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +2 -2
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +2 -0
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +67 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -0
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-6jzO9HiX.js → index-EVaSo5-g.js} +1459 -482
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +4 -4
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +27 -0
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +20 -10
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +3 -1
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -40,10 +40,17 @@ import { join, dirname } from 'node:path';
|
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
41
|
import {
|
|
42
42
|
REPORT_ROOT, REPORT_ROOT_DARK_EXPLICIT, THEME_INIT_EXPLICIT, themeButton,
|
|
43
|
-
THEME_BTN_CSS, CHROME_CSS, FAVICON_LINK, logoLockup, BRAND, confPosture,
|
|
43
|
+
THEME_BTN_CSS, CHROME_CSS, FAVICON_LINK, logoLockup, BRAND, confPosture, sectionStrip,
|
|
44
44
|
} from '../../shared/brand.mjs';
|
|
45
45
|
import { SUMMARY_BLOCK_LINE, parseSummaryBlocks } from '../../shared/summary-blocks.mjs';
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
// THIS BOARD'S FOUR ENTRIES, in its order. Its third reads "Also considered" and resolves at the filings
|
|
48
|
+
// section, which is what that section is; the rule that filters them against the finished document lives
|
|
49
|
+
// in shared/brand.mjs, beside the clearance report's own five.
|
|
50
|
+
const STRIP = Object.freeze([
|
|
51
|
+
['summary', 'Summary'], ['findings', 'Findings'], ['filings', 'Also considered'], ['next', 'Next steps'],
|
|
52
|
+
]);
|
|
53
|
+
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine, disclosedFloor, moreThan } from '../register-count.mjs';
|
|
47
54
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
48
55
|
import { officeLinkSentences } from './office-record-links.mjs';
|
|
49
56
|
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
@@ -164,6 +171,39 @@ function qualifierHtml(q) {
|
|
|
164
171
|
// The layout this report adds on top of report.css. Additive and `ko-`prefixed: report.css is shared with
|
|
165
172
|
// the frozen clearance renderer, and a selector that is inert there cannot regress it.
|
|
166
173
|
const KO_CSS = `
|
|
174
|
+
/* THE BOARD'S SECTION STRIP AND ITS SECTION FURNITURE, lifted from the approved knockout board rather
|
|
175
|
+
than written here. Four entries; the third reads "Also considered" and resolves at the filings
|
|
176
|
+
section. Marked not to print, because the board marks it so: a strip that follows the reader down
|
|
177
|
+
the page is a control, and paper has nothing for it to follow.
|
|
178
|
+
NO BACKTICK BELOW, and none in this comment — one closes the template literal this sits inside, and
|
|
179
|
+
it has cost this file two silent breakages already.
|
|
180
|
+
The number span is drawn and then hidden, which is the board's own doing: it emits the span on every
|
|
181
|
+
section and sets it to none further down its own stylesheet. Carried so the delivered document holds
|
|
182
|
+
the same elements as the approved one; no pixel moves. */
|
|
183
|
+
/* THE BREADCRUMB IS A ROW OF THE HEADER, not a bar under it (owner, 2026-09-18).
|
|
184
|
+
It used to be a SIBLING of .rep-stickyhead, pinned on its own at top:var(--tb-h,52px) — and
|
|
185
|
+
--tb-h is set nowhere in this product, so the fallback was a guess at a bar that measures ~46px:
|
|
186
|
+
content showed through the slit between the two, and z-index:20 put the strip UNDER the header's
|
|
187
|
+
100 whenever the guess was wrong. The strip is now emitted inside .rep-stickyhead, so the header
|
|
188
|
+
and the breadcrumb are one sticky surface that pins and unpins together and can never gap or
|
|
189
|
+
overlap, whatever the bar measures or how it wraps.
|
|
190
|
+
No background and no bottom hairline of its own: the wrapper carries the blurred surface and the
|
|
191
|
+
single edge, and the border-top here is the divider between the two rows. It is on .strip rather
|
|
192
|
+
than on any wrapper because sectionStrip() emits NOTHING when fewer than two of its sections are
|
|
193
|
+
live — a wrapper would leave a stray line on those reports.
|
|
194
|
+
The gutter matches the topbar's, so the first entry lines up under the back button. */
|
|
195
|
+
.strip{display:flex;gap:4px;align-items:center;padding:4px max(26px,calc((100% - 1120px)/2)) 6px;
|
|
196
|
+
border-top:1px solid var(--line);font:600 12px/1 'Satoshi','Helvetica Neue',Arial,sans-serif;
|
|
197
|
+
letter-spacing:.04em;overflow-x:auto;scrollbar-width:none}
|
|
198
|
+
.strip::-webkit-scrollbar{display:none}
|
|
199
|
+
.strip a{display:inline-flex;align-items:center;gap:7px;padding:7px 10px;border-radius:999px;color:#6b5d50;text-decoration:none;white-space:nowrap}
|
|
200
|
+
.strip a i{width:8px;height:8px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box}
|
|
201
|
+
.strip a.done{color:#4c7a4c}
|
|
202
|
+
.strip a.done i{background:#4c7a4c;border-color:#4c7a4c}
|
|
203
|
+
.strip a.now{color:#250902;background:rgba(0,0,0,.06)}
|
|
204
|
+
.strip a.now i{border-color:#860F09;border-width:3px}
|
|
205
|
+
.sec .num{display:none}
|
|
206
|
+
.sec .note{display:inline;margin-left:10px;font-size:12px;color:var(--faint)}
|
|
167
207
|
.ko-glance{padding:6px 0 2px}
|
|
168
208
|
/* — framework attribution, captioning the rows. Same gutter and hairline as.ko-row so it reads
|
|
169
209
|
as the table's head, not a floating note. */
|
|
@@ -264,14 +304,55 @@ const KO_CSS = `
|
|
|
264
304
|
.ko-scope{font-size:13.5px;color:var(--slate);line-height:1.65;margin:0;padding:18px 24px}
|
|
265
305
|
.ko-scope b{color:var(--ink)}
|
|
266
306
|
/* A wide counts table must scroll inside its own panel, never push the page sideways. */
|
|
307
|
+
/* AND IT SHOWS THAT IT SCROLLS. A panel that continues off its right edge and says nothing leaves a
|
|
308
|
+
reader who never drags it believing they have seen the whole table; the columns past the edge are
|
|
309
|
+
reachable and invisible at the same time. Ruled 2026-09-17: a scrollbar, always visible, and no text.
|
|
310
|
+
The -webkit-appearance:none is the part that does the work — without it the platform draws an OVERLAY
|
|
311
|
+
scrollbar that appears only once a scroll is already under way, which is the state this rule ends.
|
|
312
|
+
scrollbar-width and scrollbar-color are the same instruction for the engines that take the standard
|
|
313
|
+
properties. (NO BACKTICK IN THIS COMMENT: it sits inside the stylesheet's own template literal, and
|
|
314
|
+
one closes it. That is written above the export menu block too, and I still did it — so it is here
|
|
315
|
+
as well, beside the second rule anyone adds.) The colours are tokens, not fixed values, for the reason the ladder below this block
|
|
316
|
+
records: a fixed colour here reads on one ground and goes invisible on the other. */
|
|
267
317
|
.ko-scroll{overflow-x:auto}
|
|
268
|
-
|
|
318
|
+
.ko-scroll::-webkit-scrollbar{height:9px;-webkit-appearance:none}
|
|
319
|
+
.ko-scroll::-webkit-scrollbar-track{background:transparent}
|
|
320
|
+
.ko-scroll::-webkit-scrollbar-thumb{background:var(--faint);border-radius:5px}
|
|
321
|
+
/* THE TWO INSTRUCTIONS CANNOT SIT TOGETHER, and this was measured rather than reasoned. Setting the
|
|
322
|
+
standard scrollbar-width alongside the pseudo-elements makes the engine take the standard path and
|
|
323
|
+
ignore them, and on this one the standard path draws an overlay bar with no layout height at all.
|
|
324
|
+
Driven three ways on a delivered report at 390px, reading the height the bar takes out of the panel:
|
|
325
|
+
both together 0px, the pseudo-elements alone 9px, the standard properties alone 0px. So the standard
|
|
326
|
+
ones go behind a support query that the engines carrying the pseudo-elements never enter, which
|
|
327
|
+
leaves each engine exactly one instruction. */
|
|
328
|
+
@supports not selector(::-webkit-scrollbar){
|
|
329
|
+
.ko-scroll{scrollbar-width:thin;scrollbar-color:var(--faint) transparent}}
|
|
330
|
+
/* A finding's bullets carry bare source URLs, and a URL offers a line no place to break. On a phone
|
|
331
|
+
that unbreakable run becomes the minimum width of the grid item, so the single column floors at
|
|
332
|
+
443px inside a 360px viewport and the whole page scrolls sideways -- 135px of it, measured
|
|
333
|
+
2026-09-17. Only the value "anywhere" lowers that minimum: "break-word" breaks the line but
|
|
334
|
+
leaves the minimum where it was, which is why the blocks beside this one that already set
|
|
335
|
+
word-break were never the cause. Phone widths only: nothing at or above 700px changes.
|
|
336
|
+
NO BACKTICK IN THIS COMMENT -- it sits inside the stylesheet's own template literal. */
|
|
337
|
+
@media(max-width:700px){.ko-row{grid-template-columns:1fr;gap:10px}
|
|
338
|
+
.ko-bul{overflow-wrap:anywhere}}
|
|
269
339
|
|
|
270
340
|
/* The band ladder, the territories fold and the run's own caveats (the 2026-09-16 report redesign). Ported from
|
|
271
341
|
the design's stylesheet with its fixed colours replaced by this page's tokens, so the ladder reads
|
|
272
342
|
in both themes: the tick row was #a89a8a on white and the fold's summary and prose a brown pair,
|
|
273
343
|
all three of which go invisible on the dark ground this report also renders on. */
|
|
274
|
-
|
|
344
|
+
/* THE BAND PILL HANGS ABOVE THE BAR, so the space above the bar is what keeps it off the heading.
|
|
345
|
+
NO BACKTICK IN THIS COMMENT — it sits inside the stylesheet's own template literal.
|
|
346
|
+
.kmarker sits at top:-30px and the pill is ~23px tall (5+5 padding on a 13px line), so it starts
|
|
347
|
+
30px above the bar and ends 7px above it. At the LEFTMOST band the pill is also drawn flush left
|
|
348
|
+
(translateX(-12px) at left:0%) — directly under the left-aligned "Overall risk" — so that is the
|
|
349
|
+
rung where the two touch, and the house ladder's leftmost rung is Low.
|
|
350
|
+
THE ROOM IS ONE MARGIN, NOT TWO, and that is what made the arithmetic wrong twice. This margin and
|
|
351
|
+
.gauge .label's 18px bottom margin are ADJACENT SIBLINGS, so they collapse to the larger of the two
|
|
352
|
+
rather than adding: 10px here gave 18px of room against a pill that needs 30, and the first attempt
|
|
353
|
+
at 24 still left 6. Measured in a browser at every rung (scripts/report-header-render-check.mjs),
|
|
354
|
+
not reasoned: 44 gives 14px of daylight under the heading at every band. */
|
|
355
|
+
.kscale{position:relative;margin:44px 8px 34px}
|
|
275
356
|
.kbar{position:relative;height:8px;border-radius:4px}
|
|
276
357
|
.kmarker{position:absolute;top:-30px;text-align:center}
|
|
277
358
|
.kpill{display:inline-block;padding:5px 12px;border-radius:999px;color:#fff;
|
|
@@ -434,9 +515,9 @@ function territoriesLine(registerCounts) {
|
|
|
434
515
|
return `Counted on ${regions.length} registers, listed on the workbook's Register Counts sheet, on ${provider}.`;
|
|
435
516
|
}
|
|
436
517
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
518
|
+
// The reader line that used to sit under the counts table ("a count is not a conflict…") is off the page:
|
|
519
|
+
// the approved knockout board carries no caveat under the table, and the writing standard lists it among
|
|
520
|
+
// the caveat sentences a page does not print. The column headers say what each count is.
|
|
440
521
|
|
|
441
522
|
function countsSection(marks, registerCounts, positions = '') {
|
|
442
523
|
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER ──────────────────────────────────────────────────
|
|
@@ -471,9 +552,10 @@ function countsSection(marks, registerCounts, positions = '') {
|
|
|
471
552
|
const why = c?.unavailable ?? (c
|
|
472
553
|
? 'no count recorded'
|
|
473
554
|
: 'not counted on this run — it predates this column, and an archived report re-renders as what it was');
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
555
|
+
if (Number.isFinite(c?.total)) return `<td class="num">${esc(String(c.total))}</td>`;
|
|
556
|
+
// The register answered with its own floor: that is a figure, and it is printed as one.
|
|
557
|
+
if (disclosedFloor(c) !== null) return `<td class="num">${esc(moreThan(disclosedFloor(c)))}</td>`;
|
|
558
|
+
return `<td class="na" title="${escAttr(why)}">not available</td>`;
|
|
477
559
|
}).join('');
|
|
478
560
|
const scoped = e?.classScope !== 'all-classes' && (e?.classes?.length > 0);
|
|
479
561
|
const scope = scoped
|
|
@@ -491,7 +573,7 @@ function countsSection(marks, registerCounts, positions = '') {
|
|
|
491
573
|
// thing in three words each.
|
|
492
574
|
return `<div class="panel">
|
|
493
575
|
<div class="ko-counts ko-scroll"><table><thead><tr><th>Name</th>${head}</tr></thead><tbody>${rows}</tbody></table></div>
|
|
494
|
-
<p class="ko-basis">${esc(
|
|
576
|
+
<p class="ko-basis">${esc(territoriesLine(registerCounts))}${
|
|
495
577
|
positions ? `<br>${esc(positions)}` : ''}</p>
|
|
496
578
|
</div>`;
|
|
497
579
|
}
|
|
@@ -1157,16 +1239,16 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1157
1239
|
const cut = note.lastIndexOf(' \u2014 ');
|
|
1158
1240
|
const stage = cut > 0 ? note.slice(0, cut).trim() : note;
|
|
1159
1241
|
const context = String(productContext ?? '').trim();
|
|
1160
|
-
// REGISTERS COUNTED IS A ROW
|
|
1161
|
-
//
|
|
1162
|
-
// its source are what they read, and the list is one click away when they want it.
|
|
1242
|
+
// REGISTERS COUNTED IS A ROW (the 2026-09-16 report redesign). A line of country names running through
|
|
1243
|
+
// the middle of the panel is the thing a reader skips; the count and its source are what they read.
|
|
1163
1244
|
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1164
1245
|
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1165
|
-
|
|
1246
|
+
// THE BOARD'S OWN ROW: "186 registers, on Clarivate Compumark." — registers, because that is what was
|
|
1247
|
+
// counted, and no fold: the board lists no territories here, and the workbook's counts sheet carries
|
|
1248
|
+
// every register a figure was taken over.
|
|
1166
1249
|
const counted = regions.length
|
|
1167
|
-
? `${regions.length} ${regions.length === 1 ? '
|
|
1250
|
+
? `${regions.length} ${regions.length === 1 ? 'register' : 'registers'}${provider ? `, on ${provider}` : ''}.`
|
|
1168
1251
|
: (registerCounts ? `Counted worldwide${provider ? `, on ${provider}` : ''}` : '');
|
|
1169
|
-
const territories = named.length ? listWords(named) : '';
|
|
1170
1252
|
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1171
1253
|
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1172
1254
|
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
|
@@ -1179,7 +1261,7 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1179
1261
|
${row('Classes', classLine)}
|
|
1180
1262
|
${row('Where searched', where)}
|
|
1181
1263
|
${row('Context', context)}
|
|
1182
|
-
${row('Registers counted', counted
|
|
1264
|
+
${row('Registers counted', counted)}
|
|
1183
1265
|
${row('Searched on', searched)}
|
|
1184
1266
|
${/* item 18 — a note for the reviewing lawyer does not reach the delivered page */''}
|
|
1185
1267
|
</div>`;
|
|
@@ -1336,7 +1418,8 @@ function coverageClause(mark, registerCounts, probeRan) {
|
|
|
1336
1418
|
? 'Register: hit-counts are part of this search and none could be taken for this name — the reason is in the audit workbook.'
|
|
1337
1419
|
: 'Register: not included in this product tier — no register was counted for this name.';
|
|
1338
1420
|
}
|
|
1339
|
-
|
|
1421
|
+
// A floor the register disclosed is a figure it gave, so a name counted only that way was counted.
|
|
1422
|
+
const anyFigure = COUNT_PREDICATES.some((p) => Number.isFinite(entry.counts[p.key]?.total) || disclosedFloor(entry.counts[p.key]) !== null);
|
|
1340
1423
|
if (!anyFigure) return 'Register: no count could be taken for this name — the reason is in the audit workbook.';
|
|
1341
1424
|
// COVERAGE, NOT THE FIGURES. asks the renderer to own the register-tier caveat, and the numbers
|
|
1342
1425
|
// are already on this page twice — in the glance line at the top and in the counts table above this
|
|
@@ -1758,11 +1841,11 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1758
1841
|
n + registerCardsOnPage(registerCardViews(m, framework, registerRecords).cards, m, framework).length, 0);
|
|
1759
1842
|
const hasRecords = Boolean(registerRecords && (registerRecords.marks?.length || registerRecords.unavailable));
|
|
1760
1843
|
const filings = hasRecords ? filingsSection(marks, registerRecords) : '';
|
|
1761
|
-
const onFieldSec = `<div class="sec" id="findings"><h2>Conflicts</h2></div>
|
|
1844
|
+
const onFieldSec = `<div class="sec" id="findings"><span class="num"></span><h2>Conflicts</h2></div>
|
|
1762
1845
|
${counts}
|
|
1763
1846
|
${analysis}`;
|
|
1764
1847
|
const filingsSec = filings
|
|
1765
|
-
? `<div class="sec" id="filings"><h2>Also considered</h2></div>
|
|
1848
|
+
? `<div class="sec" id="filings"><span class="num"></span><h2>Also considered</h2><span class="note">the filings behind the counts</span></div>
|
|
1766
1849
|
${filings}`
|
|
1767
1850
|
: '';
|
|
1768
1851
|
|
|
@@ -1774,7 +1857,7 @@ ${filings}`
|
|
|
1774
1857
|
const cssInline = REPORT_BASE + '\n' + CHROME_CSS + '\n' + THEME_BTN_CSS + '\n' + KO_CSS;
|
|
1775
1858
|
const chromeLinkTag = chromeHref ? `<link rel="stylesheet" href="${escAttr(chromeHref)}">` : '';
|
|
1776
1859
|
|
|
1777
|
-
|
|
1860
|
+
const doc = `<!DOCTYPE html>
|
|
1778
1861
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1779
1862
|
<title>${productName ? `${esc(productName)} — ` : ''}${esc(title)} · ${esc(BRAND.name)}</title>
|
|
1780
1863
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin>
|
|
@@ -1825,6 +1908,7 @@ ${filings}`
|
|
|
1825
1908
|
`)}
|
|
1826
1909
|
</div>
|
|
1827
1910
|
</div>
|
|
1911
|
+
<!--SECTION-STRIP-->
|
|
1828
1912
|
</div>
|
|
1829
1913
|
<div class="fab-stack">${themeButton()}</div>
|
|
1830
1914
|
<script>
|
|
@@ -1849,7 +1933,7 @@ window.addEventListener('beforeprint',o);})();
|
|
|
1849
1933
|
the clearance template carries, for the same markup; they move together when the top bar is shared. */
|
|
1850
1934
|
${EXPORT_MENU_JS}</script>
|
|
1851
1935
|
<div class="wrap">
|
|
1852
|
-
<header class="hero">
|
|
1936
|
+
<header class="hero" id="summary">
|
|
1853
1937
|
${demoBannerHtml(demoData === true)}
|
|
1854
1938
|
${confLineHtml(delivery, productName)}
|
|
1855
1939
|
<h1 class="mark">${esc(title)}</h1>
|
|
@@ -1866,7 +1950,7 @@ ${EXPORT_MENU_JS}</script>
|
|
|
1866
1950
|
if (!outs.length) return '';
|
|
1867
1951
|
const label = outs.length > 1;
|
|
1868
1952
|
const body = outs.map((o) => (label && o.name ? `<h3>${esc(o.name)}</h3>` : '') + mdParagraphs(o.text)).join('');
|
|
1869
|
-
return `<div class="sec" id="next"><h2>What happens next</h2></div>
|
|
1953
|
+
return `<div class="sec" id="next"><span class="num"></span><h2>What happens next</h2></div>
|
|
1870
1954
|
<div class="panel actions"><div class="actgrp act-you">${body}</div></div>`;
|
|
1871
1955
|
})()}
|
|
1872
1956
|
|
|
@@ -1903,6 +1987,7 @@ ${EXPORT_MENU_JS}</script>
|
|
|
1903
1987
|
</footer>
|
|
1904
1988
|
</div>
|
|
1905
1989
|
</body></html>`;
|
|
1990
|
+
return doc.replace('<!--SECTION-STRIP-->', sectionStrip(doc, STRIP));
|
|
1906
1991
|
}
|
|
1907
1992
|
|
|
1908
1993
|
/**
|
|
@@ -2053,6 +2138,12 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
2053
2138
|
classScope: counted.classScope ?? null,
|
|
2054
2139
|
classes: counted.classes ?? [],
|
|
2055
2140
|
...Object.fromEntries(COUNT_PREDICATES.map((p) => [p.key, Number.isFinite(counted.counts[p.key]?.total) ? counted.counts[p.key].total : null])),
|
|
2141
|
+
// The figure stays null for a floor — a floor is never a count to be summed — and the floor
|
|
2142
|
+
// rides beside it, so a reader of this file has the register's own figure and not only a null.
|
|
2143
|
+
...(COUNT_PREDICATES.some((p) => disclosedFloor(counted.counts[p.key]) !== null)
|
|
2144
|
+
? { floors: Object.fromEntries(COUNT_PREDICATES.filter((p) => disclosedFloor(counted.counts[p.key]) !== null)
|
|
2145
|
+
.map((p) => [p.key, disclosedFloor(counted.counts[p.key])])) }
|
|
2146
|
+
: {}),
|
|
2056
2147
|
// The close column is an aggregate, so the forms under it ride with it — a consumer that
|
|
2057
2148
|
// got the number and not the forms could restate the figure but never explain it, and this
|
|
2058
2149
|
// file is what the assistant drafts client mail from.
|
|
@@ -32,7 +32,7 @@ import { parseFindingsJson, bindRecommendation, sentenceCaseLead, CLIENT_TIER_BY
|
|
|
32
32
|
import { REC, inPriorityWindow, ownerDisplayName } from '../registry-fidelity.mjs';
|
|
33
33
|
import { registrationSystem } from '../jurisdiction-systems.mjs';
|
|
34
34
|
import { READ_LEAD_RE } from '../report-card-record.mjs'; // D3 — the dedupe gate and the card's acceptance are ONE predicate
|
|
35
|
-
import { REPORT_ROOT, REPORT_ROOT_DARK_EXPLICIT, THEME_INIT_EXPLICIT, FAVICON_LINK, logoLockup, BRAND, confPosture } from '../../shared/brand.mjs';
|
|
35
|
+
import { REPORT_ROOT, REPORT_ROOT_DARK_EXPLICIT, THEME_INIT_EXPLICIT, FAVICON_LINK, logoLockup, BRAND, confPosture, sectionStrip } from '../../shared/brand.mjs';
|
|
36
36
|
import { NAV_CSS } from '../../shared/site-nav.mjs';
|
|
37
37
|
import { isEntrypoint } from "../../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
38
38
|
|
|
@@ -1041,18 +1041,55 @@ function whereItStandsSection(findings, opts) {
|
|
|
1041
1041
|
const sd = opts && opts.searchDepth;
|
|
1042
1042
|
if (!sd || !sd.counts || isFullCountry(opts)) return '';
|
|
1043
1043
|
const byC = sd.counts.recordsByCountry || {};
|
|
1044
|
-
const codes = Object.keys(byC).filter((c) => c !== 'WO');
|
|
1045
|
-
if (!codes.length) return '';
|
|
1046
1044
|
const bandBy = new Map();
|
|
1047
1045
|
for (const f of findings) {
|
|
1048
1046
|
const c = regionCode(f);
|
|
1049
1047
|
if (!c || c === COMMON_LAW || !f.band) continue;
|
|
1050
1048
|
if (!bandBy.has(c)) bandBy.set(c, f.band);
|
|
1051
1049
|
}
|
|
1050
|
+
// WHICH COUNTRIES THIS SECTION IS ABOUT COMES FROM THE SEARCH PLAN, NOT FROM THE RECORD ARCHIVE.
|
|
1051
|
+
//
|
|
1052
|
+
// The archive is the authority on what came back and was kept; the plan is the authority on what was
|
|
1053
|
+
// asked of the register. Read off the archive, a provider that keeps no records produced no countries,
|
|
1054
|
+
// no rows, no chips and no section at all — so a register nobody could archive read exactly like a
|
|
1055
|
+
// register nobody searched, on the page a client acts on. Every provider that keeps partial records
|
|
1056
|
+
// under-reported here for the same reason.
|
|
1057
|
+
//
|
|
1058
|
+
// `planTerritories` is three-valued and the third value is why this is not a one-line swap. `null`
|
|
1059
|
+
// means there is no plan to read, which is every archived and legacy run: those keep the archive as
|
|
1060
|
+
// their only authority and re-render exactly as they always have. A plan that named nothing is a
|
|
1061
|
+
// different answer from no plan at all and is allowed to leave this section empty.
|
|
1062
|
+
//
|
|
1063
|
+
// THE FINDINGS' OWN COUNTRIES ARE UNIONED IN, never filtered out. A finding sitting in a territory the
|
|
1064
|
+
// plan does not list — an international registration designating one, most often — had its row from
|
|
1065
|
+
// the archive before this and must keep it: the section's first duty is that a country carrying a
|
|
1066
|
+
// conflict is on the page.
|
|
1067
|
+
//
|
|
1068
|
+
// The plan's unreached territories are NOT drawn. This section has two slots in the approved board, a
|
|
1069
|
+
// row carrying a band and a "Nothing found" chip, and a territory the provider does not cover is
|
|
1070
|
+
// neither: filing it under "Nothing found" would state a clean result for a register nobody read,
|
|
1071
|
+
// which is the one fusion this file's court-decisions section exists to keep apart.
|
|
1072
|
+
const plan = opts && opts.planTerritories;
|
|
1073
|
+
const planned = plan && Array.isArray(plan.searched) ? plan.searched.map(String) : null;
|
|
1074
|
+
const codes = (planned
|
|
1075
|
+
? [...new Set([...planned, ...bandBy.keys()])]
|
|
1076
|
+
: Object.keys(byC)).filter((c) => c !== 'WO');
|
|
1077
|
+
if (!codes.length) return '';
|
|
1052
1078
|
const alias = { EM: 'EU', GB: 'UK' };
|
|
1053
|
-
|
|
1079
|
+
// ONE ROW PER COUNTRY, DEDUPED AFTER THE ALIAS AND NOT BEFORE. The archive wrote each country once, so
|
|
1080
|
+
// this loop never had to ask; a plan carries both spellings — the register's EM and GB alongside the EU
|
|
1081
|
+
// and UK a reader knows — and aliasing them afterwards produced two EU rows and two UK rows on the
|
|
1082
|
+
// first worldwide run measured through it. The band is looked up across every raw code that folded
|
|
1083
|
+
// into the key, so a conflict recorded under EM still reaches the EU row.
|
|
1084
|
+
const rawByKey = new Map();
|
|
1054
1085
|
for (const c of codes) {
|
|
1055
1086
|
const key = alias[c] || c;
|
|
1087
|
+
if (!rawByKey.has(key)) rawByKey.set(key, []);
|
|
1088
|
+
rawByKey.get(key).push(c);
|
|
1089
|
+
}
|
|
1090
|
+
const withF = [], clean = [];
|
|
1091
|
+
for (const [key, raws] of rawByKey) {
|
|
1092
|
+
const c = raws.find((r) => bandBy.has(r)) ?? raws[0];
|
|
1056
1093
|
// THE NAME IS LOOKED UP ON THE ALIASED KEY, NOT THE RAW CODE. The register writes the EUIPO and
|
|
1057
1094
|
// ISO spellings — EM and GB — and `alias` maps those to the codes a reader knows, EU and UK. The
|
|
1058
1095
|
// name was resolved from the RAW code, which has no entry under either spelling, so it fell back
|
|
@@ -1067,7 +1104,7 @@ function whereItStandsSection(findings, opts) {
|
|
|
1067
1104
|
const cleanHtml = clean.length
|
|
1068
1105
|
? `<div class="wclean"><span class="wk">Nothing found</span>${clean.map((c) => `<span class="wchip" title="${escAttr(c.name)}">${esc(clean.length > 12 ? c.code : c.name)}</span>`).join('')}</div>`
|
|
1069
1106
|
: '';
|
|
1070
|
-
return `<div class="sec" id="countries"><h2>Where it stands</h2></div>
|
|
1107
|
+
return `<div class="sec" id="countries"><span class="num"></span><h2>Where it stands</h2></div>
|
|
1071
1108
|
<div class="panel where">${rows}${cleanHtml}</div>`;
|
|
1072
1109
|
}
|
|
1073
1110
|
|
|
@@ -1076,17 +1113,29 @@ function whereItStandsSection(findings, opts) {
|
|
|
1076
1113
|
function courtDecisionsSection(opts) {
|
|
1077
1114
|
const sd = opts && opts.searchDepth;
|
|
1078
1115
|
if (!sd || !sd.counts || !isFullCountry(opts)) return '';
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1116
|
+
const line = courtLineFor(opts, sd.counts.courtDecisions);
|
|
1117
|
+
if (!line) return '';
|
|
1118
|
+
return `<div class="sec" id="court"><span class="num"></span><h2>Court decisions</h2></div>
|
|
1119
|
+
<div class="panel courtp"><p>${esc(line)}</p></div>`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// The court section's sentence for a state, and the one the Court decisions row carries under its state
|
|
1123
|
+
// word when the research could not be completed — one sentence, said in both places in the same words.
|
|
1124
|
+
function courtLineFor(opts, state) {
|
|
1125
|
+
const sd = opts && opts.searchDepth;
|
|
1126
|
+
const byC = (sd && sd.counts && sd.counts.recordsByCountry) || {};
|
|
1081
1127
|
const first = Object.keys(byC).filter((c) => c !== 'WO')[0];
|
|
1082
1128
|
const where = first ? (regionName(first) || first) : '';
|
|
1129
|
+
// The country comes from the record listing, so a run whose provider archives nothing has no name to
|
|
1130
|
+
// put here — and every branch below embedded it mid-sentence. The page then read "Case-law research
|
|
1131
|
+
// could not be completed for ." Each clause is now attached to the name rather than assuming one:
|
|
1132
|
+
// same words when there is a country, one clause shorter when there is not.
|
|
1133
|
+
const forWhere = where ? ` for ${where}` : '';
|
|
1083
1134
|
let line = '';
|
|
1084
|
-
if (state === 'not-checked') line = `Case-law research could not be completed
|
|
1085
|
-
else if (state === 'none-found') line = `Court decisions: none found
|
|
1086
|
-
else if (state === 'found') line = `Court decisions were searched
|
|
1087
|
-
|
|
1088
|
-
return `<div class="sec" id="court"><h2>Court decisions</h2></div>
|
|
1089
|
-
<div class="panel courtp"><p>${esc(line)}</p></div>`;
|
|
1135
|
+
if (state === 'not-checked') line = `Case-law research could not be completed${forWhere}.`;
|
|
1136
|
+
else if (state === 'none-found') line = `Court decisions: none found${forWhere}.`;
|
|
1137
|
+
else if (state === 'found') line = `Court decisions were searched${forWhere} and are cited against the findings above.`;
|
|
1138
|
+
return line;
|
|
1090
1139
|
}
|
|
1091
1140
|
|
|
1092
1141
|
// Counts only, folded closed. A count of records read is engine activity rather than something a reader
|
|
@@ -1101,16 +1150,72 @@ function whatWasSearchedSection(opts, coverage = [], findings = [], recordsByUri
|
|
|
1101
1150
|
const sd = opts && opts.searchDepth;
|
|
1102
1151
|
const open = (Array.isArray(coverage) ? coverage : []).filter((c) => COV_STATE[c?.state]?.cls !== 'ok');
|
|
1103
1152
|
const c = (sd && sd.counts) || {}, rows = [];
|
|
1153
|
+
// THE REGISTER ROW, AS THE BOARDS DRAW IT: one line of totals — records read across the countries, plus
|
|
1154
|
+
// the international registrations apart from them — and the per-country counts as chips under it, code
|
|
1155
|
+
// first and the name on hover. One country is named in the line instead ("141 records read in Japan"),
|
|
1156
|
+
// and draws no chips: a single chip would repeat the line.
|
|
1104
1157
|
const byC = c.recordsByCountry || {};
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1158
|
+
const intl = Number(byC.WO) || 0;
|
|
1159
|
+
const countries = Object.entries(byC).filter(([k, n]) => k !== 'WO' && Number(n) > 0).sort((a, b) => b[1] - a[1]);
|
|
1160
|
+
const read = countries.reduce((t, [, n]) => t + Number(n), 0);
|
|
1161
|
+
if (countries.length || intl) {
|
|
1162
|
+
const where = countries.length === 1 ? ` in ${regionName(countries[0][0]) || countries[0][0]}`
|
|
1163
|
+
: countries.length ? ` across ${countries.length.toLocaleString('en-GB')} countries` : '';
|
|
1164
|
+
const plus = intl ? `${countries.length ? ', plus ' : ''}${intl.toLocaleString('en-GB')} international registration${intl === 1 ? '' : 's'}` : '';
|
|
1165
|
+
const line = `${countries.length ? `${read.toLocaleString('en-GB')} record${read === 1 ? '' : 's'} read${where}` : ''}${plus}`;
|
|
1166
|
+
const chips = countries.length > 1
|
|
1167
|
+
? `<div class="wchips">${countries.map(([k, n]) => `<span class="wchip" title="${escAttr(regionName(k) || k)}">${esc(k)} <b>${Number(n).toLocaleString('en-GB')}</b></span>`).join('')}</div>`
|
|
1168
|
+
: '';
|
|
1169
|
+
rows.push(['Register', { html: `${esc(line)}${chips}` }]);
|
|
1170
|
+
}
|
|
1107
1171
|
const sw = c.sweep || {};
|
|
1108
1172
|
if (sw.spellings) rows.push(['Spellings searched', String(sw.spellings)]);
|
|
1109
|
-
if (sw.checks) rows.push(['Marketplace and web', `${sw.checks.toLocaleString('en-GB')} checks
|
|
1173
|
+
if (sw.checks) rows.push(['Marketplace and web', `${sw.checks.toLocaleString('en-GB')} checks on ${sw.platforms} platforms`]);
|
|
1110
1174
|
if (sw.reputation) rows.push(['Reputation and meaning', `${sw.reputation.toLocaleString('en-GB')} checks`]);
|
|
1111
|
-
rows.push(['Local-script spellings', c.localScriptSearched ? '
|
|
1112
|
-
|
|
1113
|
-
|
|
1175
|
+
rows.push(['Local-script spellings', c.localScriptSearched ? 'Searched' : 'Not searched']);
|
|
1176
|
+
// HOW DEEP THE LOCAL-LANGUAGE INVESTIGATION WENT, which is a different question from the row above it.
|
|
1177
|
+
// That one answers whether the spellings were searched; this one answers whether the investigation ran
|
|
1178
|
+
// at the depth the matter configured. The engine can run it shallower than the account asked for, and
|
|
1179
|
+
// it used to say so in exactly one place — a sentence a model wrote into the Methodology paragraph the
|
|
1180
|
+
// redesign replaced with named rows — so a run that went shallow said so on no page at all.
|
|
1181
|
+
//
|
|
1182
|
+
// EVERY ONE OF THE FOUR WORDS IS ALREADY ON THE PAGE. "Included" and "Not part of this search" are the
|
|
1183
|
+
// approved board's own values for this row; "Partially covered" and "Not run this run" are the coverage
|
|
1184
|
+
// vocabulary this file already renders, taken from COV_STATE rather than retyped so they cannot drift
|
|
1185
|
+
// apart from it. Nothing here composes a sentence.
|
|
1186
|
+
//
|
|
1187
|
+
// A state this table has no word for draws NO ROW, rather than the nearest word. The four are the whole
|
|
1188
|
+
// set the record can produce, so the fallthrough is unreachable today and is there for the fifth state
|
|
1189
|
+
// somebody adds: a row is a claim about a client's search, and the nearest word to an unknown state is
|
|
1190
|
+
// a claim nobody checked.
|
|
1191
|
+
const LL_WORD = {
|
|
1192
|
+
ran: 'Included',
|
|
1193
|
+
'ran-shallow': COV_STATE['coverage-limited'].word,
|
|
1194
|
+
'not-run': COV_STATE['not-searched'].word,
|
|
1195
|
+
'not-in-scope': 'Not part of this search',
|
|
1196
|
+
};
|
|
1197
|
+
//
|
|
1198
|
+
// AND THE ROW NEEDS A LANE RECORD BEHIND IT, not just a state. The state folds to `not-in-scope` when
|
|
1199
|
+
// there is no lane record at all, which is right for a clearance that never asked for the
|
|
1200
|
+
// investigation and WRONG for a run that asked and whose record was never written. Measured on the
|
|
1201
|
+
// full country demo: its own coverage carries "Native-language investigation depth / ja — the
|
|
1202
|
+
// configured depth for this lane was full and this run delivered a depth this run cannot establish",
|
|
1203
|
+
// and the state beside it reads not-in-scope, so this row would have told that reader the
|
|
1204
|
+
// investigation was not part of their search. An empty `lanes` map is that absence, and an absence is
|
|
1205
|
+
// not a finding — so the row is drawn from a record and not from a default. A run whose record exists
|
|
1206
|
+
// draws it on every one of the four states.
|
|
1207
|
+
const llLanes = c.localLanguage?.lanes;
|
|
1208
|
+
const llRecorded = !!llLanes && typeof llLanes === 'object' && Object.keys(llLanes).length > 0;
|
|
1209
|
+
const ll = llRecorded ? LL_WORD[c.localLanguage?.state] : null;
|
|
1210
|
+
if (ll) rows.push(['Local-language investigation', ll]);
|
|
1211
|
+
// COURT DECISIONS, AS THE FULL COUNTRY BOARD DRAWS THE ROW: its state, and for a search that could not
|
|
1212
|
+
// be completed, the same sentence the court section under it prints. A search that does not include
|
|
1213
|
+
// court decisions draws no row, as the other boards draw none.
|
|
1214
|
+
const cdWord = { 'found': 'Found', 'none-found': 'None found', 'not-checked': 'Not searched' }[c.courtDecisions];
|
|
1215
|
+
if (cdWord && isFullCountry(opts)) {
|
|
1216
|
+
const why = c.courtDecisions === 'not-checked' ? courtLineFor(opts, 'not-checked') : '';
|
|
1217
|
+
rows.push(['Court decisions', { html: `<span class="wstate">${esc(cdWord)}</span>${why ? `<span class="wnote">${esc(why)}</span>` : ''}` }]);
|
|
1218
|
+
}
|
|
1114
1219
|
const openHtml = open.length ? `<div class="openrows"><div class="rk">Left open</div>${coverageGrid(open)}</div>` : '';
|
|
1115
1220
|
// THE LEGEND FOR WHAT IS ON THE CARDS, kept when the scope fold went. It is not the engine narrating
|
|
1116
1221
|
// its own searching — it is what a linked registration number opens, why the rest are cited by number,
|
|
@@ -1122,9 +1227,14 @@ function whatWasSearchedSection(opts, coverage = [], findings = [], recordsByUri
|
|
|
1122
1227
|
const prov = (hasRecordSet || hasCards)
|
|
1123
1228
|
? `<div class="provwrap"><div class="rk">Record provenance</div><p class="provnote">${hasRecordSet ? 'Registration numbers on the cards were read from the register records. ' : ''}${officeLinkNote()}${hasIndexEntry ? 'A registration shown as a register-index entry was seen in the register index; its full record was not pulled. ' : ''}\u201cInferred\u201d beside an owner\u2019s likelihood to object means we judged it from what the owner sells and holds; we had no enforcement history to read.</p></div>`
|
|
1124
1229
|
: '';
|
|
1230
|
+
// THE BOARDS END THE SECTION WITH THE WORKBOOK: every search and its result are in the audit workbook,
|
|
1231
|
+
// which is where a reader who wants the record goes.
|
|
1232
|
+
const more = opts?.auditFile
|
|
1233
|
+
? `<div class="cmore"><a class="wb" href="${escAttr(opts.auditFile)}">Every search and result, in the audit workbook</a></div>` : '';
|
|
1125
1234
|
if (!rows.length && !openHtml && !prov) return '';
|
|
1126
|
-
return `<
|
|
1127
|
-
|
|
1235
|
+
return `<div class="sec" id="searched"><span class="num"></span><h2>What was searched</h2></div>
|
|
1236
|
+
<details class="searched"><summary><span class="gname">Counts for this search</span></summary><div class="gbody">${
|
|
1237
|
+
rows.map(([k, v]) => `<div class="row"><span class="k">${esc(k)}</span><span class="v">${typeof v === 'object' ? v.html : esc(v)}</span></div>`).join('')}${openHtml}${prov}${more}</div></details>`;
|
|
1128
1238
|
}
|
|
1129
1239
|
|
|
1130
1240
|
function alsoConsideredSection(ruledOut, recordsByUri = new Map(), opts = {}) {
|
|
@@ -1152,7 +1262,7 @@ function alsoConsideredSection(ruledOut, recordsByUri = new Map(), opts = {}) {
|
|
|
1152
1262
|
const groups = clearedGroupsHtml(sd, opts.auditFile);
|
|
1153
1263
|
const web = webNamesHtml(sd);
|
|
1154
1264
|
if (!ruledCards && !groups && !web) return '';
|
|
1155
|
-
return `<div class="sec" id="also-considered"><h2>Also considered</h2></div>
|
|
1265
|
+
return `<div class="sec" id="also-considered"><span class="num"></span><h2>Also considered</h2></div>
|
|
1156
1266
|
<div class="panel alsocons">${ruledCards ? `<div class="ruled-cards">${ruledCards}</div>` : ''}${groups || web ? `<div class="cgroups">${groups}${web}</div>` : ''}</div>`;
|
|
1157
1267
|
}
|
|
1158
1268
|
|
|
@@ -2129,7 +2239,7 @@ ${EXPORT_MENU_JS}`;
|
|
|
2129
2239
|
function markAssessmentBlock(ma) {
|
|
2130
2240
|
if (ma == null) return '';
|
|
2131
2241
|
const structured = typeof ma.distinctiveness === 'object' || typeof ma.connotation === 'object';
|
|
2132
|
-
const SEC = `<div class="sec"><h2>The mark itself</h2></div>`;
|
|
2242
|
+
const SEC = `<div class="sec"><span class="num"></span><h2>The mark itself</h2></div>`;
|
|
2133
2243
|
if (!structured) {
|
|
2134
2244
|
const dist = String(ma?.distinctiveness ?? '').trim(), conn = String(ma?.connotation ?? '').trim();
|
|
2135
2245
|
if (!dist && !conn) return '';
|
|
@@ -2199,7 +2309,7 @@ function contextNotesList(notes = []) {
|
|
|
2199
2309
|
// the findings-section assembly below.
|
|
2200
2310
|
function contextNotesBlock(notes = []) {
|
|
2201
2311
|
if (!notes.length) return '';
|
|
2202
|
-
return `<div class="sec"><h2>Famous-mark neighbours noted</h2></div>
|
|
2312
|
+
return `<div class="sec"><span class="num"></span><h2>Famous-mark neighbours noted</h2></div>
|
|
2203
2313
|
${contextNotesList(notes)}`;
|
|
2204
2314
|
}
|
|
2205
2315
|
|
|
@@ -2332,6 +2442,13 @@ function depthStripHtml(note) {
|
|
|
2332
2442
|
}
|
|
2333
2443
|
|
|
2334
2444
|
// Signature: renderHtml(parsed, findings, coverage, opts). ONE render path — there is no client variant.
|
|
2445
|
+
// THE BOARD'S FIVE ENTRIES, in its order. The rule that filters them lives in shared/brand.mjs, because
|
|
2446
|
+
// the knockout renderer draws a strip from its own board and the filtering is the half worth having once.
|
|
2447
|
+
const STRIP = Object.freeze([
|
|
2448
|
+
['summary', 'Summary'], ['findings', 'Findings'], ['also-considered', 'Also considered'],
|
|
2449
|
+
['next', 'Next steps'], ['searched', 'What was searched'],
|
|
2450
|
+
]);
|
|
2451
|
+
|
|
2335
2452
|
export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
2336
2453
|
if (findings && !Array.isArray(findings)) { opts = findings; findings = []; coverage = []; } // tolerate legacy (parsed, opts)
|
|
2337
2454
|
AS_OF = opts.asOf ?? null; // C2
|
|
@@ -2479,13 +2596,13 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2479
2596
|
const hasCL = Boolean(clBody);
|
|
2480
2597
|
let findingsSections, covNum;
|
|
2481
2598
|
if (!DISPOSITION_MODE) {
|
|
2482
|
-
findingsSections = `${onField.length ? `<div class="sec"><h2>The conflict landscape</h2></div>
|
|
2599
|
+
findingsSections = `${onField.length ? `<div class="sec" id="findings"><span class="num"></span><h2>The conflict landscape</h2></div>
|
|
2483
2600
|
<div class="landwrap">${quadrant(sorted)}${keyPanel(sorted, recordsByUri)}</div>
|
|
2484
2601
|
|
|
2485
|
-
<div class="sec"><h2>Conflicts</h2></div>
|
|
2602
|
+
<div class="sec"><span class="num"></span><h2>Conflicts</h2></div>
|
|
2486
2603
|
${onField.map(f => findingCard(f, cardFor(f), recordsByUri)).join('\n ')}` : ''}
|
|
2487
2604
|
|
|
2488
|
-
${secReg.length ? `<div class="sec"><h2>Secondary & watch</h2></div>
|
|
2605
|
+
${secReg.length ? `<div class="sec"><span class="num"></span><h2>Secondary & watch</h2></div>
|
|
2489
2606
|
${secondaryRegions(secReg, cardFor, recordsByUri)}` : ''}${hasCL ? `
|
|
2490
2607
|
|
|
2491
2608
|
${clBody}` : ''}`;
|
|
@@ -2494,10 +2611,10 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2494
2611
|
let secNum = 0;
|
|
2495
2612
|
const num = () => String(++secNum).padStart(2, '0');
|
|
2496
2613
|
const landscape = onField.length
|
|
2497
|
-
? `<div class="sec"><h2>The conflict landscape</h2></div>
|
|
2614
|
+
? `<div class="sec" id="findings"><span class="num"></span><h2>The conflict landscape</h2></div>
|
|
2498
2615
|
<div class="landwrap">${quadrant(sorted)}${keyPanel(sorted, recordsByUri)}</div>
|
|
2499
2616
|
|
|
2500
|
-
<div class="sec"><h2>Conflicts</h2></div>
|
|
2617
|
+
<div class="sec"><span class="num"></span><h2>Conflicts</h2></div>
|
|
2501
2618
|
${onField.map(f => findingCard(f, cardFor(f), recordsByUri)).join('\n ')}`
|
|
2502
2619
|
: '';
|
|
2503
2620
|
// THE COMMON-LAW CARDS GO WITH THE CONFLICTS, NOT UNDER THE NEXT HEADING. Appended at the end they
|
|
@@ -2524,7 +2641,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2524
2641
|
const notes = contextNotes.length ? `\n <p class="fold-lead"><b>Famous-mark neighbours.</b> Diligence — no register record; not scored, does not affect the risk read.</p>
|
|
2525
2642
|
${contextNotesList(contextNotes)}` : '';
|
|
2526
2643
|
if (negatives.total || notes) {
|
|
2527
|
-
tail += `\n\n <div class="sec"><h2>Notable but manageable</h2></div>
|
|
2644
|
+
tail += `\n\n <div class="sec"><span class="num"></span><h2>Notable but manageable</h2></div>
|
|
2528
2645
|
${negatives.total ? reasonedNegatives(negatives.groups, cardFor, recordsByUri) : ''}${notes}`;
|
|
2529
2646
|
}
|
|
2530
2647
|
} else if (band2r.length || band3r.length || contextNotes.length) {
|
|
@@ -2534,7 +2651,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2534
2651
|
${secondaryRegions(band3r, cardFor, recordsByUri)}`);
|
|
2535
2652
|
if (contextNotes.length) parts.push(`<p class="fold-lead"><b>Famous-mark neighbours.</b> Diligence — no register record; not scored, does not affect the risk read.</p>
|
|
2536
2653
|
${contextNotesList(contextNotes)}`);
|
|
2537
|
-
tail += `\n\n <div class="sec"><h2>Notable but manageable</h2></div>
|
|
2654
|
+
tail += `\n\n <div class="sec"><span class="num"></span><h2>Notable but manageable</h2></div>
|
|
2538
2655
|
${parts.join('\n ')}`;
|
|
2539
2656
|
}
|
|
2540
2657
|
// The 2026-09-16 report redesign — the section heading and its routing notice go; the CARDS stay, or a
|
|
@@ -2564,7 +2681,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2564
2681
|
const linkChrome = !!opts.chromeHref;
|
|
2565
2682
|
const cssInline = REPORT_BASE + '\n' + (linkChrome ? '' : NAV_SHEET) + '\n' + REP_OVERRIDE;
|
|
2566
2683
|
const chromeLinkTag = linkChrome ? `<link rel="stylesheet" href="${escAttr(opts.chromeHref)}">` : '';
|
|
2567
|
-
|
|
2684
|
+
const doc = `<!DOCTYPE html>
|
|
2568
2685
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2569
2686
|
<title>${productName ? `${esc(productName)} — ` : ''}${esc(fm.title || '')} · ${esc(BRAND.name)}</title>
|
|
2570
2687
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin>
|
|
@@ -2604,12 +2721,13 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2604
2721
|
`)}
|
|
2605
2722
|
</div>
|
|
2606
2723
|
</div>
|
|
2724
|
+
<!--SECTION-STRIP-->
|
|
2607
2725
|
</div>
|
|
2608
2726
|
|
|
2609
2727
|
<div class="wrap">
|
|
2610
2728
|
<!-- doc-52 §1 THE VERDICT — the answer leads: mark · band · one plain "subject to" line · the lawyer's
|
|
2611
2729
|
questions answered · the time-critical alert. Process + coverage detail are below and collapsible. -->
|
|
2612
|
-
<header class="hero">
|
|
2730
|
+
<header class="hero" id="summary">
|
|
2613
2731
|
${demoBannerHtml(opts.demoData === true)}
|
|
2614
2732
|
${confLineHtml(opts.delivery, productName)}
|
|
2615
2733
|
<h1 class="mark">${esc(fm.title || '')}</h1>
|
|
@@ -2635,7 +2753,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2635
2753
|
${alsoConsideredSection(ruledOut, recordsByUri, opts)}
|
|
2636
2754
|
|
|
2637
2755
|
<!-- doc-52 §3 WHAT ONLY YOU CAN CLOSE — forward decisions, plain English, after the findings. -->
|
|
2638
|
-
${buckets.you ? `<div class="sec" id="
|
|
2756
|
+
${buckets.you ? `<div class="sec" id="next"><span class="num"></span><h2>What happens next</h2></div>
|
|
2639
2757
|
<div class="panel actions"><div class="actgrp act-you">${renderProse(buckets.you.body)
|
|
2640
2758
|
.replace(/\[Time-critical\]\s*/gi, '<span class="src cl" style="margin-right:6px">Time-critical</span> ')
|
|
2641
2759
|
.replace(/\[Open question\]\s*/gi, '<span class="src" style="margin-right:6px">Open question</span> ')
|
|
@@ -2707,6 +2825,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2707
2825
|
</div>
|
|
2708
2826
|
<script>${PAGE_JS}</script>
|
|
2709
2827
|
</body></html>`;
|
|
2828
|
+
return doc.replace('<!--SECTION-STRIP-->', sectionStrip(doc, STRIP));
|
|
2710
2829
|
}
|
|
2711
2830
|
|
|
2712
2831
|
// CLI: node render.mjs <report.md> [findings.json] [outDir]
|