clearotron 0.2.4 → 0.3.0-beta.1

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.
Files changed (235) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +62 -38
  4. package/bin/brandowner.mjs +18 -169
  5. package/bin/clearotron.mjs +3 -1
  6. package/bin/connect.mjs +28 -19
  7. package/bin/disconnect.mjs +3 -3
  8. package/bin/example.mjs +7 -7
  9. package/bin/framework-preflight.mjs +49 -0
  10. package/bin/grant.mjs +151 -93
  11. package/bin/onboard.mjs +220 -138
  12. package/bin/start.mjs +146 -137
  13. package/bin/stop.mjs +2 -2
  14. package/bin/update.mjs +1 -1
  15. package/build-info.json +2 -2
  16. package/docs/CLIENT-MCP.md +2 -2
  17. package/docs/E2E.md +12 -2
  18. package/docs/ONBOARDING.md +1 -1
  19. package/docs/PORTAL.md +14 -13
  20. package/docs/SECURITY.md +23 -24
  21. package/docs/architecture/04-configuration-reference.md +12 -5
  22. package/docs/architecture/05-config-governance.md +7 -7
  23. package/docs/architecture/07-quality-and-audit.md +1 -1
  24. package/docs/architecture/08-development-guide.md +5 -0
  25. package/docs/configuration.md +118 -0
  26. package/docs/decisions/0004-documentation-structure.md +2 -2
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
  28. package/driver/CHANGELOG.md +70 -0
  29. package/driver/ask-ledger.mjs +2 -2
  30. package/driver/cancel.mjs +27 -0
  31. package/driver/case-law-sources.mjs +3 -3
  32. package/driver/company-bundle.mjs +261 -0
  33. package/driver/compare.mjs +1 -1
  34. package/driver/compose-read.mjs +2 -2
  35. package/driver/config-inventory.mjs +2 -2
  36. package/driver/contract-audit.mjs +1 -1
  37. package/driver/contract-e3-backlog.mjs +1 -1
  38. package/driver/contract-vocabulary.mjs +1 -1
  39. package/driver/declination-call.mjs +1 -1
  40. package/driver/deliver-trigger.sh +3 -3
  41. package/driver/dev-portal.mjs +3 -1
  42. package/driver/digest-queue.mjs +1 -1
  43. package/driver/disposition-tool.mjs +1 -1
  44. package/driver/doc-constants.mjs +1 -1
  45. package/driver/drain-posture.mjs +2 -2
  46. package/driver/drainer-identity.mjs +1 -1
  47. package/driver/driver.config.mjs +44 -9
  48. package/driver/effective-scope.mjs +30 -1
  49. package/driver/effort-model.mjs +6 -6
  50. package/driver/engine/CONTRACT.md +2 -2
  51. package/driver/engine/anthropic-agent.mjs +11 -11
  52. package/driver/engine/jx-turn.mjs +1 -1
  53. package/driver/engine/mcp/gather-config.mjs +29 -4
  54. package/driver/engine/mcp/recording-server.mjs +73 -1
  55. package/driver/engine/openai-agent.mjs +1 -1
  56. package/driver/engine/probe.mjs +28 -4
  57. package/driver/enqueue-schema.mjs +23 -3
  58. package/driver/findings-model.mjs +2 -2
  59. package/driver/flag-snapshot.mjs +2 -2
  60. package/driver/floor-duty.mjs +2 -2
  61. package/driver/frame-diff-model.mjs +1 -1
  62. package/driver/framework-preflight.mjs +143 -0
  63. package/driver/gateway.mjs +9 -1
  64. package/driver/hit-list.mjs +1 -1
  65. package/driver/jx-lanes.mjs +1 -1
  66. package/driver/jx.mjs +1 -1
  67. package/driver/knockout-assess-record.mjs +1 -1
  68. package/driver/knockout-review-record.mjs +435 -0
  69. package/driver/order-probe.mjs +1 -1
  70. package/driver/outbox-backoff.mjs +2 -2
  71. package/driver/owner-use-check.mjs +2 -2
  72. package/driver/package.json +1 -1
  73. package/driver/pipeline-knockout.mjs +105 -8
  74. package/driver/pipeline.mjs +81 -30
  75. package/driver/plain-register.mjs +77 -3
  76. package/driver/portal-access.mjs +141 -74
  77. package/driver/portal-config-view.mjs +59 -70
  78. package/driver/portal-report.mjs +4 -4
  79. package/driver/portal-service.mjs +348 -141
  80. package/driver/portal-upstream.mjs +105 -17
  81. package/driver/predelivery-lint.mjs +43 -18
  82. package/driver/product-rows.mjs +1 -1
  83. package/driver/products.mjs +1 -1
  84. package/driver/profile-page.html +30 -5
  85. package/driver/profile-service.mjs +197 -26
  86. package/driver/profiles.mjs +48 -1
  87. package/driver/publish/index.mjs +31 -13
  88. package/driver/publish/knockout.mjs +9 -5
  89. package/driver/publish/office-record-links.mjs +189 -0
  90. package/driver/publish/parse.mjs +3 -3
  91. package/driver/publish/publish-inputs.mjs +26 -0
  92. package/driver/publish/render-knockout.mjs +42 -42
  93. package/driver/publish/render.mjs +29 -4
  94. package/driver/publish/report-data.mjs +2 -2
  95. package/driver/publish/seed-pool.mjs +1 -1
  96. package/driver/publish/templates/report.css +8 -8
  97. package/driver/publish/xlsx.mjs +49 -7
  98. package/driver/queue-watch-verdict.mjs +2 -2
  99. package/driver/recipe-service.mjs +1 -1
  100. package/driver/record-carry.mjs +1 -1
  101. package/driver/reference-score.mjs +1 -1
  102. package/driver/reference-strip-signatures.mjs +1 -1
  103. package/driver/register-availability.mjs +4 -3
  104. package/driver/register-count.mjs +3 -3
  105. package/driver/register-records.mjs +1 -1
  106. package/driver/repair-composers.mjs +1 -1
  107. package/driver/repairs.mjs +3 -3
  108. package/driver/replay-archive.mjs +1 -1
  109. package/driver/report-card-record.mjs +1 -1
  110. package/driver/result-noun-fields.mjs +5 -0
  111. package/driver/roster-verdict.mjs +48 -5
  112. package/driver/run-activity.mjs +1 -1
  113. package/driver/run-requirements.mjs +18 -5
  114. package/driver/runner.mjs +24 -15
  115. package/driver/search-policy.mjs +8 -8
  116. package/driver/senior-rights.mjs +1 -1
  117. package/driver/skills/prelim-search/delivery-contract.md +1 -1
  118. package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
  119. package/driver/stages-knockout.mjs +72 -6
  120. package/driver/stages.mjs +10 -10
  121. package/driver/suite-census.json +238 -70
  122. package/driver/synthesis-record.mjs +2 -2
  123. package/driver/systemd/clearotron-client-mcp.service +3 -3
  124. package/driver/systemd/clearotron-deploy.service +2 -2
  125. package/driver/systemd/clearotron-mcp-face.service +1 -1
  126. package/driver/systemd/clearotron-portal.service +3 -3
  127. package/driver/systemd/clearotron-worker.service +5 -5
  128. package/driver/systemd/install-census.mjs +1 -1
  129. package/driver/systemd/render-units.mjs +9 -9
  130. package/driver/terminal-clamp.mjs +1 -1
  131. package/driver/trigger-cap.mjs +18 -2
  132. package/driver/unit-inventory.mjs +8 -8
  133. package/driver/usage-ledger.mjs +5 -3
  134. package/driver/verify-knockout.mjs +7 -7
  135. package/driver/verify.mjs +5 -5
  136. package/driver/whatif-memo-run.mjs +1 -1
  137. package/driver/whatif-queue.mjs +3 -3
  138. package/driver/whatif-worker.mjs +2 -2
  139. package/examples/README.md +1 -1
  140. package/examples/grants.example.json +25 -24
  141. package/mcp-server/CHANGELOG.md +10 -0
  142. package/mcp-server/http-server.mjs +3 -3
  143. package/mcp-server/key-socket.mjs +1 -1
  144. package/mcp-server/lib/audit-view.mjs +3 -3
  145. package/mcp-server/lib/brief.mjs +3 -3
  146. package/mcp-server/lib/driver.mjs +1 -1
  147. package/mcp-server/lib/events.mjs +1 -1
  148. package/mcp-server/lib/http-handler.mjs +2 -2
  149. package/mcp-server/lib/instructions.mjs +2 -2
  150. package/mcp-server/lib/knockout.mjs +1 -1
  151. package/mcp-server/lib/ops.mjs +6 -3
  152. package/mcp-server/lib/options.mjs +15 -4
  153. package/mcp-server/lib/plan.mjs +7 -6
  154. package/mcp-server/lib/runs.mjs +10 -0
  155. package/mcp-server/lib/whatif.mjs +5 -5
  156. package/mcp-server/package.json +1 -1
  157. package/mcp-server/packs/README.md +1 -1
  158. package/mcp-server/remote/client-mcp-apikey.service +1 -1
  159. package/mcp-server/remote/client-mcp.service +2 -2
  160. package/mcp-server/remote/trademark-artifacts-http.service +1 -1
  161. package/mcp-server/server.mjs +38 -24
  162. package/package.json +2 -2
  163. package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
  164. package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
  165. package/portal-ui/dist/index.html +2 -2
  166. package/portal-ui/package.json +1 -1
  167. package/providers/_shared/lane-probe.mjs +9 -3
  168. package/providers/jx-subclass/lookup.mjs +1 -1
  169. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  170. package/providers/oauth-mcp-bridge/package.json +1 -1
  171. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  172. package/scripts/citation-drift-report.mjs +1 -1
  173. package/scripts/citation-line-check.mjs +2 -2
  174. package/scripts/drive-env-check.mjs +1 -1
  175. package/scripts/e2e.mjs +5 -5
  176. package/scripts/env-audit.mjs +13 -1
  177. package/scripts/headless-page.mjs +5 -5
  178. package/scripts/live-surface-check.mjs +26 -2
  179. package/scripts/mint-names-in-force.mjs +19 -5
  180. package/scripts/mint-reference-strip-backlog.mjs +1 -1
  181. package/scripts/mint-suite-census.mjs +37 -10
  182. package/scripts/pack-publishable.mjs +1 -1
  183. package/scripts/preinstall-node-check.mjs +1 -1
  184. package/scripts/release-await-cut.mjs +3 -3
  185. package/scripts/release-cut-decision.mjs +1 -1
  186. package/scripts/release-dist-tag.mjs +1 -1
  187. package/scripts/release-install-check.mjs +1 -1
  188. package/scripts/release-notes-lint.mjs +1 -1
  189. package/scripts/release-publish-guard.mjs +1 -1
  190. package/scripts/release-version-pr-checks.mjs +2 -2
  191. package/scripts/release-version.mjs +61 -5
  192. package/scripts/render-brand-banner.mjs +1 -1
  193. package/scripts/render-check.mjs +2 -2
  194. package/scripts/repo-writes.mjs +1 -1
  195. package/scripts/report-frame-check.mjs +1 -1
  196. package/scripts/report-screenshot.mjs +2 -2
  197. package/scripts/retire-bare-refs.mjs +1 -1
  198. package/scripts/revisit-render-check.mjs +1 -1
  199. package/scripts/score.mjs +1 -1
  200. package/scripts/strip-titles-and-attributions.mjs +389 -0
  201. package/scripts/strip-tracker-citations.mjs +122 -5
  202. package/scripts/test-run.mjs +4 -4
  203. package/scripts/third-party-notices.mjs +1 -1
  204. package/scripts/verify-publishable.mjs +1 -1
  205. package/shared/access-audience.mjs +2 -2
  206. package/shared/anon-overlay.mjs +1 -1
  207. package/shared/brand.mjs +15 -1
  208. package/shared/bundle-freshness.mjs +1 -1
  209. package/shared/bundle-rebuild.mjs +1 -1
  210. package/shared/checkout-move.mjs +2 -2
  211. package/shared/client-door.mjs +8 -8
  212. package/shared/connect-clients.mjs +7 -7
  213. package/shared/connector-signin-probe.mjs +1 -1
  214. package/shared/env-aliases.mjs +1 -1
  215. package/shared/env-local.mjs +5 -5
  216. package/shared/grants-edit.mjs +76 -0
  217. package/shared/install-auth.mjs +1 -1
  218. package/shared/listen.mjs +3 -3
  219. package/shared/mcp-challenge.mjs +1 -1
  220. package/shared/names-in-force.mjs +2 -1
  221. package/shared/onboarding-store.mjs +19 -2
  222. package/shared/reference-guard-classes.mjs +44 -2
  223. package/shared/register-selection.mjs +1 -1
  224. package/shared/scope.mjs +223 -56
  225. package/shared/secret-file.mjs +1 -1
  226. package/shared/server-units.mjs +1 -1
  227. package/shared/staff-domain.mjs +45 -78
  228. package/shared/summary-blocks.mjs +2 -2
  229. package/shared/systemd-failure.mjs +3 -3
  230. package/shared/tracked-files.mjs +1 -1
  231. package/shared/trigger-lane.mjs +1 -1
  232. package/shared/tty-style.mjs +1 -1
  233. package/shared/usage-block.mjs +1 -1
  234. package/shared/vacuous-pass.mjs +1 -1
  235. package/shared/verb-shim.mjs +1 -1
@@ -0,0 +1,389 @@
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
+ // Removes two more classes of internal attribution, over the population the citation-opener sweep beside
5
+ // it (strip-tracker-citations.mjs) scans, with the same exclusions:
6
+ //
7
+ // TITLES — a test title that opens with an issue number:
8
+ // test("1223 with no research credential …") → test("with no research credential …")
9
+ // test("2087 arm 1 — the sources are …") → test("arm 1 — the sources are …")
10
+ // ATTRIBUTIONS — a comment that names who a ruling came from:
11
+ // // Owner ruling, 2026-08-29: … → // Ruling, 2026-08-29: …
12
+ // The ruling and its date stay; the person goes, as public attribution is by role and date.
13
+ //
14
+ // node scripts/strip-titles-and-attributions.mjs what would change, per class
15
+ // node scripts/strip-titles-and-attributions.mjs --apply change it
16
+ //
17
+ // WHY A SIBLING AND NOT MORE RULES IN THE OPENER SWEEP. That sweep is about one construction and its arms
18
+ // pin it; neither class here is a tracker citation, and folding them in would make its counts mean two
19
+ // things. What it has that is worth sharing — which files are in scope, which are excluded by name, and
20
+ // which tree is the published one — is IMPORTED, so there is still one population definition.
21
+ //
22
+ // THE POPULATION DIFFERS FROM THAT SWEEP'S IN TWO NAMED PLACES. `.env.example` is added: it carries two
23
+ // members of the attribution class and the opener sweep has never scanned it. And this file and its arms
24
+ // are excluded by name, because they quote both classes as specimens.
25
+ //
26
+ // WHAT EACH TRANSFORM WILL NOT DO, and each is an arm in the-title-and-attribution-sweep-takes-the-label.test.mjs:
27
+ // · A title's label is taken WHOLE: a workstream letter (1957F), an item suffix (2191-F54, 1135-12,
28
+ // 600.3) or a second number (526/1067) is part of it, with or without its hash. Exactly one separator
29
+ // after it goes with it: a space, `: `, or ` — `.
30
+ // · A bare number the same file uses as a value in its code is what the title is ABOUT — `409 splits: …`
31
+ // in a file that calls `withFetch(409, …)` — and a title that opens with a date is dated, not
32
+ // labelled. Both are handed off.
33
+ // · A title whose remainder would open with punctuation or with ANOTHER reference, would be empty, or
34
+ // would read the same as another title in its file is HANDED OFF, never rewritten: a reader decides.
35
+ // Another reference after the label is the title's subject ("2018 1010 still fires …"), and a sweep
36
+ // that took the label would take the subject the next time it ran. A remainder opening with
37
+ // a CLI flag (`--dry-run`) or a code span is ordinary prose and is rewritten.
38
+ // · Only the FIRST argument of test(), it() or describe() is a title. A numbered string anywhere else is
39
+ // somebody's data.
40
+ // · A title another line QUOTES BY NAME moves with it: a table that anchors on a test's full name, with
41
+ // its separator written as the character or as its escape, is rewritten in the same pass. A quote the
42
+ // sweep may not touch — in a file excluded by name, or outside the population — keeps the title as it
43
+ // is, and the title goes to a reader.
44
+ // · An attribution is rewritten only on a COMMENT line, a JSX `{/* … */}` block included, or in markdown
45
+ // PROSE outside a code fence. Inside a string the words are text a user reads or a value a run writes,
46
+ // and changing either is not a comment edit, so those are handed off. So is a line where the word after
47
+ // "owner" is a verb ("the owner ordered"): dropping the person would leave "the ordered".
48
+ // · A phrase wrapped across two lines is taken as one — "(owner⏎// ruling, 2026-09-08)" becomes
49
+ // "(ruling,⏎// 2026-09-08)" — and no line is added or removed, so every line citation into a swept
50
+ // file still lands where it did.
51
+ // · The article follows the word it now stands before: "an owner ruling" becomes "a ruling", and "an
52
+ // owner order" stays "an order", on one line or across two.
53
+ // · A line that carries a bare line citation (a line number cited with no symbol beside it) is handed off. The
54
+ // citation ratchet judges a changed line as an added one, so any rewrite of that line would read as
55
+ // adding the citation. The ratchet's own matcher decides, never a copy of its pattern.
56
+ // · THE FLOOR. After the sweep, every line the class's own instrument still counts must be on a hand-off
57
+ // list. A line neither rewritten nor handed off is a rule that cannot see it — it is reported, and
58
+ // --apply refuses to write anything while one exists.
59
+ import { readFileSync, writeFileSync } from "node:fs";
60
+ import { execFileSync } from "node:child_process";
61
+ import { join, dirname } from "node:path";
62
+ import { fileURLToPath } from "node:url";
63
+ import { isScannable, EXCLUDED } from "./strip-tracker-citations.mjs";
64
+ import { publishedOf } from "../shared/reference-guard-classes.mjs";
65
+ import { newBareCitations } from "./citation-line-check.mjs";
66
+
67
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
68
+ const APPLY = process.argv.includes("--apply");
69
+
70
+ // ── THE POPULATION ───────────────────────────────────────────────────────────────────────────────────
71
+ export const OWN_SPECIMENS = [
72
+ "scripts/strip-titles-and-attributions.mjs",
73
+ "driver/test/the-title-and-attribution-sweep-takes-the-label.test.mjs",
74
+ ];
75
+ const ALSO_SCANNED = /(?:^|\/)\.env\.example$/;
76
+ export const inPopulation = (f) =>
77
+ !OWN_SPECIMENS.includes(f) && !EXCLUDED.includes(f) && (isScannable(f) || ALSO_SCANNED.test(f));
78
+
79
+ // ── TITLES ───────────────────────────────────────────────────────────────────────────────────────────
80
+ export const TITLE_CALL = /^(\s*(?:test|it|describe)(?:\.(?:skip|only|todo))?\(\s*)(["'`])/;
81
+ const PART = String.raw`\d+(?:[A-Za-z]\d*)?(?:-[A-Za-z]?\d+[A-Za-z]?)*`;
82
+ /** The label, whole — digits, a workstream letter, item suffixes, a second number after / or . — and ONE separator. */
83
+ export const TITLE_NUMBER = new RegExp(String.raw`^#?\d{3,}(?:[A-Za-z]\d*)?(?:-[A-Za-z]?\d+[A-Za-z]?)*(?:[/.]#?${PART})*(?:\s*[—–:]\s+|\s+-\s+|\s+)`);
84
+ const OPENS_WITH_NUMBER = /^#?\d{3,}/;
85
+ const DATE_LED = /^#?\d{4}-\d{2}-\d{2}(?!\d)/;
86
+ const PUNCTUATION_LED = /^[—–:,.;!?)\]-]/;
87
+ const ORDINARY_LEAD = /^(?:--[a-z]|`)/;
88
+
89
+ /** The title literal's text, up to its own unescaped closing quote; null when it does not close on this line. */
90
+ function titleText(rest, q) {
91
+ for (let i = 0; i < rest.length; i++) {
92
+ if (rest[i] === "\\") { i++; continue; }
93
+ if (rest[i] === q) return rest.slice(0, i);
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /** A code line with its strings and trailing comment removed — what is left is what the program computes with. */
99
+ const codeOnly = (l) => l.replace(/\\./g, "").replace(/"[^"]*"|'[^']*'|`[^`]*`/g, '""').replace(/\/\/.*$/, "");
100
+
101
+ /** Every number of three or more digits a file uses as a VALUE: in code, outside strings, comments and titles. PURE. */
102
+ export function valuesInCode(lines, kinds) {
103
+ const out = new Set();
104
+ lines.forEach((l, i) => {
105
+ if (kinds[i] !== "code" || TITLE_CALL.test(l) || /^\s*[+`'"]/.test(l)) return;
106
+ for (const m of codeOnly(l).matchAll(/(?<![\w#.$-])(\d{3,})(?![\w.])/g)) out.add(m[1]);
107
+ });
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * One line's title, stripped. PURE. `valuesInCode` is the file's own numbers-as-values, from valuesInCode().
113
+ * @returns {{line: string, changed: boolean, handoff?: string, title?: string}}
114
+ */
115
+ export function stripTitle(line, { valuesInCode = new Set() } = {}) {
116
+ const m = TITLE_CALL.exec(line);
117
+ if (!m) return { line, changed: false };
118
+ const rest = line.slice(m[0].length);
119
+ if (!OPENS_WITH_NUMBER.test(rest)) return { line, changed: false };
120
+ if (DATE_LED.test(rest)) return { line, changed: false, handoff: "a date opens this title — it is dated, not labelled" };
121
+ const bare = /^(\d{3,})/.exec(rest);
122
+ if (bare && valuesInCode.has(bare[1]))
123
+ return { line, changed: false, handoff: "this file uses the number as a value in its code — it is what the title is about" };
124
+ const t = TITLE_NUMBER.exec(rest);
125
+ if (!t) return { line, changed: false, handoff: "a number with no separator after it — part of the title, or a label with none?" };
126
+ const remainder = rest.slice(t[0].length);
127
+ if (OPENS_WITH_NUMBER.test(remainder))
128
+ return { line, changed: false, handoff: "what is left opens with another reference — the title's subject, which a second run would take" };
129
+ const text = titleText(remainder, m[2]);
130
+ if (text === null || text.trim() === "") return { line, changed: false, handoff: "the title would be empty, or does not close on this line" };
131
+ if (PUNCTUATION_LED.test(remainder) && !ORDINARY_LEAD.test(remainder))
132
+ return { line, changed: false, handoff: "what is left would open with punctuation" };
133
+ return { line: m[0] + remainder, changed: true, title: text };
134
+ }
135
+
136
+ // ── ATTRIBUTIONS ─────────────────────────────────────────────────────────────────────────────────────
137
+ /** Where "owner" can start: a word boundary, or just after an escape such as the \n that opens a message. */
138
+ const START = String.raw`(?:(?<![A-Za-z0-9_])|(?<=\\[ntr]))`;
139
+ export const ATTRIBUTION = new RegExp(String.raw`${START}(an?\s+)?(owner)(\s+)(rulings?|steer|order)\b`, "gi");
140
+ /** "owner" before another form of the word — a verb, mostly: "the owner ordered". */
141
+ const OTHER_FORM = new RegExp(String.raw`${START}owner\s+(?:ruling(?!s?\b)|steer(?!\b)|order(?!\b))`, "i");
142
+ const COMMENT_BY_EXT = [
143
+ [/\.(?:mjs|js|jsx|ts|tsx|css)$/, /^\s*(?:\/\/|\*|\/\*)/],
144
+ [/\.(?:sh|yml|yaml|toml|service|timer)$|(?:^|\/)\.git(?:attributes|ignore)$|(?:^|\/)\.env\.example$/, /^\s*#/],
145
+ ];
146
+ const BLOCK_OPENS = /^\s*\{?\/\*/;
147
+ const caseLike = (model, word) =>
148
+ model === model.toUpperCase() ? word.toUpperCase()
149
+ : model[0] === model[0].toUpperCase() ? word[0].toUpperCase() + word.slice(1) : word;
150
+ const articleFor = (word) => (/^[aeiou]/i.test(word) ? "an" : "a");
151
+
152
+ /** "owner ruling" → "ruling", keeping the case the phrase was written in and fixing the article. PURE. */
153
+ export function dropOwner(text) {
154
+ return text.replace(ATTRIBUTION, (_, article, owner, _sp, word) => {
155
+ const kept = caseLike(owner, word.toLowerCase());
156
+ return article ? `${caseLike(article.trim(), articleFor(word))} ${kept}` : kept;
157
+ });
158
+ }
159
+
160
+ /**
161
+ * One line's attribution, dropped — where the line is somewhere a person reads rather than a value.
162
+ * `kind` is "comment", "prose" or "code". PURE.
163
+ */
164
+ export function stripAttribution(line, kind) {
165
+ ATTRIBUTION.lastIndex = 0;
166
+ const hit = ATTRIBUTION.test(line);
167
+ ATTRIBUTION.lastIndex = 0;
168
+ const other = OTHER_FORM.test(line);
169
+ if (!hit && !other) return { line, changed: false };
170
+ if (other) return { line, changed: false, handoff: 'the word after "owner" is a verb or another form here — dropping the person would leave "the ordered"' };
171
+ if (kind === "code") return { line, changed: false, handoff: "inside a string or code — text a user reads or a value a run writes, not a comment" };
172
+ const out = dropOwner(line);
173
+ return { line: out, changed: out !== line };
174
+ }
175
+
176
+ /** Which kind of line this is. A markdown code fence is code; a JS block comment, JSX's included, is comment throughout. */
177
+ export function lineKinds(file, lines) {
178
+ if (/\.md$/i.test(file)) {
179
+ let fenced = false;
180
+ return lines.map((l) => { if (/^\s*```/.test(l)) { fenced = !fenced; return "code"; } return fenced ? "code" : "prose"; });
181
+ }
182
+ const rule = COMMENT_BY_EXT.find(([ext]) => ext.test(file));
183
+ if (!rule) return lines.map(() => "code");
184
+ const blocks = rule === COMMENT_BY_EXT[0];
185
+ let inBlock = false;
186
+ return lines.map((l) => {
187
+ if (inBlock) { if (l.includes("*/")) inBlock = false; return "comment"; }
188
+ if (blocks && BLOCK_OPENS.test(l)) { inBlock = !l.includes("*/"); return "comment"; }
189
+ return rule[1].test(l) ? "comment" : "code";
190
+ });
191
+ }
192
+
193
+ const RATCHET_WHY = "the line carries a bare line citation, and the citation ratchet reads any rewrite of it as adding one";
194
+ /** Whether the citation ratchet would read this line, as rewritten, as ADDING a bare citation — its own matcher, called. */
195
+ const ratchetRefuses = (file, line, text) => newBareCitations([{ file, line, text }]).length > 0;
196
+
197
+ const LEADER = String.raw`\s*(?:(?:\/\/|#|\*|\{?\/\*)\s*)?`;
198
+ const WRAP_TAIL = /\b(?:an?\s+)?owner\s*$/i;
199
+ const WRAP_HEAD = new RegExp(String.raw`^(${LEADER})(rulings?|steer|order)\b([,:]?) ?`, "i");
200
+ const ARTICLE_TAIL = /\b(an?)(\s*)$/i;
201
+ const OWNER_HEAD = new RegExp(String.raw`^${LEADER}owner\s+(rulings?|steer|order)\b`, "i");
202
+
203
+ /**
204
+ * The attribution wrapped across a line break, taken as one phrase: the word moves up to where "owner" stood,
205
+ * and an article left at the end of a line agrees with the word that now follows it. Two lines in, two out. PURE.
206
+ */
207
+ export function unwrapPairs(lines, kinds, refuses = () => false) {
208
+ const out = lines.slice(), rewritten = [], handoff = [];
209
+ for (let i = 0; i + 1 < out.length; i++) {
210
+ const a = out[i], b = out[i + 1];
211
+ const tail = WRAP_TAIL.exec(a), head = WRAP_HEAD.exec(b);
212
+ const readable = kinds[i] !== "code" && kinds[i + 1] !== "code";
213
+ if (tail && head) {
214
+ if (!readable) { handoff.push({ line: i + 1, why: "a wrapped attribution inside a string or code" }); continue; }
215
+ const na = a.slice(0, tail.index) + dropOwner(`${tail[0].trimEnd()} ${head[2]}`) + head[3];
216
+ const nb = head[1] + b.slice(head[0].length);
217
+ if (refuses(i, na) || refuses(i + 1, nb)) { handoff.push({ line: i + 1, why: RATCHET_WHY }, { line: i + 2, why: RATCHET_WHY }); continue; }
218
+ out[i] = na; out[i + 1] = nb;
219
+ rewritten.push(i + 1);
220
+ continue;
221
+ }
222
+ const art = ARTICLE_TAIL.exec(a), oh = OWNER_HEAD.exec(b);
223
+ if (art && oh && readable) {
224
+ const na = a.slice(0, art.index) + caseLike(art[1], articleFor(oh[1])) + art[2];
225
+ if (na !== a && (refuses(i, na) || refuses(i + 1, dropOwner(b)))) { handoff.push({ line: i + 1, why: RATCHET_WHY }, { line: i + 2, why: RATCHET_WHY }); continue; }
226
+ out[i] = na;
227
+ }
228
+ }
229
+ return { lines: out, rewritten, handoff };
230
+ }
231
+
232
+ // ── THE FLOOR ───────────────────────────────────────────────────────────────────────────────────────
233
+ /** The two instruments the ruling counted its classes with, line-based and verbatim in what they match. */
234
+ export const INSTRUMENT = {
235
+ title: /^\s*(?:test|it|describe)(?:\.\w+)?\(\s*["'`]#?\d{3,}/,
236
+ attribution: /owner (?:ruling|steer|order)/i,
237
+ };
238
+ /**
239
+ * Every line of a swept file that an instrument still counts and no hand-off list names, and every quote that
240
+ * still names a renamed title as it was — a rule that cannot see the line, reported rather than left in silence. PURE.
241
+ */
242
+ export function accountFor(file, lines, titleHandoff, attributionHandoff, rename = () => undefined) {
243
+ const listed = (h) => new Set(h.filter((x) => x.file === file).map((x) => x.line));
244
+ const t = listed(titleHandoff), a = listed(attributionHandoff);
245
+ const out = [];
246
+ lines.forEach((l, i) => {
247
+ if (/\.test\.(?:mjs|js|ts)$/.test(file) && INSTRUMENT.title.test(l) && !t.has(i + 1)) out.push({ file, line: i + 1, class: "title", text: l.trim().slice(0, 110) });
248
+ if (INSTRUMENT.attribution.test(l) && !a.has(i + 1)) out.push({ file, line: i + 1, class: "attribution", text: l.trim().slice(0, 110) });
249
+ if (!TITLE_CALL.test(l) && renameQuoted(l, rename).hits) out.push({ file, line: i + 1, class: "reference", text: l.trim().slice(0, 110) });
250
+ });
251
+ return out;
252
+ }
253
+
254
+ // ── THE SURVEY ───────────────────────────────────────────────────────────────────────────────────────
255
+ /** A quoted string that could name a test — and the separators a test's full name is joined with. */
256
+ const QUOTED = /(["'`])([^"'`\n]*\d{3,}[^"'`\n]*)\1/g;
257
+ const NAME_SEPARATOR = /(\s*(?:›|\\u203a| > )\s*)/;
258
+
259
+ /** Every quoted part of `line` that `rename` maps, rewritten; the separators stay exactly as written. PURE. */
260
+ function renameQuoted(line, rename) {
261
+ let hits = 0;
262
+ const out = line.replace(QUOTED, (_, q, seg) => {
263
+ const parts = seg.split(NAME_SEPARATOR);
264
+ for (let k = 0; k < parts.length; k += 2) {
265
+ const to = rename(parts[k]);
266
+ if (to !== undefined) { parts[k] = to; hits++; }
267
+ }
268
+ return q + parts.join("") + q;
269
+ });
270
+ return { line: out, hits };
271
+ }
272
+
273
+ /** Per-file classification over the population. PURE; `read` is injected so an arm drives a planted tree. */
274
+ export function surveyOf(files, read) {
275
+ const titles = { rewritten: {}, handoff: [] }, attributions = { rewritten: {}, handoff: [] }, references = { rewritten: {} };
276
+ const unreadable = [], writes = {}, unaccounted = [];
277
+ const bump = (o, f, n = 1) => { o[f] = (o[f] ?? 0) + n; };
278
+ const state = new Map(); // file → { lines, out }
279
+ const renamed = new Map(); // a title as it was → { to, file, i }
280
+ // 1. TITLES — only in test files, and a file's rewrites are held back together if any two would collide.
281
+ for (const f of files.filter(inPopulation)) {
282
+ let text;
283
+ try { text = read(f); } catch (e) { unreadable.push({ file: f, why: String(e?.message ?? e).slice(0, 120) }); continue; }
284
+ const lines = text.split("\n"), out = lines.slice();
285
+ state.set(f, { lines, out });
286
+ if (!/\.test\.(?:mjs|js|ts)$/.test(f)) continue;
287
+ const values = valuesInCode(lines, lineKinds(f, lines));
288
+ const done = [];
289
+ lines.forEach((l, i) => {
290
+ const r = stripTitle(l, { valuesInCode: values });
291
+ if (r.handoff) titles.handoff.push({ file: f, line: i + 1, why: r.handoff, text: l.trim().slice(0, 110) });
292
+ else if (r.changed && ratchetRefuses(f, i + 1, r.line)) titles.handoff.push({ file: f, line: i + 1, why: RATCHET_WHY, text: l.trim().slice(0, 110) });
293
+ else if (r.changed) done.push({ i, line: r.line, title: r.title, was: l });
294
+ });
295
+ const others = new Set(lines.map((l) => { const m = TITLE_CALL.exec(l); return m ? titleText(l.slice(m[0].length), m[2]) : null; })
296
+ .filter((t, i) => t !== null && !done.some((d) => d.i === i)));
297
+ const counts = {};
298
+ for (const d of done) counts[d.title] = (counts[d.title] ?? 0) + 1;
299
+ for (const d of done) {
300
+ if (counts[d.title] > 1 || others.has(d.title)) {
301
+ titles.handoff.push({ file: f, line: d.i + 1, why: "would read the same as another title in this file", text: d.was.trim().slice(0, 110) });
302
+ continue;
303
+ }
304
+ out[d.i] = d.line;
305
+ const m = TITLE_CALL.exec(d.was);
306
+ renamed.set(titleText(d.was.slice(m[0].length), m[2]), { to: d.title, file: f, i: d.i });
307
+ }
308
+ }
309
+ // 2. A TITLE ANOTHER LINE QUOTES BY NAME moves with it, in the same pass. A quote this sweep may not touch —
310
+ // in a file excluded by name, or outside the population — keeps the title as it is, for a reader.
311
+ for (const g of files.filter((g) => !inPopulation(g) && !OWN_SPECIMENS.includes(g))) {
312
+ let text;
313
+ try { text = read(g); } catch { continue; }
314
+ if (text.includes("\0")) continue;
315
+ text.split("\n").forEach((l, j) => {
316
+ if (TITLE_CALL.test(l)) return;
317
+ renameQuoted(l, (part) => {
318
+ const r = renamed.get(part);
319
+ if (!r) return undefined;
320
+ const st = state.get(r.file);
321
+ st.out[r.i] = st.lines[r.i];
322
+ renamed.delete(part);
323
+ titles.handoff.push({ file: r.file, line: r.i + 1, why: `quoted by name at ${g}:${j + 1}, which this sweep may not touch`, text: st.lines[r.i].trim().slice(0, 110) });
324
+ return undefined;
325
+ });
326
+ });
327
+ }
328
+ for (const r of renamed.values()) bump(titles.rewritten, r.file);
329
+ const rename = (part) => renamed.get(part)?.to;
330
+ for (const [f, st] of state) st.out.forEach((l, j) => {
331
+ if (TITLE_CALL.test(l)) return;
332
+ const r = renameQuoted(l, rename);
333
+ // A quote whose line the ratchet would refuse is left, and the floor reports it: --apply then refuses
334
+ // outright rather than land a title its quote no longer names.
335
+ if (r.hits && !ratchetRefuses(f, j + 1, r.line)) { st.out[j] = r.line; bump(references.rewritten, f, r.hits); }
336
+ });
337
+ // 3. ATTRIBUTIONS — every file in the population, on the line kinds a person reads.
338
+ for (const [f, st] of state) {
339
+ const kinds = lineKinds(f, st.out);
340
+ const wrapped = unwrapPairs(st.out, kinds, (i, text) => ratchetRefuses(f, i + 1, text));
341
+ const out = wrapped.lines;
342
+ for (const _ of wrapped.rewritten) bump(attributions.rewritten, f);
343
+ const held = new Set(wrapped.handoff.map((h) => h.line - 1));
344
+ for (const h of wrapped.handoff) attributions.handoff.push({ file: f, line: h.line, why: h.why, text: st.lines[h.line - 1].trim().slice(0, 110) });
345
+ out.forEach((l, i) => {
346
+ if (held.has(i)) return;
347
+ const r = stripAttribution(l, kinds[i]);
348
+ if (r.handoff) attributions.handoff.push({ file: f, line: i + 1, why: r.handoff, text: l.trim().slice(0, 110) });
349
+ else if (r.changed && ratchetRefuses(f, i + 1, r.line)) attributions.handoff.push({ file: f, line: i + 1, why: RATCHET_WHY, text: l.trim().slice(0, 110) });
350
+ else if (r.changed) { out[i] = r.line; bump(attributions.rewritten, f); }
351
+ });
352
+ if (out.some((l, i) => l !== st.lines[i])) writes[f] = out.join("\n");
353
+ unaccounted.push(...accountFor(f, out, titles.handoff, attributions.handoff, rename));
354
+ }
355
+ return { titles, attributions, references, unreadable, writes, unaccounted };
356
+ }
357
+
358
+ const total = (o) => Object.values(o).reduce((a, n) => a + n, 0);
359
+
360
+ if (import.meta.url === `file://${process.argv[1]}`) {
361
+ // THE PUBLISHED POPULATION, NOT THE INDEX — the same helper, for the same reason, as the opener sweep.
362
+ const all = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 }).split("\n").filter(Boolean);
363
+ const pub = publishedOf(all, ROOT);
364
+ if (pub.error) { console.error(`strip-titles-and-attributions: ${pub.error}`); process.exit(2); }
365
+ const s = surveyOf(pub.files, (f) => readFileSync(join(ROOT, f), "utf8"));
366
+ if (s.unreadable.length) {
367
+ console.log(`${s.unreadable.length} file(s) COULD NOT BE READ — every count below excludes them:`);
368
+ for (const u of s.unreadable) console.log(` ${u.file} ${u.why}`);
369
+ console.log("");
370
+ }
371
+ if (s.unaccounted.length) {
372
+ console.log(`${s.unaccounted.length} line(s) an instrument still counts that the sweep neither rewrote nor handed off — UNACCOUNTED:`);
373
+ for (const u of s.unaccounted) console.log(` ${u.file}:${u.line} [${u.class}] ${u.text}`);
374
+ if (APPLY) { console.error("strip-titles-and-attributions: refusing to --apply while a line is unaccounted for"); process.exit(2); }
375
+ console.log("");
376
+ }
377
+ const verb = APPLY ? "rewrote" : "would rewrite";
378
+ console.log(`titles: ${verb} ${total(s.titles.rewritten)} across ${Object.keys(s.titles.rewritten).length} file(s); ${s.titles.handoff.length} handed to a reader`);
379
+ console.log(`attributions: ${verb} ${total(s.attributions.rewritten)} across ${Object.keys(s.attributions.rewritten).length} file(s); ${s.attributions.handoff.length} handed to a reader`);
380
+ console.log(`references: ${verb} ${total(s.references.rewritten)} quoted test name(s) across ${Object.keys(s.references.rewritten).length} file(s)`);
381
+ for (const [label, h] of [["titles", s.titles.handoff], ["attributions", s.attributions.handoff]]) {
382
+ if (!h.length) continue;
383
+ console.log(`\n${label} — the hand-off list:`);
384
+ for (const x of h) console.log(` ${x.file}:${x.line} [${x.why}] ${x.text}`);
385
+ }
386
+ if (APPLY) for (const [f, text] of Object.entries(s.writes)) writeFileSync(join(ROOT, f), text);
387
+ console.log(`\nexcluded by name, shared with the opener sweep: ${EXCLUDED.join(", ")}`);
388
+ console.log(`excluded by name, this sweep's own specimens: ${OWN_SPECIMENS.join(", ")}`);
389
+ }
@@ -58,7 +58,7 @@
58
58
  // wants a guard against reintroduction rather than a backlog. Different residue, different repair.
59
59
  import { readFileSync, writeFileSync } from "node:fs";
60
60
  import { execFileSync } from "node:child_process";
61
- import { publishedOf } from "../shared/reference-guard-classes.mjs";
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
64
 
@@ -69,10 +69,82 @@ const APPLY = process.argv.includes("--apply");
69
69
  // survives the replacement.
70
70
  export const OPENER = /((?:^|["'`(\[]|\/\/|\/\*|#|\*|·|──)\s*)(?:refs\s+)?tracker issues?\s+\d+\s*[—:–-]\s+/i;
71
71
 
72
+ // ── THE SECOND MECHANICAL CASE: A CITATION STANDING BESIDE A SENTENCE, IN PARENTHESES ──────────────
73
+ //
74
+ // Measured before it was written, because "the sweep is nearly done" was false: two openers were left
75
+ // and seven hundred and fifty citations were not openers at all. Of those, three hundred and twenty-one
76
+ // sit in parentheses that hold the citation AND NOTHING ELSE, which makes them a label beside a
77
+ // sentence rather than part of it — the same shape as an opener, wearing brackets.
78
+ //
79
+ // ONLY WHEN THE BRACKETS HOLD NOTHING ELSE, and that is the whole of the narrowing. Sixty-eight of them
80
+ // read "(tracker issue NNN, owner's ruling 2026-09-07)" or "(tracker issue NNN / tracker issue NNN)",
81
+ // where the brackets carry a ruling, a date or a second citation. Removing those brackets deletes
82
+ // content, so they are not mechanical and they go to the reader with the rest. A pattern wide enough to
83
+ // take them would be a pattern that deletes rulings.
84
+ //
85
+ // AND THE DRAWN RULE IS KEPT THE LENGTH IT WAS. A hundred and six of them sit in a box heading —
86
+ // `// ── WHAT THIS DOES (tracker issue NNN) ─────────` — where removing twenty characters leaves the
87
+ // rule twenty short and the heading ragged against every other heading in the file. Every one measured
88
+ // uses U+2500 and sits in a .mjs or .yml comment, so there is no markdown `---` to mistake for a rule.
89
+ // The replacement re-pads by exactly what it removed, which keeps the line the length it was and makes
90
+ // the diff say "the citation left" rather than "this heading was reflowed".
91
+ export const PARENTHETICAL = /[ \t]*\(\s*(?:refs\s+)?tracker issues?\s+\d+\s*\)/i;
92
+
93
+ /**
94
+ * Remove a bracketed citation, keeping a trailing drawn rule as long as it was. PURE.
95
+ *
96
+ * Returns the line unchanged when the brackets hold anything besides the citation, which is the case
97
+ * the pattern above already declines to match.
98
+ */
99
+ export function stripParenthetical(line) {
100
+ if (!PARENTHETICAL.test(line)) return line;
101
+ const was = line.length;
102
+ const out = line.replace(PARENTHETICAL, "");
103
+ const rule = /─+$/.exec(out);
104
+ if (!rule) return out;
105
+ return out.slice(0, rule.index) + "─".repeat(rule[0].length + (was - out.length));
106
+ }
107
+
72
108
  // Any remaining citation, mechanical or not. The difference between this count and the opener count is
73
109
  // the hand-off: a citation inside a sentence cannot be removed without rewriting the sentence around it.
74
110
  export const ANY_CITATION = /\btracker issues?\s+\d+/i;
75
111
 
112
+ // ── A LINE CARRYING ANOTHER CITATION IS NOT REWRITTEN, IT IS HANDED OFF ─────────────────────────────
113
+ //
114
+ // REWRITING A LINE RE-AGES EVERY CITATION ON IT. A diff has no notion of a partial edit: touching one
115
+ // character puts the whole line on the added side, so a by-line citation that has sat there for months
116
+ // becomes newly-added and fails the check that refuses new ones. Two lines in this tree are in that
117
+ // state — the citation guard's own exemption table, which quotes synthetic fixture citations as DATA —
118
+ // and rewriting them turned a strip into three new bare citations.
119
+ //
120
+ // MEASURED ON THE WRONG POPULATION FIRST, which is why this is written down rather than just fixed. I
121
+ // checked the lines the sweep LEAVES and found none carrying a by-line citation, and reported the
122
+ // hazard as absent. The hazard is on the lines the sweep REWRITES, and I had not looked at those at
123
+ // all. An answer about the wrong set is not a smaller answer, it is a different question.
124
+ // A DOT-EXTENSION, NOT A LIST OF THEM. Naming six extensions made this blind to the rest: the tree
125
+ // carries three by-line citations outside that list — one into a stylesheet and two into a run log —
126
+ // and any of them sitting on a sweepable line would have been rewritten and re-aged silently. It is the
127
+ // `:<line>` that makes a citation, not which language the target happens to be written in. Widening it
128
+ // changes nothing on today's tree, which is the point: the same answer from a rule that cannot go blind
129
+ // the day somebody cites a seventh kind of file.
130
+ // ── A CITATION THAT WRAPPED IS IN NEITHER PILE ─────────────────────────────────────────────────────
131
+ //
132
+ // Every rule here reads one line, so a citation broken across a line break is invisible to all of them:
133
+ // `tracker issue` at the end of one line and its number at the start of the next matches no pattern,
134
+ // counts in no total, and appears on no hand-off list. Thirty-six of them are in the tree. That is the
135
+ // same shape as a line stripped before it is classified — not a smaller number, an absent one.
136
+ //
137
+ // FOUND AND HANDED OFF, NOT JOINED. Joining rewrites BOTH lines, which re-ages every citation on either
138
+ // and re-flows the prose around them; and the removal itself is a sentence repair across a break, which
139
+ // is precisely the work the hand-off exists for. So this makes them visible and stops there.
140
+ // IT LIVES IN shared/ NOW, because the census needs the same answer. The residue census walked one line
141
+ // at a time and was blind to exactly this class while this file could see it — two instruments over one
142
+ // tree, disagreeing about what a citation is. Re-exported here rather than moved out of sight: this file
143
+ // is where the rule is explained, and its arms import it from here.
144
+ export { WRAPPED_HEAD, WRAPPED_TAIL, wrapsInto } from "../shared/reference-guard-classes.mjs";
145
+
146
+ export const CARRIES_ANOTHER_CITATION = /[A-Za-z0-9_.\-/]+\.[A-Za-z0-9]+:\d+/;
147
+
76
148
  export const EXCLUDED = [
77
149
  // Citations used as literal test DATA — the corpus the citation guard is checked against.
78
150
  "driver/test/a-bare-reference-added-in-a-diff-is-refused.test.mjs",
@@ -87,12 +159,41 @@ export const EXCLUDED = [
87
159
  // only reason it surfaced: before that, these lines were counted and never listed.
88
160
  "scripts/strip-tracker-citations.mjs",
89
161
  "driver/test/the-citation-strip-removes-openers-and-nothing-else.test.mjs",
162
+ // The RELEASE-NOTE linter's own corpus. Its arm asserts that "Fixed: The demo works again — tracker
163
+ // issue 97." is refused, and the citation IS the specimen: sweep it and the arm asserts that a clean
164
+ // sentence is clean, which passes over a linter that has stopped looking. Same argument as the two
165
+ // files above, found by the sweep listing a line it should never take.
166
+ "driver/test/a-release-note-is-written-for-its-reader.test.mjs",
90
167
  // Pinned at a content hash; a prose repair is not worth spending a freeze on.
91
168
  "driver/publish/render.mjs",
169
+ // The residue FLOOR's own specimens. Seven citations here, every one a synthetic number (1234, 1235)
170
+ // inside a string this file writes to a temp tree or hands to the counter, so the guard can be checked
171
+ // against a known population. Sweeping them edits the corpus the floor is measured on, and the floor
172
+ // would then hold against something it had stopped counting — the same trap the two files above name,
173
+ // in the module that counts rather than the one that strips.
174
+ "driver/test/the-public-residue-is-a-floor.test.mjs",
92
175
  ];
93
176
 
177
+ /**
178
+ * WHICH FILES THE SWEEP CAN SEE — and it could not see enough.
179
+ *
180
+ * This named six extensions and reported ZERO openers left, over a population that omitted every
181
+ * stylesheet, every unit file, the shell script, the HTML template, the React components and the
182
+ * dotfiles. Sixty-four citations sat in those, most of them the plainest mechanical case there is — a
183
+ * `/* tracker issue NNN — ` opening a comment — and the sweep's own report said the mechanical pass was
184
+ * finished. A filter that names file types cannot see the ones it omits, and the number it prints is
185
+ * true about its population and silent about the gap.
186
+ *
187
+ * The population is now the residue floor's: every tracked text file the public tree publishes. The two
188
+ * instruments disagreed about which files exist, which is a worse disagreement than the one their
189
+ * headers already explain.
190
+ */
191
+ const SCANNABLE_EXT = /\.(mjs|md|yml|yaml|ts|tsx|js|jsx|json|css|html|sh|service|timer|toml)$/;
192
+ const SCANNABLE_NAME = /^(?:.*\/)?\.(gitattributes|gitignore)$/;
193
+
94
194
  export const isScannable = (f) =>
95
- /\.(mjs|md|yml|ts|js|json)$/.test(f) && !f.startsWith("portal-ui/dist/") && !EXCLUDED.includes(f);
195
+ (SCANNABLE_EXT.test(f) || SCANNABLE_NAME.test(f))
196
+ && !f.startsWith("portal-ui/dist/") && !EXCLUDED.includes(f);
96
197
 
97
198
  /** Per-file classification. PURE, and `read` is injected so an arm can drive it over a synthetic tree. */
98
199
  export function surveyOf(files, read) {
@@ -110,13 +211,29 @@ export function surveyOf(files, read) {
110
211
  // opener AND a second citation further along was stripped once, counted as done, and never reached
111
212
  // the hand-off list a person is told to read — its survivor was invisible in the one place it should
112
213
  // have been named. Test the RESULT: a line can be both stripped and still owed to a reader.
113
- const out = text.split("\n").map((line, i) => {
114
- const after = OPENER.test(line) ? (n++, line.replace(OPENER, "$1")) : line;
214
+ const lines = text.split("\n");
215
+ // The wrapped form, before the per-line rules: it belongs to a PAIR, so no single-line pass can see
216
+ // it. Reported at the line the citation starts on, which is the one a reader has to open.
217
+ lines.forEach((line, i) => {
218
+ if (wrapsInto(line, lines[i + 1])) handoff.push({ file: f, line: i + 1, text: `${line.trim()} ⏎ ${String(lines[i + 1]).trim()}`.slice(0, 200) });
219
+ });
220
+ const out = lines.map((line, i) => {
221
+ // Refused before either rule runs, so a line is never half-swept: it goes to the reader whole.
222
+ const holdBack = CARRIES_ANOTHER_CITATION.test(line);
223
+ const opened = !holdBack && OPENER.test(line) ? (n++, line.replace(OPENER, "$1")) : line;
224
+ const after = !holdBack && PARENTHETICAL.test(opened) ? (n++, stripParenthetical(opened)) : opened;
115
225
  if (ANY_CITATION.test(after)) handoff.push({ file: f, line: i + 1, text: after.trim() });
116
226
  return after;
117
227
  });
118
228
  if (n) { stripped[f] = n; strippedTotal += n; }
119
- remainingTotal += out.filter((l) => ANY_CITATION.test(l)).length;
229
+ // COUNTED THE WAY THE HAND-OFF LIST IS BUILT, and the two used to disagree by exactly the wrapped
230
+ // ones. This was `out.filter(ANY_CITATION)` — one line at a time — and a wrapped citation matches on
231
+ // NEITHER of its lines alone, which is the whole reason it needed a pair-aware detector three lines
232
+ // up. So the list said 305 and this said 285, and the closing line is the number a reader quotes.
233
+ // A total that cannot see a class the same function already detects is worse than no total: it is
234
+ // low by a fixed amount, in the direction that flatters, and nothing about it looks wrong.
235
+ remainingTotal += out.filter((l) => ANY_CITATION.test(l)).length
236
+ + out.filter((l, i) => !ANY_CITATION.test(l) && wrapsInto(l, out[i + 1])).length;
120
237
  if (APPLY && n) writeFileSync(join(ROOT, f), out.join("\n"));
121
238
  }
122
239
  return { strippedTotal, remainingTotal, stripped, handoff, unreadable };
@@ -50,7 +50,7 @@ import { mkdtempSync, mkdirSync, rmSync, readdirSync, statSync, existsSync, read
50
50
  import { delimiter, dirname, join, parse as parsePath, resolve, sep } from "node:path";
51
51
  import { fileURLToPath } from "node:url";
52
52
  import { tmpdir } from "node:os";
53
- import { snapshotRepo, repoWrites, explainRepoWrites } from "./repo-writes.mjs"; // tracker issue 198
53
+ import { snapshotRepo, repoWrites, explainRepoWrites } from "./repo-writes.mjs";
54
54
 
55
55
 
56
56
  // ── TAIL — THIS WRAPPER READS BOTH SPELLINGS; IT DOES NOT TRANSLATE THE ENVIRONMENT ───────────
@@ -223,10 +223,10 @@ const providerChosen = ["CLEAROTRON_DATABASE"]
223
223
  .some((n) => String(process.env[n] ?? "").trim() !== "");
224
224
  if (!providerChosen) process.env.CLEAROTRON_DATABASE = "corsearch";
225
225
 
226
- // ── THE SUITE'S OWN BRAND-OWNER FIXTURES, ASKED FOR IN ONE PLACE ──────────────────────────────────
226
+ // ── THE SUITE'S OWN COMPANY FIXTURES, ASKED FOR IN ONE PLACE ──────────────────────────────────
227
227
  //
228
228
  // Three profiles in `driver/profiles/` are the suite's, and a resolved roster no longer offers them:
229
- // an outside user's brand-owner picker listed them on a `git clone` install, where the packaging
229
+ // an outside user's company picker listed them on a `git clone` install, where the packaging
230
230
  // exclusion that protects the tarball does not apply. They are marked `testFixture` and the loader
231
231
  // refuses them on every route.
232
232
  //
@@ -658,7 +658,7 @@ mkdirSync(process.env.CLEAROTRON_SUITE_TELEMETRY_DIR, { recursive: true });
658
658
 
659
659
  // ── NO TEST MAY WRITE INSIDE THIS CHECKOUT ──────────────────────────────────────────────────────────
660
660
  //
661
- // Tracker issue 198, and `scripts/repo-writes.mjs` carries the whole reason. In one line: `node --test`
661
+ // `scripts/repo-writes.mjs` carries the whole reason. In one line: `node --test`
662
662
  // runs test FILES concurrently against ONE shared working tree, so a test that writes into the checkout
663
663
  // is read by every other file in the run, and the red that causes surfaces somewhere else entirely —
664
664
  // another arm, another branch, another session, in a diff that never touched the file.
@@ -268,7 +268,7 @@ export function collect(root = ROOT, tree = npmTree(root), { linkTarget = defaul
268
268
  // the occurrences are NOT equivalent — the deduped ones are stubs with no `dependencies` at all,
269
269
  // and which one you meet first is an ordering accident.
270
270
  //
271
- // Measured on this tree (tracker issue 115): `ajv@8.20.0` appears twice under
271
+ // Measured on this tree: `ajv@8.20.0` appears twice under
272
272
  // `@modelcontextprotocol/sdk` — first as a stub with 0 children, then with 4. The stub was met
273
273
  // first, so `ajv` got its row and its ENTIRE SUBTREE was never walked. `fast-uri` is one of those
274
274
  // four children, which is how a production dependency came to ship with no licence recorded.
@@ -46,7 +46,7 @@ export function buffersDirs(root, out = []) {
46
46
 
47
47
  const keep = process.argv.includes("--keep");
48
48
  const staging = mkdtempSync(join(tmpdir(), "clearotron-verify-"));
49
- // ── CAN THIS RUN ON THE TREE IT IS POINTED AT — tracker issue 196 ───────────────────────────────────
49
+ // ── CAN THIS RUN ON THE TREE IT IS POINTED AT ───────────────────────────────────────────────────────
50
50
  //
51
51
  // THIS SCRIPT WAS INVOKED BY NOTHING for as long as it has existed on the public tree: no workflow, no
52
52
  // npm script, every other mention of its name a comment. That is the shape — an
@@ -72,7 +72,7 @@ export function audienceLabel(aud) {
72
72
  return list.map((a) => `${String(a).slice(0, 8)}…`).join(",");
73
73
  }
74
74
 
75
- // ── THE AUDIENCE THE EDGE ISSUES, AS OPPOSED TO THE ONE THIS INSTALL EXPECTS (tracker issue 241) ───
75
+ // ── THE AUDIENCE THE EDGE ISSUES, AS OPPOSED TO THE ONE THIS INSTALL EXPECTS ───────────────────────
76
76
  //
77
77
  // Everything above reads LOCAL configuration. What follows reads the EDGE, so `doctor` can compare the
78
78
  // two — the half of the recreation trap with no symptom of its own. Deleting and recreating a
@@ -133,7 +133,7 @@ const ACCESS_RESOURCE_RE = /cloudflare-access-protected-resource/i;
133
133
 
134
134
  export function readAudience({ location = "", status = null, error = null, wwwAuthenticate = "", viaEdge = false } = {}) {
135
135
  if (error) return { kind: "unreachable", why: String(error?.message ?? error).slice(0, 200) };
136
- // ── THREE DOORS, NOT ONE (tracker issue 251) ────────────────────────────────────────────────────
136
+ // ── THREE DOORS, NOT ONE ────────────────────────────────────────────────────────────────────────
137
137
  //
138
138
  // This returned `not-fronted` for every response with no redirect, and measured against production's
139
139
  // four configured hostnames that one label covered three materially different states:
@@ -83,7 +83,7 @@ const OVERLAY_BODY = `(function(){
83
83
  // Case-insensitive client lookup, and the reason it is not redundant: surfaces tag rows by profile
84
84
  // KEY ("coastline") and by display NAME ("Coastline"), so lowercasing both makes a single-token name
85
85
  // resolve to its key's alias. The two surfaces that tagged by NAME were Quality and Feedback, both
86
- // retired (tracker issue 265) — the fold stays because it is what makes the lookup total over either spelling,
86
+ // retired — the fold stays because it is what makes the lookup total over either spelling,
87
87
  // not because those pages are coming back.
88
88
  var CL = {}; for(var ck in (C.clients||{})){ if(Object.prototype.hasOwnProperty.call(C.clients,ck)) CL[String(ck).toLowerCase()] = C.clients[ck]; }
89
89
  var KEYS = (DA.clientKeys||[]).map(function(x){return String(x).toLowerCase();});