@promptci/core 0.3.0 → 0.4.0
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/dist/action-pinning.d.ts.map +1 -1
- package/dist/action-pinning.js +86 -55
- package/dist/action-pinning.js.map +1 -1
- package/dist/agent-practices.d.ts +5 -1
- package/dist/agent-practices.d.ts.map +1 -1
- package/dist/agent-practices.js +59 -34
- package/dist/agent-practices.js.map +1 -1
- package/dist/baseline.d.ts +8 -0
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +15 -0
- package/dist/baseline.js.map +1 -1
- package/dist/ci-alignment.d.ts +3 -1
- package/dist/ci-alignment.d.ts.map +1 -1
- package/dist/ci-alignment.js +8 -1
- package/dist/ci-alignment.js.map +1 -1
- package/dist/command-context.d.ts +67 -0
- package/dist/command-context.d.ts.map +1 -0
- package/dist/command-context.js +291 -0
- package/dist/command-context.js.map +1 -0
- package/dist/command-validity.d.ts.map +1 -1
- package/dist/command-validity.js +404 -32
- package/dist/command-validity.js.map +1 -1
- package/dist/dead-references.d.ts +6 -0
- package/dist/dead-references.d.ts.map +1 -1
- package/dist/dead-references.js +120 -7
- package/dist/dead-references.js.map +1 -1
- package/dist/duplicates.d.ts +24 -2
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +313 -9
- package/dist/duplicates.js.map +1 -1
- package/dist/health-score.d.ts +15 -1
- package/dist/health-score.d.ts.map +1 -1
- package/dist/health-score.js +25 -2
- package/dist/health-score.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/manifest-consistency.d.ts.map +1 -1
- package/dist/manifest-consistency.js +6 -0
- package/dist/manifest-consistency.js.map +1 -1
- package/dist/markdown-fences.d.ts.map +1 -1
- package/dist/markdown-fences.js +7 -3
- package/dist/markdown-fences.js.map +1 -1
- package/dist/provenance/check.d.ts +40 -0
- package/dist/provenance/check.d.ts.map +1 -0
- package/dist/provenance/check.js +157 -0
- package/dist/provenance/check.js.map +1 -0
- package/dist/provenance/index.d.ts +13 -0
- package/dist/provenance/index.d.ts.map +1 -0
- package/dist/provenance/index.js +21 -0
- package/dist/provenance/index.js.map +1 -0
- package/dist/provenance/instruction-advice.d.ts +3 -0
- package/dist/provenance/instruction-advice.d.ts.map +1 -0
- package/dist/provenance/instruction-advice.js +570 -0
- package/dist/provenance/instruction-advice.js.map +1 -0
- package/dist/provenance/repo-checks.d.ts +3 -0
- package/dist/provenance/repo-checks.d.ts.map +1 -0
- package/dist/provenance/repo-checks.js +644 -0
- package/dist/provenance/repo-checks.js.map +1 -0
- package/dist/provenance/tool-config.d.ts +3 -0
- package/dist/provenance/tool-config.d.ts.map +1 -0
- package/dist/provenance/tool-config.js +480 -0
- package/dist/provenance/tool-config.js.map +1 -0
- package/dist/provenance/types.d.ts +116 -0
- package/dist/provenance/types.d.ts.map +1 -0
- package/dist/provenance/types.js +17 -0
- package/dist/provenance/types.js.map +1 -0
- package/dist/repo-context.d.ts.map +1 -1
- package/dist/repo-context.js +3 -6
- package/dist/repo-context.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +3 -0
- package/dist/report.js.map +1 -1
- package/dist/sample-text.d.ts +36 -0
- package/dist/sample-text.d.ts.map +1 -0
- package/dist/sample-text.js +72 -0
- package/dist/sample-text.js.map +1 -0
- package/dist/security-pack.d.ts.map +1 -1
- package/dist/security-pack.js +83 -0
- package/dist/security-pack.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/workspace-manifests.d.ts +6 -0
- package/dist/workspace-manifests.d.ts.map +1 -1
- package/dist/workspace-manifests.js +13 -8
- package/dist/workspace-manifests.js.map +1 -1
- package/package.json +1 -1
package/dist/command-validity.js
CHANGED
|
@@ -2,10 +2,12 @@ import * as path from 'node:path';
|
|
|
2
2
|
import * as crypto from 'node:crypto';
|
|
3
3
|
import { fencedBlocks, scanFencedLines } from './markdown-fences.js';
|
|
4
4
|
import { fileIdPath } from './finding-id.js';
|
|
5
|
-
import { anyWorkspaceHasScript, isKnownBinary, selectWorkspaces } from './workspace-manifests.js';
|
|
5
|
+
import { anyWorkspaceHasScript, hasScript, isKnownBinary, selectWorkspaces } from './workspace-manifests.js';
|
|
6
6
|
import { makefileDefines, readMakefile } from './makefile.js';
|
|
7
7
|
import { incompleteCheckIssue, isAbsoluteAnywhere, treeFor } from './repo-tree.js';
|
|
8
8
|
import { closestUniqueMatch, isMechanicalRename } from './close-match.js';
|
|
9
|
+
import { htmlCommentLines } from './sample-text.js';
|
|
10
|
+
import { commandContext, directoryTokens, lineStructure } from './command-context.js';
|
|
9
11
|
/**
|
|
10
12
|
* Command Validity Detector
|
|
11
13
|
*
|
|
@@ -28,6 +30,42 @@ import { closestUniqueMatch, isMechanicalRename } from './close-match.js';
|
|
|
28
30
|
* Findings are heuristic — cautious wording is intentional.
|
|
29
31
|
*/
|
|
30
32
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* pcic-1ik0.2: why a script rewrite must NOT be applied unseen, as a clause that
|
|
35
|
+
* completes "it may be what was meant, but ...": `undefined` only when every
|
|
36
|
+
* condition for an unattended edit holds. An edit lands without a person looking,
|
|
37
|
+
* on a repository that is not ours: a missed fix costs nothing, a wrong one lands
|
|
38
|
+
* in a stranger's repo. Heuristics over prose do not converge, so the rule is
|
|
39
|
+
* default-deny and structural first:
|
|
40
|
+
* - RULE 1: the repository is a single package (see {@link singlePackageRepo}), so
|
|
41
|
+
* there is exactly one manifest the command can mean, whatever the prose says;
|
|
42
|
+
* and the closest name is in that root manifest;
|
|
43
|
+
* - RULE 2: nothing cautionary or negating frames the command, in a deliberately
|
|
44
|
+
* wide scope (the whole heading chain, block, paragraph, enclosing list items,
|
|
45
|
+
* table, blockquote and `<details>` summary, and a trailing comment);
|
|
46
|
+
* - the command is not in a comment or an indented block, and runs from the root
|
|
47
|
+
* with no `cd` before it in its block and no `--cwd`/`--prefix`/`-C`.
|
|
48
|
+
*/
|
|
49
|
+
function editBlocker(cmd, lines, inComment, context, earlierCd, singlePackage, viaRoot) {
|
|
50
|
+
if (inComment)
|
|
51
|
+
return 'this command is inside an HTML comment';
|
|
52
|
+
if (!cmd.fenced && /^(?: {4}|\t)/.test(lines[cmd.line - 1] ?? ''))
|
|
53
|
+
return 'this command is in an indented block';
|
|
54
|
+
if (typeof cmd.cwd === 'string' && cmd.cwd !== '.' && cmd.cwd !== '')
|
|
55
|
+
return 'this command runs from another directory';
|
|
56
|
+
// `-C site`, `-Csite`, `--cwd site`, `--cwd=site`.
|
|
57
|
+
if (/\s(?:--cwd|--dir|--prefix)(?:\s|=)|\s-C/.test(cmd.text))
|
|
58
|
+
return 'this command runs from another directory';
|
|
59
|
+
if (earlierCd)
|
|
60
|
+
return 'an earlier line of its block changes directory';
|
|
61
|
+
if (context.cautionary)
|
|
62
|
+
return 'the text around this command reads as an example not to follow, or as a warning';
|
|
63
|
+
if (!singlePackage)
|
|
64
|
+
return 'this repository has more than one package, which makes the manifest the command runs against uncertain';
|
|
65
|
+
if (!viaRoot)
|
|
66
|
+
return 'the closest name is not in the root manifest';
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
31
69
|
/** Occurrences of a plain substring in one line of text (pcic-oonz). */
|
|
32
70
|
function countOnLine(line, needle) {
|
|
33
71
|
if (!needle)
|
|
@@ -112,12 +150,16 @@ function missingUnless(found, message) {
|
|
|
112
150
|
return null;
|
|
113
151
|
}
|
|
114
152
|
/**
|
|
115
|
-
* The name and scripts of a package.json; `null` when there is none to read
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
153
|
+
* The name and scripts of a package.json; `null` when there is none to read;
|
|
154
|
+
* `unreadable` when a directory on the way could not be listed, so whether the
|
|
155
|
+
* script exists is unknown; `corrupt` when the manifest is not a JSON object (the
|
|
156
|
+
* detector says so in one info finding when it relied on that manifest, and
|
|
157
|
+
* checks nothing against it); `unfetched` when a caller's inventory says the
|
|
158
|
+
* manifest is there but its bytes were never fetched (pcic-csf) — nothing failed,
|
|
159
|
+
* so callers stay silent rather than warn.
|
|
120
160
|
*/
|
|
161
|
+
/** A package.json larger than this is not read: a hostile or generated manifest must not stall a scan. */
|
|
162
|
+
const MAX_MANIFEST_BYTES = 512 * 1024;
|
|
121
163
|
function readPackageScripts(tree, packageJsonPath) {
|
|
122
164
|
// pcic-x1vl: looked up through the tree, so a manifest path that leaves the
|
|
123
165
|
// root (`cd` through a link out of the tree) is never touched, and a
|
|
@@ -132,18 +174,21 @@ function readPackageScripts(tree, packageJsonPath) {
|
|
|
132
174
|
// them missing is the false positive the inventory exists to remove. The two
|
|
133
175
|
// "cannot read" answers stay apart: `unreadable` is a real failure the scan
|
|
134
176
|
// should name, `unfetched` is nothing to name (see RepoTree.readText).
|
|
135
|
-
const read = tree.readText(packageJsonPath);
|
|
136
|
-
if (read.state === 'unreadable'
|
|
177
|
+
const read = tree.readText(packageJsonPath, { maxBytes: MAX_MANIFEST_BYTES });
|
|
178
|
+
if (read.state === 'unreadable')
|
|
137
179
|
return 'unreadable';
|
|
138
|
-
|
|
180
|
+
// Oversized is treated like listed-but-not-downloaded (pcic-1ik0.2): the scripts are unknown,
|
|
181
|
+
// not absent, and there is nothing a user could fix, so no finding and no warning.
|
|
182
|
+
if (read.state === 'unfetched' || read.state === 'too-large')
|
|
139
183
|
return 'unfetched';
|
|
140
184
|
if (read.state !== 'found')
|
|
141
185
|
return null;
|
|
142
186
|
try {
|
|
143
187
|
const raw = read.content;
|
|
144
188
|
const parsed = JSON.parse(raw);
|
|
189
|
+
// A manifest that is not a JSON object is corrupt, not absent.
|
|
145
190
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
146
|
-
return
|
|
191
|
+
return 'corrupt';
|
|
147
192
|
const scripts = typeof parsed.scripts === 'object' && parsed.scripts !== null && !Array.isArray(parsed.scripts)
|
|
148
193
|
? Object.fromEntries(Object.entries(parsed.scripts).filter((entry) => typeof entry[1] === 'string'))
|
|
149
194
|
: {};
|
|
@@ -153,8 +198,172 @@ function readPackageScripts(tree, packageJsonPath) {
|
|
|
153
198
|
};
|
|
154
199
|
}
|
|
155
200
|
catch {
|
|
156
|
-
|
|
201
|
+
// Corrupt JSON is not "no scripts": whether the script exists is unknown.
|
|
202
|
+
return 'corrupt';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A script token that is a glob or placeholder (`build*`, `test:<name>`,
|
|
207
|
+
* `$SCRIPT`, `${NAME}`, `%NAME%`), never a misspelled name.
|
|
208
|
+
*/
|
|
209
|
+
const SCRIPT_PATTERN_RE = /[*?<>{}[\]$`]|%\w+%/;
|
|
210
|
+
/** Directories walked looking for package.json files, and how deep: bounded so a huge repo cannot stall a scan. */
|
|
211
|
+
const MAX_MANIFEST_DIRS = 2000;
|
|
212
|
+
const MAX_MANIFEST_DEPTH = 12;
|
|
213
|
+
/** Manifests a finding relied on (named by the text, or the directory a `cd` entered) that turned out to be corrupt. */
|
|
214
|
+
const corruptUsed = new WeakMap();
|
|
215
|
+
function noteCorrupt(tree, manifest) {
|
|
216
|
+
let set = corruptUsed.get(tree);
|
|
217
|
+
if (!set)
|
|
218
|
+
corruptUsed.set(tree, (set = new Set()));
|
|
219
|
+
set.add(manifest);
|
|
220
|
+
}
|
|
221
|
+
const packageIndexCache = new WeakMap();
|
|
222
|
+
/**
|
|
223
|
+
* Scripts of every package.json under the root outside `.git` and `node_modules`
|
|
224
|
+
* (dot directories are walked: `.github/actions/release/package.json` is a package).
|
|
225
|
+
* Breadth-first and sorted, so the same repository always yields the same index;
|
|
226
|
+
* and it says when it is not the whole picture, so that a caller can tell "defined
|
|
227
|
+
* nowhere" from "not seen".
|
|
228
|
+
*/
|
|
229
|
+
function packageIndex(tree) {
|
|
230
|
+
const cached = packageIndexCache.get(tree);
|
|
231
|
+
if (cached)
|
|
232
|
+
return cached;
|
|
233
|
+
const index = { scripts: new Map(), unfetched: [], incomplete: false, foreign: false };
|
|
234
|
+
const queue = [{ dir: '', depth: 0 }];
|
|
235
|
+
for (let next = 0; next < queue.length; next++) {
|
|
236
|
+
if (next >= MAX_MANIFEST_DIRS) {
|
|
237
|
+
index.incomplete = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
const { dir, depth } = queue[next];
|
|
241
|
+
const abs = dir ? path.join(tree.root, dir) : tree.root;
|
|
242
|
+
const listing = tree.entries(abs);
|
|
243
|
+
if (listing === 'unreadable')
|
|
244
|
+
index.incomplete = true;
|
|
245
|
+
if (typeof listing === 'string')
|
|
246
|
+
continue;
|
|
247
|
+
const names = [...listing.keys()].sort();
|
|
248
|
+
if (names.includes('package.json')) {
|
|
249
|
+
const pkg = readPackageScripts(tree, path.join(abs, 'package.json'));
|
|
250
|
+
if (pkg === 'unfetched')
|
|
251
|
+
index.unfetched.push(dir || '.');
|
|
252
|
+
else if (pkg === 'unreadable' || pkg === 'corrupt')
|
|
253
|
+
index.incomplete = true;
|
|
254
|
+
else if (pkg)
|
|
255
|
+
index.scripts.set(dir || '.', pkg.scripts);
|
|
256
|
+
}
|
|
257
|
+
if (names.some((n) => FOREIGN_MANIFESTS.has(n)))
|
|
258
|
+
index.foreign = true;
|
|
259
|
+
for (const deno of ['deno.json', 'deno.jsonc']) {
|
|
260
|
+
if (!names.includes(deno))
|
|
261
|
+
continue;
|
|
262
|
+
const text = tree.readText(path.join(abs, deno), { maxBytes: MAX_MANIFEST_BYTES });
|
|
263
|
+
// Tasks are deno's scripts. A file that cannot be read may hold them.
|
|
264
|
+
if (text.state !== 'found' || /"tasks"\s*:/.test(text.content))
|
|
265
|
+
index.foreign = true;
|
|
266
|
+
}
|
|
267
|
+
for (const name of names) {
|
|
268
|
+
const entry = listing.get(name);
|
|
269
|
+
if (entry.isSymbolicLink()) {
|
|
270
|
+
// A link that leaves the repository points at content this scan never reads.
|
|
271
|
+
const target = tree.lookup(path.join(abs, name)).state;
|
|
272
|
+
if (target === 'outside')
|
|
273
|
+
index.foreign = true;
|
|
274
|
+
else if (target === 'unreadable')
|
|
275
|
+
index.incomplete = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!entry.isDirectory())
|
|
279
|
+
continue;
|
|
280
|
+
if (name === 'node_modules' || name === '.git')
|
|
281
|
+
continue;
|
|
282
|
+
// A directory below the depth bound could hold a manifest nobody read.
|
|
283
|
+
if (depth >= MAX_MANIFEST_DEPTH) {
|
|
284
|
+
index.incomplete = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
queue.push({ dir: dir ? `${dir}/${name}` : name, depth: depth + 1 });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
packageIndexCache.set(tree, index);
|
|
291
|
+
return index;
|
|
292
|
+
}
|
|
293
|
+
/** Repo-relative directories (never the root) of the packages that define `script`, sorted. */
|
|
294
|
+
function packagesDefining(context, tree, script) {
|
|
295
|
+
const dirs = new Set();
|
|
296
|
+
for (const ws of context.workspaces ?? [])
|
|
297
|
+
if (ws.dir !== '.' && hasScript(ws.scripts, script))
|
|
298
|
+
dirs.add(ws.dir);
|
|
299
|
+
for (const [dir, scripts] of packageIndex(tree).scripts) {
|
|
300
|
+
if (dir !== '.' && !dir.split('/').some((segment) => segment.startsWith('.')) && hasScript(scripts, script))
|
|
301
|
+
dirs.add(dir);
|
|
302
|
+
}
|
|
303
|
+
return [...dirs].sort();
|
|
304
|
+
}
|
|
305
|
+
/** Files at the repository root that declare a multi-package layout. */
|
|
306
|
+
const WORKSPACE_FILES = ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'rush.json', '.gitmodules'];
|
|
307
|
+
/** Manifests of other package managers: a repository holding one is not certainly a single npm package. */
|
|
308
|
+
const FOREIGN_MANIFESTS = new Set(['package.yaml', 'package.json5', 'rush.json']);
|
|
309
|
+
const singlePackageCache = new WeakMap();
|
|
310
|
+
/**
|
|
311
|
+
* pcic-1ik0.2 (RULE 1, structural): true only when the repository is certainly ONE
|
|
312
|
+
* package: the inventory is complete and fully fetched (no unread manifest, no
|
|
313
|
+
* unlisted directory, no partial caller inventory), the root package.json is the
|
|
314
|
+
* only package.json, and nothing declares workspaces (`workspaces` field,
|
|
315
|
+
* pnpm-workspace.yaml, lerna.json, nx.json, turbo.json). The unattended typo edit
|
|
316
|
+
* is offered only then: in any other repository which manifest a command runs
|
|
317
|
+
* against depends on prose, and prose heuristics do not converge.
|
|
318
|
+
*/
|
|
319
|
+
function singlePackageRepo(context, tree) {
|
|
320
|
+
const cached = singlePackageCache.get(tree);
|
|
321
|
+
if (cached !== undefined)
|
|
322
|
+
return cached;
|
|
323
|
+
const index = packageIndex(tree);
|
|
324
|
+
let single = !index.incomplete &&
|
|
325
|
+
!index.foreign &&
|
|
326
|
+
index.unfetched.length === 0 &&
|
|
327
|
+
index.scripts.size === 1 &&
|
|
328
|
+
index.scripts.has('.') &&
|
|
329
|
+
(tree.inventory === undefined || tree.inventory.complete);
|
|
330
|
+
if (single && /"workspaces"\s*:/.test(context.packageJson.raw ?? ''))
|
|
331
|
+
single = false;
|
|
332
|
+
if (single) {
|
|
333
|
+
for (const file of WORKSPACE_FILES) {
|
|
334
|
+
if (tree.lookup(path.join(tree.root, file)).state !== 'missing')
|
|
335
|
+
single = false;
|
|
336
|
+
}
|
|
157
337
|
}
|
|
338
|
+
singlePackageCache.set(tree, single);
|
|
339
|
+
return single;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The existing directories among the tokens the text around a command names,
|
|
343
|
+
* as repo-relative POSIX paths: resolved from the repository root and, failing
|
|
344
|
+
* that, from the instruction file's own directory. Whether a named directory
|
|
345
|
+
* defines the script is the caller's check, so a stray mention can only silence
|
|
346
|
+
* a finding when that directory really holds the script.
|
|
347
|
+
*/
|
|
348
|
+
function resolveHintDirs(tokens, fileDir, tree) {
|
|
349
|
+
const dirs = [];
|
|
350
|
+
const seen = new Set();
|
|
351
|
+
for (const token of tokens) {
|
|
352
|
+
for (const base of fileDir === '.' ? ['.'] : ['.', fileDir]) {
|
|
353
|
+
const abs = withinRoot(tree, tree.root, base === '.' ? token : `${base}/${token}`);
|
|
354
|
+
if (abs === null)
|
|
355
|
+
continue;
|
|
356
|
+
const rel = path.relative(tree.root, abs).split(path.sep).join('/');
|
|
357
|
+
if (!rel || seen.has(rel))
|
|
358
|
+
continue;
|
|
359
|
+
const found = tree.lookup(abs);
|
|
360
|
+
if (found.state !== 'found' || !found.isDir)
|
|
361
|
+
continue;
|
|
362
|
+
seen.add(rel);
|
|
363
|
+
dirs.push(rel);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return dirs;
|
|
158
367
|
}
|
|
159
368
|
function pnpmFilterValue(parts) {
|
|
160
369
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -216,7 +425,7 @@ function workspaceHasScript(context, tree, selector, scriptName) {
|
|
|
216
425
|
// `workspaces` globs, any layout). The path-based lookup below is only for
|
|
217
426
|
// contexts built without them.
|
|
218
427
|
if (context.workspaces) {
|
|
219
|
-
return selectWorkspaces(context, normalizedSelector).some((ws) =>
|
|
428
|
+
return selectWorkspaces(context, normalizedSelector).some((ws) => hasScript(ws.scripts, scriptName));
|
|
220
429
|
}
|
|
221
430
|
const { candidates, complete } = workspacePackageJsonCandidates(tree, normalizedSelector);
|
|
222
431
|
let unknown = !complete;
|
|
@@ -225,16 +434,16 @@ function workspaceHasScript(context, tree, selector, scriptName) {
|
|
|
225
434
|
const pkg = readPackageScripts(tree, candidate);
|
|
226
435
|
if (pkg === 'unreadable')
|
|
227
436
|
unknown = true;
|
|
228
|
-
if (pkg === 'unfetched')
|
|
437
|
+
if (pkg === 'unfetched' || pkg === 'corrupt')
|
|
229
438
|
silent = true;
|
|
230
|
-
if (!pkg || pkg === 'unreadable' || pkg === 'unfetched')
|
|
439
|
+
if (!pkg || pkg === 'unreadable' || pkg === 'unfetched' || pkg === 'corrupt')
|
|
231
440
|
continue;
|
|
232
441
|
const packageDir = path.dirname(candidate).replace(/\\/g, '/');
|
|
233
442
|
const relDir = path.relative(context.repoRoot, packageDir).replace(/\\/g, '/');
|
|
234
443
|
const selectorMatches = pkg.name === normalizedSelector ||
|
|
235
444
|
relDir === normalizedSelector ||
|
|
236
445
|
relDir.endsWith(`/${normalizedSelector}`);
|
|
237
|
-
if (selectorMatches &&
|
|
446
|
+
if (selectorMatches && hasScript(pkg.scripts, scriptName))
|
|
238
447
|
return true;
|
|
239
448
|
}
|
|
240
449
|
if (unknown)
|
|
@@ -316,6 +525,37 @@ function resolveCwd(current, target) {
|
|
|
316
525
|
}
|
|
317
526
|
return parts.length === 0 ? '.' : parts.join('/');
|
|
318
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* pcic-1ik0.2: what the earlier lines of the fenced block holding line `line` do to
|
|
530
|
+
* the working directory: whether any `cd`, `pushd` or `popd` came first, and the
|
|
531
|
+
* directory they leave (`null` when it cannot be known). The detector still judges
|
|
532
|
+
* each line from the root (CV6); this only feeds the text-names-a-directory logic
|
|
533
|
+
* and withholds edits.
|
|
534
|
+
*/
|
|
535
|
+
function earlierCd(s, line) {
|
|
536
|
+
const open = s.opener.get(line);
|
|
537
|
+
let dir = '.';
|
|
538
|
+
let any = false;
|
|
539
|
+
if (open === undefined)
|
|
540
|
+
return { any, dir };
|
|
541
|
+
for (let n = open + 1; n < line; n++) {
|
|
542
|
+
const text = (s.lines[n - 1] ?? '').trim().replace(/^\$\s*/, '');
|
|
543
|
+
if (!text || text.startsWith('#'))
|
|
544
|
+
continue;
|
|
545
|
+
for (const segment of splitOnAmpersand(text)) {
|
|
546
|
+
const change = /^(?:cd|pushd)\s+(.+)$/.exec(segment);
|
|
547
|
+
if (change) {
|
|
548
|
+
any = true;
|
|
549
|
+
dir = resolveCwd(dir, change[1].trim());
|
|
550
|
+
}
|
|
551
|
+
else if (/^(?:cd|pushd|popd)$/.test(segment) || segment.startsWith('popd ')) {
|
|
552
|
+
any = true;
|
|
553
|
+
dir = null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { any, dir };
|
|
558
|
+
}
|
|
319
559
|
/**
|
|
320
560
|
* Splits a command string on `&&`, `||`, `;`, and newlines, yielding
|
|
321
561
|
* individual segments. This allows a single `run:` block or backtick
|
|
@@ -330,9 +570,18 @@ function resolveCwd(current, target) {
|
|
|
330
570
|
function splitOnAmpersand(cmd) {
|
|
331
571
|
return cmd
|
|
332
572
|
.split(/&&|\|\||;|\n/)
|
|
333
|
-
.map((s) => s.trim())
|
|
573
|
+
.map((s) => unwrapSubshell(s.trim()))
|
|
334
574
|
.filter(Boolean);
|
|
335
575
|
}
|
|
576
|
+
/** `(cd site` and `npm run dev)`: the parentheses of a subshell belong to the shell, not to a word of the command. */
|
|
577
|
+
function unwrapSubshell(segment) {
|
|
578
|
+
let out = segment;
|
|
579
|
+
while (out.startsWith('('))
|
|
580
|
+
out = out.slice(1).trim();
|
|
581
|
+
while (out.endsWith(')') && (out.match(/\)/g)?.length ?? 0) > (out.match(/\(/g)?.length ?? 0))
|
|
582
|
+
out = out.slice(0, -1).trim();
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
336
585
|
/** Returns true if a text looks like it could be a shell command to validate. */
|
|
337
586
|
function isPotentialCommand(text) {
|
|
338
587
|
const KNOWN_PREFIXES = [
|
|
@@ -521,7 +770,7 @@ function cwdOverride(parts) {
|
|
|
521
770
|
}
|
|
522
771
|
return undefined;
|
|
523
772
|
}
|
|
524
|
-
function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool, lookup, onScript, suggest) {
|
|
773
|
+
function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool, lookup, onScript, suggest, scope) {
|
|
525
774
|
const names = scriptName.includes('/')
|
|
526
775
|
? scriptName.split('/').map((s) => s.trim()).filter(Boolean)
|
|
527
776
|
: [scriptName];
|
|
@@ -559,12 +808,12 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
559
808
|
* without discovered workspaces to resolve it against — the candidate pool
|
|
560
809
|
* would then be the root manifest, which is not where the script would run.
|
|
561
810
|
*/
|
|
562
|
-
const propose = (name, pool) => {
|
|
811
|
+
const propose = (name, pool, viaRoot = false) => {
|
|
563
812
|
if (!suggest || names.length !== 1)
|
|
564
813
|
return;
|
|
565
814
|
const candidate = closestUniqueMatch(name, pool);
|
|
566
815
|
if (candidate !== undefined) {
|
|
567
|
-
suggest({ missing: name, candidate, mechanical: isMechanicalRename(name, candidate) });
|
|
816
|
+
suggest({ missing: name, candidate, mechanical: isMechanicalRename(name, candidate), viaRoot });
|
|
568
817
|
}
|
|
569
818
|
};
|
|
570
819
|
const scopedToWorkspaces = selector !== undefined || allWorkspaces;
|
|
@@ -577,6 +826,11 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
577
826
|
// warning here would name zero locations and tell the reader nothing.
|
|
578
827
|
if (dirPkg === 'unfetched')
|
|
579
828
|
return null;
|
|
829
|
+
// A corrupt manifest in the directory the command names: nothing to judge it against.
|
|
830
|
+
if (dirPkg === 'corrupt') {
|
|
831
|
+
noteCorrupt(tree, path.join(tree.root, cwd, 'package.json'));
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
580
834
|
// pcic-x1vl: a directory on the way could not be listed — say so, don't guess.
|
|
581
835
|
if (dirPkg === 'unreadable')
|
|
582
836
|
return UNJUDGED;
|
|
@@ -587,6 +841,7 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
587
841
|
// No manifest we can check anywhere — stay silent.
|
|
588
842
|
if (!dirPkg && !rootScriptsLoaded)
|
|
589
843
|
return null;
|
|
844
|
+
let pendingElsewhere;
|
|
590
845
|
for (const name of names) {
|
|
591
846
|
if (builtins.has(name))
|
|
592
847
|
continue;
|
|
@@ -600,7 +855,7 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
600
855
|
continue; // only a sample of the workspace was read
|
|
601
856
|
return `no workspace matches "${selector}" for ${tool} script "${name}"`;
|
|
602
857
|
}
|
|
603
|
-
if (selectedWorkspaces.some((ws) =>
|
|
858
|
+
if (selectedWorkspaces.some((ws) => hasScript(ws.scripts, name)))
|
|
604
859
|
continue;
|
|
605
860
|
const target = selectedWorkspaces.length === 1
|
|
606
861
|
? `${selectedWorkspaces[0].name ?? selectedWorkspaces[0].dir} (${selectedWorkspaces[0].dir}/package.json)`
|
|
@@ -620,7 +875,7 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
620
875
|
// from an explicit --filter workspace target. A same-named root script is
|
|
621
876
|
// NOT reachable here, so it must not mask a genuinely missing subdir
|
|
622
877
|
// script.
|
|
623
|
-
if (dirPkg.scripts
|
|
878
|
+
if (hasScript(dirPkg.scripts, name))
|
|
624
879
|
continue;
|
|
625
880
|
const inWorkspace = inWorkspaceScope(name);
|
|
626
881
|
if (inWorkspace === true || inWorkspace === 'silent')
|
|
@@ -635,15 +890,52 @@ function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool
|
|
|
635
890
|
const inWorkspace = inWorkspaceScope(name);
|
|
636
891
|
if (inWorkspace === true || inWorkspace === 'silent')
|
|
637
892
|
continue;
|
|
638
|
-
if (context.packageJson.scripts
|
|
893
|
+
if (hasScript(context.packageJson.scripts, name))
|
|
639
894
|
continue;
|
|
640
895
|
// pcic-x1vl: a selected workspace may define it in a directory that could not be read.
|
|
641
896
|
if (inWorkspace === 'unknown')
|
|
642
897
|
return UNJUDGED;
|
|
643
|
-
if (!scopedToWorkspaces)
|
|
644
|
-
|
|
898
|
+
if (!scopedToWorkspaces) {
|
|
899
|
+
// pcic-1ik0.2: the root manifest is not the only place a documented
|
|
900
|
+
// script can live. A directory the text names that defines it settles the
|
|
901
|
+
// command; a package elsewhere that defines it turns the finding into a
|
|
902
|
+
// note and withdraws the typo suggestion (the name is not misspelled).
|
|
903
|
+
//
|
|
904
|
+
// Each name of a slash-joined list is judged on its own: one that is
|
|
905
|
+
// settled or only elsewhere must not hide a sibling that is missing.
|
|
906
|
+
// Text that says "at the repo root" is an explicit root: no hint applies.
|
|
907
|
+
let settled = false;
|
|
908
|
+
if (!scope?.explicitRoot) {
|
|
909
|
+
for (const dir of scope?.hintDirs?.() ?? []) {
|
|
910
|
+
const pkg = readPackageScripts(tree, path.join(tree.root, dir, 'package.json'));
|
|
911
|
+
// Listed by a caller's inventory but never fetched: the script may be there. Say nothing.
|
|
912
|
+
if (pkg === 'unfetched')
|
|
913
|
+
settled = true;
|
|
914
|
+
else if (pkg === 'corrupt') {
|
|
915
|
+
noteCorrupt(tree, path.join(tree.root, dir, 'package.json'));
|
|
916
|
+
settled = true;
|
|
917
|
+
}
|
|
918
|
+
else if (pkg === 'unreadable')
|
|
919
|
+
return UNJUDGED;
|
|
920
|
+
else if (pkg && hasScript(pkg.scripts, name))
|
|
921
|
+
settled = true;
|
|
922
|
+
}
|
|
923
|
+
if (settled)
|
|
924
|
+
continue;
|
|
925
|
+
const elsewhere = packagesDefining(context, tree, name);
|
|
926
|
+
if (elsewhere.length > 0) {
|
|
927
|
+
pendingElsewhere ??= { name, dirs: elsewhere };
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
propose(name, Object.keys(context.packageJson.scripts), true);
|
|
932
|
+
}
|
|
645
933
|
return `${tool} script "${name}" does not appear in package.json scripts`;
|
|
646
934
|
}
|
|
935
|
+
if (pendingElsewhere) {
|
|
936
|
+
scope?.elsewhere?.(pendingElsewhere.name, pendingElsewhere.dirs);
|
|
937
|
+
return `${tool} script "${pendingElsewhere.name}" does not appear in package.json scripts`;
|
|
938
|
+
}
|
|
647
939
|
return null;
|
|
648
940
|
}
|
|
649
941
|
const BUILTINS_BY_TOOL = {
|
|
@@ -661,7 +953,7 @@ const BUILTINS_BY_TOOL = {
|
|
|
661
953
|
* to an installed binary), `yarn workspace <name> <script>`, and workspace or
|
|
662
954
|
* directory selectors.
|
|
663
955
|
*/
|
|
664
|
-
function validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, suggest) {
|
|
956
|
+
function validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, suggest, scope) {
|
|
665
957
|
const subIdx = firstNonFlagArgIndex(parts);
|
|
666
958
|
const sub = parts[subIdx];
|
|
667
959
|
if (!sub)
|
|
@@ -687,6 +979,14 @@ function validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir,
|
|
|
687
979
|
const scriptName = parts[nameIdx];
|
|
688
980
|
if (!scriptName || scriptName.startsWith('-'))
|
|
689
981
|
return null;
|
|
982
|
+
// pcic-qc33: `pnpm build*` means every script that starts with `build`, and
|
|
983
|
+
// `npm run test:<name>` is a template. A pattern is never a misspelling, so
|
|
984
|
+
// there is nothing to report and nothing to rewrite. Recorded as judged so
|
|
985
|
+
// manifest-consistency does not read the same token as a missing script.
|
|
986
|
+
if (SCRIPT_PATTERN_RE.test(scriptName)) {
|
|
987
|
+
onScript?.(scriptName);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
690
990
|
if (tool === 'bun') {
|
|
691
991
|
// `bun <x>` / `bun run <x>` also run files — a path is not a script name.
|
|
692
992
|
if (!explicitRun && BUN_BUILTINS.has(scriptName))
|
|
@@ -703,17 +1003,18 @@ function validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir,
|
|
|
703
1003
|
// (`npm run bulid`, `pnpm --filter web lnit`) rather than a bare word.
|
|
704
1004
|
const prefix = parts.slice(0, nameIdx).join(' ');
|
|
705
1005
|
const onSuggestion = suggest
|
|
706
|
-
? ({ missing, candidate, mechanical }) => suggest({
|
|
1006
|
+
? ({ missing, candidate, mechanical, viaRoot }) => suggest({
|
|
707
1007
|
find: prefix ? `${prefix} ${missing}` : missing,
|
|
708
1008
|
replace: prefix ? `${prefix} ${candidate}` : candidate,
|
|
709
1009
|
missing,
|
|
710
1010
|
candidate,
|
|
711
1011
|
mechanical,
|
|
1012
|
+
viaRoot,
|
|
712
1013
|
})
|
|
713
1014
|
: undefined;
|
|
714
1015
|
return findMissingScript(context, tree, cwd, parts, scriptName, BUILTINS_BY_TOOL[tool], tool,
|
|
715
1016
|
// npm/pnpm `run` only run scripts; every other form can fall through to a binary.
|
|
716
|
-
{ allowBinary: !(explicitRun && (tool === 'npm' || tool === 'pnpm')), workspaceName }, onScript, onSuggestion);
|
|
1017
|
+
{ allowBinary: !(explicitRun && (tool === 'npm' || tool === 'pnpm')), workspaceName }, onScript, onSuggestion, scope);
|
|
717
1018
|
}
|
|
718
1019
|
// ── make targets ──────────────────────────────────────────────────────────────
|
|
719
1020
|
/** make options whose value is the NEXT token (unless attached: `-Cdir`, `-j4`). */
|
|
@@ -818,7 +1119,7 @@ function validateSegment(seg, context, cwd = '.', onScript, opts = {}) {
|
|
|
818
1119
|
: tree.root;
|
|
819
1120
|
// ── Package manager scripts ────────────────────────────────────────────────
|
|
820
1121
|
if (tool === 'pnpm' || tool === 'npm' || tool === 'yarn' || tool === 'bun') {
|
|
821
|
-
return validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, opts.suggestScript);
|
|
1122
|
+
return validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, opts.suggestScript, opts.scope);
|
|
822
1123
|
}
|
|
823
1124
|
// ── File-executing commands ────────────────────────────────────────────────
|
|
824
1125
|
const FILE_EXECUTORS = ['node', 'tsx', 'ts-node', 'bash', 'sh', 'python', 'python3'];
|
|
@@ -937,8 +1238,11 @@ function computeCommandAnalysis(context) {
|
|
|
937
1238
|
// pcic-oonz: the file's own lines, to confirm a rewrite's `find` text is
|
|
938
1239
|
// unambiguous there before the finding claims to be auto-applicable.
|
|
939
1240
|
const contentLines = file.content.split(/\r?\n/);
|
|
1241
|
+
const commentLines = htmlCommentLines(file.content);
|
|
1242
|
+
const structure = lineStructure(file.content);
|
|
940
1243
|
const evaluated = new Set();
|
|
941
1244
|
const fileDir = path.relative(context.repoRoot, path.dirname(file.path)).replace(/\\/g, '/') || '.';
|
|
1245
|
+
const nearDir = fileDir.startsWith('..') ? '.' : fileDir;
|
|
942
1246
|
evaluatedScripts.set(file.path, evaluated);
|
|
943
1247
|
for (const cmd of commands) {
|
|
944
1248
|
// pcic-2b6.13: `cmd.cwd ?? '.'` used to turn an unknown directory (null,
|
|
@@ -948,15 +1252,37 @@ function computeCommandAnalysis(context) {
|
|
|
948
1252
|
// unverifiable.
|
|
949
1253
|
const cwd = cmd.cwd === undefined ? '.' : cmd.cwd;
|
|
950
1254
|
let rewrite;
|
|
1255
|
+
let elsewhere;
|
|
1256
|
+
// pcic-1ik0.2: what the text around the command says, read only when it is needed.
|
|
1257
|
+
const namesDirectory = (text) => resolveHintDirs(directoryTokens([text]), nearDir, tree).length > 0;
|
|
1258
|
+
let framing;
|
|
1259
|
+
const framingOf = () => (framing ??= commandContext(structure, cmd.line, cmd.text, namesDirectory));
|
|
1260
|
+
const blockCd = earlierCd(structure, cmd.line);
|
|
1261
|
+
const blockDirs = () => blockCd.dir !== null && blockCd.dir !== '.' ? resolveHintDirs([blockCd.dir], nearDir, tree) : [];
|
|
1262
|
+
let hints;
|
|
1263
|
+
const hintDirs = () => (hints ??=
|
|
1264
|
+
cwd === '.'
|
|
1265
|
+
? [...new Set([...blockDirs(), ...resolveHintDirs(directoryTokens(framingOf().text), nearDir, tree)])]
|
|
1266
|
+
: []);
|
|
951
1267
|
const error = validateSegment(cmd.text, context, cwd, (name) => evaluated.add(`${cmd.line}:${name}`), {
|
|
952
1268
|
fenced: cmd.fenced === true,
|
|
953
1269
|
makefileFor,
|
|
954
|
-
fileDir:
|
|
1270
|
+
fileDir: nearDir,
|
|
955
1271
|
tree,
|
|
956
1272
|
suggestScript: (r) => { rewrite = r; },
|
|
1273
|
+
scope: {
|
|
1274
|
+
// Only a command run from the repo root is placed by the text around it.
|
|
1275
|
+
hintDirs,
|
|
1276
|
+
get explicitRoot() { return cwd === '.' && framingOf().explicitRoot; },
|
|
1277
|
+
elsewhere: (script, dirs) => { elsewhere = { script, dirs }; },
|
|
1278
|
+
},
|
|
957
1279
|
});
|
|
958
1280
|
if (!error)
|
|
959
1281
|
continue;
|
|
1282
|
+
// A command in an HTML comment is text no reader sees: judged (so no other
|
|
1283
|
+
// detector re-reads it), never reported.
|
|
1284
|
+
if (commentLines[cmd.line - 1] === true)
|
|
1285
|
+
continue;
|
|
960
1286
|
if (error === UNJUDGED) {
|
|
961
1287
|
const evidence = `Not checked: \`${cmd.text}\``;
|
|
962
1288
|
const entry = unchecked.get(evidence) ?? { filePaths: new Set(), fileLines: new Map(), evidence };
|
|
@@ -978,16 +1304,45 @@ function computeCommandAnalysis(context) {
|
|
|
978
1304
|
// (`isMechanicalRename`; `test:api` vs `test:app` is not), and the text
|
|
979
1305
|
// to replace appears exactly once on the line, so the edit cannot land on
|
|
980
1306
|
// another command that shares it.
|
|
981
|
-
|
|
982
|
-
|
|
1307
|
+
//
|
|
1308
|
+
// Only where the edit cannot change what the sentence means (editBlocker):
|
|
1309
|
+
// an edit is applied unseen in a fix PR on someone else's repository.
|
|
1310
|
+
const mechanical = rewrite?.mechanical === true && countOnLine(contentLines[cmd.line - 1] ?? '', rewrite.find) === 1;
|
|
1311
|
+
const editContext = mechanical
|
|
1312
|
+
? editBlocker(cmd, contentLines, commentLines[cmd.line - 1] === true, framingOf(), blockCd.any, singlePackageRepo(context, tree), rewrite?.viaRoot === true)
|
|
983
1313
|
: undefined;
|
|
1314
|
+
const fix = mechanical && editContext === undefined ? rewrite : undefined;
|
|
984
1315
|
// A candidate too close to a different script to apply is still worth
|
|
985
1316
|
// naming — cautiously, and in the recommendation rather than the summary,
|
|
986
1317
|
// so nothing reads as a verdict.
|
|
987
1318
|
const hint = rewrite && !fix
|
|
988
1319
|
? ` The manifest defines "${rewrite.candidate}", which is close to "${rewrite.missing}"; ` +
|
|
989
|
-
|
|
1320
|
+
(editContext
|
|
1321
|
+
? `it may be what was meant, but ${editContext}, so it was left unedited; check before changing it.`
|
|
1322
|
+
: 'it may be what was meant, but it is near enough to be a different script, so check before changing it.')
|
|
990
1323
|
: '';
|
|
1324
|
+
if (elsewhere) {
|
|
1325
|
+
// pcic-1ik0.2: the script exists, in a package the text does not point at.
|
|
1326
|
+
const named = elsewhere.dirs.slice(0, 3).map((d) => `${d}/package.json`);
|
|
1327
|
+
const more = elsewhere.dirs.length - named.length;
|
|
1328
|
+
const where = `${named.join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
|
|
1329
|
+
const dir = elsewhere.dirs[0];
|
|
1330
|
+
issues.push({
|
|
1331
|
+
id: issueId(fileIdPath(file), cmd.text),
|
|
1332
|
+
severity: 'info',
|
|
1333
|
+
category: 'command_validity',
|
|
1334
|
+
title: `Command may need a working directory: \`${cmd.text.split(/\s+/).slice(0, 3).join(' ')}\``,
|
|
1335
|
+
summary: `${error}, but ${where} defines "${elsewhere.script}". ` +
|
|
1336
|
+
'Nothing near the command names that directory, so it may be meant to run from there.',
|
|
1337
|
+
filePaths: [file.path],
|
|
1338
|
+
locations: [{ filePath: file.path, startLine: cmd.line, endLine: cmd.line }],
|
|
1339
|
+
evidence: [`Command: \`${cmd.text}\``, `In file: ${file.path}`, `Defined in: ${where}`],
|
|
1340
|
+
recommendation: `If the command runs from \`${dir}/\`, say so next to it (for example "from \`${dir}/\`", or \`cd ${dir} && ${cmd.text}\`) ` +
|
|
1341
|
+
'so a reader or an agent runs it in the right place. If the script was renamed or removed, update the instruction file.',
|
|
1342
|
+
confidence: 0.6,
|
|
1343
|
+
});
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
991
1346
|
issues.push({
|
|
992
1347
|
id: issueId(fileIdPath(file), cmd.text),
|
|
993
1348
|
severity: 'warning',
|
|
@@ -1014,6 +1369,23 @@ function computeCommandAnalysis(context) {
|
|
|
1014
1369
|
});
|
|
1015
1370
|
}
|
|
1016
1371
|
}
|
|
1372
|
+
// One info finding per manifest the check relied on and could not parse: about the manifest, not
|
|
1373
|
+
// about anyone's instructions, so it never carries the weight of a warning.
|
|
1374
|
+
for (const manifest of [...(corruptUsed.get(tree) ?? [])].sort()) {
|
|
1375
|
+
const rel = path.relative(tree.root, manifest).split(path.sep).join('/');
|
|
1376
|
+
issues.push({
|
|
1377
|
+
id: issueId('', `(unparsable ${rel})`),
|
|
1378
|
+
severity: 'info',
|
|
1379
|
+
category: 'command_validity',
|
|
1380
|
+
title: `Could not parse ${rel}`,
|
|
1381
|
+
summary: `${rel} is not a valid JSON object, so the commands that depend on its scripts were not checked.`,
|
|
1382
|
+
filePaths: [manifest],
|
|
1383
|
+
locations: [{ filePath: manifest }],
|
|
1384
|
+
evidence: [`File: ${rel}`],
|
|
1385
|
+
recommendation: `Fix the syntax of ${rel}; commands documented to run there are checked once it parses.`,
|
|
1386
|
+
confidence: 0.9,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1017
1389
|
if (unchecked.size > 0) {
|
|
1018
1390
|
issues.push(incompleteCheckIssue(tree, [...unchecked.values()], {
|
|
1019
1391
|
id: issueId('', '(command check incomplete)'),
|