@dzhechkov/p-replicator 1.13.2 → 1.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.dz-manifest.json +56 -24
  2. package/bin/cli.js +0 -0
  3. package/package.json +11 -10
  4. package/sbom.json +103 -23
  5. package/scripts/check-pipeline-gaps.sh +0 -0
  6. package/src/utils.js +1 -0
  7. package/templates/.claude/commands/replicate.md +9 -1
  8. package/templates/.claude/hooks/check-dangling-refs.cjs +89 -0
  9. package/templates/.claude/hooks/check-docs-complete.cjs +7 -0
  10. package/templates/.claude/hooks/check-external-deps.cjs +18 -3
  11. package/templates/.claude/hooks/statusline.cjs +1 -1
  12. package/templates/.claude/rules/cost-of-detection-ladder.md +37 -4
  13. package/templates/.claude/rules/docker-ports.md +28 -0
  14. package/templates/.claude/rules/feature-lifecycle.md +21 -0
  15. package/templates/.claude/rules/replicate-pipeline.md +3 -3
  16. package/templates/.claude/skills/brutal-honesty-review/resources/assessment-rubrics.md +12 -2
  17. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md +45 -0
  18. package/tests/dz-availability.js +34 -0
  19. package/tests/e2e/packed-insights-writer.test.js +5 -1
  20. package/tests/npm-cli-resolver.js +366 -0
  21. package/tests/npm-resolver-cases.json +128 -0
  22. package/tests/snapshot/baseline.json +13 -12
  23. package/tests/unit/capture-source-path.test.js +73 -24
  24. package/tests/unit/check-dangling-refs.test.js +91 -0
  25. package/tests/unit/check-external-deps.test.js +26 -2
  26. package/tests/unit/detection-ladder-contract.test.js +20 -10
  27. package/tests/unit/guard-honest-input-meta.test.js +49 -0
  28. package/tests/unit/honest-failure-rules.test.js +23 -1
  29. package/tests/unit/insights-writer.test.js +5 -1
  30. package/tests/unit/negative-conclusion-gate.test.js +3 -3
  31. package/tests/unit/npm-cli-resolver.test.js +314 -0
  32. package/tests/unit/optional-doc-idiom.test.js +83 -0
  33. package/tests/unit/quote-provenance.test.js +4 -0
  34. package/tests/unit/traceability-negative-fixture.test.js +19 -5
  35. package/tests/unit/verdict-vocabulary.test.js +72 -0
  36. package/LICENSE +0 -21
@@ -0,0 +1,366 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Which npm may a test use? — CommonJS twin of
5
+ * `packages/@dzhechkov/harness-cli/test/npm-cli-resolver.ts` (ADR-001 of feature
6
+ * `repo-sweep-honesty`, FR-3 + FR-4).
7
+ *
8
+ * WHY A TWIN AND NOT A SHARED HELPER. This package PUBLISHES its `tests/` directory (see the
9
+ * `files` field of its package.json), so an import reaching outside the package would travel into
10
+ * the tarball as a broken reference to a file the consumer never receives. The two packages also
11
+ * live in different module systems — harness-cli is `type: "module"` TypeScript, this one is
12
+ * CommonJS JavaScript — so one file fit for both does not exist. What is shared instead is the
13
+ * CONTRACT: `npm-resolver-cases.json`, one byte-identical copy per package, held in step by
14
+ * `packages/@dzhechkov/harness-core/test/npm-resolver-cases-twin-drift.test.ts`.
15
+ *
16
+ * THE DEFECT THIS REPLACES. The previous fallback here read
17
+ * `npm_execpath && fs.existsSync(npm_execpath) ? npm_execpath : <global npm>`. Under `pnpm test`,
18
+ * `npm_execpath` is pnpm.cjs; that file EXISTS; so the fallback never fired in the one case it was
19
+ * written for, and pnpm was loaded as if it were the npm CLI — `Unknown option: 'dry-run'`
20
+ * (MEASURED 2026-09-18). The question is therefore identity, not existence.
21
+ *
22
+ * WHAT "IDENTITY" MEANS HERE — three facts a look-alike cannot borrow together (review r1, HIGH-5):
23
+ * 1. the candidate is DEREFERENCED first (`realpath`), because a genuine npm is normally reached
24
+ * through a symlink (`/usr/bin/npm` -> `../lib/node_modules/npm/bin/npm-cli.js`, MEASURED) and
25
+ * resolving modules from `/usr/bin` finds nothing — the real npm was being REJECTED;
26
+ * 2. the nearest enclosing `package.json` says `"name": "npm"`, because module resolution answers
27
+ * a question about a DIRECTORY, so without this any intruder dropped into a tree that contains
28
+ * `libnpmpack` was ACCEPTED;
29
+ * 3. `libnpmpack` resolves from the dereferenced file and lives in the SAME installation.
30
+ */
31
+
32
+ const fs = require('node:fs');
33
+ const Module = require('node:module');
34
+ const path = require('node:path');
35
+
36
+ /** The module whose resolvability IS npm's identity. Nothing else about npm is checked. */
37
+ const NPM_IDENTITY_MODULE = 'libnpmpack';
38
+
39
+ /** Where npm's CLI entry point sits inside a global module root. */
40
+ const NPM_CLI_RELATIVE_PATH = path.join('npm', 'bin', 'npm-cli.js');
41
+
42
+ /** The name the enclosing package.json must carry for a file to be part of npm. */
43
+ const NPM_PACKAGE_NAME = 'npm';
44
+
45
+ /** How far up the tree the enclosing `package.json` is looked for. npm's CLI is 2 levels deep. */
46
+ const PACKAGE_ROOT_SEARCH_DEPTH = 8;
47
+
48
+ /**
49
+ * `true` when `child` is `parent` itself or lies underneath it.
50
+ * @param {string} parent
51
+ * @param {string} child
52
+ * @returns {boolean}
53
+ */
54
+ function isInside(parent, child) {
55
+ if (!parent || !child) return false;
56
+ const base = parent.endsWith(path.sep) ? parent.slice(0, -path.sep.length) : parent;
57
+ return child === base || child.startsWith(base + path.sep);
58
+ }
59
+
60
+ /**
61
+ * @param {string} candidate
62
+ * @returns {string|null}
63
+ */
64
+ function realPathOrNull(candidate) {
65
+ try {
66
+ return fs.realpathSync(candidate);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The directory of the npm PACKAGE that contains `realCandidate`, or `null` when the nearest
74
+ * enclosing `package.json` belongs to something else (or there is none at all).
75
+ *
76
+ * @param {string} realCandidate
77
+ * @returns {string|null}
78
+ */
79
+ function npmPackageRoot(realCandidate) {
80
+ let dir = path.dirname(realCandidate);
81
+ for (let depth = 0; depth < PACKAGE_ROOT_SEARCH_DEPTH; depth += 1) {
82
+ const manifest = path.join(dir, 'package.json');
83
+ if (fs.existsSync(manifest)) {
84
+ try {
85
+ const parsed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
86
+ return parsed && typeof parsed === 'object' && parsed.name === NPM_PACKAGE_NAME ? dir : null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ const parent = path.dirname(dir);
92
+ if (parent === dir) return null;
93
+ dir = parent;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * The dereferenced path of `candidate` when it really is npm's CLI, otherwise `null`.
100
+ * @param {string|undefined|null} candidate
101
+ * @returns {string|null}
102
+ */
103
+ function resolveNpmCliPath(candidate) {
104
+ if (typeof candidate !== 'string' || candidate === '') return null;
105
+ if (!fs.existsSync(candidate)) return null;
106
+ const real = realPathOrNull(candidate);
107
+ if (real === null) return null;
108
+
109
+ const packageRoot = npmPackageRoot(real);
110
+ if (packageRoot === null) return null;
111
+
112
+ let resolved;
113
+ try {
114
+ resolved = Module.createRequire(real).resolve(NPM_IDENTITY_MODULE);
115
+ } catch {
116
+ return null;
117
+ }
118
+ const realResolved = realPathOrNull(resolved) || resolved;
119
+ // The identity module must belong to the SAME installation: either inside npm's own package, or
120
+ // hoisted into the `node_modules` directory that holds it.
121
+ const sameInstall = isInside(packageRoot, realResolved) || isInside(path.dirname(packageRoot), realResolved);
122
+ return sameInstall ? real : null;
123
+ }
124
+
125
+ /**
126
+ * Is the file at `candidate` npm itself? The boolean face of {@link resolveNpmCliPath}.
127
+ * @param {string|undefined|null} candidate
128
+ * @returns {boolean}
129
+ */
130
+ function isNpmCli(candidate) {
131
+ return resolveNpmCliPath(candidate) !== null;
132
+ }
133
+
134
+ /**
135
+ * @param {string[]} roots
136
+ * @returns {string[]}
137
+ */
138
+ function dedupe(roots) {
139
+ const seen = new Set();
140
+ const unique = [];
141
+ for (const root of roots) {
142
+ if (!root) continue;
143
+ const normalised = path.resolve(root);
144
+ if (seen.has(normalised)) continue;
145
+ seen.add(normalised);
146
+ unique.push(normalised);
147
+ }
148
+ return unique;
149
+ }
150
+
151
+ /**
152
+ * @param {NodeJS.ProcessEnv} env
153
+ * @returns {string[]}
154
+ */
155
+ function pathEntries(env) {
156
+ const raw = typeof env.PATH === 'string' ? env.PATH : (typeof env.Path === 'string' ? env.Path : '');
157
+ return raw.split(path.delimiter).filter((entry) => entry !== '');
158
+ }
159
+
160
+ /**
161
+ * Module roots that come from this MACHINE.
162
+ *
163
+ * MEASURED 2026-09-18 and load-bearing: `Module.globalPaths` on this machine is
164
+ * ['/root/.node_modules', '/root/.node_libraries', '/usr/lib/node'] and does NOT contain
165
+ * '/usr/lib/node_modules', where npm actually lives. A lookup that trusted `globalPaths` alone
166
+ * could report "npm not installed" on a machine that has npm, so the layout derived from
167
+ * `process.execPath` is tried first.
168
+ *
169
+ * @param {string} [execPath]
170
+ * @param {NodeJS.ProcessEnv} [env]
171
+ * @returns {string[]}
172
+ */
173
+ function ambientGlobalRoots(execPath, env) {
174
+ const exec = typeof execPath === 'string' && execPath !== '' ? execPath : process.execPath;
175
+ const environment = env || process.env;
176
+ const binDir = path.dirname(exec);
177
+ const nodePath = typeof environment.NODE_PATH === 'string' ? environment.NODE_PATH : '';
178
+ return dedupe([
179
+ path.join(binDir, '..', 'lib', 'node_modules'),
180
+ path.join(binDir, '..', '..', 'lib', 'node_modules'),
181
+ path.join(binDir, 'node_modules'),
182
+ ...nodePath.split(path.delimiter).filter((entry) => entry !== ''),
183
+ ...Module.globalPaths,
184
+ ]);
185
+ }
186
+
187
+ /**
188
+ * npm's configured install prefix — the same value `npm config get prefix` prints, obtained WITHOUT
189
+ * spawning npm.
190
+ *
191
+ * Why not spawn it: running npm to find npm is circular (on the machine where the answer matters
192
+ * most the spawn is exactly what fails), and a subprocess inside a resolver every test calls is a
193
+ * hang risk on a loaded runner. The sources are npm's own, in npm's own order: the
194
+ * `npm_config_prefix` environment variable, then the user config file (`$npm_config_userconfig`,
195
+ * else `$HOME/.npmrc`).
196
+ *
197
+ * HONEST GAP: npm's builtin and global config files are not read, so a prefix set ONLY in
198
+ * `/usr/etc/npmrc` is not seen. The env is the only source consulted, never the ambient
199
+ * `homedir()`, so a caller handing in a bare env gets a deterministic answer.
200
+ *
201
+ * @param {NodeJS.ProcessEnv} [env]
202
+ * @returns {string|undefined}
203
+ */
204
+ function npmConfigPrefix(env) {
205
+ const environment = env || process.env;
206
+ const direct = environment.npm_config_prefix;
207
+ if (typeof direct === 'string' && direct !== '') return direct;
208
+
209
+ const home = typeof environment.HOME === 'string' && environment.HOME !== ''
210
+ ? environment.HOME
211
+ : (typeof environment.USERPROFILE === 'string' ? environment.USERPROFILE : '');
212
+ const userConfig = typeof environment.npm_config_userconfig === 'string' && environment.npm_config_userconfig !== ''
213
+ ? environment.npm_config_userconfig
214
+ : (home ? path.join(home, '.npmrc') : '');
215
+ if (!userConfig || !fs.existsSync(userConfig)) return undefined;
216
+
217
+ try {
218
+ for (const rawLine of fs.readFileSync(userConfig, 'utf8').split(/\r?\n/)) {
219
+ const line = rawLine.trim();
220
+ if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
221
+ const match = /^prefix\s*=\s*(.+)$/.exec(line);
222
+ if (match && match[1] !== undefined) return match[1].trim().replace(/^["']|["']$/g, '');
223
+ }
224
+ } catch {
225
+ return undefined;
226
+ }
227
+ return undefined;
228
+ }
229
+
230
+ /**
231
+ * Module roots under npm's configured prefix (both the POSIX and the Windows layout).
232
+ * @param {NodeJS.ProcessEnv} [env]
233
+ * @returns {string[]}
234
+ */
235
+ function npmPrefixRoots(env) {
236
+ const prefix = npmConfigPrefix(env);
237
+ if (prefix === undefined) return [];
238
+ return dedupe([path.join(prefix, 'lib', 'node_modules'), path.join(prefix, 'node_modules')]);
239
+ }
240
+
241
+ /**
242
+ * Module roots derived from the directories on PATH. A machine whose npm lives under an unrelated
243
+ * prefix (nvm, asdf, Homebrew, a per-user install) is invisible to `process.execPath` and to
244
+ * `globalPaths` alike, but its `bin` directory is on PATH by construction.
245
+ *
246
+ * @param {NodeJS.ProcessEnv} [env]
247
+ * @returns {string[]}
248
+ */
249
+ function pathRoots(env) {
250
+ const environment = env || process.env;
251
+ const roots = [];
252
+ for (const entry of pathEntries(environment)) {
253
+ roots.push(path.join(entry, '..', 'lib', 'node_modules'), path.join(entry, 'node_modules'));
254
+ }
255
+ return dedupe(roots);
256
+ }
257
+
258
+ /**
259
+ * The `npm` executables sitting on PATH. Each is DEREFERENCED and identity-checked, which is what
260
+ * makes the usual `bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js` symlink a usable answer.
261
+ *
262
+ * `npm.cmd` / `npm.ps1` on Windows are batch wrappers, not JavaScript; they are not candidates for
263
+ * a resolver whose answer is fed to `node <path>`, and they are deliberately not listed.
264
+ *
265
+ * @param {NodeJS.ProcessEnv} [env]
266
+ * @returns {string[]}
267
+ */
268
+ function pathNpmExecutables(env) {
269
+ return pathEntries(env || process.env).map((entry) => path.join(entry, 'npm'));
270
+ }
271
+
272
+ /**
273
+ * Every root this resolver would search for a given machine + environment, in order.
274
+ * @param {string} [execPath]
275
+ * @param {NodeJS.ProcessEnv} [env]
276
+ * @returns {string[]}
277
+ */
278
+ function defaultGlobalRoots(execPath, env) {
279
+ return dedupe([...ambientGlobalRoots(execPath, env), ...npmPrefixRoots(env), ...pathRoots(env)]);
280
+ }
281
+
282
+ /**
283
+ * The ladder, in the one order ADR-001 fixes:
284
+ * npm_execpath (only if it IS npm) -> the installed npm -> an honest, named unavailability.
285
+ *
286
+ * `options.globalRoots` replaces the AMBIENT roots only; the env-derived roots (npm's configured
287
+ * prefix, PATH) always apply, because they describe the environment the caller handed in.
288
+ *
289
+ * @param {NodeJS.ProcessEnv} [env]
290
+ * @param {{ globalRoots?: string[] }} [options]
291
+ * @returns {{kind:'npm-cli', path:string, source:'npm_execpath'|'global-install'}|{kind:'unavailable', reason:string}}
292
+ */
293
+ function resolveNpmCli(env, options) {
294
+ const environment = env || process.env;
295
+ const settings = options || {};
296
+ const handedOver = environment.npm_execpath;
297
+
298
+ const handedOverReal = resolveNpmCliPath(handedOver);
299
+ if (handedOverReal !== null) {
300
+ return { kind: 'npm-cli', path: handedOverReal, source: 'npm_execpath' };
301
+ }
302
+
303
+ const roots = dedupe([
304
+ ...(Array.isArray(settings.globalRoots) ? settings.globalRoots : ambientGlobalRoots(process.execPath, environment)),
305
+ ...npmPrefixRoots(environment),
306
+ ...pathRoots(environment),
307
+ ]);
308
+ for (const root of roots) {
309
+ const found = resolveNpmCliPath(path.join(root, NPM_CLI_RELATIVE_PATH));
310
+ if (found !== null) {
311
+ return { kind: 'npm-cli', path: found, source: 'global-install' };
312
+ }
313
+ }
314
+
315
+ const executables = pathNpmExecutables(environment);
316
+ for (const executable of executables) {
317
+ const found = resolveNpmCliPath(executable);
318
+ if (found !== null) {
319
+ return { kind: 'npm-cli', path: found, source: 'global-install' };
320
+ }
321
+ }
322
+
323
+ const handedOverNote = typeof handedOver !== 'string' || handedOver === ''
324
+ ? 'npm_execpath was not set'
325
+ : `npm_execpath=${handedOver} is not npm (module '${NPM_IDENTITY_MODULE}' does not resolve from it)`;
326
+ return {
327
+ kind: 'unavailable',
328
+ reason: `npm not found: ${handedOverNote}; `
329
+ + `no '${NPM_CLI_RELATIVE_PATH}' whose '${NPM_IDENTITY_MODULE}' resolves was found under `
330
+ + `[${roots.join(', ')}]`
331
+ + `, nor an 'npm' executable on PATH [${executables.join(', ')}]`,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * FR-4 — a skip must be AUDIBLE. A silent `skip` is how a test stops proving anything without
337
+ * anyone noticing, so the reason (which names the missing tool) is written to the suite's output
338
+ * before the test steps aside.
339
+ *
340
+ * @param {{kind:'unavailable', reason:string}} resolution
341
+ * @param {((line: string) => void)|null} [write]
342
+ * @returns {string}
343
+ */
344
+ function announceNpmSkip(resolution, write) {
345
+ const emit = typeof write === 'function' ? write : (line) => console.log(line);
346
+ const line = `SKIPPED — this test needs the real npm packer. ${resolution.reason}`;
347
+ emit(line);
348
+ return line;
349
+ }
350
+
351
+ module.exports = {
352
+ NPM_IDENTITY_MODULE,
353
+ NPM_CLI_RELATIVE_PATH,
354
+ NPM_PACKAGE_NAME,
355
+ ambientGlobalRoots,
356
+ announceNpmSkip,
357
+ defaultGlobalRoots,
358
+ isNpmCli,
359
+ npmConfigPrefix,
360
+ npmPackageRoot,
361
+ npmPrefixRoots,
362
+ pathNpmExecutables,
363
+ pathRoots,
364
+ resolveNpmCli,
365
+ resolveNpmCliPath,
366
+ };
@@ -0,0 +1,128 @@
1
+ {
2
+ "contract": "resolveNpmCli(env, options) — ADR-001 of feature repo-sweep-honesty. A test that needs the real npm packer must check the IDENTITY of the tool it was handed, never the existence of a path: under pnpm, npm_execpath points at pnpm.cjs, that file exists, and an existsSync fallback therefore never fires in the one case it was written for (MEASURED 2026-09-18).",
3
+ "identity": [
4
+ "A candidate is npm only if ALL THREE hold (review r1, HIGH-5 — the first and third were missing):",
5
+ "1. the candidate is DEREFERENCED first (realpath), because a genuine npm is normally reached through a symlink (/usr/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js) and resolving modules from /usr/bin finds nothing;",
6
+ "2. the nearest enclosing package.json says name == 'npm', because module resolution answers a question about a DIRECTORY — without this, any intruder dropped into a tree that contains libnpmpack passes;",
7
+ "3. 'libnpmpack' resolves from the dereferenced file AND the module it resolves to lives in the SAME npm installation (inside npm's own package directory, or in the node_modules that holds it)."
8
+ ],
9
+ "ladder": [
10
+ "1. npm_execpath is used only if it passes the identity check above (the answer is the DEREFERENCED path).",
11
+ "2. otherwise the installed npm is looked up in the global module roots: the ambient machine roots (process.execPath layout, NODE_PATH, module.globalPaths), then npm's configured prefix, then the directories on PATH.",
12
+ "3. otherwise an 'npm' executable on PATH is dereferenced and identity-checked.",
13
+ "4. otherwise the answer is 'unavailable' carrying a reason that NAMES the tool that was not found and everywhere it was looked for."
14
+ ],
15
+ "twins": [
16
+ "packages/@dzhechkov/harness-cli/test/npm-resolver-cases.json",
17
+ "packages/@dzhechkov/p-replicator/tests/npm-resolver-cases.json"
18
+ ],
19
+ "twinGuard": "packages/@dzhechkov/harness-core/test/npm-resolver-cases-twin-drift.test.ts keeps the two copies byte-identical. Two implementations exist because p-replicator PUBLISHES its tests/ directory, so it may not import from the repo root; the contract, not the code, is what is shared.",
20
+ "givenFields": {
21
+ "npmExecPath": "placeholder name for env.npm_execpath, or null to leave it unset",
22
+ "globalRoots": "'defaultRoots' to let the resolver pick its own AMBIENT roots, or a list of placeholder names that replaces them",
23
+ "env": "optional map of extra environment variables; each value is a placeholder name. Env-derived roots (npm's prefix, PATH) apply even when globalRoots is overridden, because they describe the environment handed in rather than the machine",
24
+ "nullArguments": "optional; when true the resolver is called with explicit nulls for BOTH optional arguments instead of an env object"
25
+ },
26
+ "placeholders": {
27
+ "npmCli": "absolute path to a real npm-cli.js, located WITHOUT the resolver under test",
28
+ "pnpmCli": "absolute path to a real pnpm.cjs — an existing file that is NOT npm",
29
+ "decoyCli": "a freshly written .js file that exists and is NOT npm; the fixture that an existence check would wrongly accept",
30
+ "intruderCli": "a freshly written .js file that is NOT npm but sits in a tree where 'libnpmpack' DOES resolve; the fixture that a resolution-only check would wrongly accept",
31
+ "npmCliSymlink": "a symlink pointing at the real npm-cli.js; the fixture a resolver that does not dereference would wrongly reject",
32
+ "npmPrefix": "npm's install prefix, derived from `npm root -g` without the resolver under test",
33
+ "npmrcHome": "a directory holding a .npmrc whose `prefix=` names npmPrefix",
34
+ "npmBinDir": "a directory holding a symlink named 'npm' that points at the real npm-cli.js, as a bin directory on PATH does",
35
+ "missingPath": "an absolute path that does not exist",
36
+ "emptyRoot": "an existing directory holding no npm installation",
37
+ "defaultRoots": "the resolver's own ambient global-root list, whatever it is on this machine"
38
+ },
39
+ "cases": [
40
+ {
41
+ "id": "execpath-that-is-npm-is-used",
42
+ "why": "the happy path the whole ladder exists to preserve: a real npm hand-off is honoured",
43
+ "given": { "npmExecPath": "npmCli", "globalRoots": "defaultRoots" },
44
+ "requires": ["npmCli"],
45
+ "expect": { "kind": "npm-cli", "source": "npm_execpath", "path": "npmCli" }
46
+ },
47
+ {
48
+ "id": "execpath-that-is-pnpm-is-rejected",
49
+ "why": "the measured defect: pnpm.cjs exists, so an existence check accepts it and the caller then loads pnpm as if it were npm",
50
+ "given": { "npmExecPath": "pnpmCli", "globalRoots": "defaultRoots" },
51
+ "requires": ["npmCli", "pnpmCli"],
52
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
53
+ },
54
+ {
55
+ "id": "execpath-that-is-an-existing-decoy-is-rejected",
56
+ "why": "machine-independent proof that IDENTITY is checked and not EXISTENCE; this file always exists",
57
+ "given": { "npmExecPath": "decoyCli", "globalRoots": "defaultRoots" },
58
+ "requires": ["npmCli"],
59
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
60
+ },
61
+ {
62
+ "id": "intruder-in-a-tree-where-libnpmpack-resolves-is-rejected",
63
+ "why": "review r1 HIGH-5: resolving libnpmpack proves something about the DIRECTORY, not about the file; without the enclosing-package check any file dropped beside a node_modules/libnpmpack passed as npm",
64
+ "given": { "npmExecPath": "intruderCli", "globalRoots": "defaultRoots" },
65
+ "requires": ["npmCli", "intruderCli"],
66
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
67
+ },
68
+ {
69
+ "id": "symlinked-npm-is-dereferenced-and-accepted",
70
+ "why": "review r1 HIGH-5, the other half: a genuine npm is normally reached through a symlink, and resolving from the symlink's own directory finds nothing — the real npm was being rejected",
71
+ "given": { "npmExecPath": "npmCliSymlink", "globalRoots": "defaultRoots" },
72
+ "requires": ["npmCli", "npmCliSymlink"],
73
+ "expect": { "kind": "npm-cli", "source": "npm_execpath", "path": "npmCli" }
74
+ },
75
+ {
76
+ "id": "execpath-that-does-not-exist-is-rejected",
77
+ "why": "a stale hand-off must fall through the ladder rather than crash the caller",
78
+ "given": { "npmExecPath": "missingPath", "globalRoots": "defaultRoots" },
79
+ "requires": ["npmCli"],
80
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
81
+ },
82
+ {
83
+ "id": "no-execpath-falls-through-to-the-installed-npm",
84
+ "why": "direct runs (npx vitest run / node --test) set no npm_execpath at all",
85
+ "given": { "npmExecPath": null, "globalRoots": "defaultRoots" },
86
+ "requires": ["npmCli"],
87
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
88
+ },
89
+ {
90
+ "id": "npm-config-prefix-from-the-environment-is-searched",
91
+ "why": "review r1 HIGH-5: an npm installed under an unrelated prefix is invisible to process.execPath and to globalPaths, and was reported absent on a machine that has it",
92
+ "given": { "npmExecPath": "decoyCli", "globalRoots": ["emptyRoot"], "env": { "npm_config_prefix": "npmPrefix" } },
93
+ "requires": ["npmCli", "npmPrefix"],
94
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
95
+ },
96
+ {
97
+ "id": "npm-config-prefix-from-the-user-npmrc-is-searched",
98
+ "why": "the same question `npm config get prefix` answers, read from npm's own user config instead of by spawning npm (spawning the tool you are looking for is circular)",
99
+ "given": { "npmExecPath": "decoyCli", "globalRoots": ["emptyRoot"], "env": { "HOME": "npmrcHome" } },
100
+ "requires": ["npmCli", "npmrcHome"],
101
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
102
+ },
103
+ {
104
+ "id": "an-npm-on-PATH-is-found-when-every-root-is-empty",
105
+ "why": "review r1 HIGH-5: the fallback ignored PATH, yet a bin directory on PATH is how the user runs npm at all; the entry there is a symlink, so this also proves the dereferencing",
106
+ "given": { "npmExecPath": "decoyCli", "globalRoots": ["emptyRoot"], "env": { "PATH": "npmBinDir" } },
107
+ "requires": ["npmCli", "npmBinDir"],
108
+ "expect": { "kind": "npm-cli", "source": "global-install", "path": "npmCli" }
109
+ },
110
+ {
111
+ "id": "explicit-null-arguments-behave-like-omitted-ones",
112
+ "why": "review r1 LOW-7: a default parameter fires only on undefined, so the TypeScript twin threw on resolveNpmCli(null) while the CommonJS twin fell back to process.env — two twins that disagree on a value a caller can pass are not twins",
113
+ "given": { "npmExecPath": null, "globalRoots": "defaultRoots", "nullArguments": true },
114
+ "requires": ["npmCli"],
115
+ "expect": { "kind": "npm-cli" }
116
+ },
117
+ {
118
+ "id": "no-npm-anywhere-is-an-honest-unavailable",
119
+ "why": "FR-4: absence is reported with a NAMED reason, never as a pass and never as a crash",
120
+ "given": { "npmExecPath": "decoyCli", "globalRoots": ["emptyRoot"] },
121
+ "requires": [],
122
+ "expect": {
123
+ "kind": "unavailable",
124
+ "reasonIncludes": ["npm", "libnpmpack", "npm/bin/npm-cli.js"]
125
+ }
126
+ }
127
+ ]
128
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-02T07:49:08.074Z",
3
- "totalFiles": 166,
2
+ "generatedAt": "2026-09-20T09:34:29.101Z",
3
+ "totalFiles": 167,
4
4
  "files": {
5
5
  ".claude/agents/doc-validator.md": "e57950758cd4764b2cb54b3f1905567e9dbc1ed20ac6ecd30998614894689f4f",
6
6
  ".claude/agents/harvest-coordinator.md": "71e524e8379df46bbf457dbc1d4480634e05c99b7d3a4f59a356b8d71a0a4166",
@@ -14,7 +14,7 @@
14
14
  ".claude/commands/myinsights.md": "f31c5e133b5027197359e4179cbba1ab535f990634a69ea7c5c73bd2bc329d68",
15
15
  ".claude/commands/next.md": "1e4984306c3aa80dfc8734dc1c975dfe5fdf2cf6396ddd759e4c7635646107b8",
16
16
  ".claude/commands/plan.md": "6fef889f51168981bf4a98b8392a0d595e3d199e1cfaa8e08c0a181ec6c440e4",
17
- ".claude/commands/replicate.md": "165d7b3c0606be6817893b53cbfc5ca55c7b81cbe35cbdd0dccf8679d702e81a",
17
+ ".claude/commands/replicate.md": "f176d8ec871459b5a8d2111b8527331f34340d6e169da8cc7792953e3e334774",
18
18
  ".claude/commands/run.md": "87144dfe8c56c15ba7c117daa544d8a08b84d7205f21dafcc48d27c6c3995059",
19
19
  ".claude/commands/start.md": "fb8a7762f4073b9e8689dec3830680907efb9ec68bb2faa39b8a547793d9a078",
20
20
  ".claude/hooks/autocommit-insights.cjs": "5e7cef87b24d457f25315d79fe15f0dfaf94d9269495d638817f0faee2593c6f",
@@ -22,9 +22,10 @@
22
22
  ".claude/hooks/autocommit-roadmap.cjs": "f4dcc9ffd9a3aed91dd56e011e699fcf8af903cc81594e4ab1ef61e5432a36ad",
23
23
  ".claude/hooks/capture-source-path.cjs": "bed92548c3313d69110b6cc06e6b2b858d9d548b8c18c82750ae90d514212bf2",
24
24
  ".claude/hooks/check-canon.cjs": "99e6bff9b0078eb2ec23f7df98b7ac06c894f9018eba4a8c358e8f92e5ec7b06",
25
- ".claude/hooks/check-docs-complete.cjs": "27c05c2adcedc806e395b30b455243193a4fbd0968ce96e415679e1f78db951a",
25
+ ".claude/hooks/check-dangling-refs.cjs": "8f012876b1af110a1093319d36a2f84208a15780dbb8abb5a43eb4aeb8f76865",
26
+ ".claude/hooks/check-docs-complete.cjs": "0f516df0882c9da53d166a1b4e30ff1fc37bd5a2027c7e5fbdab6cba116ca828",
26
27
  ".claude/hooks/check-embed-contract.cjs": "9e963a07c12350b760264133021e75fb699439b1f060332e1ccdd962b645b4a0",
27
- ".claude/hooks/check-external-deps.cjs": "7bce49a744170a3193edf4b2238bcc5a6f1b0efa29333760a52e4fd145739d18",
28
+ ".claude/hooks/check-external-deps.cjs": "d671353b2e73e0790206cf963ee85f1ff6b97d56d975b9eb3ad59a14d7239e2a",
28
29
  ".claude/hooks/check-file-ownership.cjs": "c6273e7b0817d15e375586131924a08095722aff7d0e6a6a04c857a480788e1f",
29
30
  ".claude/hooks/check-growth-trace.cjs": "91994fc109c8440306c9dd3d5eea176c533eb0818fcdb09bac2b309397d9908a",
30
31
  ".claude/hooks/check-handoff-manifest.cjs": "768cd0befc0da3adcddcded86ed6d1bd1ac4f25afb563ae578002060a54968cf",
@@ -40,25 +41,25 @@
40
41
  ".claude/hooks/check-webhook-contract.cjs": "dfa27bd24cee8868fd60be5d24749c206f303fe4afcc1e72024d526324562a39",
41
42
  ".claude/hooks/session-insights.cjs": "31fbe90d81b01dfb56557ed5d68dbde2211a14d5469c5d3761f45fb63c07c3a7",
42
43
  ".claude/hooks/state-update.cjs": "47146a76b768ac2273abe8f2f017856a0e28b45afaf90048148da417dd19604e",
43
- ".claude/hooks/statusline.cjs": "50c08decec3e80ed325f50d7b062fbe0add47e8594002fcc165248492242ab56",
44
+ ".claude/hooks/statusline.cjs": "bac385da01dd9f1548d0db48a2acdada5e89a7822a7f0ecfb61cb74ff4cda138",
44
45
  ".claude/hooks/write-insight.cjs": "743f2d85b037cf522dfaf4775947b0cd331e52f2f304e88360c160e0e8e9a589",
45
- ".claude/rules/cost-of-detection-ladder.md": "f15309ee8f1494478ff7678380dbe8fc8eecd154be36d57c9090cd17126a07da",
46
- ".claude/rules/docker-ports.md": "59eb430f29dd762e8b76ad6d1177fe8b5b250de007856c743387cb88ee157942",
46
+ ".claude/rules/cost-of-detection-ladder.md": "d2e7da3d56c8d4c6d1aa58e793fa0d5eecd65a5c01be38dfa0dc434273306838",
47
+ ".claude/rules/docker-ports.md": "a9e5bb9a797ea4ef4c0671cee2cbec95f2557f8b9e1d20b84bbcee7ce4c21162",
47
48
  ".claude/rules/embeddable-widget.md": "0159224f8e9599b67a860738bb42e3e40adf598176c48ea6c86a01c891f0f18c",
48
- ".claude/rules/feature-lifecycle.md": "4e926f84f244feb6824114a0624733738669c93e0508693fa2afa4e9a35c2aa3",
49
+ ".claude/rules/feature-lifecycle.md": "a7d0adf6bc63b942d3dec339b3ae68972c3dedc0b48a91d27653309c7da7f11f",
49
50
  ".claude/rules/git-workflow.md": "7d8d2ded47fcc0fb95da3dc226efa6cb3c80d6adf7296977588c181dfce20192",
50
51
  ".claude/rules/honest-configuration.md": "851bc28e9e2baf1043bc09b982f3df3431bc7af1e076f8ff9df3d6729835bad5",
51
52
  ".claude/rules/incoming-webhooks.md": "92f3de02814ed0610df644acf554f4b41b2bae2f9d281695e2e217a1d6558e84",
52
53
  ".claude/rules/insights-capture.md": "d529e6be79178636ca362d83366b1dd7a843371e8ba4f2b09d65c009fab52dc4",
53
54
  ".claude/rules/long-running-job.md": "e6bd0efff89338998d34440a94dc9288cf7bcf790b27bcaa51b83d43f57942ed",
54
55
  ".claude/rules/model-call-cost.md": "26c3b20ac3ed7f9cf6deb5e94302ade87f9e3e51fffca7ac2377ff31bc144f34",
55
- ".claude/rules/replicate-pipeline.md": "13bf073273820addeb87167402495b2f683cefac70a9f6cb07b6e054b1851e0f",
56
+ ".claude/rules/replicate-pipeline.md": "9c4bbaa9ac628ec5d4e44d3ace6eec0b5bd602b5dccc5d36f123d73be448f159",
56
57
  ".claude/rules/skill-interface-protocol.md": "c3f624d0a8a4c7e0f5744e529eb4486cc0589fdb732b9799bccc75091a6d63ca",
57
58
  ".claude/rules/swarm-file-evidence.md": "c71a216dbc3fd633c30949823ceac354f8439b96279a1a8dd606afb1b3ef5c45",
58
59
  ".claude/settings.json": "18db09827dce410667b93e2b1a41ef8e46d4431114733e4304a5fc2192b2fc66",
59
60
  ".claude/skills/brutal-honesty-review/evals/brutal-honesty-review.yaml": "855c155d55575e12aab23fb3a5fb2fada74d93974ff755dfedac6f2d5bd41f13",
60
61
  ".claude/skills/brutal-honesty-review/README.md": "2a026b4af50b21456e50424fbfa089cb983fbbaa0c68024c8c17c695bea25a57",
61
- ".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "dac09c91c765e2211799cc230738fb72a966f47511edb872306a906e49640cbe",
62
+ ".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "b64894e1ddf8b9da7f2f146c737d35215e7281e3d8be10891d4a9b8aa2bd6d07",
62
63
  ".claude/skills/brutal-honesty-review/resources/review-template.md": "95182c8605b1d821360257e1c5dbac4300bda333d861785de417cbb84090b25b",
63
64
  ".claude/skills/brutal-honesty-review/schemas/output.json": "2e252a3497b86322e1a6e6bd1aca8708b44389db3a2d81a6f893d467b6351210",
64
65
  ".claude/skills/brutal-honesty-review/scripts/assess-code.sh": "2b10d2972b1379adde9a68ddf8bcdb399b7d65075e458a54523ffd8d99ddc746",
@@ -79,7 +80,7 @@
79
80
  ".claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md": "657e35db0cb77d3a9d1a4d01215a9b8d50441dd863593bee3a84f62aad8b00bc",
80
81
  ".claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md": "5e87c91956eabc70b4a1084b47b2c9a20ede18bb245735812ead956eafabf3af",
81
82
  ".claude/skills/cc-toolkit-generator-enhanced/references/maturity-integration.md": "aa4e743bcd0281525336a298d0489c31481f69ff1a534e1ed1d262d888937deb",
82
- ".claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md": "5f1d650c426cbd2c9536b385306605ea62a5a520ee4566d23ca7254409c672a5",
83
+ ".claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md": "4a7fae9ea1870c30b71c923fd5d3626a8926c5e960209fad709f4b8e0534fc80",
83
84
  ".claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md": "9ce3db4c3c5b9696a312152067c1869b1f421ce77fbcfdca83edaa797c5caf3f",
84
85
  ".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md": "c91f81da197a072b57378fd0018eea26c35d909a558bdca6f070eb60153c97b3",
85
86
  ".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md": "8398f2493434db101476300e514a34bf8124a51eb732cf81923f9a5b5a3d5db4",