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
|
@@ -68,7 +68,7 @@ function assessmentField(v) {
|
|
|
68
68
|
* "no data file this publish" and stamps meta.reportSchema only on success.
|
|
69
69
|
*/
|
|
70
70
|
export function clearanceReportData({
|
|
71
|
-
runId, codename, matter, markName, title, customerKey, issued, url, auditFile, engineCommit = null,
|
|
71
|
+
runId, codename, matter, markName, title, customerKey, issued, url, auditFile, engineCommit = null, servedModels = null,
|
|
72
72
|
searchLevel, stageLabel, framework, verdictInfo, findings, coverage, contextNotes,
|
|
73
73
|
markAssessment, fourAnswers, askAnswers, actions, jurisdiction, searchedJurisdictions, scopeBasis, caption,
|
|
74
74
|
} = {}) {
|
|
@@ -91,6 +91,9 @@ export function clearanceReportData({
|
|
|
91
91
|
issued: issued || null,
|
|
92
92
|
// The engine build that produced this report — the join from a flagged finding to a diff.
|
|
93
93
|
engineCommit: engineCommit || null,
|
|
94
|
+
// The models that served the run, in first-use order, as tokens.mjs servedModels names them for a
|
|
95
|
+
// client: a deployment's own name never, its tier instead. null when nothing was read.
|
|
96
|
+
servedModels: Array.isArray(servedModels) ? servedModels : null,
|
|
94
97
|
url: url || null,
|
|
95
98
|
auditFile: auditFile || null,
|
|
96
99
|
level: { searchLevel: searchLevel ?? null, stageLabel: stageLabel ?? null },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// report-topbar.mjs — THE EXPORT MENU, ONCE, FOR BOTH REPORT TEMPLATES.
|
|
4
|
+
//
|
|
5
|
+
// Two templates draw a report: the clearance renderer and the knockout renderer. Each emits its own top
|
|
6
|
+
// bar, which is right — the boards draw two different bars, with different items in a different order.
|
|
7
|
+
// What they must not each own is the EXPORT MENU: the toggle, the popover it opens, the aria wiring that
|
|
8
|
+
// says it is a menu, and the two listeners that open and close it. Those are one control, and until this
|
|
9
|
+
// module they were two copies of one control, in two files, with nothing to say when they drifted.
|
|
10
|
+
//
|
|
11
|
+
// ── WHAT IS SHARED AND WHAT IS NOT, AND WHY THE LINE IS THERE ───────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// The SHELL is shared: the button, the panel, the behaviour. The ENTRIES are not. The clearance report
|
|
14
|
+
// filters its export to the findings a reader has ticked, so its menu says so and offers a select-all;
|
|
15
|
+
// the knockout has no tick boxes at all, deliberately, because it has nothing to filter, and a menu
|
|
16
|
+
// offering to tick there would name a control that cannot exist. An entry list is a statement about what
|
|
17
|
+
// a template can do, and the two templates can do different things.
|
|
18
|
+
//
|
|
19
|
+
// ── THE BYTES DO NOT MOVE ───────────────────────────────────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// `driver/publish/render.mjs` is frozen at a content hash, and the freeze's checklist asks whether a
|
|
22
|
+
// change is reachable from a republish. This one is: pool-admin re-renders archived runs through that
|
|
23
|
+
// module. So the contract here is stricter than "it looks the same" — the composed markup is the bytes
|
|
24
|
+
// the two templates already emitted, character for character, and the commit that introduces this module
|
|
25
|
+
// records a real archived run rendered through both the old module and the new one and byte-compared, in
|
|
26
|
+
// the internal pass and the client pass. A shared definition that changed a delivered report while
|
|
27
|
+
// tidying it up would be the worst of both.
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The button that opens the export menu. One spelling, both templates.
|
|
31
|
+
*
|
|
32
|
+
* `aria-haspopup` and `aria-expanded` are part of the control rather than decoration: the listener below
|
|
33
|
+
* keeps `aria-expanded` in step, and a second copy of this button that forgot either would announce
|
|
34
|
+
* itself to a screen reader as an ordinary button that does nothing.
|
|
35
|
+
*/
|
|
36
|
+
export const EXPORT_TOGGLE = '<button type="button" class="tbbtn primary tb-exp-toggle" aria-haspopup="true" aria-expanded="false">⬇ <span class="tb-lbl">Export</span> ▾</button>';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The panel the toggle opens, around whatever entries a template offers. PURE.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} entries the template's own menu contents, already escaped
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export const exportPopover = (entries) => `<div class="tb-pop tb-exp-pop" hidden>${entries}</div>`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Open on the button, close on a click outside and on Escape.
|
|
48
|
+
*
|
|
49
|
+
* GLOBAL, NOT WRAPPED, and that is the same reason the templates' own verbs are global: the portal
|
|
50
|
+
* frames a served report and drives it by looking names up on the page. These two listeners need no
|
|
51
|
+
* name, but they sit in the same script as the verbs that do, and a wrapper around the pair would be one
|
|
52
|
+
* more difference between the two files for no gain.
|
|
53
|
+
*
|
|
54
|
+
* NO BACKTICK IN THIS STRING. It is interpolated into a template literal in both templates, and a
|
|
55
|
+
* backtick ends that literal — the failure arrives at import time naming a token nobody wrote.
|
|
56
|
+
*/
|
|
57
|
+
export const EXPORT_MENU_JS = `document.addEventListener('click',function(e){var t=e.target.closest('.tb-exp-toggle'),pop=document.querySelector('.tb-exp-pop');if(t){if(pop){pop.hidden=!pop.hidden;t.setAttribute('aria-expanded',String(!pop.hidden));}return;}if(pop&&!pop.hidden&&!e.target.closest('.tb-exp-pop')){pop.hidden=true;var b=document.querySelector('.tb-exp-toggle');if(b)b.setAttribute('aria-expanded','false');}});
|
|
58
|
+
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');}}});`;
|
|
@@ -92,7 +92,29 @@ export function clearedNames(auditMd, recordIndex = {}) {
|
|
|
92
92
|
uri,
|
|
93
93
|
});
|
|
94
94
|
} else if (!/^NR\d+/.test(title) && /common-law/i.test(layer) && !/^\(none/i.test(title)) {
|
|
95
|
-
|
|
95
|
+
// ── A NAME IS A NAME AT A PLACE. THE READINGS ARE NOT NAMES ─────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// The common-law layer carries two kinds of block under one heading style: a name somebody is
|
|
98
|
+
// trading under, and a reading of what the mark MEANS. The first has a `url` — it is a thing at a
|
|
99
|
+
// place a reader can go and look at. The second has none, because there is nothing to open: it is
|
|
100
|
+
// an etymology, a sensitivity, a piece of context.
|
|
101
|
+
//
|
|
102
|
+
// Both were listed as "Web and marketplace names", and a reading is a sentence, so the mark chip
|
|
103
|
+
// built for a name truncated it with an ellipsis and its right-hand column rendered empty.
|
|
104
|
+
// Measured on the delivered full country report: one such chip held 416px of content in a 200px
|
|
105
|
+
// box. Across every demo product, 7 of 31 blocks carried no url and every one of the 7 was a
|
|
106
|
+
// reading rather than a name.
|
|
107
|
+
//
|
|
108
|
+
// NOTHING IS LOST BY LEAVING THEM OUT, and that was measured rather than assumed: the report
|
|
109
|
+
// already carries each of those readings in the section written for them — the Quechua one appears
|
|
110
|
+
// five more times in the same document, under "Connotation & meaning" and again in the decision it
|
|
111
|
+
// asks the reader to take — and the audit workbook carries every block whatever this does.
|
|
112
|
+
//
|
|
113
|
+
// WHAT IT COSTS, stated rather than hidden: a marketplace name sighted with no URL recorded would
|
|
114
|
+
// not be listed here. None exists in any demo, and the row such a block produced was already a
|
|
115
|
+
// name beside an empty column. If one appears, the fix is to record where it was seen.
|
|
116
|
+
const url = field(block, "url");
|
|
117
|
+
if (url) out.web.push({ title, url, type: field(block, "type") });
|
|
96
118
|
}
|
|
97
119
|
}
|
|
98
120
|
return out;
|
|
@@ -104,9 +126,18 @@ export function clearedNames(auditMd, recordIndex = {}) {
|
|
|
104
126
|
* EVERY COUNTRY THE RUN READ, including the ones that came back clean — those are the whole point. A
|
|
105
127
|
* count keyed off the findings would list only countries with a conflict, which is the gap this closes.
|
|
106
128
|
*
|
|
107
|
-
*
|
|
129
|
+
* THREE-VALUED, in the house pattern outputMeta already uses for a stage's output: `null` in means the
|
|
130
|
+
* run has NO `_records/` store, and `null` comes back out — we cannot say how many records were read.
|
|
131
|
+
* An empty ARRAY is the other thing entirely: the store is there and holds nothing, which is a real zero
|
|
132
|
+
* and renders as one. Collapsing the two is what this fixes; they arrived here as the same `[]` and the
|
|
133
|
+
* renderer could only drop the section, so a register that archives nothing read as a register nobody
|
|
134
|
+
* searched.
|
|
135
|
+
*
|
|
136
|
+
* @param {string[]|null} recordFileNames the `_records/` listing, named `<cc>-<id>.json`; null = no store
|
|
137
|
+
* @returns {object|null} counts by country code, or null when the run cannot say
|
|
108
138
|
*/
|
|
109
139
|
export function recordsByCountry(recordFileNames = []) {
|
|
140
|
+
if (recordFileNames === null) return null;
|
|
110
141
|
const out = {};
|
|
111
142
|
for (const name of recordFileNames) {
|
|
112
143
|
const cc = (String(name).match(/^([a-z]{2})-/i) || [])[1];
|
|
@@ -115,6 +146,16 @@ export function recordsByCountry(recordFileNames = []) {
|
|
|
115
146
|
return out;
|
|
116
147
|
}
|
|
117
148
|
|
|
149
|
+
/** Band record ids → `<office>-<id>` names, one per distinct record, in the archive's own naming. PURE. */
|
|
150
|
+
export function recordNamesFromIds(ids) {
|
|
151
|
+
const out = new Set();
|
|
152
|
+
for (const id of ids ?? []) {
|
|
153
|
+
const m = /^\/mark\/([a-z]{2,4})\/(.+)$/i.exec(String(id ?? ""));
|
|
154
|
+
if (m) out.add(`${m[1].toLowerCase()}-${m[2]}`);
|
|
155
|
+
}
|
|
156
|
+
return [...out];
|
|
157
|
+
}
|
|
158
|
+
|
|
118
159
|
/** Marketplace, web, reputation and meaning checks, from the deterministic grid the tools wrote. PURE. */
|
|
119
160
|
export function sweepCounts(commonLawGrid, auditMd = "") {
|
|
120
161
|
const cells = Array.isArray(commonLawGrid?.cells) ? commonLawGrid.cells : [];
|
|
@@ -148,6 +189,81 @@ export function courtDecisionsState(caseLawText) {
|
|
|
148
189
|
return "found";
|
|
149
190
|
}
|
|
150
191
|
|
|
192
|
+
/**
|
|
193
|
+
* WHICH TERRITORIES THE RUN SEARCHED, AND WHICH IT COULD NOT REACH — from the register PLAN. PURE.
|
|
194
|
+
*
|
|
195
|
+
* The plan is the authority on what was asked of the register; the `_records/` archive is only the
|
|
196
|
+
* authority on what came back and was kept. Reading "what was searched" off the archive is why a
|
|
197
|
+
* provider that keeps no records read as a provider nobody asked: no records, no countries, no section.
|
|
198
|
+
* Both halves are on the plan whether or not anything is archived — `entries[].regions` is what it will
|
|
199
|
+
* query, and `deferred_coverage` is what this provider does not cover, carrying the reason for each.
|
|
200
|
+
*
|
|
201
|
+
* Null for a run with no plan to read, which is an archived or legacy run: that is "cannot say", and it
|
|
202
|
+
* is not the same answer as a plan that named nothing.
|
|
203
|
+
*
|
|
204
|
+
* @param {object|null} plan the parsed `register-plan.json`, or null when there is none
|
|
205
|
+
*/
|
|
206
|
+
export function planTerritoriesOf(plan) {
|
|
207
|
+
if (!plan || typeof plan !== "object") return null;
|
|
208
|
+
const entryRegions = (plan.entries ?? []).flatMap((e) => (Array.isArray(e?.regions) ? e.regions : []));
|
|
209
|
+
// `plan.regions` is the older shape and is the fallback, not a second source: a plan carrying entries
|
|
210
|
+
// has already said which regions it will query, and unioning the two would report a region the
|
|
211
|
+
// compiler moved OUT of `regions` into the deferral list as though it had been searched.
|
|
212
|
+
const searched = entryRegions.length ? [...new Set(entryRegions.map(String))]
|
|
213
|
+
: [...new Set((Array.isArray(plan.regions) ? plan.regions : []).map(String))];
|
|
214
|
+
// A WORLDWIDE PLAN THAT NAMES NO REGION CANNOT SAY WHERE IT REACHED. On a provider that takes no region
|
|
215
|
+
// list, worldwide compiles to queries with no jurisdiction clause at all — the whole database — so the
|
|
216
|
+
// plan names nothing, and read as "searched these" that is "searched nowhere": the section that says
|
|
217
|
+
// where a search reached disappeared on exactly the searches that reached furthest. Null is the answer
|
|
218
|
+
// the plan can actually give, and the page then reads the countries off what the register returned.
|
|
219
|
+
// Only when nothing was deferred: a worldwide order with deferrals is not an unrestricted sweep.
|
|
220
|
+
if (plan.scope_basis === "worldwide" && !searched.length
|
|
221
|
+
&& !(Array.isArray(plan.deferred_coverage) && plan.deferred_coverage.length)) return null;
|
|
222
|
+
const unreached = (Array.isArray(plan.deferred_coverage) ? plan.deferred_coverage : [])
|
|
223
|
+
.map((d) => ({ jurisdiction: String(d?.jurisdiction ?? "").trim(), reason: String(d?.reason ?? "").trim() }))
|
|
224
|
+
.filter((d) => d.jurisdiction);
|
|
225
|
+
return { searched, unreached };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* HOW DEEP THE LOCAL-LANGUAGE INVESTIGATION ACTUALLY WENT, against what the matter configured. PURE.
|
|
230
|
+
*
|
|
231
|
+
* The engine can run this investigation shallower than the account asked for, and until now it said so
|
|
232
|
+
* in exactly one place: a sentence a model wrote in the Methodology paragraph. The redesigned report
|
|
233
|
+
* replaces that paragraph with counts and named rows, so a run that went shallow said so on no page at
|
|
234
|
+
* all. This is the field behind that row.
|
|
235
|
+
*
|
|
236
|
+
* DERIVED FROM THE RUN'S OWN RECORD, NEVER FROM PROSE, and not derived here either: the caller hands in
|
|
237
|
+
* what `deriveLaneDepthVerdicts` produced, which is the one author of asked-versus-ran and reads the
|
|
238
|
+
* frozen lane sidecar against the slices that executed. A second opinion computed in the publish path
|
|
239
|
+
* would be a second answer to a question the engine has already answered.
|
|
240
|
+
*
|
|
241
|
+
* THE FOUR STATES, and the order they are decided in matters:
|
|
242
|
+
* not-in-scope no lane was asked for anything — a plain clearance, or every lane switched off
|
|
243
|
+
* not-run lanes were asked and none of them ran
|
|
244
|
+
* ran-shallow a lane fell short of its ask, or was asked and did not run while another did
|
|
245
|
+
* ran every lane that was asked ran at the depth it was asked for
|
|
246
|
+
*
|
|
247
|
+
* `ran: null` IS NOT `candidates`. A lane whose slices settle to nothing readable cannot say what it
|
|
248
|
+
* delivered, and the jx verdicts are careful to report that as unestablished rather than as the lesser
|
|
249
|
+
* depth. Folding it to `ran` here would put that claim back on a client's page, so it counts as short.
|
|
250
|
+
*
|
|
251
|
+
* @param {object|null} verdicts per-lane `{asked, ran, shortfall}` from deriveLaneDepthVerdicts
|
|
252
|
+
*/
|
|
253
|
+
export function localLanguageDepth(verdicts) {
|
|
254
|
+
if (!verdicts || typeof verdicts !== "object") return { state: "not-in-scope", lanes: {} };
|
|
255
|
+
const lanes = {};
|
|
256
|
+
for (const [lane, v] of Object.entries(verdicts)) {
|
|
257
|
+
lanes[lane] = { configured: v?.asked ?? null, achieved: v?.ran ?? null };
|
|
258
|
+
}
|
|
259
|
+
const asked = Object.entries(verdicts).filter(([, v]) => v?.asked && v.asked !== "off");
|
|
260
|
+
if (!asked.length) return { state: "not-in-scope", lanes };
|
|
261
|
+
const ran = asked.filter(([, v]) => v?.ran);
|
|
262
|
+
if (!ran.length) return { state: "not-run", lanes };
|
|
263
|
+
const short = asked.some(([, v]) => v?.shortfall === true || !v?.ran);
|
|
264
|
+
return { state: short ? "ran-shallow" : "ran", lanes };
|
|
265
|
+
}
|
|
266
|
+
|
|
151
267
|
/** Was the name searched in a non-Latin script? Read off the plan's own terms, never asserted. PURE. */
|
|
152
268
|
export function localScriptSearched(registerPlan) {
|
|
153
269
|
const entries = Array.isArray(registerPlan?.entries) ? registerPlan.entries : [];
|
|
@@ -159,7 +275,16 @@ export function localScriptSearched(registerPlan) {
|
|
|
159
275
|
*
|
|
160
276
|
* @returns {{schemaVersion: number, cleared: object, counts: object}}
|
|
161
277
|
*/
|
|
162
|
-
export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNames = [], commonLawGrid = null, caseLawText = "", registerPlan = null } = {}) {
|
|
278
|
+
export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNames = [], bandRecordIds = null, commonLawGrid = null, caseLawText = "", registerPlan = null, laneDepthVerdicts = null } = {}) {
|
|
279
|
+
// `recordFileNames: null` travels all the way to the page — see recordsByCountry. The default stays `[]`
|
|
280
|
+
// because that is "the caller said nothing", not "the store is absent"; only the publish path knows the
|
|
281
|
+
// difference and it is the one producer.
|
|
282
|
+
//
|
|
283
|
+
// A PROVIDER THAT ARCHIVES NO RECORDS STILL RETURNED THEM. Its search answer is the band, and every
|
|
284
|
+
// record in it carries its office in its own id (`/mark/<office>/<id>`). So where there is no archive
|
|
285
|
+
// the band is what was read: counted by record, filed by office. Without it a run that read 785 register
|
|
286
|
+
// records reported "cannot say" and the report carried no register row and no country at all.
|
|
287
|
+
if (recordFileNames === null && Array.isArray(bandRecordIds)) recordFileNames = recordNamesFromIds(bandRecordIds);
|
|
163
288
|
const cleared = clearedNames(auditMd, recordIndex);
|
|
164
289
|
const groups = {};
|
|
165
290
|
for (const key of CLEARED_GROUPS) groups[key] = 0;
|
|
@@ -169,10 +294,14 @@ export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNa
|
|
|
169
294
|
cleared: { register: cleared.register, web: cleared.web, groups },
|
|
170
295
|
counts: {
|
|
171
296
|
recordsByCountry: recordsByCountry(recordFileNames),
|
|
172
|
-
recordsRead: recordFileNames.length,
|
|
297
|
+
recordsRead: recordFileNames === null ? null : recordFileNames.length,
|
|
173
298
|
sweep: sweepCounts(commonLawGrid, auditMd),
|
|
174
299
|
localScriptSearched: localScriptSearched(registerPlan),
|
|
175
300
|
courtDecisions: courtDecisionsState(caseLawText),
|
|
301
|
+
// `localScriptSearched` above answers whether the spellings were searched; this answers how deep
|
|
302
|
+
// the investigation went against what was configured. Two different facts, and the row the report
|
|
303
|
+
// reserves is for the second.
|
|
304
|
+
localLanguage: localLanguageDepth(laneDepthVerdicts),
|
|
176
305
|
},
|
|
177
306
|
};
|
|
178
307
|
}
|
|
@@ -113,6 +113,39 @@
|
|
|
113
113
|
.panel.actions p,.panel.actions li{font-size:13.5px;color:var(--slate);line-height:1.55}
|
|
114
114
|
.panel.actions ul{margin:0;padding-left:20px} .panel.actions li{margin:3px 0}
|
|
115
115
|
|
|
116
|
+
/* ── THE BOARD'S OWN SECTION STRIP, lifted from the approved mock rather than written here ────────
|
|
117
|
+
Five entries and the board's own labels, marked not to print because the board marks it so: a strip
|
|
118
|
+
that follows the reader down the page is a control, and paper has nothing for it to follow. Every
|
|
119
|
+
anchor it names is drawn by the renderer above it.
|
|
120
|
+
|
|
121
|
+
THE SECTION NUMBER IS DRAWN AND HIDDEN, and that is the board's doing, not an oversight here: the
|
|
122
|
+
mock emits `<span class="num"></span>` on every section and then sets `display:none` on it further
|
|
123
|
+
down its own stylesheet. Carried faithfully, so the delivered document and the approved one hold the
|
|
124
|
+
same elements — this changes no pixel, and a reader looking for a visible number will not find one
|
|
125
|
+
in the board either. */
|
|
126
|
+
/* THE BREADCRUMB IS A ROW OF THE HEADER, not a bar under it (owner, 2026-09-18).
|
|
127
|
+
It used to be a SIBLING of .rep-stickyhead, pinned on its own at top:var(--tb-h,52px) — and
|
|
128
|
+
--tb-h is set nowhere in this product, so the fallback was a guess at a bar that measures ~46px:
|
|
129
|
+
content showed through the slit between the two, and z-index:20 put the strip UNDER the header's
|
|
130
|
+
100 whenever the guess was wrong. The strip is now emitted inside .rep-stickyhead, so the header
|
|
131
|
+
and the breadcrumb are one sticky surface that pins and unpins together and can never gap or
|
|
132
|
+
overlap, whatever the bar measures or how it wraps.
|
|
133
|
+
No background and no bottom hairline of its own: the wrapper carries the blurred surface and the
|
|
134
|
+
single edge, and the border-top here is the divider between the two rows. It is on .strip rather
|
|
135
|
+
than on any wrapper because sectionStrip() emits NOTHING when fewer than two of its sections are
|
|
136
|
+
live — a wrapper would leave a stray line on those reports.
|
|
137
|
+
The gutter matches the topbar's, so the first entry lines up under the back button. */
|
|
138
|
+
.strip{display:flex;gap:4px;align-items:center;padding:4px max(26px,calc((100% - 1120px)/2)) 6px;
|
|
139
|
+
border-top:1px solid var(--line);font:600 12px/1 'Satoshi','Helvetica Neue',Arial,sans-serif;
|
|
140
|
+
letter-spacing:.04em;overflow-x:auto;scrollbar-width:none}
|
|
141
|
+
.strip::-webkit-scrollbar{display:none}
|
|
142
|
+
.strip a{display:inline-flex;align-items:center;gap:7px;padding:7px 10px;border-radius:999px;color:#6b5d50;text-decoration:none;white-space:nowrap}
|
|
143
|
+
.strip a i{width:8px;height:8px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box}
|
|
144
|
+
.strip a.done{color:#4c7a4c}
|
|
145
|
+
.strip a.done i{background:#4c7a4c;border-color:#4c7a4c}
|
|
146
|
+
.strip a.now{color:#250902;background:rgba(0,0,0,.06)}
|
|
147
|
+
.strip a.now i{border-color:#860F09;border-width:3px}
|
|
148
|
+
.sec .num{display:none}
|
|
116
149
|
.sec{margin:56px 0 18px;display:flex;align-items:baseline;gap:15px;border-top:2px solid var(--ink);padding-top:14px}
|
|
117
150
|
.sec .num{font-family:var(--mono);font-size:13px;color:var(--crimson);font-weight:600}
|
|
118
151
|
.sec h2{font-weight:900;font-size:25px;margin:0;letter-spacing:-.015em}
|
|
@@ -239,7 +272,19 @@
|
|
|
239
272
|
.tb-hint{font-size:11px;color:var(--faint);line-height:1.4;margin:2px 2px 0}
|
|
240
273
|
@media(max-width:860px){.tb-matter{display:none}}
|
|
241
274
|
@media(max-width:640px){.tb-issued{display:none}}
|
|
242
|
-
@media(max-width:560px){.wm small{display:none}.tb-back-lbl{display:none}.tb-lbl{display:none}
|
|
275
|
+
@media(max-width:560px){.wm small{display:none}.tb-back-lbl{display:none}.tb-lbl{display:none}
|
|
276
|
+
/* THE SAME ROW, CLOSER TOGETHER — the ladder above already collapses this bar's LABELS at these
|
|
277
|
+
widths, and it was still 35 to 51px wider than a phone screen, so the whole document scrolled
|
|
278
|
+
sideways: the reader dragged the page left and right to read a paragraph. Nothing is hidden and
|
|
279
|
+
nothing moves; the gutter, the gaps and one dead margin come in. The margin is dead here by the
|
|
280
|
+
line above — it separated the wordmark from a tagline that is no longer displayed. */
|
|
281
|
+
.topbar{gap:4px;padding-left:12px;padding-right:12px}}
|
|
282
|
+
/* AND ON A SMALL PHONE, ONE THING GOES. Closing 375px and below is not a spacing problem — the row's
|
|
283
|
+
own items are wider than the screen — so something has to go, and which one is a decision about what
|
|
284
|
+
a reader keeps rather than a number to tune. Ruled: the back arrow. The product's name and the risk
|
|
285
|
+
label stay, because they say where the reader is and what the answer was; the arrow is a route back
|
|
286
|
+
to a list, and a reader on a phone has the browser's own. (owner, 2026-09-17) */
|
|
287
|
+
@media(max-width:375px){.topbar .tb-back{display:none}}
|
|
243
288
|
|
|
244
289
|
/* hero: conclusion card (dial + verdict) + scope card jurisdiction chips */
|
|
245
290
|
.heroGrid .gauge{display:flex;flex-direction:column;justify-content:flex-start}
|
|
@@ -268,7 +313,26 @@
|
|
|
268
313
|
|
|
269
314
|
|
|
270
315
|
/* cross-region rights-holder rows (.rrow) + secondary region groups (.rgroup) */
|
|
316
|
+
/* A PANEL THAT SCROLLS SIDEWAYS SAYS SO, the same way the knockout's tables do.
|
|
317
|
+
`overflow-y:auto` alone makes the OTHER axis compute to auto as well, so this panel has scrolled
|
|
318
|
+
horizontally for as long as it has scrolled vertically — and drawn nothing to say it. Measured on a
|
|
319
|
+
delivered global preliminary at 390px: 377px of rows in a 336px box, 41px unreachable, no bar.
|
|
320
|
+
A reader who cannot see that it drags does not drag it, and a holder's jurisdiction sits in the part
|
|
321
|
+
that is off the edge.
|
|
322
|
+
|
|
323
|
+
THE TWO INSTRUCTIONS CANNOT SIT TOGETHER, and that was measured rather than reasoned on the
|
|
324
|
+
knockout: setting the standard scrollbar-width alongside the pseudo-elements makes the engine take
|
|
325
|
+
the standard path and ignore them, and on the engine that publishes these reports the standard path
|
|
326
|
+
draws an overlay bar with no layout height at all — both together 0px, the pseudo-elements alone
|
|
327
|
+
9px, the standard properties alone 0px. So the standard ones sit behind a support query the engines
|
|
328
|
+
carrying the pseudo-elements never enter, and each engine gets exactly one instruction. Written the
|
|
329
|
+
way anyone would write it — belt and braces — it renders nothing and the stylesheet gives no sign. */
|
|
271
330
|
.keypanel{overflow-y:auto}
|
|
331
|
+
.keypanel::-webkit-scrollbar{height:9px;-webkit-appearance:none}
|
|
332
|
+
.keypanel::-webkit-scrollbar-track{background:transparent}
|
|
333
|
+
.keypanel::-webkit-scrollbar-thumb{background:var(--faint);border-radius:5px}
|
|
334
|
+
@supports not selector(::-webkit-scrollbar){
|
|
335
|
+
.keypanel{scrollbar-width:thin;scrollbar-color:var(--faint) transparent}}
|
|
272
336
|
.rrow{border-top:1px solid var(--line)}
|
|
273
337
|
.rrow:first-of-type{border-top:0}
|
|
274
338
|
.rrow>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:9px;padding:8px 4px}
|
|
@@ -332,9 +396,11 @@
|
|
|
332
396
|
and internal notes (.int-note, hidden above) are the only two things print ever drops. */
|
|
333
397
|
details>*:not(summary){display:block!important}
|
|
334
398
|
details>summary{list-style:none;cursor:default}
|
|
335
|
-
/* `details.searched`
|
|
336
|
-
|
|
337
|
-
|
|
399
|
+
/* `details.searched` keeps its summary in print like the region rows and the secondary groups. The
|
|
400
|
+
reason has CHANGED and the behaviour has not: the summary used to carry the section's own heading,
|
|
401
|
+
so dropping it in print lost the heading over the counts. The heading is now an <h2> in a section
|
|
402
|
+
of its own, as the approved board draws it, and print cannot lose it. What the summary still
|
|
403
|
+
carries is the sub-label over the counts, which is worth printing on its own. What it must not print is the ▸/▾ toggle, which on paper is a
|
|
338
404
|
right-pointing arrow over content that is already fully expanded. Found by the print check, which
|
|
339
405
|
walks a rendered page and refuses any disclosure that is in neither list; the public suite does
|
|
340
406
|
not run it, so the commit that added this fold could ship the half it did not think about. */
|
|
@@ -507,9 +573,17 @@
|
|
|
507
573
|
details.searched[open]>summary::before{content:'▾'}
|
|
508
574
|
details.searched .gcount{margin-left:auto;color:var(--slate);font-weight:500;font-size:12.5px}
|
|
509
575
|
details.searched .gbody{padding:0 18px 12px}
|
|
576
|
+
.wchips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
577
|
+
details.searched .wstate{font-weight:600}
|
|
578
|
+
details.searched .wnote{display:block;margin-top:4px;font-size:12.5px;line-height:1.45;color:var(--slate)}
|
|
510
579
|
details.searched .row{display:grid;grid-template-columns:200px 1fr;gap:12px;padding:7px 0;border-top:1px solid var(--line);font-size:13.5px}
|
|
511
580
|
details.searched .k{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
512
581
|
.openrows,.provwrap{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}
|
|
513
582
|
.openrows .rk,.provwrap .rk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-bottom:6px}
|
|
514
583
|
.provnote{margin:0;font-size:12.5px;line-height:1.5;color:var(--slate)}
|
|
515
584
|
.topbar .tb-lockup{margin-right:10px;flex:0 0 auto}
|
|
585
|
+
/* AFTER the rule it overrides, not beside its siblings at the breakpoint above: both selectors carry
|
|
586
|
+
the same weight, so the later one wins and a phone rule written earlier in this file did nothing at
|
|
587
|
+
all — measured, not assumed. The margin separates the wordmark from the tagline, and the tagline is
|
|
588
|
+
already hidden at this width. */
|
|
589
|
+
@media(max-width:560px){.topbar .tb-lockup{margin-right:0}}
|
package/driver/publish/xlsx.mjs
CHANGED
|
@@ -618,7 +618,26 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
|
|
|
618
618
|
});
|
|
619
619
|
|
|
620
620
|
// 4 · Coverage & gaps — the honest completeness ledger, state coloured.
|
|
621
|
-
|
|
621
|
+
// ── A CONDITION THAT REACHED NO PAGE IS A GAP, AND IT BELONGS ON THE GAPS SHEET ──────────────────
|
|
622
|
+
//
|
|
623
|
+
// A report republished from a run recorded before conditions carried two texts can hold a condition
|
|
624
|
+
// whose reader-facing sentence was never stored and cannot be composed. It is dropped rather than
|
|
625
|
+
// printed in the engine's own words, and a drop nobody records is the disclosure closing quietly —
|
|
626
|
+
// which is worse than the sentence it replaced. Ruled 2026-09-17: one row here, one line in the run
|
|
627
|
+
// record, no new wording, and delivery never fails for it.
|
|
628
|
+
//
|
|
629
|
+
// THE ROW IS AN ORDINARY COVERAGE ROW, built by the same function as every other, so it carries the
|
|
630
|
+
// same four columns and takes the same State colour. It is not a second shape and not a new sheet.
|
|
631
|
+
// The words in it are the run record's own: nothing here composes prose.
|
|
632
|
+
//
|
|
633
|
+
// AND THE PROBES THE RUN DECIDED ON AND DID NOT MAKE, on the same footing. The recall net's owner
|
|
634
|
+
// budget drops its excess with the party and the probe id recorded; until this row the excess reached
|
|
635
|
+
// no reader, so a search that decided on nineteen ownership checks and made five read as one that
|
|
636
|
+
// made the checks it wanted. Same builder, same four columns, same State colour: this is not a second
|
|
637
|
+
// shape and not a new sheet, and the words are the run receipt's own.
|
|
638
|
+
addSheet(wb, 'Coverage & gaps', COVERAGE_COLS,
|
|
639
|
+
[...coverageRows(coverage), ...coverageRows(contract?.droppedConditions || []),
|
|
640
|
+
...coverageRows(contract?.undispatchedProbes || [])], (row, _d, kept) => {
|
|
622
641
|
if (!kept.has('State')) return;
|
|
623
642
|
const st = row.getCell('State'); const f = STATE_FILL[String(st.value).trim()];
|
|
624
643
|
if (f) { st.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF' + f } }; st.font = { bold: true }; }
|
package/driver/queue-order.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// parse set, or prelim-driver.path's watch — and reordering must NOT wake the runner.
|
|
22
22
|
|
|
23
23
|
import { readFileSync, writeFileSync, renameSync, mkdirSync, readdirSync } from "node:fs";
|
|
24
|
-
import { join, dirname } from "node:path";
|
|
24
|
+
import { join, dirname } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
25
25
|
|
|
26
26
|
export function queueOrderPath(qdir) { return join(dirname(qdir), ".queue-order.json"); }
|
|
27
27
|
|
|
@@ -69,7 +69,7 @@ export function queueDirsUnder(workspaceRoot) {
|
|
|
69
69
|
try { names = readdirSync(workspaceRoot); } catch { return out; }
|
|
70
70
|
for (const n of names) {
|
|
71
71
|
if (!n.startsWith("workspace-")) continue;
|
|
72
|
-
const q = join(workspaceRoot, n, "
|
|
72
|
+
const q = join(studioDirFor(join(workspaceRoot, n)), "queue");
|
|
73
73
|
try { readdirSync(q); out.push(q); } catch { /* no queue in this workspace */ }
|
|
74
74
|
}
|
|
75
75
|
return out.sort();
|
|
@@ -70,7 +70,7 @@ export const SURFACE = "surface";
|
|
|
70
70
|
* performs anyway. Since the warm and cold rungs DERIVE the tool name from `TOOL_WRITTEN_ARTIFACTS`,
|
|
71
71
|
* so adding that row — step 1 of every conversion — makes both rungs name the record tool, and (a) went
|
|
72
72
|
* quiet for it whatever the dispatch said. Measured on conversion 3 before its dispatch was touched: the
|
|
73
|
-
* grant carried `
|
|
73
|
+
* grant carried `record_clearance_variants`, the dispatch did not mention it, both repair rungs did, and (a)
|
|
74
74
|
* was silent. The guard asked a question the conversion's own bookkeeping answered — the tautology shape
|
|
75
75
|
* `b04d6d58` and the `RECORDING_TOOLS` non-derivation both exist to remove.
|
|
76
76
|
*
|
|
@@ -1107,7 +1107,7 @@ export function planSubQueries({ plan = null, execution = null, scopeTerritories
|
|
|
1107
1107
|
// ── THE IN-SCOPE SWEEP'S OWN STATE — what decides whether a narrow was OWED at all ───────────────
|
|
1108
1108
|
//
|
|
1109
1109
|
// `subQueryState` returns `none` for a territory with no entry of its own, and that one value covers
|
|
1110
|
-
// two opposite situations. The doctrine (`skills/
|
|
1110
|
+
// two opposite situations. The doctrine (`skills/clearance-register/SKILL.md`, Recipe 1 §2b) says a slice
|
|
1111
1111
|
// gets its own `register_enumerate` ONLY on the guarded crowd-narrow path — when Step 2, the
|
|
1112
1112
|
// region-scoped in-scope sweep, returned `incomplete` and a major may sit in the un-paged remainder.
|
|
1113
1113
|
// When Step 2 returns `enumerated` the complete set provably contains every in-scope slice and the
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs:
|
|
44
|
+
// (register-plan.mjs:779 compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
|
-
// `error:true && deferred:true` (register-plan.mjs:
|
|
46
|
+
// `error:true && deferred:true` (register-plan.mjs:1317 extendRegisterPlan).
|
|
47
47
|
//
|
|
48
48
|
// The consequence was not academic. `deferred_coverage` rode the plan and nothing that a reader sees
|
|
49
49
|
// read it: coverage-form.mjs seeded its deferred rows from skeleton qids alone, so an EU+US matter on a
|
|
@@ -336,10 +336,24 @@ export async function countRegisterHits({
|
|
|
336
336
|
try { r = await counter(term, p, { classes: scoped, regions }); }
|
|
337
337
|
catch (e) { r = { ok: false, total: null, reason: `count threw: ${String(e?.message ?? e).slice(0, 200)}` }; }
|
|
338
338
|
const ok = Boolean(r?.ok) && Number.isFinite(r?.total);
|
|
339
|
+
// A DISCLOSED APPROXIMATION IS AN ANSWER, AND IT WAS BEING READ AS A FAILURE. When the register
|
|
340
|
+
// says "more than ten thousand", it has answered — it has answered with a floor instead of a
|
|
341
|
+
// count, which is a different thing from not answering at all. `ok` is false for it because the
|
|
342
|
+
// total is deliberately not finite (an approximation must never become a number), so everything
|
|
343
|
+
// downstream saw a dead probe and the client was told the count was not available: the same words
|
|
344
|
+
// the report uses when the register could not be reached. The direction was inverted, too —
|
|
345
|
+
// saturation is a finding about the mark, and the denser the crowd the more certainly it was
|
|
346
|
+
// suppressed.
|
|
347
|
+
const approximated = !ok && Boolean(r?.ok) && r?.approximate === true && Number.isFinite(r?.floor);
|
|
348
|
+
// THE REFUSAL FIELD IS `cause` ON THIS PROVIDER AND `reason` ON OTHERS, and reading only one of
|
|
349
|
+
// them is why an honest refusal arrived as the fallback string with the receipts ledger recording
|
|
350
|
+
// "unknown". The same mismatch is described above `listRecords`, where it was fixed; this is the
|
|
351
|
+
// other half of it. Both are read, in the order a caller would expect.
|
|
352
|
+
const refusal = r?.reason ?? r?.cause ?? null;
|
|
339
353
|
// A CLIENT-SIDE refusal (the provider's query language cannot express this question — a term
|
|
340
354
|
// carrying parentheses, an office outside its vocabulary) is
|
|
341
355
|
// deterministic: no retry and no resume can change it, so it settles rather than re-billing.
|
|
342
|
-
const deterministic = !ok && (isCapabilityGap(
|
|
356
|
+
const deterministic = !ok && !approximated && (isCapabilityGap(refusal) || Boolean(r?.unsupported));
|
|
343
357
|
if (ledgerPath) {
|
|
344
358
|
try {
|
|
345
359
|
appendFileSync(ledgerPath, JSON.stringify({
|
|
@@ -350,13 +364,18 @@ export async function countRegisterHits({
|
|
|
350
364
|
...(form ? { term, variant_form: form } : {}),
|
|
351
365
|
classes: scoped, regions, provider, probe: r?.probe ?? null,
|
|
352
366
|
ok, total: ok ? r.total : null, took_ms: Date.now() - started,
|
|
353
|
-
|
|
367
|
+
// An approximation is recorded as what it is. It billed and it answered, so a receipt
|
|
368
|
+
// calling it a failure with cause "unknown" misreports both halves.
|
|
369
|
+
...(approximated ? { approximate: true, floor: r.floor } : {}),
|
|
370
|
+
...(ok || approximated ? {} : { cause: String(refusal ?? "unknown").slice(0, 300) }),
|
|
354
371
|
}) + "\n");
|
|
355
372
|
} catch { /* receipts are best-effort, never fatal — same as the sweep ledger */ }
|
|
356
373
|
}
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
if (ok) return { total: r.total };
|
|
375
|
+
// `total` stays null for an approximation and that is the rule, not an oversight: the floor is not
|
|
376
|
+
// a count and may never be filled in as one. What travels beside it is the disclosure.
|
|
377
|
+
if (approximated) return { total: null, approximate: true, floor: r.floor };
|
|
378
|
+
return { total: null, unavailable: String(refusal ?? "the count could not be taken").slice(0, 300), ...(deterministic ? { deterministic: true } : {}) };
|
|
360
379
|
};
|
|
361
380
|
|
|
362
381
|
// The aggregate: the provider's EXACT predicate, once per generated form, summed.
|
|
@@ -503,12 +522,38 @@ export function countsForMark(doc, name) {
|
|
|
503
522
|
* Office CODES, not names: `scope.regions` on the same artifact is already codes, and the alternative
|
|
504
523
|
* is inventing a display layer that has to stay in step with the office vocabulary of six providers.
|
|
505
524
|
*/
|
|
525
|
+
/**
|
|
526
|
+
* The register's own floor for a count it stopped taking, or null.
|
|
527
|
+
*
|
|
528
|
+
* ONE READING FOR EVERY PAGE THAT SHOWS A COUNT. The glance line printed the floor and the counts table,
|
|
529
|
+
* the coverage clause and the workbook beside it still printed "not available", so one report said two
|
|
530
|
+
* different things about the same cell. A floor counts only with the register's flag AND the number —
|
|
531
|
+
* a flag with no number says no more than "unknown" does.
|
|
532
|
+
*/
|
|
533
|
+
export function disclosedFloor(c) {
|
|
534
|
+
return c?.approximate === true && Number.isFinite(c?.floor) ? c.floor : null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** A floor as every page prints it: the register's own figure, and no sentence around it. */
|
|
538
|
+
export const moreThan = (floor) => `more than ${floor.toLocaleString("en-US")}`;
|
|
539
|
+
|
|
506
540
|
export function countLine(entry) {
|
|
507
541
|
if (!entry?.counts) return null;
|
|
508
542
|
const parts = COUNT_PREDICATES.map((p) => {
|
|
509
543
|
const c = entry.counts[p.key];
|
|
510
544
|
const word = p.glance ?? p.label.toLowerCase();
|
|
511
545
|
if (Number.isFinite(c?.total)) return `${c.total} ${word}`;
|
|
546
|
+
// A REGISTER THAT ANSWERS WITH A FLOOR HAS ANSWERED. Some registers stop counting and report
|
|
547
|
+
// "more than ten thousand" rather than a total; that is the register's own figure and it is what
|
|
548
|
+
// the client is shown, as a number. Rendering it as "not available" beside a register that could
|
|
549
|
+
// not be reached at all tells a reader the same thing about two different facts, and the one the
|
|
550
|
+
// reader would act on — go and look elsewhere — is wrong for this one.
|
|
551
|
+
//
|
|
552
|
+
// No sentence, no adjective: the rest of this line is figures and what they counted, and a
|
|
553
|
+
// qualification written here would be the only prose on it.
|
|
554
|
+
if (disclosedFloor(c) !== null) return `${word}: ${moreThan(disclosedFloor(c))}`;
|
|
555
|
+
// Left for a register this deployment could not reach or could not ask. Those have no figure at
|
|
556
|
+
// all, which is what this phrase now means and the only thing it means.
|
|
512
557
|
return `${word}: not available`;
|
|
513
558
|
});
|
|
514
559
|
const scope = entry.classScope === "all-classes"
|