@holmes-lab/holmes-kit 0.18.0 → 0.19.2
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/CHANGELOG.md +149 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +22 -0
- package/dist/holmes/cli/agents.js +76 -1
- package/dist/holmes/cli/approve.js +6 -1
- package/dist/holmes/cli/doctor.d.ts +36 -1
- package/dist/holmes/cli/doctor.js +182 -35
- package/dist/holmes/cli/index.js +7 -1
- package/dist/holmes/cli/init.js +12 -0
- package/dist/holmes/cli/native-deps.d.ts +65 -0
- package/dist/holmes/cli/native-deps.js +131 -0
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +68 -0
- package/dist/holmes/cpg/arch-observe.d.ts +15 -0
- package/dist/holmes/cpg/arch-observe.js +19 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
- package/dist/holmes/cpg/cpg-scanner.js +27 -3
- package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
- package/dist/holmes/cpg/cycle-detect.js +251 -0
- package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
- package/dist/holmes/cpg/cycle-observation.js +146 -0
- package/dist/holmes/cpg/scan-cache.d.ts +1 -1
- package/dist/holmes/cpg/scanned-file.d.ts +36 -0
- package/dist/holmes/cpg/scanned-file.js +2 -0
- package/dist/holmes/governance/approval-queue.d.ts +23 -4
- package/dist/holmes/governance/approval-queue.js +44 -6
- package/dist/holmes/governance/constitution.d.ts +20 -0
- package/dist/holmes/governance/constitution.js +17 -0
- package/dist/holmes/governance/ledger-store.d.ts +9 -0
- package/dist/holmes/governance/ledger-store.js +47 -0
- package/dist/holmes/governance/provenance-chain.d.ts +16 -1
- package/dist/holmes/governance/provenance-chain.js +5 -3
- package/dist/holmes/hooks/pre-tool-use.js +3 -1
- package/dist/holmes/hooks/stop.d.ts +29 -0
- package/dist/holmes/hooks/stop.js +119 -3
- package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
- package/dist/holmes/mcp/defuse-bound.js +8 -0
- package/dist/holmes/mcp/handlers.d.ts +7 -0
- package/dist/holmes/mcp/handlers.js +132 -6
- package/dist/holmes/mcp/history-admission.d.ts +15 -0
- package/dist/holmes/mcp/history-admission.js +37 -0
- package/dist/holmes/mcp/maintenance-analyze.d.ts +45 -0
- package/dist/holmes/mcp/maintenance-analyze.js +117 -9
- package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
- package/dist/holmes/mcp/maintenance-evidence.js +71 -4
- package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
- package/dist/holmes/project/install-scripts-policy.js +131 -0
- package/dist/holmes/project/npx-bin.d.ts +6 -0
- package/dist/holmes/project/npx-bin.js +10 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/failed-test-names.d.ts +19 -0
- package/dist/holmes/review/failed-test-names.js +43 -0
- package/dist/holmes/review/paired-power.d.ts +14 -0
- package/dist/holmes/review/paired-power.js +57 -0
- package/dist/holmes/review/replay-corpus.d.ts +11 -0
- package/dist/holmes/review/replay-corpus.js +34 -0
- package/dist/holmes/review/run-replay.d.ts +23 -0
- package/dist/holmes/review/run-replay.js +90 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/review/test-runner.d.ts +27 -0
- package/dist/holmes/review/test-runner.js +59 -3
- package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
- package/dist/holmes/rtm/defuse-symbols.js +91 -0
- package/dist/holmes/rtm/incremental.js +5 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +32 -5
- package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/spec/legacy-fields.d.ts +2 -0
- package/dist/holmes/spec/legacy-fields.js +9 -0
- package/dist/holmes/spec/legacy-format.d.ts +1 -1
- package/dist/holmes/spec/legacy-format.js +4 -1
- package/dist/holmes/spec/spec-parser.js +5 -3
- package/docs/install-guide.md +54 -5
- package/package.json +4 -1
- package/playbooks/author-slice/PLAYBOOK.md +14 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
|
@@ -37,7 +37,10 @@ exports.STRIPPED_FOR_PROBE = void 0;
|
|
|
37
37
|
exports.globalInstallDir = globalInstallDir;
|
|
38
38
|
exports.npmCliEntry = npmCliEntry;
|
|
39
39
|
exports.prefixVerdict = prefixVerdict;
|
|
40
|
+
exports.grammarProbe = grammarProbe;
|
|
41
|
+
exports.gatherNativeEvidence = gatherNativeEvidence;
|
|
40
42
|
exports.probeEnv = probeEnv;
|
|
43
|
+
exports.resolveWiringPath = resolveWiringPath;
|
|
41
44
|
exports.runDoctor = runDoctor;
|
|
42
45
|
exports.wiringSpawnCheck = wiringSpawnCheck;
|
|
43
46
|
exports.pushGateCheck = pushGateCheck;
|
|
@@ -48,8 +51,11 @@ exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
|
|
|
48
51
|
// @implements A-SPEC-442
|
|
49
52
|
// @implements A-SPEC-207
|
|
50
53
|
// @implements A-SPEC-100.2
|
|
54
|
+
// @implements A-SPEC-580
|
|
51
55
|
const fs = __importStar(require("node:fs"));
|
|
52
56
|
const npx_bin_1 = require("../project/npx-bin");
|
|
57
|
+
const install_scripts_policy_1 = require("../project/install-scripts-policy");
|
|
58
|
+
const native_deps_1 = require("./native-deps");
|
|
53
59
|
const tier_1 = require("../semantic/tier");
|
|
54
60
|
const path = __importStar(require("node:path"));
|
|
55
61
|
const role_policy_1 = require("../governance/role-policy");
|
|
@@ -175,6 +181,114 @@ function prefixVerdict(input) {
|
|
|
175
181
|
fix,
|
|
176
182
|
};
|
|
177
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* @implements A-SPEC-580
|
|
186
|
+
* Parse a trivial source with every grammar, in a child node whose module resolution starts at
|
|
187
|
+
* `packageRoot`. `parsed` counts typescript and tsx separately (8 for the 7 grammar packages).
|
|
188
|
+
*/
|
|
189
|
+
function grammarProbe(packageRoot) {
|
|
190
|
+
const script = `
|
|
191
|
+
const out = { parsed: 0, failed: [] };
|
|
192
|
+
let Parser;
|
|
193
|
+
try { Parser = require('tree-sitter'); }
|
|
194
|
+
catch (e) { process.stdout.write(JSON.stringify({ ...out, error: String(e && e.message || e).split('\\n')[0] })); process.exit(0); }
|
|
195
|
+
const loaders = [
|
|
196
|
+
['typescript', () => require('tree-sitter-typescript').typescript],
|
|
197
|
+
['tsx', () => require('tree-sitter-typescript').tsx],
|
|
198
|
+
];
|
|
199
|
+
for (const g of ${JSON.stringify(GRAMMARS.filter((g) => g !== 'tree-sitter-typescript'))}) {
|
|
200
|
+
loaders.push([g.replace('tree-sitter-', ''), () => require(g)]);
|
|
201
|
+
}
|
|
202
|
+
for (const [name, load] of loaders) {
|
|
203
|
+
try {
|
|
204
|
+
const p = new Parser();
|
|
205
|
+
p.setLanguage(load());
|
|
206
|
+
const node = p.parse('x').rootNode;
|
|
207
|
+
const root = node && node.type;
|
|
208
|
+
if (!root) throw new Error('parse produced no root node');
|
|
209
|
+
out.parsed++;
|
|
210
|
+
} catch (e) { out.failed.push(name + ': ' + String(e && e.message || e).split('\\n')[0]); }
|
|
211
|
+
}
|
|
212
|
+
process.stdout.write(JSON.stringify(out));
|
|
213
|
+
`;
|
|
214
|
+
try {
|
|
215
|
+
const r = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', script], { cwd: packageRoot, encoding: 'utf8', timeout: 30000, env: { ...process.env, NODE_PATH: path.join(packageRoot, 'node_modules') } });
|
|
216
|
+
if (r.status !== 0 || !r.stdout)
|
|
217
|
+
return { parsed: 0, failed: [], error: (r.stderr || `exit ${r.status}`).split('\n')[0] };
|
|
218
|
+
return JSON.parse(r.stdout);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
return { parsed: 0, failed: [], error: e.message.split('\n')[0] };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** A short spawn whose failure means "not observed" — never a verdict. */
|
|
225
|
+
function observe(file, args) {
|
|
226
|
+
try {
|
|
227
|
+
return (0, node_child_process_1.execFileSync)(file, args, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* @implements A-SPEC-580
|
|
235
|
+
* Everything observable about the better-sqlite3 install, gathered for `nativeVerdict`. Each probe
|
|
236
|
+
* is independent and optional: a spawn that fails leaves its field `undefined` (not observed), and
|
|
237
|
+
* the verdict says so rather than treating silence as evidence.
|
|
238
|
+
*
|
|
239
|
+
* Which package.json holds the approval policy depends on the layout: this checkout's own for a
|
|
240
|
+
* repository run, the PROJECT's (three levels up from node_modules/@holmes-lab/holmes-kit) for a
|
|
241
|
+
* local dependency. A global or npx install has no project package.json that npm consults, so its
|
|
242
|
+
* coverage is `uncovered` by construction and the remedy is the per-command `--allow-scripts`.
|
|
243
|
+
*/
|
|
244
|
+
function gatherNativeEvidence(packageRoot) {
|
|
245
|
+
const name = 'better-sqlite3';
|
|
246
|
+
const pkgDir = path.join(packageRoot, 'node_modules', name);
|
|
247
|
+
const bindingPresent = fs.existsSync(path.join(pkgDir, 'build', 'Release', 'better_sqlite3.node'));
|
|
248
|
+
let packageVersion = '(unknown)';
|
|
249
|
+
try {
|
|
250
|
+
packageVersion = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')).version ?? packageVersion;
|
|
251
|
+
}
|
|
252
|
+
catch { /* not installed */ }
|
|
253
|
+
let loadError;
|
|
254
|
+
try {
|
|
255
|
+
const Database = require(name);
|
|
256
|
+
const db = new Database(':memory:');
|
|
257
|
+
db.prepare('SELECT 1 AS ok').get();
|
|
258
|
+
db.close();
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
loadError = e.message.split('\n')[0];
|
|
262
|
+
}
|
|
263
|
+
const npmCli = npmCliEntry();
|
|
264
|
+
const npmVersion = npmCli !== null ? observe(process.execPath, [npmCli, '--version']) : observe('npm', ['--version']);
|
|
265
|
+
const npmMajor = npmVersion !== undefined && /^\d+/.test(npmVersion) ? Number(npmVersion.match(/^\d+/)[0]) : undefined;
|
|
266
|
+
const prefix = npmCli !== null ? observe(process.execPath, [npmCli, 'config', 'get', 'prefix']) : observe('npm', ['config', 'get', 'prefix']);
|
|
267
|
+
const kind = (0, native_deps_1.installKind)(packageRoot, prefix ? globalInstallDir(prefix, process.platform) : undefined);
|
|
268
|
+
let coverage = 'uncovered';
|
|
269
|
+
const policyJson = kind === 'repo' ? path.join(packageRoot, 'package.json')
|
|
270
|
+
: kind === 'local' ? path.join(packageRoot, '..', '..', '..', 'package.json')
|
|
271
|
+
: undefined;
|
|
272
|
+
if (policyJson !== undefined) {
|
|
273
|
+
try {
|
|
274
|
+
const allow = JSON.parse(fs.readFileSync(policyJson, 'utf8')).allowScripts;
|
|
275
|
+
coverage = (0, install_scripts_policy_1.allowScriptsCoverage)(allow, name, packageVersion);
|
|
276
|
+
}
|
|
277
|
+
catch { /* unreadable → uncovered, which is what npm would see too */ }
|
|
278
|
+
}
|
|
279
|
+
const ev = {
|
|
280
|
+
platform: process.platform, packageName: name, packageVersion, bindingPresent, loadError, npmMajor,
|
|
281
|
+
coverage, installKind: kind, pathHasSpace: packageRoot.includes(' '),
|
|
282
|
+
};
|
|
283
|
+
if (process.platform === 'win32') {
|
|
284
|
+
const python = observe('where.exe', ['python']) !== undefined || observe('where.exe', ['py']) !== undefined;
|
|
285
|
+
const vswhere = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
|
|
286
|
+
const msvc = fs.existsSync(vswhere) || observe('where.exe', ['cl']) !== undefined || process.env.VCINSTALLDIR !== undefined;
|
|
287
|
+
ev.toolchain = { python, msvc };
|
|
288
|
+
ev.psPolicy = observe('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-ExecutionPolicy']);
|
|
289
|
+
}
|
|
290
|
+
return ev;
|
|
291
|
+
}
|
|
178
292
|
/** The parent environment minus the variables that legitimately change a gate decision. Pure. */
|
|
179
293
|
function probeEnv(parent) {
|
|
180
294
|
const out = { ...parent };
|
|
@@ -208,6 +322,23 @@ function wiredSettingsPath(target) {
|
|
|
208
322
|
return (0, init_1.settingsPathOf)(target, 'project');
|
|
209
323
|
return (0, init_1.settingsPathOf)(target, fs.existsSync((0, init_1.settingsPathOf)(target, 'local')) ? 'local' : 'project');
|
|
210
324
|
}
|
|
325
|
+
// @implements A-SPEC-581.1
|
|
326
|
+
/**
|
|
327
|
+
* Where a wiring's executable actually is, judged from the PROJECT rather than from the caller.
|
|
328
|
+
*
|
|
329
|
+
* Measured 2026-09-10: the same `.codex/config.toml` under the same `--target` read PASS from the
|
|
330
|
+
* repository root and FAIL from `/tmp`, because a relative arg is resolved by Node against
|
|
331
|
+
* `process.cwd()`. A relative arg is the CONVENTION here — `.mcp.json` has always carried
|
|
332
|
+
* `bin/holmes-mcp.js` — so the verdict was decided by where the person diagnosing stood.
|
|
333
|
+
*
|
|
334
|
+
* An absolute arg is returned untouched: agy's wiring is absolute and must not move. An empty arg
|
|
335
|
+
* stays empty — resolving it would conjure the target directory itself into a "file that exists".
|
|
336
|
+
*/
|
|
337
|
+
function resolveWiringPath(target, arg) {
|
|
338
|
+
if (arg === '')
|
|
339
|
+
return '';
|
|
340
|
+
return path.isAbsolute(arg) ? arg : path.resolve(target, arg);
|
|
341
|
+
}
|
|
211
342
|
async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
212
343
|
const checks = [];
|
|
213
344
|
const add = (name, level, detail, fix) => checks.push({ name, level, detail, fix });
|
|
@@ -227,35 +358,34 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
227
358
|
}
|
|
228
359
|
catch { /* not resolvable from here */ }
|
|
229
360
|
add('node version', 'PASS', `node ${process.version}; better-sqlite3 declares engines.node=${sqliteRange}`, 'If a native module fails to load after a Node major upgrade, reinstall holmes-kit.');
|
|
230
|
-
// 3. tree-sitter + EACH grammar
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
361
|
+
// 3. tree-sitter + EACH grammar — PARSED, not merely resolved. `require.resolve` only proves the
|
|
362
|
+
// package directory exists; a grammar whose prebuilt binding is missing for this platform
|
|
363
|
+
// resolves fine and dies at `setLanguage`. language-parser.ts loads them at module scope, so
|
|
364
|
+
// ONE dead grammar kills MCP server startup — name the dead ones individually.
|
|
365
|
+
// @implements A-SPEC-580
|
|
366
|
+
// Measured in a FRESH process: tree-sitter's native addon binds to the first module registry
|
|
367
|
+
// that loads it, so a second `require` inside the same process (jest workers; any host that
|
|
368
|
+
// already loaded it) hands back a Parser whose `parse()` yields no rootNode — a false FAIL that
|
|
369
|
+
// says nothing about the install. The probe therefore runs in a child node, which is also
|
|
370
|
+
// exactly what the MCP server does at startup.
|
|
371
|
+
{
|
|
372
|
+
const g = grammarProbe(packageRoot);
|
|
373
|
+
if (g.error !== undefined) {
|
|
374
|
+
add('tree-sitter grammars', 'FAIL', `tree-sitter failed to load: ${g.error}`, 'The tree-sitter runtime binding for this platform is missing from its prebuilds. Reinstall; a source build needs Python and a C++ toolchain.');
|
|
375
|
+
}
|
|
376
|
+
else if (g.failed.length === 0) {
|
|
377
|
+
add('tree-sitter grammars', 'PASS', `tree-sitter + ${g.parsed} grammars parse`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
add('tree-sitter grammars', 'FAIL', `failed to parse with: ${g.failed.join('; ')}`, 'Grammars load from shipped prebuilds (no install script needed) — reinstall the package. A single dead grammar prevents the MCP server from starting.');
|
|
237
381
|
}
|
|
238
|
-
catch {
|
|
239
|
-
return true;
|
|
240
|
-
} });
|
|
241
|
-
if (missing.length === 0)
|
|
242
|
-
add('tree-sitter grammars', 'PASS', `tree-sitter + ${GRAMMARS.length} grammars resolve`);
|
|
243
|
-
else
|
|
244
|
-
add('tree-sitter grammars', 'FAIL', `missing: ${missing.join(', ')}`, 'A single missing grammar prevents the MCP server from starting. Reinstall; if a native build failed, ensure a C++ toolchain is available.');
|
|
245
|
-
}
|
|
246
|
-
catch (e) {
|
|
247
|
-
add('tree-sitter grammars', 'FAIL', `tree-sitter failed to load: ${e.message}`, 'Native module build failed. Install Xcode Command Line Tools (macOS) or build-essential, then reinstall.');
|
|
248
|
-
}
|
|
249
|
-
// 4. better-sqlite3 — ABI-locked (not N-API), the most fragile dependency.
|
|
250
|
-
try {
|
|
251
|
-
const Database = require('better-sqlite3');
|
|
252
|
-
const db = new Database(':memory:');
|
|
253
|
-
db.prepare('SELECT 1 AS ok').get();
|
|
254
|
-
db.close();
|
|
255
|
-
add('better-sqlite3', 'PASS', 'loads and executes against :memory:');
|
|
256
382
|
}
|
|
257
|
-
|
|
258
|
-
|
|
383
|
+
// 4. better-sqlite3 — the one dependency whose install script must RUN (prebuild-install ||
|
|
384
|
+
// node-gyp rebuild). Its failure has at least five causes with five different remedies, so the
|
|
385
|
+
// verdict is computed from evidence (A-SPEC-580), never from the load error's wording alone.
|
|
386
|
+
{
|
|
387
|
+
const v = (0, native_deps_1.nativeVerdict)(gatherNativeEvidence(packageRoot));
|
|
388
|
+
add('better-sqlite3', v.level, v.detail, v.fix);
|
|
259
389
|
}
|
|
260
390
|
// 5. Hooks actually gate — prove it with a live allow AND a live deny (exit 2).
|
|
261
391
|
if (built) {
|
|
@@ -809,11 +939,18 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
809
939
|
// the whole check for the common wiring, so "the file is there" was reported as if the
|
|
810
940
|
// server had been proven to start. `wiringHandshakeChecks` below now proves that, for
|
|
811
941
|
// every wired harness; this stays as the cheap, fast FAIL for a path that is simply gone.
|
|
942
|
+
// @implements A-SPEC-581.1 — resolved against the TARGET; the message keeps the string
|
|
943
|
+
// as written, because what the operator must fix is the wiring file, not our arithmetic.
|
|
812
944
|
const bin = server.args[0] ?? '';
|
|
813
|
-
|
|
945
|
+
const binAt = resolveWiringPath(target, bin);
|
|
946
|
+
add('mcp wiring spawn', fs.existsSync(binAt) ? 'PASS' : 'FAIL', fs.existsSync(binAt) ? `node wiring resolves: ${bin} (handshake proven separately)` : `node wiring points at a missing file: ${bin}`, fs.existsSync(binAt) ? undefined : 'Run `holmes-kit init` in the target to rewire the absolute path.');
|
|
814
947
|
}
|
|
815
948
|
else {
|
|
816
|
-
|
|
949
|
+
// @implements A-SPEC-581.1 — the SIBLING of the handshake spawn, and it must move with
|
|
950
|
+
// it. Measured on a consumer install: the same npx wiring read PASS from the handshake
|
|
951
|
+
// (launched in the target) and FAIL here (launched in doctor's cwd) — two checks, one
|
|
952
|
+
// wiring, opposite verdicts.
|
|
953
|
+
checks.push(await wiringSpawnCheck(server.command, server.args, undefined, undefined, target));
|
|
817
954
|
}
|
|
818
955
|
}
|
|
819
956
|
catch {
|
|
@@ -883,17 +1020,19 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
883
1020
|
// the "resolves" claim stronger than the check (adversarial round, Finding 2). Verify the
|
|
884
1021
|
// basename too, to close that gap.
|
|
885
1022
|
const bin = entry.args[0] ?? '';
|
|
1023
|
+
// @implements A-SPEC-581.1 — against the TARGET, not the cwd.
|
|
1024
|
+
const binAt = resolveWiringPath(target, bin);
|
|
886
1025
|
// Resolve the link before judging the name (round-2 F4): a symlink literally named
|
|
887
1026
|
// `holmes-mcp.js` pointing at /etc/hosts must not read as "resolves to this install".
|
|
888
1027
|
const realBin = (() => { try {
|
|
889
|
-
return fs.realpathSync(
|
|
1028
|
+
return fs.realpathSync(binAt);
|
|
890
1029
|
}
|
|
891
1030
|
catch {
|
|
892
1031
|
return '';
|
|
893
1032
|
} })();
|
|
894
1033
|
const resolvesHere = realBin !== '' && path.basename(realBin) === 'holmes-mcp.js';
|
|
895
1034
|
add('codex wiring', resolvesHere ? 'PASS' : 'FAIL', resolvesHere ? `MCP wiring resolves to this install: ${bin}`
|
|
896
|
-
: (fs.existsSync(
|
|
1035
|
+
: (fs.existsSync(binAt) ? `MCP wiring points at a file that is not holmes-mcp.js: ${bin}` : `MCP wiring points at a missing file: ${bin}`), resolvesHere ? undefined : 'Run holmes-kit init --target <dir> --agent codex --force to refresh the absolute path.');
|
|
897
1036
|
}
|
|
898
1037
|
else {
|
|
899
1038
|
const pin = (0, mcp_version_1.mcpLaunchVersion)({ command: entry.command, args: entry.args });
|
|
@@ -1095,7 +1234,12 @@ function cleanupOnSignal(dir) {
|
|
|
1095
1234
|
// harness will actually run reaches a server — the gap the dead npx form lived in.
|
|
1096
1235
|
// @implements A-SPEC-499.1 — exported for the prescription tests; `platform` is injectable so the
|
|
1097
1236
|
// win32 branch is testable off-Windows.
|
|
1098
|
-
function wiringSpawnCheck(command, args, timeoutMs = 30000, platform = process.platform
|
|
1237
|
+
function wiringSpawnCheck(command, args, timeoutMs = 30000, platform = process.platform,
|
|
1238
|
+
// @implements A-SPEC-581.1 — the directory the wiring is launched FROM. A harness starts the
|
|
1239
|
+
// server in the project, so a relative arg (`bin/holmes-mcp.js`, the .mcp.json convention)
|
|
1240
|
+
// resolves there. Left undefined the child inherits doctor's cwd, which made the handshake
|
|
1241
|
+
// verdict depend on where doctor ran — measured 2026-09-10.
|
|
1242
|
+
cwd) {
|
|
1099
1243
|
// The DISPLAYED command stays the original wiring string even when the win32 adapter rewraps the
|
|
1100
1244
|
// execution — the user compares this against their wiring file, not against cmd.exe plumbing.
|
|
1101
1245
|
const quoted = `${command} ${args.join(' ')}`;
|
|
@@ -1124,7 +1268,7 @@ function wiringSpawnCheck(command, args, timeoutMs = 30000, platform = process.p
|
|
|
1124
1268
|
};
|
|
1125
1269
|
const timer = setTimeout(() => finish('FAIL', `wiring did not answer initialize within ${timeoutMs / 1000}s: \`${quoted}\``, 'Re-run `holmes-kit init` in the target to rewrite the wiring, then re-run doctor.'), timeoutMs);
|
|
1126
1270
|
try {
|
|
1127
|
-
child = (0, node_child_process_1.spawn)(spec.command, spec.args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1271
|
+
child = (0, node_child_process_1.spawn)(spec.command, spec.args, { stdio: ['pipe', 'pipe', 'pipe'], ...(cwd === undefined ? {} : { cwd }) });
|
|
1128
1272
|
}
|
|
1129
1273
|
catch (e) {
|
|
1130
1274
|
finish('FAIL', `wiring could not be spawned: \`${quoted}\` — ${e.message}`, spawnFailFix(e));
|
|
@@ -1385,13 +1529,16 @@ async function wiringHandshakeChecks(target) {
|
|
|
1385
1529
|
}
|
|
1386
1530
|
if (entry.command === 'node') {
|
|
1387
1531
|
const bin = entry.args[0] ?? '';
|
|
1388
|
-
|
|
1532
|
+
// @implements A-SPEC-581.1 — the handshake covers ALL THREE harnesses, so the cwd-dependent
|
|
1533
|
+
// read here was the widest instance of the same defect.
|
|
1534
|
+
if (!fs.existsSync(resolveWiringPath(target, bin))) {
|
|
1389
1535
|
out.push({ name, level: 'FAIL', detail: `The wiring points at a missing file: ${bin}`,
|
|
1390
1536
|
fix: `Run holmes-kit init --target <dir> --agent ${wiring.label} --force to refresh the path.` });
|
|
1391
1537
|
continue;
|
|
1392
1538
|
}
|
|
1393
1539
|
}
|
|
1394
|
-
|
|
1540
|
+
// @implements A-SPEC-581.1 — launched FROM the target, exactly as the harness would.
|
|
1541
|
+
const check = await wiringSpawnCheck(entry.command, entry.args, undefined, undefined, target);
|
|
1395
1542
|
out.push({ ...check, name });
|
|
1396
1543
|
}
|
|
1397
1544
|
return out;
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -770,7 +770,13 @@ async function main(argv) {
|
|
|
770
770
|
// The detail row travels with it: round-5 measured the one-line form at 81 columns, so the flag
|
|
771
771
|
// path prints the same two rows the screen does — subject read, id acted on.
|
|
772
772
|
const resolveRef = (ref) => {
|
|
773
|
-
|
|
773
|
+
// @implements A-SPEC-576.1 — the grammar decides which queue the reference is resolved against.
|
|
774
|
+
// A NUMBER is an index into the list the operator was just shown, so it must resolve against
|
|
775
|
+
// the same filtered list or `--grant 3` would act on a row nobody saw. An ID or prefix is the
|
|
776
|
+
// operator naming a specific request — including a `shell` refusal they approved out of band
|
|
777
|
+
// (A-SPEC-563.2), which the inbox filter hides from the list but must never make ungrantable.
|
|
778
|
+
const byIndex = /^\d+$/.test(ref.trim());
|
|
779
|
+
const r = resolveRequestRef(readQueue(root, byIndex ? undefined : { includeAllKinds: true }).pending, ref);
|
|
774
780
|
return r.ok ? { id: r.entry.id, subject: (prefix) => decisionSubject(r.entry, prefix), detail: (tail) => decisionDetail(r.entry, tail) } : { refusal: r.reason };
|
|
775
781
|
};
|
|
776
782
|
const root = typeof flags.target === 'string' ? path.resolve(flags.target) : process.cwd();
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -458,6 +458,18 @@ function runInit(opts) {
|
|
|
458
458
|
for (const agent of opts.agents ?? []) {
|
|
459
459
|
for (const f of (0, agents_1.agentFiles)(agent, { target: opts.target, packageRoot: opts.packageRoot, specsDir: opts.specsDir, launcher: opts.mcpLauncher })) {
|
|
460
460
|
const before = fs.existsSync(f.path) ? fs.readFileSync(f.path, 'utf8') : null;
|
|
461
|
+
// @implements A-SPEC-576.2 — AGENTS.md is the one file here a PERSON edits. Regeneration
|
|
462
|
+
// used to overwrite it whole: it deleted the ADR-018 parity rule a commit had just added,
|
|
463
|
+
// and said nothing. The merge runs in the COMPUTE phase so `--dry-run` predicts the same
|
|
464
|
+
// bytes the write produces (A-SPEC-190 §9), and whatever it moves, it names.
|
|
465
|
+
if (path.basename(f.path) === 'AGENTS.md') {
|
|
466
|
+
const merged = (0, agents_1.mergeAgentsMd)(before, f.content);
|
|
467
|
+
changes.push({ path: f.path, before, after: merged.content });
|
|
468
|
+
if (merged.preserved.length > 0) {
|
|
469
|
+
messages.push(`${f.path}: ${merged.note} — ${merged.preserved.join(' | ')}`);
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
461
473
|
changes.push({ path: f.path, before, after: f.content });
|
|
462
474
|
}
|
|
463
475
|
messages.push(agents_1.HARNESS_ENFORCES[agent]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Coverage } from '../project/install-scripts-policy';
|
|
2
|
+
/**
|
|
3
|
+
* @implements A-SPEC-580
|
|
4
|
+
* Why better-sqlite3 has no binary — judged from EVIDENCE, not from the load error's wording.
|
|
5
|
+
*
|
|
6
|
+
* Measured 2026-09-09 (Windows, npm 12.0.1, Node 24.19.0): doctor said "ABI-locked … reinstall"
|
|
7
|
+
* when the real cause was npm 12 skipping the install script for lack of an `allowScripts` entry.
|
|
8
|
+
* A reinstall reproduces the same state. At least five causes hide behind one FAIL and each has a
|
|
9
|
+
* different remedy, so this module collects what is observable and refuses to assert what is not:
|
|
10
|
+
* a prebuilt-download failure leaves no trace doctor can read, and is named as a possibility only.
|
|
11
|
+
*
|
|
12
|
+
* Pure. The caller (doctor) gathers the evidence; every process it spawns for that is optional,
|
|
13
|
+
* and "not observed" is carried as `undefined`, never as `false`.
|
|
14
|
+
*/
|
|
15
|
+
export type InstallKind = 'repo' | 'local' | 'global' | 'npx' | 'unknown';
|
|
16
|
+
export type NativeCause = 'ok' | 'scripts-blocked' | 'abi-mismatch' | 'build-failed' | 'unknown';
|
|
17
|
+
export type Level = 'PASS' | 'WARN' | 'FAIL';
|
|
18
|
+
export interface NativeEvidence {
|
|
19
|
+
platform: string;
|
|
20
|
+
packageName: string;
|
|
21
|
+
packageVersion: string;
|
|
22
|
+
/** `build/Release/<name>.node` exists under the package. */
|
|
23
|
+
bindingPresent: boolean;
|
|
24
|
+
/** The first line of the `require` failure, when it failed. */
|
|
25
|
+
loadError?: string;
|
|
26
|
+
/** `npm --version` major; undefined when npm could not be consulted. */
|
|
27
|
+
npmMajor?: number;
|
|
28
|
+
/** How the ROOT package.json that governs this install covers the package (A-SPEC-579). */
|
|
29
|
+
coverage: Coverage;
|
|
30
|
+
installKind: InstallKind;
|
|
31
|
+
/** win32 only: whether a source build could even start. */
|
|
32
|
+
toolchain?: {
|
|
33
|
+
python: boolean;
|
|
34
|
+
msvc: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** node-gyp is known to trip over spaces in the install path. */
|
|
37
|
+
pathHasSpace: boolean;
|
|
38
|
+
/** win32 only: `Get-ExecutionPolicy`, when observed. */
|
|
39
|
+
psPolicy?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Where this package lives — the layout decides which package.json (if any) holds the policy. */
|
|
42
|
+
export declare function installKind(packageRoot: string, globalDir?: string): InstallKind;
|
|
43
|
+
/**
|
|
44
|
+
* The narrowest commands that repair each layout. `--allow-scripts=<pkg>` is a per-invocation flag
|
|
45
|
+
* scoped to ONE package — never `--dangerously-allow-all-scripts`, never a change to npm config.
|
|
46
|
+
*
|
|
47
|
+
* Global targets the DEPENDENCY, not the holmes-kit package: measured 2026-09-09 (npm 12.0.1,
|
|
48
|
+
* Windows), `npm rebuild -g @holmes-lab/holmes-kit` re-links the bin and dies EEXIST on the
|
|
49
|
+
* existing `holmes-kit` shim before any script runs, while `npm rebuild -g better-sqlite3
|
|
50
|
+
* --allow-scripts=better-sqlite3` runs `prebuild-install` in place. (It then needs the global
|
|
51
|
+
* prefix to be writable — a protected prefix fails EPERM there, which is the `global prefix`
|
|
52
|
+
* check's territory, not this one's.)
|
|
53
|
+
*/
|
|
54
|
+
export declare function recoveryCommands(ev: Pick<NativeEvidence, 'platform' | 'installKind' | 'packageName' | 'packageVersion'>): string[];
|
|
55
|
+
/**
|
|
56
|
+
* What a PowerShell execution policy means for the emitted commands. Only the two policies that
|
|
57
|
+
* refuse every unsigned local script block the `npm.ps1`/`npx.ps1` shims; the rest add nothing.
|
|
58
|
+
*/
|
|
59
|
+
export declare function powershellPolicyNote(policy: string | undefined): string;
|
|
60
|
+
export declare function nativeVerdict(ev: NativeEvidence): {
|
|
61
|
+
cause: NativeCause;
|
|
62
|
+
level: Level;
|
|
63
|
+
detail: string;
|
|
64
|
+
fix?: string;
|
|
65
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installKind = installKind;
|
|
4
|
+
exports.recoveryCommands = recoveryCommands;
|
|
5
|
+
exports.powershellPolicyNote = powershellPolicyNote;
|
|
6
|
+
exports.nativeVerdict = nativeVerdict;
|
|
7
|
+
// @implements A-SPEC-580
|
|
8
|
+
// @implements A-SPEC-580.1
|
|
9
|
+
const npx_bin_1 = require("../project/npx-bin");
|
|
10
|
+
const install_scripts_policy_1 = require("../project/install-scripts-policy");
|
|
11
|
+
const HOLMES_PKG = '@holmes-lab/holmes-kit';
|
|
12
|
+
/** Where this package lives — the layout decides which package.json (if any) holds the policy. */
|
|
13
|
+
function installKind(packageRoot, globalDir) {
|
|
14
|
+
const norm = packageRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
15
|
+
if (norm.split('/').includes('_npx'))
|
|
16
|
+
return 'npx';
|
|
17
|
+
if (globalDir) {
|
|
18
|
+
const g = globalDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
19
|
+
if (norm.toLowerCase().startsWith(`${g.toLowerCase()}/`))
|
|
20
|
+
return 'global';
|
|
21
|
+
}
|
|
22
|
+
if (/\/node_modules\/@holmes-lab\/holmes-kit$/.test(norm))
|
|
23
|
+
return 'local';
|
|
24
|
+
return 'repo';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The narrowest commands that repair each layout. `--allow-scripts=<pkg>` is a per-invocation flag
|
|
28
|
+
* scoped to ONE package — never `--dangerously-allow-all-scripts`, never a change to npm config.
|
|
29
|
+
*
|
|
30
|
+
* Global targets the DEPENDENCY, not the holmes-kit package: measured 2026-09-09 (npm 12.0.1,
|
|
31
|
+
* Windows), `npm rebuild -g @holmes-lab/holmes-kit` re-links the bin and dies EEXIST on the
|
|
32
|
+
* existing `holmes-kit` shim before any script runs, while `npm rebuild -g better-sqlite3
|
|
33
|
+
* --allow-scripts=better-sqlite3` runs `prebuild-install` in place. (It then needs the global
|
|
34
|
+
* prefix to be writable — a protected prefix fails EPERM there, which is the `global prefix`
|
|
35
|
+
* check's territory, not this one's.)
|
|
36
|
+
*/
|
|
37
|
+
function recoveryCommands(ev) {
|
|
38
|
+
const npm = (0, npx_bin_1.npmBin)(ev.platform);
|
|
39
|
+
const project = [
|
|
40
|
+
(0, install_scripts_policy_1.approveCommand)(ev.packageName, ev.packageVersion, npm),
|
|
41
|
+
`${npm} rebuild ${ev.packageName} --foreground-scripts`,
|
|
42
|
+
];
|
|
43
|
+
switch (ev.installKind) {
|
|
44
|
+
case 'global': return [`${npm} rebuild -g ${ev.packageName} --foreground-scripts --allow-scripts=${ev.packageName}`];
|
|
45
|
+
case 'npx': return [`${npm} install --save-dev ${HOLMES_PKG}`, ...project];
|
|
46
|
+
default: return project;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A rebuild alone (the script is approved or the policy is not the problem). */
|
|
50
|
+
function rebuildCommand(ev) {
|
|
51
|
+
const npm = (0, npx_bin_1.npmBin)(ev.platform);
|
|
52
|
+
return ev.installKind === 'global'
|
|
53
|
+
? `${npm} rebuild -g ${ev.packageName} --foreground-scripts --allow-scripts=${ev.packageName}`
|
|
54
|
+
: `${npm} rebuild ${ev.packageName} --foreground-scripts`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What a PowerShell execution policy means for the emitted commands. Only the two policies that
|
|
58
|
+
* refuse every unsigned local script block the `npm.ps1`/`npx.ps1` shims; the rest add nothing.
|
|
59
|
+
*/
|
|
60
|
+
function powershellPolicyNote(policy) {
|
|
61
|
+
if (policy === 'Restricted' || policy === 'AllSigned') {
|
|
62
|
+
return `PowerShell execution policy ${policy} blocks the npm.ps1/npx.ps1 shims — use npm.cmd/npx.cmd (the commands above already do).`;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
const ABI_RE = /NODE_MODULE_VERSION|compiled against a different Node\.js version/;
|
|
67
|
+
function nativeVerdict(ev) {
|
|
68
|
+
const pkg = `${ev.packageName}@${ev.packageVersion}`;
|
|
69
|
+
const isWin = ev.platform === 'win32';
|
|
70
|
+
const psNote = isWin ? powershellPolicyNote(ev.psPolicy) : '';
|
|
71
|
+
const withPs = (detail) => (psNote ? `${detail} ${psNote}` : detail);
|
|
72
|
+
const approved = ev.coverage === 'approved-pinned' || ev.coverage === 'approved-unpinned';
|
|
73
|
+
const policyHome = ev.installKind === 'repo' || ev.installKind === 'local'
|
|
74
|
+
? 'the project package.json (allowScripts)'
|
|
75
|
+
: `no package.json can carry the approval for a ${ev.installKind} install — approve per command instead`;
|
|
76
|
+
// 1. Loads → nothing to diagnose. The wording is read by other suites; keep it byte-identical.
|
|
77
|
+
if (ev.bindingPresent && !ev.loadError) {
|
|
78
|
+
return { cause: 'ok', level: 'PASS', detail: 'loads and executes against :memory:' };
|
|
79
|
+
}
|
|
80
|
+
// 2. The binary exists but was built for another Node ABI — the ONE case the old wording fit.
|
|
81
|
+
if (ev.loadError && ABI_RE.test(ev.loadError)) {
|
|
82
|
+
return {
|
|
83
|
+
cause: 'abi-mismatch', level: 'FAIL',
|
|
84
|
+
detail: withPs(`${pkg} was built for a different Node ABI (running ${process.version}): ${ev.loadError}`),
|
|
85
|
+
fix: `Rebuild against this Node: ${rebuildCommand(ev)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!ev.bindingPresent && !approved) {
|
|
89
|
+
// 3. npm ≥ 12 skips the script without an approval. That the approval is missing IS observed;
|
|
90
|
+
// an unobserved npm version does not change the remedy, so it is said and the same commands go out.
|
|
91
|
+
if (ev.npmMajor === undefined || ev.npmMajor >= 12) {
|
|
92
|
+
const npmSaid = ev.npmMajor === undefined ? 'npm (npm version not observed)' : `npm ${ev.npmMajor}`;
|
|
93
|
+
const denied = ev.coverage === 'denied' ? ` (allowScripts explicitly denied ${ev.packageName})` : '';
|
|
94
|
+
return {
|
|
95
|
+
cause: 'scripts-blocked', level: 'FAIL',
|
|
96
|
+
detail: withPs(`no binary — ${npmSaid} blocks dependency install scripts unless allowScripts covers ${pkg}${denied}; policy home: ${policyHome}.`),
|
|
97
|
+
fix: `Approve the one script that must run, then rebuild: ${recoveryCommands(ev).join(' && ')}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// 6b. Early npm 11 did not block scripts, so a missing approval proves nothing about why the
|
|
101
|
+
// script left no binary — say so instead of inventing a cause.
|
|
102
|
+
return {
|
|
103
|
+
cause: 'unknown', level: 'FAIL',
|
|
104
|
+
detail: withPs(`no binary — npm ${ev.npmMajor} may or may not have run the install script; rerun with --foreground-scripts to see what happened.`),
|
|
105
|
+
fix: `${rebuildCommand(ev)} — then run doctor again.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// 4. Approved, yet no binary: the script ran and produced nothing. What it could not do is
|
|
109
|
+
// partly observable (toolchain, path); a failed prebuild download is not.
|
|
110
|
+
if (!ev.bindingPresent) {
|
|
111
|
+
const missing = [];
|
|
112
|
+
if (ev.toolchain?.python === false)
|
|
113
|
+
missing.push('Python is not on PATH (node-gyp needs it)');
|
|
114
|
+
if (ev.toolchain?.msvc === false)
|
|
115
|
+
missing.push('Visual Studio C++ Build Tools (MSVC) were not found');
|
|
116
|
+
if (ev.pathHasSpace)
|
|
117
|
+
missing.push('the install path contains a space, which node-gyp is known to mishandle');
|
|
118
|
+
const because = missing.length ? ` Observed obstacles to a source build: ${missing.join('; ')}.` : '';
|
|
119
|
+
return {
|
|
120
|
+
cause: 'build-failed', level: 'FAIL',
|
|
121
|
+
detail: withPs(`the install script for ${pkg} is approved but produced no binary — a prebuilt download failure is not observable here.${because} Rerun with --foreground-scripts to see the script's own output.`),
|
|
122
|
+
fix: `${rebuildCommand(ev)} — install the missing toolchain (or use a Node version with a prebuilt binary) if the output shows a compile step.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// 6a. Present but failing to load for a reason we do not recognise — quote it, do not classify it.
|
|
126
|
+
return {
|
|
127
|
+
cause: 'unknown', level: 'FAIL',
|
|
128
|
+
detail: withPs(`${pkg} is present but failed to load: ${ev.loadError ?? '(no error text)'}`),
|
|
129
|
+
fix: `${rebuildCommand(ev)} — then run doctor again.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub target this project publishes to, read from `package.json`.
|
|
3
|
+
*
|
|
4
|
+
* Derived, never hardcoded: this playbook is installed INTO consuming projects, so a literal
|
|
5
|
+
* `owner/repo` here would make someone else's release overwrite ours. When the field is missing,
|
|
6
|
+
* malformed, or points somewhere other than GitHub the answer is `null` — the caller then SKIPS and
|
|
7
|
+
* says so, which is the honest outcome. A fallback would be a guess wearing a default's clothes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function repoTargetFrom(pkg: unknown): {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export interface ProfileDriftInput {
|
|
14
|
+
profileText: string;
|
|
15
|
+
/** The version live on the registry, or null when the lookup failed. */
|
|
16
|
+
npmVersion: string | null;
|
|
17
|
+
profilePushedAt: string | null;
|
|
18
|
+
lastReleaseAt: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sentences a person should read before the next release — never a rewrite, and never a grade.
|
|
22
|
+
*
|
|
23
|
+
* No severity, no score: attaching one is how an advisory becomes a gate, and the wording of a
|
|
24
|
+
* positioning document is not something a gate should decide. An empty list is a real answer, which
|
|
25
|
+
* is why the "current document" case is tested — a check that always speaks is not a check.
|
|
26
|
+
*/
|
|
27
|
+
export declare function profileDriftFindings(input: ProfileDriftInput): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-575.1
|
|
3
|
+
// Publishing reaches surfaces the release procedure never knew about. This repository has NO GitHub
|
|
4
|
+
// remote — origin is a local gitea — so three releases went out while the README that npm's
|
|
5
|
+
// `homepage` points at stayed frozen at 2026-08-31, and the org profile still said the project was
|
|
6
|
+
// a walking skeleton under construction.
|
|
7
|
+
//
|
|
8
|
+
// Two surfaces, two treatments. The repo README is a COPY of the local one (section-for-section
|
|
9
|
+
// identical, measured), so it is synchronised. The profile README is an independent positioning
|
|
10
|
+
// document, so it is only INSPECTED — rewriting someone's positioning is not a release step.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.repoTargetFrom = repoTargetFrom;
|
|
13
|
+
exports.profileDriftFindings = profileDriftFindings;
|
|
14
|
+
/**
|
|
15
|
+
* The GitHub target this project publishes to, read from `package.json`.
|
|
16
|
+
*
|
|
17
|
+
* Derived, never hardcoded: this playbook is installed INTO consuming projects, so a literal
|
|
18
|
+
* `owner/repo` here would make someone else's release overwrite ours. When the field is missing,
|
|
19
|
+
* malformed, or points somewhere other than GitHub the answer is `null` — the caller then SKIPS and
|
|
20
|
+
* says so, which is the honest outcome. A fallback would be a guess wearing a default's clothes.
|
|
21
|
+
*/
|
|
22
|
+
function repoTargetFrom(pkg) {
|
|
23
|
+
const repository = pkg?.repository;
|
|
24
|
+
const url = typeof repository === 'string' ? repository
|
|
25
|
+
: typeof repository?.url === 'string'
|
|
26
|
+
? repository.url
|
|
27
|
+
: null;
|
|
28
|
+
if (url === null)
|
|
29
|
+
return null;
|
|
30
|
+
// The three shapes npm accepts, plus the `git+` prefix and the optional `.git` suffix.
|
|
31
|
+
const m = /^(?:git\+)?(?:https?:\/\/github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim());
|
|
32
|
+
return m === null ? null : { owner: m[1], repo: m[2] };
|
|
33
|
+
}
|
|
34
|
+
/** Phrases that claim the project has not shipped yet. */
|
|
35
|
+
const PRE_RELEASE_CLAIMS = ['early stage', 'under construction', 'walking-skeleton', 'walking skeleton', 'bootstrap'];
|
|
36
|
+
/**
|
|
37
|
+
* Sentences a person should read before the next release — never a rewrite, and never a grade.
|
|
38
|
+
*
|
|
39
|
+
* No severity, no score: attaching one is how an advisory becomes a gate, and the wording of a
|
|
40
|
+
* positioning document is not something a gate should decide. An empty list is a real answer, which
|
|
41
|
+
* is why the "current document" case is tested — a check that always speaks is not a check.
|
|
42
|
+
*/
|
|
43
|
+
function profileDriftFindings(input) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const text = input.profileText.toLowerCase();
|
|
46
|
+
// Only run the maturity check when the registry actually answered. Not knowing must not turn into
|
|
47
|
+
// a claim about what the document gets wrong.
|
|
48
|
+
if (input.npmVersion !== null && isShipped(input.npmVersion)) {
|
|
49
|
+
const claimed = PRE_RELEASE_CLAIMS.filter((p) => text.includes(p));
|
|
50
|
+
if (claimed.length > 0) {
|
|
51
|
+
out.push(`profile README still claims pre-release status (${claimed.join(', ')}) `
|
|
52
|
+
+ `while npm has ${input.npmVersion} — the claim is no longer true.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (input.profilePushedAt !== null && input.lastReleaseAt !== null
|
|
56
|
+
&& Date.parse(input.profilePushedAt) < Date.parse(input.lastReleaseAt)) {
|
|
57
|
+
out.push(`profile README (${input.profilePushedAt}) is older than the last release `
|
|
58
|
+
+ `(${input.lastReleaseAt}) — it has not been looked at across at least one shipment.`);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** A version that means "this shipped" — anything at or past 0.1.0. */
|
|
63
|
+
function isShipped(version) {
|
|
64
|
+
const m = /^(\d+)\.(\d+)\./.exec(version);
|
|
65
|
+
if (m === null)
|
|
66
|
+
return false;
|
|
67
|
+
return Number(m[1]) > 0 || Number(m[2]) > 0;
|
|
68
|
+
}
|