@webpieces/rules-config 0.4.726 → 0.4.727
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/package.json +1 -1
- package/src/abstract-rule.d.ts +2 -1
- package/src/abstract-rule.js +2 -1
- package/src/abstract-rule.js.map +1 -1
- package/src/build-log-doc.js +2 -1
- package/src/build-log-doc.js.map +1 -1
- package/src/builds-log.d.ts +1 -1
- package/src/builds-log.js +1 -1
- package/src/builds-log.js.map +1 -1
- package/src/cli-args.d.ts +1 -1
- package/src/cli-args.js +1 -1
- package/src/cli-args.js.map +1 -1
- package/src/fix-option.d.ts +2 -1
- package/src/fix-option.js +2 -1
- package/src/fix-option.js.map +1 -1
- package/src/home-config.d.ts +5 -3
- package/src/home-config.js +7 -5
- package/src/home-config.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.727",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/abstract-rule.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ import { BaseRuleConfig } from './rule-configs';
|
|
|
23
23
|
*
|
|
24
24
|
* BOTH are constructor params with NO default. A `configKey = name` default would make the common
|
|
25
25
|
* case silent and the shared case a thing you have to remember — which is the same "widening that is
|
|
26
|
-
* an ABSENCE rather than a token"
|
|
26
|
+
* an ABSENCE rather than a token" `.claude/rules/no-backwards-compat.md` rejects. Every rule says which
|
|
27
|
+
* key configures it, out loud,
|
|
27
28
|
* so `grep "'branch-state-guard'"` lists every class that key governs.
|
|
28
29
|
*/
|
|
29
30
|
export declare abstract class AbstractRule<C extends BaseRuleConfig> {
|
package/src/abstract-rule.js
CHANGED
|
@@ -26,7 +26,8 @@ const skip_rule_1 = require("./skip-rule");
|
|
|
26
26
|
*
|
|
27
27
|
* BOTH are constructor params with NO default. A `configKey = name` default would make the common
|
|
28
28
|
* case silent and the shared case a thing you have to remember — which is the same "widening that is
|
|
29
|
-
* an ABSENCE rather than a token"
|
|
29
|
+
* an ABSENCE rather than a token" `.claude/rules/no-backwards-compat.md` rejects. Every rule says which
|
|
30
|
+
* key configures it, out loud,
|
|
30
31
|
* so `grep "'branch-state-guard'"` lists every class that key governs.
|
|
31
32
|
*/
|
|
32
33
|
class AbstractRule {
|
package/src/abstract-rule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abstract-rule.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/abstract-rule.ts"],"names":[],"mappings":";;;AACA,2CAA6C;AAE7C
|
|
1
|
+
{"version":3,"file":"abstract-rule.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/abstract-rule.ts"],"names":[],"mappings":";;;AACA,2CAA6C;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAsB,YAAY;IACC;IAAoB;IAAuB;IAA1E,YAA+B,MAAS,EAAW,IAAY,EAAW,SAAiB;QAA5D,WAAM,GAAN,MAAM,CAAG;QAAW,SAAI,GAAJ,IAAI,CAAQ;QAAW,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAE/F,uFAAuF;IACvF,SAAS;QACL,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAA,0BAAc,EAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,IAAI,CAAC,MAAM,CAAC,wBAAwB,CAAC,CAAC;QACrG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;IACtB,CAAC;CACJ;AATD,oCASC","sourcesContent":["import { BaseRuleConfig } from './rule-configs';\nimport { shouldSkipRule } from './skip-rule';\n\n/**\n * Shared base for every rule in BOTH packages (ai-hook-rules and code-rules). A rule is\n * constructed with its typed config (`new NoAnyUnknownRule(config['no-any-unknown'])`), so the\n * config class is genuinely consumed — find-usages/rename work across packages.\n *\n * It is execution-agnostic: it owns only `name` + `configKey` + the on/off + escape-hatch decision\n * (`shouldRun`). Each package's base adds its own execution surface (ai-hook `check(ctx)`,\n * code-rules `run(workspaceRoot)`), so rules-config stays free of package-specific types.\n *\n * ## `name` and `configKey` are two different identities, and they are BOTH required\n *\n * `name` is the OPERATOR identity: it is what a decision-log line carries as `rule=`, what a deny\n * report titles itself with, and what prose in `guards/**` names when it describes behaviour. It is\n * per-CLASS and it never changes when the config collapses.\n *\n * `configKey` is the webpieces.config.json KEY whose entry configures this rule. Several classes may\n * share one key when they implement one POLICY — the four branch-state guards all read\n * `branch-state-guard`, the four PR-lifecycle guards all read `pr-lifecycle-guard` — because a\n * consumer switches a POLICY on and off, not an implementation class. Half a policy (`read-stale-guard`\n * OFF while `merged-branch-bash-guard` stays ON: read the file, yes; `cat` the same file, no) was\n * representable while the two were the same string, and this split is what makes it unrepresentable.\n *\n * BOTH are constructor params with NO default. A `configKey = name` default would make the common\n * case silent and the shared case a thing you have to remember — which is the same \"widening that is\n * an ABSENCE rather than a token\" `.claude/rules/no-backwards-compat.md` rejects. Every rule says which\n * key configures it, out loud,\n * so `grep \"'branch-state-guard'\"` lists every class that key governs.\n */\nexport abstract class AbstractRule<C extends BaseRuleConfig> {\n constructor(protected readonly config: C, readonly name: string, readonly configKey: string) {}\n\n /** True unless the rule is `mode: \"OFF\"` or skipped by a branch/epoch escape hatch. */\n shouldRun(): boolean {\n if (this.config.mode === 'OFF') return false;\n const skip = shouldSkipRule(this.config.turnOffRuleUntilEpoch, this.config.turnOffRuleWhileOnBranch);\n return !skip.skip;\n }\n}\n"]}
|
package/src/build-log-doc.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// propagate; a webpieces PR cannot edit a consumer's CLAUDE.md, and should not.
|
|
11
11
|
//
|
|
12
12
|
// Generated guidance does propagate: this doc rides git-workflow.md's link closure, so every repo
|
|
13
|
-
// receives the corrected wording on its next `wp-*` command. That is the
|
|
13
|
+
// receives the corrected wording on its next `wp-*` command. That is the corollary in
|
|
14
|
+
// `.claude/rules/no-backwards-compat.md` applied
|
|
14
15
|
// to itself — name the tool, let the tool print the details.
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/src/build-log-doc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-log-doc.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/build-log-doc.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,qGAAqG;AACrG,sGAAsG;AACtG,oGAAoG;AACpG,oGAAoG;AACpG,gGAAgG;AAChG,gFAAgF;AAChF,EAAE;AACF,kGAAkG;AAClG,
|
|
1
|
+
{"version":3,"file":"build-log-doc.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/build-log-doc.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,qGAAqG;AACrG,sGAAsG;AACtG,oGAAoG;AACpG,oGAAoG;AACpG,gGAAgG;AAChG,gFAAgF;AAChF,EAAE;AACF,kGAAkG;AAClG,sFAAsF;AACtF,iDAAiD;AACjD,6DAA6D;AAC7D,8EAA8E;;;AAE9E,qGAAqG;AACxF,QAAA,aAAa,GAAG,uBAAuB,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// `webpieces.buildlog.md` — where the build's output actually is.\n//\n// WHY THIS IS A GENERATED DOC AND NOT A PARAGRAPH IN A REPO'S CLAUDE.md. The rule \"read the absolute\n// `FullLog :` path THIS run printed\" was written into webpieces-ts's own CLAUDE.md and stopped there.\n// A fleet audit then found three consumer repos still naming a bare `.webpieces/build.log` on their\n// own `origin/main` — one of them zero commits behind, so staleness was not the explanation — while\n// their real logs sat in `.webpieces/worktrees/<name>/build.log`. Hand-copied guidance does not\n// propagate; a webpieces PR cannot edit a consumer's CLAUDE.md, and should not.\n//\n// Generated guidance does propagate: this doc rides git-workflow.md's link closure, so every repo\n// receives the corrected wording on its next `wp-*` command. That is the corollary in\n// `.claude/rules/no-backwards-compat.md` applied\n// to itself — name the tool, let the tool print the details.\n// ---------------------------------------------------------------------------\n\n/** The rules-config template name, and the file name it lands under in `.webpieces/instruct-ai/`. */\nexport const BUILD_LOG_DOC = 'webpieces.buildlog.md';\n"]}
|
package/src/builds-log.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export declare class BuildsLog {
|
|
|
93
93
|
*
|
|
94
94
|
* `by` is the CALLER — `BuildGateOptions.stage`, i.e. `build` | `review` | `finish`. There is no
|
|
95
95
|
* second "caller" concept anywhere: the stage id already is one, and a second spelling of it would
|
|
96
|
-
* be the shim
|
|
96
|
+
* be the shim `.claude/rules/no-backwards-compat.md` rejects.
|
|
97
97
|
*
|
|
98
98
|
* Returns a ticket even when the append failed. A build whose START row never landed still has to be
|
|
99
99
|
* able to call `finish()`; the alternative is a nullable return that every call site must branch on
|
package/src/builds-log.js
CHANGED
|
@@ -133,7 +133,7 @@ let BuildsLog = class BuildsLog {
|
|
|
133
133
|
*
|
|
134
134
|
* `by` is the CALLER — `BuildGateOptions.stage`, i.e. `build` | `review` | `finish`. There is no
|
|
135
135
|
* second "caller" concept anywhere: the stage id already is one, and a second spelling of it would
|
|
136
|
-
* be the shim
|
|
136
|
+
* be the shim `.claude/rules/no-backwards-compat.md` rejects.
|
|
137
137
|
*
|
|
138
138
|
* Returns a ticket even when the append failed. A build whose START row never landed still has to be
|
|
139
139
|
* able to call `finish()`; the alternative is a nullable return that every call site must branch on
|
package/src/builds-log.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builds-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/builds-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as crypto from 'crypto';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { DotWebpieces } from './state-dir';\nimport { HOME_CONFIG_DIR } from './home-config';\nimport { toError } from './to-error';\n\n/**\n * `~/.webpieces/builds.log` — the MACHINE-WIDE, append-only ledger of every build this box has started.\n *\n * ─── WHY THIS ONE FILE LIVES OUTSIDE THE REPO ─────────────────────────────────────────────────────────\n * `no-machine-global-state.spec.ts` records the standing rule: webpieces writes state under\n * `{repo}/.webpieces` and nowhere else. This is the ONE carve-out, and the argument is written out in\n * `decisions/0006-the-build-ledger-is-machine-global.md`. In short:\n *\n * • The FACT is machine-scoped. \"How many builds are burning this box's CPU right now\" is not a\n * property of any repo; it is a property of the machine. A per-repo ledger cannot answer it — every\n * linked worktree has its OWN `.webpieces/`, so it would be blind to the sibling worktree it is\n * actually contending with, never mind the four other repos on the disk.\n * • It is NOT A CACHE. The retired `PrBodyStore` that the no-machine-global rule was written for was a\n * local copy of a fact GitHub owned, so it could be stale, missing, or on the wrong computer. There\n * is no remote copy of this. The file IS the fact.\n * • Its key is an ABSOLUTE LOCAL PATH, which is stable precisely because it never leaves the machine —\n * the instability that killed `PrBodyStore`'s `<host>/<owner>/<repo>` key cannot arise here.\n *\n * ─── WHY IT IS SAFE TO WRITE CONCURRENTLY ─────────────────────────────────────────────────────────────\n * Rows are deliberately kept under `MAX_ROW_BYTES` (512, macOS `PIPE_BUF`). A single `O_APPEND`\n * `write(2)` at or below that size is indivisible, so two builds appending at the same instant cannot\n * interleave halves of a line. The lock is therefore belt-and-braces for the APPEND and genuinely\n * load-bearing for ROTATION, where a rename-and-reopen really does race.\n *\n * ─── IT MAY NEVER FAIL A BUILD ────────────────────────────────────────────────────────────────────────\n * Every method here is best-effort and swallows its own errors. A build must never die because a log\n * file was busy, unwritable, or on a full disk. Lock acquisition retries and then gives up and appends\n * anyway — which the row-size invariant above makes safe.\n */\nexport const BUILDS_LOG_FILE = 'builds.log';\nexport const BUILDS_LOCK_FILE = 'builds.log.lock';\n\n/** START, and the two terminal kinds. `DONE-` is the greppable prefix that pairs with a START. */\nexport const BUILD_START = 'START';\nexport const BUILD_DONE_SUCCESS = 'DONE-SUCCESS';\nexport const BUILD_DONE_FAIL = 'DONE-FAIL';\n\n/** Rotate at 1 MB, keeping five generations (`.1` … `.5`); the old `.5` is dropped. */\nexport const MAX_BUILDS_LOG_BYTES = 1024 * 1024;\nexport const BUILDS_LOG_GENERATIONS = 5;\n\n/**\n * macOS `PIPE_BUF`. A row at or under this size is written by ONE indivisible `write(2)`, which is what\n * makes a lost lock a non-event rather than a corrupted file. Long paths are clipped to hold the line\n * under it — see `clip`.\n */\nexport const MAX_ROW_BYTES = 512;\n\n/** How long a build may hold the lock before another writer stops waiting and appends anyway. */\nconst LOCK_RETRY_MS = 50;\nconst LOCK_TIMEOUT_MS = 2000;\n\n/**\n * The handle a START row hands back, and the ONLY thing `finish()` accepts. Data-only (a class, per\n * CLAUDE.md), carrying exactly the fields the DONE row needs to pair itself with its START: the uuid,\n * the caller, the repo, and when it began (so `took=` is computed from one clock, not two).\n */\nexport class BuildTicket {\n id: string;\n by: string;\n repo: string;\n startedMs: number;\n\n constructor(id: string, by: string, repo: string, startedMs: number) {\n this.id = id;\n this.by = by;\n this.repo = repo;\n this.startedMs = startedMs;\n }\n}\n\n/**\n * One build that is STILL RUNNING — a START row with no matching `DONE-`, whose pid is still alive.\n * Data-only. This is what the refusal message renders, so it carries the three things a reader needs to\n * recognise the build in question: where it is, which tree, and how old it is.\n */\nexport class RunningBuild {\n id: string;\n by: string;\n repo: string;\n tree: string;\n cwd: string;\n branch: string;\n pid: number;\n startedMs: number;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n id: string, by: string, repo: string, tree: string,\n cwd: string, branch: string, pid: number, startedMs: number,\n ) {\n this.id = id;\n this.by = by;\n this.repo = repo;\n this.tree = tree;\n this.cwd = cwd;\n this.branch = branch;\n this.pid = pid;\n this.startedMs = startedMs;\n }\n}\n\n/**\n * The ledger. See the file docblock for why it is machine-global and why every operation swallows its\n * own errors.\n *\n * `homeDir` is a parameter on every public method, defaulted to `os.homedir()`, for exactly the reason\n * `HomeConfigService.configPath` takes one: a spec must be able to exercise the real code against a temp\n * directory and must never touch the developer's actual `~/.webpieces`.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildsLog {\n constructor(private readonly dotDir: DotWebpieces) {}\n\n /** `~/.webpieces/builds.log`. */\n logPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, BUILDS_LOG_FILE);\n }\n\n /** `~/.webpieces/builds.log.lock` — `{\"pid\":N,\"started\":<epochMs>}`. */\n lockPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, BUILDS_LOCK_FILE);\n }\n\n /** `~/.webpieces/builds.log.<n>` — generation `n`, 1 being the most recent. */\n rotatedPath(generation: number, homeDir: string = os.homedir()): string {\n return `${this.logPath(homeDir)}.${String(generation)}`;\n }\n\n /**\n * Record that a build is starting, and hand back the ticket its DONE row will need.\n *\n * `by` is the CALLER — `BuildGateOptions.stage`, i.e. `build` | `review` | `finish`. There is no\n * second \"caller\" concept anywhere: the stage id already is one, and a second spelling of it would\n * be the shim CLAUDE.md rejects.\n *\n * Returns a ticket even when the append failed. A build whose START row never landed still has to be\n * able to call `finish()`; the alternative is a nullable return that every call site must branch on\n * for a logging failure that is, by policy, not an error.\n */\n start(by: string, startDir: string, homeDir: string = os.homedir()): BuildTicket {\n const ticket = new BuildTicket(\n crypto.randomUUID(), by, this.dotDir.primaryRoot(startDir), Date.now());\n this.append(this.startRow(ticket, startDir), homeDir);\n return ticket;\n }\n\n /**\n * Record that the build behind `ticket` has ended. `exitCode` 0 writes `DONE-SUCCESS`; anything else\n * writes `DONE-FAIL` carrying the code, so `grep DONE-FAIL` lists every red build on the machine.\n */\n finish(ticket: BuildTicket, exitCode: number, homeDir: string = os.homedir()): void {\n this.append(this.doneRow(ticket, exitCode), homeDir);\n }\n\n /**\n * Every build that is still live: a `START` with no matching `DONE-` row, whose pid is still alive.\n *\n * The pid filter is not an optimisation, it is what keeps the ledger from wedging the machine. A\n * build killed with SIGKILL — an agent cancelled mid-run, a terminal closed — writes no DONE row, so\n * without the liveness test its START would count forever and the fourth build would be refused for\n * the rest of the machine's life. The uuid answers \"which build\"; the pid answers \"is it still real\".\n *\n * Only the CURRENT generation is read. A rotated-away START is by definition at least 1 MB of rows\n * old and is not a build anyone is contending with.\n */\n running(homeDir: string = os.homedir()): RunningBuild[] {\n const lines = this.readLines(this.logPath(homeDir));\n const done = new Set<string>();\n for (const line of lines) {\n if (line.startsWith(`${BUILD_DONE_SUCCESS}\\t`) || line.startsWith(`${BUILD_DONE_FAIL}\\t`)) {\n done.add(this.field(line, 'id'));\n }\n }\n const live: RunningBuild[] = [];\n for (const line of lines) {\n if (!line.startsWith(`${BUILD_START}\\t`)) continue;\n const build = this.toRunningBuild(line);\n if (build === null || done.has(build.id)) continue;\n if (!this.isAlive(build.pid)) continue;\n live.push(build);\n }\n return live;\n }\n\n /**\n * The `@webpieces` release ACTUALLY EXECUTING — found by walking UP from this module's own directory\n * to the nearest enclosing `node_modules/@webpieces/<pkg>/package.json`. `''` when this code is\n * running from source rather than from an installed package (which is the state in this repo's own\n * specs, and a perfectly ordinary answer).\n *\n * ─── WHY THIS IS NOT `WebpiecesVersions.readInstalled(root)` ──────────────────────────────────────\n * They answer DIFFERENT QUESTIONS and merging them would break the older one. `readInstalled` joins\n * `<root>/node_modules/@webpieces/...` at a FIXED tree root ON PURPOSE: its whole job is to detect\n * DRIFT between what a tree PINS and what some other tree pins, and a walk-up would silently resolve\n * a worktree with no install of its own to the primary clone's copy — hiding exactly the skew that\n * guard exists to catch. This question is the opposite one: \"whichever copy is running, name it\", and\n * for that the walk-up is the only correct answer. Do not fold them together.\n */\n executingVersion(): string {\n let dir = __dirname;\n for (let hops = 0; hops < 40; hops += 1) {\n const version = this.versionOfEnclosingPackage(dir);\n if (version !== '') return version;\n const parent = path.dirname(dir);\n if (parent === dir) return '';\n dir = parent;\n }\n return '';\n }\n\n // `<dir>` is `node_modules/@webpieces/<pkg>/...`? Then that package's version, else ''.\n private versionOfEnclosingPackage(dir: string): string {\n const parent = path.dirname(dir);\n const grandparent = path.dirname(parent);\n if (path.basename(parent) !== '@webpieces' || path.basename(grandparent) !== 'node_modules') return '';\n const text = this.readTextOrEmpty(path.join(dir, 'package.json'));\n const match = /\"version\"\\s*:\\s*\"([^\"]+)\"/.exec(text);\n return match === null ? '' : match[1];\n }\n\n // ─── ROW RENDERING ────────────────────────────────────────────────────────────────────────────────\n\n private startRow(ticket: BuildTicket, startDir: string): string {\n return [\n BUILD_START,\n `id=${ticket.id}`,\n `t=${new Date(ticket.startedMs).toISOString()}`,\n `ms=${String(ticket.startedMs)}`,\n `by=${ticket.by}`,\n `repo=${this.clip(ticket.repo)}`,\n `tree=${this.dotDir.worktreeName(startDir) || 'primary'}`,\n `cwd=${this.clip(startDir)}`,\n `branch=${this.gitBranch(startDir)}`,\n `pid=${String(process.pid)}`,\n `wp=${this.executingVersion()}`,\n ].join('\\t');\n }\n\n private doneRow(ticket: BuildTicket, exitCode: number): string {\n const now = Date.now();\n const fields = [\n exitCode === 0 ? BUILD_DONE_SUCCESS : BUILD_DONE_FAIL,\n `id=${ticket.id}`,\n `t=${new Date(now).toISOString()}`,\n `ms=${String(now)}`,\n `by=${ticket.by}`,\n `repo=${this.clip(ticket.repo)}`,\n `took=${String(now - ticket.startedMs)}`,\n ];\n if (exitCode !== 0) fields.push(`exit=${String(exitCode)}`);\n fields.push(`pid=${String(process.pid)}`);\n return fields.join('\\t');\n }\n\n /**\n * Hold a long path down to `max` characters by keeping its TAIL, which is the half that identifies\n * the tree; a clipped value is marked with a leading `…` so nobody mistakes it for a real path.\n *\n * This is what keeps a row under `MAX_ROW_BYTES` — see the file docblock. `append` re-checks the\n * assembled line as a backstop, because three clipped fields plus a long branch name can still add up.\n */\n private clip(value: string, max = 120): string {\n const oneLine = value.replace(/[\\t\\n\\r]/g, ' ');\n return oneLine.length <= max ? oneLine : `…${oneLine.slice(oneLine.length - max)}`;\n }\n\n // ─── ROW PARSING ──────────────────────────────────────────────────────────────────────────────────\n\n /** The value of `<name>=` on a TSV row, or '' when the row does not carry it. */\n private field(line: string, name: string): string {\n for (const part of line.split('\\t')) {\n if (part.startsWith(`${name}=`)) return part.slice(name.length + 1);\n }\n return '';\n }\n\n // A START row as a RunningBuild, or null when it is missing the two fields that make it usable.\n private toRunningBuild(line: string): RunningBuild | null {\n const id = this.field(line, 'id');\n const pid = Number.parseInt(this.field(line, 'pid'), 10);\n if (id === '' || !Number.isInteger(pid) || pid <= 0) return null;\n const startedMs = Number.parseInt(this.field(line, 'ms'), 10);\n return new RunningBuild(\n id, this.field(line, 'by'), this.field(line, 'repo'), this.field(line, 'tree'),\n this.field(line, 'cwd'), this.field(line, 'branch'), pid,\n Number.isInteger(startedMs) ? startedMs : 0,\n );\n }\n\n /**\n * Is `pid` still addressable? `process.kill(pid, 0)` sends no signal — it only asks the kernel. ESRCH\n * is the ONE answer that proves death; EPERM proves the opposite (it exists, it is somebody else's).\n * Pid reuse is an accepted imprecision here: being wrong in the \"still running\" direction costs\n * one extra refusal, being wrong the other way lets a fourth build start.\n *\n * WHY A PID IS MEANINGFUL HERE AND WAS NOT FOR AGENT WORKTREE LOCKS. Every row in this ledger is\n * written BY the process it names — one real OS process per build — so its pid identifies it.\n * A Claude Code subagent is not a process at all: every agent in a session records the SAME pid,\n * the session's, which is why that check was deleted rather than shared (see\n * harness-agent-activity.ts). Do not generalise this one back out to anything but a real process.\n */\n private isAlive(pid: number): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n }\n\n // ─── APPEND, LOCK, ROTATE ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Append one row, best-effort. Takes the lock so rotation cannot race, and appends ANYWAY when the\n * lock cannot be had within `LOCK_TIMEOUT_MS` — the row is under `PIPE_BUF`, so an unlocked\n * `O_APPEND` write is still indivisible, and a build must never die because a log file was busy.\n */\n private append(row: string, homeDir: string): void {\n const line = `${this.truncateToRowLimit(row)}\\n`;\n const held = this.tryAcquireLock(homeDir);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n this.ensureDir(this.logPath(homeDir));\n if (held) this.rotateIfLarge(homeDir);\n fs.appendFileSync(this.logPath(homeDir), line);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // logging may never fail a build — see the file docblock\n } finally {\n if (held) this.releaseLock(homeDir);\n }\n }\n\n // The backstop for the PIPE_BUF invariant, measured in BYTES rather than characters because a path\n // may hold multi-byte characters. Truncating a row loses fields off the end, which is strictly better\n // than a torn line that breaks every row after it.\n private truncateToRowLimit(row: string): string {\n const bytes = Buffer.from(row, 'utf8');\n // -1 for the newline `append` adds.\n if (bytes.length <= MAX_ROW_BYTES - 1) return row;\n return bytes.subarray(0, MAX_ROW_BYTES - 1).toString('utf8');\n }\n\n /**\n * Take the ledger lock, retrying every `LOCK_RETRY_MS` until `LOCK_TIMEOUT_MS`. False means \"carry on\n * without it\" — never an error, and never a reason to skip the append.\n *\n * The mechanism is `MainSyncStatusService.tryAcquireMainSyncLock`'s, proven and deliberately copied\n * rather than re-invented: an `wx` (O_CREAT|O_EXCL) create so exactly one of N racers wins, a payload\n * carrying pid + started so a dead holder is identifiable, stale reclaim gated on pid liveness, and a\n * re-read afterwards to confirm the entry on disk is OURS (a simultaneous reclaimer could have\n * unlinked ours and written its own between the two calls).\n */\n private tryAcquireLock(homeDir: string): boolean {\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n this.ensureDir(this.lockPath(homeDir));\n // Rendered rather than JSON.stringify'd off an anonymous object — two fields, both numbers, and\n // the file's whole contract is `{\"pid\":N,\"started\":M}`.\n const payload = `{\"pid\":${String(process.pid)},\"started\":${String(Date.now())}}\\n`;\n for (;;) {\n if (this.createExclusive(this.lockPath(homeDir), payload)) return true;\n if (!this.isHolderAlive(homeDir)) {\n this.unlinkQuietly(this.lockPath(homeDir));\n if (this.createExclusive(this.lockPath(homeDir), payload) && this.holderIsUs(homeDir)) return true;\n }\n if (Date.now() >= deadline) return false;\n this.sleep(LOCK_RETRY_MS);\n }\n }\n\n private releaseLock(homeDir: string): void {\n if (!this.holderIsUs(homeDir)) return;\n this.unlinkQuietly(this.lockPath(homeDir));\n }\n\n // O_CREAT|O_EXCL write: true when THIS call created the file, false when it already existed.\n private createExclusive(file: string, payload: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(file, payload, { flag: 'wx' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // The pid recorded in the lock file, or 0 when there is no readable lock.\n private lockHolderPid(homeDir: string): number {\n const text = this.readTextOrEmpty(this.lockPath(homeDir));\n if (text === '') return 0;\n const match = /\"pid\"\\s*:\\s*(\\d+)/.exec(text);\n return match === null ? 0 : Number.parseInt(match[1], 10);\n }\n\n // An unreadable or pid-less lock file counts as DEAD: it is a corpse from a crashed writer, and\n // leaving it forever would mean every future append silently skips rotation.\n private isHolderAlive(homeDir: string): boolean {\n const pid = this.lockHolderPid(homeDir);\n return pid > 0 && this.isAlive(pid);\n }\n\n private holderIsUs(homeDir: string): boolean {\n return this.lockHolderPid(homeDir) === process.pid;\n }\n\n /**\n * `.4→.5, .3→.4, … .log→.1`, dropping the old `.5`. Runs INSIDE the lock, which is the one place the\n * lock is genuinely load-bearing: a rename-and-reopen really does race, and a writer that opened the\n * old inode mid-shift would append into a file nobody reads again.\n */\n private rotateIfLarge(homeDir: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(this.logPath(homeDir))) return;\n if (fs.statSync(this.logPath(homeDir)).size < MAX_BUILDS_LOG_BYTES) return;\n this.unlinkQuietly(this.rotatedPath(BUILDS_LOG_GENERATIONS, homeDir));\n for (let gen = BUILDS_LOG_GENERATIONS - 1; gen >= 1; gen -= 1) {\n this.renameQuietly(this.rotatedPath(gen, homeDir), this.rotatedPath(gen + 1, homeDir));\n }\n this.renameQuietly(this.logPath(homeDir), this.rotatedPath(1, homeDir));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // ─── FILESYSTEM PRIMITIVES, ALL SWALLOWING ────────────────────────────────────────────────────────\n\n private ensureDir(file: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private unlinkQuietly(file: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (fs.existsSync(file)) fs.unlinkSync(file);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private renameQuietly(from: string, to: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (fs.existsSync(from)) fs.renameSync(from, to);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private readTextOrEmpty(file: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n private readLines(file: string): string[] {\n const text = this.readTextOrEmpty(file);\n if (text === '') return [];\n return text.split('\\n').filter((line: string): boolean => line.trim() !== '');\n }\n\n // A blocking sleep, because the lock retry sits on a synchronous append path that must not become\n // async — `finish()` is called from a `finally` and an async logger there could outlive the process.\n private sleep(ms: number): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // The checked-out branch in `startDir`, or '' when git cannot say. spawnSync does not throw on a\n // non-zero exit, so \"not a repo\" arrives as a status, not an exception.\n private gitBranch(startDir: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'],\n { cwd: startDir, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return '';\n return result.stdout.trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"builds-log.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/builds-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as crypto from 'crypto';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { DotWebpieces } from './state-dir';\nimport { HOME_CONFIG_DIR } from './home-config';\nimport { toError } from './to-error';\n\n/**\n * `~/.webpieces/builds.log` — the MACHINE-WIDE, append-only ledger of every build this box has started.\n *\n * ─── WHY THIS ONE FILE LIVES OUTSIDE THE REPO ─────────────────────────────────────────────────────────\n * `no-machine-global-state.spec.ts` records the standing rule: webpieces writes state under\n * `{repo}/.webpieces` and nowhere else. This is the ONE carve-out, and the argument is written out in\n * `decisions/0006-the-build-ledger-is-machine-global.md`. In short:\n *\n * • The FACT is machine-scoped. \"How many builds are burning this box's CPU right now\" is not a\n * property of any repo; it is a property of the machine. A per-repo ledger cannot answer it — every\n * linked worktree has its OWN `.webpieces/`, so it would be blind to the sibling worktree it is\n * actually contending with, never mind the four other repos on the disk.\n * • It is NOT A CACHE. The retired `PrBodyStore` that the no-machine-global rule was written for was a\n * local copy of a fact GitHub owned, so it could be stale, missing, or on the wrong computer. There\n * is no remote copy of this. The file IS the fact.\n * • Its key is an ABSOLUTE LOCAL PATH, which is stable precisely because it never leaves the machine —\n * the instability that killed `PrBodyStore`'s `<host>/<owner>/<repo>` key cannot arise here.\n *\n * ─── WHY IT IS SAFE TO WRITE CONCURRENTLY ─────────────────────────────────────────────────────────────\n * Rows are deliberately kept under `MAX_ROW_BYTES` (512, macOS `PIPE_BUF`). A single `O_APPEND`\n * `write(2)` at or below that size is indivisible, so two builds appending at the same instant cannot\n * interleave halves of a line. The lock is therefore belt-and-braces for the APPEND and genuinely\n * load-bearing for ROTATION, where a rename-and-reopen really does race.\n *\n * ─── IT MAY NEVER FAIL A BUILD ────────────────────────────────────────────────────────────────────────\n * Every method here is best-effort and swallows its own errors. A build must never die because a log\n * file was busy, unwritable, or on a full disk. Lock acquisition retries and then gives up and appends\n * anyway — which the row-size invariant above makes safe.\n */\nexport const BUILDS_LOG_FILE = 'builds.log';\nexport const BUILDS_LOCK_FILE = 'builds.log.lock';\n\n/** START, and the two terminal kinds. `DONE-` is the greppable prefix that pairs with a START. */\nexport const BUILD_START = 'START';\nexport const BUILD_DONE_SUCCESS = 'DONE-SUCCESS';\nexport const BUILD_DONE_FAIL = 'DONE-FAIL';\n\n/** Rotate at 1 MB, keeping five generations (`.1` … `.5`); the old `.5` is dropped. */\nexport const MAX_BUILDS_LOG_BYTES = 1024 * 1024;\nexport const BUILDS_LOG_GENERATIONS = 5;\n\n/**\n * macOS `PIPE_BUF`. A row at or under this size is written by ONE indivisible `write(2)`, which is what\n * makes a lost lock a non-event rather than a corrupted file. Long paths are clipped to hold the line\n * under it — see `clip`.\n */\nexport const MAX_ROW_BYTES = 512;\n\n/** How long a build may hold the lock before another writer stops waiting and appends anyway. */\nconst LOCK_RETRY_MS = 50;\nconst LOCK_TIMEOUT_MS = 2000;\n\n/**\n * The handle a START row hands back, and the ONLY thing `finish()` accepts. Data-only (a class, per\n * CLAUDE.md), carrying exactly the fields the DONE row needs to pair itself with its START: the uuid,\n * the caller, the repo, and when it began (so `took=` is computed from one clock, not two).\n */\nexport class BuildTicket {\n id: string;\n by: string;\n repo: string;\n startedMs: number;\n\n constructor(id: string, by: string, repo: string, startedMs: number) {\n this.id = id;\n this.by = by;\n this.repo = repo;\n this.startedMs = startedMs;\n }\n}\n\n/**\n * One build that is STILL RUNNING — a START row with no matching `DONE-`, whose pid is still alive.\n * Data-only. This is what the refusal message renders, so it carries the three things a reader needs to\n * recognise the build in question: where it is, which tree, and how old it is.\n */\nexport class RunningBuild {\n id: string;\n by: string;\n repo: string;\n tree: string;\n cwd: string;\n branch: string;\n pid: number;\n startedMs: number;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n id: string, by: string, repo: string, tree: string,\n cwd: string, branch: string, pid: number, startedMs: number,\n ) {\n this.id = id;\n this.by = by;\n this.repo = repo;\n this.tree = tree;\n this.cwd = cwd;\n this.branch = branch;\n this.pid = pid;\n this.startedMs = startedMs;\n }\n}\n\n/**\n * The ledger. See the file docblock for why it is machine-global and why every operation swallows its\n * own errors.\n *\n * `homeDir` is a parameter on every public method, defaulted to `os.homedir()`, for exactly the reason\n * `HomeConfigService.configPath` takes one: a spec must be able to exercise the real code against a temp\n * directory and must never touch the developer's actual `~/.webpieces`.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildsLog {\n constructor(private readonly dotDir: DotWebpieces) {}\n\n /** `~/.webpieces/builds.log`. */\n logPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, BUILDS_LOG_FILE);\n }\n\n /** `~/.webpieces/builds.log.lock` — `{\"pid\":N,\"started\":<epochMs>}`. */\n lockPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, BUILDS_LOCK_FILE);\n }\n\n /** `~/.webpieces/builds.log.<n>` — generation `n`, 1 being the most recent. */\n rotatedPath(generation: number, homeDir: string = os.homedir()): string {\n return `${this.logPath(homeDir)}.${String(generation)}`;\n }\n\n /**\n * Record that a build is starting, and hand back the ticket its DONE row will need.\n *\n * `by` is the CALLER — `BuildGateOptions.stage`, i.e. `build` | `review` | `finish`. There is no\n * second \"caller\" concept anywhere: the stage id already is one, and a second spelling of it would\n * be the shim `.claude/rules/no-backwards-compat.md` rejects.\n *\n * Returns a ticket even when the append failed. A build whose START row never landed still has to be\n * able to call `finish()`; the alternative is a nullable return that every call site must branch on\n * for a logging failure that is, by policy, not an error.\n */\n start(by: string, startDir: string, homeDir: string = os.homedir()): BuildTicket {\n const ticket = new BuildTicket(\n crypto.randomUUID(), by, this.dotDir.primaryRoot(startDir), Date.now());\n this.append(this.startRow(ticket, startDir), homeDir);\n return ticket;\n }\n\n /**\n * Record that the build behind `ticket` has ended. `exitCode` 0 writes `DONE-SUCCESS`; anything else\n * writes `DONE-FAIL` carrying the code, so `grep DONE-FAIL` lists every red build on the machine.\n */\n finish(ticket: BuildTicket, exitCode: number, homeDir: string = os.homedir()): void {\n this.append(this.doneRow(ticket, exitCode), homeDir);\n }\n\n /**\n * Every build that is still live: a `START` with no matching `DONE-` row, whose pid is still alive.\n *\n * The pid filter is not an optimisation, it is what keeps the ledger from wedging the machine. A\n * build killed with SIGKILL — an agent cancelled mid-run, a terminal closed — writes no DONE row, so\n * without the liveness test its START would count forever and the fourth build would be refused for\n * the rest of the machine's life. The uuid answers \"which build\"; the pid answers \"is it still real\".\n *\n * Only the CURRENT generation is read. A rotated-away START is by definition at least 1 MB of rows\n * old and is not a build anyone is contending with.\n */\n running(homeDir: string = os.homedir()): RunningBuild[] {\n const lines = this.readLines(this.logPath(homeDir));\n const done = new Set<string>();\n for (const line of lines) {\n if (line.startsWith(`${BUILD_DONE_SUCCESS}\\t`) || line.startsWith(`${BUILD_DONE_FAIL}\\t`)) {\n done.add(this.field(line, 'id'));\n }\n }\n const live: RunningBuild[] = [];\n for (const line of lines) {\n if (!line.startsWith(`${BUILD_START}\\t`)) continue;\n const build = this.toRunningBuild(line);\n if (build === null || done.has(build.id)) continue;\n if (!this.isAlive(build.pid)) continue;\n live.push(build);\n }\n return live;\n }\n\n /**\n * The `@webpieces` release ACTUALLY EXECUTING — found by walking UP from this module's own directory\n * to the nearest enclosing `node_modules/@webpieces/<pkg>/package.json`. `''` when this code is\n * running from source rather than from an installed package (which is the state in this repo's own\n * specs, and a perfectly ordinary answer).\n *\n * ─── WHY THIS IS NOT `WebpiecesVersions.readInstalled(root)` ──────────────────────────────────────\n * They answer DIFFERENT QUESTIONS and merging them would break the older one. `readInstalled` joins\n * `<root>/node_modules/@webpieces/...` at a FIXED tree root ON PURPOSE: its whole job is to detect\n * DRIFT between what a tree PINS and what some other tree pins, and a walk-up would silently resolve\n * a worktree with no install of its own to the primary clone's copy — hiding exactly the skew that\n * guard exists to catch. This question is the opposite one: \"whichever copy is running, name it\", and\n * for that the walk-up is the only correct answer. Do not fold them together.\n */\n executingVersion(): string {\n let dir = __dirname;\n for (let hops = 0; hops < 40; hops += 1) {\n const version = this.versionOfEnclosingPackage(dir);\n if (version !== '') return version;\n const parent = path.dirname(dir);\n if (parent === dir) return '';\n dir = parent;\n }\n return '';\n }\n\n // `<dir>` is `node_modules/@webpieces/<pkg>/...`? Then that package's version, else ''.\n private versionOfEnclosingPackage(dir: string): string {\n const parent = path.dirname(dir);\n const grandparent = path.dirname(parent);\n if (path.basename(parent) !== '@webpieces' || path.basename(grandparent) !== 'node_modules') return '';\n const text = this.readTextOrEmpty(path.join(dir, 'package.json'));\n const match = /\"version\"\\s*:\\s*\"([^\"]+)\"/.exec(text);\n return match === null ? '' : match[1];\n }\n\n // ─── ROW RENDERING ────────────────────────────────────────────────────────────────────────────────\n\n private startRow(ticket: BuildTicket, startDir: string): string {\n return [\n BUILD_START,\n `id=${ticket.id}`,\n `t=${new Date(ticket.startedMs).toISOString()}`,\n `ms=${String(ticket.startedMs)}`,\n `by=${ticket.by}`,\n `repo=${this.clip(ticket.repo)}`,\n `tree=${this.dotDir.worktreeName(startDir) || 'primary'}`,\n `cwd=${this.clip(startDir)}`,\n `branch=${this.gitBranch(startDir)}`,\n `pid=${String(process.pid)}`,\n `wp=${this.executingVersion()}`,\n ].join('\\t');\n }\n\n private doneRow(ticket: BuildTicket, exitCode: number): string {\n const now = Date.now();\n const fields = [\n exitCode === 0 ? BUILD_DONE_SUCCESS : BUILD_DONE_FAIL,\n `id=${ticket.id}`,\n `t=${new Date(now).toISOString()}`,\n `ms=${String(now)}`,\n `by=${ticket.by}`,\n `repo=${this.clip(ticket.repo)}`,\n `took=${String(now - ticket.startedMs)}`,\n ];\n if (exitCode !== 0) fields.push(`exit=${String(exitCode)}`);\n fields.push(`pid=${String(process.pid)}`);\n return fields.join('\\t');\n }\n\n /**\n * Hold a long path down to `max` characters by keeping its TAIL, which is the half that identifies\n * the tree; a clipped value is marked with a leading `…` so nobody mistakes it for a real path.\n *\n * This is what keeps a row under `MAX_ROW_BYTES` — see the file docblock. `append` re-checks the\n * assembled line as a backstop, because three clipped fields plus a long branch name can still add up.\n */\n private clip(value: string, max = 120): string {\n const oneLine = value.replace(/[\\t\\n\\r]/g, ' ');\n return oneLine.length <= max ? oneLine : `…${oneLine.slice(oneLine.length - max)}`;\n }\n\n // ─── ROW PARSING ──────────────────────────────────────────────────────────────────────────────────\n\n /** The value of `<name>=` on a TSV row, or '' when the row does not carry it. */\n private field(line: string, name: string): string {\n for (const part of line.split('\\t')) {\n if (part.startsWith(`${name}=`)) return part.slice(name.length + 1);\n }\n return '';\n }\n\n // A START row as a RunningBuild, or null when it is missing the two fields that make it usable.\n private toRunningBuild(line: string): RunningBuild | null {\n const id = this.field(line, 'id');\n const pid = Number.parseInt(this.field(line, 'pid'), 10);\n if (id === '' || !Number.isInteger(pid) || pid <= 0) return null;\n const startedMs = Number.parseInt(this.field(line, 'ms'), 10);\n return new RunningBuild(\n id, this.field(line, 'by'), this.field(line, 'repo'), this.field(line, 'tree'),\n this.field(line, 'cwd'), this.field(line, 'branch'), pid,\n Number.isInteger(startedMs) ? startedMs : 0,\n );\n }\n\n /**\n * Is `pid` still addressable? `process.kill(pid, 0)` sends no signal — it only asks the kernel. ESRCH\n * is the ONE answer that proves death; EPERM proves the opposite (it exists, it is somebody else's).\n * Pid reuse is an accepted imprecision here: being wrong in the \"still running\" direction costs\n * one extra refusal, being wrong the other way lets a fourth build start.\n *\n * WHY A PID IS MEANINGFUL HERE AND WAS NOT FOR AGENT WORKTREE LOCKS. Every row in this ledger is\n * written BY the process it names — one real OS process per build — so its pid identifies it.\n * A Claude Code subagent is not a process at all: every agent in a session records the SAME pid,\n * the session's, which is why that check was deleted rather than shared (see\n * harness-agent-activity.ts). Do not generalise this one back out to anything but a real process.\n */\n private isAlive(pid: number): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n }\n\n // ─── APPEND, LOCK, ROTATE ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Append one row, best-effort. Takes the lock so rotation cannot race, and appends ANYWAY when the\n * lock cannot be had within `LOCK_TIMEOUT_MS` — the row is under `PIPE_BUF`, so an unlocked\n * `O_APPEND` write is still indivisible, and a build must never die because a log file was busy.\n */\n private append(row: string, homeDir: string): void {\n const line = `${this.truncateToRowLimit(row)}\\n`;\n const held = this.tryAcquireLock(homeDir);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n this.ensureDir(this.logPath(homeDir));\n if (held) this.rotateIfLarge(homeDir);\n fs.appendFileSync(this.logPath(homeDir), line);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // logging may never fail a build — see the file docblock\n } finally {\n if (held) this.releaseLock(homeDir);\n }\n }\n\n // The backstop for the PIPE_BUF invariant, measured in BYTES rather than characters because a path\n // may hold multi-byte characters. Truncating a row loses fields off the end, which is strictly better\n // than a torn line that breaks every row after it.\n private truncateToRowLimit(row: string): string {\n const bytes = Buffer.from(row, 'utf8');\n // -1 for the newline `append` adds.\n if (bytes.length <= MAX_ROW_BYTES - 1) return row;\n return bytes.subarray(0, MAX_ROW_BYTES - 1).toString('utf8');\n }\n\n /**\n * Take the ledger lock, retrying every `LOCK_RETRY_MS` until `LOCK_TIMEOUT_MS`. False means \"carry on\n * without it\" — never an error, and never a reason to skip the append.\n *\n * The mechanism is `MainSyncStatusService.tryAcquireMainSyncLock`'s, proven and deliberately copied\n * rather than re-invented: an `wx` (O_CREAT|O_EXCL) create so exactly one of N racers wins, a payload\n * carrying pid + started so a dead holder is identifiable, stale reclaim gated on pid liveness, and a\n * re-read afterwards to confirm the entry on disk is OURS (a simultaneous reclaimer could have\n * unlinked ours and written its own between the two calls).\n */\n private tryAcquireLock(homeDir: string): boolean {\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n this.ensureDir(this.lockPath(homeDir));\n // Rendered rather than JSON.stringify'd off an anonymous object — two fields, both numbers, and\n // the file's whole contract is `{\"pid\":N,\"started\":M}`.\n const payload = `{\"pid\":${String(process.pid)},\"started\":${String(Date.now())}}\\n`;\n for (;;) {\n if (this.createExclusive(this.lockPath(homeDir), payload)) return true;\n if (!this.isHolderAlive(homeDir)) {\n this.unlinkQuietly(this.lockPath(homeDir));\n if (this.createExclusive(this.lockPath(homeDir), payload) && this.holderIsUs(homeDir)) return true;\n }\n if (Date.now() >= deadline) return false;\n this.sleep(LOCK_RETRY_MS);\n }\n }\n\n private releaseLock(homeDir: string): void {\n if (!this.holderIsUs(homeDir)) return;\n this.unlinkQuietly(this.lockPath(homeDir));\n }\n\n // O_CREAT|O_EXCL write: true when THIS call created the file, false when it already existed.\n private createExclusive(file: string, payload: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(file, payload, { flag: 'wx' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n // The pid recorded in the lock file, or 0 when there is no readable lock.\n private lockHolderPid(homeDir: string): number {\n const text = this.readTextOrEmpty(this.lockPath(homeDir));\n if (text === '') return 0;\n const match = /\"pid\"\\s*:\\s*(\\d+)/.exec(text);\n return match === null ? 0 : Number.parseInt(match[1], 10);\n }\n\n // An unreadable or pid-less lock file counts as DEAD: it is a corpse from a crashed writer, and\n // leaving it forever would mean every future append silently skips rotation.\n private isHolderAlive(homeDir: string): boolean {\n const pid = this.lockHolderPid(homeDir);\n return pid > 0 && this.isAlive(pid);\n }\n\n private holderIsUs(homeDir: string): boolean {\n return this.lockHolderPid(homeDir) === process.pid;\n }\n\n /**\n * `.4→.5, .3→.4, … .log→.1`, dropping the old `.5`. Runs INSIDE the lock, which is the one place the\n * lock is genuinely load-bearing: a rename-and-reopen really does race, and a writer that opened the\n * old inode mid-shift would append into a file nobody reads again.\n */\n private rotateIfLarge(homeDir: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(this.logPath(homeDir))) return;\n if (fs.statSync(this.logPath(homeDir)).size < MAX_BUILDS_LOG_BYTES) return;\n this.unlinkQuietly(this.rotatedPath(BUILDS_LOG_GENERATIONS, homeDir));\n for (let gen = BUILDS_LOG_GENERATIONS - 1; gen >= 1; gen -= 1) {\n this.renameQuietly(this.rotatedPath(gen, homeDir), this.rotatedPath(gen + 1, homeDir));\n }\n this.renameQuietly(this.logPath(homeDir), this.rotatedPath(1, homeDir));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // ─── FILESYSTEM PRIMITIVES, ALL SWALLOWING ────────────────────────────────────────────────────────\n\n private ensureDir(file: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private unlinkQuietly(file: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (fs.existsSync(file)) fs.unlinkSync(file);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private renameQuietly(from: string, to: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (fs.existsSync(from)) fs.renameSync(from, to);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n private readTextOrEmpty(file: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n private readLines(file: string): string[] {\n const text = this.readTextOrEmpty(file);\n if (text === '') return [];\n return text.split('\\n').filter((line: string): boolean => line.trim() !== '');\n }\n\n // A blocking sleep, because the lock retry sits on a synchronous append path that must not become\n // async — `finish()` is called from a `finally` and an async logger there could outlive the process.\n private sleep(ms: number): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n // The checked-out branch in `startDir`, or '' when git cannot say. spawnSync does not throw on a\n // non-zero exit, so \"not a repo\" arrives as a status, not an exception.\n private gitBranch(startDir: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'],\n { cwd: startDir, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return '';\n return result.stdout.trim();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n}\n"]}
|
package/src/cli-args.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare class CliFlag {
|
|
|
22
22
|
*
|
|
23
23
|
* `flags` defaults to [] — the no-argument case stays a two-arg construction, which is what MOST `wp-*`
|
|
24
24
|
* bins are. (Deliberately not a count: the last one written down went stale the next time a bin grew a
|
|
25
|
-
* flag, which is exactly the drift the corollary in
|
|
25
|
+
* flag, which is exactly the drift the corollary in `.claude/rules/no-backwards-compat.md` is about.)
|
|
26
26
|
*
|
|
27
27
|
* A flag a command does not DECLARE here is still rejected with exit 2: that guard is
|
|
28
28
|
* the reason this class exists (`wp-start-upsert-pr --help` once launched a squash-merge), and making it
|
package/src/cli-args.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.CliFlag = CliFlag;
|
|
|
33
33
|
*
|
|
34
34
|
* `flags` defaults to [] — the no-argument case stays a two-arg construction, which is what MOST `wp-*`
|
|
35
35
|
* bins are. (Deliberately not a count: the last one written down went stale the next time a bin grew a
|
|
36
|
-
* flag, which is exactly the drift the corollary in
|
|
36
|
+
* flag, which is exactly the drift the corollary in `.claude/rules/no-backwards-compat.md` is about.)
|
|
37
37
|
*
|
|
38
38
|
* A flag a command does not DECLARE here is still rejected with exit 2: that guard is
|
|
39
39
|
* the reason this class exists (`wp-start-upsert-pr --help` once launched a squash-merge), and making it
|
package/src/cli-args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/cli-args.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from './cli-exit-error';\n\n/**\n * One optional `--flag` a command accepts. Data-only. The description is printed in `--help`, so it is\n * written for the reader who has to DECIDE whether to pass it, not as a restatement of the name.\n */\nexport class CliFlag {\n name: string; // including the leading dashes, e.g. '--no-optional'\n description: string;\n /**\n * The flag MAY carry a value: `--resolve dean/ONE-2275` or `--resolve=dean/ONE-2275`.\n *\n * \"May\", not \"must\". The one flag that needs this (`wp-push-dev --resolve`) is meaningful both bare\n * (queue every other copy) and with an argument (queue just that one), and a `valueRequired` variant\n * would be a second concept for a case nothing has. A following token is consumed as the value only\n * when it does not itself start with `-`, so `--resolve --force` still reads as two flags.\n */\n takesValue: boolean;\n\n constructor(name: string, description: string, takesValue = false) {\n this.name = name;\n this.description = description;\n this.takesValue = takesValue;\n }\n}\n\n/**\n * Usage descriptor for a `wp-*` bin. Data-only (classes-over-interfaces): a command name, its one-line\n * summary, and the flags it accepts. `CliArgs.classify` turns it into the `--help` / unknown-arg message.\n *\n * `flags` defaults to [] — the no-argument case stays a two-arg construction, which is what MOST `wp-*`\n * bins are. (Deliberately not a count: the last one written down went stale the next time a bin grew a\n * flag, which is exactly the drift the corollary in CLAUDE.md is about.)\n *\n * A flag a command does not DECLARE here is still rejected with exit 2: that guard is\n * the reason this class exists (`wp-start-upsert-pr --help` once launched a squash-merge), and making it\n * flag-aware must not soften it.\n */\nexport class CliUsage {\n command: string;\n summary: string;\n flags: CliFlag[];\n\n constructor(command: string, summary: string, flags: CliFlag[] = []) {\n this.command = command;\n this.summary = summary;\n this.flags = flags;\n }\n}\n\n/**\n * Which declared flags argv actually carried. Data-only (a class, per CLAUDE.md), with a `has()` accessor\n * so every consumer asks the question the same way instead of open-coding `includes` against a raw array.\n */\nexport class CliArgSet {\n present: string[];\n // Values for the value-taking flags that carried one, keyed by flag name. A flag passed bare is in\n // `present` but absent here, which is exactly the distinction `--resolve` (bare) vs\n // `--resolve <branch>` needs.\n values: Map<string, string>;\n\n constructor(present: string[] = [], values: Map<string, string> = new Map<string, string>()) {\n this.present = present;\n this.values = values;\n }\n\n has(flag: string): boolean {\n return this.present.includes(flag);\n }\n\n /** The value passed with `flag`, or '' when the flag was absent or passed bare. */\n value(flag: string): string {\n return this.values.get(flag) ?? '';\n }\n}\n\n/**\n * Data-only outcome of checking argv against a no-argument command. `ok` true → run normally; else\n * `exitCode`/`message` are what the bin should exit with (help = 0, unknown arg = 2). Kept a pure\n * value so it can be asserted directly in tests without provoking a throw.\n */\nexport class CliArgsCheck {\n ok: boolean;\n exitCode: number;\n message: string;\n\n constructor(ok: boolean, exitCode: number, message: string) {\n this.ok = ok;\n this.exitCode = exitCode;\n this.message = message;\n }\n}\n\n/** One argv walk's result: which declared flags were seen, their values, and every unrecognized token. */\nclass CliScan {\n present: string[] = [];\n values: Map<string, string> = new Map<string, string>();\n unknown: string[] = [];\n}\n\n/** Argument guard for the no-argument `wp-*` bins. */\n@injectable(bindingScopeValues.Singleton)\nexport class CliArgs {\n // The help/usage block shown for `--help` and appended to an unknown-arg error. A command with no\n // declared flags says so outright, because \"takes no arguments\" is the whole usage for most of them.\n private usageText(usage: CliUsage): string {\n const head = `${usage.command} — ${usage.summary}\\n\\n`;\n if (usage.flags.length === 0) {\n return head + `Usage: pnpm ${usage.command}\\nThis command takes no arguments.`;\n }\n const label = (f: CliFlag): string => (f.takesValue ? `${f.name} [<value>]` : f.name);\n const width = Math.max(...usage.flags.map((f: CliFlag): number => label(f).length));\n const rows = usage.flags.map((f: CliFlag): string => ` ${label(f).padEnd(width)} ${f.description}`);\n return head + `Usage: pnpm ${usage.command} [flags]\\n\\nFlags:\\n${rows.join('\\n')}`;\n }\n\n /**\n * Pure argv classifier. No args → ok. `--help`/`-h` → not-ok, exit 0 with the usage block. Any token\n * the command did not DECLARE → not-ok, exit 2 naming the offending one(s). Split out from\n * `assertNoArgs`/`parse` so the decision is unit-testable without a thrown exception.\n *\n * An undeclared token is still fatal even for a command that accepts flags — a mistyped `--no-optionl`\n * must never be silently ignored and then run the flow WITH the reviews the caller meant to skip.\n */\n classify(args: string[], usage: CliUsage): CliArgsCheck {\n if (args.length === 0) return new CliArgsCheck(true, 0, '');\n if (args.includes('--help') || args.includes('-h')) {\n return new CliArgsCheck(false, 0, this.usageText(usage));\n }\n const scan = this.scan(args, usage);\n if (scan.unknown.length > 0) {\n return new CliArgsCheck(false, 2, `❌ Unknown argument(s): ${scan.unknown.join(' ')}\\n\\n` + this.usageText(usage));\n }\n return new CliArgsCheck(true, 0, '');\n }\n\n /**\n * Walk argv once, classifying every token as a declared flag, a value belonging to the\n * value-taking flag before it, or unknown. ONE walk backs both `classify` and `parse` so the set of\n * tokens the guard accepts and the set `parse` reports can never diverge — an accepted-but-unreported\n * flag would silently run the flow without the behaviour the caller asked for.\n */\n private scan(args: string[], usage: CliUsage): CliScan {\n const byName = new Map<string, CliFlag>();\n for (const flag of usage.flags) byName.set(flag.name, flag);\n const scan = new CliScan();\n for (let i = 0; i < args.length; i += 1) {\n const token = args[i];\n const eq = token.indexOf('=');\n // `--flag=value` — split before lookup so the name is what gets matched, not the whole token.\n const name = eq > 0 ? token.slice(0, eq) : token;\n const flag = byName.get(name);\n if (flag === undefined) {\n scan.unknown.push(token);\n continue;\n }\n scan.present.push(name);\n if (!flag.takesValue) {\n // `--no-optional=x` is a typo, not an accepted flag: the value would be silently dropped.\n if (eq > 0) scan.unknown.push(token);\n continue;\n }\n if (eq > 0) {\n scan.values.set(name, token.slice(eq + 1));\n continue;\n }\n // OPTIONAL value: only a following token that is not itself a flag. `--resolve --force`\n // therefore reads as two flags, not as a resolve of a branch literally named `--force`.\n const next = i + 1 < args.length ? args[i + 1] : '';\n if (next !== '' && !next.startsWith('-')) {\n scan.values.set(name, next);\n i += 1;\n }\n }\n return scan;\n }\n\n /**\n * The flag-accepting sibling of {@link assertNoArgs}: same guard, but it RETURNS which declared flags\n * were passed. Call it in exactly the same place — first thing inside `runMain`, before the app touches\n * git.\n */\n parse(usage: CliUsage): CliArgSet {\n const args = process.argv.slice(2);\n const check = this.classify(args, usage);\n if (!check.ok) throw new CliExitError(check.exitCode, check.message);\n const scan = this.scan(args, usage);\n return new CliArgSet(scan.present, scan.values);\n }\n\n /**\n * Call it as the FIRST thing inside `runMain`, BEFORE the app touches git — a bogus flag must\n * never start a mutation flow (the `wp-start-upsert-pr --help` incident: an ignored flag silently\n * launched the squash-merge and stranded the checkout on a `…PreMerge<n>` branch).\n *\n * Throws `CliExitError` (never `process.exit`) so `runMain` stays the single sanctioned exit site\n * (`no-process-exit-outside-main`): help exits 0, an unknown arg exits 2, and in both cases the\n * flow never begins.\n */\n assertNoArgs(usage: CliUsage): void {\n const check = this.classify(process.argv.slice(2), usage);\n if (check.ok) return;\n throw new CliExitError(check.exitCode, check.message);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/cli-args.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from './cli-exit-error';\n\n/**\n * One optional `--flag` a command accepts. Data-only. The description is printed in `--help`, so it is\n * written for the reader who has to DECIDE whether to pass it, not as a restatement of the name.\n */\nexport class CliFlag {\n name: string; // including the leading dashes, e.g. '--no-optional'\n description: string;\n /**\n * The flag MAY carry a value: `--resolve dean/ONE-2275` or `--resolve=dean/ONE-2275`.\n *\n * \"May\", not \"must\". The one flag that needs this (`wp-push-dev --resolve`) is meaningful both bare\n * (queue every other copy) and with an argument (queue just that one), and a `valueRequired` variant\n * would be a second concept for a case nothing has. A following token is consumed as the value only\n * when it does not itself start with `-`, so `--resolve --force` still reads as two flags.\n */\n takesValue: boolean;\n\n constructor(name: string, description: string, takesValue = false) {\n this.name = name;\n this.description = description;\n this.takesValue = takesValue;\n }\n}\n\n/**\n * Usage descriptor for a `wp-*` bin. Data-only (classes-over-interfaces): a command name, its one-line\n * summary, and the flags it accepts. `CliArgs.classify` turns it into the `--help` / unknown-arg message.\n *\n * `flags` defaults to [] — the no-argument case stays a two-arg construction, which is what MOST `wp-*`\n * bins are. (Deliberately not a count: the last one written down went stale the next time a bin grew a\n * flag, which is exactly the drift the corollary in `.claude/rules/no-backwards-compat.md` is about.)\n *\n * A flag a command does not DECLARE here is still rejected with exit 2: that guard is\n * the reason this class exists (`wp-start-upsert-pr --help` once launched a squash-merge), and making it\n * flag-aware must not soften it.\n */\nexport class CliUsage {\n command: string;\n summary: string;\n flags: CliFlag[];\n\n constructor(command: string, summary: string, flags: CliFlag[] = []) {\n this.command = command;\n this.summary = summary;\n this.flags = flags;\n }\n}\n\n/**\n * Which declared flags argv actually carried. Data-only (a class, per CLAUDE.md), with a `has()` accessor\n * so every consumer asks the question the same way instead of open-coding `includes` against a raw array.\n */\nexport class CliArgSet {\n present: string[];\n // Values for the value-taking flags that carried one, keyed by flag name. A flag passed bare is in\n // `present` but absent here, which is exactly the distinction `--resolve` (bare) vs\n // `--resolve <branch>` needs.\n values: Map<string, string>;\n\n constructor(present: string[] = [], values: Map<string, string> = new Map<string, string>()) {\n this.present = present;\n this.values = values;\n }\n\n has(flag: string): boolean {\n return this.present.includes(flag);\n }\n\n /** The value passed with `flag`, or '' when the flag was absent or passed bare. */\n value(flag: string): string {\n return this.values.get(flag) ?? '';\n }\n}\n\n/**\n * Data-only outcome of checking argv against a no-argument command. `ok` true → run normally; else\n * `exitCode`/`message` are what the bin should exit with (help = 0, unknown arg = 2). Kept a pure\n * value so it can be asserted directly in tests without provoking a throw.\n */\nexport class CliArgsCheck {\n ok: boolean;\n exitCode: number;\n message: string;\n\n constructor(ok: boolean, exitCode: number, message: string) {\n this.ok = ok;\n this.exitCode = exitCode;\n this.message = message;\n }\n}\n\n/** One argv walk's result: which declared flags were seen, their values, and every unrecognized token. */\nclass CliScan {\n present: string[] = [];\n values: Map<string, string> = new Map<string, string>();\n unknown: string[] = [];\n}\n\n/** Argument guard for the no-argument `wp-*` bins. */\n@injectable(bindingScopeValues.Singleton)\nexport class CliArgs {\n // The help/usage block shown for `--help` and appended to an unknown-arg error. A command with no\n // declared flags says so outright, because \"takes no arguments\" is the whole usage for most of them.\n private usageText(usage: CliUsage): string {\n const head = `${usage.command} — ${usage.summary}\\n\\n`;\n if (usage.flags.length === 0) {\n return head + `Usage: pnpm ${usage.command}\\nThis command takes no arguments.`;\n }\n const label = (f: CliFlag): string => (f.takesValue ? `${f.name} [<value>]` : f.name);\n const width = Math.max(...usage.flags.map((f: CliFlag): number => label(f).length));\n const rows = usage.flags.map((f: CliFlag): string => ` ${label(f).padEnd(width)} ${f.description}`);\n return head + `Usage: pnpm ${usage.command} [flags]\\n\\nFlags:\\n${rows.join('\\n')}`;\n }\n\n /**\n * Pure argv classifier. No args → ok. `--help`/`-h` → not-ok, exit 0 with the usage block. Any token\n * the command did not DECLARE → not-ok, exit 2 naming the offending one(s). Split out from\n * `assertNoArgs`/`parse` so the decision is unit-testable without a thrown exception.\n *\n * An undeclared token is still fatal even for a command that accepts flags — a mistyped `--no-optionl`\n * must never be silently ignored and then run the flow WITH the reviews the caller meant to skip.\n */\n classify(args: string[], usage: CliUsage): CliArgsCheck {\n if (args.length === 0) return new CliArgsCheck(true, 0, '');\n if (args.includes('--help') || args.includes('-h')) {\n return new CliArgsCheck(false, 0, this.usageText(usage));\n }\n const scan = this.scan(args, usage);\n if (scan.unknown.length > 0) {\n return new CliArgsCheck(false, 2, `❌ Unknown argument(s): ${scan.unknown.join(' ')}\\n\\n` + this.usageText(usage));\n }\n return new CliArgsCheck(true, 0, '');\n }\n\n /**\n * Walk argv once, classifying every token as a declared flag, a value belonging to the\n * value-taking flag before it, or unknown. ONE walk backs both `classify` and `parse` so the set of\n * tokens the guard accepts and the set `parse` reports can never diverge — an accepted-but-unreported\n * flag would silently run the flow without the behaviour the caller asked for.\n */\n private scan(args: string[], usage: CliUsage): CliScan {\n const byName = new Map<string, CliFlag>();\n for (const flag of usage.flags) byName.set(flag.name, flag);\n const scan = new CliScan();\n for (let i = 0; i < args.length; i += 1) {\n const token = args[i];\n const eq = token.indexOf('=');\n // `--flag=value` — split before lookup so the name is what gets matched, not the whole token.\n const name = eq > 0 ? token.slice(0, eq) : token;\n const flag = byName.get(name);\n if (flag === undefined) {\n scan.unknown.push(token);\n continue;\n }\n scan.present.push(name);\n if (!flag.takesValue) {\n // `--no-optional=x` is a typo, not an accepted flag: the value would be silently dropped.\n if (eq > 0) scan.unknown.push(token);\n continue;\n }\n if (eq > 0) {\n scan.values.set(name, token.slice(eq + 1));\n continue;\n }\n // OPTIONAL value: only a following token that is not itself a flag. `--resolve --force`\n // therefore reads as two flags, not as a resolve of a branch literally named `--force`.\n const next = i + 1 < args.length ? args[i + 1] : '';\n if (next !== '' && !next.startsWith('-')) {\n scan.values.set(name, next);\n i += 1;\n }\n }\n return scan;\n }\n\n /**\n * The flag-accepting sibling of {@link assertNoArgs}: same guard, but it RETURNS which declared flags\n * were passed. Call it in exactly the same place — first thing inside `runMain`, before the app touches\n * git.\n */\n parse(usage: CliUsage): CliArgSet {\n const args = process.argv.slice(2);\n const check = this.classify(args, usage);\n if (!check.ok) throw new CliExitError(check.exitCode, check.message);\n const scan = this.scan(args, usage);\n return new CliArgSet(scan.present, scan.values);\n }\n\n /**\n * Call it as the FIRST thing inside `runMain`, BEFORE the app touches git — a bogus flag must\n * never start a mutation flow (the `wp-start-upsert-pr --help` incident: an ignored flag silently\n * launched the squash-merge and stranded the checkout on a `…PreMerge<n>` branch).\n *\n * Throws `CliExitError` (never `process.exit`) so `runMain` stays the single sanctioned exit site\n * (`no-process-exit-outside-main`): help exits 0, an unknown arg exits 2, and in both cases the\n * flow never begins.\n */\n assertNoArgs(usage: CliUsage): void {\n const check = this.classify(process.argv.slice(2), usage);\n if (check.ok) return;\n throw new CliExitError(check.exitCode, check.message);\n }\n}\n"]}
|
package/src/fix-option.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* WHY it was moved down (2026-08-18): `RuleFailError.fixHints` used to be `readonly string[]` while
|
|
10
10
|
* `FixHint.fixOptions` was `readonly Option[]`. That was TWO shapes for one concept — the shim shape
|
|
11
|
-
*
|
|
11
|
+
* `.claude/rules/no-backwards-compat.md` calls "two spellings of one thing" — and the `string[]` half
|
|
12
|
+
* could not express `preferred`
|
|
12
13
|
* at all, so a build-time rule had no way to say which cure to reach for first. There is now exactly one
|
|
13
14
|
* spelling; the old one does not compile.
|
|
14
15
|
*
|
package/src/fix-option.js
CHANGED
|
@@ -12,7 +12,8 @@ exports.formatFixOptions = formatFixOptions;
|
|
|
12
12
|
*
|
|
13
13
|
* WHY it was moved down (2026-08-18): `RuleFailError.fixHints` used to be `readonly string[]` while
|
|
14
14
|
* `FixHint.fixOptions` was `readonly Option[]`. That was TWO shapes for one concept — the shim shape
|
|
15
|
-
*
|
|
15
|
+
* `.claude/rules/no-backwards-compat.md` calls "two spellings of one thing" — and the `string[]` half
|
|
16
|
+
* could not express `preferred`
|
|
16
17
|
* at all, so a build-time rule had no way to say which cure to reach for first. There is now exactly one
|
|
17
18
|
* spelling; the old one does not compile.
|
|
18
19
|
*
|
package/src/fix-option.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fix-option.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/fix-option.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"fix-option.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/fix-option.ts"],"names":[],"mappings":";;;AAsCA,4CASC;AA/CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,MAAM;IACf,yFAAyF;IAChF,IAAI,CAAS;IACtB,gFAAgF;IACvE,SAAS,CAAU;IAE5B,YAAY,IAAY,EAAE,SAAS,GAAG,KAAK;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAVD,wBAUC;AAED;;;;;GAKG;AACH,mIAAmI;AACnI,SAAgB,gBAAgB,CAAC,OAA0B,EAAE,MAAM,GAAG,IAAI;IACtE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,OAAO,CAAC,OAAO,CAAC,CAAC,GAAW,EAAE,CAAS,EAAE,EAAE;QACvC,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,cAAc,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/E,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACjB,CAAC","sourcesContent":["/**\n * `Option` is THE representation of one cure — \"here is a way to fix this\" — for BOTH rule engines.\n *\n * It lives HERE, in the lowest-level package, because both engines need it and the dependency runs one\n * way: `@webpieces/ai-hook-rules` depends on `@webpieces/rules-config` (see its package.json), never the\n * reverse. `RuleFailError` (this package) carries `readonly Option[]`, and `FixHint`\n * (`ai-hook-rules/src/core/fix-hint.ts`) carries the same `Option` — one class, one import path.\n *\n * WHY it was moved down (2026-08-18): `RuleFailError.fixHints` used to be `readonly string[]` while\n * `FixHint.fixOptions` was `readonly Option[]`. That was TWO shapes for one concept — the shim shape\n * `.claude/rules/no-backwards-compat.md` calls \"two spellings of one thing\" — and the `string[]` half\n * could not express `preferred`\n * at all, so a build-time rule had no way to say which cure to reach for first. There is now exactly one\n * spelling; the old one does not compile.\n *\n * The framework — `formatFixOptions` below, and `report.ts` in ai-hook-rules — owns the\n * \"Fix Option N:\" numbering and the \"(preferred)\" tag. Rule authors NEVER hand-write those labels, and\n * never hand-number cures inside a string literal.\n */\nexport class Option {\n /** The fix text. May be multi-line; continuation lines are indented under the option. */\n readonly text: string;\n /** When true the framework prefixes the rendered option with \"(preferred) \". */\n readonly preferred: boolean;\n\n constructor(text: string, preferred = false) {\n this.text = text;\n this.preferred = preferred;\n }\n}\n\n/**\n * The ONE renderer for a list of cures, so the numbering and the \"(preferred)\" tag have a single\n * implementation across the edit-time report, the edit-time thrown-rule path, and the build-time\n * console. `indent` is the leading whitespace for the \"Fix Option N:\" line; continuation lines of a\n * multi-line option get `indent + ' '`.\n */\n// webpieces-disable no-function-outside-class -- a pure string formatter, sibling to `atRoot`; a class around it would be ceremony\nexport function formatFixOptions(options: readonly Option[], indent = ' '): readonly string[] {\n const lines: string[] = [];\n options.forEach((opt: Option, i: number) => {\n const optLines = opt.text.split('\\n');\n const tag = opt.preferred ? '(preferred) ' : '';\n lines.push(`${indent}Fix Option ${String(i + 1)}: ${tag}${optLines[0] ?? ''}`);\n for (const l of optLines.slice(1)) lines.push(`${indent} ${l}`);\n });\n return lines;\n}\n"]}
|
package/src/home-config.d.ts
CHANGED
|
@@ -139,7 +139,8 @@ export declare const HOME_KEY_MAX_CONCURRENT_BUILDS = "maxConcurrentBuilds";
|
|
|
139
139
|
* The one NON-boolean default, and the one key whose absent value is not `GUARD_OFF_WHEN_ABSENT`.
|
|
140
140
|
*
|
|
141
141
|
* Three, because contention between agents running full sweeps at once was measured at ~3.2x total test
|
|
142
|
-
* time (
|
|
142
|
+
* time (`.claude/rules/build-verification.md` § "What actually makes builds slow"), and a fourth simultaneous build
|
|
143
|
+
* is well past the
|
|
143
144
|
* point where anybody gains anything. It is a NUMBER rather than an on/off flag because the useful
|
|
144
145
|
* machine-to-machine difference here is core count, not opinion — which is also why it is the one key in
|
|
145
146
|
* this file with a non-false default: "0 builds allowed" would be a machine that cannot build at all.
|
|
@@ -185,8 +186,9 @@ export declare const ALLOWED_TOP_LEVEL: readonly string[];
|
|
|
185
186
|
* the key from the CODE, which has the identical effect from the owner's seat. A rule protecting a
|
|
186
187
|
* FILE does not protect the SETTING it selects.
|
|
187
188
|
*
|
|
188
|
-
* If you believe an experiment should end, SAY SO and leave the flag alone.
|
|
189
|
-
* ENDS AN EXPERIMENT" carries the full rule and what a
|
|
189
|
+
* If you believe an experiment should end, SAY SO and leave the flag alone.
|
|
190
|
+
* `.claude/rules/experiments.md` §"ONLY A HUMAN ENDS AN EXPERIMENT" carries the full rule and what a
|
|
191
|
+
* human-ended retirement looks like.
|
|
190
192
|
*/
|
|
191
193
|
export declare const ALLOWED_EXPERIMENTAL_BOOLEANS: readonly string[];
|
|
192
194
|
export declare const ALLOWED_EXPERIMENTAL_NUMBERS: readonly string[];
|
package/src/home-config.js
CHANGED
|
@@ -183,7 +183,8 @@ const GUARD_OFF_WHEN_ABSENT = false;
|
|
|
183
183
|
* The one NON-boolean default, and the one key whose absent value is not `GUARD_OFF_WHEN_ABSENT`.
|
|
184
184
|
*
|
|
185
185
|
* Three, because contention between agents running full sweeps at once was measured at ~3.2x total test
|
|
186
|
-
* time (
|
|
186
|
+
* time (`.claude/rules/build-verification.md` § "What actually makes builds slow"), and a fourth simultaneous build
|
|
187
|
+
* is well past the
|
|
187
188
|
* point where anybody gains anything. It is a NUMBER rather than an on/off flag because the useful
|
|
188
189
|
* machine-to-machine difference here is core count, not opinion — which is also why it is the one key in
|
|
189
190
|
* this file with a non-false default: "0 builds allowed" would be a machine that cannot build at all.
|
|
@@ -229,8 +230,9 @@ exports.ALLOWED_TOP_LEVEL = [exports.HOME_EXPERIMENTAL_SECTION];
|
|
|
229
230
|
* the key from the CODE, which has the identical effect from the owner's seat. A rule protecting a
|
|
230
231
|
* FILE does not protect the SETTING it selects.
|
|
231
232
|
*
|
|
232
|
-
* If you believe an experiment should end, SAY SO and leave the flag alone.
|
|
233
|
-
* ENDS AN EXPERIMENT" carries the full rule and what a
|
|
233
|
+
* If you believe an experiment should end, SAY SO and leave the flag alone.
|
|
234
|
+
* `.claude/rules/experiments.md` §"ONLY A HUMAN ENDS AN EXPERIMENT" carries the full rule and what a
|
|
235
|
+
* human-ended retirement looks like.
|
|
234
236
|
*/
|
|
235
237
|
exports.ALLOWED_EXPERIMENTAL_BOOLEANS = [
|
|
236
238
|
exports.HOME_KEY_WHOLE_REPO_BUILD_GUARD, exports.HOME_KEY_ORPHAN_DIR_SWEEP,
|
|
@@ -309,8 +311,8 @@ class HomeConfig {
|
|
|
309
311
|
// ALL THREE required, no defaults. A defaulted parameter would leave `new HomeConfig(true)` compiling
|
|
310
312
|
// after this class grew a second flag, silently meaning "guard off" — an old spelling that still
|
|
311
313
|
// typechecks with a changed meaning is exactly the shim this repo does not ship. The 3-arg arity this
|
|
312
|
-
// class had before `maxConcurrentBuilds` is DELETED rather than overloaded, per
|
|
313
|
-
//
|
|
314
|
+
// class had before `maxConcurrentBuilds` is DELETED rather than overloaded, per
|
|
315
|
+
// `.claude/rules/no-backwards-compat.md`: the compile errors ARE the migration. The
|
|
314
316
|
// absent-file state is constructed in exactly one place — load()'s absent-file branch.
|
|
315
317
|
constructor(wholeRepoBuildGuard, orphanDirSweep, maxConcurrentBuilds) {
|
|
316
318
|
this.wholeRepoBuildGuard = wholeRepoBuildGuard;
|
package/src/home-config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/home-config.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\nimport { DOCUMENTATION_KEYS, HOME_KEY_DOC, HOME_KEY_AI_DOC, HomeDocKeys } from './home-config-doc-keys';\nimport {\n RetiredHomeConfigKey, RETIRED_HOME_CONFIG_KEYS, EndedExperiment, ENDED_EXPERIMENTS,\n} from './home-config-retired-keys';\n\nexport { DOCUMENTATION_KEYS, HOME_KEY_DOC, HOME_KEY_AI_DOC, HomeDocKeys };\nexport { RetiredHomeConfigKey, RETIRED_HOME_CONFIG_KEYS, EndedExperiment, ENDED_EXPERIMENTS };\n\n/**\n * `~/.webpieces/config.json` — the MACHINE-GLOBAL preference file, and a different thing from the repo's\n * tracked `webpieces.config.json`. It is EXPERIMENTAL and entirely OPTIONAL.\n *\n * ─── ABSENT IS THE NORMAL STATE, AND IT IS NEVER AN ERROR ─────────────────────────────────────────────\n * Essentially every consumer of these packages has no such file, and for them every `wp-*` command must\n * behave byte-for-byte as it did before this file was ever read: no error, no warning, no log line, no\n * extra file, nothing. `load()` therefore treats EVERY failure to READ the bytes — the file missing, the\n * `~/.webpieces` directory missing, a permission error, a path component that is not a directory — as\n * \"not opted in\", and returns all-defaults silently. That is not a fallback for a wrong shape; it is the\n * definition of \"the user did not create this file\".\n *\n * ─── THE STANDING RULE, FOR WHOEVER EDITS THIS FILE NEXT ──────────────────────────────────────────────\n * This file is MACHINE-GLOBAL: ONE document on the disk, read by EVERY repo on the machine, and those\n * repos are pinned to DIFFERENT webpieces releases. Two rules follow, and they are not negotiable:\n *\n * (a) NO KEY MAY EVER BE REQUIRED. `readOptionalBoolean` and `readOptionalPositiveInteger` are the\n * ONLY readers, and BOTH are optional-by-construction. There is no `readRequiredBoolean`, no\n * `RequiredHomeFlag`, no `REQUIRED_HOME_FLAGS` — those existed once and were deleted; do not\n * reintroduce them under any name, in any type.\n * (b) AN UNKNOWN KEY IS IGNORED, never rejected. `warnUnknownKeys` warns; nothing throws.\n *\n * Both halves are needed, and either one alone still leaves a file that some installed release rejects:\n *\n * (a) covers the OLD FILE on a NEW release — a document written before a key existed, missing it.\n * (b) covers the NEW FILE on an OLD release — a document carrying a key that release never heard of.\n *\n * Break either one and every repo on the machine that is not on the newest release hard-blocks: a\n * rejection here fails config load, and that denies every tool call in that repo until somebody upgrades\n * all of them in lockstep. That is the outage this design exists to make impossible, and it is not\n * hypothetical — it is the shape of the incident recorded at the bottom of this docblock.\n *\n * `home-config.spec.ts` pins both halves, and pins them by ENUMERATING `ALLOWED_EXPERIMENTAL` rather\n * than by a hand-written list, so a key added later is covered by the invariant automatically instead of\n * silently escaping it.\n *\n * ─── PRESENT IS STRICT ABOUT WHAT IT UNDERSTANDS, AND FORWARD-COMPATIBLE ABOUT WHAT IT DOES NOT ───────\n * Once the bytes are readable, someone DELIBERATELY created this file, and three of the four failure\n * modes are REJECTED exactly as webpieces.config.json rejects them (see `retired-config-keys.ts`), with\n * an error naming the exact fix and no `??` fallback anywhere:\n *\n * UNPARSEABLE not JSON, or not a single JSON object → REJECT\n * RETIRED KEY listed in RETIRED_HOME_CONFIG_KEYS → REJECT, carrying the migration\n * WRONG TYPE a KNOWN key holding a value of the wrong type → REJECT\n * (a non-boolean for a boolean key; anything but a positive whole number for a\n * numeric one — see `readOptionalPositiveInteger`)\n * UNKNOWN KEY a key no version of this validator has heard of → IGNORED, with a warning\n *\n * ─── WHY THE LAST ROW DIFFERS FROM webpieces.config.json, WHICH STAYS STRICT ──────────────────────────\n * The difference is not a softening of policy; it is that the two files have different CARDINALITY.\n *\n * webpieces.config.json is REPO-TRACKED. One repo, one file, one pinned @webpieces release, and the\n * file moves through git in lockstep with the code that reads it. \"One version reads this document\" is\n * true by construction, so an unknown key there can only be a typo or a dead key — and rejecting it is\n * right, because it is the delivery mechanism for the migration.\n *\n * THIS file is MACHINE-GLOBAL. ONE document, read by EVERY repo on the machine, and those repos pin\n * DIFFERENT releases — deliberately, since a repo's pin is tracked and moves when its own PR lands.\n * So \"an unknown key\" here has a second, entirely legitimate cause that cannot occur in the repo file:\n * a key a NEWER release added, being read by an OLDER one. Rejecting it means adding any key to this\n * file hard-blocks every repo on the machine that has not yet been upgraded — an outage produced by\n * opting IN to an experimental flag, which is the same shape of failure that moved these flags out of\n * webpieces.config.json in the first place (see the section below).\n *\n * This is the exact mirror of the already-settled rule that no key here may be REQUIRED. Both halves fall\n * out of one fact: the set of valid documents must be non-empty for EVERY release on the machine at once.\n * • omit a new key → an old release must not demand it (already true: every key is optional)\n * • add a new key → an old release must not reject it (this change)\n * With only the first half, the set of valid files was still empty the moment a key was added.\n *\n * ─── WHAT THAT COSTS, AND WHAT PAYS FOR IT ────────────────────────────────────────────────────────────\n * The cost is real and worth stating plainly: a TYPO now silently does nothing. `\"whole-repo-build-gaurd\"`\n * used to be a loud rejection; it is now a key nothing reads, so the flag keeps its default and nothing\n * about the machine's behaviour reveals the mistake.\n *\n * Every key here is an OPT-IN that defaults OFF, so a typo costs the author the feature they meant to\n * switch on: misspell `whole-repo-build-guard` and the guard stays inert while they believe they armed\n * it. That is the milder of the two failures — nothing they were doing stops working — but it is still\n * invisible without a signal, which is why the warning below is not optional decoration, and why\n * `nearestKnownKey` had to get fuzzier than the case-insensitive match it replaced: `gaurd` is a\n * transposition, exactly the class of typo an equality test cannot see.\n *\n * That is mitigated, not eliminated, by making the ignore VISIBLE: every unknown key is printed once per\n * load as a `[webpieces]` warning on stderr, and `nearestKnownKey` upgrades that line with a \"did you\n * mean\" whenever the key is within a two-character edit of a known one — which is what a typo is, and\n * what a key from a newer release is not. A warning naming a close match is the strongest signal\n * available that does not also block a colleague on an older pin.\n *\n * The trade was taken this way round because the two mistakes are not symmetric. A typo costs its author\n * one flag that did not turn on, discoverable the moment they check whether the feature is doing\n * anything, on their own machine. A rejection costs every repo on the machine every tool call, and the\n * person it blocks is usually not the person who edited the file.\n *\n * An unknown TOP-LEVEL key is ignored on the identical argument, and it is the more important half: a\n * future release adding a second section (`preferences`, say) beside `experimental` would otherwise be\n * unreadable by every older release on the machine, which is precisely the sequencing being deleted here.\n *\n * ─── REJECTING IS STILL SELF-RECOVERABLE, FOR THE THREE ROWS THAT STILL REJECT ────────────────────────\n * A Write/Edit targeting THIS path is an unconditional PASS in the hook guards (see `isHomeConfigPath`,\n * wired into ai-hook-rules' runner beside the webpieces.config.json pass), so an agent can always repair\n * the file the loader just rejected.\n *\n * ─── WHY A MACHINE-LOCAL SWITCH LIVES HERE AND NOT IN webpieces.config.json ───────────────────────────\n * `whole-repo-build-guard` first shipped as an ordinary validated guard: `mode: 'ON'` by default AND an\n * entry required under `hookGuards`. The consequence on upgrade was an outage — a consumer repo that had\n * not yet added the entry hit fault Y, which blocks EVERY Bash call, for a feature nobody had opted into.\n *\n * The rule that buys back: a switch that lives HERE needs no file, no key and no edit to be in its\n * default state. A repo-tracked config key cannot express that — an entry there is something every\n * consumer must add, on a schedule set by whoever bumps the release.\n *\n * Note which half of that was the outage. It was the REQUIRED KEY: the failure was at config LOAD,\n * before any command was judged. That is why every key here stays OPTIONAL and why an absent file\n * returns all-defaults silently.\n *\n * The DEFAULT is settled separately, by a standing policy this file does not get to re-litigate: EVERY\n * `experimental.*` flag ships OFF and stays OFF for two years. `whole-repo-build-guard` is one of them,\n * so it is OFF unless a machine writes `{\"experimental\": {\"whole-repo-build-guard\": true}}`. A flag that\n * defaults ON is not an experiment — it is a shipped behaviour that skipped its soak period, and it\n * changes what every agent on every machine can do the moment they upgrade. Low uptake of an opt-in\n * experiment is information ABOUT the experiment; it is not a licence to force it on everybody.\n */\nexport const HOME_CONFIG_DIR = '.webpieces';\nexport const HOME_CONFIG_FILE = 'config.json';\n\n// The `experimental` section and its keys. Named as constants because both the validator and its error\n// text must spell them identically — a validator whose message names a different key than the one it\n// checks is worse than no message.\nexport const HOME_EXPERIMENTAL_SECTION = 'experimental';\n// The on/off switch for `whole-repo-build-guard`. Spelled with the GUARD's own name, hyphens and all,\n// so `grep -rn whole-repo-build-guard` finds the switch beside the guard — and so nobody has to learn a\n// second name for one thing.\n//\n// `buildGateLogCapture` used to sit beside it and is GONE: capturing the build's output to a file is now\n// what the gate always does (see BuildAffected.runBuildGate), so the flag had nothing left to switch. It\n// is deliberately NOT in RETIRED_HOME_CONFIG_KEYS — a retired key here is a HARD FAILURE on exact match,\n// and this file is machine-global and hand-authored, so a machine that opted INTO a behaviour it now\n// gets unconditionally must not have its shell broken for saying yes early. It falls through to the\n// unknown-key WARNING instead, which says the key had no effect and names what is understood.\nexport const HOME_KEY_WHOLE_REPO_BUILD_GUARD = 'whole-repo-build-guard';\n// The on/off switch for the orphan-directory sweep `wp-sync-main` runs. Named for the thing\n// it switches, exactly as the guard key above is — one name, greppable from either end.\nexport const HOME_KEY_ORPHAN_DIR_SWEEP = 'orphan-dir-sweep';\n/**\n * How many builds may be live on this machine before `pnpm wp-build` refuses to start another. The FIRST\n * NUMERIC key in this file — see `readOptionalPositiveInteger` for why \"known key, wrong type → REJECT\"\n * applies to it exactly as it applies to the booleans.\n */\nexport const HOME_KEY_MAX_CONCURRENT_BUILDS = 'maxConcurrentBuilds';\n\n/**\n * EVERY key's value when it is not named — including on the machine with no such file at all, which is\n * essentially every machine. False, for all of them, with no exceptions and no per-key table.\n *\n * That uniformity is the policy, not a coincidence: every `experimental.*` flag ships OFF and stays OFF\n * for two years, so \"this machine never opted in\" is byte-for-byte the behaviour of having no file. ON\n * requires an explicit `true`; absent, and an explicit `false`, are the same state.\n *\n * Named rather than written as a bare `false` at each call site so the reason travels with the value —\n * and there is deliberately exactly ONE such constant, because a second one would be a second place a\n * default is stated, free to disagree with this one.\n */\nconst GUARD_OFF_WHEN_ABSENT = false;\n\n/**\n * The one NON-boolean default, and the one key whose absent value is not `GUARD_OFF_WHEN_ABSENT`.\n *\n * Three, because contention between agents running full sweeps at once was measured at ~3.2x total test\n * time (CLAUDE.md § \"What actually makes builds slow\"), and a fourth simultaneous build is well past the\n * point where anybody gains anything. It is a NUMBER rather than an on/off flag because the useful\n * machine-to-machine difference here is core count, not opinion — which is also why it is the one key in\n * this file with a non-false default: \"0 builds allowed\" would be a machine that cannot build at all.\n */\nexport const DEFAULT_MAX_CONCURRENT_BUILDS = 3;\n\n/**\n * The complete UNDERSTOOD shape. A key not on these lists is ignored with a warning rather than\n * rejected (see the class docblock: this document is machine-global and older releases must survive\n * meeting a newer release's key), so adding a key still means adding it here — a key absent from these\n * lists is never read at all, and the flag it was meant to set keeps the default above.\n *\n * EXPORTED so `home-config.spec.ts` can ENUMERATE them rather than restate them. The cross-version\n * invariant (\"every key is independently omittable\") is only as good as the list the test walks, and a\n * hand-written copy of that list means a NEW key silently escapes the invariant on the day it is added —\n * which is the one failure mode nobody would notice until an older release started rejecting files.\n * Walking the real constant makes the test cover a new key the moment it appears here.\n */\n\n/**\n * The SETTINGS only. Documentation keys are deliberately NOT here: these lists are walked to build\n * sample documents and to assert cross-version invariants, and every entry is assumed to be a setting\n * with a typed value. `warnUnknownKeys` accepts the documentation keys separately, everywhere.\n */\nexport const ALLOWED_TOP_LEVEL: readonly string[] = [HOME_EXPERIMENTAL_SECTION];\n/**\n * The understood `experimental.*` keys, SPLIT BY VALUE TYPE — because the spec walks these lists to build\n * a sample document, and a sample that wrote `false` into a numeric key would be rejected by the very\n * loader it is testing. Splitting them means a key added to either list is covered by the cross-version\n * invariants automatically, with the right sample value, which is the whole reason the lists are exported.\n *\n * `ALLOWED_EXPERIMENTAL` stays the ONE list the validator warns against — derived from the two, never\n * hand-maintained beside them, so it cannot fall out of step.\n */\n/**\n * ─── ONLY A HUMAN ENDS AN EXPERIMENT ──────────────────────────────────────────────────────────────\n *\n * An AI agent may ADD a flag to these lists. It may NEVER DELETE one, and may never make a flagged\n * behaviour unconditional — however settled it looks, however old the flag is, however good the\n * reasoning. Ending an experiment judges evidence that lives on someone else's machine.\n *\n * From a live incident: PR #711 deleted `buildGateLogCapture` and made capture unconditional. Its\n * owner's config said `true`, and after that release the opt-in silently meant nothing. Note the\n * shape — that file says \"AI: DO NOT EDIT this file!!\", and the agent never touched it; it deleted\n * the key from the CODE, which has the identical effect from the owner's seat. A rule protecting a\n * FILE does not protect the SETTING it selects.\n *\n * If you believe an experiment should end, SAY SO and leave the flag alone. CLAUDE.md §\"ONLY A HUMAN\n * ENDS AN EXPERIMENT\" carries the full rule and what a human-ended retirement looks like.\n */\nexport const ALLOWED_EXPERIMENTAL_BOOLEANS: readonly string[] = [\n HOME_KEY_WHOLE_REPO_BUILD_GUARD, HOME_KEY_ORPHAN_DIR_SWEEP,\n];\nexport const ALLOWED_EXPERIMENTAL_NUMBERS: readonly string[] = [HOME_KEY_MAX_CONCURRENT_BUILDS];\nexport const ALLOWED_EXPERIMENTAL: readonly string[] = [\n ...ALLOWED_EXPERIMENTAL_BOOLEANS, ...ALLOWED_EXPERIMENTAL_NUMBERS,\n];\n\n/**\n * ─── EVERY KEY IS OPTIONAL, AND THAT IS A HARD REQUIREMENT OF WHERE THIS FILE LIVES ───────────────────\n * This file is MACHINE-GLOBAL: one document, read by every repo on the machine, and those repos pin\n * DIFFERENT webpieces releases. A REQUIRED key cannot survive that, because it makes the set of valid\n * files EMPTY:\n *\n * • omit the new key → the NEW release rejects the file (\"REQUIRED and not set\")\n * • add the new key → every OLDER release rejects the file (\"not a known key\")\n *\n * There is no third option, and both rejections block. `whole-repo-build-guard` was required for the\n * reason recorded in #627 — a flag that decides whether a command RUNS should not be inferred — and that\n * reasoning was sound for a single version and wrong for a shared file. It is optional now, along with\n * every other key.\n *\n * Absent then reads as the ONE declared default, `GUARD_OFF_WHEN_ABSENT`: false, for every key without\n * exception, which is byte-for-byte the behaviour of having no file at all.\n *\n * The other half of cross-version safety — an OLD release IGNORING a key a NEW one added, rather than\n * rejecting it — is solved by `warnUnknownKeys` below. The two halves are one invariant: for the set of\n * valid documents to be non-empty across every release installed on the machine, neither omitting a key\n * nor adding one may be an error.\n */\n\n// Read errors that mean \"the file is not there / not reachable\" rather than \"the file is wrong\". Every\n// one of these resolves to the all-defaults config, silently. Widened deliberately past ENOENT: the\n// parent `~/.webpieces` may not exist (ENOENT), may be a file (ENOTDIR), may be unreadable (EACCES /\n// EPERM), and the path itself may be a directory (EISDIR). None of those is a user who opted in.\nconst ABSENT_ERROR_CODES: readonly string[] = ['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'EISDIR', 'ELOOP', 'ENAMETOOLONG'];\n\n/** The parsed `~/.webpieces/config.json`. Data-only (per CLAUDE.md — classes, not interfaces, for data). */\nexport class HomeConfig {\n /**\n * EXPERIMENTAL, and OFF unless this machine opts IN with an explicit `true`. When true,\n * `whole-repo-build-guard` BLOCKS a Bash command that would build the WHOLE monorepo and hands back\n * the repo's own scoped build command (`pnpm wp-build`). False — and absent, and no file at all —\n * makes the guard completely inert: no block, no log, no message.\n *\n * This is the guard's ONLY switch, and it is an OPT-IN. There is deliberately no\n * webpieces.config.json entry for it (see RETIRED_CONFIG_KEYS): a guard that every consumer must\n * ADD A KEY to avoid being blocked by is a guard that ships an outage on upgrade, which is exactly\n * what happened once. Living here means the default state needs no file, no key and no edit.\n */\n wholeRepoBuildGuard: boolean;\n\n /**\n * EXPERIMENTAL. When true, `wp-sync-main` ARCHIVES the orphan directories it finds — the\n * package directories left behind on every clone by an `nx g move`, which git cannot remove because\n * an ignored `dist/` or `node_modules/` survives the deletion of every tracked file under them.\n *\n * False — and the all-defaults value for a machine with no such file — means the sweep only REPORTS\n * what it found and moves nothing. That asymmetry is the point of shipping this behind the home\n * config at all: the author can run it live across their own clones for a release while every\n * colleague's repo is untouched, and neither state depends on a tracked key anybody must add.\n *\n * The sweep itself never deletes (see OrphanDirArchiver): directories move under `.webpieces/trash/`\n * with a printed `recover=`, so the worst case for a false positive is a `mv` somebody undoes. The\n * ARCHIVE is reaped after 30 days, which is a real deletion — of the second copy, on a timer long\n * enough that anything still wanted has been noticed.\n */\n orphanDirSweep: boolean;\n\n /**\n * How many builds may be live on this MACHINE before `pnpm wp-build` refuses to start another. Live\n * is counted from `~/.webpieces/builds.log` — see `builds-log.ts`, and\n * `decisions/0006-the-build-ledger-is-machine-global.md` for why that file lives outside any repo.\n *\n * `DEFAULT_MAX_CONCURRENT_BUILDS` when absent. The gate stages (`wp-review-upsert-pr`,\n * `wp-finish-upsert-pr`) are NEVER refused whatever this says — blocking the sanctioned path is how\n * you wedge a PR — though their builds do count toward what refuses an ad-hoc `wp-build`.\n */\n maxConcurrentBuilds: number;\n\n // ALL THREE required, no defaults. A defaulted parameter would leave `new HomeConfig(true)` compiling\n // after this class grew a second flag, silently meaning \"guard off\" — an old spelling that still\n // typechecks with a changed meaning is exactly the shim this repo does not ship. The 3-arg arity this\n // class had before `maxConcurrentBuilds` is DELETED rather than overloaded, per CLAUDE.md § \"NO\n // webpieces surface is released backwards-compatible\": the compile errors ARE the migration. The\n // absent-file state is constructed in exactly one place — load()'s absent-file branch.\n constructor(\n wholeRepoBuildGuard: boolean, orphanDirSweep: boolean, maxConcurrentBuilds: number,\n ) {\n this.wholeRepoBuildGuard = wholeRepoBuildGuard;\n this.orphanDirSweep = orphanDirSweep;\n this.maxConcurrentBuilds = maxConcurrentBuilds;\n }\n}\n\n\n/**\n * Loads and validates `~/.webpieces/config.json`, and resolves whether a path IS that file (for the\n * guard carve-out that keeps a rejection repairable).\n *\n * Strict about everything it UNDERSTANDS (a retired key, a known key of the wrong type, and a document\n * that is not JSON all throw); forward-compatible about everything it does not (an unknown key is\n * ignored with a warning). The class docblock at the top of this file has the reasoning.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class HomeConfigService {\n constructor(private readonly docKeys: HomeDocKeys = new HomeDocKeys()) {}\n\n /** Absolute path to the preference file. `homeDir` is a parameter so specs never touch a real HOME. */\n configPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, HOME_CONFIG_FILE);\n }\n\n /**\n * The preferences. Returns all-defaults, silently and without touching anything, when the file is not\n * there. THROWS InformAiError, naming the fix, when a file that IS there is wrong in a way this\n * release can be sure about — unparseable, a RETIRED key, or a KNOWN key of the wrong type. A key it\n * simply does not recognise is ignored with a warning, because it may be a newer release's key and\n * this file is shared by every repo on the machine.\n */\n load(homeDir: string = os.homedir()): HomeConfig {\n const raw = this.readIfPresent(this.configPath(homeDir));\n // THE ABSENT-FILE STATE, and the ONE place it is constructed. Every flag is off — the same value\n // a present file that does not name the key gets, so \"no file\" and \"file that ignores this key\"\n // can never disagree. Spelled out rather than defaulted in the constructor — see the note there\n // on why a defaulted parameter is a shim.\n if (raw === null) {\n return new HomeConfig(\n GUARD_OFF_WHEN_ABSENT, GUARD_OFF_WHEN_ABSENT, DEFAULT_MAX_CONCURRENT_BUILDS);\n }\n return this.validate(this.parse(raw, this.configPath(homeDir)), this.configPath(homeDir));\n }\n\n /**\n * True when `candidate` names `~/.webpieces/config.json`, in any of the forms an agent writes it:\n * an absolute path, a `~/`-prefixed path, or one still spelled `$HOME/…` / `${HOME}/…`.\n *\n * This is what the hook guards call to grant the file an unconditional Write/Edit PASS. Without it a\n * strict loader could reject the file while the guards blocked the edit that would fix it — the one\n * wedge webpieces.config.json is already immune to, and the reason its own carve-out exists.\n */\n isHomeConfigPath(candidate: string, homeDir: string = os.homedir()): boolean {\n if (candidate.trim() === '') return false;\n return path.resolve(this.expandHome(candidate.trim(), homeDir)) === path.resolve(this.configPath(homeDir));\n }\n\n // `~`, `$HOME` and `${HOME}` at the FRONT only — a home reference anywhere else is not a home path.\n private expandHome(candidate: string, homeDir: string): string {\n if (candidate === '~') return homeDir;\n for (const prefix of ['~/', '$HOME/', '${HOME}/']) {\n if (candidate.startsWith(prefix)) return path.join(homeDir, candidate.slice(prefix.length));\n }\n return candidate;\n }\n\n /**\n * The file's bytes, or null meaning \"no such file — the user did not opt in\".\n *\n * EVERY read failure is null. This is the single most important behaviour in the file: the absent\n * path is the path every consumer of these packages is on, and it may never reach an error branch.\n * An error code outside ABSENT_ERROR_CODES is genuinely exceptional (EIO, EBUSY) and is rethrown,\n * because silently disabling on a failing disk would be its own kind of lie.\n */\n private readIfPresent(file: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: this catch IS the \"you have no such file\"\n // decision, and that decision may never surface as a failure to a user who never created the file\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(file, 'utf8');\n } catch (err: unknown) {\n const error = toError(err);\n if (this.isAbsent(error)) return null;\n throw error;\n }\n }\n\n private isAbsent(error: Error): boolean {\n // webpieces-disable no-any-unknown -- node attaches `code` to fs errors without typing it on Error\n const code = (error as unknown as Record<string, unknown>)['code'];\n return typeof code === 'string' && ABSENT_ERROR_CODES.includes(code);\n }\n\n // A readable file that is not a JSON object is a WRONG file, not an absent one — hence the throw.\n // webpieces-disable no-any-unknown -- an unvalidated user-authored document; every field is narrowed below\n private parse(raw: string, file: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: JSON.parse's own message is useless to an\n // agent on its own, so it is re-thrown as the InformAiError that names the file and the fix\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed immediately below\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new InformAiError(this.error(file, 'the file must contain a single JSON OBJECT, e.g. {}.'));\n }\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n return parsed as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError) throw error;\n throw new InformAiError(this.error(file, `the file is not valid JSON (${error.message}).`), { cause: error });\n }\n }\n\n // webpieces-disable no-any-unknown -- see parse(); the document is user-authored and unvalidated\n private validate(raw: Record<string, unknown>, file: string): HomeConfig {\n this.assertNotRetired(raw, file);\n this.docKeys.assertAreStrings(raw, '', (m: string): string => this.error(file, m));\n this.warnUnknownKeys(Object.keys(raw), ALLOWED_TOP_LEVEL, '');\n const section = raw[HOME_EXPERIMENTAL_SECTION];\n if (section !== undefined && (typeof section !== 'object' || section === null || Array.isArray(section))) {\n throw new InformAiError(this.error(file, `\"${HOME_EXPERIMENTAL_SECTION}\" must be a JSON object.`));\n }\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n const experimental = (section ?? {}) as Record<string, unknown>;\n this.docKeys.assertAreStrings(experimental, `${HOME_EXPERIMENTAL_SECTION}.`,\n (m: string): string => this.error(file, m));\n this.warnUnknownKeys(Object.keys(experimental), ALLOWED_EXPERIMENTAL, `${HOME_EXPERIMENTAL_SECTION}.`);\n return new HomeConfig(\n this.readOptionalBoolean(experimental, HOME_KEY_WHOLE_REPO_BUILD_GUARD, file, GUARD_OFF_WHEN_ABSENT),\n this.readOptionalBoolean(experimental, HOME_KEY_ORPHAN_DIR_SWEEP, file, GUARD_OFF_WHEN_ABSENT),\n this.readOptionalPositiveInteger(\n experimental, HOME_KEY_MAX_CONCURRENT_BUILDS, file, DEFAULT_MAX_CONCURRENT_BUILDS),\n );\n }\n\n /**\n * The NUMERIC sibling of {@link readOptionalBoolean}, and the same three rules apply unchanged: the\n * key is OPTIONAL (absent → `whenAbsent`, stated out loud by the caller), a PRESENT value of the\n * wrong type is an ERROR, and nothing here ever guesses.\n *\n * \"Wrong type\" is stricter than `typeof value === 'number'`, because for this key the wrong NUMBERS\n * are as meaningless as the wrong types: `0` is a machine that may never build, `-1` and `2.5` are\n * not counts of anything, and `NaN` compares false against every threshold and would silently\n * disable the check. A positive integer is the only value that means something, so it is the only\n * value accepted — and the rejection names the offending value, exactly as the boolean one does.\n *\n * This does NOT soften the machine-global forward-compatibility rule: an UNKNOWN key is still\n * ignored with a warning. Only a key THIS release understands is type-checked, and no release of\n * webpieces has ever given `maxConcurrentBuilds` a non-numeric meaning.\n */\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private readOptionalPositiveInteger(\n experimental: Record<string, unknown>, key: string, file: string, whenAbsent: number,\n ): number {\n const value = experimental[key];\n if (value === undefined) return whenAbsent;\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new InformAiError(this.error(file,\n `\"${HOME_EXPERIMENTAL_SECTION}.${key}\" must be a POSITIVE WHOLE NUMBER (1 or more), not ` +\n `${JSON.stringify(value)}. Write it as a bare JSON number, e.g. ` +\n `\"${key}\": ${String(whenAbsent)} — or delete the key to use the default of ` +\n `${String(whenAbsent)}.`));\n }\n return value;\n }\n\n /**\n * An absent key falls back to `whenAbsent`, which every caller states OUT LOUD by passing\n * `GUARD_OFF_WHEN_ABSENT` — an implicit \"absent means false\" buried in this method would put the\n * default and the key that carries it in different places, free to drift apart.\n *\n * A PRESENT key of the wrong type is still an ERROR, and that is the line neither the unknown-key change\n * nor this one moved: `\"whole-repo-build-guard\": \"yes\"` is a file somebody wrote wrongly, not\n * a file written for a different release. No release of webpieces has ever given this key a string\n * meaning, so there is no forward-compatibility story to protect and nothing is gained by guessing —\n * whereas guessing would turn a typed value into a silent fallback to the default, which is the very\n * cost the unknown-key warning exists to bound.\n *\n * This is the ONLY reader; see the every-key-is-optional note above for why there is no required\n * variant.\n */\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private readOptionalBoolean(\n experimental: Record<string, unknown>, key: string, file: string, whenAbsent: boolean,\n ): boolean {\n const value = experimental[key];\n if (value === undefined) return whenAbsent;\n return this.asBoolean(value, key, file, ' Remove the quotes, or delete the key.');\n }\n\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private asBoolean(value: unknown, key: string, file: string, fix: string): boolean {\n if (typeof value !== 'boolean') {\n throw new InformAiError(this.error(file,\n `\"${HOME_EXPERIMENTAL_SECTION}.${key}\" must be the boolean ` +\n `true or false, not ${JSON.stringify(value)}.${fix}`));\n }\n return value;\n }\n\n // Retired keys are checked BEFORE unknown-key reporting: \"unknown key\" would send an agent deleting a\n // key whose value it should be MOVING, which is the whole reason the retirement table exists.\n // webpieces-disable no-any-unknown -- see parse()\n private assertNotRetired(raw: Record<string, unknown>, file: string): void {\n for (const entry of RETIRED_HOME_CONFIG_KEYS) {\n if (!this.isPresentAt(raw, entry.key)) continue;\n const destination = entry.movedTo === ''\n ? 'It was removed with no replacement.'\n : `It moved to \"${entry.movedTo}\".`;\n throw new InformAiError(this.error(file,\n `\"${entry.key}\" is RETIRED. ${destination} ${entry.instruction}`));\n }\n }\n\n // Is `dotted` (one or two segments — the file is two levels deep by construction) actually present?\n // webpieces-disable no-any-unknown -- see parse()\n private isPresentAt(raw: Record<string, unknown>, dotted: string): boolean {\n const parts = dotted.split('.');\n if (parts.length === 1) return raw[parts[0]] !== undefined;\n const section = raw[parts[0]];\n if (typeof section !== 'object' || section === null || Array.isArray(section)) return false;\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n return (section as Record<string, unknown>)[parts[1]] !== undefined;\n }\n\n /**\n * The understood keys AT ONE LEVEL, rendered from the allow-list rather than hand-listed — and\n * rendered at the level the reader's key was actually found, so a mistyped SECTION is answered with\n * the sections and a mistyped FLAG with the flags. The hand-listed version named two keys and went\n * stale the moment a third arrived, telling an agent its brand-new key was not accepted while the\n * validator right above accepted it.\n */\n private quotedKeys(allowed: readonly string[], prefix: string): string {\n return allowed.map((key: string): string => `\"${prefix}${key}\"`).join(', ');\n }\n\n /**\n * An unknown key is IGNORED — see the class docblock for why this one file cannot reject it — but it\n * is never SILENT. The warning is the entire mitigation for the cost of ignoring, so it says both\n * things a reader needs: that the key did nothing, and what the understood keys are.\n *\n * ─── WHY THIS IS NOT THE \"console side channel\" SHAPE ────────────────────────────────────────────\n * That shape is a rule or a library reporting a FAILURE — or a cure for one — by printing it instead\n * of throwing a structured value to the one top-level handler. This is the opposite case, and the\n * distinction is the entire subject of this change: an unknown key here is NOT a failure. The load\n * SUCCEEDS, a valid HomeConfig is returned, every caller proceeds normally, and there is no cure the\n * reader is obliged to apply — a key from a newer release is a CORRECT file being read by an older\n * validator. There is no throw this could be, because throwing is precisely the behaviour being\n * deleted here; and returning it would mean inventing a warnings channel through `load()` that no\n * caller has any reason to render.\n *\n * stderr, not stdout, for the usual reason: this runs inside hooks whose stdout is a JSON decision\n * and inside `wp-*` commands whose stdout is their real output, and neither may be polluted. It is\n * the same channel, with the same `[webpieces]` prefix, that `state-dir-migration.announce` already\n * uses in this package for the same category of finding — something a human may want to know about\n * and is not required to act on.\n */\n /**\n * The sentence for a flag a HUMAN ended, or '' when this key is not one.\n *\n * Consulted BEFORE the retired-near-miss hint and before the generic \"typo or newer release\" line,\n * because for these keys that generic line is actively misleading: it invites the reader to check\n * their spelling or upgrade, when the truth is that they spelled it correctly, they are on a new\n * enough release, and the thing they asked for now happens anyway.\n */\n private endedExperimentHint(dotted: string): string {\n const ended = ENDED_EXPERIMENTS.find((e: EndedExperiment): boolean => e.key === dotted);\n if (ended === undefined) return '';\n return ` That experiment ENDED in @webpieces ${ended.endedIn}. ${ended.note}`;\n }\n\n private warnUnknownKeys(found: string[], allowed: readonly string[], prefix: string): void {\n // ACCEPTED everywhere, ADVERTISED nowhere. A documentation key is not a setting, so it is\n // skipped here rather than added to the allowed lists: those lists are walked elsewhere to build\n // typed sample documents, and listing `_doc` under \"Understood here\" — or offering it as the\n // nearest match to a misspelled setting — would send someone hunting for what it configures.\n // It configures nothing.\n for (const key of found) {\n if (allowed.includes(key) || this.docKeys.isDocumentationKey(key)) continue;\n const near = this.nearestKnownKey(key, allowed);\n const guess = near !== ''\n ? ` Did you mean \"${prefix}${near}\"?`\n : this.endedExperimentHint(`${prefix}${key}`)\n || this.nearRetiredHint(`${prefix}${key}`)\n || ' If it is a typo, fix the spelling; if it is from a NEWER @webpieces than this repo pins, upgrade this repo to use it.';\n this.warn(\n `\"${prefix}${key}\" is not a key this @webpieces release understands, so it was IGNORED ` +\n `and had NO effect.${guess} Understood here: ${this.quotedKeys(allowed, prefix)}.`);\n }\n }\n\n /**\n * A near-miss of a RETIRED key, pointed at its migration — or '' when nothing retired is close.\n *\n * The gap this closes: `assertNotRetired` matches a retired key EXACTLY, so `captureBuildGateLog`\n * throws with its migration instruction while `captureBuildGateLogg` — one stray character away, and\n * a far likelier thing to type — falls through to the generic \"IGNORED, might be from a newer\n * release\" line. That is the least helpful of the three answers offered to the reader whose intent is the\n * clearest, so the retired table is consulted here too, at the same distance-2 threshold.\n *\n * It only ever produces a WARNING, never a throw: this release cannot know whether the reader meant\n * the retired key or a newer one, and guessing wrong in the throwing direction is what the whole\n * change is about. Known keys are matched first, so a typo of a LIVE key is never answered with a\n * dead one.\n */\n private nearRetiredHint(dottedKey: string): string {\n for (const entry of RETIRED_HOME_CONFIG_KEYS) {\n if (this.editDistance(dottedKey.toLowerCase(), entry.key.toLowerCase()) > 2) continue;\n const destination = entry.movedTo === ''\n ? 'it was removed with no replacement'\n : `it moved to \"${entry.movedTo}\"`;\n return ` Did you mean the RETIRED key \"${entry.key}\"? If so, ${destination}. ${entry.instruction}`;\n }\n return '';\n }\n\n // One shape for every non-fatal finding, matching state-dir-migration's `[webpieces] <what>:` prefix\n // so a reader can tell at a glance which subsystem is talking.\n private warn(message: string): void {\n process.stderr.write(`[webpieces] ~/.webpieces/config.json: ${message}\\n`);\n }\n\n /**\n * The closest understood key within two edits, or '' when nothing is close.\n *\n * This used to be a case-insensitive EQUALITY test, which was adequate while an unknown key was a\n * hard error — the error itself was the signal, and the suggestion only saved a reading. Now the\n * suggestion IS the signal, so it has to catch the typos an equality test misses: a doubled letter,\n * a dropped one, a transposition, a stray trailing character (`orphan-dir-sweeped`). Two is the\n * useful threshold — it covers every one of those and still refuses to guess for a genuinely new\n * key, which is the case that must NOT be dressed up as a typo.\n */\n private nearestKnownKey(key: string, allowed: readonly string[]): string {\n let best = '';\n let bestDistance = 3;\n for (const candidate of allowed) {\n const distance = this.editDistance(key.toLowerCase(), candidate.toLowerCase());\n if (distance >= bestDistance) continue;\n bestDistance = distance;\n best = candidate;\n }\n return best;\n }\n\n /** Ordinary Levenshtein distance, one row at a time — the key names are short and this runs once. */\n private editDistance(a: string, b: string): number {\n let previous: number[] = [];\n for (let j = 0; j <= b.length; j += 1) previous.push(j);\n for (let i = 1; i <= a.length; i += 1) {\n const current: number[] = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);\n current.push(Math.min(substitution, previous[j] + 1, current[j - 1] + 1));\n }\n previous = current;\n }\n return previous[b.length];\n }\n\n // One shape for every rejection: what is wrong, in which file, and the fact that deleting the file is\n // always a legal fix — because the file is optional, and \"no file\" is a fully supported state.\n private error(file: string, detail: string): string {\n return `[~/.webpieces/config.json] ${detail}\\n\\n` +\n `File: ${file}\\n` +\n `This machine-local preference file is OPTIONAL and EXPERIMENTAL. Editing it is always ` +\n `permitted, even while it is invalid, and deleting it outright is a valid fix — with no such ` +\n `file every webpieces command behaves exactly as it does by default.`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"home-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/home-config.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\nimport { DOCUMENTATION_KEYS, HOME_KEY_DOC, HOME_KEY_AI_DOC, HomeDocKeys } from './home-config-doc-keys';\nimport {\n RetiredHomeConfigKey, RETIRED_HOME_CONFIG_KEYS, EndedExperiment, ENDED_EXPERIMENTS,\n} from './home-config-retired-keys';\n\nexport { DOCUMENTATION_KEYS, HOME_KEY_DOC, HOME_KEY_AI_DOC, HomeDocKeys };\nexport { RetiredHomeConfigKey, RETIRED_HOME_CONFIG_KEYS, EndedExperiment, ENDED_EXPERIMENTS };\n\n/**\n * `~/.webpieces/config.json` — the MACHINE-GLOBAL preference file, and a different thing from the repo's\n * tracked `webpieces.config.json`. It is EXPERIMENTAL and entirely OPTIONAL.\n *\n * ─── ABSENT IS THE NORMAL STATE, AND IT IS NEVER AN ERROR ─────────────────────────────────────────────\n * Essentially every consumer of these packages has no such file, and for them every `wp-*` command must\n * behave byte-for-byte as it did before this file was ever read: no error, no warning, no log line, no\n * extra file, nothing. `load()` therefore treats EVERY failure to READ the bytes — the file missing, the\n * `~/.webpieces` directory missing, a permission error, a path component that is not a directory — as\n * \"not opted in\", and returns all-defaults silently. That is not a fallback for a wrong shape; it is the\n * definition of \"the user did not create this file\".\n *\n * ─── THE STANDING RULE, FOR WHOEVER EDITS THIS FILE NEXT ──────────────────────────────────────────────\n * This file is MACHINE-GLOBAL: ONE document on the disk, read by EVERY repo on the machine, and those\n * repos are pinned to DIFFERENT webpieces releases. Two rules follow, and they are not negotiable:\n *\n * (a) NO KEY MAY EVER BE REQUIRED. `readOptionalBoolean` and `readOptionalPositiveInteger` are the\n * ONLY readers, and BOTH are optional-by-construction. There is no `readRequiredBoolean`, no\n * `RequiredHomeFlag`, no `REQUIRED_HOME_FLAGS` — those existed once and were deleted; do not\n * reintroduce them under any name, in any type.\n * (b) AN UNKNOWN KEY IS IGNORED, never rejected. `warnUnknownKeys` warns; nothing throws.\n *\n * Both halves are needed, and either one alone still leaves a file that some installed release rejects:\n *\n * (a) covers the OLD FILE on a NEW release — a document written before a key existed, missing it.\n * (b) covers the NEW FILE on an OLD release — a document carrying a key that release never heard of.\n *\n * Break either one and every repo on the machine that is not on the newest release hard-blocks: a\n * rejection here fails config load, and that denies every tool call in that repo until somebody upgrades\n * all of them in lockstep. That is the outage this design exists to make impossible, and it is not\n * hypothetical — it is the shape of the incident recorded at the bottom of this docblock.\n *\n * `home-config.spec.ts` pins both halves, and pins them by ENUMERATING `ALLOWED_EXPERIMENTAL` rather\n * than by a hand-written list, so a key added later is covered by the invariant automatically instead of\n * silently escaping it.\n *\n * ─── PRESENT IS STRICT ABOUT WHAT IT UNDERSTANDS, AND FORWARD-COMPATIBLE ABOUT WHAT IT DOES NOT ───────\n * Once the bytes are readable, someone DELIBERATELY created this file, and three of the four failure\n * modes are REJECTED exactly as webpieces.config.json rejects them (see `retired-config-keys.ts`), with\n * an error naming the exact fix and no `??` fallback anywhere:\n *\n * UNPARSEABLE not JSON, or not a single JSON object → REJECT\n * RETIRED KEY listed in RETIRED_HOME_CONFIG_KEYS → REJECT, carrying the migration\n * WRONG TYPE a KNOWN key holding a value of the wrong type → REJECT\n * (a non-boolean for a boolean key; anything but a positive whole number for a\n * numeric one — see `readOptionalPositiveInteger`)\n * UNKNOWN KEY a key no version of this validator has heard of → IGNORED, with a warning\n *\n * ─── WHY THE LAST ROW DIFFERS FROM webpieces.config.json, WHICH STAYS STRICT ──────────────────────────\n * The difference is not a softening of policy; it is that the two files have different CARDINALITY.\n *\n * webpieces.config.json is REPO-TRACKED. One repo, one file, one pinned @webpieces release, and the\n * file moves through git in lockstep with the code that reads it. \"One version reads this document\" is\n * true by construction, so an unknown key there can only be a typo or a dead key — and rejecting it is\n * right, because it is the delivery mechanism for the migration.\n *\n * THIS file is MACHINE-GLOBAL. ONE document, read by EVERY repo on the machine, and those repos pin\n * DIFFERENT releases — deliberately, since a repo's pin is tracked and moves when its own PR lands.\n * So \"an unknown key\" here has a second, entirely legitimate cause that cannot occur in the repo file:\n * a key a NEWER release added, being read by an OLDER one. Rejecting it means adding any key to this\n * file hard-blocks every repo on the machine that has not yet been upgraded — an outage produced by\n * opting IN to an experimental flag, which is the same shape of failure that moved these flags out of\n * webpieces.config.json in the first place (see the section below).\n *\n * This is the exact mirror of the already-settled rule that no key here may be REQUIRED. Both halves fall\n * out of one fact: the set of valid documents must be non-empty for EVERY release on the machine at once.\n * • omit a new key → an old release must not demand it (already true: every key is optional)\n * • add a new key → an old release must not reject it (this change)\n * With only the first half, the set of valid files was still empty the moment a key was added.\n *\n * ─── WHAT THAT COSTS, AND WHAT PAYS FOR IT ────────────────────────────────────────────────────────────\n * The cost is real and worth stating plainly: a TYPO now silently does nothing. `\"whole-repo-build-gaurd\"`\n * used to be a loud rejection; it is now a key nothing reads, so the flag keeps its default and nothing\n * about the machine's behaviour reveals the mistake.\n *\n * Every key here is an OPT-IN that defaults OFF, so a typo costs the author the feature they meant to\n * switch on: misspell `whole-repo-build-guard` and the guard stays inert while they believe they armed\n * it. That is the milder of the two failures — nothing they were doing stops working — but it is still\n * invisible without a signal, which is why the warning below is not optional decoration, and why\n * `nearestKnownKey` had to get fuzzier than the case-insensitive match it replaced: `gaurd` is a\n * transposition, exactly the class of typo an equality test cannot see.\n *\n * That is mitigated, not eliminated, by making the ignore VISIBLE: every unknown key is printed once per\n * load as a `[webpieces]` warning on stderr, and `nearestKnownKey` upgrades that line with a \"did you\n * mean\" whenever the key is within a two-character edit of a known one — which is what a typo is, and\n * what a key from a newer release is not. A warning naming a close match is the strongest signal\n * available that does not also block a colleague on an older pin.\n *\n * The trade was taken this way round because the two mistakes are not symmetric. A typo costs its author\n * one flag that did not turn on, discoverable the moment they check whether the feature is doing\n * anything, on their own machine. A rejection costs every repo on the machine every tool call, and the\n * person it blocks is usually not the person who edited the file.\n *\n * An unknown TOP-LEVEL key is ignored on the identical argument, and it is the more important half: a\n * future release adding a second section (`preferences`, say) beside `experimental` would otherwise be\n * unreadable by every older release on the machine, which is precisely the sequencing being deleted here.\n *\n * ─── REJECTING IS STILL SELF-RECOVERABLE, FOR THE THREE ROWS THAT STILL REJECT ────────────────────────\n * A Write/Edit targeting THIS path is an unconditional PASS in the hook guards (see `isHomeConfigPath`,\n * wired into ai-hook-rules' runner beside the webpieces.config.json pass), so an agent can always repair\n * the file the loader just rejected.\n *\n * ─── WHY A MACHINE-LOCAL SWITCH LIVES HERE AND NOT IN webpieces.config.json ───────────────────────────\n * `whole-repo-build-guard` first shipped as an ordinary validated guard: `mode: 'ON'` by default AND an\n * entry required under `hookGuards`. The consequence on upgrade was an outage — a consumer repo that had\n * not yet added the entry hit fault Y, which blocks EVERY Bash call, for a feature nobody had opted into.\n *\n * The rule that buys back: a switch that lives HERE needs no file, no key and no edit to be in its\n * default state. A repo-tracked config key cannot express that — an entry there is something every\n * consumer must add, on a schedule set by whoever bumps the release.\n *\n * Note which half of that was the outage. It was the REQUIRED KEY: the failure was at config LOAD,\n * before any command was judged. That is why every key here stays OPTIONAL and why an absent file\n * returns all-defaults silently.\n *\n * The DEFAULT is settled separately, by a standing policy this file does not get to re-litigate: EVERY\n * `experimental.*` flag ships OFF and stays OFF for two years. `whole-repo-build-guard` is one of them,\n * so it is OFF unless a machine writes `{\"experimental\": {\"whole-repo-build-guard\": true}}`. A flag that\n * defaults ON is not an experiment — it is a shipped behaviour that skipped its soak period, and it\n * changes what every agent on every machine can do the moment they upgrade. Low uptake of an opt-in\n * experiment is information ABOUT the experiment; it is not a licence to force it on everybody.\n */\nexport const HOME_CONFIG_DIR = '.webpieces';\nexport const HOME_CONFIG_FILE = 'config.json';\n\n// The `experimental` section and its keys. Named as constants because both the validator and its error\n// text must spell them identically — a validator whose message names a different key than the one it\n// checks is worse than no message.\nexport const HOME_EXPERIMENTAL_SECTION = 'experimental';\n// The on/off switch for `whole-repo-build-guard`. Spelled with the GUARD's own name, hyphens and all,\n// so `grep -rn whole-repo-build-guard` finds the switch beside the guard — and so nobody has to learn a\n// second name for one thing.\n//\n// `buildGateLogCapture` used to sit beside it and is GONE: capturing the build's output to a file is now\n// what the gate always does (see BuildAffected.runBuildGate), so the flag had nothing left to switch. It\n// is deliberately NOT in RETIRED_HOME_CONFIG_KEYS — a retired key here is a HARD FAILURE on exact match,\n// and this file is machine-global and hand-authored, so a machine that opted INTO a behaviour it now\n// gets unconditionally must not have its shell broken for saying yes early. It falls through to the\n// unknown-key WARNING instead, which says the key had no effect and names what is understood.\nexport const HOME_KEY_WHOLE_REPO_BUILD_GUARD = 'whole-repo-build-guard';\n// The on/off switch for the orphan-directory sweep `wp-sync-main` runs. Named for the thing\n// it switches, exactly as the guard key above is — one name, greppable from either end.\nexport const HOME_KEY_ORPHAN_DIR_SWEEP = 'orphan-dir-sweep';\n/**\n * How many builds may be live on this machine before `pnpm wp-build` refuses to start another. The FIRST\n * NUMERIC key in this file — see `readOptionalPositiveInteger` for why \"known key, wrong type → REJECT\"\n * applies to it exactly as it applies to the booleans.\n */\nexport const HOME_KEY_MAX_CONCURRENT_BUILDS = 'maxConcurrentBuilds';\n\n/**\n * EVERY key's value when it is not named — including on the machine with no such file at all, which is\n * essentially every machine. False, for all of them, with no exceptions and no per-key table.\n *\n * That uniformity is the policy, not a coincidence: every `experimental.*` flag ships OFF and stays OFF\n * for two years, so \"this machine never opted in\" is byte-for-byte the behaviour of having no file. ON\n * requires an explicit `true`; absent, and an explicit `false`, are the same state.\n *\n * Named rather than written as a bare `false` at each call site so the reason travels with the value —\n * and there is deliberately exactly ONE such constant, because a second one would be a second place a\n * default is stated, free to disagree with this one.\n */\nconst GUARD_OFF_WHEN_ABSENT = false;\n\n/**\n * The one NON-boolean default, and the one key whose absent value is not `GUARD_OFF_WHEN_ABSENT`.\n *\n * Three, because contention between agents running full sweeps at once was measured at ~3.2x total test\n * time (`.claude/rules/build-verification.md` § \"What actually makes builds slow\"), and a fourth simultaneous build\n * is well past the\n * point where anybody gains anything. It is a NUMBER rather than an on/off flag because the useful\n * machine-to-machine difference here is core count, not opinion — which is also why it is the one key in\n * this file with a non-false default: \"0 builds allowed\" would be a machine that cannot build at all.\n */\nexport const DEFAULT_MAX_CONCURRENT_BUILDS = 3;\n\n/**\n * The complete UNDERSTOOD shape. A key not on these lists is ignored with a warning rather than\n * rejected (see the class docblock: this document is machine-global and older releases must survive\n * meeting a newer release's key), so adding a key still means adding it here — a key absent from these\n * lists is never read at all, and the flag it was meant to set keeps the default above.\n *\n * EXPORTED so `home-config.spec.ts` can ENUMERATE them rather than restate them. The cross-version\n * invariant (\"every key is independently omittable\") is only as good as the list the test walks, and a\n * hand-written copy of that list means a NEW key silently escapes the invariant on the day it is added —\n * which is the one failure mode nobody would notice until an older release started rejecting files.\n * Walking the real constant makes the test cover a new key the moment it appears here.\n */\n\n/**\n * The SETTINGS only. Documentation keys are deliberately NOT here: these lists are walked to build\n * sample documents and to assert cross-version invariants, and every entry is assumed to be a setting\n * with a typed value. `warnUnknownKeys` accepts the documentation keys separately, everywhere.\n */\nexport const ALLOWED_TOP_LEVEL: readonly string[] = [HOME_EXPERIMENTAL_SECTION];\n/**\n * The understood `experimental.*` keys, SPLIT BY VALUE TYPE — because the spec walks these lists to build\n * a sample document, and a sample that wrote `false` into a numeric key would be rejected by the very\n * loader it is testing. Splitting them means a key added to either list is covered by the cross-version\n * invariants automatically, with the right sample value, which is the whole reason the lists are exported.\n *\n * `ALLOWED_EXPERIMENTAL` stays the ONE list the validator warns against — derived from the two, never\n * hand-maintained beside them, so it cannot fall out of step.\n */\n/**\n * ─── ONLY A HUMAN ENDS AN EXPERIMENT ──────────────────────────────────────────────────────────────\n *\n * An AI agent may ADD a flag to these lists. It may NEVER DELETE one, and may never make a flagged\n * behaviour unconditional — however settled it looks, however old the flag is, however good the\n * reasoning. Ending an experiment judges evidence that lives on someone else's machine.\n *\n * From a live incident: PR #711 deleted `buildGateLogCapture` and made capture unconditional. Its\n * owner's config said `true`, and after that release the opt-in silently meant nothing. Note the\n * shape — that file says \"AI: DO NOT EDIT this file!!\", and the agent never touched it; it deleted\n * the key from the CODE, which has the identical effect from the owner's seat. A rule protecting a\n * FILE does not protect the SETTING it selects.\n *\n * If you believe an experiment should end, SAY SO and leave the flag alone.\n * `.claude/rules/experiments.md` §\"ONLY A HUMAN ENDS AN EXPERIMENT\" carries the full rule and what a\n * human-ended retirement looks like.\n */\nexport const ALLOWED_EXPERIMENTAL_BOOLEANS: readonly string[] = [\n HOME_KEY_WHOLE_REPO_BUILD_GUARD, HOME_KEY_ORPHAN_DIR_SWEEP,\n];\nexport const ALLOWED_EXPERIMENTAL_NUMBERS: readonly string[] = [HOME_KEY_MAX_CONCURRENT_BUILDS];\nexport const ALLOWED_EXPERIMENTAL: readonly string[] = [\n ...ALLOWED_EXPERIMENTAL_BOOLEANS, ...ALLOWED_EXPERIMENTAL_NUMBERS,\n];\n\n/**\n * ─── EVERY KEY IS OPTIONAL, AND THAT IS A HARD REQUIREMENT OF WHERE THIS FILE LIVES ───────────────────\n * This file is MACHINE-GLOBAL: one document, read by every repo on the machine, and those repos pin\n * DIFFERENT webpieces releases. A REQUIRED key cannot survive that, because it makes the set of valid\n * files EMPTY:\n *\n * • omit the new key → the NEW release rejects the file (\"REQUIRED and not set\")\n * • add the new key → every OLDER release rejects the file (\"not a known key\")\n *\n * There is no third option, and both rejections block. `whole-repo-build-guard` was required for the\n * reason recorded in #627 — a flag that decides whether a command RUNS should not be inferred — and that\n * reasoning was sound for a single version and wrong for a shared file. It is optional now, along with\n * every other key.\n *\n * Absent then reads as the ONE declared default, `GUARD_OFF_WHEN_ABSENT`: false, for every key without\n * exception, which is byte-for-byte the behaviour of having no file at all.\n *\n * The other half of cross-version safety — an OLD release IGNORING a key a NEW one added, rather than\n * rejecting it — is solved by `warnUnknownKeys` below. The two halves are one invariant: for the set of\n * valid documents to be non-empty across every release installed on the machine, neither omitting a key\n * nor adding one may be an error.\n */\n\n// Read errors that mean \"the file is not there / not reachable\" rather than \"the file is wrong\". Every\n// one of these resolves to the all-defaults config, silently. Widened deliberately past ENOENT: the\n// parent `~/.webpieces` may not exist (ENOENT), may be a file (ENOTDIR), may be unreadable (EACCES /\n// EPERM), and the path itself may be a directory (EISDIR). None of those is a user who opted in.\nconst ABSENT_ERROR_CODES: readonly string[] = ['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'EISDIR', 'ELOOP', 'ENAMETOOLONG'];\n\n/** The parsed `~/.webpieces/config.json`. Data-only (per CLAUDE.md — classes, not interfaces, for data). */\nexport class HomeConfig {\n /**\n * EXPERIMENTAL, and OFF unless this machine opts IN with an explicit `true`. When true,\n * `whole-repo-build-guard` BLOCKS a Bash command that would build the WHOLE monorepo and hands back\n * the repo's own scoped build command (`pnpm wp-build`). False — and absent, and no file at all —\n * makes the guard completely inert: no block, no log, no message.\n *\n * This is the guard's ONLY switch, and it is an OPT-IN. There is deliberately no\n * webpieces.config.json entry for it (see RETIRED_CONFIG_KEYS): a guard that every consumer must\n * ADD A KEY to avoid being blocked by is a guard that ships an outage on upgrade, which is exactly\n * what happened once. Living here means the default state needs no file, no key and no edit.\n */\n wholeRepoBuildGuard: boolean;\n\n /**\n * EXPERIMENTAL. When true, `wp-sync-main` ARCHIVES the orphan directories it finds — the\n * package directories left behind on every clone by an `nx g move`, which git cannot remove because\n * an ignored `dist/` or `node_modules/` survives the deletion of every tracked file under them.\n *\n * False — and the all-defaults value for a machine with no such file — means the sweep only REPORTS\n * what it found and moves nothing. That asymmetry is the point of shipping this behind the home\n * config at all: the author can run it live across their own clones for a release while every\n * colleague's repo is untouched, and neither state depends on a tracked key anybody must add.\n *\n * The sweep itself never deletes (see OrphanDirArchiver): directories move under `.webpieces/trash/`\n * with a printed `recover=`, so the worst case for a false positive is a `mv` somebody undoes. The\n * ARCHIVE is reaped after 30 days, which is a real deletion — of the second copy, on a timer long\n * enough that anything still wanted has been noticed.\n */\n orphanDirSweep: boolean;\n\n /**\n * How many builds may be live on this MACHINE before `pnpm wp-build` refuses to start another. Live\n * is counted from `~/.webpieces/builds.log` — see `builds-log.ts`, and\n * `decisions/0006-the-build-ledger-is-machine-global.md` for why that file lives outside any repo.\n *\n * `DEFAULT_MAX_CONCURRENT_BUILDS` when absent. The gate stages (`wp-review-upsert-pr`,\n * `wp-finish-upsert-pr`) are NEVER refused whatever this says — blocking the sanctioned path is how\n * you wedge a PR — though their builds do count toward what refuses an ad-hoc `wp-build`.\n */\n maxConcurrentBuilds: number;\n\n // ALL THREE required, no defaults. A defaulted parameter would leave `new HomeConfig(true)` compiling\n // after this class grew a second flag, silently meaning \"guard off\" — an old spelling that still\n // typechecks with a changed meaning is exactly the shim this repo does not ship. The 3-arg arity this\n // class had before `maxConcurrentBuilds` is DELETED rather than overloaded, per\n // `.claude/rules/no-backwards-compat.md`: the compile errors ARE the migration. The\n // absent-file state is constructed in exactly one place — load()'s absent-file branch.\n constructor(\n wholeRepoBuildGuard: boolean, orphanDirSweep: boolean, maxConcurrentBuilds: number,\n ) {\n this.wholeRepoBuildGuard = wholeRepoBuildGuard;\n this.orphanDirSweep = orphanDirSweep;\n this.maxConcurrentBuilds = maxConcurrentBuilds;\n }\n}\n\n\n/**\n * Loads and validates `~/.webpieces/config.json`, and resolves whether a path IS that file (for the\n * guard carve-out that keeps a rejection repairable).\n *\n * Strict about everything it UNDERSTANDS (a retired key, a known key of the wrong type, and a document\n * that is not JSON all throw); forward-compatible about everything it does not (an unknown key is\n * ignored with a warning). The class docblock at the top of this file has the reasoning.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class HomeConfigService {\n constructor(private readonly docKeys: HomeDocKeys = new HomeDocKeys()) {}\n\n /** Absolute path to the preference file. `homeDir` is a parameter so specs never touch a real HOME. */\n configPath(homeDir: string = os.homedir()): string {\n return path.join(homeDir, HOME_CONFIG_DIR, HOME_CONFIG_FILE);\n }\n\n /**\n * The preferences. Returns all-defaults, silently and without touching anything, when the file is not\n * there. THROWS InformAiError, naming the fix, when a file that IS there is wrong in a way this\n * release can be sure about — unparseable, a RETIRED key, or a KNOWN key of the wrong type. A key it\n * simply does not recognise is ignored with a warning, because it may be a newer release's key and\n * this file is shared by every repo on the machine.\n */\n load(homeDir: string = os.homedir()): HomeConfig {\n const raw = this.readIfPresent(this.configPath(homeDir));\n // THE ABSENT-FILE STATE, and the ONE place it is constructed. Every flag is off — the same value\n // a present file that does not name the key gets, so \"no file\" and \"file that ignores this key\"\n // can never disagree. Spelled out rather than defaulted in the constructor — see the note there\n // on why a defaulted parameter is a shim.\n if (raw === null) {\n return new HomeConfig(\n GUARD_OFF_WHEN_ABSENT, GUARD_OFF_WHEN_ABSENT, DEFAULT_MAX_CONCURRENT_BUILDS);\n }\n return this.validate(this.parse(raw, this.configPath(homeDir)), this.configPath(homeDir));\n }\n\n /**\n * True when `candidate` names `~/.webpieces/config.json`, in any of the forms an agent writes it:\n * an absolute path, a `~/`-prefixed path, or one still spelled `$HOME/…` / `${HOME}/…`.\n *\n * This is what the hook guards call to grant the file an unconditional Write/Edit PASS. Without it a\n * strict loader could reject the file while the guards blocked the edit that would fix it — the one\n * wedge webpieces.config.json is already immune to, and the reason its own carve-out exists.\n */\n isHomeConfigPath(candidate: string, homeDir: string = os.homedir()): boolean {\n if (candidate.trim() === '') return false;\n return path.resolve(this.expandHome(candidate.trim(), homeDir)) === path.resolve(this.configPath(homeDir));\n }\n\n // `~`, `$HOME` and `${HOME}` at the FRONT only — a home reference anywhere else is not a home path.\n private expandHome(candidate: string, homeDir: string): string {\n if (candidate === '~') return homeDir;\n for (const prefix of ['~/', '$HOME/', '${HOME}/']) {\n if (candidate.startsWith(prefix)) return path.join(homeDir, candidate.slice(prefix.length));\n }\n return candidate;\n }\n\n /**\n * The file's bytes, or null meaning \"no such file — the user did not opt in\".\n *\n * EVERY read failure is null. This is the single most important behaviour in the file: the absent\n * path is the path every consumer of these packages is on, and it may never reach an error branch.\n * An error code outside ABSENT_ERROR_CODES is genuinely exceptional (EIO, EBUSY) and is rethrown,\n * because silently disabling on a failing disk would be its own kind of lie.\n */\n private readIfPresent(file: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: this catch IS the \"you have no such file\"\n // decision, and that decision may never surface as a failure to a user who never created the file\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(file, 'utf8');\n } catch (err: unknown) {\n const error = toError(err);\n if (this.isAbsent(error)) return null;\n throw error;\n }\n }\n\n private isAbsent(error: Error): boolean {\n // webpieces-disable no-any-unknown -- node attaches `code` to fs errors without typing it on Error\n const code = (error as unknown as Record<string, unknown>)['code'];\n return typeof code === 'string' && ABSENT_ERROR_CODES.includes(code);\n }\n\n // A readable file that is not a JSON object is a WRONG file, not an absent one — hence the throw.\n // webpieces-disable no-any-unknown -- an unvalidated user-authored document; every field is narrowed below\n private parse(raw: string, file: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: JSON.parse's own message is useless to an\n // agent on its own, so it is re-thrown as the InformAiError that names the file and the fix\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed immediately below\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new InformAiError(this.error(file, 'the file must contain a single JSON OBJECT, e.g. {}.'));\n }\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n return parsed as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError) throw error;\n throw new InformAiError(this.error(file, `the file is not valid JSON (${error.message}).`), { cause: error });\n }\n }\n\n // webpieces-disable no-any-unknown -- see parse(); the document is user-authored and unvalidated\n private validate(raw: Record<string, unknown>, file: string): HomeConfig {\n this.assertNotRetired(raw, file);\n this.docKeys.assertAreStrings(raw, '', (m: string): string => this.error(file, m));\n this.warnUnknownKeys(Object.keys(raw), ALLOWED_TOP_LEVEL, '');\n const section = raw[HOME_EXPERIMENTAL_SECTION];\n if (section !== undefined && (typeof section !== 'object' || section === null || Array.isArray(section))) {\n throw new InformAiError(this.error(file, `\"${HOME_EXPERIMENTAL_SECTION}\" must be a JSON object.`));\n }\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n const experimental = (section ?? {}) as Record<string, unknown>;\n this.docKeys.assertAreStrings(experimental, `${HOME_EXPERIMENTAL_SECTION}.`,\n (m: string): string => this.error(file, m));\n this.warnUnknownKeys(Object.keys(experimental), ALLOWED_EXPERIMENTAL, `${HOME_EXPERIMENTAL_SECTION}.`);\n return new HomeConfig(\n this.readOptionalBoolean(experimental, HOME_KEY_WHOLE_REPO_BUILD_GUARD, file, GUARD_OFF_WHEN_ABSENT),\n this.readOptionalBoolean(experimental, HOME_KEY_ORPHAN_DIR_SWEEP, file, GUARD_OFF_WHEN_ABSENT),\n this.readOptionalPositiveInteger(\n experimental, HOME_KEY_MAX_CONCURRENT_BUILDS, file, DEFAULT_MAX_CONCURRENT_BUILDS),\n );\n }\n\n /**\n * The NUMERIC sibling of {@link readOptionalBoolean}, and the same three rules apply unchanged: the\n * key is OPTIONAL (absent → `whenAbsent`, stated out loud by the caller), a PRESENT value of the\n * wrong type is an ERROR, and nothing here ever guesses.\n *\n * \"Wrong type\" is stricter than `typeof value === 'number'`, because for this key the wrong NUMBERS\n * are as meaningless as the wrong types: `0` is a machine that may never build, `-1` and `2.5` are\n * not counts of anything, and `NaN` compares false against every threshold and would silently\n * disable the check. A positive integer is the only value that means something, so it is the only\n * value accepted — and the rejection names the offending value, exactly as the boolean one does.\n *\n * This does NOT soften the machine-global forward-compatibility rule: an UNKNOWN key is still\n * ignored with a warning. Only a key THIS release understands is type-checked, and no release of\n * webpieces has ever given `maxConcurrentBuilds` a non-numeric meaning.\n */\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private readOptionalPositiveInteger(\n experimental: Record<string, unknown>, key: string, file: string, whenAbsent: number,\n ): number {\n const value = experimental[key];\n if (value === undefined) return whenAbsent;\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new InformAiError(this.error(file,\n `\"${HOME_EXPERIMENTAL_SECTION}.${key}\" must be a POSITIVE WHOLE NUMBER (1 or more), not ` +\n `${JSON.stringify(value)}. Write it as a bare JSON number, e.g. ` +\n `\"${key}\": ${String(whenAbsent)} — or delete the key to use the default of ` +\n `${String(whenAbsent)}.`));\n }\n return value;\n }\n\n /**\n * An absent key falls back to `whenAbsent`, which every caller states OUT LOUD by passing\n * `GUARD_OFF_WHEN_ABSENT` — an implicit \"absent means false\" buried in this method would put the\n * default and the key that carries it in different places, free to drift apart.\n *\n * A PRESENT key of the wrong type is still an ERROR, and that is the line neither the unknown-key change\n * nor this one moved: `\"whole-repo-build-guard\": \"yes\"` is a file somebody wrote wrongly, not\n * a file written for a different release. No release of webpieces has ever given this key a string\n * meaning, so there is no forward-compatibility story to protect and nothing is gained by guessing —\n * whereas guessing would turn a typed value into a silent fallback to the default, which is the very\n * cost the unknown-key warning exists to bound.\n *\n * This is the ONLY reader; see the every-key-is-optional note above for why there is no required\n * variant.\n */\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private readOptionalBoolean(\n experimental: Record<string, unknown>, key: string, file: string, whenAbsent: boolean,\n ): boolean {\n const value = experimental[key];\n if (value === undefined) return whenAbsent;\n return this.asBoolean(value, key, file, ' Remove the quotes, or delete the key.');\n }\n\n // webpieces-disable no-any-unknown -- see parse()\n // eslint-disable-next-line @typescript-eslint/max-params\n private asBoolean(value: unknown, key: string, file: string, fix: string): boolean {\n if (typeof value !== 'boolean') {\n throw new InformAiError(this.error(file,\n `\"${HOME_EXPERIMENTAL_SECTION}.${key}\" must be the boolean ` +\n `true or false, not ${JSON.stringify(value)}.${fix}`));\n }\n return value;\n }\n\n // Retired keys are checked BEFORE unknown-key reporting: \"unknown key\" would send an agent deleting a\n // key whose value it should be MOVING, which is the whole reason the retirement table exists.\n // webpieces-disable no-any-unknown -- see parse()\n private assertNotRetired(raw: Record<string, unknown>, file: string): void {\n for (const entry of RETIRED_HOME_CONFIG_KEYS) {\n if (!this.isPresentAt(raw, entry.key)) continue;\n const destination = entry.movedTo === ''\n ? 'It was removed with no replacement.'\n : `It moved to \"${entry.movedTo}\".`;\n throw new InformAiError(this.error(file,\n `\"${entry.key}\" is RETIRED. ${destination} ${entry.instruction}`));\n }\n }\n\n // Is `dotted` (one or two segments — the file is two levels deep by construction) actually present?\n // webpieces-disable no-any-unknown -- see parse()\n private isPresentAt(raw: Record<string, unknown>, dotted: string): boolean {\n const parts = dotted.split('.');\n if (parts.length === 1) return raw[parts[0]] !== undefined;\n const section = raw[parts[0]];\n if (typeof section !== 'object' || section === null || Array.isArray(section)) return false;\n // webpieces-disable no-any-unknown -- narrowed to a non-null, non-array object one line above\n return (section as Record<string, unknown>)[parts[1]] !== undefined;\n }\n\n /**\n * The understood keys AT ONE LEVEL, rendered from the allow-list rather than hand-listed — and\n * rendered at the level the reader's key was actually found, so a mistyped SECTION is answered with\n * the sections and a mistyped FLAG with the flags. The hand-listed version named two keys and went\n * stale the moment a third arrived, telling an agent its brand-new key was not accepted while the\n * validator right above accepted it.\n */\n private quotedKeys(allowed: readonly string[], prefix: string): string {\n return allowed.map((key: string): string => `\"${prefix}${key}\"`).join(', ');\n }\n\n /**\n * An unknown key is IGNORED — see the class docblock for why this one file cannot reject it — but it\n * is never SILENT. The warning is the entire mitigation for the cost of ignoring, so it says both\n * things a reader needs: that the key did nothing, and what the understood keys are.\n *\n * ─── WHY THIS IS NOT THE \"console side channel\" SHAPE ────────────────────────────────────────────\n * That shape is a rule or a library reporting a FAILURE — or a cure for one — by printing it instead\n * of throwing a structured value to the one top-level handler. This is the opposite case, and the\n * distinction is the entire subject of this change: an unknown key here is NOT a failure. The load\n * SUCCEEDS, a valid HomeConfig is returned, every caller proceeds normally, and there is no cure the\n * reader is obliged to apply — a key from a newer release is a CORRECT file being read by an older\n * validator. There is no throw this could be, because throwing is precisely the behaviour being\n * deleted here; and returning it would mean inventing a warnings channel through `load()` that no\n * caller has any reason to render.\n *\n * stderr, not stdout, for the usual reason: this runs inside hooks whose stdout is a JSON decision\n * and inside `wp-*` commands whose stdout is their real output, and neither may be polluted. It is\n * the same channel, with the same `[webpieces]` prefix, that `state-dir-migration.announce` already\n * uses in this package for the same category of finding — something a human may want to know about\n * and is not required to act on.\n */\n /**\n * The sentence for a flag a HUMAN ended, or '' when this key is not one.\n *\n * Consulted BEFORE the retired-near-miss hint and before the generic \"typo or newer release\" line,\n * because for these keys that generic line is actively misleading: it invites the reader to check\n * their spelling or upgrade, when the truth is that they spelled it correctly, they are on a new\n * enough release, and the thing they asked for now happens anyway.\n */\n private endedExperimentHint(dotted: string): string {\n const ended = ENDED_EXPERIMENTS.find((e: EndedExperiment): boolean => e.key === dotted);\n if (ended === undefined) return '';\n return ` That experiment ENDED in @webpieces ${ended.endedIn}. ${ended.note}`;\n }\n\n private warnUnknownKeys(found: string[], allowed: readonly string[], prefix: string): void {\n // ACCEPTED everywhere, ADVERTISED nowhere. A documentation key is not a setting, so it is\n // skipped here rather than added to the allowed lists: those lists are walked elsewhere to build\n // typed sample documents, and listing `_doc` under \"Understood here\" — or offering it as the\n // nearest match to a misspelled setting — would send someone hunting for what it configures.\n // It configures nothing.\n for (const key of found) {\n if (allowed.includes(key) || this.docKeys.isDocumentationKey(key)) continue;\n const near = this.nearestKnownKey(key, allowed);\n const guess = near !== ''\n ? ` Did you mean \"${prefix}${near}\"?`\n : this.endedExperimentHint(`${prefix}${key}`)\n || this.nearRetiredHint(`${prefix}${key}`)\n || ' If it is a typo, fix the spelling; if it is from a NEWER @webpieces than this repo pins, upgrade this repo to use it.';\n this.warn(\n `\"${prefix}${key}\" is not a key this @webpieces release understands, so it was IGNORED ` +\n `and had NO effect.${guess} Understood here: ${this.quotedKeys(allowed, prefix)}.`);\n }\n }\n\n /**\n * A near-miss of a RETIRED key, pointed at its migration — or '' when nothing retired is close.\n *\n * The gap this closes: `assertNotRetired` matches a retired key EXACTLY, so `captureBuildGateLog`\n * throws with its migration instruction while `captureBuildGateLogg` — one stray character away, and\n * a far likelier thing to type — falls through to the generic \"IGNORED, might be from a newer\n * release\" line. That is the least helpful of the three answers offered to the reader whose intent is the\n * clearest, so the retired table is consulted here too, at the same distance-2 threshold.\n *\n * It only ever produces a WARNING, never a throw: this release cannot know whether the reader meant\n * the retired key or a newer one, and guessing wrong in the throwing direction is what the whole\n * change is about. Known keys are matched first, so a typo of a LIVE key is never answered with a\n * dead one.\n */\n private nearRetiredHint(dottedKey: string): string {\n for (const entry of RETIRED_HOME_CONFIG_KEYS) {\n if (this.editDistance(dottedKey.toLowerCase(), entry.key.toLowerCase()) > 2) continue;\n const destination = entry.movedTo === ''\n ? 'it was removed with no replacement'\n : `it moved to \"${entry.movedTo}\"`;\n return ` Did you mean the RETIRED key \"${entry.key}\"? If so, ${destination}. ${entry.instruction}`;\n }\n return '';\n }\n\n // One shape for every non-fatal finding, matching state-dir-migration's `[webpieces] <what>:` prefix\n // so a reader can tell at a glance which subsystem is talking.\n private warn(message: string): void {\n process.stderr.write(`[webpieces] ~/.webpieces/config.json: ${message}\\n`);\n }\n\n /**\n * The closest understood key within two edits, or '' when nothing is close.\n *\n * This used to be a case-insensitive EQUALITY test, which was adequate while an unknown key was a\n * hard error — the error itself was the signal, and the suggestion only saved a reading. Now the\n * suggestion IS the signal, so it has to catch the typos an equality test misses: a doubled letter,\n * a dropped one, a transposition, a stray trailing character (`orphan-dir-sweeped`). Two is the\n * useful threshold — it covers every one of those and still refuses to guess for a genuinely new\n * key, which is the case that must NOT be dressed up as a typo.\n */\n private nearestKnownKey(key: string, allowed: readonly string[]): string {\n let best = '';\n let bestDistance = 3;\n for (const candidate of allowed) {\n const distance = this.editDistance(key.toLowerCase(), candidate.toLowerCase());\n if (distance >= bestDistance) continue;\n bestDistance = distance;\n best = candidate;\n }\n return best;\n }\n\n /** Ordinary Levenshtein distance, one row at a time — the key names are short and this runs once. */\n private editDistance(a: string, b: string): number {\n let previous: number[] = [];\n for (let j = 0; j <= b.length; j += 1) previous.push(j);\n for (let i = 1; i <= a.length; i += 1) {\n const current: number[] = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);\n current.push(Math.min(substitution, previous[j] + 1, current[j - 1] + 1));\n }\n previous = current;\n }\n return previous[b.length];\n }\n\n // One shape for every rejection: what is wrong, in which file, and the fact that deleting the file is\n // always a legal fix — because the file is optional, and \"no file\" is a fully supported state.\n private error(file: string, detail: string): string {\n return `[~/.webpieces/config.json] ${detail}\\n\\n` +\n `File: ${file}\\n` +\n `This machine-local preference file is OPTIONAL and EXPERIMENTAL. Editing it is always ` +\n `permitted, even while it is invalid, and deleting it outright is a valid fix — with no such ` +\n `file every webpieces command behaves exactly as it does by default.`;\n }\n}\n"]}
|