clearotron 0.2.2 → 0.2.3
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 +28 -0
- package/.nvmrc +1 -1
- package/INSTALL.md +13 -11
- package/README.md +11 -16
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +4 -0
- package/bin/onboard.mjs +195 -19
- package/bin/start.mjs +154 -8
- package/build-info.json +2 -2
- 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/PORTAL.md +2 -2
- package/docs/README.md +5 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +14 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/driver/CHANGELOG.md +56 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/dev-portal.mjs +2 -2
- package/driver/driver.config.mjs +1 -1
- package/driver/engine/cli-version.mjs +103 -0
- 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 +18 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +5 -5
- package/driver/plain-register.mjs +120 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +40 -1
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +26 -7
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +121 -1
- 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 +2 -2
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +2 -2
- 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/stages.mjs +4 -4
- package/driver/suite-census.json +134 -38
- package/driver/unit-inventory.mjs +184 -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 +2 -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 +28 -10
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-CEYvXdqH.js} +226 -49
- 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 +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/strip-tracker-citations.mjs +29 -6
- package/scripts/test-run.mjs +16 -0
- package/shared/anon-overlay.mjs +3 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/invocation.mjs +28 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/node-floor.mjs +97 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- 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
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.3",
|
|
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.
|
|
@@ -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/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"],
|
|
@@ -90,7 +90,7 @@ import { unitsActiveVerdict } from "../driver/unit-state-verdict.mjs";
|
|
|
90
90
|
import { deploymentBox } from "../shared/deployment-box.mjs"; // — extracted; one allowlist, two readers
|
|
91
91
|
import { unitFileDriftVerdict } from "../driver/unit-file-drift.mjs"; //
|
|
92
92
|
import { placeholdersIn, resolveValues, renderUnit } from "../driver/systemd/render-units.mjs"; //
|
|
93
|
-
import { CHECKED_UNITS, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory, unitClone } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
93
|
+
import { CHECKED_UNITS, CHECKED_TIMERS, timerVerdict, unitInventoryVerdict, serviceCommitVerdict, unitWorkingDirectory, unitClone } from "../driver/unit-inventory.mjs"; // · -bundle ·
|
|
94
94
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // the ONE ExecStart parser — a unit says which module it runs
|
|
95
95
|
import { treeOfRunning } from "../shared/checkout-move.mjs"; // …and the live argv says which tree that module came from
|
|
96
96
|
import { findUnitFiles, unitFilePath } from "../driver/unit-files.mjs"; //
|
|
@@ -226,6 +226,35 @@ function userBusEnv() {
|
|
|
226
226
|
// Returns {clones, probe}. `probe` is the honest answer to "could I look at all?" — {ok, why} — and it is
|
|
227
227
|
// SEPARATE from the clones list on purpose. "enumerated, nothing to compare" and "could not enumerate"
|
|
228
228
|
// were the same empty array before, and this suite exists because absences were read as successes.
|
|
229
|
+
/**
|
|
230
|
+
* Ask each declared TIMER about itself, as a timer.
|
|
231
|
+
*
|
|
232
|
+
* Deliberately not folded into the loop below. That loop asks a service for its WorkingDirectory and
|
|
233
|
+
* MainPID and attributes a running process to a checkout — none of which a `.timer` has, so putting
|
|
234
|
+
* timers through it would produce rows whose every attribution field is empty for a reason that is not
|
|
235
|
+
* a finding. Two questions, two calls, and each says what it could not answer.
|
|
236
|
+
*/
|
|
237
|
+
function declaredTimers() {
|
|
238
|
+
if (!CHECKED_TIMERS.length) return { rows: [], probeFailed: null };
|
|
239
|
+
const env = userBusEnv();
|
|
240
|
+
const rows = [];
|
|
241
|
+
let probeFailed = null;
|
|
242
|
+
for (const unit of CHECKED_TIMERS) {
|
|
243
|
+
try {
|
|
244
|
+
const shown = execFileSync("systemctl", ["--user", "show", unit, "-p", "ActiveState", "-p", "LoadState"],
|
|
245
|
+
{ encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
|
|
246
|
+
const f = Object.fromEntries(shown.split("\n").filter(Boolean).map((l) => { const [k, ...v] = l.split("="); return [k, v.join("=")]; }));
|
|
247
|
+
// An EMPTY answer is the no-session-bus case, and it must not read as a stopped timer — that is
|
|
248
|
+
// the shape that turns "could not ask" into a finding about the box.
|
|
249
|
+
if (!f.ActiveState) { probeFailed = probeFailed ?? `systemctl --user answered nothing for ${unit}`; continue; }
|
|
250
|
+
rows.push({ unit, active: f.ActiveState || null, load: f.LoadState || null });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
probeFailed = probeFailed ?? String(e?.message ?? e).slice(0, 120);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { rows, probeFailed };
|
|
256
|
+
}
|
|
257
|
+
|
|
229
258
|
function serviceClones() {
|
|
230
259
|
// — the list is DECLARED, not written here. It used to be eight names inline, which put a unit
|
|
231
260
|
// inside the drift guarantee or outside it by omission: `client-access` was live on production and in
|
|
@@ -889,6 +918,14 @@ else {
|
|
|
889
918
|
const v = unitInventoryVerdict({ live: liveUnits, files: walk.files, collisions: walk.collisions,
|
|
890
919
|
filesError: walk.error, box, probe });
|
|
891
920
|
record("every live unit is declared", v.state, v.message);
|
|
921
|
+
|
|
922
|
+
// — AND WHETHER ANYTHING STILL STARTS THE TIMER-DRIVEN ONES. Reported separately from the line above
|
|
923
|
+
// because it answers a different question: that one says a declared unit exists and is not adrift,
|
|
924
|
+
// this one says its timer is still armed. A service whose timer was stopped satisfies the first and
|
|
925
|
+
// fails the second, and reads `inactive` for both — which is why one line could not carry both.
|
|
926
|
+
const t = declaredTimers();
|
|
927
|
+
const tv = timerVerdict(t.rows, { probeFailed: t.probeFailed });
|
|
928
|
+
record("every declared timer is still armed", tv.state, tv.message);
|
|
892
929
|
}
|
|
893
930
|
|
|
894
931
|
// ── — EVERY QUEUE THIS DEPLOYMENT WOULD DRAIN IS WATCHED BY SOMETHING ──────────────────────────
|
|
@@ -151,8 +151,6 @@ export function noticeBody({ runUrl, sha, subject, failedJobs = [], lastGreenAt,
|
|
|
151
151
|
+ "passes — a run that never started cannot make the platform green."
|
|
152
152
|
: "This thread is reused for every red night. Close it once the platform is green again — the next "
|
|
153
153
|
+ "red opens a new one.",
|
|
154
|
-
"",
|
|
155
|
-
"Agent: role-dev · bergface",
|
|
156
154
|
].join("\n");
|
|
157
155
|
}
|
|
158
156
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// The install refuses an unsupported Node BEFORE anything is written — tracker issue 364.
|
|
5
|
+
//
|
|
6
|
+
// Three declarations already stated the requirement and none of them bound anyone: `engines.node` is a
|
|
7
|
+
// WARNING unless the person installing has set `engine-strict`, `.nvmrc` is read by nvm and nothing
|
|
8
|
+
// else, and the install guide is prose. An outside report arrived as "some node issues, not as obvious
|
|
9
|
+
// as the README made it seem" — which is what a warning inside an install log looks like from outside.
|
|
10
|
+
//
|
|
11
|
+
// So this is a `preinstall` hook, node builtins only, because nothing is installed when it runs.
|
|
12
|
+
//
|
|
13
|
+
// IT NAMES THE LATER FAILURE, not just the version. The whole defect is that an unsupported Node
|
|
14
|
+
// installs cleanly and then fails somewhere else saying nothing about Node, so a message that only
|
|
15
|
+
// prints a semver range leaves the reader where they started.
|
|
16
|
+
import { declaredRange, floorOf, meetsFloor, nodeFloorRefusal } from "../shared/node-floor.mjs";
|
|
17
|
+
|
|
18
|
+
const range = declaredRange();
|
|
19
|
+
if (!meetsFloor(process.versions.node, floorOf(range))) {
|
|
20
|
+
process.stderr.write(`\n${nodeFloorRefusal({ current: process.versions.node, required: floorOf(range).join("."), range })}\n`);
|
|
21
|
+
process.stderr.write(" Nothing has been installed.\n\n nvm install 22 && nvm use 22\n\n");
|
|
22
|
+
process.stderr.write(" Without nvm, take the current 22.x from https://nodejs.org/.\n\n");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
@@ -77,6 +77,15 @@ export const EXCLUDED = [
|
|
|
77
77
|
"driver/test/a-bare-reference-added-in-a-diff-is-refused.test.mjs",
|
|
78
78
|
"driver/test/prompt-payload-names-no-tracker-issue.test.mjs",
|
|
79
79
|
"driver/test/euipo-environment-doctrine.test.mjs",
|
|
80
|
+
// THIS RULE'S OWN DEFINITION, and it caught itself. Both files below QUOTE the residue in order to
|
|
81
|
+
// define it — the specimens the sweep is tested against, and the worked examples in the header above.
|
|
82
|
+
// Sweeping them rewrites the corpus this instrument is checked by, and it would then pass on something
|
|
83
|
+
// it had stopped checking: the arms would assert that a stripped line equals a stripped line. The
|
|
84
|
+
// sibling module that counts the earlier strip's residue learned the same lesson and names its own
|
|
85
|
+
// three files for the same reason. Found by running the sweep after fixing its report, which is the
|
|
86
|
+
// only reason it surfaced: before that, these lines were counted and never listed.
|
|
87
|
+
"scripts/strip-tracker-citations.mjs",
|
|
88
|
+
"driver/test/the-citation-strip-removes-openers-and-nothing-else.test.mjs",
|
|
80
89
|
// Pinned at a content hash; a prose repair is not worth spending a freeze on.
|
|
81
90
|
"driver/publish/render.mjs",
|
|
82
91
|
];
|
|
@@ -86,28 +95,42 @@ export const isScannable = (f) =>
|
|
|
86
95
|
|
|
87
96
|
/** Per-file classification. PURE, and `read` is injected so an arm can drive it over a synthetic tree. */
|
|
88
97
|
export function surveyOf(files, read) {
|
|
89
|
-
const stripped = {}, handoff = [];
|
|
98
|
+
const stripped = {}, handoff = [], unreadable = [];
|
|
90
99
|
let strippedTotal = 0, remainingTotal = 0;
|
|
91
100
|
for (const f of files.filter(isScannable)) {
|
|
92
101
|
let text;
|
|
93
|
-
|
|
102
|
+
// AN UNREADABLE FILE IS A FINDING, NOT A QUIET SKIP. This used to `continue` into no counter and no
|
|
103
|
+
// error channel, so "I could not open this" and "there was nothing to do here" produced identical
|
|
104
|
+
// output — in the report whose whole job is to tell somebody a hundred-file rewrite is safe. The
|
|
105
|
+
// count is reported before anything else when it is non-zero.
|
|
106
|
+
try { text = read(f); } catch (e) { unreadable.push({ file: f, why: String(e?.message ?? e).slice(0, 120) }); continue; }
|
|
94
107
|
let n = 0;
|
|
108
|
+
// CLASSIFY AFTER REPLACING, NOT INSTEAD OF IT. The replacement is not global, so a line carrying an
|
|
109
|
+
// opener AND a second citation further along was stripped once, counted as done, and never reached
|
|
110
|
+
// the hand-off list a person is told to read — its survivor was invisible in the one place it should
|
|
111
|
+
// have been named. Test the RESULT: a line can be both stripped and still owed to a reader.
|
|
95
112
|
const out = text.split("\n").map((line, i) => {
|
|
96
|
-
|
|
97
|
-
if (ANY_CITATION.test(
|
|
98
|
-
return
|
|
113
|
+
const after = OPENER.test(line) ? (n++, line.replace(OPENER, "$1")) : line;
|
|
114
|
+
if (ANY_CITATION.test(after)) handoff.push({ file: f, line: i + 1, text: after.trim() });
|
|
115
|
+
return after;
|
|
99
116
|
});
|
|
100
117
|
if (n) { stripped[f] = n; strippedTotal += n; }
|
|
101
118
|
remainingTotal += out.filter((l) => ANY_CITATION.test(l)).length;
|
|
102
119
|
if (APPLY && n) writeFileSync(join(ROOT, f), out.join("\n"));
|
|
103
120
|
}
|
|
104
|
-
return { strippedTotal, remainingTotal, stripped, handoff };
|
|
121
|
+
return { strippedTotal, remainingTotal, stripped, handoff, unreadable };
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
108
125
|
const tracked = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 })
|
|
109
126
|
.split("\n").filter(Boolean);
|
|
110
127
|
const s = surveyOf(tracked, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
128
|
+
// BEFORE ANYTHING ELSE, because every number under it is about the files that COULD be read.
|
|
129
|
+
if (s.unreadable.length) {
|
|
130
|
+
console.log(`${s.unreadable.length} file(s) COULD NOT BE READ — every count below excludes them:`);
|
|
131
|
+
for (const u of s.unreadable) console.log(` ${u.file} ${u.why}`);
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
111
134
|
console.log(`${APPLY ? "stripped" : "would strip"} ${s.strippedTotal} opener(s) across ${Object.keys(s.stripped).length} file(s)`);
|
|
112
135
|
console.log(`${s.handoff.length} citation(s) are NOT openers and need a reader — the hand-off list:`);
|
|
113
136
|
for (const h of s.handoff) console.log(` ${h.file}:${h.line} ${h.text.slice(0, 110)}`);
|
package/scripts/test-run.mjs
CHANGED
|
@@ -223,6 +223,22 @@ 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 ──────────────────────────────────
|
|
227
|
+
//
|
|
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
|
|
230
|
+
// exclusion that protects the tarball does not apply. They are marked `testFixture` and the loader
|
|
231
|
+
// refuses them on every route.
|
|
232
|
+
//
|
|
233
|
+
// The checks still need them, under the same keys the baselines use, and most do not call the loader —
|
|
234
|
+
// they exercise code that does, so there is no argument to pass. One variable here is the whole ask,
|
|
235
|
+
// and it is visible: a reader of this file can see that the suite runs against a roster no install
|
|
236
|
+
// resolves, which is a thing worth being able to see rather than a default nobody set.
|
|
237
|
+
process.env.CLEAROTRON_TEST_FIXTURE_PROFILES ??= "1";
|
|
238
|
+
// The demo account is refused from a fresh install's roster for the same reason and by the same
|
|
239
|
+
// mechanism; the suite asks for it here, once, so no check has to know it was hidden.
|
|
240
|
+
process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
|
|
241
|
+
|
|
226
242
|
// ── A PARTLY-BLIND SUITE IS NOT A FAILING SUITE ────────────────────────────────────────────
|
|
227
243
|
//
|
|
228
244
|
// `git worktree add` gives you no `node_modules`, nothing in this repo creates one, and the tracker asks
|
package/shared/anon-overlay.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Feedback console went with the quality subsystem, so two of those five had not rendered for
|
|
7
7
|
// months. Lets us screen-share the site without
|
|
8
8
|
// revealing real client names or the marks being cleared: client names map to descriptive aliases
|
|
9
|
-
// (e.g.
|
|
9
|
+
// (e.g. Coastline Drinks → "Beverage company") and marks blur out, behind ONE site-wide toggle (no
|
|
10
10
|
// hover-reveal). Display-only — it never mutates stored data, run metadata, or form values.
|
|
11
11
|
//
|
|
12
12
|
// SHAPE (mirrors site-nav.mjs's NAV_CSS/siteNav split):
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// • anonToggle(cfg?) — the toggle button markup for the nav ('' when disabled).
|
|
20
20
|
//
|
|
21
21
|
// Tagging conventions the browser overlay understands:
|
|
22
|
-
// <span data-anon="client" data-anon-key="
|
|
22
|
+
// <span data-anon="client" data-anon-key="coastline">Coastline Drinks</span> → text swapped for the alias
|
|
23
23
|
// <span data-anon="mark" data-anon-key="…" data-anon-run="…">NOVA PULSE</span> → blurred (unless exempt)
|
|
24
24
|
// <a data-anon-href data-anon-key="…" data-anon-run="…" href="…"> → href neutralised to # (unless exempt)
|
|
25
25
|
// A run/client is EXEMPT (shown real) when its key ∈ demoAllow.clientKeys or its run matches
|
|
@@ -81,7 +81,7 @@ const OVERLAY_BODY = `(function(){
|
|
|
81
81
|
var DEAD_TITLE = "hidden in privacy mode — toggle Privacy OFF to open";
|
|
82
82
|
var DA = C.demoAllow || {clientKeys:[],runs:[]};
|
|
83
83
|
// Case-insensitive client lookup, and the reason it is not redundant: surfaces tag rows by profile
|
|
84
|
-
// KEY ("
|
|
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
86
|
// retired (tracker issue 265) — the fold stays because it is what makes the lookup total over either spelling,
|
|
87
87
|
// not because those pages are coming back.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `process.env.PROFILE_DIR || join(HERE, "profiles")` while the runs, the roster, the account picker and
|
|
8
8
|
// the artifacts door read `CLEAROTRON_CUSTOMERS_DIR`. Nothing set `PROFILE_DIR` — not `onboard`, not
|
|
9
9
|
// `.env.example`, not the box's own env — so the settings surface served the product's BUNDLED DEMO
|
|
10
|
-
// BUNDLE
|
|
10
|
+
// BUNDLE while the runs served the real store.
|
|
11
11
|
//
|
|
12
12
|
// WHAT THE CLIENT SAW: every brand owner added the documented way was "These settings are not available
|
|
13
13
|
// to you." The clearance itself submitted and ran under their framework. Only the surface that
|
package/shared/invocation.mjs
CHANGED
|
@@ -92,6 +92,34 @@ export function standFrom(dir) {
|
|
|
92
92
|
return at === -1 ? s : s.slice(0, at);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* WHICH ROUTE THIS INSTALL ARRIVED BY — a package, or a source checkout.
|
|
97
|
+
*
|
|
98
|
+
* The setup wizard has two spellings and only one of them works for any given reader. Someone who ran
|
|
99
|
+
* `npm i clearotron` has no package scripts, so `npm run setup` is a command they cannot type; someone
|
|
100
|
+
* working in a clone of the source has the scripts but no `clearotron` binary linked for them, because
|
|
101
|
+
* npm links a package's bin for its dependencies and never for itself. Naming one of the two on a
|
|
102
|
+
* screen is a coin flip, and the screen that names it is read by people who have just installed and
|
|
103
|
+
* have nothing else to go on.
|
|
104
|
+
*
|
|
105
|
+
* The answer is a property of where this file sits on disk, so it is derived from that. A package —
|
|
106
|
+
* local or global — is unpacked inside a `node_modules` directory; a checkout is not. `standFrom`
|
|
107
|
+
* already draws that line for the sibling question of where to stand, and reusing it is deliberate:
|
|
108
|
+
* two predicates for "am I a package" would be two chances to disagree.
|
|
109
|
+
*
|
|
110
|
+
* NO PATH IS RETURNED, and that is the point of splitting it out from `invocationForm`. That function's
|
|
111
|
+
* `prefix` can be `cd /srv/whatever && npx `, which is the server's own layout, its account name and
|
|
112
|
+
* its directory structure. This answers the same question with a word, so the answer can travel to a
|
|
113
|
+
* browser without carrying the machine with it.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} dir where this module is installed
|
|
116
|
+
* @returns {'packaged'|'checkout'}
|
|
117
|
+
*/
|
|
118
|
+
export function installRoute(dir = INSTALL_DIR) {
|
|
119
|
+
const s = String(dir ?? "");
|
|
120
|
+
return standFrom(s) === s ? "checkout" : "packaged";
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
/**
|
|
96
124
|
* WHERE npm PUT THE EXECUTABLE for a GLOBAL install of this package —.
|
|
97
125
|
*
|
|
@@ -43,6 +43,7 @@ export const NAMES_IN_FORCE = Object.freeze([
|
|
|
43
43
|
"CLEAROTRON_DEFAULT_AGENT",
|
|
44
44
|
"CLEAROTRON_DELIVERY",
|
|
45
45
|
"CLEAROTRON_DEMO",
|
|
46
|
+
"CLEAROTRON_DEMO_PROFILES",
|
|
46
47
|
"CLEAROTRON_DISPATCH_RECORD",
|
|
47
48
|
"CLEAROTRON_DOCTOR_ASSUME_PINNED",
|
|
48
49
|
"CLEAROTRON_DUMP_JSON",
|
|
@@ -162,6 +163,7 @@ export const NAMES_IN_FORCE = Object.freeze([
|
|
|
162
163
|
"CLEAROTRON_SUITE_TELEMETRY_DIR",
|
|
163
164
|
"CLEAROTRON_SYNTHESIS_MODEL",
|
|
164
165
|
"CLEAROTRON_TAINT_CHAIN",
|
|
166
|
+
"CLEAROTRON_TEST_FIXTURE_PROFILES",
|
|
165
167
|
"CLEAROTRON_TURN_CAP",
|
|
166
168
|
"CLEAROTRON_TURN_LOCK_POLL_MS",
|
|
167
169
|
"CLEAROTRON_UNREACHABLE_SENIOR",
|