clearotron 0.3.2-beta.4 → 0.3.2-beta.6

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/INSTALL.md CHANGED
@@ -331,6 +331,7 @@ terminal and is rewritten on every start.
331
331
  ~/trademark/config/profiles the company profiles
332
332
  ~/trademark/grants.json who may use this install
333
333
  ~/trademark/portal-audit.log what the portal was asked to do
334
+ ~/trademark/telemetry/trademark-mcp-access.jsonl what a connected assistant asked for, and what it was told
334
335
  ~/trademark/portal-local-credential.json the sign-in this install minted for you
335
336
  ```
336
337
 
package/bin/onboard.mjs CHANGED
@@ -2702,6 +2702,71 @@ export async function runCheck() {
2702
2702
  + `Run \`${invoke("start")}\` — it creates the list.`);
2703
2703
  }
2704
2704
  }
2705
+ // ── IS THIS DOOR'S ACCESS LOG BEING WRITTEN, AND WHERE? ───────────────────────────────────────────
2706
+ //
2707
+ // A client's assistant can list runs, read reports and pull artifacts through this door. The record of
2708
+ // what a key did is the only way to answer a client, a lawyer or an incident review afterwards, and a
2709
+ // log nobody can find is a log nobody checks.
2710
+ //
2711
+ // THE OVERRIDE IS READ FROM THE UNIT, NOT FROM THIS PROCESS. `TRADEMARK_MCP_AUDIT_LOG` decides where
2712
+ // the writer appends, and the writer is the service — so this command's own environment is not the
2713
+ // answer and using it would produce a confident report about a file the door never touches. Where the
2714
+ // two disagree, that disagreement is the finding and is said out loud rather than resolved silently.
2715
+ //
2716
+ // WITH NO OVERRIDE THERE ARE TWO CANDIDATES, and which one the writer picked depends on what existed
2717
+ // when IT started — a legacy path is kept if it is already there, so that a compliance trail is not
2718
+ // split in two by a moved default. `auditPaths()` is the module's own reader for exactly this and is
2719
+ // called rather than re-derived; re-deriving that branch here is the defect its source warns about.
2720
+ try {
2721
+ const { auditPaths: auditCandidates } = await import(pathToFileURL(join(REPO, "mcp-server", "lib", "audit.mjs")).href);
2722
+ const seen = hosted ? unitValue(unitEnv, "TRADEMARK_MCP_AUDIT_LOG") : { state: "unset", value: null };
2723
+ if (seen.state === "unknown") {
2724
+ info(couldNotDetermine("TRADEMARK_MCP_AUDIT_LOG", unitEnv) + " The access log's location cannot be named.");
2725
+ } else {
2726
+ const mine = String(process.env.TRADEMARK_MCP_AUDIT_LOG ?? "").trim();
2727
+ if (seen.state === "unset" && mine) {
2728
+ warn(`this shell sets TRADEMARK_MCP_AUDIT_LOG and the units do not, so the door is NOT writing where `
2729
+ + `this command would look. The door's log is at its default; ${mine} is this shell's answer only.`);
2730
+ }
2731
+ const paths = seen.state === "set" ? [seen.value] : auditCandidates();
2732
+ let wrote = null;
2733
+ for (const p of paths) {
2734
+ try { const s = statSync(p); if (s.size > 0) { wrote = p; break; } } catch { /* not this one */ }
2735
+ }
2736
+ if (wrote) {
2737
+ ok(`the client door's access log is being written: ${wrote}`);
2738
+ } else {
2739
+ // NOT-YET-WRITTEN IS NOT UNWRITABLE. A door nobody has called through has no log, and saying it
2740
+ // is broken would be the same lie as calling an unreachable unit inactive.
2741
+ //
2742
+ // NAME THE ONE THE WRITER WOULD PICK, WHICH IS NOT SIMPLY THE FIRST. With no override there are
2743
+ // two candidates and the older is kept only when it is ALREADY THERE, so that a compliance trail
2744
+ // is not split by a moved default. Naming the older one unconditionally would print the
2745
+ // integrator platform's folder at setup on installs that have no such file — a folder this engine
2746
+ // does not require and whose mention was deliberately removed from this command. Where it does
2747
+ // exist it IS the log and is named, because that is then the true answer.
2748
+ const existing = paths.find((c) => { try { statSync(c); return true; } catch { return false; } });
2749
+ const willUse = existing ?? paths[paths.length - 1];
2750
+ const dir = dirname(willUse);
2751
+ let writable = null;
2752
+ try { accessSync(dir, constants.W_OK); writable = true; } catch (e) { writable = e?.code === "ENOENT" ? null : false; }
2753
+ if (writable === false) {
2754
+ blocking(`the client door's access log cannot be written: ${dir} is not writable by this user. `
2755
+ + "Every call through this door would go unrecorded, and an absent record reads as nothing happened.");
2756
+ } else if (writable === null) {
2757
+ info(`no access log yet at ${willUse} and its directory does not exist — the door creates it on the `
2758
+ + "first call. Nothing has come through it, or nothing has since the path last changed.");
2759
+ } else {
2760
+ info(`no access log yet at ${willUse}, and its directory is writable — nothing has come through `
2761
+ + "this door yet. The first call creates it.");
2762
+ }
2763
+ }
2764
+ }
2765
+ } catch (e) {
2766
+ info(`could not check the client door's access log (${e?.code ?? e?.message ?? e}) — this is a report `
2767
+ + "that this command could not look, not that the log is missing.");
2768
+ }
2769
+
2705
2770
  try {
2706
2771
  const { clientDoorState: doorState, describeDoorState, connectKeyReport, CLIENT_DOOR_UNIT: doorUnit } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2707
2772
  const { loadGrants: readGrantsFile, isRevoked: revokedCheck } = await import(pathToFileURL(join(REPO, "shared", "scope.mjs")).href);
package/bin/start.mjs CHANGED
@@ -449,6 +449,10 @@ export function installPaths(base) {
449
449
  locks: join(base, "locks"),
450
450
  grants: join(base, "grants.json"),
451
451
  audit: join(base, "portal-audit.log"),
452
+ // The connector doors' access log — the record of what a client key did, which is what answers a
453
+ // client or a review afterwards. NAMED IN THE LAYOUT rather than built at the call site, for the
454
+ // reason the store below gives: a demo's isolation must be a property of where things live.
455
+ accessLog: join(base, "telemetry", "trademark-mcp-access.jsonl"),
452
456
  // The saved-search store is a git repository because recipe-service commits every save through
453
457
  // `git add` + `git commit` — that is the product's config-store model, not something invented here.
454
458
  // It is its own directory, NOT `base`, so the commit path never sees the pool, the queue or the
@@ -680,6 +684,16 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
680
684
  "PROFILE_AUDIT": "",
681
685
  "RECIPE_AUDIT": "",
682
686
  "CLEAROTRON_FEEDBACK_DIR": "",
687
+ // AN EXPLICIT PATH, NOT "" LIKE THE TWO ABOVE. Empty is unset, and unset is what sends this one to
688
+ // the REAL install's home: its default is computed from `homedir()`, not from any directory a demo
689
+ // controls. So a demo left to fall back writes its access lines into the install beside it, and the
690
+ // demo stops being one directory a reader can delete. Measured as exactly that: a demo run left
691
+ // `telemetry/trademark-mcp-access.jsonl` in the install it booted next to.
692
+ //
693
+ // ONLY IN A DEMO. A real install must NOT have this pinned: with no override the writer keeps an
694
+ // older log where one already exists, so that moving the default does not split a compliance trail
695
+ // in two. Pinning the name here would override that and do the splitting.
696
+ "TRADEMARK_MCP_AUDIT_LOG": paths.accessLog,
683
697
  } : {}),
684
698
  });
685
699
  return {
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "9bb3ab371e736a3b4a80f2d6dbdd71fd719b173e",
3
- "version": "0.3.2-beta.4"
2
+ "commit": "91905bd9b7ffd04985537f799d2ef49359e3d0e6",
3
+ "version": "0.3.2-beta.6"
4
4
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "ts": "2026-09-04T21:48:51.264Z",
2
+ "ts": "2026-09-16T16:48:57.832Z",
3
3
  "lane": "knockout",
4
4
  "checks": [
5
5
  {
@@ -43,9 +43,33 @@
43
43
  "surface": "findings",
44
44
  "pass": true,
45
45
  "detail": ""
46
+ },
47
+ {
48
+ "id": "reviewer-note-subject",
49
+ "family": "voice",
50
+ "surface": "findings",
51
+ "pass": true,
52
+ "detail": ""
53
+ },
54
+ {
55
+ "id": "plain-language-vocabulary",
56
+ "family": "voice",
57
+ "surface": "findings",
58
+ "pass": false,
59
+ "detail": "the lawyer's vocabulary on lines a reader meets before opening anything — rewrite the sentence in the words the reader already owns, do not swap the word: the batch summary: \"belt-and-braces\" → \"extra\"; the batch summary: \"chunk\" (an engine word — cut it)"
60
+ },
61
+ {
62
+ "id": "plain-language-sentence-length",
63
+ "family": "voice",
64
+ "surface": "findings",
65
+ "pass": false,
66
+ "detail": "a default-visible sentence carrying more than one idea (over 25 words) — split it, conclusion first: the batch summary: 28 words; the batch summary: 28 words; the batch summary: 27 words; the batch summary: 39 words; the batch summary: 26 words; +4 more"
46
67
  }
47
68
  ],
48
- "failures": [],
69
+ "failures": [
70
+ "plain-language-vocabulary",
71
+ "plain-language-sentence-length"
72
+ ],
49
73
  "notApplicable": [
50
74
  {
51
75
  "id": "names-cell-populated",
@@ -1,5 +1,21 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.2-beta.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 911b0b4: New: The report a client opens has been redrawn. It opens with what was asked, then the rating with its reasons, then the conflicts. What a search is and is not is no longer narrated at length.
8
+
9
+ New: A finding's full detail shows the goods as the register recorded them, and the record's own dates. This is on a full country search.
10
+
11
+ New: A knockout report closes with what happens next. The paragraph moves out of the long read rather than being repeated.
12
+
13
+ ## 0.3.2-beta.5
14
+
15
+ ### Patch Changes
16
+
17
+ - a4ea94d: For operators: The connector access log now records what happened to each call, and records calls that were refused as well as calls that got through. A line also names which door took the call, so a client key and a staff session can be told apart. Before, a call was recorded only as having been made, and a refused one left no line at all. `doctor` now names where that log is being written. It reads that from the service's own settings, not from the shell you are typing in.
18
+
3
19
  ## 0.3.2-beta.4
4
20
 
5
21
  ### Patch Changes
@@ -611,6 +611,32 @@ export function findDroppedConnotationQueries(spec, mergedGrid) {
611
611
  * Deliberately NOT counted as searched: laundering an honest error into a clean receipt would be a worse
612
612
  * defect than the one being fixed.
613
613
  */
614
+ /**
615
+ * Of a caller's OWN list of missing queries, which ones did the plugin honestly report as having thrown?
616
+ *
617
+ * The sibling below derives its missing list itself, from the full spec. This one is handed one, because
618
+ * the meaning-seat gate computes a different list: it joins per half on `queryKey` where this module
619
+ * joins the merged grid on `norm`. Two callers, two populations, ONE author of what a gap row means —
620
+ * which is the point of the split. Reuse the key, not the question.
621
+ *
622
+ * Returns the CALLER'S query verbatim, never the gap row's term, so the caller can index what comes back
623
+ * against the list it passed in. A gap term that matched is by definition the same query under `norm`,
624
+ * and handing back the other spelling would make the caller join twice on two keys to use the answer.
625
+ */
626
+ export function erroredConnotationQueriesAmong(missingQueries, mergedGrid) {
627
+ const gaps = (Array.isArray(mergedGrid?.gaps) ? mergedGrid.gaps : [])
628
+ .filter((g) => g && String(g.platform ?? "").toLowerCase() === "connotation");
629
+ if (!gaps.length) return [];
630
+ const out = [];
631
+ for (const m of missingQueries ?? []) {
632
+ const k = norm(String(m ?? ""));
633
+ if (!k) continue;
634
+ const hit = gaps.find((g) => norm(String(g.term ?? "").trim()) === k);
635
+ if (hit) out.push({ query: m, error: String(hit.error ?? "unspecified") });
636
+ }
637
+ return out;
638
+ }
639
+
614
640
  export function findErroredConnotationQueries(spec, mergedGrid) {
615
641
  const missing = new Set(findDroppedConnotationQueries(spec, mergedGrid).map(norm));
616
642
  if (!missing.size) return [];
@@ -488,7 +488,7 @@ export const E3_BACKLOG = [
488
488
  where: "driver/stages.mjs:3231",
489
489
  surface: "stage-message",
490
490
  evidence: "- meters: {\"mark_similarity\":{...},\"goods_proximity\":{...},\"use\":{...},\"enforcer\":{...}} — all four present, each {\"token\",\"basis\",\"source\"}. … mark_similarity = high | medium | low. goods_proximity = high | medium | low. enforcer = high | medium | low | unknown. use = confirmed | not-confirmed | un",
491
- reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; driver/verify.mjs:1127 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
491
+ reparsedBy: "driver/findings-model.mjs:813 parseFindingsJson; driver/verify.mjs:1153 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
492
492
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
493
493
  },
494
494
  {
@@ -684,7 +684,7 @@ export const E3_BACKLOG = [
684
684
  // because an un-anchorable row lands in the NOT-CHECKED slice, and that is coverage lost rather than
685
685
  // a pass.
686
686
  evidence: "is ONE of coverage-disposition | fact | rating | narrative — pick the one your own legal read says the correction IS: coverage-disposition (a coverage row / disposition placement is wrong or dishonest)",
687
- reparsedBy: "driver/verify.mjs:803 CORRECTION_KIND_RE = /\\[kind:\\s*([a-z][a-z-]*)\\s*\\]/i → parseCorrectionKinds (verify.mjs:1026), consumed in pipelineInner() in pipeline.mjs for the run.jsonl `correction-kinds` histogram, which since #1558 also carries `kindChannelOk` — the counts are DERIVED from the parsed rows, and that key states whether the reviewer's kind channel produced anything at all rather than leaving a reader to infer it by comparing untyped against total. Telemetry only today",
687
+ reparsedBy: "driver/verify.mjs:829 CORRECTION_KIND_RE = /\\[kind:\\s*([a-z][a-z-]*)\\s*\\]/i → parseCorrectionKinds (verify.mjs:1052), consumed in pipelineInner() in pipeline.mjs for the run.jsonl `correction-kinds` histogram, which since #1558 also carries `kindChannelOk` — the counts are DERIVED from the parsed rows, and that key states whether the reviewer's kind channel produced anything at all rather than leaving a reader to infer it by comparing untyped against total. Telemetry only today",
688
688
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
689
689
  },
690
690
  {
@@ -697,7 +697,7 @@ export const E3_BACKLOG = [
697
697
  // `[on: -]` case went from a value to an ABSENCE — you omit the field — which is the one part a
698
698
  // reader could get wrong from the old wording, since there is no value meaning "no finding".
699
699
  evidence: "**AND EVERY FLAG CARRIES WHICH FINDING IT IS ABOUT** — the `on` field, an array of ordinals. Same rule as `kind`: you send the values, the driver renders the token.",
700
- reparsedBy: "driver/verify.mjs:820 CORRECTION_ON_RE = /\\[on:\\s*([0-9,\\s-]*?)\\s*\\]/i. SKILL-FILE ONLY — the stage message at stages.mjs:1842-1877 never mentions `[on:]`. This is #850's \"the element shape is in the skill file, not the stage message\" in its purest form: an E3 lint reading stages.mjs alone sees the [kind:] token and misses its twin",
700
+ reparsedBy: "driver/verify.mjs:846 CORRECTION_ON_RE = /\\[on:\\s*([0-9,\\s-]*?)\\s*\\]/i. SKILL-FILE ONLY — the stage message at stages.mjs:1842-1877 never mentions `[on:]`. This is #850's \"the element shape is in the skill file, not the stage message\" in its purest form: an E3 lint reading stages.mjs alone sees the [kind:] token and misses its twin",
701
701
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
702
702
  },
703
703
  {
@@ -17,12 +17,12 @@
17
17
  //
18
18
  // D1 verify.mjs:940 fail(`connotation_${reason}:…`) — reason iterates the table at line 894
19
19
  // D2 verify.mjs fail(String(e.message)) — parseFindingsJson throws token-first
20
- // D3 verify.mjs:1184 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
20
+ // D3 verify.mjs:1210 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
21
21
  // D4 verify.mjs parseCoverageLedgerJson, same shape
22
22
  // D5 verify.mjs:1504 fail(`${unaccounted[0].token}:…`) — token minted in a DATA ROW
23
23
  // D6 verify.mjs:1567 fail(`${violations[0].token}…`) — validatePlanFeasibility in register-plan.mjs
24
24
  // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2018 disclosureTextByAxis
25
- // D8 verify.mjs:1692 fail(caseLawLedgerFail(…)) — token built in case-law-ledger.mjs:204
25
+ // D8 verify.mjs:2425 caseLawLedgerFail fail(caseLawLedgerFail(…)) — token built in case-law-ledger.mjs:204 caseLawLedgerFail
26
26
  //
27
27
  // A partition built on the 60 tokens a regex CAN see would run green while blind to the rest, which is
28
28
  // worse than having no E2 at all: it certifies a partition it never checked. So the census is authored
@@ -109,7 +109,7 @@ export const VOCABULARY = [
109
109
  { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
110
110
  { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
111
111
  { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
112
- { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1510 coverageFormFail", family: "driver/register-plan.mjs:1642 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
112
+ { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1536 coverageFormFail", family: "driver/register-plan.mjs:1642 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
113
113
  { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1417 validatePlanFeasibility", dynamic: "D6" },
114
114
  { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1766 searchedJurisdictionsFromPlan", dynamic: "D6" },
115
115
  { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2018 disclosureTextByAxis", dynamic: "D7" },
@@ -130,7 +130,7 @@ export const VOCABULARY = [
130
130
  { token: "variantmodel_term_markup", stages: ["prelim-variants"], site: "driver/verify.mjs" },
131
131
  { token: "variantmodel_missing", stages: ["prelim-variants"], site: "driver/verify.mjs:1153, 1212" },
132
132
  // Recovered during E2 authoring, absent from the draft census: variant-manifest.json is strict-parsed
133
- // through checkSiblingJson (verify.mjs:1197) → checkJson (:742), so the WHOLE variantmodel_* family
133
+ // through checkSiblingJson (verify.mjs:1223) → checkJson (:742), so the WHOLE variantmodel_* family
134
134
  // reaches prelim-variants, not just the four literal tokens above.
135
135
  // CONVERSION 3 widened this family's SOURCE without widening its prefix. `acceptPrelimVariants` raises
136
136
  // `variantmodel_scope_layer_invalid`, `_scope_status_invalid`, `_scope_item_missing` and `_scope_pipe`
@@ -184,7 +184,7 @@ export const VOCABULARY = [
184
184
  // ── case-law / narrative-refutation ────────────────────────────────────────────────────────────────
185
185
  { token: "caselaw_ledger_missing", stages: ["case-law"], site: "driver/verify.mjs:1682, 1685" },
186
186
  { token: "caselaw_ledger_unparseable", stages: ["case-law"], site: "driver/verify.mjs" },
187
- { token: "caselaw_ledger", stages: ["case-law"], site: "driver/verify.mjs:1692", family: "driver/case-law-ledger.mjs:204 (census reasons)", dynamic: "D8" },
187
+ { token: "caselaw_ledger", stages: ["case-law"], site: "driver/verify.mjs:2425 caseLawLedgerFail", family: "driver/case-law-ledger.mjs caseLawLedgerFail (census reasons)", dynamic: "D8" },
188
188
  { token: "no_verdict_line", stages: ["narrative-refutation"], site: "driver/verify.mjs" },
189
189
  { token: "plan_audit_missing", stages: ["narrative-refutation"], site: "driver/verify.mjs" },
190
190
 
@@ -477,7 +477,7 @@ export const INNER_CODES = Object.freeze([
477
477
  // `axis_invalid` cause into its own family before counting the rest. A ruling naming only
478
478
  // `coverage_no_status` would be true of most `no_status` records and false of the ones that matter most.
479
479
  { code: "no_status", mints: ["driver/coverage-form.mjs:836"], rollsUpTo: ["coverage_no_status", "coverage_form_axis_invalid"],
480
- why: "Two composites, split on the record's `cause`: verify.mjs:1172 for cause `axis_invalid`, verify.mjs:1263 for the rest. NOT cited at coverage-form.mjs:810 — that is the JSDoc @returns annotation, not the mint." },
480
+ why: "Two composites, split on the record's `cause`: `checkFindingsSibling()` in verify.mjs for cause `axis_invalid`, `dispositionForm()` there for the rest. NOT the `@returns` annotation above `COVERAGE_CAUSES` in coverage-form.mjs — that is the annotation, not the mint." },
481
481
  { code: "engine_vocabulary", mints: ["driver/coverage-form.mjs:826"], rollsUpTo: ["coverage_form_engine_vocabulary"],
482
482
  why: "#669 — the seat wrote an engine token into the `reason` sentence that reaches the reader's page. Checked on settled rows too, because a row the seat considers finished is exactly the one whose sentence gets printed. Namespaced at verify.mjs:1165; the bare code names a row, never a stage." },
483
483
 
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.4",
5
+ "version": "0.3.2-beta.6",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -6437,8 +6437,10 @@ function injectFrontMatter(path, fields, runDir = null) {
6437
6437
  // spec 62 — the per-field CONFIGURATION PROVENANCE rows for a project run: for each PROJECT_KEY, its effective
6438
6438
  // value (compact) and where it came from (project | customer | house). Read off the frozen sidecar `pr` (both
6439
6439
  // origins and the effective values live there), serialised to a single-line JSON string for the report
6440
- // front-matter, and rendered INTERNAL-ONLY in the scope fold (never on the client export — provenance, not
6441
- // process-exhaust). Absent origins ⇒ [] ⇒ nothing injected (no-project runs are untouched).
6440
+ // front-matter. It was rendered internal-only in the scope fold; the 2026-09-16 redesign takes that fold
6441
+ // off the page, and this row set went with it — it was the one block in there labelled internal, which is
6442
+ // the class that ruling removed. IT IS STILL RECORDED, here and in the workbook, which is where a reader
6443
+ // who wants to know which layer set a knob now finds out. Absent origins ⇒ [] ⇒ nothing injected.
6442
6444
  function originRowsForReport(pr) {
6443
6445
  if (!pr?.origins) return [];
6444
6446
  const short = (s, n = 48) => { const t = String(s ?? "").trim(); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };