clearotron 0.4.0-beta.0 → 0.4.0-beta.2
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 +9 -0
- package/INSTALL.md +4 -31
- package/README.md +5 -5
- package/bin/brandowner.mjs +1 -1
- package/bin/onboard.mjs +18 -74
- package/bin/start.mjs +58 -14
- package/bin/update.mjs +10 -2
- package/build-info.json +2 -2
- package/demo/full-country-search/run/_driver/search-policy.json +1 -1
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/_driver/search-policy.json +1 -1
- package/demo/knockout-search/run/status.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/README.md +1 -0
- package/docs/SECURITY-OWASP.md +298 -0
- package/docs/SECURITY.md +18 -13
- package/docs/architecture/02-architecture.md +5 -5
- package/docs/architecture/03-run-lifecycle.md +28 -29
- package/docs/architecture/04-configuration-reference.md +5 -10
- package/docs/architecture/05-config-governance.md +5 -7
- package/docs/architecture/06-operations-runbook.md +0 -12
- package/docs/architecture/07-quality-and-audit.md +12 -13
- package/docs/architecture/09-security-and-data.md +9 -4
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +4 -2
- package/driver/authority-trees.mjs +90 -8
- package/driver/band-shape.mjs +13 -13
- package/driver/blind-frame-model.mjs +1 -1
- package/driver/case-law-ledger.mjs +19 -3
- package/driver/claim-liveness.mjs +30 -1
- package/driver/clearance-variants-record.mjs +10 -3
- package/driver/common-law-receipts.mjs +64 -14
- package/driver/commonlaw-carry.mjs +2 -2
- package/driver/compare.mjs +2 -2
- package/driver/connotation-search.mjs +33 -104
- package/driver/contract-arm2-baseline.json +6 -5
- package/driver/contract-e3-backlog.mjs +62 -62
- package/driver/contract-vocabulary.mjs +38 -31
- package/driver/coverage-form-io.mjs +13 -1
- package/driver/coverage-form.mjs +42 -4
- package/driver/coverage-ledger.mjs +3 -5
- package/driver/cross-check-wait.mjs +43 -0
- package/driver/declination-call.mjs +34 -10
- package/driver/declination-tool.mjs +27 -15
- package/driver/deferral-row.mjs +139 -0
- package/driver/degraded-parts.mjs +377 -0
- package/driver/deliver-trigger.sh +1 -1
- package/driver/demo-container.mjs +4 -1
- package/driver/dev-portal.mjs +5 -5
- package/driver/dispatch-record.mjs +2 -2
- package/driver/driver.config.mjs +193 -96
- package/driver/e2e/README.md +1 -1
- package/driver/effective-scope.mjs +58 -2
- package/driver/engine/anthropic-agent.mjs +103 -27
- package/driver/engine/auth.mjs +2 -2
- package/driver/engine/child-record.mjs +11 -0
- package/driver/engine/cli-version.mjs +4 -3
- package/driver/engine/common.mjs +17 -2
- package/driver/engine/deny-authority-write.mjs +3 -1
- package/driver/engine/engine-env.mjs +174 -0
- package/driver/engine/engine-spawn.mjs +179 -0
- package/driver/engine/mcp/band-server.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +2 -1
- package/driver/engine/mcp/codex-config.mjs +112 -3
- package/driver/engine/mcp/corsearch-server.mjs +1 -0
- package/driver/engine/mcp/declination-server.mjs +9 -8
- package/driver/engine/mcp/dispositions-server.mjs +2 -1
- package/driver/engine/mcp/euipo-server.mjs +1 -0
- package/driver/engine/mcp/fetch-server.mjs +14 -11
- package/driver/engine/mcp/free-tier-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +23 -11
- package/driver/engine/mcp/perplexity-server.mjs +97 -17
- package/driver/engine/mcp/probe-server.mjs +43 -21
- package/driver/engine/mcp/public-fetch.mjs +195 -0
- package/driver/engine/mcp/recording-server.mjs +79 -14
- package/driver/engine/mcp/signa-server.mjs +1 -0
- package/driver/engine/mcp/supplemental.mjs +14 -14
- package/driver/engine/mcp/unit-note-server.mjs +50 -1
- package/driver/engine/mcp/uspto-local-server.mjs +1 -0
- package/driver/engine/openai-agent.mjs +109 -28
- package/driver/engine/probe.mjs +152 -19
- package/driver/enqueue-schema.mjs +5 -5
- package/driver/feedback-issues.mjs +1 -1
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +67 -16
- package/driver/flag-snapshot.mjs +1 -1
- package/driver/floor-duty.mjs +23 -7
- package/driver/form-neighbourhood.mjs +52 -17
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework-method.mjs +304 -0
- package/driver/gateway.mjs +49 -21
- package/driver/hand-off-exits.mjs +129 -0
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +8 -5
- package/driver/knockout-assess-record.mjs +7 -1
- package/driver/knockout-frame-record.mjs +96 -34
- package/driver/knockout-review-record.mjs +7 -2
- package/driver/log.mjs +2 -2
- package/driver/matter-frame-record.mjs +31 -2
- package/driver/methodology-witness.mjs +6 -3
- package/driver/named-band.mjs +1 -1
- package/driver/owner-use-check.mjs +45 -10
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +4 -0
- package/driver/phase0.mjs +2 -2
- package/driver/pipeline-knockout.mjs +256 -129
- package/driver/pipeline.mjs +417 -614
- package/driver/placement-carry.mjs +22 -3
- package/driver/placement-form-io.mjs +18 -4
- package/driver/placement-form.mjs +53 -4
- package/driver/placement-union.mjs +32 -5
- package/driver/portal-report.mjs +3 -3
- package/driver/portal-service.mjs +6 -5
- package/driver/portal-static.mjs +11 -18
- package/driver/predelivery-lint.mjs +10 -10
- package/driver/profile-page.html +5 -5
- package/driver/profiles.mjs +5 -5
- package/driver/progress.mjs +6 -4
- package/driver/provider-usage.mjs +25 -2
- package/driver/publish/index.mjs +97 -25
- package/driver/publish/knockout.mjs +136 -14
- package/driver/publish/profiles-page.mjs +2 -0
- package/driver/publish/publish-inputs.mjs +10 -4
- package/driver/publish/render-knockout.mjs +48 -21
- package/driver/publish/render.mjs +13 -8
- package/driver/publish/report-data.mjs +2 -0
- package/driver/publish/search-depth.mjs +77 -4
- package/driver/publish/templates/report.css +1 -1
- package/driver/publish/xlsx.mjs +34 -15
- package/driver/reasoning-tripwires.mjs +2 -160
- package/driver/recall-receipt.mjs +27 -0
- package/driver/recall-reconciliation.mjs +1 -1
- package/driver/record-carry.mjs +14 -14
- package/driver/recording-agreement.mjs +2 -2
- package/driver/reference-score.mjs +169 -47
- package/driver/register-count.mjs +61 -4
- package/driver/register-digest-record.mjs +14 -1
- package/driver/register-plan.mjs +161 -80
- package/driver/register-records.mjs +37 -4
- package/driver/registration-scripts.mjs +22 -0
- package/driver/registry-fidelity.mjs +4 -4
- package/driver/repair-composers.mjs +23 -7
- package/driver/repair-contract.mjs +1 -1
- package/driver/repairs.mjs +5 -2
- package/driver/reviewer-open-points.mjs +33 -0
- package/driver/rule-shape.mjs +8 -8
- package/driver/run-economics.mjs +3 -3
- package/driver/run-integrity.mjs +2 -2
- package/driver/runner.mjs +17 -5
- package/driver/scope-facts.mjs +89 -12
- package/driver/score-redaction.mjs +247 -0
- package/driver/screen-gate.mjs +1 -1
- package/driver/search-policy.mjs +17 -0
- package/driver/skills/README.md +2 -2
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/SKILL.md +29 -36
- package/driver/skills/clearance-common-law/perplexity-prompts.md +8 -30
- package/driver/skills/clearance-register/SKILL.md +15 -16
- package/driver/skills/clearance-register/digest.md +15 -31
- package/driver/skills/clearance-register/register-recipes.md +14 -56
- package/driver/skills/clearance-register/unit.md +37 -48
- package/driver/skills/clearance-search/SKILL.md +6 -8
- package/driver/skills/clearance-search/delivery-contract.md +0 -7
- package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
- package/driver/skills/clearance-search/phase2-execution.md +5 -6
- package/driver/skills/clearance-search/report-prose.md +5 -5
- package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
- package/driver/skills/clearance-search/synthesis-rules.md +9 -9
- package/driver/skills/clearance-search/template-formatting.md +2 -2
- package/driver/skills/clearance-variants/SKILL.md +8 -8
- package/driver/skills/clearance-variants/transliteration-scripts.md +2 -2
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/skills/knockout-assess/SKILL.md +40 -10
- package/driver/skills/knockout-frame/SKILL.md +42 -10
- package/driver/skills/matter-frame/SKILL.md +12 -5
- package/driver/skills/matter-frame/watchlist-reference.md +1 -1
- package/driver/skills/narrative-refutation/SKILL.md +5 -3
- package/driver/skills/placement-inquiry/SKILL.md +5 -1
- package/driver/stage-context.mjs +27 -6
- package/driver/stages-knockout.mjs +92 -79
- package/driver/stages.mjs +111 -68
- package/driver/stray-artifacts.mjs +9 -5
- package/driver/suite-census.json +696 -150
- package/driver/synthesis-record.mjs +26 -4
- package/driver/systemd/README.md +5 -6
- package/driver/systemd/clearotron-client-mcp.service +24 -0
- package/driver/systemd/clearotron-mcp-face.service +24 -0
- package/driver/systemd/clearotron-portal.service +24 -0
- package/driver/systemd/clearotron-worker.service +29 -0
- package/driver/tokens.mjs +10 -9
- package/driver/turnaround-bands.mjs +1 -1
- package/driver/unit-inventory.mjs +24 -43
- package/driver/variant-manifest-model.mjs +22 -3
- package/driver/verify-knockout.mjs +163 -4
- package/driver/verify.mjs +42 -16
- package/driver/web-grid.mjs +150 -0
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/withheld-families.mjs +113 -4
- package/driver/worker-heartbeat.mjs +13 -2
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +10 -10
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/trace.mjs +3 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -3
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DVtz44vH.js → index-BPAUjcI0.js} +3 -2
- package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
- package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
- package/portal-ui/dist/index.html +2 -15
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -1
- package/providers/_shared/answer-memory.mjs +199 -0
- package/providers/_shared/enumerate.mjs +155 -50
- package/providers/_shared/execute-plan.mjs +62 -16
- package/providers/_shared/ledger-path.mjs +1 -1
- package/providers/_shared/ledger.mjs +48 -6
- package/providers/_shared/plan-guards.mjs +7 -0
- package/providers/_shared/script-form.mjs +24 -5
- package/providers/_shared/term-shape.mjs +5 -5
- package/providers/clarivate/src/capabilities.js +11 -0
- package/providers/clarivate/src/core.js +193 -18
- package/providers/corsearch/README.md +1 -2
- package/providers/corsearch/src/core.js +2 -2
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/README.md +8 -7
- package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
- package/providers/perplexity/src/core.js +246 -21
- package/providers/signa/src/capabilities.js +24 -0
- package/providers/signa/src/core.js +93 -25
- package/scripts/README.md +2 -1
- package/scripts/added-reference-check.mjs +2 -1
- package/scripts/backup-recall-stores.mjs +5 -5
- package/scripts/demo-evidence.mjs +2 -1
- package/scripts/deprecate-below.mjs +114 -2
- package/scripts/drive-env-check.mjs +2 -1
- package/scripts/e2e-first-time.mjs +469 -0
- package/scripts/e2e-scenario-ops.mjs +441 -0
- package/scripts/e2e.mjs +104 -16
- package/scripts/env-audit.mjs +8 -0
- package/scripts/freeze-example-run.mjs +7 -7
- package/scripts/generated-files-are-current.mjs +2 -1
- package/scripts/hand-off-exits-probe.mjs +75 -0
- package/scripts/import-cycle-check.mjs +2 -1
- package/scripts/merge-shape-check.mjs +2 -1
- package/scripts/package-size-budget.mjs +2 -1
- package/scripts/record-carry-probe.mjs +1 -1
- package/scripts/release-entry-catch-up.mjs +77 -8
- package/scripts/release-install-check.mjs +4 -1
- package/scripts/release-note-required.mjs +139 -9
- package/scripts/release-rehearsal-version.mjs +60 -0
- package/scripts/release-sbom.mjs +104 -0
- package/scripts/release-visible-check.mjs +7 -5
- package/scripts/render-check.mjs +5 -2
- package/scripts/report-offline-render-check.mjs +169 -0
- package/scripts/score.mjs +79 -8
- package/scripts/strip-titles-and-attributions.mjs +2 -1
- package/scripts/strip-tracker-citations.mjs +2 -1
- package/scripts/test-full.mjs +2 -1
- package/scripts/test-run.mjs +10 -3
- package/scripts/third-party-notices.mjs +3 -1
- package/scripts/travelling-predicates.mjs +1 -1
- package/scripts/writing-standard-check.mjs +2 -1
- package/shared/brand-fonts.mjs +59 -0
- package/shared/brand.mjs +6 -6
- package/shared/browser-temp-root.mjs +3 -2
- package/shared/doctrine-overlay.mjs +1 -1
- package/shared/driver-dir.mjs +47 -11
- package/shared/fonts/OFL-fira-code.txt +93 -0
- package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
- package/shared/fonts/README.md +37 -0
- package/shared/fonts/fira-code.woff2 +0 -0
- package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/npm-cli.mjs +23 -0
- package/shared/os-advice.mjs +14 -2
- package/shared/path-seps.mjs +42 -0
- package/shared/process-table.mjs +71 -3
- package/shared/reference-guard-classes.mjs +30 -6
- package/shared/root-doc-commands.mjs +6 -2
- package/shared/running-start.mjs +24 -7
- package/shared/scope.mjs +2 -2
- package/shared/wsl.mjs +1 -1
- package/driver/known-conflicts.mjs +0 -327
package/scripts/score.mjs
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
4
|
// score.mjs — score a completed run against the lawyer's reference answer for its scenario.
|
|
5
5
|
//
|
|
6
|
-
// node scripts/score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json]
|
|
6
|
+
// node scripts/score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json] [--names]
|
|
7
|
+
//
|
|
8
|
+
// `--names` prints the reference's marks, proprietors and sentences. WITHOUT it they are withheld and
|
|
9
|
+
// each is shown as a stable `«name N»` token, because scoring a run is routine work done in sessions
|
|
10
|
+
// that record everything they print, and a reference answer is a real lawyer's answer to a real matter.
|
|
11
|
+
// Every number, axis and bucket is unaffected: an entry's index answers every question this tool asks.
|
|
12
|
+
// The flag is for the one job that needs the words, reading a surprising result against the finding text.
|
|
7
13
|
//
|
|
8
14
|
// `--previous auto` resolves the OTHER HALF OF A PAIR: the run of the round immediately before
|
|
9
15
|
// this one, same scenario, same door. It reads the scenario's declared refs from the config store and
|
|
@@ -56,6 +62,9 @@ import { recordQids } from "../driver/named-band.mjs";
|
|
|
56
62
|
// reason it is not in scripts/e2e.mjs: this scorer is deliberately offline, and importing the harness
|
|
57
63
|
// would drag portal-mcp-client, enqueue-schema and door-gates in behind it.
|
|
58
64
|
import { previousRunDir, scenarioRefs } from "../driver/e2e-rounds.mjs";
|
|
65
|
+
// The names rule, kept out of this file so it is testable without a run directory — the same reason
|
|
66
|
+
// reference-score.mjs holds the scoring rules rather than this script.
|
|
67
|
+
import { protectedStrings, redactor, installRedaction, unclassifiedNotice, REDACTION_NOTICE } from "../driver/score-redaction.mjs";
|
|
59
68
|
import { readSettleStamp } from "../driver/settle-stamp.mjs";
|
|
60
69
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
61
70
|
|
|
@@ -133,6 +142,9 @@ function findingsOf(runDir) {
|
|
|
133
142
|
// layer is load-bearing alone.
|
|
134
143
|
ordinal: Number.isInteger(f.ordinal) ? f.ordinal : null,
|
|
135
144
|
mark: f.mark ?? "", owner: ownerName(f.owner), band: f.band?.label ?? null,
|
|
145
|
+
// The offices the finding's records are filed in, for the owner-or-country join: the office
|
|
146
|
+
// segment of each registration's `/mark/<office>/…` uri, else the owner's country.
|
|
147
|
+
territories: officesOf(f.owner),
|
|
136
148
|
disposition: f.disposition ?? null, ruled_out: f.ruled_out === true,
|
|
137
149
|
ruled_out_reason: f.ruled_out_reason ?? null,
|
|
138
150
|
// — the EVIDENCE CLASS, derived at the artifact boundary so the scorer stays pure and
|
|
@@ -204,6 +216,18 @@ function findingsOf(runDir) {
|
|
|
204
216
|
return { lane: null, rows: [], subjects: null };
|
|
205
217
|
}
|
|
206
218
|
|
|
219
|
+
/** The office a record id names: `/mark/<office>/…` → `<office>`, else null. */
|
|
220
|
+
function officeOfRecord(recordId) {
|
|
221
|
+
const m = /^\/mark\/([a-z]{2,3})\//i.exec(String(recordId ?? ""));
|
|
222
|
+
return m ? m[1] : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The offices a finding's owner is filed in, from its registration uris; else the owner's country. */
|
|
226
|
+
function officesOf(owner) {
|
|
227
|
+
const offices = [...new Set((owner?.registrations ?? []).map((r) => officeOfRecord(r?.uri)).filter(Boolean))];
|
|
228
|
+
return offices.length ? offices : (owner?.country ? [owner.country] : []);
|
|
229
|
+
}
|
|
230
|
+
|
|
207
231
|
/**
|
|
208
232
|
* Every label the run RETRIEVED — the corpus `withheld` is measured against.
|
|
209
233
|
*
|
|
@@ -227,7 +251,7 @@ function retrievedOf(runDir) {
|
|
|
227
251
|
const k = `${mark} ${record_id ?? ""}`;
|
|
228
252
|
if (!mark) return;
|
|
229
253
|
const prior = out.get(k);
|
|
230
|
-
if (!prior) { out.set(k, { mark, record_id: record_id ?? null, owner: owner ?? null, qids: [...new Set(qids ?? [])] }); return; }
|
|
254
|
+
if (!prior) { out.set(k, { mark, record_id: record_id ?? null, owner: owner ?? null, territory: officeOfRecord(record_id), qids: [...new Set(qids ?? [])] }); return; }
|
|
231
255
|
for (const q of qids ?? []) if (!prior.qids.includes(q)) prior.qids.push(q); // union across both sources
|
|
232
256
|
};
|
|
233
257
|
|
|
@@ -271,6 +295,17 @@ function searchedTextOf(runDir) {
|
|
|
271
295
|
const d = driverDir(runDir);
|
|
272
296
|
for (const f of readdirSync(d)) if (/^grid-spec.*\.json$/.test(f)) parts.push(readText(join(d, f)));
|
|
273
297
|
} catch { /* no _driver — reported by the caller */ }
|
|
298
|
+
// THE KNOCKOUT LANE WRITES NONE OF THE ABOVE. It keeps no plan-execution record and no grid ledger:
|
|
299
|
+
// where it looked is recorded in what it cites, the evidence addresses on each finding in
|
|
300
|
+
// knockout-findings.json and the pages its research notes name, one file per mark under research/.
|
|
301
|
+
// Read only the files above, a knockout printed every channel ABSENT while its findings cited the
|
|
302
|
+
// very store the reference names. Neither file exists on the clearance lane, so its reading is
|
|
303
|
+
// unchanged.
|
|
304
|
+
parts.push(readText(join(runDir, "knockout-findings.json")));
|
|
305
|
+
try {
|
|
306
|
+
const r = join(runDir, "research");
|
|
307
|
+
for (const f of readdirSync(r)) if (/\.md$/.test(f)) parts.push(readText(join(r, f)));
|
|
308
|
+
} catch { /* no research/ — a clearance run, or a knockout that wrote no notes */ }
|
|
274
309
|
return parts.join("\n");
|
|
275
310
|
}
|
|
276
311
|
|
|
@@ -435,7 +470,7 @@ function scoreOne(run, ref) {
|
|
|
435
470
|
//
|
|
436
471
|
// Refusing the unshaped pairing was only half of it. A gold set that gains a `counts` block usually
|
|
437
472
|
// KEEPS its lawyer sheet — it is a real answer to a real matter and deleting it loses history — and
|
|
438
|
-
// scoring both prints `LOST ·
|
|
473
|
+
// scoring both prints `LOST · WAVO PUNCH` beside the count that measures the product. That is the same
|
|
439
474
|
// structural zero the issue is about, one row smaller, and it lands in the bucket table a reader
|
|
440
475
|
// scans first.
|
|
441
476
|
//
|
|
@@ -446,7 +481,7 @@ function scoreOne(run, ref) {
|
|
|
446
481
|
const buckets = scoreRecall({
|
|
447
482
|
reference: countShaped ? [] : ref.register, findings: run.findings, retrieved: run.retrieved,
|
|
448
483
|
scopeClasses: run.scopeClasses, scopeTerritories: run.scopeTerritories, registerOnly, collapseReason, preAccepted: ref.pre_accepted ?? [],
|
|
449
|
-
coverage,
|
|
484
|
+
coverage, searchedMarks: [...(ref.covers_marks ?? []), ref.mark].filter(Boolean),
|
|
450
485
|
});
|
|
451
486
|
return {
|
|
452
487
|
registerOnly, collapseReason, coverage, buckets, countShaped,
|
|
@@ -644,7 +679,7 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
644
679
|
console.log(renderCarryThrough(run.dir, { indent: " " }).lines.join("\n"));
|
|
645
680
|
|
|
646
681
|
// — a record in two buckets is a defect in THIS TOOL, and it is printed as loudly as any score.
|
|
647
|
-
// Silence here is what let `
|
|
682
|
+
// Silence here is what let `KORPHI GENETICS` sit in LOST and `DG KORPHI GENETICS` in NOISE on the same
|
|
648
683
|
// page, and the recall number that reached the round handover was wrong for a week because nobody
|
|
649
684
|
// reads two lists against each other.
|
|
650
685
|
if (B.collisions?.length) {
|
|
@@ -656,7 +691,7 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
656
691
|
//
|
|
657
692
|
// These used to print under the line above, which tells the reader not to read the recall numbers.
|
|
658
693
|
// One large filer anywhere in a matter therefore suppressed a whole run's measurement, and did: R2's
|
|
659
|
-
// real 88% → 63% recall movement went unquoted on the regression issue because `
|
|
694
|
+
// real 88% → 63% recall movement went unquoted on the regression issue because `Norvanta AG` held one
|
|
660
695
|
// withheld reference mark and one surfaced non-reference mark. Those two rows are both true.
|
|
661
696
|
//
|
|
662
697
|
// Kept visible because a reader may still want the pairing, and deliberately WITHOUT a verdict: this
|
|
@@ -877,7 +912,7 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
877
912
|
|
|
878
913
|
// ── main ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
879
914
|
|
|
880
|
-
const USAGE = `usage: score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json]`;
|
|
915
|
+
const USAGE = `usage: score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json] [--names]`;
|
|
881
916
|
|
|
882
917
|
/**
|
|
883
918
|
* `--previous auto` — the other half of the pair, resolved rather than typed.
|
|
@@ -907,9 +942,10 @@ function resolvePreviousAuto(id, runDir) {
|
|
|
907
942
|
}
|
|
908
943
|
|
|
909
944
|
// One pass, so a directory named like a flag's value can never be mistaken for the scenario id.
|
|
910
|
-
const opts = { json: false, run: null, previous: null, id: null };
|
|
945
|
+
const opts = { json: false, run: null, previous: null, id: null, names: false };
|
|
911
946
|
for (let i = 0, a = process.argv.slice(2); i < a.length; i++) {
|
|
912
947
|
if (a[i] === "--json") opts.json = true;
|
|
948
|
+
else if (a[i] === "--names") opts.names = true;
|
|
913
949
|
else if (a[i] === "--run") opts.run = a[++i];
|
|
914
950
|
else if (a[i] === "--previous") opts.previous = a[++i];
|
|
915
951
|
else if (a[i].startsWith("--")) die(`unknown flag ${a[i]}\n${USAGE}`);
|
|
@@ -929,11 +965,40 @@ const previousDir = opts.previous === "auto" ? resolvePreviousAuto(id, runDir) :
|
|
|
929
965
|
const prev = previousDir ? scoreOne(readRun(previousDir, ref), ref) : null;
|
|
930
966
|
const delta = prev ? bucketDelta(scored.buckets, prev.buckets) : null;
|
|
931
967
|
|
|
968
|
+
// ── NAMES ARE WITHHELD UNLESS SOMEBODY ASKS FOR THEM ─────────────────────────────────────────────────
|
|
969
|
+
//
|
|
970
|
+
// A reference answer is a real lawyer's answer to a real matter, and scoring a run is routine work done
|
|
971
|
+
// inside sessions that record everything they print. So the default output carries the axes, the buckets
|
|
972
|
+
// and which entry fell in which, with every mark, proprietor and reference sentence swapped for a stable
|
|
973
|
+
// token; `--names` prints them as before.
|
|
974
|
+
//
|
|
975
|
+
// INSTALLED OVER THE STREAM, not at each of the twenty-odd sites that interpolate a name. Redacting
|
|
976
|
+
// per-site is the shape that leaves one site out, and it would have to be revisited by every later edit
|
|
977
|
+
// that adds a print. Here `renderCarryThrough`'s lines and anything added after this was written are
|
|
978
|
+
// covered without anyone remembering to.
|
|
979
|
+
//
|
|
980
|
+
// The two inputs are the reference AND the scored buckets: a proprietor this run surfaced that the
|
|
981
|
+
// lawyer never named is still somebody's name, and it reaches the page through `noise`.
|
|
982
|
+
let unclassifiedKeys = [];
|
|
983
|
+
if (!opts.names) {
|
|
984
|
+
const { names, prose, unclassified } = protectedStrings({ reference: ref, scored });
|
|
985
|
+
unclassifiedKeys = unclassified;
|
|
986
|
+
installRedaction(redactor({ names, prose }));
|
|
987
|
+
}
|
|
988
|
+
|
|
932
989
|
if (opts.json) {
|
|
933
990
|
console.log(JSON.stringify({
|
|
934
991
|
// — the instrument that produced these numbers, so a reader comparing two archived scores can
|
|
935
992
|
// tell whether the comparison is valid. A score with no `scorer_version` predates this stamp.
|
|
936
993
|
scorer_version: SCORER_VERSION,
|
|
994
|
+
// — WHETHER THE NAMES IN THIS PAYLOAD ARE THE REAL ONES. `--json` goes to the same terminal the
|
|
995
|
+
// human path does, so it is redacted on the same rule; stamped rather than left to be inferred,
|
|
996
|
+
// because a consumer that cannot tell a redacted score from a named one will compare the two.
|
|
997
|
+
names: opts.names ? "printed" : "redacted",
|
|
998
|
+
// THE SAME WARNING THE HUMAN PATH PRINTS, AS A FIELD RATHER THAN A LINE. It cannot be printed here:
|
|
999
|
+
// this payload is parsed, and a sentence above it makes the whole document unreadable — which is how
|
|
1000
|
+
// it first went wrong. Keys only, never their values.
|
|
1001
|
+
names_unclassified: unclassifiedKeys,
|
|
937
1002
|
// — the OTHER half of the same question. A consumer comparing two archived scores needs
|
|
938
1003
|
// both instruments: which scorer read the run, and which engine produced it.
|
|
939
1004
|
engine_commit: run.engineCommit,
|
|
@@ -949,6 +1014,12 @@ if (opts.json) {
|
|
|
949
1014
|
carry_through: (() => { const ct = carryThrough(run.dir); return { ...ct, coverage: coverageConflicts(run.dir, ct.lost) }; })(),
|
|
950
1015
|
}, null, 2));
|
|
951
1016
|
} else {
|
|
1017
|
+
// Said before the first number, not after the last: a reader who stops at the top screen must know
|
|
1018
|
+
// which of the two readings they are holding.
|
|
1019
|
+
if (!opts.names) console.log(`\n ${REDACTION_NOTICE}`);
|
|
1020
|
+
// A reference that has grown a field this module does not classify is unprotected in exactly that
|
|
1021
|
+
// field, so the reader learns it before anything below, not after.
|
|
1022
|
+
if (unclassifiedKeys.length) console.log(` ${unclassifiedNotice(unclassifiedKeys)}`);
|
|
952
1023
|
print(String(id).toUpperCase(), ref, run, scored, delta, refPath);
|
|
953
1024
|
// — THE LAWYER'S OWN STATEMENTS OF WHAT THE RUN MUST DEMONSTRATE. The buckets cannot carry
|
|
954
1025
|
// these: an assertion says WHY a mark matters, and that reasoning is what tells a reader which lane to
|
|
@@ -63,6 +63,7 @@ import { fileURLToPath } from "node:url";
|
|
|
63
63
|
import { isScannable, EXCLUDED } from "./strip-tracker-citations.mjs";
|
|
64
64
|
import { publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
65
65
|
import { newBareCitations } from "./citation-line-check.mjs";
|
|
66
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
66
67
|
|
|
67
68
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
68
69
|
const APPLY = process.argv.includes("--apply");
|
|
@@ -357,7 +358,7 @@ export function surveyOf(files, read) {
|
|
|
357
358
|
|
|
358
359
|
const total = (o) => Object.values(o).reduce((a, n) => a + n, 0);
|
|
359
360
|
|
|
360
|
-
if (import.meta.url
|
|
361
|
+
if (isEntrypoint(import.meta.url)) {
|
|
361
362
|
// THE PUBLISHED POPULATION, NOT THE INDEX — the same helper, for the same reason, as the opener sweep.
|
|
362
363
|
const all = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 }).split("\n").filter(Boolean);
|
|
363
364
|
const pub = publishedOf(all, ROOT);
|
|
@@ -61,6 +61,7 @@ import { execFileSync } from "node:child_process";
|
|
|
61
61
|
import { publishedOf, wrapsInto } from "../shared/reference-guard-classes.mjs";
|
|
62
62
|
import { join, dirname } from "node:path";
|
|
63
63
|
import { fileURLToPath } from "node:url";
|
|
64
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
64
65
|
|
|
65
66
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
66
67
|
const APPLY = process.argv.includes("--apply");
|
|
@@ -239,7 +240,7 @@ export function surveyOf(files, read) {
|
|
|
239
240
|
return { strippedTotal, remainingTotal, stripped, handoff, unreadable };
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
if (import.meta.url
|
|
243
|
+
if (isEntrypoint(import.meta.url)) {
|
|
243
244
|
// THE PUBLISHED POPULATION, NOT THE INDEX. The counts this prints are read as a statement about the
|
|
244
245
|
// public tree, and under an overlay `git ls-files` would have made them a statement about the
|
|
245
246
|
// withheld corpus instead. Same helper as the residue floor and the backlog minter.
|
package/scripts/test-full.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { spawnSync } from "node:child_process";
|
|
|
43
43
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
44
44
|
import { join, dirname } from "node:path";
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
46
47
|
|
|
47
48
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
48
49
|
|
|
@@ -316,4 +317,4 @@ function main() {
|
|
|
316
317
|
if (bad.length) process.exit(1);
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
if (import.meta.url
|
|
320
|
+
if (isEntrypoint(import.meta.url)) main();
|
package/scripts/test-run.mjs
CHANGED
|
@@ -399,8 +399,11 @@ const isContained = (value) => {
|
|
|
399
399
|
// The acceptance asks the refusal to name the value's ROOT, because that is the part that says "this is
|
|
400
400
|
// somebody's live estate" at a glance, where a long path does not.
|
|
401
401
|
const rootOf = (value) => {
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
// From the path's own root, so a Windows path names its drive and first folder (D:\a), not "\D:".
|
|
403
|
+
const p = resolve(value);
|
|
404
|
+
const { root } = parsePath(p);
|
|
405
|
+
const first = p.slice(root.length).split(sep).find(Boolean);
|
|
406
|
+
return first ? root + first : root;
|
|
404
407
|
};
|
|
405
408
|
|
|
406
409
|
// A guard with no way through gets deleted the first time somebody genuinely needs it, and then nothing
|
|
@@ -436,7 +439,7 @@ if (escaped.length) {
|
|
|
436
439
|
// ── WHAT THE NEXT TWENTY MINUTES OF OUTPUT IS, SAID BEFORE IT STARTS ───────────────────────
|
|
437
440
|
//
|
|
438
441
|
// The suites print thousands of lines shaped like
|
|
439
|
-
// [register-unit:primary-sweep] attempt 1/1 (engine=anthropic-agent agent=
|
|
442
|
+
// [register-unit:primary-sweep] attempt 1/1 (engine=anthropic-agent agent=mailagent model=opus … timeout=1500s)
|
|
440
443
|
// and to somebody who cloned this repo ten minutes ago that is their subscription being spent, at
|
|
441
444
|
// speed, for reasons nobody explained. It is the mock engine's dispatch record. Nothing here can tell
|
|
442
445
|
// them that except a line that says it, and it has to come FIRST — an explanation after the scroll is
|
|
@@ -791,6 +794,10 @@ child = spawn(argv[0], argv.slice(1), {
|
|
|
791
794
|
env: {
|
|
792
795
|
...process.env,
|
|
793
796
|
TMPDIR: root,
|
|
797
|
+
// Windows' os.tmpdir() reads TEMP and TMP, not TMPDIR, so without these a Windows run's fixtures
|
|
798
|
+
// landed in the machine's own temp folder.
|
|
799
|
+
TEMP: root,
|
|
800
|
+
TMP: root,
|
|
794
801
|
// AND THE REAL BASE, so a NESTED run does not root itself inside this one. The line above hands the
|
|
795
802
|
// child a TMPDIR pointing at this run's own root; `os.tmpdir()` honours it, so a child that starts
|
|
796
803
|
// its own runner would put its root INSIDE the parent's and the parent's cleanup — or its sweep of
|
|
@@ -23,6 +23,7 @@ import { join, dirname } from "node:path";
|
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
25
25
|
import { nonEmpty } from "../shared/vacuous-pass.mjs"; // — an empty row set would confirm compliance over nothing
|
|
26
|
+
import { npmInvocation } from "../shared/npm-cli.mjs";
|
|
26
27
|
|
|
27
28
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
29
|
export const OUTPUT = join(ROOT, "THIRD-PARTY-NOTICES.md");
|
|
@@ -80,7 +81,8 @@ export function npmTree(root = ROOT, run = execFileSync) {
|
|
|
80
81
|
const opts = { cwd: root, encoding: "utf8", maxBuffer: 128 * 1024 * 1024 };
|
|
81
82
|
let out;
|
|
82
83
|
try {
|
|
83
|
-
|
|
84
|
+
const npm = npmInvocation(["ls", "--omit=dev", "--all", "--json"]); // npm.cmd on Windows needs a shell; npm-cli.js does not
|
|
85
|
+
out = run(npm.command, npm.args, opts);
|
|
84
86
|
} catch (e) {
|
|
85
87
|
out = e?.stdout;
|
|
86
88
|
if (typeof out !== "string" || !out.trim()) throw e;
|
|
@@ -102,7 +102,7 @@ function isPlumbing(node, child) {
|
|
|
102
102
|
// This was the whole of the unresolved bucket — three sites, all one shape:
|
|
103
103
|
//
|
|
104
104
|
// const wrote = files.length ? files.some(…) : null; gateway.mjs (by name)
|
|
105
|
-
// const inScope = scope.size ? tokens.some(…) : (…); reasoning-tripwires.mjs
|
|
105
|
+
// const inScope = scope.size ? tokens.some(…) : (…); findRecallFloorViolations() in reasoning-tripwires.mjs
|
|
106
106
|
// const reached = b.layer === "national" ? (…) : regions.some(…); resolveRegions() in register-plan.mjs
|
|
107
107
|
//
|
|
108
108
|
// The climb stopped at the ternary and reported "unresolved", which reads as a limit of the pattern
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { execFileSync } from "node:child_process";
|
|
43
43
|
import { readFileSync } from "node:fs";
|
|
44
44
|
import { CLASSES, fileOffences, isExempt } from "../shared/writing-standard-classes.mjs";
|
|
45
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
45
46
|
|
|
46
47
|
const baseArg = () => {
|
|
47
48
|
const i = process.argv.indexOf("--base");
|
|
@@ -119,4 +120,4 @@ function main() {
|
|
|
119
120
|
process.exit(1);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
if (import.meta.url
|
|
123
|
+
if (isEntrypoint(import.meta.url)) main();
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
//
|
|
4
|
+
// THE FONTS TRAVEL WITH THE PAGE. A report used to link its typefaces from two font services, so opening
|
|
5
|
+
// one told those services the reader's address and that a report had been opened, from the portal, from
|
|
6
|
+
// disk or from an email alike. Every page this product writes now carries its fonts inside itself, as
|
|
7
|
+
// data, and fetches nothing to draw its text.
|
|
8
|
+
//
|
|
9
|
+
// ONE PLACE. The files are shared/fonts/*.woff2 (their provenance is in shared/fonts/README.md), and every
|
|
10
|
+
// surface that sets text takes its @font-face rules from here: the report renderers, the pool index pages,
|
|
11
|
+
// the portal's own sign-in pages and the staff profile page. The portal's browser bundle is the one
|
|
12
|
+
// exception: it imports the same files through its own stylesheet, because a bundle serves files rather
|
|
13
|
+
// than inlining them.
|
|
14
|
+
//
|
|
15
|
+
// EACH LICENCE RIDES BESIDE ITS FONT. Both are under the SIL Open Font License 1.1, which asks that every
|
|
16
|
+
// copy of the font carry its copyright notice and the licence. The font files carry both in their name
|
|
17
|
+
// records, and the full licence text goes into the page as a stylesheet comment right before the rules
|
|
18
|
+
// that embed the font, so a copy saved out of a report still has it.
|
|
19
|
+
//
|
|
20
|
+
// NOT FOR EMAIL. A mail client strips web fonts, and the embedded face would add tens of kilobytes to
|
|
21
|
+
// every message. Email keeps its font stack by name only.
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
|
|
25
|
+
const at = (name) => new URL(`./fonts/${name}`, import.meta.url);
|
|
26
|
+
|
|
27
|
+
/** The text face, as every stylesheet names it. */
|
|
28
|
+
export const TEXT_FACE = "Plus Jakarta Sans";
|
|
29
|
+
/** The code face, as every stylesheet names it. */
|
|
30
|
+
export const CODE_FACE = "Fira Code";
|
|
31
|
+
|
|
32
|
+
// The code points each file holds, from shared/fonts/README.md. Stated so a browser never asks a face for
|
|
33
|
+
// a character it does not have: Japanese, Greek and the rest go straight to the reader's system fonts.
|
|
34
|
+
export const FONT_UNICODE_RANGE = "U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,"
|
|
35
|
+
+ "U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2190-2193,U+2212,U+2215,U+FEFF,U+FFFD,U+0100-02BA,U+02BD-02C5,"
|
|
36
|
+
+ "U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,"
|
|
37
|
+
+ "U+2113,U+2C60-2C7F,U+A720-A7FF";
|
|
38
|
+
|
|
39
|
+
export const FONT_FILES = [
|
|
40
|
+
{ family: TEXT_FACE, file: "plus-jakarta-sans.woff2", licence: "OFL-plus-jakarta-sans.txt", weight: "200 800" },
|
|
41
|
+
{ family: CODE_FACE, file: "fira-code.woff2", licence: "OFL-fira-code.txt", weight: "300 700" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function faceCss({ family, file, licence, weight }) {
|
|
45
|
+
const licenceText = readFileSync(at(licence), "utf8").trim();
|
|
46
|
+
// A licence containing the comment terminator would end the comment early and spill into the stylesheet.
|
|
47
|
+
if (licenceText.includes("*/")) throw new Error(`${licence} contains "*/" and cannot be carried in a stylesheet comment`);
|
|
48
|
+
const data = readFileSync(at(file)).toString("base64");
|
|
49
|
+
return `/*\n${family}\n\n${licenceText}\n*/\n`
|
|
50
|
+
+ `@font-face{font-family:'${family}';font-style:normal;font-weight:${weight};font-display:swap;`
|
|
51
|
+
+ `src:url(data:font/woff2;base64,${data}) format('woff2');unicode-range:${FONT_UNICODE_RANGE}}\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [TEXT, CODE] = FONT_FILES.map(faceCss);
|
|
55
|
+
|
|
56
|
+
/** The text face alone, for pages that set no code: the pool index, the sign-in pages, the profile page. */
|
|
57
|
+
export const TEXT_FONT_STYLE = `<style>${TEXT}</style>`;
|
|
58
|
+
/** Both faces, for a report, which sets identifiers and dates in the code face. */
|
|
59
|
+
export const REPORT_FONT_STYLE = `<style>${TEXT}${CODE}</style>`;
|
package/shared/brand.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// • the Run status page (writer retired with the quality subsystem in)
|
|
8
8
|
//
|
|
9
9
|
// Built on the Cordillera brand package (v3, legacy — the values below are the record): limestone-cream + crimson,
|
|
10
|
-
//
|
|
10
|
+
// Plus Jakarta Sans. Crimson is the ONLY strong accent; warm health/risk tones replace traffic-light colours.
|
|
11
11
|
// Hex values are canonical here; CSS is case-insensitive, so casing in any rendered :root is irrelevant.
|
|
12
12
|
//
|
|
13
13
|
// Two ready-made :root blocks are exported because the two surface families use different token names:
|
|
@@ -253,8 +253,8 @@ export const PALETTE_DARK = {
|
|
|
253
253
|
codeBg: 'rgba(236,229,216,.08)',// parchment at 8%
|
|
254
254
|
};
|
|
255
255
|
|
|
256
|
-
//
|
|
257
|
-
|
|
256
|
+
// The typefaces themselves are not linked from anywhere: brand-fonts.mjs embeds them in each page that
|
|
257
|
+
// sets text, and this module only names them, so it stays free of file reads for the tools that import it.
|
|
258
258
|
|
|
259
259
|
// The canonical Cordillera ridge mark — the exact path from the v3 brand package (legacy; this constant IS the record now),
|
|
260
260
|
// — THE RIDGE IS GONE FROM EVERY SURFACE THIS PRODUCT RENDERS, and the constants went with it.
|
|
@@ -434,7 +434,7 @@ export const REPORT_ROOT =
|
|
|
434
434
|
// The mark-itself disclosure toggles. A DEDICATED token: --crimson has no dark variant, and
|
|
435
435
|
// reusing it left the toggles barely legible on the dark ground.
|
|
436
436
|
`--ma-toggle:#860F09;` +
|
|
437
|
-
`--font:'
|
|
437
|
+
`--font:'Plus Jakarta Sans','Helvetica Neue',system-ui,sans-serif;` +
|
|
438
438
|
`--mono:'Fira Code',ui-monospace,monospace;` +
|
|
439
439
|
`--shadow:0 1px 2px rgba(37,9,2,.05), 0 10px 30px rgba(37,9,2,.07);` +
|
|
440
440
|
`}`;
|
|
@@ -575,7 +575,7 @@ export const DOOR_ROOT =
|
|
|
575
575
|
`--code-bg:${P.codeBg};` +
|
|
576
576
|
// The doors' own font stack, not brand's: they must render with no bundle and no webfont, so the
|
|
577
577
|
// fallbacks are the platform's rather than Helvetica Neue / Fira Code.
|
|
578
|
-
`--font:"
|
|
578
|
+
`--font:"Plus Jakarta Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;` +
|
|
579
579
|
`--mono:ui-monospace,SFMono-Regular,Menlo,monospace}`;
|
|
580
580
|
|
|
581
581
|
// Same token names, dark values. Nothing here is a scoped rule override: every value the dark scheme
|
|
@@ -718,7 +718,7 @@ export const CHROME_CSS = `
|
|
|
718
718
|
thing. The three body elements that carried the class drop it too, and --wm-alpha goes with them:
|
|
719
719
|
a token nothing reads is a knob somebody re-wires.
|
|
720
720
|
NOTE FOR ANY FUTURE DECORATION ON body: the rule here pinned right:0 and never a negative inset,
|
|
721
|
-
because right:-2% put 26px of horizontal scrollbar on a 1280px report
|
|
721
|
+
because right:-2% put 26px of horizontal scrollbar on a 1280px report and pointer-events:none
|
|
722
722
|
does not exempt a box from scrollable overflow. That is a property of the position, not of this
|
|
723
723
|
rule, and scripts/report-overflow-check.mjs still measures it. */
|
|
724
724
|
/* Floating settings stack (theme + privacy) — pinned bottom-right, OUT of the nav bar. Icon-only: the toggle keeps its .tt-lbl/.anon-lbl text for screen readers but clips it visually.
|
|
@@ -75,9 +75,10 @@ export const ROOT_PREFIX = "ctb-";
|
|
|
75
75
|
* Returned rather than thrown so a caller can decide: a check that has already produced a result
|
|
76
76
|
* should say this and exit, not lose the result to an exception.
|
|
77
77
|
*/
|
|
78
|
-
export function rootRefusal(dir) {
|
|
78
|
+
export function rootRefusal(dir, { platform = process.platform } = {}) {
|
|
79
79
|
if (typeof dir !== "string" || dir === "") return "a browser temp root must be a non-empty path";
|
|
80
|
-
|
|
80
|
+
// On Windows the browser's singleton is a named pipe, not a socket at a file path, so no length binds.
|
|
81
|
+
if (platform === "win32" || dir.length <= MAX_ROOT_LENGTH) return null;
|
|
81
82
|
return (
|
|
82
83
|
`browser temp root is ${dir.length} characters and the limit is ${MAX_ROOT_LENGTH}: ${dir}\n` +
|
|
83
84
|
` A browser writes its process-singleton socket at <root>${LOCK_SUFFIX}, and a unix socket ` +
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// is explicit that an edit for brevity changes what a clearance concludes. So an override that has
|
|
7
7
|
// silently gone stale is a run answering from old doctrine with nothing to say so.
|
|
8
8
|
//
|
|
9
|
-
// WE RAN THIS EXPERIMENT ON OURSELVES. `driver/driver.config.mjs
|
|
9
|
+
// WE RAN THIS EXPERIMENT ON OURSELVES. the SKILL RESOLUTION note above `skillsBaseDir` in `driver/driver.config.mjs` records the repo split forking
|
|
10
10
|
// the whole tree: **30 of 37 shared files had silently drifted apart, in BOTH directions**. The
|
|
11
11
|
// overlay fixed the mechanism for us and fixes nothing for a self-hoster, whose copies win and whose
|
|
12
12
|
// upstream moves underneath them with nothing comparing the two.
|
package/shared/driver-dir.mjs
CHANGED
|
@@ -40,15 +40,23 @@
|
|
|
40
40
|
//
|
|
41
41
|
// ── WHAT THIS DELIBERATELY DOES NOT DO ────────────────────────────────────────────────────────────
|
|
42
42
|
//
|
|
43
|
-
//
|
|
44
|
-
// commit would be a semantic change riding inside a seam, and the seam is the part that has to be
|
|
45
|
-
// reviewable by reading nineteen call sites and agreeing they still mean what they meant. The mode is
|
|
46
|
-
// That business, and when it is taken it is one line in this file.
|
|
43
|
+
// ── THE MODE, TAKEN HERE ONCE (2026-09-23) ────────────────────────────────────────────────────────
|
|
47
44
|
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
45
|
+
// A new run folder is created 0750: its owner, and the group the deployment reads reports through, and
|
|
46
|
+
// no other account. It used to take whatever the umask gave, 775 on a real run, so on a machine whose
|
|
47
|
+
// home or data folder other accounts can enter, any of them could read client matter. The folder that
|
|
48
|
+
// holds a run is created by the call below (measured by tracing a whole clearance: `ensureDriverDir` is
|
|
49
|
+
// the first write into a new run folder, and the archive is a rename that keeps the mode), so this one
|
|
50
|
+
// line closes the live run and its archived copy. The published copy takes the same constant.
|
|
51
|
+
//
|
|
52
|
+
// A MODE GIVEN TO mkdir, NEVER A chmod. The kernel sets a new folder's set-GID bit from its parent
|
|
53
|
+
// whatever mode is asked for, which is what the set-GID pool root relies on; a chmod by an account
|
|
54
|
+
// outside that group strips the bit silently and every report then answers 403. So nothing here ever
|
|
55
|
+
// changes an existing folder: a folder that already exists keeps the mode it has.
|
|
56
|
+
//
|
|
57
|
+
// IT DOES NOT RESTRAIN THE SEAT. The agent runs as the same account that owns these directories, so owner
|
|
58
|
+
// bits apply whatever the group bits say. Said here because a module named for a boundary invites the
|
|
59
|
+
// stronger reading.
|
|
52
60
|
//
|
|
53
61
|
// IT IS NOT `driverDirs`. `scripts/seat-retry-report.mjs` exports `driverDirs(root)`, which WALKS a tree
|
|
54
62
|
// to find every `_driver` beneath it. Discovery, not construction — a different concern that happens to
|
|
@@ -60,6 +68,34 @@ import { join } from "node:path";
|
|
|
60
68
|
/** The directory's name. The one place the string lives, so renaming it is an edit rather than a sweep. */
|
|
61
69
|
export const DRIVER_DIR = "_driver";
|
|
62
70
|
|
|
71
|
+
/** The mode a new run folder is created with: owner and group, no other account. See the header. */
|
|
72
|
+
export const RUN_DIR_MODE = 0o750;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A name inside `_driver/` as it is written on `platform`.
|
|
76
|
+
*
|
|
77
|
+
* ON WINDOWS A COLON CANNOT BE IN A FILE NAME, and a stage's label carries one (`register-unit:primary-
|
|
78
|
+
* sweep`, `common-law-half:a`, `report-card:1`), which names its attempt log, its dispatch record and its
|
|
79
|
+
* input stamp. NTFS does not refuse the name: it reads `register-unit:primary-sweep.jsonl` as a hidden
|
|
80
|
+
* stream of a file called `register-unit`, so every register unit's log lands in that one file, reading it
|
|
81
|
+
* back by the same name appears to work, and the copy into an experiment's sandbox fails with EINVAL.
|
|
82
|
+
* Measured on a Windows runner, 2026-09-23. There the colon is written `%3A`, which no label contains, so
|
|
83
|
+
* the name still reads back to its label. Everywhere else the name is left exactly as it was.
|
|
84
|
+
*/
|
|
85
|
+
export function driverFileName(name, platform = process.platform) {
|
|
86
|
+
return platform === "win32" ? String(name).replaceAll(":", "%3A") : name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The stage label a `_driver/` file was written for: `register-unit%3Aprimary-sweep.jsonl` →
|
|
91
|
+
* `register-unit:primary-sweep`. Every platform decodes, because no label holds `%`, so a record written
|
|
92
|
+
* on Windows reads back under its label wherever it is read. `ext` is the suffix to take off.
|
|
93
|
+
*/
|
|
94
|
+
export function labelOfDriverFile(name, ext = ".jsonl") {
|
|
95
|
+
const base = String(name ?? "");
|
|
96
|
+
return (base.endsWith(ext) ? base.slice(0, -ext.length) : base).replaceAll("%3A", ":");
|
|
97
|
+
}
|
|
98
|
+
|
|
63
99
|
/**
|
|
64
100
|
* The path to a run's `_driver/`, or to something inside it.
|
|
65
101
|
*
|
|
@@ -67,7 +103,7 @@ export const DRIVER_DIR = "_driver";
|
|
|
67
103
|
* @param {...string} parts optional path segments beneath it
|
|
68
104
|
*/
|
|
69
105
|
export function driverDir(base, ...parts) {
|
|
70
|
-
return join(base, DRIVER_DIR, ...parts);
|
|
106
|
+
return join(base, DRIVER_DIR, ...parts.map((part) => driverFileName(part)));
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
/**
|
|
@@ -79,7 +115,7 @@ export function driverDir(base, ...parts) {
|
|
|
79
115
|
* reaching this directory by one.
|
|
80
116
|
*/
|
|
81
117
|
export function driverRel(...parts) {
|
|
82
|
-
return join(DRIVER_DIR, ...parts);
|
|
118
|
+
return join(DRIVER_DIR, ...parts.map((part) => driverFileName(part)));
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
/**
|
|
@@ -91,6 +127,6 @@ export function driverRel(...parts) {
|
|
|
91
127
|
*/
|
|
92
128
|
export function ensureDriverDir(base, ...parts) {
|
|
93
129
|
const dir = driverDir(base, ...parts);
|
|
94
|
-
mkdirSync(dir, { recursive: true });
|
|
130
|
+
mkdirSync(dir, { recursive: true, mode: RUN_DIR_MODE });
|
|
95
131
|
return dir;
|
|
96
132
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|