@webpieces/rules-config 0.4.735 → 0.4.737
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.737",
|
|
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/stale-bin-sweep.js
CHANGED
|
@@ -24,11 +24,15 @@ const to_error_1 = require("./to-error");
|
|
|
24
24
|
// argument for the call site below — the sweep has to ride a path that runs ROUTINELY, because the defect
|
|
25
25
|
// is created routinely.
|
|
26
26
|
//
|
|
27
|
-
// The predecessor's NAME is deliberately not written here.
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
27
|
+
// The predecessor's NAME is deliberately not written here, and DO NOT WRITE IT BACK IN. Two reasons, and
|
|
28
|
+
// the second is the one that matters. First, nothing in the code needs it: the predicate below is
|
|
29
|
+
// STRUCTURAL — a `wp-*` entry whose link target does not resolve — so naming one particular retired
|
|
30
|
+
// command would invite a reader to think the sweep is a list of known dead names somebody has to keep up
|
|
31
|
+
// to date. Second, a retired command named in tracked source is exactly how a dead name outlives its
|
|
32
|
+
// tooling: it gets copied into a cure string, and an agent reading that cure at the moment every other
|
|
33
|
+
// route is closed is handed a command that does not exist. That is the very defect this module cleans up
|
|
34
|
+
// one level out, and there is no longer an automated scan standing behind this paragraph — the reader is.
|
|
35
|
+
// PR #743 and the issue this shipped under carry the literal name; nothing in the tree needs to.
|
|
32
36
|
//
|
|
33
37
|
// WHY IT MATTERS MORE THAN TIDINESS. `ls node_modules/.bin` lists them, so a dangling entry ADVERTISES a
|
|
34
38
|
// capability that does not exist — a human and an AI were both misled by that listing before checking the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale-bin-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/stale-bin-sweep.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// SWEEP DANGLING `node_modules/.bin/wp-*` SYMLINKS.\n//\n// THE DEFECT. pnpm's linker ADDS a link for every entry in the current manifest's `bin` map, but it never\n// sweeps `.bin` for orphans left behind by a PREVIOUS version of the SAME package. `@webpieces/pr-gate`\n// and `@webpieces/ai-hook-rules` are upgraded, never removed, so nothing ever triggers a delete: every\n// bin either package has EVER shipped stays linked forever, pointing at a script that is no longer on\n// disk. A scan of nine clones on ONE machine found 18 distinct dangling `wp-*` names.\n//\n// THE DEFECT IS ONGOING, AND A ROUTINE RENAME PRODUCES IT. Measured on this repo's own upgrade to\n// 0.4.728, DURING the session that wrote this file: PR #743 hard-renamed one `wp-*` command with no alias,\n// so the new manifest declares `wp-sync-main` and no longer declares its predecessor. One `pnpm install`\n// later, `.bin` held the NEW link created that minute AND the predecessor's link still sitting there from\n// the previous install, dangling — reproduced independently in two separate trees of this repo. So this is\n// not a historical mess left by a deleted feature that a one-time migration could mop up: it is\n// regenerated by the most ordinary change a package can make, on every clone, every time. That is the\n// argument for the call site below — the sweep has to ride a path that runs ROUTINELY, because the defect\n// is created routinely.\n//\n// The predecessor's NAME is deliberately not written here. `no-old-sync-main-name` forbids the dead\n// spelling in tracked source and blocked an earlier draft of this very comment, which is the rule working:\n// a retired command named in source is exactly how a dead name outlives its tooling — the thing this\n// module exists to clean up, one level out. PR #743 and the issue this shipped under carry the literal\n// name; nothing in the code needs it, because the predicate below is structural.\n//\n// WHY IT MATTERS MORE THAN TIDINESS. `ls node_modules/.bin` lists them, so a dangling entry ADVERTISES a\n// capability that does not exist — a human and an AI were both misled by that listing before checking the\n// link target — and the failure it eventually produces is self-referential and useless:\n// `Command \"wp-authorize\" not found / Did you mean \"pnpm wp-authorize\"?`. This is `upgrade-shim.ts`'s own\n// governing principle one level out: an entry pointing at a missing file is WORSE than absence.\n//\n// WHY A PREFIX AND NOT A NAME LIST. A hardcoded list of retired bin names would go stale in exactly the\n// way the symlinks did — the list assembled from the two names that prompted this would have caught 2 of\n// the 18, and it would NOT have caught the rename above, which had not happened yet when the list would\n// have been written. That is the general case: the next orphan is always created by a release later than\n// any list. A name list is also unwriteable here on purpose — see the note above about naming dead\n// commands in source. The predicate is structural instead — a `wp-` prefixed entry that is a SYMLINK whose target\n// does not exist — so it needs no maintenance and cannot miss a name nobody has thought of. The prefix is\n// the whole safety story: another package's bins are never touched.\n//\n// WHY NOT A postinstall HOOK. `setupDebugging.md` records the postinstall approach as ABANDONED in this\n// repo. The call site is the `wp-*` startup pass that regenerates `.webpieces/instruct-ai/*` — see\n// TemplateWriter — and that placement is the load-bearing half of the fix. This is not a tidy-up for one\n// repo: every developer's machine has this graveyard, and a cure that only cleans the tree somebody happens\n// to run it in is worthless. Riding the pass EVERY `wp-*` command takes is what makes the RELEASED sweep\n// reach every clone on every machine, healing each one the next time any `wp-*` command runs there.\n//\n// NOT ALSO CALLED FROM `wp-upgrade-shim`, though it would read naturally there (that bin already deletes a\n// retired FILE on the same principle). `upgrade-shim.ts` may import only `fs`/`path` so it still runs on a\n// tree too broken to load the rule engine: this package's barrel would pull in inversify and the config\n// loader, and a subpath import would resolve against the INSTALLED rules-config, which is a release behind\n// the local source — so a spawned `wp-upgrade-shim` would die on module resolution, in the one command an\n// L0-blocked session has left. Its header records that, and points here.\n//\n// DEPENDENCY-FREE ANYWAY: `fs`, `path` and `toError`. No inversify (see StaleBinSweeper), so this module\n// stays cheap for the startup path it runs on and importable by anything that later needs it.\n// ---------------------------------------------------------------------------\n\n// The one place the prefix is spelled. Everything outside it belongs to some other package.\nconst WP_BIN_PREFIX = 'wp-';\n\n/**\n * One dangling entry the sweep ACTED ON: what was linked, the target that was not there, and — when the\n * removal itself failed — why. Data-only (per CLAUDE.md).\n *\n * `failure` exists because the removal is an `fs.rmSync`, not just a probe: an unwritable `.bin` (EACCES,\n * a read-only mount) would otherwise heal silently-never while the tree kept advertising a command that\n * does not exist, which is the precise defect this module exists to remove. Not thrown — the sweep is a\n * courtesy and must never fail the `wp-*` command that called it — so the diagnostic rides back here and\n * {@link StaleBinSweeper.report} states both outcomes.\n */\nexport class StaleBinRemoval {\n name: string; // the bin name as it appeared in .bin (e.g. 'wp-authorize')\n target: string; // the link target that does not exist, as recorded in the symlink\n failure: string; // '' = removed; non-empty = still there, and this is why\n\n constructor(name: string, target: string, failure = '') {\n this.name = name;\n this.target = target;\n this.failure = failure;\n }\n}\n\n/**\n * Removes `node_modules/.bin/wp-*` entries whose symlink target is gone.\n *\n * Deliberately NOT `@injectable`: decorating it would import inversify, and this module must stay loadable\n * by `wp-upgrade-shim` on a tree that cannot build a DI container (see the header). Callers use the shared\n * {@link staleBinSweeper} instance, which is also what makes {@link sweepOnce}'s memo process-wide.\n */\nexport class StaleBinSweeper {\n // Roots already swept in THIS process. `writeTemplate` is called several times per `wp-*` command (once\n // per instruct-ai doc), and a sweep that reported per call would print the same removals repeatedly.\n private readonly swept = new Set<string>();\n\n /** Where the bins live for a tree. Public so a test can point at a fixture without guessing the layout. */\n binDir(repoRoot: string): string {\n return path.join(repoRoot, 'node_modules', '.bin');\n }\n\n /**\n * Sweep once per root per process, returning what was removed ([] on every later call for the same\n * root, and [] when there was nothing to remove — the two are indistinguishable to a caller ON PURPOSE,\n * because both mean \"say nothing\").\n */\n sweepOnce(repoRoot: string): StaleBinRemoval[] {\n if (this.swept.has(repoRoot)) return [];\n this.swept.add(repoRoot);\n return this.sweep(repoRoot);\n }\n\n /**\n * Remove every dangling `wp-*` symlink under the tree's `.bin`, returning what went. [] when the\n * directory does not exist (a linked worktree with no install of its own is the common case) or when\n * everything there resolves.\n *\n * BEST EFFORT, PER ENTRY. A `.bin` that cannot be read, or one entry that cannot be removed, must never\n * take down the `wp-*` command that called this — the sweep is a courtesy, not the command's job.\n */\n sweep(repoRoot: string): StaleBinRemoval[] {\n const dir = this.binDir(repoRoot);\n const removed: StaleBinRemoval[] = [];\n for (const name of this.entries(dir)) {\n if (!name.startsWith(WP_BIN_PREFIX)) continue;\n const removal = this.removeIfDangling(path.join(dir, name), name);\n if (removal !== null) removed.push(removal);\n }\n return removed;\n }\n\n /**\n * The lines a caller prints for a sweep. [] for an empty sweep, so REMOVING NOTHING IS SILENT — this\n * runs on every `wp-*` command and the common case must add no noise at all.\n *\n * Rendered here rather than at each call site so the two callers cannot describe the same act\n * differently; each still chooses its own output channel and its own leading icon convention.\n */\n report(removed: readonly StaleBinRemoval[]): string[] {\n if (removed.length === 0) return [];\n const gone = removed.filter((r: StaleBinRemoval): boolean => r.failure === '');\n const stuck = removed.filter((r: StaleBinRemoval): boolean => r.failure !== '');\n const lines: string[] = [];\n if (gone.length > 0) {\n lines.push(`✅ @webpieces: removed ${gone.length} dangling node_modules/.bin/wp-* symlink(s) left by an earlier release:`);\n for (const r of gone) lines.push(` ${r.name} -> ${r.target} (target missing)`);\n lines.push(' They pointed at scripts this release no longer ships, so they could only ever fail on execution.');\n }\n // A failed removal is STATED, never quietly dropped: the entry is still there, still advertising a\n // command that does not exist, and only a human can fix an unwritable .bin.\n if (stuck.length > 0) {\n lines.push(`⚠️ @webpieces: ${stuck.length} dangling node_modules/.bin/wp-* symlink(s) could NOT be removed:`);\n for (const r of stuck) lines.push(` ${r.name} -> ${r.target} (target missing; ${r.failure})`);\n lines.push(' They still advertise commands this release does not ship. Nothing else is affected — remove them by hand.');\n }\n return lines;\n }\n\n // The directory listing, or [] when there is no .bin (or it cannot be read).\n private entries(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable .bin means \"nothing to sweep\", never a failed wp-* command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(dir)) return [];\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: no readable .bin means there is nothing to sweep\n return [];\n }\n }\n\n /**\n * Remove ONE entry if — and only if — it is a symlink whose target does not exist. Returns what was\n * acted on, with `failure` set when the entry was dangling but could not be removed; `null` when the\n * entry was not a dangling `wp-*` link at all.\n */\n private removeIfDangling(full: string, name: string): StaleBinRemoval | null {\n const target = this.danglingTarget(full);\n if (target === null) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unremovable entry is REPORTED, never fatal to the wp-* command that called the sweep\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(full, { force: true });\n return new StaleBinRemoval(name, target);\n } catch (err: unknown) {\n const error = toError(err);\n return new StaleBinRemoval(name, target, error.message);\n }\n }\n\n /**\n * The link target of a DANGLING symlink, or `null` when this entry is not one — a real file, a live\n * link, or something we cannot stat. `existsSync` FOLLOWS symlinks, so a false answer on a path `lstat`\n * calls a link is exactly the dangling case, with no need to resolve the target ourselves.\n *\n * Split from the removal so the two catches mean different things: an unreadable entry is simply not\n * ours to touch, while a failed REMOVAL is a fact worth printing.\n */\n private danglingTarget(full: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an entry we cannot stat is not ours to touch\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.lstatSync(full).isSymbolicLink()) return null;\n if (fs.existsSync(full)) return null;\n return fs.readlinkSync(full);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: an entry we cannot read is left exactly as it is\n return null;\n }\n }\n}\n\n// The shared instance — the memo in `sweepOnce` is per-instance, so every caller must use this one.\nexport const staleBinSweeper = new StaleBinSweeper();\n"]}
|
|
1
|
+
{"version":3,"file":"stale-bin-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/stale-bin-sweep.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// SWEEP DANGLING `node_modules/.bin/wp-*` SYMLINKS.\n//\n// THE DEFECT. pnpm's linker ADDS a link for every entry in the current manifest's `bin` map, but it never\n// sweeps `.bin` for orphans left behind by a PREVIOUS version of the SAME package. `@webpieces/pr-gate`\n// and `@webpieces/ai-hook-rules` are upgraded, never removed, so nothing ever triggers a delete: every\n// bin either package has EVER shipped stays linked forever, pointing at a script that is no longer on\n// disk. A scan of nine clones on ONE machine found 18 distinct dangling `wp-*` names.\n//\n// THE DEFECT IS ONGOING, AND A ROUTINE RENAME PRODUCES IT. Measured on this repo's own upgrade to\n// 0.4.728, DURING the session that wrote this file: PR #743 hard-renamed one `wp-*` command with no alias,\n// so the new manifest declares `wp-sync-main` and no longer declares its predecessor. One `pnpm install`\n// later, `.bin` held the NEW link created that minute AND the predecessor's link still sitting there from\n// the previous install, dangling — reproduced independently in two separate trees of this repo. So this is\n// not a historical mess left by a deleted feature that a one-time migration could mop up: it is\n// regenerated by the most ordinary change a package can make, on every clone, every time. That is the\n// argument for the call site below — the sweep has to ride a path that runs ROUTINELY, because the defect\n// is created routinely.\n//\n// The predecessor's NAME is deliberately not written here, and DO NOT WRITE IT BACK IN. Two reasons, and\n// the second is the one that matters. First, nothing in the code needs it: the predicate below is\n// STRUCTURAL — a `wp-*` entry whose link target does not resolve — so naming one particular retired\n// command would invite a reader to think the sweep is a list of known dead names somebody has to keep up\n// to date. Second, a retired command named in tracked source is exactly how a dead name outlives its\n// tooling: it gets copied into a cure string, and an agent reading that cure at the moment every other\n// route is closed is handed a command that does not exist. That is the very defect this module cleans up\n// one level out, and there is no longer an automated scan standing behind this paragraph — the reader is.\n// PR #743 and the issue this shipped under carry the literal name; nothing in the tree needs to.\n//\n// WHY IT MATTERS MORE THAN TIDINESS. `ls node_modules/.bin` lists them, so a dangling entry ADVERTISES a\n// capability that does not exist — a human and an AI were both misled by that listing before checking the\n// link target — and the failure it eventually produces is self-referential and useless:\n// `Command \"wp-authorize\" not found / Did you mean \"pnpm wp-authorize\"?`. This is `upgrade-shim.ts`'s own\n// governing principle one level out: an entry pointing at a missing file is WORSE than absence.\n//\n// WHY A PREFIX AND NOT A NAME LIST. A hardcoded list of retired bin names would go stale in exactly the\n// way the symlinks did — the list assembled from the two names that prompted this would have caught 2 of\n// the 18, and it would NOT have caught the rename above, which had not happened yet when the list would\n// have been written. That is the general case: the next orphan is always created by a release later than\n// any list. A name list is also unwriteable here on purpose — see the note above about naming dead\n// commands in source. The predicate is structural instead — a `wp-` prefixed entry that is a SYMLINK whose target\n// does not exist — so it needs no maintenance and cannot miss a name nobody has thought of. The prefix is\n// the whole safety story: another package's bins are never touched.\n//\n// WHY NOT A postinstall HOOK. `setupDebugging.md` records the postinstall approach as ABANDONED in this\n// repo. The call site is the `wp-*` startup pass that regenerates `.webpieces/instruct-ai/*` — see\n// TemplateWriter — and that placement is the load-bearing half of the fix. This is not a tidy-up for one\n// repo: every developer's machine has this graveyard, and a cure that only cleans the tree somebody happens\n// to run it in is worthless. Riding the pass EVERY `wp-*` command takes is what makes the RELEASED sweep\n// reach every clone on every machine, healing each one the next time any `wp-*` command runs there.\n//\n// NOT ALSO CALLED FROM `wp-upgrade-shim`, though it would read naturally there (that bin already deletes a\n// retired FILE on the same principle). `upgrade-shim.ts` may import only `fs`/`path` so it still runs on a\n// tree too broken to load the rule engine: this package's barrel would pull in inversify and the config\n// loader, and a subpath import would resolve against the INSTALLED rules-config, which is a release behind\n// the local source — so a spawned `wp-upgrade-shim` would die on module resolution, in the one command an\n// L0-blocked session has left. Its header records that, and points here.\n//\n// DEPENDENCY-FREE ANYWAY: `fs`, `path` and `toError`. No inversify (see StaleBinSweeper), so this module\n// stays cheap for the startup path it runs on and importable by anything that later needs it.\n// ---------------------------------------------------------------------------\n\n// The one place the prefix is spelled. Everything outside it belongs to some other package.\nconst WP_BIN_PREFIX = 'wp-';\n\n/**\n * One dangling entry the sweep ACTED ON: what was linked, the target that was not there, and — when the\n * removal itself failed — why. Data-only (per CLAUDE.md).\n *\n * `failure` exists because the removal is an `fs.rmSync`, not just a probe: an unwritable `.bin` (EACCES,\n * a read-only mount) would otherwise heal silently-never while the tree kept advertising a command that\n * does not exist, which is the precise defect this module exists to remove. Not thrown — the sweep is a\n * courtesy and must never fail the `wp-*` command that called it — so the diagnostic rides back here and\n * {@link StaleBinSweeper.report} states both outcomes.\n */\nexport class StaleBinRemoval {\n name: string; // the bin name as it appeared in .bin (e.g. 'wp-authorize')\n target: string; // the link target that does not exist, as recorded in the symlink\n failure: string; // '' = removed; non-empty = still there, and this is why\n\n constructor(name: string, target: string, failure = '') {\n this.name = name;\n this.target = target;\n this.failure = failure;\n }\n}\n\n/**\n * Removes `node_modules/.bin/wp-*` entries whose symlink target is gone.\n *\n * Deliberately NOT `@injectable`: decorating it would import inversify, and this module must stay loadable\n * by `wp-upgrade-shim` on a tree that cannot build a DI container (see the header). Callers use the shared\n * {@link staleBinSweeper} instance, which is also what makes {@link sweepOnce}'s memo process-wide.\n */\nexport class StaleBinSweeper {\n // Roots already swept in THIS process. `writeTemplate` is called several times per `wp-*` command (once\n // per instruct-ai doc), and a sweep that reported per call would print the same removals repeatedly.\n private readonly swept = new Set<string>();\n\n /** Where the bins live for a tree. Public so a test can point at a fixture without guessing the layout. */\n binDir(repoRoot: string): string {\n return path.join(repoRoot, 'node_modules', '.bin');\n }\n\n /**\n * Sweep once per root per process, returning what was removed ([] on every later call for the same\n * root, and [] when there was nothing to remove — the two are indistinguishable to a caller ON PURPOSE,\n * because both mean \"say nothing\").\n */\n sweepOnce(repoRoot: string): StaleBinRemoval[] {\n if (this.swept.has(repoRoot)) return [];\n this.swept.add(repoRoot);\n return this.sweep(repoRoot);\n }\n\n /**\n * Remove every dangling `wp-*` symlink under the tree's `.bin`, returning what went. [] when the\n * directory does not exist (a linked worktree with no install of its own is the common case) or when\n * everything there resolves.\n *\n * BEST EFFORT, PER ENTRY. A `.bin` that cannot be read, or one entry that cannot be removed, must never\n * take down the `wp-*` command that called this — the sweep is a courtesy, not the command's job.\n */\n sweep(repoRoot: string): StaleBinRemoval[] {\n const dir = this.binDir(repoRoot);\n const removed: StaleBinRemoval[] = [];\n for (const name of this.entries(dir)) {\n if (!name.startsWith(WP_BIN_PREFIX)) continue;\n const removal = this.removeIfDangling(path.join(dir, name), name);\n if (removal !== null) removed.push(removal);\n }\n return removed;\n }\n\n /**\n * The lines a caller prints for a sweep. [] for an empty sweep, so REMOVING NOTHING IS SILENT — this\n * runs on every `wp-*` command and the common case must add no noise at all.\n *\n * Rendered here rather than at each call site so the two callers cannot describe the same act\n * differently; each still chooses its own output channel and its own leading icon convention.\n */\n report(removed: readonly StaleBinRemoval[]): string[] {\n if (removed.length === 0) return [];\n const gone = removed.filter((r: StaleBinRemoval): boolean => r.failure === '');\n const stuck = removed.filter((r: StaleBinRemoval): boolean => r.failure !== '');\n const lines: string[] = [];\n if (gone.length > 0) {\n lines.push(`✅ @webpieces: removed ${gone.length} dangling node_modules/.bin/wp-* symlink(s) left by an earlier release:`);\n for (const r of gone) lines.push(` ${r.name} -> ${r.target} (target missing)`);\n lines.push(' They pointed at scripts this release no longer ships, so they could only ever fail on execution.');\n }\n // A failed removal is STATED, never quietly dropped: the entry is still there, still advertising a\n // command that does not exist, and only a human can fix an unwritable .bin.\n if (stuck.length > 0) {\n lines.push(`⚠️ @webpieces: ${stuck.length} dangling node_modules/.bin/wp-* symlink(s) could NOT be removed:`);\n for (const r of stuck) lines.push(` ${r.name} -> ${r.target} (target missing; ${r.failure})`);\n lines.push(' They still advertise commands this release does not ship. Nothing else is affected — remove them by hand.');\n }\n return lines;\n }\n\n // The directory listing, or [] when there is no .bin (or it cannot be read).\n private entries(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable .bin means \"nothing to sweep\", never a failed wp-* command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(dir)) return [];\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: no readable .bin means there is nothing to sweep\n return [];\n }\n }\n\n /**\n * Remove ONE entry if — and only if — it is a symlink whose target does not exist. Returns what was\n * acted on, with `failure` set when the entry was dangling but could not be removed; `null` when the\n * entry was not a dangling `wp-*` link at all.\n */\n private removeIfDangling(full: string, name: string): StaleBinRemoval | null {\n const target = this.danglingTarget(full);\n if (target === null) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unremovable entry is REPORTED, never fatal to the wp-* command that called the sweep\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(full, { force: true });\n return new StaleBinRemoval(name, target);\n } catch (err: unknown) {\n const error = toError(err);\n return new StaleBinRemoval(name, target, error.message);\n }\n }\n\n /**\n * The link target of a DANGLING symlink, or `null` when this entry is not one — a real file, a live\n * link, or something we cannot stat. `existsSync` FOLLOWS symlinks, so a false answer on a path `lstat`\n * calls a link is exactly the dangling case, with no need to resolve the target ourselves.\n *\n * Split from the removal so the two catches mean different things: an unreadable entry is simply not\n * ours to touch, while a failed REMOVAL is a fact worth printing.\n */\n private danglingTarget(full: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an entry we cannot stat is not ours to touch\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.lstatSync(full).isSymbolicLink()) return null;\n if (fs.existsSync(full)) return null;\n return fs.readlinkSync(full);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: an entry we cannot read is left exactly as it is\n return null;\n }\n }\n}\n\n// The shared instance — the memo in `sweepOnce` is per-instance, so every caller must use this one.\nexport const staleBinSweeper = new StaleBinSweeper();\n"]}
|
|
@@ -56,7 +56,7 @@ the option you pick EXACTLY as written and run nothing else on that line.
|
|
|
56
56
|
|
|
57
57
|
### `S` — a webpieces-managed hook file, one of the harness hook registrations (.claude/settings.json, .codex/hooks.json) or the managed env entry does not match this release
|
|
58
58
|
|
|
59
|
-
- **Option 1 (preferred)**: `pnpm exec wp-upgrade-shim` ← pick this when this fault fires at all — it is the only cure that repairs EVERY managed surface (ai-hook.sh, each harness hook registration, and the Claude settings env entry), and it also deletes the retired guarantee-root.sh and any entry still naming it, and it touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer
|
|
59
|
+
- **Option 1 (preferred)**: `pnpm exec wp-upgrade-shim` ← pick this when this fault fires at all — it is the only cure that repairs EVERY managed surface (ai-hook.sh, each harness hook registration, the anchoring of the neighbour hook commands registered beside ours, and the Claude settings env entry), and it also deletes the retired guarantee-root.sh and any entry still naming it, and it touches no config; needs installed @webpieces/ai-hook-rules 0.4.408 or newer
|
|
60
60
|
- **Option 2**: `cp node_modules/@webpieces/ai-hook-rules/templates/ai-hook.sh .claude/webpieces/ai-hook.sh` ← pick this when the installed @webpieces/ai-hook-rules is OLDER than 0.4.408, so wp-upgrade-shim does not exist yet — it is PARTIAL (it repairs ai-hook.sh and NOTHING else), so upgrade @webpieces afterwards and run Option 1 to finish
|
|
61
61
|
|
|
62
62
|
### `C` — webpieces.config.json missing
|