clearotron 0.2.2 → 0.2.3
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 +28 -0
- package/.nvmrc +1 -1
- package/INSTALL.md +13 -11
- package/README.md +11 -16
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +4 -0
- package/bin/onboard.mjs +195 -19
- package/bin/start.mjs +154 -8
- package/build-info.json +2 -2
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +5 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +14 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/driver/CHANGELOG.md +56 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/dev-portal.mjs +2 -2
- package/driver/driver.config.mjs +1 -1
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +18 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +5 -5
- package/driver/plain-register.mjs +120 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +40 -1
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +26 -7
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +121 -1
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +2 -2
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +2 -2
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stages.mjs +4 -4
- package/driver/suite-census.json +134 -38
- package/driver/unit-inventory.mjs +184 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +28 -10
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-CEYvXdqH.js} +226 -49
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/strip-tracker-citations.mjs +29 -6
- package/scripts/test-run.mjs +16 -0
- package/shared/anon-overlay.mjs +3 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/invocation.mjs +28 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/node-floor.mjs +97 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import { SUMMARY_BLOCK_LINE, parseSummaryBlocks } from '../../shared/summary-blocks.mjs';
|
|
46
46
|
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine } from '../register-count.mjs';
|
|
47
47
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
48
|
-
import { knockoutFindingViews } from '../findings-model.mjs';
|
|
48
|
+
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
49
49
|
import { demoBannerHtml } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
50
50
|
// — the two facts the register card is allowed to read off a raw record, and NEITHER is minted
|
|
51
51
|
// here. `makeClassifyStatus` and `isAllClass` are the screening lane's own, already shipped, already
|
|
@@ -220,6 +220,28 @@ const KO_CSS = `
|
|
|
220
220
|
saved to PDF would drop the narrative this block exists to preserve. The beforeprint handler below
|
|
221
221
|
opens them; this hides the now-pointless toggle in print. */
|
|
222
222
|
@media print{.ko-full summary{display:none}}
|
|
223
|
+
/* THE LABELS THE READER ACTUALLY READS. ko-lbl above is 9.5px uppercase grey — the owner's
|
|
224
|
+
"what holds what?" was asked of a label in that style, so a new word in the same style is the
|
|
225
|
+
same defect with different letters. These are body size and body colour, and they carry the
|
|
226
|
+
band's own word ("Why High"), which is what makes them answerable without a legend. */
|
|
227
|
+
.ko-lbl2{display:block;font-size:14px;font-weight:700;color:var(--ink);margin:0 0 5px;letter-spacing:0}
|
|
228
|
+
/* The card's fold. ko-full rides with it so openAll() and the beforeprint handler reach it —
|
|
229
|
+
what folds on screen still prints, which is the rule this template already states for the
|
|
230
|
+
narrative fold. ko-why carries only the spacing a card needs inside its own body. */
|
|
231
|
+
.ko-why{margin:6px 0 5px}
|
|
232
|
+
.ko-why summary{font-size:12px;text-transform:none;letter-spacing:0;color:var(--rose,var(--faint));font-weight:600}
|
|
233
|
+
/* "About this request" — what was asked, and any flag on the asking. It sits in the hero because a
|
|
234
|
+
mis-scoped request makes every number below it answer the wrong question. */
|
|
235
|
+
.ko-req{margin:14px 0 0;padding:13px 16px;border:1px solid var(--line);border-radius:3px}
|
|
236
|
+
.ko-req .ko-lbl2{margin-bottom:6px}
|
|
237
|
+
.ko-req p{margin:0 0 6px;font-size:13.5px;color:var(--slate);line-height:1.6}
|
|
238
|
+
.ko-req p:last-child{margin-bottom:0}
|
|
239
|
+
.ko-reqflag{border-left:3px solid var(--rose,var(--line));padding-left:11px;margin-top:9px}
|
|
240
|
+
/* THE PURPLE NOTES COME OFF THE EXPORT, exactly as the clearance page removes its internal notes
|
|
241
|
+
("Internal (review-only) notes are removed on export"). The knockout's export is window.print()
|
|
242
|
+
via exportPDF(), so print is the whole export path and this rule is the whole strip. Without it
|
|
243
|
+
a knockout PDF forwarded to a client carries the reviewing lawyer's notes. */
|
|
244
|
+
@media print{.internal{display:none !important}}
|
|
223
245
|
.ko-reg{margin:11px 0 0;padding:8px 0 0;border-top:1px solid var(--line);
|
|
224
246
|
font-family:var(--mono);font-size:12px;color:var(--faint);line-height:1.5}
|
|
225
247
|
.ko-filings{padding:14px 0 2px;border-bottom:1px solid var(--line)}
|
|
@@ -297,8 +319,111 @@ function glanceSection(marks, framework, registerCounts) {
|
|
|
297
319
|
// all-classes figure alongside a class-scoped one would be a second count — one more provider call per
|
|
298
320
|
// mark per predicate, billable on Corsearch — and that is a spend the owner has not ruled on. Raised as
|
|
299
321
|
// a follow-up rather than assumed here (tracker issue 717).
|
|
322
|
+
// ── TERRITORIES IN WORDS, NEVER CODES (tracker issue 331 C) ─────────────────────────────────────────
|
|
323
|
+
//
|
|
324
|
+
// The line read "territories: EM, US, WO". On a worldwide run the same line printed every register code
|
|
325
|
+
// the provider offers — roughly two hundred, AD through ZZ, internal groupings among them — which the
|
|
326
|
+
// owner called a meaningless list, correctly: a reader cannot tell anything from two hundred codes and
|
|
327
|
+
// cannot tell much from three.
|
|
328
|
+
//
|
|
329
|
+
// TWO CODES ARE REGISTERS RATHER THAN COUNTRIES and neither is an ISO region, so neither can be resolved
|
|
330
|
+
// by a country table: EM is the EU Intellectual Property Office and WO is WIPO's international register.
|
|
331
|
+
// They are named here because they are the two a reader meets constantly.
|
|
332
|
+
//
|
|
333
|
+
// A CODE THIS TABLE CANNOT NAME IS DROPPED, NOT PRINTED. The provider's internal groupings (XA, XG, XS,
|
|
334
|
+
// XW, ZZ and their like) name no register a reader could look up, and printing one is the defect this
|
|
335
|
+
// change exists to remove. Dropping them is safe because the COUNT of registers is stated separately and
|
|
336
|
+
// is taken before any naming — so a reader is never told about fewer registers than were counted.
|
|
337
|
+
// TWO FORMS PER REGISTER, because English needs both: "Counted in the United States" takes the article
|
|
338
|
+
// and "United States application" refuses it. One map with one form produced "the United States
|
|
339
|
+
// application (pending)" on a card, which is why they are separate fields rather than a regex over one.
|
|
340
|
+
const REGISTER_NAMES = Object.freeze({
|
|
341
|
+
EM: { bare: 'European Union', phrase: 'the European Union' },
|
|
342
|
+
EU: { bare: 'European Union', phrase: 'the European Union' },
|
|
343
|
+
WO: { bare: 'international', phrase: 'the WIPO register' },
|
|
344
|
+
US: { bare: 'United States', phrase: 'the United States' },
|
|
345
|
+
GB: { bare: 'United Kingdom', phrase: 'the United Kingdom' },
|
|
346
|
+
UK: { bare: 'United Kingdom', phrase: 'the United Kingdom' },
|
|
347
|
+
CH: { bare: 'Swiss', phrase: 'Switzerland' },
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* A register code as a reader's words, or '' when this build cannot name it. NEVER returns a code.
|
|
352
|
+
* `bare: true` gives the attributive form for a phrase like "United States application".
|
|
353
|
+
*/
|
|
354
|
+
function territoryName(code, { bare = false } = {}) {
|
|
355
|
+
const c = String(code ?? '').trim().toUpperCase();
|
|
356
|
+
if (!c) return '';
|
|
357
|
+
if (REGISTER_NAMES[c]) return bare ? REGISTER_NAMES[c].bare : REGISTER_NAMES[c].phrase;
|
|
358
|
+
// The provider's internal groupings all sit in the X* and Z* space and name no lookup-able register.
|
|
359
|
+
if (/^[XZ]/.test(c)) return '';
|
|
360
|
+
try {
|
|
361
|
+
const name = new Intl.DisplayNames(['en'], { type: 'region' }).of(c);
|
|
362
|
+
// Intl hands the CODE back when it knows no region by that name — which would print exactly the
|
|
363
|
+
// thing this function exists to prevent, so an unresolved code is an empty string.
|
|
364
|
+
return name && name.toUpperCase() !== c ? name : '';
|
|
365
|
+
} catch { return ''; }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** "a, b and c" — the reader's list, not a join on commas. */
|
|
369
|
+
function listWords(items) {
|
|
370
|
+
const xs = (items ?? []).filter(Boolean);
|
|
371
|
+
if (xs.length <= 1) return xs[0] ?? '';
|
|
372
|
+
return `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The counted scope, in one sentence (tracker issue 331 C). Three shapes, and the shape is chosen by
|
|
377
|
+
* how many registers were counted rather than by which they were:
|
|
378
|
+
*
|
|
379
|
+
* every register the provider offers -> "Counted worldwide, 190 registers, on Clarivate Compumark."
|
|
380
|
+
* up to six -> named in full
|
|
381
|
+
* more than six, not all -> the count, and where the list is
|
|
382
|
+
*
|
|
383
|
+
* The provider is named here and nowhere else on the page; 331 D takes the vendor's name off the cards.
|
|
384
|
+
*/
|
|
385
|
+
const TERRITORY_NAME_CAP = 6;
|
|
386
|
+
function territoriesLine(registerCounts) {
|
|
387
|
+
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? 'the register';
|
|
388
|
+
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
389
|
+
const worldwide = registerCounts?.scope?.worldwide === true || regions.length === 0;
|
|
390
|
+
if (worldwide) {
|
|
391
|
+
const n = regions.length;
|
|
392
|
+
return n
|
|
393
|
+
? `Counted worldwide, ${n} registers, on ${provider}.`
|
|
394
|
+
: `Counted worldwide on ${provider}.`;
|
|
395
|
+
}
|
|
396
|
+
const named = regions.map(territoryName).filter(Boolean);
|
|
397
|
+
if (named.length && named.length <= TERRITORY_NAME_CAP) {
|
|
398
|
+
return `Counted in ${listWords(named)}, on ${provider}.`;
|
|
399
|
+
}
|
|
400
|
+
return `Counted on ${regions.length} registers, listed on the workbook's Register Counts sheet, on ${provider}.`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** What the counts do and do not say — the one line that replaces the count-basis paragraph on the page. */
|
|
404
|
+
const COUNTS_READER_LINE = 'Counts include live, pending and dead filings. '
|
|
405
|
+
+ 'A count is not a conflict; the cards above say which filings matter.';
|
|
406
|
+
|
|
300
407
|
function countsSection(marks, registerCounts) {
|
|
301
|
-
|
|
408
|
+
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER (tracker issue 331 B) ────────────────────────────
|
|
409
|
+
//
|
|
410
|
+
// The 70-word count-basis paragraph existed because three one-word headers — Identical, Containing,
|
|
411
|
+
// Close variations — did not say what they counted, so the definition had to go somewhere. Put it in
|
|
412
|
+
// the header and the paragraph has nothing left to do.
|
|
413
|
+
//
|
|
414
|
+
// ONE NAME ⇒ THE HEADER NAMES IT, because "Exactly ORBIT" needs no gloss at all. Several names share
|
|
415
|
+
// one table and no header can name one of them, so they keep the general form and each row's own
|
|
416
|
+
// forms line (already rendered, unchanged) carries that row's near-spellings.
|
|
417
|
+
const single = marks.length === 1 ? String(marks[0]?.name ?? '').trim() : '';
|
|
418
|
+
const forms = single
|
|
419
|
+
? ((countsForMark(registerCounts, single)?.counts?.close?.forms ?? []).map((f) => String(f?.form ?? '').trim()).filter(Boolean))
|
|
420
|
+
: [];
|
|
421
|
+
const headLabel = (pk) => {
|
|
422
|
+
if (pk === 'identical') return single ? `Exactly ${single}` : 'Exactly the name';
|
|
423
|
+
if (pk === 'containing') return single ? `Contains ${single}` : 'Contains the name';
|
|
424
|
+
return forms.length ? `Near-spellings (${forms.join(', ')})` : 'Near-spellings';
|
|
425
|
+
};
|
|
426
|
+
const head = COUNT_PREDICATES.map((p) => `<th>${esc(headLabel(p.key))}</th>`).join('');
|
|
302
427
|
const rows = marks.map((m) => {
|
|
303
428
|
const e = countsForMark(registerCounts, m.name);
|
|
304
429
|
const cells = COUNT_PREDICATES.map((p) => {
|
|
@@ -326,11 +451,13 @@ function countsSection(marks, registerCounts) {
|
|
|
326
451
|
return `<tr><td><b>${esc(m.name)}</b><br><span class="ko-classes">${esc(scope)}</span></td>${cells}</tr>`
|
|
327
452
|
+ (forms ? `<tr class="ko-forms"><td colspan="${COUNT_PREDICATES.length + 1}">${esc(forms)}</td></tr>` : '');
|
|
328
453
|
}).join('');
|
|
329
|
-
|
|
330
|
-
|
|
454
|
+
// COUNT_BASIS IS OFF THE PAGE, NOT OUT OF THE RECORD (tracker issue 331 B). It is still written to
|
|
455
|
+
// report-data.json (knockoutReportData -> registerCountBasis) and still on the workbook, which is where
|
|
456
|
+
// 331 says it belongs; what it stops doing is printing twice on a page whose headers now say the same
|
|
457
|
+
// thing in three words each.
|
|
331
458
|
return `<div class="panel">
|
|
332
459
|
<div class="ko-counts ko-scroll"><table><thead><tr><th>Name</th>${head}</tr></thead><tbody>${rows}</tbody></table></div>
|
|
333
|
-
<p class="ko-basis">${esc(
|
|
460
|
+
<p class="ko-basis">${esc(COUNTS_READER_LINE)}<br>${esc(territoriesLine(registerCounts))}</p>
|
|
334
461
|
</div>`;
|
|
335
462
|
}
|
|
336
463
|
|
|
@@ -532,6 +659,29 @@ function tierAbsenceLine(registerCounts, probeRan) {
|
|
|
532
659
|
// A NAME is deliberately not emitted here for the control to send. `ref` is right there and reads
|
|
533
660
|
// nicely, but portal-ui/src/contract/reportFrame.ts refuses a mark from the document side on purpose,
|
|
534
661
|
// and the composite key IS a mark name. The server reads `ref` back off disk once it has resolved.
|
|
662
|
+
/**
|
|
663
|
+
* THE CARD'S DETAIL, FOLDED (tracker issue 331 A.3).
|
|
664
|
+
*
|
|
665
|
+
* Visible on a card: the reference, the name, the band chip, the source chip, the one-sentence read and
|
|
666
|
+
* the evidence links. Folded: the paragraph that argues the band, and on a register card the use-check
|
|
667
|
+
* line and the record link with it.
|
|
668
|
+
*
|
|
669
|
+
* WHY A FOLD AND NOT A CUT. The paragraph is the reasoning, and 331 rejects "shortening by dropping the
|
|
670
|
+
* reasons" in as many words. It is one click away and it still prints — `ko-full` is on the element, so
|
|
671
|
+
* openAll() and the beforeprint handler both reach it and a saved PDF carries the whole argument.
|
|
672
|
+
*
|
|
673
|
+
* EMPTY IN, EMPTY OUT. A card with no detail renders no fold rather than an empty one, which is also the
|
|
674
|
+
* archived-run path: a run whose findings carry no `basis` renders exactly the card it was delivered.
|
|
675
|
+
*/
|
|
676
|
+
function whyBandFold(inner) {
|
|
677
|
+
const body = String(inner ?? '').trim();
|
|
678
|
+
if (!body) return '';
|
|
679
|
+
return `<details class="ko-full ko-why"><summary>${esc(WHY_BAND_LABEL)}</summary>${body}</details>`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** The card fold's own word. Stated once so the renderer and its tests read the same string. */
|
|
683
|
+
const WHY_BAND_LABEL = 'Why this band';
|
|
684
|
+
|
|
535
685
|
function findingBlock(v, framework, markIndex) {
|
|
536
686
|
const meta = [v.type, v.owner].filter(Boolean).map((s) => esc(s)).join(' · ');
|
|
537
687
|
const ev = v.evidence.map((u) => linkOrText(u)).join(' · ');
|
|
@@ -564,7 +714,7 @@ function findingBlock(v, framework, markIndex) {
|
|
|
564
714
|
</div>
|
|
565
715
|
${meta ? `<p class="ko-findmeta">${meta}</p>` : ''}
|
|
566
716
|
${v.lead ? `<p class="ko-findnet">${inlineMd(v.lead)}</p>` : ''}
|
|
567
|
-
${v.detail ? `<p class="ko-findbasis">${inlineMd(v.detail)}</p>` : ''}
|
|
717
|
+
${whyBandFold(v.detail ? `<p class="ko-findbasis">${inlineMd(v.detail)}</p>` : '')}
|
|
568
718
|
${ev ? `<p class="ko-findev">Evidence: ${ev}</p>` : ''}
|
|
569
719
|
</div>
|
|
570
720
|
</div>
|
|
@@ -714,18 +864,52 @@ export function promotableRecords(entry, mark) {
|
|
|
714
864
|
/** One sentence of what the filing IS, composed from the register's own fields. No adjective that the
|
|
715
865
|
* record did not supply, and every absent field says it is absent rather than rendering as a blank —
|
|
716
866
|
* a blank cell and an unstated field read alike to a human and only one is honest. */
|
|
867
|
+
// ── THE OFFICE, NOT THE VENDOR (tracker issue 331 D) ────────────────────────────────────────────────
|
|
868
|
+
//
|
|
869
|
+
// The sentence read "A filing of the identical name on Clarivate Compumark — held by ..., status
|
|
870
|
+
// PENDING, classes 9, 38, 45, territory us." Clarivate Compumark is the search vendor; the filing is at
|
|
871
|
+
// the United States office. Naming the vendor in the position where a reader expects the register says
|
|
872
|
+
// the filing exists somewhere it does not, and prints a raw two-letter territory code besides.
|
|
873
|
+
//
|
|
874
|
+
// The provider is still named — once, in scope, as the data source (territoriesLine), which is what it
|
|
875
|
+
// is. A card states the office, the kind of right, its status, its owner and its classes.
|
|
876
|
+
//
|
|
877
|
+
// STATUS IS THE RECORD'S OWN WORD, lower-cased for the sentence and never re-classified: "pending" here
|
|
878
|
+
// is the register's PENDING, not this renderer's reading of it. A record that states none says so.
|
|
879
|
+
const REGISTRATION_WORDS = /^(?:reg|registered|registration)/i;
|
|
880
|
+
|
|
881
|
+
/** "United States application (pending)" — the office and the kind of right, from the record's own fields. */
|
|
882
|
+
function registerRightPhrase(r) {
|
|
883
|
+
const office = territoryName(r?.territory, { bare: true });
|
|
884
|
+
const status = String(r?.status ?? '').trim();
|
|
885
|
+
// A right whose status names a registration is a registration; anything else is an application. The
|
|
886
|
+
// record's raw word rides in the bracket either way, so the reader never has to trust the reading.
|
|
887
|
+
const kind = REGISTRATION_WORDS.test(status) ? 'registration' : 'application';
|
|
888
|
+
const where = office ? `${office} ` : '';
|
|
889
|
+
// The bracket exists to show the register's OWN word where that word adds something. On a record whose
|
|
890
|
+
// status already names the kind ("REGISTERED" beside "registration") it adds nothing but noise, so it
|
|
891
|
+
// is dropped — the reader has the same fact either way, in one word instead of two.
|
|
892
|
+
// "registration (registered)" and "application (application)" say the kind twice. Both roots are
|
|
893
|
+
// tested rather than stemmed, because a stemmer that turns "registered" and "registration" into one
|
|
894
|
+
// token also turns "pending" and "pended" into one, and "(pending)" is a fact the reader wants.
|
|
895
|
+
const echoes = (kind === 'registration' && REGISTRATION_WORDS.test(status))
|
|
896
|
+
|| (kind === 'application' && /^(?:appl|filed?)/i.test(status));
|
|
897
|
+
const bracket = status && !echoes ? ` (${status.toLowerCase()})` : '';
|
|
898
|
+
return `${where}${kind}${bracket}`.trim();
|
|
899
|
+
}
|
|
900
|
+
|
|
717
901
|
function registerStatement(r, provider) {
|
|
718
|
-
const
|
|
719
|
-
? `A filing on ${provider} found under the close variation ${r.matchedForm ?? '—'}`
|
|
720
|
-
: `A filing of the identical name on ${provider}`;
|
|
902
|
+
const name = String(r?.mark ?? r?.name ?? '').trim();
|
|
721
903
|
const classes = (Array.isArray(r.classes) ? r.classes : []).filter((n) => Number.isFinite(Number(n)));
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
904
|
+
const lead = [name, registerRightPhrase(r)].filter(Boolean).join(', ');
|
|
905
|
+
const owner = r.owner ? `by ${r.owner}` : 'owner not stated on the record';
|
|
906
|
+
const cls = classes.length
|
|
907
|
+
? `class${classes.length === 1 ? '' : 'es'} ${classes.join(', ')}`
|
|
908
|
+
: 'classes not stated on the record';
|
|
909
|
+
// A close-variation card says WHICH near-form matched, because the name at the head of the sentence is
|
|
910
|
+
// then not the name the client asked about and a reader must not have to guess which it is.
|
|
911
|
+
const via = r.matchedBasis === 'close' && r.matchedForm ? ` Found under the close variation ${r.matchedForm}.` : '';
|
|
912
|
+
return `${lead} ${owner}, ${cls}.${via}`;
|
|
729
913
|
}
|
|
730
914
|
|
|
731
915
|
/**
|
|
@@ -741,6 +925,72 @@ function registerStatement(r, provider) {
|
|
|
741
925
|
* form would point a reader at a row that does not exist. Its receipt is the register record itself,
|
|
742
926
|
* which the card links.
|
|
743
927
|
*/
|
|
928
|
+
/**
|
|
929
|
+
* WHICH OF A MARK'S REGISTER CARDS THE PAGE DRAWS (tracker issue 331 A.3).
|
|
930
|
+
*
|
|
931
|
+
* THIS FILTERS THE PAGE, NEVER THE RECORD, and that separation is the whole point. 331 rejects dropping
|
|
932
|
+
* anything from report-data.json or the workbook to make the page shorter, so `registerCardViews` still
|
|
933
|
+
* builds every promoted filing and still assigns the ordinals it always did — knockoutReportData reads
|
|
934
|
+
* that, unchanged. This function decides only what is DRAWN.
|
|
935
|
+
*
|
|
936
|
+
* Keeping one projection also keeps the flag path honest: portal-service resolves a flag as
|
|
937
|
+
* `marks[i].findings.find(f => f.ordinal === ordinal)`, so a card on the page carries the ordinal its
|
|
938
|
+
* own data entry has. A card the page does not draw cannot be flagged, because there is nothing to click.
|
|
939
|
+
*
|
|
940
|
+
* ONLY AN EXPLICIT LOWEST-RUNG BAND SUPPRESSES A CARD. Absence never does, and that is the whole rule —
|
|
941
|
+
* there is no second condition and no archived-run special case, because this one covers both.
|
|
942
|
+
*
|
|
943
|
+
* The first cut read an absent band as "lowest" and needed a clause-F escape hatch beside it: if no
|
|
944
|
+
* filing on the mark carried a band, keep them all. That escape hatch is what a wrong default looks
|
|
945
|
+
* like. It handled the run with NO bands and silently mishandled the run with SOME: `band` is optional
|
|
946
|
+
* per row, so a rater who bands the filings that matter and leaves the rest alone is doing what the
|
|
947
|
+
* shape invites — and the mark whose one typed band happened to be the bottom rung lost every card it
|
|
948
|
+
* had, including the filings nobody ruled on.
|
|
949
|
+
*
|
|
950
|
+
* `atLowestBand` below already argues this, one function down: a band it cannot place on the ladder
|
|
951
|
+
* is kept, because "the rater said something, and the safe reading of something we cannot rank is that
|
|
952
|
+
* it is worth pointing at". An absent band is strictly LESS information than an unrankable one, so if
|
|
953
|
+
* unknown keeps, absent keeps. Dropping it would be this renderer deciding a filing is immaterial on
|
|
954
|
+
* the rater's behalf — the same claim tracker issue 274 removed from these cards for the same reason.
|
|
955
|
+
*
|
|
956
|
+
* A run that carries no bands at all still renders every card it was delivered with, which is what
|
|
957
|
+
* clause F asks for; it now falls out of the rule instead of being carved around it.
|
|
958
|
+
*/
|
|
959
|
+
function registerCardsOnPage(cards, mark, framework) {
|
|
960
|
+
return cards.filter((v) => !atLowestBand(framework, readBandFor(mark, v?.record?.recordId)));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* DID THE RATER PUT THIS FILING ON THE LADDER'S LOWEST RUNG? (tracker issue 331 A.3)
|
|
965
|
+
*
|
|
966
|
+
* The ladder runs worst-first, so the lowest rung is the LAST entry. A filing the rater put there is one
|
|
967
|
+
* the rater called manageable, and 331 keeps those in the filings table rather than on a card: three
|
|
968
|
+
* cards whose own text says the filing does not bear materially on the rating cost the reader more than
|
|
969
|
+
* they told him.
|
|
970
|
+
*
|
|
971
|
+
* Stated as "is it AT the lowest rung" rather than "is it above it", because that is the only direction
|
|
972
|
+
* a caller may act on. Every other answer — a rung higher up, a word this build cannot place on the
|
|
973
|
+
* run's own ladder, no band at all — is a filing that keeps its card. An unplaceable word is not a band
|
|
974
|
+
* this function may quietly demote: the rater said something, and the safe reading of something we
|
|
975
|
+
* cannot rank is that it is worth pointing at. Absence says even less, so it keeps the card too.
|
|
976
|
+
*/
|
|
977
|
+
function atLowestBand(framework, band) {
|
|
978
|
+
const ladder = Array.isArray(framework?.bands) ? framework.bands : [];
|
|
979
|
+
const word = String(band ?? '').trim().toLowerCase();
|
|
980
|
+
if (!word || !ladder.length) return false;
|
|
981
|
+
const lowest = String(ladder[ladder.length - 1]?.label ?? '').trim().toLowerCase();
|
|
982
|
+
return Boolean(lowest) && word === lowest;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** The rater's band for one filing, off the row that names it. `null` when the rater gave none. */
|
|
986
|
+
function readBandFor(mark, recordId) {
|
|
987
|
+
const id = String(recordId ?? '').trim();
|
|
988
|
+
if (!id) return null;
|
|
989
|
+
const row = (Array.isArray(mark?.registerReads) ? mark.registerReads : [])
|
|
990
|
+
.find((x) => String(x?.recordId ?? '').trim() === id);
|
|
991
|
+
return String(row?.band ?? '').trim() || null;
|
|
992
|
+
}
|
|
993
|
+
|
|
744
994
|
function registerCardViews(mark, framework, registerRecords) {
|
|
745
995
|
if (!registerRecords || registerRecords.unavailable) return { cards: [], entry: null, promoted: 0 };
|
|
746
996
|
const entry = recordsForMark(registerRecords, mark?.name);
|
|
@@ -819,7 +1069,7 @@ function sourceChips(v) {
|
|
|
819
1069
|
// already defines — the same one the clearance report uses for this material — so a reader can see whose
|
|
820
1070
|
// voice a line is in. Merging them into the client-voiced body would make the reviewer's asides read as
|
|
821
1071
|
// findings about the mark, which is the one way this ruling could produce a worse document.
|
|
822
|
-
const REVIEWER_NOTES_LEGEND = '
|
|
1072
|
+
const REVIEWER_NOTES_LEGEND = 'Purple notes are for the reviewing lawyer. Remove them before this goes to the client.';
|
|
823
1073
|
|
|
824
1074
|
// The clearance lane's own label, copied rather than re-worded (tracker issue 276). One spelling across
|
|
825
1075
|
// both products is the point: a reader who has seen it on a clearance report knows what it means here.
|
|
@@ -833,12 +1083,46 @@ function ownerCheckFor(ownerChecks, recordId) {
|
|
|
833
1083
|
.find((c) => (Array.isArray(c?.recordIds) ? c.recordIds : []).some((x) => String(x).trim() === id)) ?? null;
|
|
834
1084
|
}
|
|
835
1085
|
|
|
1086
|
+
// The two-place sort lives in findings-model.mjs (splitKnockoutNotes) because the predelivery lint
|
|
1087
|
+
// needs the same answer: the page files each note, and the reviewer warns a writer whose note will file
|
|
1088
|
+
// the way they did not intend. One reader, so the two can never disagree.
|
|
1089
|
+
/**
|
|
1090
|
+
* "About this request" — what was asked, and any flag on the asking (tracker issue 331 A.1).
|
|
1091
|
+
*
|
|
1092
|
+
* WHAT WAS ASKED IS CODE-OWNED. It is read off the run's own frozen instructed scope, never off model
|
|
1093
|
+
* prose, so the line states the request the run was given rather than a paraphrase of it. An archived run
|
|
1094
|
+
* that carries no such sidecar renders the flags alone, and a run with neither renders nothing at all —
|
|
1095
|
+
* the block never appears empty and never appears with a heading over nothing.
|
|
1096
|
+
*
|
|
1097
|
+
* THE FLAGS ARE THE REVIEWER'S OWN WORDS, unedited. This surface moves them; it does not rewrite them.
|
|
1098
|
+
* They keep the purple convention here exactly as they wear it under the cards, which is also what keeps
|
|
1099
|
+
* them off the export: the strip is one rule on .internal and it reaches both places.
|
|
1100
|
+
*/
|
|
1101
|
+
function aboutRequestBlock(scope, requestNotes) {
|
|
1102
|
+
const goods = String(scope?.goods ?? '').trim();
|
|
1103
|
+
const classes = (Array.isArray(scope?.classes) ? scope.classes : []).filter((c) => c || c === 0);
|
|
1104
|
+
const jx = (Array.isArray(scope?.jurisdictions) ? scope.jurisdictions : []).map((t) => territoryName(t)).filter(Boolean);
|
|
1105
|
+
const asked = [
|
|
1106
|
+
classes.length ? `Class${classes.length === 1 ? '' : 'es'} ${classes.join(', ')}` : '',
|
|
1107
|
+
goods,
|
|
1108
|
+
].filter(Boolean).join(' — ');
|
|
1109
|
+
const where = jx.length ? `Searched in ${listWords(jx)}.` : '';
|
|
1110
|
+
const flags = (requestNotes ?? []).filter(Boolean);
|
|
1111
|
+
if (!asked && !where && !flags.length) return '';
|
|
1112
|
+
return `<div class="ko-req">
|
|
1113
|
+
<span class="ko-lbl2">About this request</span>
|
|
1114
|
+
${asked ? `<p>${esc(asked)}</p>` : ''}
|
|
1115
|
+
${where ? `<p>${esc(where)}</p>` : ''}
|
|
1116
|
+
${flags.length ? `<div class="internal ko-reqflag">${
|
|
1117
|
+
flags.map((n) => `<p class="ko-bul">${inlineMd(n)}</p>`).join('')}</div>` : ''}
|
|
1118
|
+
</div>`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
836
1121
|
function reviewerNotesBlock(m) {
|
|
837
|
-
const notes = (
|
|
838
|
-
.map((n) => String(n ?? '').trim()).filter(Boolean);
|
|
1122
|
+
const notes = splitKnockoutNotes(m).name;
|
|
839
1123
|
if (!notes.length) return '';
|
|
840
1124
|
return `<div class="internal">
|
|
841
|
-
<span class="tag">For
|
|
1125
|
+
<span class="tag">For the reviewing lawyer</span>
|
|
842
1126
|
${notes.map((n) => `<p class="ko-bul">${inlineMd(n)}</p>`).join('')}
|
|
843
1127
|
</div>`;
|
|
844
1128
|
}
|
|
@@ -1003,34 +1287,117 @@ function coverageClause(mark, registerCounts, probeRan) {
|
|
|
1003
1287
|
// NOTHING IS DISCARDED. Where a run carries both the typed fields and prose bullets the bullets survive
|
|
1004
1288
|
// under a collapsed "Full narrative" — the reader who wants the long form still has it, one click away,
|
|
1005
1289
|
// and no sentence the stage wrote is dropped on the floor.
|
|
1006
|
-
|
|
1290
|
+
/**
|
|
1291
|
+
* THE LABELS, IN THE READER'S WORDS (tracker issue 331 A.2).
|
|
1292
|
+
*
|
|
1293
|
+
* The old pair was "What holds it there" and "What would move it", 9.5px grey capitals. The owner read
|
|
1294
|
+
* the first one and asked "what holds what?" — so the fix is not a synonym in the same style, it is a
|
|
1295
|
+
* label that carries the band's own word and answers the question by itself.
|
|
1296
|
+
*
|
|
1297
|
+
* factors → "Why High" (the mark's own band)
|
|
1298
|
+
* counterFactors → "Why not Very High" (the rung ABOVE it on the run's own ladder)
|
|
1299
|
+
* mitigation → "What would lower the risk"
|
|
1300
|
+
*
|
|
1301
|
+
* The ladder runs worst-first, so the rung above is the PREVIOUS entry. At the top rung there is no
|
|
1302
|
+
* higher band to name and the honest label is "What keeps it here" — the same question, asked where the
|
|
1303
|
+
* comparative form has no answer. A band this build cannot find on the ladder gets that label too,
|
|
1304
|
+
* rather than a comparative naming a rung that may not exist.
|
|
1305
|
+
*/
|
|
1306
|
+
function counterLabel(framework, band) {
|
|
1307
|
+
const ladder = Array.isArray(framework?.bands) ? framework.bands : [];
|
|
1308
|
+
const i = ladder.findIndex((b) => String(b?.label ?? '').trim().toLowerCase() === String(band ?? '').trim().toLowerCase());
|
|
1309
|
+
const up = i > 0 ? String(ladder[i - 1]?.label ?? '').trim() : '';
|
|
1310
|
+
return up ? `Why not ${up}` : 'What keeps it here';
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const ASSESSMENT_FOLD_LABEL = 'Read the full assessment';
|
|
1314
|
+
|
|
1315
|
+
/** The scope block's own words, as one string, so the caveat filter reads exactly what the page prints. */
|
|
1316
|
+
const SCOPE_BLOCK_TEXT = 'What this is. A fast screen for obvious blockers to using each name, from '
|
|
1317
|
+
+ 'marketplace and web use plus a count of register filings. What it is not. A clearance search. We '
|
|
1318
|
+
+ 'drew no register conclusions and give no filing advice. A name that passes here is not clear; it '
|
|
1319
|
+
+ 'goes on to clearance. Every conflict above links to the material we found. The audit workbook holds '
|
|
1320
|
+
+ 'every search run, every empty result and the working notes. Register data.';
|
|
1321
|
+
|
|
1322
|
+
/** Words that carry no claim, so their presence or absence says nothing about what a sentence asserts. */
|
|
1323
|
+
const STOPWORDS = new Set(['a', 'an', 'and', 'are', 'as', 'at', 'be', 'been', 'but', 'by', 'can', 'do',
|
|
1324
|
+
'does', 'each', 'for', 'from', 'has', 'have', 'here', 'in', 'is', 'it', 'its', 'no', 'not', 'of', 'on',
|
|
1325
|
+
'or', 'that', 'the', 'their', 'them', 'there', 'these', 'they', 'this', 'to', 'up', 'was', 'we', 'were',
|
|
1326
|
+
'what', 'when', 'which', 'will', 'with', 'you', 'your']);
|
|
1327
|
+
|
|
1328
|
+
const contentWords = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9\s-]/g, ' ')
|
|
1329
|
+
.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Does this line assert anything the reference text does not already assert?
|
|
1333
|
+
*
|
|
1334
|
+
* TRUE unless every content word in the line is already in the reference. An empty line has nothing to
|
|
1335
|
+
* say and returns false; a line with one unfamiliar word is kept. Singular/plural is folded so that
|
|
1336
|
+
* "conclusion" does not read as new beside "conclusions".
|
|
1337
|
+
*/
|
|
1338
|
+
function saysSomethingNew(line, reference) {
|
|
1339
|
+
// The stem must be IDEMPOTENT on the singular, or the fold does nothing: an earlier form stripped
|
|
1340
|
+
// "es" and turned "gives" into "giv" while leaving "give" alone, so the two never matched and every
|
|
1341
|
+
// caveat looked new. Strip one trailing "s" and nothing else.
|
|
1342
|
+
const stem = (w) => w.replace(/ies$/, 'y').replace(/s$/, '');
|
|
1343
|
+
const known = new Set(contentWords(reference).map(stem));
|
|
1344
|
+
const words = contentWords(line);
|
|
1345
|
+
if (!words.length) return false;
|
|
1346
|
+
return words.some((w) => !known.has(stem(w)));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function readBlock(m, framework) {
|
|
1007
1350
|
const factors = (m.factors ?? []).filter((s) => typeof s === 'string' && s.trim());
|
|
1008
1351
|
const counter = (m.counterFactors ?? []).filter((s) => typeof s === 'string' && s.trim());
|
|
1009
1352
|
const structured = Boolean(m.basis || factors.length || counter.length || m.mitigation);
|
|
1010
1353
|
const bullets = (m.bullets ?? []).map((b) => `<li>${inlineMd(b)}</li>`).join('');
|
|
1011
1354
|
if (!structured) return bullets ? `<ul class="ko-bul">${bullets}</ul>` : '';
|
|
1355
|
+
const assessment = String(m.assessment ?? '').trim();
|
|
1356
|
+
const band = String(m.rating ?? '').trim();
|
|
1012
1357
|
return [
|
|
1013
1358
|
m.basis ? `<p class="ko-basisline">${inlineMd(m.basis)}</p>` : '',
|
|
1014
|
-
factors.length ? `<
|
|
1015
|
-
|
|
1359
|
+
factors.length ? `<div class="ko-why-band">${band ? `<span class="ko-lbl2">Why ${esc(band)}</span>` : ''}<ul class="ko-bul">${
|
|
1360
|
+
factors.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
|
|
1361
|
+
counter.length ? `<div class="ko-counter"><span class="ko-lbl2">${esc(counterLabel(framework, band))}</span><ul class="ko-bul">${
|
|
1016
1362
|
counter.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
|
|
1017
|
-
m.mitigation ? `<div class="ko-mitig"><span class="ko-
|
|
1018
|
-
|
|
1363
|
+
m.mitigation ? `<div class="ko-mitig"><span class="ko-lbl2">What would lower the risk</span><p>${inlineMd(m.mitigation)}</p></div>` : '',
|
|
1364
|
+
// ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD (tracker issue 331 A.2) ────────────────────
|
|
1365
|
+
//
|
|
1366
|
+
// The model writes both. `bullets` (five) say in other words what `factors` (four) already say
|
|
1367
|
+
// visible above; `assessment` is the long-form read under its own four headings, and until now the
|
|
1368
|
+
// page rendered it NOWHERE — validated, carried in report-data.json and the workbook, never shown.
|
|
1369
|
+
// The owner found the bullets fold by accident and called it the most useful block on the page; the
|
|
1370
|
+
// assessment is the better version of the same thing.
|
|
1371
|
+
//
|
|
1372
|
+
// `bullets` is NOT dropped from the record — knockoutReportData still carries it and the workbook
|
|
1373
|
+
// still writes it. What retires is one fold on one page, replaced by a longer answer to the same
|
|
1374
|
+
// question. 331 rejects dropping the field itself, and this does not.
|
|
1375
|
+
//
|
|
1376
|
+
// CLAUSE F. An archived run carries bullets and no assessment: it keeps the fold it was delivered
|
|
1377
|
+
// with, under its own old label. The fallback is the old block, never an empty new one.
|
|
1378
|
+
assessment
|
|
1379
|
+
? `<details class="ko-full"><summary>${esc(ASSESSMENT_FOLD_LABEL)}</summary><div class="ko-assess">${mdParagraphs(assessment)}</div></details>`
|
|
1380
|
+
: (bullets ? `<details class="ko-full"><summary>Full narrative</summary><ul class="ko-bul">${bullets}</ul></details>` : ''),
|
|
1019
1381
|
].filter(Boolean).join('');
|
|
1020
1382
|
}
|
|
1021
1383
|
|
|
1022
1384
|
function analysisSection(marks, framework, { registerCounts = null, probeRan = false, registerRecords = null, ownerChecks = [] } = {}) {
|
|
1023
1385
|
const cards = marks.map((m, markIndex) => {
|
|
1024
1386
|
const stop = bandStop(framework, m.rating);
|
|
1025
|
-
const bullets = readBlock(m);
|
|
1387
|
+
const bullets = readBlock(m, framework);
|
|
1026
1388
|
// — the promoted register filings, in the SAME list as the common-law conflicts. They render
|
|
1027
1389
|
// after the typed findings and that position is not a rank: a card with no band already sorts last
|
|
1028
1390
|
// under compareKnockoutBlockingPower, so appending them is the ladder's own rule applied to a row
|
|
1029
1391
|
// that has no rung, not a new one invented for the register.
|
|
1030
1392
|
const reg = registerCardViews(m, framework, registerRecords);
|
|
1031
|
-
const
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1393
|
+
const drawn = registerCardsOnPage(reg.cards, m, framework);
|
|
1394
|
+
const regBlocks = drawn.map((v) => registerFindingBlock(v, markIndex, m?.registerReads, framework, ownerChecks)).join('');
|
|
1395
|
+
// The overflow counts what is NOT on the page against what met the promotion test — cards held back
|
|
1396
|
+
// by the cap and cards the rater put on the lowest rung alike, because from the reader's side they
|
|
1397
|
+
// are the same fact: further filings for this name, all of them listed below.
|
|
1398
|
+
const held = reg.promoted - drawn.length;
|
|
1399
|
+
const overflow = held > 0
|
|
1400
|
+
? `<p class="ko-bul">${esc(`${held} further filing${held === 1 ? '' : 's'} for this name met the same test — every one of them is in the filings section below.`)}</p>`
|
|
1034
1401
|
: '';
|
|
1035
1402
|
// THE DEFECT THIS CLOSES. This line was `(m.findings ??).filter((f) => f?.url)`, and a
|
|
1036
1403
|
// typed finding has no `url` — so every conflict was dropped from the page and a mark whose findings
|
|
@@ -1121,87 +1488,6 @@ function depthStrip(note) {
|
|
|
1121
1488
|
// says what the lines under it are. It is not a filter and it rewrites no caveat.
|
|
1122
1489
|
const CAVEAT_LEAD = 'This screen also carries the following limits:';
|
|
1123
1490
|
|
|
1124
|
-
/**
|
|
1125
|
-
* WHAT THIS RUN SEARCHED AND DID NOT, FROM THE RUN'S OWN POLICY.
|
|
1126
|
-
*
|
|
1127
|
-
* The line this replaces was composed from `registerCounts` — an ARTIFACT — and said "Common-law/
|
|
1128
|
-
* marketplace screen only, register searches are addressed separately" whenever no counts were present.
|
|
1129
|
-
* That reads the absence of a result as a statement of scope, which is the class this repo keeps paying
|
|
1130
|
-
* for: a run INSTRUCTED to take register counts whose provider refused printed the same sentence as a
|
|
1131
|
-
* run never instructed to take them at all. One of those is a scope statement and the other is a gap in
|
|
1132
|
-
* the record, and a client cannot tell them apart.
|
|
1133
|
-
*
|
|
1134
|
-
* The clearance report composes its scope from the run's frozen components (`productCoverageNote`,
|
|
1135
|
-
* search-policy.mjs) and this now does the same, from the same object, so the two lanes cannot describe
|
|
1136
|
-
* the same component in different words.
|
|
1137
|
-
*
|
|
1138
|
-
* THE ARTIFACT IS STILL REPORTED — separately, and as what it is. Where the policy says a component ran
|
|
1139
|
-
* and its output is missing, that is disclosed as a gap rather than folded into "we did not do it".
|
|
1140
|
-
*
|
|
1141
|
-
* @returns {string[]} lines, most load-bearing first. Empty when there is no policy to read, and the
|
|
1142
|
-
* caller falls back to the artifact-derived sentence rather than printing nothing.
|
|
1143
|
-
*/
|
|
1144
|
-
export function knockoutScopeLines(policy, { registerCounts = null, probeRan = false } = {}) {
|
|
1145
|
-
const c = policy?.components;
|
|
1146
|
-
if (!c) return [];
|
|
1147
|
-
const lines = [];
|
|
1148
|
-
lines.push('This is a screen, not a clearance search: it triages names against use we can see, '
|
|
1149
|
-
+ 'and makes no filing recommendation.');
|
|
1150
|
-
if (c.registerProbe) {
|
|
1151
|
-
const who = registerCounts?.providerLabel ?? registerCounts?.provider ?? null;
|
|
1152
|
-
// THE BASIS RIDES WITH THE COMPONENT THAT NEEDS IT, in methodLine's own words rather than new ones.
|
|
1153
|
-
// Composing this section from the policy moved methodLine off the page for every run that HAS a
|
|
1154
|
-
// policy — which is every live run — and took COUNT_BASIS and "Counting is not searching" with it.
|
|
1155
|
-
// That is the sentence saying a count weighs nothing and decides nothing, on the page that prints
|
|
1156
|
-
// the counts. `runner.knockout-e2e.test.mjs` caught it and this branch called it pre-existing;
|
|
1157
|
-
// it is not — origin/main's renderer passes that arm 3/3 in the same worktree.
|
|
1158
|
-
lines.push(registerCounts
|
|
1159
|
-
? `Registers were searched for a hit-count per name${who ? ` on ${who}` : ''}, and the filings behind `
|
|
1160
|
-
+ `the narrow counts are listed below. ${COUNT_BASIS} Counting is not searching: no register `
|
|
1161
|
-
+ `conclusions, no filing advice, and a full register clearance is a separate piece of work.`
|
|
1162
|
-
: 'Registers were searched for a hit-count per name, and no counts reached this report — '
|
|
1163
|
-
+ 'that is a gap in the record for this run, not a statement that registers were left out.');
|
|
1164
|
-
} else if (c.registerProbe === false) {
|
|
1165
|
-
lines.push(probeRan
|
|
1166
|
-
? 'Registers were not part of this search. A register count screen ran separately; full register analysis is its own piece of work.'
|
|
1167
|
-
: 'Registers were not searched in this search.');
|
|
1168
|
-
}
|
|
1169
|
-
// A COMPONENT THE POLICY DOES NOT MENTION IS NOT A COMPONENT THAT WAS OFF. Same rule as the missing
|
|
1170
|
-
// policy above, one level down: `undefined` is silence, and silence is not `false`. Reached only when
|
|
1171
|
-
// `components` arrives without this key — every row in search-policy.mjs states all three, so the
|
|
1172
|
-
// product table cannot produce it; `{ ...policyFor(product)?.components, ...r.components }` can, when
|
|
1173
|
-
// the product resolves to nothing. Driven before this line existed: an empty `components` printed
|
|
1174
|
-
// "Registers were not searched in this search" on a report that was carrying a register hit-count
|
|
1175
|
-
// table, which is the document contradicting itself in the section that exists to state scope. Saying
|
|
1176
|
-
// nothing is the honest answer, and it needs no new sentence in front of a client.
|
|
1177
|
-
//
|
|
1178
|
-
// THE TWO BELOW STAY `!== true`, and the difference is the population rather than the principle.
|
|
1179
|
-
// `registerProbe` is true on two knockout rows (knockout-search, knockout-register), so silence about
|
|
1180
|
-
// it is genuinely ambiguous. `commonLawGrid` and `jxLanes` are false on EVERY row this template
|
|
1181
|
-
// renders — they are what the clearance search is — so silence there states a thing that is true of
|
|
1182
|
-
// the whole population, and making them conditional would drop a disclosure the clearance report
|
|
1183
|
-
// makes about itself. Symmetry here would cost a client the sentence naming the lane they did not buy.
|
|
1184
|
-
// Stated because the clearance report states its own absence of them, in these words. A reader
|
|
1185
|
-
// comparing two documents must not have to infer that a lane is missing from its silence.
|
|
1186
|
-
if (c.commonLawGrid !== true) {
|
|
1187
|
-
lines.push('The systematic common-law grid sweep is not part of this search — that is the clearance search.');
|
|
1188
|
-
}
|
|
1189
|
-
if (c.jxLanes !== true) {
|
|
1190
|
-
lines.push('The per-jurisdiction native-script deep dive is not part of this search.');
|
|
1191
|
-
}
|
|
1192
|
-
return lines;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function methodLine(registerCounts, probeRan, { citedFindings = 0, uncitedFindings = 0 } = {}) {
|
|
1196
|
-
const receipts = (citedFindings && !uncitedFindings)
|
|
1197
|
-
? ` Every conflict named above cites material held in this run's own research payload for that name.`
|
|
1198
|
-
: '';
|
|
1199
|
-
return (registerCounts
|
|
1200
|
-
? `Common-law/marketplace screen, plus a register HIT-COUNT per name on ${registerCounts.providerLabel ?? registerCounts.provider ?? 'the register'}. ${COUNT_BASIS} `
|
|
1201
|
-
+ `Counting is not searching: no register conclusions, no filing advice, and a full register clearance is a separate piece of work.`
|
|
1202
|
-
: `Common-law/marketplace screen only — register searches are addressed separately${probeRan ? ' (a register count screen ran; full register analysis follows separately)' : ''}. `
|
|
1203
|
-
+ `A knockout screen is triage, not a clearance search: no register conclusions, no filing advice.`) + receipts;
|
|
1204
|
-
}
|
|
1205
1491
|
|
|
1206
1492
|
/**
|
|
1207
1493
|
* Render the knockout report.
|
|
@@ -1231,6 +1517,12 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1231
1517
|
// instructed to do rather than what its artifacts happen to show. Defaults to null, and an archived
|
|
1232
1518
|
// run that carries none keeps the artifact-derived sentence it was delivered with.
|
|
1233
1519
|
searchPolicy = null,
|
|
1520
|
+
// — the run's own frozen instructed scope (tracker issue 331 A.1): the goods, classes and
|
|
1521
|
+
// territories the requester asked for, in the requester's words. Defaults to null, and an archived run
|
|
1522
|
+
// that carries no such sidecar renders "About this request" from its request-level notes alone, or not
|
|
1523
|
+
// at all when it has none. Never derived from model prose — this is what was ASKED, not what was said
|
|
1524
|
+
// about it.
|
|
1525
|
+
instructedScope = null,
|
|
1234
1526
|
// — the run's frozen delivery overlay. It defaults to null, which is the NO-OPINION state and
|
|
1235
1527
|
// renders the same plain "Privileged & Confidential" this template always printed — so the ~15 unit
|
|
1236
1528
|
// fixtures and both render-check scripts, none of which pass one, are unchanged by its arrival.
|
|
@@ -1257,7 +1549,12 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1257
1549
|
const allViews = marks.flatMap((m) => knockoutFindingViews(m, { manifest: framework }));
|
|
1258
1550
|
const citedFindings = allViews.filter((v) => v.evidence.length).length;
|
|
1259
1551
|
const uncitedFindings = allViews.length - citedFindings;
|
|
1260
|
-
|
|
1552
|
+
// ── A ONE-NAME PAGE HAS NO INDEX (tracker issue 331 A.6) ────────────────────────────────────────
|
|
1553
|
+
//
|
|
1554
|
+
// The glance row exists to let a reader of a BATCH find the name they care about. With one name there
|
|
1555
|
+
// is nothing to index, and the row printed that name, its band and its classes for the first of three
|
|
1556
|
+
// times before the reader reached a single sentence of reading. On a batch it stays exactly as it was.
|
|
1557
|
+
const glance = marks.length > 1 ? glanceSection(marks, framework, registerCounts) : '';
|
|
1261
1558
|
const hasCounts = Boolean(registerCounts?.marks?.length);
|
|
1262
1559
|
// The register block is a table when counts were taken and a SENTENCE when they were not — never
|
|
1263
1560
|
// nothing. It renders in the place the numbers would have occupied, at the top of the section that
|
|
@@ -1266,7 +1563,24 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1266
1563
|
const counts = hasCounts
|
|
1267
1564
|
? countsSection(marks, registerCounts)
|
|
1268
1565
|
: `<div class="panel"><p class="ko-tier">${esc(tierLine)}</p></div>`;
|
|
1566
|
+
// ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED (tracker issue 331 E) ──
|
|
1567
|
+
//
|
|
1568
|
+
// The four model-written caveats on the measured run overlap the fixed text completely — 331 item 9,
|
|
1569
|
+
// and 333 rule 4 ("say it once") fixes it at the source for runs written under that doctrine. This is
|
|
1570
|
+
// the reader for the ones already written.
|
|
1571
|
+
//
|
|
1572
|
+
// THE TEST IS SUBSET, NOT SIMILARITY, and the difference is the whole safety argument. A caveat is
|
|
1573
|
+
// dropped only when EVERY content word in it already appears in the scope block. A caveat making any
|
|
1574
|
+
// new claim brings at least one new content word with it and is kept — so this cannot silently delete
|
|
1575
|
+
// a disclosure, which is the one failure that would matter on a client-facing page. A similarity score
|
|
1576
|
+
// could; that is why there isn't one.
|
|
1577
|
+
const newCaveats = caveats.filter((c) => saysSomethingNew(c, SCOPE_BLOCK_TEXT));
|
|
1269
1578
|
const analysis = analysisSection(marks, framework, { registerCounts, probeRan, registerRecords, ownerChecks });
|
|
1579
|
+
// The request-level notes, gathered across every mark on the document and rendered ONCE at the top.
|
|
1580
|
+
// They are about the asking, not about a name, so a batch repeating them per mark would be the same
|
|
1581
|
+
// sentence three times. reviewerNotesBlock renders the rest, under that mark's own cards.
|
|
1582
|
+
const requestNotes = marks.flatMap((m) => splitKnockoutNotes(m).request);
|
|
1583
|
+
const aboutRequest = aboutRequestBlock(instructedScope, requestNotes);
|
|
1270
1584
|
const provider = hasCounts ? (registerCounts.providerLabel ?? registerCounts.provider ?? 'the register') : null;
|
|
1271
1585
|
// The filings appendix renders only when the run produced a listing artifact — never on its absence,
|
|
1272
1586
|
// and never as an empty table. A knockout with no sidecar publishes exactly the counts-only document.
|
|
@@ -1277,7 +1591,10 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1277
1591
|
// promoted filing that sentence sent a reader to a row that does not exist. A false pointer on the
|
|
1278
1592
|
// audit trail is worse than no pointer, and the honest fix is to narrow the claim rather than mint a
|
|
1279
1593
|
// second drill-through key for a card whose receipt is the register record itself.
|
|
1280
|
-
|
|
1594
|
+
// Counted over what the page DRAWS: the sentence explains a REG reference a reader can see, so a run
|
|
1595
|
+
// whose register cards are all held back needs no sentence about them.
|
|
1596
|
+
const registerCardCount = marks.reduce((n, m) =>
|
|
1597
|
+
n + registerCardsOnPage(registerCardViews(m, framework, registerRecords).cards, m, framework).length, 0);
|
|
1281
1598
|
const hasRecords = Boolean(registerRecords && (registerRecords.marks?.length || registerRecords.unavailable));
|
|
1282
1599
|
const filings = hasRecords ? filingsSection(marks, registerRecords) : '';
|
|
1283
1600
|
const onFieldSec = `<div class="sec"><span class="num">${num()}</span><h2>On-field conflicts</h2><span class="note">${hasCounts ? `register hit-counts (${esc(provider)}) and the read, per name` : 'what we found, per name'}</span></div>
|
|
@@ -1338,6 +1655,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1338
1655
|
${depthStrip(depthNote)}
|
|
1339
1656
|
<h1 class="mark">${esc(title)}</h1>
|
|
1340
1657
|
${productContext ? `<p class="ko-classes" style="font-size:13.5px;margin:0 0 16px">${inlineMd(productContext)}</p>` : ''}
|
|
1658
|
+
${aboutRequest}
|
|
1341
1659
|
${summary ? `<div class="sub">${mdParagraphs(summary)}</div>` : ''}
|
|
1342
1660
|
</header>
|
|
1343
1661
|
|
|
@@ -1359,14 +1677,31 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1359
1677
|
// FROM THE POLICY WHERE THERE IS ONE (tracker issue 1935). The fallback is not a tidy default: an
|
|
1360
1678
|
// archived run froze no policy, and re-rendering it must not invent a scope claim about a run
|
|
1361
1679
|
// nobody can now ask. It keeps the sentence it was delivered with.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1680
|
+
// ── THE SCOPE BLOCK, WHOLE (tracker issue 331 E) ──────────────────────────────────────────────
|
|
1681
|
+
//
|
|
1682
|
+
// The composed lines said one thing five times in 362 words: "not a clearance" three times,
|
|
1683
|
+
// "proceeds to clearance" twice, and the count-basis sentence a second time after the counts table
|
|
1684
|
+
// had already carried it. This is the owner's replacement text, and its two halves are the two
|
|
1685
|
+
// questions a reader of a screen actually has.
|
|
1686
|
+
//
|
|
1687
|
+
// "Every conflict above links to the material we found" STAYS CONDITIONAL and that is not a
|
|
1688
|
+
// stylistic carry-over: said unconditionally it is an absence claim wider than what was examined,
|
|
1689
|
+
// which this file refuses elsewhere in the same words. It prints only when every rendered conflict
|
|
1690
|
+
// cites something.
|
|
1691
|
+
const linked = citedFindings && !uncitedFindings
|
|
1692
|
+
? ' Every conflict above links to the material we found.'
|
|
1693
|
+
: '';
|
|
1694
|
+
const source = registerCounts
|
|
1695
|
+
? ` Register data: ${registerCounts.providerLabel ?? registerCounts.provider ?? 'the register'}.`
|
|
1366
1696
|
: '';
|
|
1367
|
-
return
|
|
1697
|
+
return `<p class="ko-scope"><b>What this is.</b> A fast screen for obvious blockers to using each `
|
|
1698
|
+
+ `name, from marketplace and web use plus a count of register filings.</p>`
|
|
1699
|
+
+ `<p class="ko-scope"><b>What it is not.</b> A clearance search. We drew no register conclusions `
|
|
1700
|
+
+ `and give no filing advice. A name that passes here is not clear; it goes on to clearance.</p>`
|
|
1701
|
+
+ `<p class="ko-scope">${linked.trim()}${linked ? ' ' : ''}The audit workbook holds every search run, `
|
|
1702
|
+
+ `every empty result and the working notes.${source}</p>`;
|
|
1368
1703
|
})()}
|
|
1369
|
-
${
|
|
1704
|
+
${newCaveats.length ? `<p class="ko-scope" style="border-top:1px solid var(--line)">${CAVEAT_LEAD}<br>${newCaveats.map((c) => inlineMd(c)).join('<br>')}</p>` : ''}
|
|
1370
1705
|
${auditFile ? `<p class="ko-scope" style="border-top:1px solid var(--line)"><a href="${escAttr(auditFile)}">Download the audit workbook (Excel)</a> — every search run, every negative result, and the working notes behind these ratings.${
|
|
1371
1706
|
citedFindings ? ` The reference beside each common-law conflict above (for example <span class="mono">${esc(firstRef(marks, framework))}</span>) is its row on the workbook's Findings sheet.${
|
|
1372
1707
|
registerCardCount ? ` A <span class="mono">REG</span> reference is a register filing rather than a common-law conflict — it has no Findings row, and its receipt is the register record the card links.` : ''}` : ''}</p>` : ''}
|