clearotron 0.3.2-beta.7 → 0.3.2-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -25,13 +25,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
// its own split, which is how the two rules diverged. It calls stripTelemetry now, so the renderer holds
|
|
26
26
|
// no copy of the RULE either, only a call to it.
|
|
27
27
|
import { parseReport, stripInternal, stripTelemetry } from './parse.mjs';
|
|
28
|
-
import { clientConditions } from '../terminal-clamp.mjs';
|
|
28
|
+
import { clientConditions } from '../terminal-clamp.mjs';
|
|
29
|
+
import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mjs'; // the export menu's shell and behaviour, shared with the knockout template // the reader's clause per condition, shared with the cover note
|
|
29
30
|
import { COMMON_LAW, normRegion, regionName, REGION_NAMES } from './regions.mjs';
|
|
30
31
|
import { parseFindingsJson, bindRecommendation, sentenceCaseLead, CLIENT_TIER_BY_COMPOSITE, bandOf, compareBlockingPower, inDispositionMode, reasonedNegativeGroups } from '../findings-model.mjs';
|
|
31
32
|
import { REC, inPriorityWindow, ownerDisplayName } from '../registry-fidelity.mjs';
|
|
32
33
|
import { registrationSystem } from '../jurisdiction-systems.mjs';
|
|
33
34
|
import { READ_LEAD_RE } from '../report-card-record.mjs'; // D3 — the dedupe gate and the card's acceptance are ONE predicate
|
|
34
|
-
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';
|
|
35
36
|
import { NAV_CSS } from '../../shared/site-nav.mjs';
|
|
36
37
|
import { isEntrypoint } from "../../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
37
38
|
|
|
@@ -59,6 +60,16 @@ let SEARCHED_JUR = null;
|
|
|
59
60
|
let SCOPE_WORLDWIDE = null;
|
|
60
61
|
// T7 — the two deterministic evidence joins (set per render from opts):
|
|
61
62
|
let CASE_LAW_BY_ORD = new Map(); // E5: ordinal → grounded case-law profile
|
|
63
|
+
// Whether the case-law pass found anything: 'found' | 'none-found' | 'not-checked' | null.
|
|
64
|
+
// The CARD strand is suppressed on the two states that mean there is no precedent to cite, because the
|
|
65
|
+
// profile body is then the engine's account of WHY — adapter names, session error codes, which sources
|
|
66
|
+
// were out of scope — a true sentence about the machinery and not the client's answer, and the Court
|
|
67
|
+
// decisions section states that outcome in the reader's own words already.
|
|
68
|
+
//
|
|
69
|
+
// NULL IS NOT ONE OF THEM. A report with no court state carries no Court decisions section either (it
|
|
70
|
+
// is full-country only), so suppressing there would take case-law off the page with nothing left
|
|
71
|
+
// saying so. Absence of a state is not a statement that nothing was found.
|
|
72
|
+
let COURT_DECISIONS = null;
|
|
62
73
|
let ENFORCER_SIGNALS = new Map(); // E6: registration uri (lowercase) → {aggression, oppositions, owner}
|
|
63
74
|
// WP-receipts W2 — per-render provider record-link origin + label, resolved by publish from the run's
|
|
64
75
|
// OWN _driver/receipts.json provider (never the currently-configured provider — a re-published archive
|
|
@@ -143,11 +154,19 @@ function frameworkTickIndex(findings) {
|
|
|
143
154
|
// verdict with EVERY condition, "Why <band>" as the four answers with the basis each rests on, the
|
|
144
155
|
// highest exposure, and what was searched to get there. Nothing here is composed — the conditions are
|
|
145
156
|
// the sidecar's own client-voice clauses, the answers are the engine's, and the coverage line is counts.
|
|
146
|
-
function ratingExtras(fm, findings, coverage, fourAnswers, opts, bandWord) {
|
|
157
|
+
function ratingExtras(fm, findings, coverage, fourAnswers, opts, bandWord, conditionsInVerdict = false) {
|
|
147
158
|
const out = [];
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
// THE CONDITIONS HAVE ONE HOME PER PATH, AND IT IS NEVER NONE. Where the verdict row could take
|
|
160
|
+
// them — a sidecar whose composed statement carries the "conditional on:" lede — they render there,
|
|
161
|
+
// inside the verdict, which is where the mock puts them and where they read as the terms the verdict
|
|
162
|
+
// is conditional on rather than a list beside it. Where it could not, this row renders exactly as it
|
|
163
|
+
// did: the legacy gauge has no lede to hang them from, and dropping the row there would hide every
|
|
164
|
+
// condition on precisely the archived runs that cannot be re-rendered with better text.
|
|
165
|
+
if (!conditionsInVerdict) {
|
|
166
|
+
const conds = clientConditions(VERDICT_INFO || {});
|
|
167
|
+
if (conds.length) out.push(`<div class="gconds-wrap"><span class="gk">Conditions</span><ul class="gconds">${
|
|
168
|
+
conds.map((c) => `<li>${esc(c)}</li>`).join('')}</ul></div>`);
|
|
169
|
+
}
|
|
151
170
|
const fa = fourAnswers && typeof fourAnswers === 'object' ? fourAnswers : null;
|
|
152
171
|
if (fa) {
|
|
153
172
|
const rows = FOUR_ANSWER_LABELS.map(([key, label], i) => {
|
|
@@ -240,8 +259,27 @@ function frameworkGauge(fm, findings, coverage = [], fourAnswers = null, opts =
|
|
|
240
259
|
? (bindRecommendation(fm.recommendation, VERDICT_INFO.verdict, []) || VERDICT_INFO.tier || '')
|
|
241
260
|
: (fm.recommendation || fm.overall_label || '');
|
|
242
261
|
const recLabel = VERDICT_INFO?.statement ? 'Verdict' : 'Recommendation';
|
|
262
|
+
// THE VERDICT CARRIES EVERY CONDITION, NOT THE FIRST AND A COUNT. The composed statement ends
|
|
263
|
+
// "(and N more)" because it is also a ONE-LINE surface — the email lede, the registry row — where a
|
|
264
|
+
// list cannot go. On the page there is room for all of them, and a condition a client is told exists
|
|
265
|
+
// but is not told is one they cannot act on. The lede is taken from the statement's OWN prefix rather
|
|
266
|
+
// than re-composed, so the tier wording stays the engine's and only the truncated tail is replaced;
|
|
267
|
+
// the clauses are the sidecar's client-voice ones, the same list the separate row used to carry.
|
|
268
|
+
// The legacy `gauge()` below keeps the composed line as it stands: an archived run with no framework
|
|
269
|
+
// sidecar renders byte-identically, which is the contract stated there.
|
|
270
|
+
// THE LIST RENDERS ON BOTH PATHS. A sidecar with a composed statement gets the lede and its
|
|
271
|
+
// conditions; a LEGACY one — reasons, no statement, no client-voice clause — has no lede to match, and
|
|
272
|
+
// dropping the list there would hide every condition on exactly the archived runs that cannot be
|
|
273
|
+
// re-rendered with better text. Those still render under the bound recommendation, which is where
|
|
274
|
+
// they were before this row moved.
|
|
275
|
+
const verdictConds = clientConditions(VERDICT_INFO || {});
|
|
276
|
+
const ledeMatch = typeof rec === 'string' ? rec.match(/^(.*?conditional on:)/i) : null;
|
|
277
|
+
const conditionsInVerdict = Boolean(ledeMatch && verdictConds.length);
|
|
278
|
+
const recHtml = conditionsInVerdict
|
|
279
|
+
? `${esc(ledeMatch[1])}<ul class="gconds">${verdictConds.map((c) => `<li>${esc(c)}</li>`).join('')}</ul>`
|
|
280
|
+
: esc(rec);
|
|
243
281
|
const conc = [
|
|
244
|
-
rec && `<div class="grow"><span class="gk">${recLabel}</span><span class="gv gv-rec">${
|
|
282
|
+
rec && `<div class="grow"><span class="gk">${recLabel}</span><span class="gv gv-rec">${recHtml}</span></div>`,
|
|
245
283
|
].filter(Boolean).join('');
|
|
246
284
|
// — THE FRAMEWORK IS NAMED WHERE ITS WORDS ARE READ, not only in the footer. `.ticks` below spells
|
|
247
285
|
// a vocabulary ("Manageable", "Moderate") that is meaningless without the framework in force, and the
|
|
@@ -255,7 +293,7 @@ function frameworkGauge(fm, findings, coverage = [], fourAnswers = null, opts =
|
|
|
255
293
|
<div class="scale" style="background:${grad}">${marker}</div>
|
|
256
294
|
<div class="ticks">${ticks}</div>
|
|
257
295
|
<div class="gconc">${conc}</div>
|
|
258
|
-
${ratingExtras(fm, findings, coverage, fourAnswers, opts, label)}
|
|
296
|
+
${ratingExtras(fm, findings, coverage, fourAnswers, opts, label, conditionsInVerdict)}
|
|
259
297
|
</div>`;
|
|
260
298
|
}
|
|
261
299
|
|
|
@@ -514,7 +552,6 @@ const useEvidence = (m) => [USE_EVIDENCE_LABEL[m?._status], USE_SOURCE_LABEL[m?.
|
|
|
514
552
|
// runs carry the old value forever and a fourth spelling of it would have to be accepted everywhere.
|
|
515
553
|
const USE_CHECK_NO_RESULT = 'perplexity_research — no result';
|
|
516
554
|
const USE_CHECK_NO_RESULT_CITE = 'Nothing found in the marketplaces searched.';
|
|
517
|
-
const USE_CHECK_NO_RESULT_SHORT = 'marketplace search — no result found';
|
|
518
555
|
// — MATCHED ON NORMALISED PUNCTUATION, NOT ONE SPELLING. The constant itself does not
|
|
519
556
|
// move (archived runs carry it forever, the validators name it), but the SEAT emitted a hyphen where
|
|
520
557
|
// the doctrine writes an em dash, and exact equality let the raw tool name through to a delivered
|
|
@@ -963,8 +1000,13 @@ function clearedGroupsHtml(searchDepth, auditFile) {
|
|
|
963
1000
|
const items = reg.filter((c) => c.group === g);
|
|
964
1001
|
if (!items.length) return '';
|
|
965
1002
|
const shown = items.slice(0, NAMES_CAP);
|
|
1003
|
+
// "Cl." and a lower-case status, which is how the mock reads and how a lawyer writes it. The
|
|
1004
|
+
// register hands the status back in capitals — REGISTERED, CANCELLED — and shouting a neutral
|
|
1005
|
+
// fact at a reader is the register's habit, not ours. Only the case changes; the word is the
|
|
1006
|
+
// register's own and is not translated.
|
|
966
1007
|
const rows = shown.map((c) => `<div class="crow"><span class="cm-mark">${esc(c.mark || c.term || '')}</span><span class="cwho">${
|
|
967
|
-
esc([c.owner, regionName(c.country) || c.country, c.classes ? `
|
|
1008
|
+
esc([c.owner, regionName(c.country) || c.country, c.classes ? `Cl. ${c.classes}` : '',
|
|
1009
|
+
c.status ? String(c.status).toLowerCase() : ''].filter(Boolean).join(' \u00b7 '))}</span></div>`).join('');
|
|
968
1010
|
return `<details class="cgroup"><summary><span class="gname">${esc(CLEARED_GROUP_LABEL[g] || g)}</span><span class="gcount">${
|
|
969
1011
|
items.length.toLocaleString('en-GB')} ${items.length === 1 ? 'name' : 'names'}</span></summary><div class="gbody">${rows}${
|
|
970
1012
|
items.length > shown.length ? link(items.length) : ''}</div></details>`;
|
|
@@ -999,26 +1041,70 @@ function whereItStandsSection(findings, opts) {
|
|
|
999
1041
|
const sd = opts && opts.searchDepth;
|
|
1000
1042
|
if (!sd || !sd.counts || isFullCountry(opts)) return '';
|
|
1001
1043
|
const byC = sd.counts.recordsByCountry || {};
|
|
1002
|
-
const codes = Object.keys(byC).filter((c) => c !== 'WO');
|
|
1003
|
-
if (!codes.length) return '';
|
|
1004
1044
|
const bandBy = new Map();
|
|
1005
1045
|
for (const f of findings) {
|
|
1006
1046
|
const c = regionCode(f);
|
|
1007
1047
|
if (!c || c === COMMON_LAW || !f.band) continue;
|
|
1008
1048
|
if (!bandBy.has(c)) bandBy.set(c, f.band);
|
|
1009
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 '';
|
|
1010
1078
|
const alias = { EM: 'EU', GB: 'UK' };
|
|
1011
|
-
|
|
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();
|
|
1012
1085
|
for (const c of codes) {
|
|
1013
1086
|
const key = alias[c] || c;
|
|
1014
|
-
(
|
|
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];
|
|
1093
|
+
// THE NAME IS LOOKED UP ON THE ALIASED KEY, NOT THE RAW CODE. The register writes the EUIPO and
|
|
1094
|
+
// ISO spellings — EM and GB — and `alias` maps those to the codes a reader knows, EU and UK. The
|
|
1095
|
+
// name was resolved from the RAW code, which has no entry under either spelling, so it fell back
|
|
1096
|
+
// to the code itself and the row read "EU EM" and "UK GB": a country column printing a second
|
|
1097
|
+
// code, beside the four rows where the register happened to write the code we already knew.
|
|
1098
|
+
// The raw code is still tried, so anything the alias does not cover resolves exactly as before.
|
|
1099
|
+
const name = regionName(key) || regionName(c) || key;
|
|
1100
|
+
(bandBy.has(c) || bandBy.has(key) ? withF : clean).push({ code: key, name, band: bandBy.get(c) || bandBy.get(key) });
|
|
1015
1101
|
}
|
|
1016
1102
|
if (!withF.length && !clean.length) return '';
|
|
1017
1103
|
const rows = withF.map((c) => `<div class="wrow"><span class="rcode">${esc(c.code)}</span><span class="wname">${esc(c.name)}</span><span class="kc">${esc(c.band)}</span></div>`).join('');
|
|
1018
1104
|
const cleanHtml = clean.length
|
|
1019
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>`
|
|
1020
1106
|
: '';
|
|
1021
|
-
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>
|
|
1022
1108
|
<div class="panel where">${rows}${cleanHtml}</div>`;
|
|
1023
1109
|
}
|
|
1024
1110
|
|
|
@@ -1027,17 +1113,29 @@ function whereItStandsSection(findings, opts) {
|
|
|
1027
1113
|
function courtDecisionsSection(opts) {
|
|
1028
1114
|
const sd = opts && opts.searchDepth;
|
|
1029
1115
|
if (!sd || !sd.counts || !isFullCountry(opts)) return '';
|
|
1030
|
-
const
|
|
1031
|
-
|
|
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) || {};
|
|
1032
1127
|
const first = Object.keys(byC).filter((c) => c !== 'WO')[0];
|
|
1033
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}` : '';
|
|
1034
1134
|
let line = '';
|
|
1035
|
-
if (state === 'not-checked') line = `Case-law research could not be completed
|
|
1036
|
-
else if (state === 'none-found') line = `Court decisions: none found
|
|
1037
|
-
else if (state === 'found') line = `Court decisions were searched
|
|
1038
|
-
|
|
1039
|
-
return `<div class="sec" id="court"><h2>Court decisions</h2></div>
|
|
1040
|
-
<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;
|
|
1041
1139
|
}
|
|
1042
1140
|
|
|
1043
1141
|
// Counts only, folded closed. A count of records read is engine activity rather than something a reader
|
|
@@ -1052,16 +1150,72 @@ function whatWasSearchedSection(opts, coverage = [], findings = [], recordsByUri
|
|
|
1052
1150
|
const sd = opts && opts.searchDepth;
|
|
1053
1151
|
const open = (Array.isArray(coverage) ? coverage : []).filter((c) => COV_STATE[c?.state]?.cls !== 'ok');
|
|
1054
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.
|
|
1055
1157
|
const byC = c.recordsByCountry || {};
|
|
1056
|
-
const
|
|
1057
|
-
|
|
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
|
+
}
|
|
1058
1171
|
const sw = c.sweep || {};
|
|
1059
1172
|
if (sw.spellings) rows.push(['Spellings searched', String(sw.spellings)]);
|
|
1060
|
-
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`]);
|
|
1061
1174
|
if (sw.reputation) rows.push(['Reputation and meaning', `${sw.reputation.toLocaleString('en-GB')} checks`]);
|
|
1062
|
-
rows.push(['Local-script spellings', c.localScriptSearched ? '
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
+
}
|
|
1065
1219
|
const openHtml = open.length ? `<div class="openrows"><div class="rk">Left open</div>${coverageGrid(open)}</div>` : '';
|
|
1066
1220
|
// THE LEGEND FOR WHAT IS ON THE CARDS, kept when the scope fold went. It is not the engine narrating
|
|
1067
1221
|
// its own searching — it is what a linked registration number opens, why the rest are cited by number,
|
|
@@ -1073,16 +1227,30 @@ function whatWasSearchedSection(opts, coverage = [], findings = [], recordsByUri
|
|
|
1073
1227
|
const prov = (hasRecordSet || hasCards)
|
|
1074
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>`
|
|
1075
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>` : '';
|
|
1076
1234
|
if (!rows.length && !openHtml && !prov) return '';
|
|
1077
|
-
return `<
|
|
1078
|
-
|
|
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>`;
|
|
1079
1238
|
}
|
|
1080
1239
|
|
|
1081
1240
|
function alsoConsideredSection(ruledOut, recordsByUri = new Map(), opts = {}) {
|
|
1082
1241
|
const sd = opts.searchDepth || null;
|
|
1083
1242
|
const ruledCards = ruledOut.map((f) => {
|
|
1084
1243
|
const who = recordOwner(f, recordsByUri) || f.owner?.name || '';
|
|
1085
|
-
|
|
1244
|
+
// THE ENGINE'S OWN SENTENCE LEADS THE CARD. The face printed a fixed line — "a different name in
|
|
1245
|
+
// a related field" — on every ruled-out card, whatever the run had actually concluded. `net` is the
|
|
1246
|
+
// finding's own one-line reason, written for a reader, and it was rendered NOWHERE: not on the
|
|
1247
|
+
// face, not in the fold, not anywhere on the page. So a client read the same generic sentence
|
|
1248
|
+
// about every name we set aside, while the specific reason we set THAT one aside was discarded at
|
|
1249
|
+
// render time. The fold keeps the legal and practical positions, which are the longer argument.
|
|
1250
|
+
// The fixed line stays as the last resort, for a finding that carries neither.
|
|
1251
|
+
const why = f.ruled_out_reason ? esc(f.ruled_out_reason)
|
|
1252
|
+
: f.net ? esc(String(f.net))
|
|
1253
|
+
: 'a different name in a related field — not a conflict with your mark';
|
|
1086
1254
|
const legal = f.legal_position ? esc(String(f.legal_position)) : '';
|
|
1087
1255
|
const practical = f.practical_position ? esc(String(f.practical_position)) : '';
|
|
1088
1256
|
const fold = (legal || practical)
|
|
@@ -1094,7 +1262,7 @@ function alsoConsideredSection(ruledOut, recordsByUri = new Map(), opts = {}) {
|
|
|
1094
1262
|
const groups = clearedGroupsHtml(sd, opts.auditFile);
|
|
1095
1263
|
const web = webNamesHtml(sd);
|
|
1096
1264
|
if (!ruledCards && !groups && !web) return '';
|
|
1097
|
-
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>
|
|
1098
1266
|
<div class="panel alsocons">${ruledCards ? `<div class="ruled-cards">${ruledCards}</div>` : ''}${groups || web ? `<div class="cgroups">${groups}${web}</div>` : ''}</div>`;
|
|
1099
1267
|
}
|
|
1100
1268
|
|
|
@@ -1284,36 +1452,19 @@ function keyPanel(findings, recordsByUri = new Map()) {
|
|
|
1284
1452
|
// complete in one place. The rights-holder landscape panel keeps its "Common-law" group — that
|
|
1285
1453
|
// panel is the index of everything, this section is the reading surface.
|
|
1286
1454
|
function commonLawSection(clSecondary, clOnField, cardFor, recordsByUri = new Map(), allFindings = []) {
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
// the
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
|
|
1295
|
-
.filter((f) => f && f.disposition !== 'withdrawn' && f.use_check?.source && regionCode(f) !== COMMON_LAW)
|
|
1296
|
-
.map((f) => {
|
|
1297
|
-
// D4 — the evidence pair is LABELLED here too, and it reads out of the one USE_SOURCE_LABEL.
|
|
1298
|
-
const st = useEvidence(f.meters?.use);
|
|
1299
|
-
// D7 — the sentinel is mapped to client words BEFORE the URL parse is attempted. It is not a
|
|
1300
|
-
// URL, so `new URL` threw and the catch printed `host.slice(0, 40)` — and the sentinel is 31
|
|
1301
|
-
// characters, so the page printed the raw tool name, whole.
|
|
1302
|
-
const raw = String(f.use_check.source);
|
|
1303
|
-
let where;
|
|
1304
|
-
if (isUseCheckNoResult(raw)) where = USE_CHECK_NO_RESULT_SHORT;
|
|
1305
|
-
else { try { where = new URL((raw.match(/https?:\/\/[^\s,|]+/) || [raw])[0]).host; } catch { where = raw.slice(0, 40); } }
|
|
1306
|
-
return `<li style="margin:3px 0"><a href="#c${f.ordinal}">#${f.ordinal} ${esc(f.mark)}</a> — use ${esc(humanize(f.meters?.use?.token ?? 'unknown'))} — ${esc(where)}${st ? ` <i class="evstat">(evidence: ${esc(st)})</i>` : ''}</li>`;
|
|
1307
|
-
}).join('');
|
|
1308
|
-
const contribBlock = contrib
|
|
1309
|
-
? `<p style="margin:4px 0 2px;font-size:13px"><b>What the marketplace layer added to register findings</b></p><ul style="margin:0 0 10px;padding-left:20px;font-size:13px">${contrib}</ul>`
|
|
1310
|
-
: '';
|
|
1311
|
-
if (!clSecondary.length && !clOnField.length && !contribBlock) return '';
|
|
1455
|
+
// THE LAYER'S CONTRIBUTION IS ON THE CARD THAT CARRIES IT, NOT ALSO IN A LIST ABOVE THEM. A
|
|
1456
|
+
// block headed "what the marketplace layer added to register findings" restated, per finding, the
|
|
1457
|
+
// use token, the host and the evidence pair that the finding's OWN card already states in its use
|
|
1458
|
+
// line — measured on the delivered reports: one such line per finding with use evidence, on the
|
|
1459
|
+
// card, in every case the block listed. It attributed a layer to itself in the engine's own terms
|
|
1460
|
+
// and made a reader read the same fact twice, the second time out of the context that explains it.
|
|
1461
|
+
// Nothing is lost with it: the cards it linked to sit directly below.
|
|
1462
|
+
if (!clSecondary.length && !clOnField.length) return '';
|
|
1312
1463
|
const links = clOnField.length
|
|
1313
1464
|
? `<p class="clx" style="margin:4px 0 10px;font-size:13.5px">On-field common-law conflicts (full cards above): ${clOnField.map(f => `<a href="#c${f.ordinal}">#${f.ordinal} ${esc(f.mark)}</a>`).join(' · ')}</p>`
|
|
1314
1465
|
: '';
|
|
1315
1466
|
const cards = clSecondary.map(f => compactCard(f, cardFor(f), recordsByUri)).join('\n ');
|
|
1316
|
-
return
|
|
1467
|
+
return links + cards;
|
|
1317
1468
|
}
|
|
1318
1469
|
|
|
1319
1470
|
// Secondary findings → collapsible region groups (§2.4). Same region order as the key panel; each region a
|
|
@@ -1632,11 +1783,20 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1632
1783
|
// preparation is portal-report.mjs's job, never a second render fork here.
|
|
1633
1784
|
const clHead = clProfile ? `<b>Case-law${clProfile.jurisdiction ? ` (${esc(clProfile.jurisdiction)})` : ''}.</b>` : '';
|
|
1634
1785
|
const clBody = !clProfile ? '' : renderProse(clProfile.body); // one report: the full body (renderProse handles ::p:: internal lines)
|
|
1635
|
-
|
|
1786
|
+
// Only a pass that FOUND precedent puts a strand on the card. Where none was found, or the research
|
|
1787
|
+
// could not be completed, the body is the engine's account of the attempt and the Court decisions
|
|
1788
|
+
// section says the outcome plainly on its own; repeating it here said it twice, the second time in
|
|
1789
|
+
// the machinery's voice on the card a client reads most closely.
|
|
1790
|
+
const caseLawStrand = clProfile && COURT_DECISIONS !== 'not-checked' && COURT_DECISIONS !== 'none-found'
|
|
1636
1791
|
? `<div class="clstrand" style="margin:10px 0 0;padding-top:8px;border-top:1px dashed var(--line,#ddd)">${clHead}${clBody}</div>`
|
|
1637
1792
|
: '';
|
|
1638
1793
|
const link = f.source?.resolved_link;
|
|
1639
|
-
|
|
1794
|
+
// NO AUDIT REFERENCE ON A CLIENT'S CARD. "audit ref F1" is the engine's handle for the finding —
|
|
1795
|
+
// it indexes the workbook, it means nothing to the reader holding the report, and the mock carries no
|
|
1796
|
+
// such line. The SOURCE LINK stays: it is the only address a reader has for the record on this card
|
|
1797
|
+
// until the workbook row lands beside it, and dropping both would take a fact away rather than a
|
|
1798
|
+
// label. With no link there is nothing left to say, so the row does not render at all.
|
|
1799
|
+
const prov = link ? `<div class="prov"><a href="${esc(link)}" target="_blank" rel="noopener noreferrer">${esc(link.replace(/^https?:\/\//, '').slice(0, 48))}</a></div>` : '';
|
|
1640
1800
|
// WP-receipts W4 — the code-owned senior-right line (Owner decision 2026-07-05: VERY SIMPLE CLEAR ENGLISH,
|
|
1641
1801
|
// stated qualification, verdict untouched). Verified senior → nothing extra (the W2 receipt line on
|
|
1642
1802
|
// the fetched leg is the proof). Unverified → the open item, plainly, where the finding lives.
|
|
@@ -2059,8 +2219,7 @@ window.addEventListener('hashchange',_cardHashGo);
|
|
|
2059
2219
|
window.addEventListener('load',_cardHashGo);
|
|
2060
2220
|
window.addEventListener('beforeprint',function(){document.querySelectorAll('details').forEach(function(d){d.dataset.o=d.open?'1':'';d.open=true;});});
|
|
2061
2221
|
window.addEventListener('afterprint',function(){document.querySelectorAll('details').forEach(function(d){d.open=d.dataset.o==='1';});_hidden.forEach(function(c){c.classList.remove('print-hidden');});_hidden=[];});
|
|
2062
|
-
|
|
2063
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){var pop=document.querySelector('.tb-exp-pop');if(pop&&!pop.hidden){pop.hidden=true;var b=document.querySelector('.tb-exp-toggle');if(b)b.setAttribute('aria-expanded','false');}}});`;
|
|
2222
|
+
${EXPORT_MENU_JS}`;
|
|
2064
2223
|
|
|
2065
2224
|
// Render a parsed report + findings.json to a full self-contained HTML string.
|
|
2066
2225
|
// A1 — famous-neighbour context notes: knowledge-cited references kept for diligence (digest.md's "never
|
|
@@ -2080,7 +2239,7 @@ document.addEventListener('keydown',function(e){if(e.key==='Escape'){var pop=doc
|
|
|
2080
2239
|
function markAssessmentBlock(ma) {
|
|
2081
2240
|
if (ma == null) return '';
|
|
2082
2241
|
const structured = typeof ma.distinctiveness === 'object' || typeof ma.connotation === 'object';
|
|
2083
|
-
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>`;
|
|
2084
2243
|
if (!structured) {
|
|
2085
2244
|
const dist = String(ma?.distinctiveness ?? '').trim(), conn = String(ma?.connotation ?? '').trim();
|
|
2086
2245
|
if (!dist && !conn) return '';
|
|
@@ -2150,7 +2309,7 @@ function contextNotesList(notes = []) {
|
|
|
2150
2309
|
// the findings-section assembly below.
|
|
2151
2310
|
function contextNotesBlock(notes = []) {
|
|
2152
2311
|
if (!notes.length) return '';
|
|
2153
|
-
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>
|
|
2154
2313
|
${contextNotesList(notes)}`;
|
|
2155
2314
|
}
|
|
2156
2315
|
|
|
@@ -2283,6 +2442,13 @@ function depthStripHtml(note) {
|
|
|
2283
2442
|
}
|
|
2284
2443
|
|
|
2285
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
|
+
|
|
2286
2452
|
export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
2287
2453
|
if (findings && !Array.isArray(findings)) { opts = findings; findings = []; coverage = []; } // tolerate legacy (parsed, opts)
|
|
2288
2454
|
AS_OF = opts.asOf ?? null; // C2
|
|
@@ -2292,6 +2458,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2292
2458
|
SEARCHED_JUR = Array.isArray(opts.searchedJurisdictions) && opts.searchedJurisdictions.length ? opts.searchedJurisdictions : null; // T6 (D4)
|
|
2293
2459
|
SCOPE_WORLDWIDE = opts.scopeBasis === 'worldwide' ? true : null; // the plan's scope_basis; null ⇒ fall back to the ledger-prose sniff
|
|
2294
2460
|
CASE_LAW_BY_ORD = opts.caseLawByOrdinal instanceof Map ? opts.caseLawByOrdinal : new Map(); // T7 (E5)
|
|
2461
|
+
COURT_DECISIONS = opts.searchDepth?.counts?.courtDecisions ?? null; // gates the card's case-law strand
|
|
2295
2462
|
ENFORCER_SIGNALS = new Map((Array.isArray(opts.enforcerSignals) ? opts.enforcerSignals : []).map((e) => [String(e.uri ?? '').toLowerCase(), e])); // T7 (E6)
|
|
2296
2463
|
RECORD_ORIGIN = opts.recordOrigin ?? null; // WP-receipts W2
|
|
2297
2464
|
// — `null` and `` mean DIFFERENT things and the render must not collapse them. `` is an
|
|
@@ -2429,13 +2596,13 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2429
2596
|
const hasCL = Boolean(clBody);
|
|
2430
2597
|
let findingsSections, covNum;
|
|
2431
2598
|
if (!DISPOSITION_MODE) {
|
|
2432
|
-
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>
|
|
2433
2600
|
<div class="landwrap">${quadrant(sorted)}${keyPanel(sorted, recordsByUri)}</div>
|
|
2434
2601
|
|
|
2435
|
-
<div class="sec"><h2>Conflicts</h2></div>
|
|
2602
|
+
<div class="sec"><span class="num"></span><h2>Conflicts</h2></div>
|
|
2436
2603
|
${onField.map(f => findingCard(f, cardFor(f), recordsByUri)).join('\n ')}` : ''}
|
|
2437
2604
|
|
|
2438
|
-
${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>
|
|
2439
2606
|
${secondaryRegions(secReg, cardFor, recordsByUri)}` : ''}${hasCL ? `
|
|
2440
2607
|
|
|
2441
2608
|
${clBody}` : ''}`;
|
|
@@ -2444,10 +2611,10 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2444
2611
|
let secNum = 0;
|
|
2445
2612
|
const num = () => String(++secNum).padStart(2, '0');
|
|
2446
2613
|
const landscape = onField.length
|
|
2447
|
-
? `<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>
|
|
2448
2615
|
<div class="landwrap">${quadrant(sorted)}${keyPanel(sorted, recordsByUri)}</div>
|
|
2449
2616
|
|
|
2450
|
-
<div class="sec"><h2>Conflicts</h2></div>
|
|
2617
|
+
<div class="sec"><span class="num"></span><h2>Conflicts</h2></div>
|
|
2451
2618
|
${onField.map(f => findingCard(f, cardFor(f), recordsByUri)).join('\n ')}`
|
|
2452
2619
|
: '';
|
|
2453
2620
|
// THE COMMON-LAW CARDS GO WITH THE CONFLICTS, NOT UNDER THE NEXT HEADING. Appended at the end they
|
|
@@ -2474,7 +2641,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2474
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>
|
|
2475
2642
|
${contextNotesList(contextNotes)}` : '';
|
|
2476
2643
|
if (negatives.total || notes) {
|
|
2477
|
-
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>
|
|
2478
2645
|
${negatives.total ? reasonedNegatives(negatives.groups, cardFor, recordsByUri) : ''}${notes}`;
|
|
2479
2646
|
}
|
|
2480
2647
|
} else if (band2r.length || band3r.length || contextNotes.length) {
|
|
@@ -2484,7 +2651,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2484
2651
|
${secondaryRegions(band3r, cardFor, recordsByUri)}`);
|
|
2485
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>
|
|
2486
2653
|
${contextNotesList(contextNotes)}`);
|
|
2487
|
-
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>
|
|
2488
2655
|
${parts.join('\n ')}`;
|
|
2489
2656
|
}
|
|
2490
2657
|
// The 2026-09-16 report redesign — the section heading and its routing notice go; the CARDS stay, or a
|
|
@@ -2514,7 +2681,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2514
2681
|
const linkChrome = !!opts.chromeHref;
|
|
2515
2682
|
const cssInline = REPORT_BASE + '\n' + (linkChrome ? '' : NAV_SHEET) + '\n' + REP_OVERRIDE;
|
|
2516
2683
|
const chromeLinkTag = linkChrome ? `<link rel="stylesheet" href="${escAttr(opts.chromeHref)}">` : '';
|
|
2517
|
-
|
|
2684
|
+
const doc = `<!DOCTYPE html>
|
|
2518
2685
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2519
2686
|
<title>${productName ? `${esc(productName)} — ` : ''}${esc(fm.title || '')} · ${esc(BRAND.name)}</title>
|
|
2520
2687
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin>
|
|
@@ -2530,11 +2697,20 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2530
2697
|
<span class="sp"></span>
|
|
2531
2698
|
<span class="tb-risk" style="background:var(${STOP_VAR[i]})">${riskLabel}</span>
|
|
2532
2699
|
<span class="mono tb-matter" style="font-size:11px;color:var(--faint)">${esc(fm.matter || opts.runId || '')}${fm.title ? ' / ' + esc(fm.title) : ''}</span>
|
|
2533
|
-
${
|
|
2700
|
+
${/* A DATE, NOT A TIMESTAMP. This read "Issued on 2026-09-17 · 08:36 GMT+2" — the minute the file
|
|
2701
|
+
was written, and the zone the machine that wrote it happened to be in. Neither tells a reader
|
|
2702
|
+
anything, and on work that spans days it implies a precision the work does not have. Every mock
|
|
2703
|
+
issues the date alone. Taken by PATTERN rather than by cutting at the separator, so a value in
|
|
2704
|
+
some other shape falls through whole instead of being truncated at whatever character sits
|
|
2705
|
+
there — an archived run's stamp is not this publisher's to assume. */''}
|
|
2706
|
+
${(() => {
|
|
2707
|
+
const d = (String(opts.issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? opts.issued;
|
|
2708
|
+
return d ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued on ${esc(d)}</span>` : '';
|
|
2709
|
+
})()}
|
|
2534
2710
|
<button type="button" class="tbbtn tb-ask no-print">✦ <span class="tb-lbl">Ask AI</span></button>
|
|
2535
2711
|
<div class="tb-menu">
|
|
2536
|
-
|
|
2537
|
-
|
|
2712
|
+
${EXPORT_TOGGLE}
|
|
2713
|
+
${exportPopover(`
|
|
2538
2714
|
<div class="tb-pop-title">Export & audit</div>
|
|
2539
2715
|
<button class="util primary" onclick="exportPDF()">⬇ Export PDF (ticked findings)</button>
|
|
2540
2716
|
${excelBtn2}
|
|
@@ -2542,15 +2718,16 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2542
2718
|
<div class="tb-row"><button class="util" onclick="pickAll(true)">Select all</button><button class="util" onclick="pickAll(false)">Select none</button></div>
|
|
2543
2719
|
<div class="tb-row"><button class="util" onclick="openAll(true)">Expand all</button><button class="util" onclick="openAll(false)">Collapse all</button></div>
|
|
2544
2720
|
<p class="tb-hint">Tick a finding to keep it in the exported PDF; untick to drop it. Internal (review-only) notes are removed on export.</p>
|
|
2545
|
-
|
|
2721
|
+
`)}
|
|
2546
2722
|
</div>
|
|
2547
2723
|
</div>
|
|
2724
|
+
<!--SECTION-STRIP-->
|
|
2548
2725
|
</div>
|
|
2549
2726
|
|
|
2550
2727
|
<div class="wrap">
|
|
2551
2728
|
<!-- doc-52 §1 THE VERDICT — the answer leads: mark · band · one plain "subject to" line · the lawyer's
|
|
2552
2729
|
questions answered · the time-critical alert. Process + coverage detail are below and collapsible. -->
|
|
2553
|
-
<header class="hero">
|
|
2730
|
+
<header class="hero" id="summary">
|
|
2554
2731
|
${demoBannerHtml(opts.demoData === true)}
|
|
2555
2732
|
${confLineHtml(opts.delivery, productName)}
|
|
2556
2733
|
<h1 class="mark">${esc(fm.title || '')}</h1>
|
|
@@ -2576,7 +2753,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2576
2753
|
${alsoConsideredSection(ruledOut, recordsByUri, opts)}
|
|
2577
2754
|
|
|
2578
2755
|
<!-- doc-52 §3 WHAT ONLY YOU CAN CLOSE — forward decisions, plain English, after the findings. -->
|
|
2579
|
-
${buckets.you ? `<div class="sec" id="
|
|
2756
|
+
${buckets.you ? `<div class="sec" id="next"><span class="num"></span><h2>What happens next</h2></div>
|
|
2580
2757
|
<div class="panel actions"><div class="actgrp act-you">${renderProse(buckets.you.body)
|
|
2581
2758
|
.replace(/\[Time-critical\]\s*/gi, '<span class="src cl" style="margin-right:6px">Time-critical</span> ')
|
|
2582
2759
|
.replace(/\[Open question\]\s*/gi, '<span class="src" style="margin-right:6px">Open question</span> ')
|
|
@@ -2598,25 +2775,57 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2598
2775
|
${whatWasSearchedSection(opts, coverage, findings, recordsByUri)}
|
|
2599
2776
|
|
|
2600
2777
|
<footer>
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2778
|
+
${/* ONE CLIENT LINE, AND IT NAMES WHAT EACH DATE IS. It ran to four.
|
|
2779
|
+
"Rated under" STAYS, and it is not an oversight: the document carries the reviewer's
|
|
2780
|
+
provenance and portal-report strips that one line for every embedded reader at serve time,
|
|
2781
|
+
which is stated there and held by an arm. Deleting it here would take provenance off the
|
|
2782
|
+
reviewer's copy to save the portal a job it already does.
|
|
2783
|
+
"Run under project" also STAYS on the document, for the same reason and with the same
|
|
2784
|
+
remedy. It is internal provenance — an end-to-end arm reads it out of the published internal
|
|
2785
|
+
report, through the front-matter seam — and it was reaching clients only because nothing
|
|
2786
|
+
removed it at serve time. It does now, beside the provenance line, so the document keeps what
|
|
2787
|
+
review needs and the client reads neither. Deleting it here would have taken provenance off
|
|
2788
|
+
the internal copy to fix a leak that belongs in the same place the other one is fixed.
|
|
2789
|
+
The product name lead goes: the identity line already says which search this is.
|
|
2790
|
+
The run string is composed as "<date> · <provider>", and it was printed with neither part
|
|
2791
|
+
labelled, so a bare date sat next to a matter identifier meaning nothing in particular. It is
|
|
2792
|
+
split on the DATE by pattern — the same way `dateOf` does it one file over — and the remainder
|
|
2793
|
+
is the provider. A run string with no date in it is not this renderer's to take apart, so it
|
|
2794
|
+
is printed whole and unlabelled rather than guessed at. */''}
|
|
2795
|
+
<!-- WHICH MODELS SERVED THIS RUN. It used to close the scope fold, and the 2026-09-16 redesign
|
|
2796
|
+
deleted that fold — so the line was re-homed rather than dropped with its container, which
|
|
2797
|
+
would have removed a statement of provenance from the client's page as a side effect of a
|
|
2798
|
+
merge. Owner ruling, 2026-09-17: it belongs in the footer, beside the matter and the framework.
|
|
2799
|
+
It renders as '' on a run that recorded no models, so an archived run republishes exactly as
|
|
2800
|
+
delivered, and the call sits ADJACENT so the whitespace around it belongs to the line and goes
|
|
2801
|
+
with it — the freeze tool sets this paragraph aside by a pattern that takes the space before
|
|
2802
|
+
it. -->${servedModelsLine(opts.servedModels)}
|
|
2803
|
+
<span>${(() => {
|
|
2804
|
+
const runStr = String(fm.run ?? '').trim();
|
|
2805
|
+
const searchedOn = (runStr.match(/\d{4}-\d{2}-\d{2}/) || [])[0] ?? null;
|
|
2806
|
+
const provider = searchedOn
|
|
2807
|
+
? runStr.replace(searchedOn, '').replace(/^[\s\u00b7]+|[\s\u00b7]+$/g, '').trim()
|
|
2808
|
+
: runStr;
|
|
2809
|
+
const issuedOn = (String(opts.issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? opts.issued ?? null;
|
|
2810
|
+
const parts = [
|
|
2811
|
+
fm.matter ? `Matter ${esc(fm.matter)}` : '',
|
|
2812
|
+
searchedOn ? `searched on ${esc(searchedOn)}` : '',
|
|
2813
|
+
issuedOn ? `issued on ${esc(issuedOn)}` : '',
|
|
2814
|
+
provider ? esc(provider) : '',
|
|
2815
|
+
].filter(Boolean);
|
|
2816
|
+
const line = parts.length ? `${parts.join(' \u00b7 ')}.` : '';
|
|
2817
|
+
// The reviewer's provenance rides BELOW the client line, where portal-report removes it.
|
|
2818
|
+
// Both provenance lines ride BELOW the client line, where portal-report removes them.
|
|
2819
|
+
return line
|
|
2820
|
+
+ (fm.rated_under ? `<br>Rated under: <span class="mono">${esc(fm.rated_under)}</span>.` : '')
|
|
2821
|
+
+ (fm.run_under_project ? `<br>Run under project: <span class="mono">${esc(fm.run_under_project)}</span>.` : '');
|
|
2822
|
+
})()}</span>
|
|
2615
2823
|
${logoLockup({ mark: 16 })}
|
|
2616
2824
|
</footer>
|
|
2617
2825
|
</div>
|
|
2618
2826
|
<script>${PAGE_JS}</script>
|
|
2619
2827
|
</body></html>`;
|
|
2828
|
+
return doc.replace('<!--SECTION-STRIP-->', sectionStrip(doc, STRIP));
|
|
2620
2829
|
}
|
|
2621
2830
|
|
|
2622
2831
|
// CLI: node render.mjs <report.md> [findings.json] [outDir]
|
|
@@ -2658,3 +2867,24 @@ function officeLinkNote() {
|
|
|
2658
2867
|
return (linked ? 'A registration number shown as a link opens the office’s own page for that record. ' : '')
|
|
2659
2868
|
+ officeReasonSentences(RECORD_LINKS).map((s) => `${esc(s)} `).join('');
|
|
2660
2869
|
}
|
|
2870
|
+
|
|
2871
|
+
// THE MODELS THAT SERVED THIS SEARCH, as one closing line of the scope section (2026-09-14). The ids are
|
|
2872
|
+
// the ones the engine reported for its turns (tokens.mjs servedModels), never the tier a stage asked
|
|
2873
|
+
// for in place of a model the engine named: every tier goes to the program as the vendor's alias, and an
|
|
2874
|
+
// alias names no model. Both report
|
|
2875
|
+
// kinds call this, so they say it in the same words. '' when the run recorded none, so a run published
|
|
2876
|
+
// before the record existed renders exactly as it was delivered.
|
|
2877
|
+
//
|
|
2878
|
+
// A TIER WORD IS CLAUDE'S. servedModels lists a turn served under a company's own deployment name as the
|
|
2879
|
+
// tier it asked for ("Opus"), never the name, so the list may read "claude-opus-5, Haiku". Both are
|
|
2880
|
+
// Claude's and the line says so once; in a list that also names another vendor, the word says it itself
|
|
2881
|
+
// ("Claude Opus"). The four words, Fable among them, are the ones servedModels writes. They are kept here
|
|
2882
|
+
// rather than imported, because tokens.mjs loads the driver's settings and this module renders without them.
|
|
2883
|
+
const CLAUDE_TIER_WORD_RE = /^(?:Opus|Sonnet|Haiku|Fable)$/;
|
|
2884
|
+
export function servedModelsLine(ids) {
|
|
2885
|
+
const list = (Array.isArray(ids) ? ids : []).map((s) => String(s ?? '').trim()).filter(Boolean);
|
|
2886
|
+
if (!list.length) return '';
|
|
2887
|
+
const claude = list.every((id) => /^claude-/i.test(id) || CLAUDE_TIER_WORD_RE.test(id));
|
|
2888
|
+
const shown = claude ? list : list.map((id) => (CLAUDE_TIER_WORD_RE.test(id) ? `Claude ${id}` : id));
|
|
2889
|
+
return `<p class="servedby" style="margin:10px 0 0;font-size:13px">Prepared with${claude ? ' Claude' : ''}: ${shown.map(esc).join(', ')}.</p>`;
|
|
2890
|
+
}
|