@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.
- package/.dz-manifest.json +56 -24
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +103 -23
- package/scripts/check-pipeline-gaps.sh +0 -0
- package/src/utils.js +1 -0
- package/templates/.claude/commands/replicate.md +9 -1
- package/templates/.claude/hooks/check-dangling-refs.cjs +89 -0
- package/templates/.claude/hooks/check-docs-complete.cjs +7 -0
- package/templates/.claude/hooks/check-external-deps.cjs +18 -3
- package/templates/.claude/hooks/statusline.cjs +1 -1
- package/templates/.claude/rules/cost-of-detection-ladder.md +37 -4
- package/templates/.claude/rules/docker-ports.md +28 -0
- package/templates/.claude/rules/feature-lifecycle.md +21 -0
- package/templates/.claude/rules/replicate-pipeline.md +3 -3
- package/templates/.claude/skills/brutal-honesty-review/resources/assessment-rubrics.md +12 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md +45 -0
- package/tests/dz-availability.js +34 -0
- package/tests/e2e/packed-insights-writer.test.js +5 -1
- package/tests/npm-cli-resolver.js +366 -0
- package/tests/npm-resolver-cases.json +128 -0
- package/tests/snapshot/baseline.json +13 -12
- package/tests/unit/capture-source-path.test.js +73 -24
- package/tests/unit/check-dangling-refs.test.js +91 -0
- package/tests/unit/check-external-deps.test.js +26 -2
- package/tests/unit/detection-ladder-contract.test.js +20 -10
- package/tests/unit/guard-honest-input-meta.test.js +49 -0
- package/tests/unit/honest-failure-rules.test.js +23 -1
- package/tests/unit/insights-writer.test.js +5 -1
- package/tests/unit/negative-conclusion-gate.test.js +3 -3
- package/tests/unit/npm-cli-resolver.test.js +314 -0
- package/tests/unit/optional-doc-idiom.test.js +83 -0
- package/tests/unit/quote-provenance.test.js +4 -0
- package/tests/unit/traceability-negative-fixture.test.js +19 -5
- package/tests/unit/verdict-vocabulary.test.js +72 -0
- 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-
|
|
3
|
-
"totalFiles":
|
|
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": "
|
|
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-
|
|
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": "
|
|
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": "
|
|
44
|
+
".claude/hooks/statusline.cjs": "bac385da01dd9f1548d0db48a2acdada5e89a7822a7f0ecfb61cb74ff4cda138",
|
|
44
45
|
".claude/hooks/write-insight.cjs": "743f2d85b037cf522dfaf4775947b0cd331e52f2f304e88360c160e0e8e9a589",
|
|
45
|
-
".claude/rules/cost-of-detection-ladder.md": "
|
|
46
|
-
".claude/rules/docker-ports.md": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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",
|