clearotron 0.2.2 → 0.2.4
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 +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-KFAHMgdT.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="/portal/assets/index-1ziUJX1E.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
|
56
56
|
<div id="root"></div>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.4",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -115,7 +115,7 @@ const MARKUP_ARMS = [
|
|
|
115
115
|
* long-form arm. A slogan mark has a legitimately long dominant element and cannot be restated any
|
|
116
116
|
* shorter, so refusing a value for BEING LONG is right at the compiler and wrong at a corrective
|
|
117
117
|
* stage gate — it hands back a reason with no remedy. An annotation always has a remedy: delete the
|
|
118
|
-
* note, keep the term. — that is also why it may sit ABOVE a word-count floor: `
|
|
118
|
+
* note, keep the term. — that is also why it may sit ABOVE a word-count floor: `COASTLINE (root)`
|
|
119
119
|
* is an annotation by no measure a word count can see, exactly as the one-word `**BIOVELTRIN**`
|
|
120
120
|
* was markup by no measure a word count could see.
|
|
121
121
|
*
|
|
@@ -489,8 +489,8 @@ function predicateClause(predicate, term) {
|
|
|
489
489
|
// predicate. Unanchored, because corporate names arrive in every spelling there is.
|
|
490
490
|
//
|
|
491
491
|
// NO FTS NARROWER HERE, deliberately, and it is the same trap as the suffix one. An FTS
|
|
492
|
-
// token-prefix candidate set is NOT a superset of an unanchored contains: owner "
|
|
493
|
-
// searched for "
|
|
492
|
+
// token-prefix candidate set is NOT a superset of an unanchored contains: owner "FOXGLADE INTERACTIVE"
|
|
493
|
+
// searched for "OXGLADE" satisfies the LIKE, but no token starts with URORA, so the narrower
|
|
494
494
|
// would drop a true match and the verification could never put it back. A narrower may only
|
|
495
495
|
// ever be a superset of its verifier. So this is an honest full scan, and how expensive that is
|
|
496
496
|
// at register scale is UNMEASURED.
|
|
@@ -2,110 +2,59 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// WHAT THIS CHANGE ADDS IS REFUSED. What is already here is not this check's business.
|
|
6
6
|
//
|
|
7
7
|
// node scripts/added-reference-check.mjs [--base <ref>]
|
|
8
8
|
//
|
|
9
9
|
// ── WHY THIS IS DIFF-SHAPED AND NOT A SWEEP ──────────────────────────────────────────────────────
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// The classes below have a standing population in this tree: citations into a tracker nobody outside
|
|
12
|
+
// can open, account names, home directories, and the words this project uses for how it is built. A
|
|
13
|
+
// guard that refused them all would refuse every pull request from its first day, and a guard
|
|
14
|
+
// everybody bypasses protects nothing. So this one asks a smaller question that has a clean answer:
|
|
15
|
+
// did THIS change add another.
|
|
16
16
|
//
|
|
17
|
-
// The
|
|
18
|
-
//
|
|
17
|
+
// The standing population is not therefore accepted. It is counted, per file and per class, in
|
|
18
|
+
// `driver/test/fixtures/public-residue-backlog.json`, and the floor beside it refuses any file that
|
|
19
|
+
// grows. Between the two, the number can only fall: this check stops the inflow and the floor stops
|
|
20
|
+
// the backsliding. Neither works alone — a diff guard with no floor watches the total drift upward one
|
|
21
|
+
// repaired-and-reintroduced line at a time, and a floor with no diff guard is a number that goes stale
|
|
22
|
+
// the first time somebody adds a file.
|
|
19
23
|
//
|
|
20
24
|
// ── WHAT IT READS, AND WHAT IT DELIBERATELY DOES NOT ─────────────────────────────────────────────
|
|
21
25
|
//
|
|
22
26
|
// COMMENTS AND PROSE ONLY. A hash followed by digits is not always a reference: a three-digit one is
|
|
23
27
|
// also a CSS colour, and a composite key or a fixture string can hold anything. Reading only comment
|
|
24
|
-
// markdown keeps the check away from every context where the token means something else,
|
|
25
|
-
// lets it refuse without a table of exceptions that would rot.
|
|
28
|
+
// and markdown text keeps the check away from every context where the token means something else,
|
|
29
|
+
// which is what lets it refuse without a table of exceptions that would rot.
|
|
26
30
|
//
|
|
27
|
-
// SO A TEST NAME IS OUT OF SCOPE, and that is a real hole rather than an oversight:
|
|
28
|
-
//
|
|
29
|
-
// it — widening to string literals means deciding what to do about CSS and
|
|
30
|
-
// that decision belongs with the cleanup, not with this.
|
|
31
|
+
// SO A TEST NAME IS OUT OF SCOPE, and that is a real hole rather than an oversight: most of the
|
|
32
|
+
// standing citation population is test names, which are string literals. Named here so the next reader
|
|
33
|
+
// does not have to rediscover it — widening to string literals means deciding what to do about CSS and
|
|
34
|
+
// composite keys first, and that decision belongs with the cleanup, not with this.
|
|
31
35
|
//
|
|
32
36
|
// LINK TARGETS ARE EXEMPT. A markdown anchor and a URL fragment are addresses, not references.
|
|
33
37
|
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
import { execFileSync } from "node:child_process";
|
|
37
|
-
|
|
38
|
-
// THE TWELVE FILES RESTORED VERBATIM FROM THE FROZEN TIP AND KEPT. Thirteen were restored; the
|
|
39
|
-
// duplicate-skip arms were dropped in the same branch, so twelve reach main. They came across
|
|
40
|
-
// byte-exact, which is what made their arms trustworthy and also brought their references with them.
|
|
41
|
-
// Exempted for that one merge and removed by the cleanup; see the note above.
|
|
42
|
-
// RESTORED_VERBATIM IS GONE (tracker issue 188). It exempted twelve files by literal path so that a
|
|
43
|
-
// byte-for-byte restoration could land without the guard refusing its own restored text. Their tokens
|
|
44
|
-
// are now retired like everything else, so the list exempts nothing real — and a stale exemption list is
|
|
45
|
-
// worse than none: it silently covers files nobody is checking any more, and it is invisible in a diff
|
|
46
|
-
// that does not touch this file.
|
|
47
|
-
|
|
48
|
-
const TOKEN = /#[0-9]{3,}/g;
|
|
49
|
-
|
|
50
|
-
// A `#` COMMENT IS A COMMENT WHEREVER THE FILE FORMAT SAYS SO, not only in YAML (tracker issue 188).
|
|
51
|
-
//
|
|
52
|
-
// This read `#` as a comment for YAML alone, so the same sentence was refused in a .yml file and waved
|
|
53
|
-
// through in .env.example, a systemd unit or a shell script. Those comments are exactly as publicly
|
|
54
|
-
// visible, and `# REQUIRED — tracker issue 774 removed the code default` was sitting in .env.example on the public
|
|
55
|
-
// tree while this guard reported the tree clean. Found while measuring the class for the retirement pass:
|
|
56
|
-
// the guard's own rule flagged 359 tokens, and thousands more sat in files it had never classified.
|
|
57
|
-
//
|
|
58
|
-
// Extensionless is deliberate: a systemd unit or a dotfile often has no extension worth matching, so the
|
|
59
|
-
// KNOWN `#`-comment names are listed and everything else keeps the source rule.
|
|
60
|
-
const HASH_COMMENT = /(^|\/)(\.env[^/]*|[^/]*\.(ya?ml|sh|bash|service|timer|path|socket|conf|ini|toml|properties)|Dockerfile[^/]*|Makefile|\.gitignore|\.gitattributes)$/;
|
|
61
|
-
|
|
62
|
-
/** Is this added line one the check reads at all? Comments in source, everything in markdown. */
|
|
63
|
-
export const isProse = (path, line) => {
|
|
64
|
-
if (/\.mde?$/.test(path) || path.endsWith(".md")) return true;
|
|
65
|
-
const t = line.trim();
|
|
66
|
-
if (HASH_COMMENT.test(path)) return t.startsWith("#");
|
|
67
|
-
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// ── A COLOUR IS NOT A CITATION, AND THE DIGITS ALONE CANNOT SAY WHICH ───────────────────────────
|
|
71
|
-
//
|
|
72
|
-
// `TOKEN` matches digits only, so a six-digit hex colour is read as its leading digits and refused as a
|
|
73
|
-
// reference. An earlier sweep acted on exactly that reading and rewrote sixteen colour literals as issue
|
|
74
|
-
// text, two of them live mermaid `classDef` directives — so the documentation's diagrams rendered
|
|
75
|
-
// broken, and thirteen comments stated a value that was no longer there. Restoring them then hit this
|
|
76
|
-
// guard, whose refusal told the author to write the very text that had caused it.
|
|
38
|
+
// `demo/**` AND `driver/skills/**` ARE NOT READ. The reasons are in shared/reference-guard-classes.mjs
|
|
39
|
+
// beside the rule itself, because that is where the next person changing it will be looking.
|
|
77
40
|
//
|
|
78
|
-
//
|
|
41
|
+
// ── WHERE THE TABLE LIVES ────────────────────────────────────────────────────────────────────────
|
|
79
42
|
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// whole point of the check, and the arm asserts that direction too.
|
|
89
|
-
const HEX_COLOUR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
|
|
90
|
-
const COLOUR_PROPERTY = /(?:^|[;{\s(,])(?:color|background|background-color|border|border-color|fill|stroke|outline|box-shadow|text-shadow)\s*:\s*$/i;
|
|
91
|
-
|
|
92
|
-
/** Strip hex colours, so what remains is only tokens that could be a reference. */
|
|
93
|
-
export const withoutColourValues = (line) => String(line).replace(HEX_COLOUR, (m, offset, whole) => {
|
|
94
|
-
if (/[a-fA-F]/.test(m.slice(1))) return ""; // letters ⇒ not a decimal number
|
|
95
|
-
return COLOUR_PROPERTY.test(whole.slice(0, offset)) ? "" : m; // all digits ⇒ the property decides
|
|
96
|
-
});
|
|
43
|
+
// shared/reference-guard-classes.mjs, with the census that reads the same table. Two definitions of
|
|
44
|
+
// "what a public tree must not acquire" is one definition and one imitation of it, and the imitation
|
|
45
|
+
// is whichever the reader did not run.
|
|
46
|
+
import { execFileSync } from "node:child_process";
|
|
47
|
+
import {
|
|
48
|
+
CLASSES, offendingClasses, isProse, isScannable,
|
|
49
|
+
withoutColourValues, withoutLinkTargets,
|
|
50
|
+
} from "../shared/reference-guard-classes.mjs";
|
|
97
51
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.replace(/https?:\/\/\S+/g, "") // bare URLs and their fragments
|
|
102
|
-
.replace(/<[^>]*>/g, ""); // angle-bracket autolinks
|
|
52
|
+
// Re-exported because scripts/retire-bare-refs.mjs and the arms beside this file import them from
|
|
53
|
+
// here, and moving the table should not move every caller in the same commit.
|
|
54
|
+
export { CLASSES, offendingClasses, isProse, isScannable, withoutColourValues, withoutLinkTargets };
|
|
103
55
|
|
|
104
56
|
/** Every offending token on one added line, or an empty array. */
|
|
105
|
-
export const offendingTokens = (path, line) =>
|
|
106
|
-
if (!isProse(path, line)) return [];
|
|
107
|
-
return [...withoutColourValues(withoutLinkTargets(line)).matchAll(TOKEN)].map((m) => m[0]);
|
|
108
|
-
};
|
|
57
|
+
export const offendingTokens = (path, line) => offendingClasses(path, line).map((c) => c.token);
|
|
109
58
|
|
|
110
59
|
const baseArg = () => {
|
|
111
60
|
const i = process.argv.indexOf("--base");
|
|
@@ -143,14 +92,21 @@ function main() {
|
|
|
143
92
|
// difference between "looked and found nothing" and "had nothing to look at".
|
|
144
93
|
const hits = [];
|
|
145
94
|
for (const { path, line } of added) {
|
|
146
|
-
for (const
|
|
95
|
+
for (const c of offendingClasses(path, line)) hits.push({ path, ...c, line: line.trim().slice(0, 100) });
|
|
147
96
|
}
|
|
148
97
|
console.log(`added-reference-check: read ${added.length} added line(s) against ${base}`);
|
|
149
98
|
if (!hits.length) return;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
99
|
+
|
|
100
|
+
// GROUPED BY CLASS, because the remedy is per class and a flat list makes the reader derive it eight
|
|
101
|
+
// times. Each heading is said once, then the lines it applies to.
|
|
102
|
+
console.error(`\n${hits.length} line(s) added that a public tree must not carry:\n`);
|
|
103
|
+
for (const { id, why } of CLASSES) {
|
|
104
|
+
const mine = hits.filter((h) => h.id === id);
|
|
105
|
+
if (!mine.length) continue;
|
|
106
|
+
console.error(` ${id} — ${why}`);
|
|
107
|
+
for (const h of mine) console.error(` ${h.path}: ${h.token}\n ${h.line}`);
|
|
108
|
+
console.error("");
|
|
109
|
+
}
|
|
154
110
|
process.exit(1);
|
|
155
111
|
}
|
|
156
112
|
|
|
@@ -31,12 +31,15 @@
|
|
|
31
31
|
// That last paragraph prints on every run, including a clean one, because a guard whose "clean" is
|
|
32
32
|
// near-meaningless and does not say so is worse than no guard: its presence reads as coverage.
|
|
33
33
|
//
|
|
34
|
-
// The remedy is the convention, not this script —
|
|
35
|
-
//
|
|
34
|
+
// The remedy is the convention, not this script — ADR-0005, "Cite the symbol, not the line": a symbol
|
|
35
|
+
// survives every move and a number survives none, and where the target is not a named symbol, quote a few
|
|
36
|
+
// words of it. This used to name CONTRIBUTING.md, which contains no such section — that record says in as
|
|
37
|
+
// many words that it SUPERSEDES CONTRIBUTING's instruction, so the pointer was to the superseded half. This guard cannot enforce that (the
|
|
36
38
|
// ruling explicitly declines a form guard without an allowlist for the ~800 existing citations), so it
|
|
37
39
|
// holds the decidable perimeter and names the gap.
|
|
38
40
|
|
|
39
41
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
42
|
+
import { execFileSync } from "node:child_process"; // — the ratchet reads a range; injected in tests
|
|
40
43
|
import { fileURLToPath } from "node:url";
|
|
41
44
|
import { join, dirname, basename } from "node:path";
|
|
42
45
|
|
|
@@ -370,6 +373,41 @@ function main() {
|
|
|
370
373
|
// — the no-symbol slice: a cited span that is entirely blank or brace-only.
|
|
371
374
|
const { misses: blankTarget, unshipped: blankUnshipped } = structuralMisses(citations, readLines);
|
|
372
375
|
|
|
376
|
+
// ── `--ratchet`: refuse a NEW citation that carries a line number and no symbol ────────────────────
|
|
377
|
+
//
|
|
378
|
+
// Separate mode rather than folded into the default run, because it answers about a RANGE and the rest
|
|
379
|
+
// of this script answers about the tree. Mixing them would make one exit code stand for two questions.
|
|
380
|
+
if (process.argv.includes("--ratchet")) {
|
|
381
|
+
const i = process.argv.indexOf("--base");
|
|
382
|
+
const base = i >= 0 ? process.argv[i + 1] : "origin/main";
|
|
383
|
+
const { error, lines } = addedLinesSince(base);
|
|
384
|
+
if (error) {
|
|
385
|
+
// A RANGE THAT COULD NOT BE READ IS NOT A RANGE THAT ADDED NOTHING, and 2 keeps the two apart —
|
|
386
|
+
// 1 already means "found something" here.
|
|
387
|
+
console.error(`${GUARD} --ratchet: ${error}`);
|
|
388
|
+
console.error(" That is this check failing to look, not a clean range. Nothing was judged.");
|
|
389
|
+
process.exit(2);
|
|
390
|
+
}
|
|
391
|
+
const bare = newBareCitations(lines);
|
|
392
|
+
console.log(`${GUARD} --ratchet: ${lines.length} added line(s) against ${base}, ${bare.length} new bare citation(s)`);
|
|
393
|
+
for (const b of bare) console.log(` ${b.file}:${b.line} cites ${b.cited}:${b.start} with no symbol\n ${b.text.slice(0, 110)}`);
|
|
394
|
+
if (bare.length) {
|
|
395
|
+
console.error("\nA citation carrying a line number and no symbol is checked for EXISTENCE and nothing");
|
|
396
|
+
console.error("else, so it drifts onto a different real line and every test here still passes.");
|
|
397
|
+
console.error("");
|
|
398
|
+
console.error("The documented form (ADR-0005) carries no line number at all:");
|
|
399
|
+
console.error(" `toolGroupsForStage()` in `gather-config.mjs`");
|
|
400
|
+
console.error("Where the target is not a named symbol, quote a few words of it. A line number is also");
|
|
401
|
+
console.error("accepted when a bare identifier sits directly beside it — `pipeline.mjs:875 recordsFromSearch`");
|
|
402
|
+
console.error("— because that one this script can check. Backticks around the symbol are NOT stepped over,");
|
|
403
|
+
console.error("so a line number with a backticked symbol still reads as bare.");
|
|
404
|
+
console.error("");
|
|
405
|
+
console.error("The existing corpus is not your problem: this judges only what the range ADDS.");
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
|
|
373
411
|
if (process.argv.includes("--json")) {
|
|
374
412
|
console.log(JSON.stringify({
|
|
375
413
|
files: corpus.length, citations: citations.length, resolved: resolved.length,
|
|
@@ -595,6 +633,87 @@ const CALLS = /^\s*\(/;
|
|
|
595
633
|
const OWN_NUMBER = /^\s*(?:at\s+\d|:\d)/;
|
|
596
634
|
|
|
597
635
|
/** The symbol-shaped tokens adjoining a citation, in BOTH grammars: `cite SYM` and `SYM (cite)`. PURE. */
|
|
636
|
+
// ── THE RATCHET: A NEWLY ADDED CITATION CARRIES A SYMBOL (tracker issue 125) ────────────────────────
|
|
637
|
+
//
|
|
638
|
+
// The blindness above is not closeable on the existing corpus. Of the line citations in this tree only a
|
|
639
|
+
// minority name a symbol beside the number, and the rest are checked for EXISTENCE and nothing more — a
|
|
640
|
+
// citation that drifted onto a different real line reads as correct to every test here. Repointing them by
|
|
641
|
+
// hand is not the repair: the two hand measurements recorded in BLINDNESS both found wrong lines
|
|
642
|
+
// introduced by exactly that kind of pass.
|
|
643
|
+
//
|
|
644
|
+
// So this closes the class GOING FORWARD, at the only moment the correct symbol is cheap to write — when
|
|
645
|
+
// the author still knows what they meant. Same shape as the catalogue and release-note ratchets: the
|
|
646
|
+
// existing population is left to convert by attrition, and the number that must not grow is the number of
|
|
647
|
+
// citations nothing can check.
|
|
648
|
+
//
|
|
649
|
+
// WHAT IS REFUSED IS NARROW, and the two questions the issue left open are answered here rather than
|
|
650
|
+
// assumed:
|
|
651
|
+
//
|
|
652
|
+
// · a line number WITH a symbol is fine. The symbol is what makes it checkable, and `symbolClaims`
|
|
653
|
+
// above already verifies it; forbidding the number as well would be stricter than the harm.
|
|
654
|
+
// · a citation with NO line number is fine, symbol or not. It cannot go stale from an edit above it,
|
|
655
|
+
// which is the whole failure. This is also the answer for a data file, a fixture or a generated
|
|
656
|
+
// table, where there is no symbol to name — cite the file, leave the line off.
|
|
657
|
+
//
|
|
658
|
+
// Refused, therefore, is exactly one shape: a NEW citation carrying a line number and no symbol.
|
|
659
|
+
//
|
|
660
|
+
// PURE, and the added lines are injected, so a test drives it without a git repository — a ratchet that
|
|
661
|
+
// can only run against the real tree cannot be shown to fail.
|
|
662
|
+
// THE RULE'S OWN DEFINITION IS EXEMPT, AND NAMED RATHER THAN PATTERN-MATCHED. Its specimens are bare
|
|
663
|
+
// citations on purpose — a test that cannot write the shape it refuses cannot exercise it — so scanning it
|
|
664
|
+
// puts the guard's own examples into the population it polices, and the ratchet refuses its own arrival.
|
|
665
|
+
// Caught by driving the check against a range containing it, not by review.
|
|
666
|
+
//
|
|
667
|
+
// The cost of naming a file is that a REAL bare citation added there also escapes. That is the same trade
|
|
668
|
+
// the reference-strip signatures make for the same reason, and it is preferable to a pattern, which would
|
|
669
|
+
// quietly widen to files nobody considered.
|
|
670
|
+
export const RATCHET_EXEMPT = ["driver/test/a-new-citation-carries-something-that-can-be-checked.test.mjs"];
|
|
671
|
+
|
|
672
|
+
export function newBareCitations(addedLines) {
|
|
673
|
+
const out = [];
|
|
674
|
+
for (const { file, line, text } of addedLines ?? []) {
|
|
675
|
+
if (RATCHET_EXEMPT.includes(file)) continue;
|
|
676
|
+
for (const m of String(text ?? "").matchAll(CITE_RE)) {
|
|
677
|
+
// The same discriminator the corpus scan uses: a second `:number` closing a paren is a V8 stack
|
|
678
|
+
// frame, not a citation. Reading them as citations would refuse a fixture that captured a trace.
|
|
679
|
+
if (/^:\d+\)/.test(String(text).slice(m.index + m[0].length))) continue;
|
|
680
|
+
if (symbolsBeside(String(text), m.index, m[0].length).length > 0) continue;
|
|
681
|
+
out.push({ file, line, cited: m[1], start: Number(m[2]), text: String(text).trim() });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The lines a range ADDS, as `{ file, line, text }`. `run` is injected for the same reason as above.
|
|
689
|
+
*
|
|
690
|
+
* `--unified=0` so context lines are not read as additions — with context, every citation near an edit
|
|
691
|
+
* would be reported as new and the ratchet would refuse changes that added nothing.
|
|
692
|
+
*/
|
|
693
|
+
export function addedLinesSince(base = "origin/main", run = null) {
|
|
694
|
+
const exec = run ?? ((...a) => execFileSync("git", ["-C", ROOT, ...a], { encoding: "utf8", maxBuffer: 1e9 }));
|
|
695
|
+
let diff;
|
|
696
|
+
try { diff = exec("diff", "--unified=0", "--no-color", `${base}...HEAD`); }
|
|
697
|
+
catch (e) {
|
|
698
|
+
// A range that could not be read is not a range that added nothing, and the caller must not confuse them.
|
|
699
|
+
return { error: `could not diff ${base}...HEAD (${String(e?.message ?? e).split("\n")[0].slice(0, 90)})`, lines: [] };
|
|
700
|
+
}
|
|
701
|
+
const lines = [];
|
|
702
|
+
let file = null, next = 0;
|
|
703
|
+
for (const raw of diff.split("\n")) {
|
|
704
|
+
// The two file headers git writes, matched in full rather than by a "+++" prefix. An added line
|
|
705
|
+
// beginning with "++" arrives here as "+++...", so a prefix test drops it AND numbers every line
|
|
706
|
+
// after it in the hunk one low, because the line it skipped never advanced the counter. Both
|
|
707
|
+
// forms are exact: a deletion's header is the bare string, with no path and no timestamp.
|
|
708
|
+
if (raw.startsWith("+++ b/")) { file = raw.slice(6); continue; }
|
|
709
|
+
if (raw === "+++ /dev/null") { file = null; continue; }
|
|
710
|
+
const h = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
711
|
+
if (h) { next = Number(h[1]); continue; }
|
|
712
|
+
if (raw.startsWith("+")) { lines.push({ file, line: next++, text: raw.slice(1) }); }
|
|
713
|
+
}
|
|
714
|
+
return { error: null, lines };
|
|
715
|
+
}
|
|
716
|
+
|
|
598
717
|
export function symbolsBeside(line, index, length) {
|
|
599
718
|
const after = String(line).slice(index + length).replace(SEP_AFTER, "");
|
|
600
719
|
const am = after.match(/^[A-Za-z_$][A-Za-z0-9_$]*/);
|
package/scripts/e2e.mjs
CHANGED
|
@@ -630,7 +630,7 @@ export function bandForScenario(scenario, resolve = resolveForDoor) {
|
|
|
630
630
|
/**
|
|
631
631
|
* — WHICH ENGINE COMMIT THIS RUN LOADED, read from the run's own artifacts.
|
|
632
632
|
*
|
|
633
|
-
*
|
|
633
|
+
* the testing handover requires the commit on every run, and until this existed it was reconstructed
|
|
634
634
|
* by joining a checkout's reflog against `startedAt`. That reconstruction expires with the reflog,
|
|
635
635
|
* dies with a re-clone, and is simply wrong for any run made while the checkout sat detached — it
|
|
636
636
|
* produced two near-miss wrong certifications in two days.
|
|
@@ -1354,7 +1354,7 @@ function evalAssertion(a, runDir) {
|
|
|
1354
1354
|
// ── — THIS IS A DELIVERY CONTRACT, AND A RUN THAT DID NOT DELIVER NEVER ENTERED IT ────────
|
|
1355
1355
|
//
|
|
1356
1356
|
// Owner ruling, 2026-08-22, verbatim: "clean up the failed runs. they owe the client nothing."
|
|
1357
|
-
// (
|
|
1357
|
+
// (Recorded here because the rule this line encodes is a product
|
|
1358
1358
|
// decision, not a harness preference.)
|
|
1359
1359
|
//
|
|
1360
1360
|
// Measured before the ruling: `sendPending` is carried by 25 of 25 delivered runs and 0 of 29
|
package/scripts/env-classify.mjs
CHANGED
|
@@ -321,7 +321,7 @@ export function defaultAtReadSite(name, root = ROOT) {
|
|
|
321
321
|
|
|
322
322
|
/** The whole classification, pure over its inputs so a test drives it without a box. */
|
|
323
323
|
export function classify({ catalogue, sources, setup = setupNames(), readSites = defaultAtReadSite,
|
|
324
|
-
deploymentNames = DEPLOYMENT_NAMES } = {}) {
|
|
324
|
+
deploymentNames = DEPLOYMENT_NAMES, declared = new Map() } = {}) {
|
|
325
325
|
// NAMED OVERRIDES, listed rather than folded into a pattern so that each can be argued with.
|
|
326
326
|
//
|
|
327
327
|
// `CLEAROTRON_AGENT_WHATSAPP` matches no deployment shape and is not a knob: production sets it to a
|
|
@@ -370,7 +370,11 @@ export function classify({ catalogue, sources, setup = setupNames(), readSites =
|
|
|
370
370
|
|
|
371
371
|
const rows = catalogue.map((name) => {
|
|
372
372
|
const setIn = ["prod", "test", "config", "ci", "e2e"].filter((k) => sources[k]?.has(name));
|
|
373
|
-
|
|
373
|
+
// `declared` is the catalogue's own `# effect:` for this name, carried on the row so the artifact
|
|
374
|
+
// says what the document claims beside what this script derived. The two are different questions and
|
|
375
|
+
// the row is where a reader compares them.
|
|
376
|
+
return { name, class: cls(name), everSet: setIn, declared: declared.get(name) ?? null,
|
|
377
|
+
documented: Boolean(sources.docs?.has(name)) };
|
|
374
378
|
});
|
|
375
379
|
|
|
376
380
|
// "Set nowhere" is not by itself a licence to delete, and this is where 62 of 84 left the list.
|
|
@@ -378,6 +382,57 @@ export function classify({ catalogue, sources, setup = setupNames(), readSites =
|
|
|
378
382
|
const GATE_RE = /(===|!==|==|!=)\s*"[01]"|"[01]"\s*(===|!==|==|!=)|\|\|\s*"1"/;
|
|
379
383
|
const sub = {};
|
|
380
384
|
for (const n of tuning) {
|
|
385
|
+
// ── A DOCUMENT SAYING "THIS IS NOT A KNOB" KEEPS ITS NAME OFF THIS LIST ────────────────────────
|
|
386
|
+
//
|
|
387
|
+
// The classifier keys on the NAME and `tuning` is its residual — what a name falls to when no shape
|
|
388
|
+
// matches. So a name whose catalogue row declares any other effect reaches the deletion walk on a
|
|
389
|
+
// class its own documentation contradicts.
|
|
390
|
+
//
|
|
391
|
+
// THE TEST IS `!== "tuning"`, SO IT EXCLUDES ALL FIVE OTHER CLASSES. `tuning` is the only declared
|
|
392
|
+
// effect that AGREES with being a knob, so it is the only one that may reach the walk. Each of the
|
|
393
|
+
// five, in the catalogue's own words, and why deleting the name would cost something:
|
|
394
|
+
//
|
|
395
|
+
// silent-output-change "changes what a run produces, and nothing in the run's own artifacts says
|
|
396
|
+
// so" — the worst one to delete, because the loss is invisible in the output.
|
|
397
|
+
// disclosed-gate "changes what a run covers, AND the run discloses the gap" — deleting it
|
|
398
|
+
// silently restores coverage the operator chose to switch off, or removes
|
|
399
|
+
// their ability to switch it off at all.
|
|
400
|
+
// credential "absent, the run refuses at preflight by name" — deleting it turns a
|
|
401
|
+
// named refusal into an unexplained one.
|
|
402
|
+
// deployment "where input and output live; the conclusion a run reaches is unchanged"
|
|
403
|
+
// — unchanged CONCLUSION is not unchanged behaviour. This is the class the
|
|
404
|
+
// whole rule comes from: two notification addresses, legitimately unset on
|
|
405
|
+
// the deployment being read, were proposed for deletion on exactly this
|
|
406
|
+
// mismatch. Excluding it is the original finding, not an extension of it.
|
|
407
|
+
// harness "read only on a fixture, replay or self-test path; no production run
|
|
408
|
+
// reaches it" — the one that reads as safe to delete and is not. Deleting a
|
|
409
|
+
// harness name does not remove a knob nobody uses; it removes the only way
|
|
410
|
+
// a test can run. That argument is already made below for `instrument`, and
|
|
411
|
+
// it is the same argument. A declaration is a stronger version of it,
|
|
412
|
+
// because it does not depend on the spelling.
|
|
413
|
+
//
|
|
414
|
+
// The alternative — narrowing to the three classes this comment used to name — would put
|
|
415
|
+
// `deployment` back on the list and re-admit the defect the rule exists to stop.
|
|
416
|
+
//
|
|
417
|
+
// WHAT THIS TREE CAN SHOW YOU, and it is not the number to look for. Measured 2026-09-08: this tree
|
|
418
|
+
// carries 43 catalogued rows and 12 declarations, and NO name is in the contradicting position, so
|
|
419
|
+
// the rule changes nothing here and a check written against the live catalogue would pass while
|
|
420
|
+
// looking at nothing. That is why the checks plant a catalogue rather than reading this one. Over
|
|
421
|
+
// the full catalogue the same day the position holds seventeen names — ten declared
|
|
422
|
+
// `silent-output-change`, four `disclosed-gate`, two `harness`, one `credential` — and every one of
|
|
423
|
+
// them leaves the walk further down for an UNRELATED reason: the instrument regex matching their
|
|
424
|
+
// spelling, a non-numeric default, or no default found. The file already says what that is worth
|
|
425
|
+
// about a different name: exclusion "for an unrelated reason (no default found), which is luck, not
|
|
426
|
+
// a rule". Rename one of them to something without `DUMP` or `PROBE` in it and it joins the deletion
|
|
427
|
+
// population with a document beside it saying it changes what a run produces.
|
|
428
|
+
//
|
|
429
|
+
// Those counts are a dated reading and they move; two of the seventeen were added the same week.
|
|
430
|
+
// The RULE is what is being asserted here, not the population.
|
|
431
|
+
//
|
|
432
|
+
// So the declaration is read FIRST and it is the rule. Nothing is silently dropped: the names land
|
|
433
|
+
// in their own bucket, and the row carries the declaration that put them there.
|
|
434
|
+
const say = declared.get(n);
|
|
435
|
+
if (say && say !== "tuning") { sub[n] = "declared-not-a-knob"; continue; }
|
|
381
436
|
const body = git("grep", "-n", "--", n, "--", "*.mjs", "*.js")
|
|
382
437
|
.split("\n").filter((l) => l && !/(^|\/)test\//.test(l)).join("\n");
|
|
383
438
|
const onlyTests = git("grep", "-l", "--", n, "--", "*.mjs", "*.js").split("\n").filter(Boolean)
|
|
@@ -405,6 +460,7 @@ export function classify({ catalogue, sources, setup = setupNames(), readSites =
|
|
|
405
460
|
instrument: of("instrument"), "path-switch": of("path-switch"),
|
|
406
461
|
"model-or-agent-selector": of("model-or-agent-selector"),
|
|
407
462
|
"non-numeric-default": of("non-numeric-default"), "no-default-found": of("no-default-found"),
|
|
463
|
+
"declared-not-a-knob": of("declared-not-a-knob"),
|
|
408
464
|
"deletable-number": of("deletable-number"),
|
|
409
465
|
} };
|
|
410
466
|
}
|
|
@@ -414,7 +470,11 @@ function build() {
|
|
|
414
470
|
{ encoding: "utf8", cwd: ROOT, maxBuffer: 1e8 }));
|
|
415
471
|
const catalogue = audit.catalogue.rows.map((r) => r.name);
|
|
416
472
|
const sources = gather();
|
|
417
|
-
|
|
473
|
+
// The catalogue's own declarations, handed in so the deletion walk can read them. Same rows the
|
|
474
|
+
// audit already produced — not a second parse of the same documents, which would be a second thing
|
|
475
|
+
// to keep in step.
|
|
476
|
+
const declared = new Map(audit.catalogue.rows.filter((r) => r.effect).map((r) => [r.name, r.effect]));
|
|
477
|
+
const { rows, buckets } = classify({ catalogue, sources, declared });
|
|
418
478
|
const prodRead = sources.prodRead;
|
|
419
479
|
const by = (k) => rows.filter((r) => r.class === k).length;
|
|
420
480
|
return {
|
|
@@ -428,6 +488,10 @@ function build() {
|
|
|
428
488
|
names: buckets["deletable-number"], count: buckets["deletable-number"].length,
|
|
429
489
|
},
|
|
430
490
|
_excludedFromDeletion: {
|
|
491
|
+
// FIRST, because it is the only one of these that is a RULE rather than an observation about a
|
|
492
|
+
// name's spelling or its default: the catalogue says this name changes what a run produces, so it
|
|
493
|
+
// is not a spare knob whatever its shape suggests.
|
|
494
|
+
"declared-not-a-knob": buckets["declared-not-a-knob"],
|
|
431
495
|
instrument: buckets.instrument, "path-switch": buckets["path-switch"],
|
|
432
496
|
"model-or-agent-selector": buckets["model-or-agent-selector"],
|
|
433
497
|
"non-numeric-default": buckets["non-numeric-default"], "no-default-found": buckets["no-default-found"],
|