@codyswann/lisa 3.13.1 → 3.13.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +6 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/check-conflict-markers.mjs +312 -0
- package/scripts/plugin-routing-validate.mjs +55 -14
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic gate against leftover merge-conflict markers in tracked files
|
|
4
|
+
* (issue #2552).
|
|
5
|
+
*
|
|
6
|
+
* PR #2548 committed literal `<<<<<<< HEAD` conflict blocks into six generated
|
|
7
|
+
* parity `SKILL.md` files and passed every gate on the way in:
|
|
8
|
+
*
|
|
9
|
+
* - the parity push gate greps for the pin *value*, which was present inside
|
|
10
|
+
* the conflict block, so it read as current;
|
|
11
|
+
* - the test suite parses skill frontmatter, never the body;
|
|
12
|
+
* - `🧩 Plugin artifacts match source` compares the generated copies against
|
|
13
|
+
* `plugins/src`, and both sides were broken the *same* way, so they matched.
|
|
14
|
+
*
|
|
15
|
+
* Nothing in the loop read the bytes. This script does, and only that.
|
|
16
|
+
*
|
|
17
|
+
* Determinism guarantees (so the unit test is reproducible and CI is stable):
|
|
18
|
+
* - zero third-party dependencies (Node built-ins only),
|
|
19
|
+
* - no network access,
|
|
20
|
+
* - no `Date` / `Math.random`,
|
|
21
|
+
* - the file list comes from `git ls-files`, so the gate sees exactly what a
|
|
22
|
+
* push would carry — an untracked scratch file can never block a push.
|
|
23
|
+
*
|
|
24
|
+
* Detection deliberately requires the COMPLETE ordered marker triple
|
|
25
|
+
* (`<<<<<<<` … `=======` … `>>>>>>>`), which is what git actually writes.
|
|
26
|
+
* Content between the start and the separator is ignored, so the diff3 base
|
|
27
|
+
* marker (`|||||||`) is accepted. A lone `<<<<<<<` line is NOT reported:
|
|
28
|
+
* documentation that quotes one side of a conflict is legitimate content, and a
|
|
29
|
+
* gate that fires on files it should not read is its own outage.
|
|
30
|
+
*
|
|
31
|
+
* CLI:
|
|
32
|
+
* node scripts/check-conflict-markers.mjs [--root <dir>] [--json]
|
|
33
|
+
*
|
|
34
|
+
* Exit codes (mirroring the sibling parity scripts):
|
|
35
|
+
* 0 — no tracked file carries a conflict block.
|
|
36
|
+
* 1 — ≥1 tracked file carries a conflict block.
|
|
37
|
+
* 2 — operational/usage error: unknown flag, a flag missing its value,
|
|
38
|
+
* `--root` absent or not a git repository, or git being unavailable.
|
|
39
|
+
*
|
|
40
|
+
* @module scripts/check-conflict-markers
|
|
41
|
+
*/
|
|
42
|
+
import { execFileSync } from "node:child_process";
|
|
43
|
+
import fs from "node:fs";
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
import process from "node:process";
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
47
|
+
|
|
48
|
+
const REPO_ROOT = path.resolve(
|
|
49
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
50
|
+
".."
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** `<<<<<<<` alone, or followed by whitespace and a label (`<<<<<<< HEAD`). */
|
|
54
|
+
const START_RE = /^<{7}(?:[ \t].*)?$/;
|
|
55
|
+
|
|
56
|
+
/** The separator git writes between the two sides: exactly seven `=`. */
|
|
57
|
+
const SEPARATOR_RE = /^={7}$/;
|
|
58
|
+
|
|
59
|
+
/** `>>>>>>>` alone, or followed by whitespace and a label. */
|
|
60
|
+
const END_RE = /^>{7}(?:[ \t].*)?$/;
|
|
61
|
+
|
|
62
|
+
/** Files larger than this are skipped — no source file is this big. */
|
|
63
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
/** Leading bytes probed for a NUL, which marks the file as binary. */
|
|
66
|
+
const BINARY_PROBE_BYTES = 8000;
|
|
67
|
+
|
|
68
|
+
/** Max bytes of `git ls-files` output (7k+ tracked paths is ~0.3 MB today). */
|
|
69
|
+
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Usage error — thrown by `parseArgs` / `listTrackedFiles` for an invalid
|
|
73
|
+
* invocation so `main` can distinguish it (exit 2) from a finding (exit 1).
|
|
74
|
+
*/
|
|
75
|
+
export class UsageError extends Error {}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find every complete conflict block in `content`.
|
|
79
|
+
*
|
|
80
|
+
* A block is a `<<<<<<<` line, then a `=======` line, then a `>>>>>>>` line, in
|
|
81
|
+
* that order. A second `<<<<<<<` abandons any partial block and starts a new
|
|
82
|
+
* one, matching how git nests nothing and always re-opens.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} content - full text of a file.
|
|
85
|
+
* @returns {{ startLine: number, separatorLine: number, endLine: number }[]}
|
|
86
|
+
* one entry per complete block, with 1-based line numbers.
|
|
87
|
+
*/
|
|
88
|
+
export function findConflictBlocks(content) {
|
|
89
|
+
const lines = String(content).split(/\r?\n/);
|
|
90
|
+
const blocks = [];
|
|
91
|
+
let startLine = -1;
|
|
92
|
+
let separatorLine = -1;
|
|
93
|
+
for (let index = 0; index < lines.length; index++) {
|
|
94
|
+
const line = lines[index];
|
|
95
|
+
if (START_RE.test(line)) {
|
|
96
|
+
startLine = index + 1;
|
|
97
|
+
separatorLine = -1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (startLine === -1) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (SEPARATOR_RE.test(line)) {
|
|
104
|
+
if (separatorLine === -1) {
|
|
105
|
+
separatorLine = index + 1;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (END_RE.test(line)) {
|
|
110
|
+
if (separatorLine !== -1) {
|
|
111
|
+
blocks.push({ endLine: index + 1, separatorLine, startLine });
|
|
112
|
+
}
|
|
113
|
+
startLine = -1;
|
|
114
|
+
separatorLine = -1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return blocks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* True iff `buffer` looks binary (a NUL byte in its leading bytes) — the same
|
|
122
|
+
* heuristic git uses to decide a blob is not text.
|
|
123
|
+
*
|
|
124
|
+
* @param {Buffer} buffer - the file contents.
|
|
125
|
+
* @returns {boolean} whether the file should be skipped as binary.
|
|
126
|
+
*/
|
|
127
|
+
function isBinary(buffer) {
|
|
128
|
+
return (
|
|
129
|
+
buffer.indexOf(0, 0) !== -1 && buffer.indexOf(0, 0) < BINARY_PROBE_BYTES
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List every tracked file in `root`, relative to it. Throws `UsageError` when
|
|
135
|
+
* git is unavailable or `root` is not a repository.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} root - the repository root.
|
|
138
|
+
* @returns {string[]} tracked paths, relative to `root`.
|
|
139
|
+
*/
|
|
140
|
+
function listTrackedFiles(root) {
|
|
141
|
+
let stdout;
|
|
142
|
+
try {
|
|
143
|
+
stdout = execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new UsageError(
|
|
150
|
+
`could not list tracked files in ${root}: ${error.message}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return stdout.split("\0").filter(entry => entry !== "");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Scan one tracked file. Missing (staged-deleted), oversized, and binary files
|
|
158
|
+
* are skipped rather than reported.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} root - the repository root.
|
|
161
|
+
* @param {string} file - a tracked path relative to `root`.
|
|
162
|
+
* @returns {{ file: string, blocks: ReadonlyArray<Record<string, number>> } | null}
|
|
163
|
+
* the finding, or `null` when the file is clean or skipped.
|
|
164
|
+
*/
|
|
165
|
+
function scanFile(root, file) {
|
|
166
|
+
const absolute = path.join(root, file);
|
|
167
|
+
let stat;
|
|
168
|
+
try {
|
|
169
|
+
stat = fs.statSync(absolute);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
let buffer;
|
|
177
|
+
try {
|
|
178
|
+
buffer = fs.readFileSync(absolute);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (isBinary(buffer)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const blocks = findConflictBlocks(buffer.toString("utf8"));
|
|
186
|
+
return blocks.length === 0 ? null : { blocks, file };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Assemble the machine-readable report.
|
|
191
|
+
*
|
|
192
|
+
* @param {ReadonlyArray<{ file: string, blocks: ReadonlyArray<Record<string, number>> }>} results
|
|
193
|
+
* the conflicted files, sorted by path.
|
|
194
|
+
* @param {{ root: string, scanned: number }} opts - resolved options + scan size.
|
|
195
|
+
* @returns {Record<string, unknown>} the report object.
|
|
196
|
+
*/
|
|
197
|
+
export function buildReport(results, opts) {
|
|
198
|
+
return {
|
|
199
|
+
results,
|
|
200
|
+
root: opts.root,
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
summary: {
|
|
203
|
+
clean: opts.scanned - results.length,
|
|
204
|
+
conflicted: results.length,
|
|
205
|
+
scanned: opts.scanned,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Render the human-readable report.
|
|
212
|
+
*
|
|
213
|
+
* @param {{ results: ReadonlyArray<{ file: string, blocks: ReadonlyArray<Record<string, number>> }>, summary: { scanned: number, conflicted: number } }} report
|
|
214
|
+
* the report object.
|
|
215
|
+
* @returns {string} the rendered report.
|
|
216
|
+
*/
|
|
217
|
+
function humanReport(report) {
|
|
218
|
+
if (report.summary.conflicted === 0) {
|
|
219
|
+
return `✓ no leftover conflict markers in ${report.summary.scanned} tracked files`;
|
|
220
|
+
}
|
|
221
|
+
const lines = report.results.flatMap(result => [
|
|
222
|
+
`✗ ${result.file}`,
|
|
223
|
+
...result.blocks.map(
|
|
224
|
+
block =>
|
|
225
|
+
` - conflict block opens at line ${block.startLine}, separator at line ${block.separatorLine}, closes at line ${block.endLine}`
|
|
226
|
+
),
|
|
227
|
+
]);
|
|
228
|
+
return [
|
|
229
|
+
...lines,
|
|
230
|
+
"",
|
|
231
|
+
`${report.summary.conflicted} of ${report.summary.scanned} tracked files carry leftover conflict markers`,
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse argv into resolved options. Throws `UsageError` on a bad invocation.
|
|
237
|
+
*
|
|
238
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
239
|
+
* @returns {{ root: string, json: boolean }} options.
|
|
240
|
+
*/
|
|
241
|
+
export function parseArgs(argv) {
|
|
242
|
+
let root = null;
|
|
243
|
+
let json = false;
|
|
244
|
+
for (let i = 0; i < argv.length; i++) {
|
|
245
|
+
const arg = argv[i];
|
|
246
|
+
if (arg === "--json") {
|
|
247
|
+
json = true;
|
|
248
|
+
} else if (arg === "--root") {
|
|
249
|
+
const next = argv[i + 1];
|
|
250
|
+
if (next === undefined || next.startsWith("--")) {
|
|
251
|
+
throw new UsageError("--root requires a value");
|
|
252
|
+
}
|
|
253
|
+
root = next;
|
|
254
|
+
i += 1;
|
|
255
|
+
} else {
|
|
256
|
+
throw new UsageError(`unknown argument: ${arg}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { json, root: path.resolve(root ?? REPO_ROOT) };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Run the gate. Returns the process exit code (does not call `exit`).
|
|
264
|
+
*
|
|
265
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
266
|
+
* @param {{ stdout?: { write(s: string): void }, stderr?: { write(s: string): void } }} [io]
|
|
267
|
+
* injectable streams (defaults to process streams).
|
|
268
|
+
* @returns {number} the exit code (0 clean, 1 markers found, 2 usage error).
|
|
269
|
+
*/
|
|
270
|
+
export function main(argv, io = {}) {
|
|
271
|
+
const out = io.stdout ?? process.stdout;
|
|
272
|
+
const err = io.stderr ?? process.stderr;
|
|
273
|
+
let opts;
|
|
274
|
+
let files;
|
|
275
|
+
try {
|
|
276
|
+
opts = parseArgs(argv);
|
|
277
|
+
if (!fs.existsSync(opts.root) || !fs.statSync(opts.root).isDirectory()) {
|
|
278
|
+
throw new UsageError(`--root is not a directory: ${opts.root}`);
|
|
279
|
+
}
|
|
280
|
+
files = listTrackedFiles(opts.root);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
err.write(`error: ${error.message}\n`);
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
let results;
|
|
286
|
+
try {
|
|
287
|
+
results = files
|
|
288
|
+
.map(file => scanFile(opts.root, file))
|
|
289
|
+
.filter(result => result !== null)
|
|
290
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
err.write(`error: failed to scan tracked files: ${error.message}\n`);
|
|
293
|
+
return 2;
|
|
294
|
+
}
|
|
295
|
+
const report = buildReport(results, {
|
|
296
|
+
root: opts.root,
|
|
297
|
+
scanned: files.length,
|
|
298
|
+
});
|
|
299
|
+
out.write(
|
|
300
|
+
(opts.json ? JSON.stringify(report, null, 2) : humanReport(report)) + "\n"
|
|
301
|
+
);
|
|
302
|
+
return results.length === 0 ? 0 : 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
process.argv[1] &&
|
|
307
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
308
|
+
) {
|
|
309
|
+
// exitCode (not process.exit): when stdout is a pipe, writes are async and
|
|
310
|
+
// process.exit() truncates the report mid-flush.
|
|
311
|
+
process.exitCode = main(process.argv.slice(2));
|
|
312
|
+
}
|
|
@@ -158,6 +158,14 @@ export function cacheMaxVersion(cacheRoot, name, marketplace) {
|
|
|
158
158
|
* contract): a semver value must equal the cache max; `"unknown"` is valid only
|
|
159
159
|
* when the cache has no semver anywhere.
|
|
160
160
|
*
|
|
161
|
+
* A semver `upstreamVersion` with NO semver in the cache is deliberately not an
|
|
162
|
+
* error. It means THIS machine cannot see the plugin (fresh clone, CI runner,
|
|
163
|
+
* plugin uninstalled) — not that the artifact is wrong. `isVersionUnverifiable`
|
|
164
|
+
* reports that state separately so the caller can warn instead of blocking,
|
|
165
|
+
* mirroring how the drift detector treats `not-installed` (issue #2552). Every
|
|
166
|
+
* other gate — schema, routing, coverage, anti-patterns — still runs, so a
|
|
167
|
+
* cacheless machine validates strictly more than it used to.
|
|
168
|
+
*
|
|
161
169
|
* @param {unknown} upstreamVersion - the artifact's `upstreamVersion`.
|
|
162
170
|
* @param {string | null} cacheMax - resolved max semver, or null.
|
|
163
171
|
* @returns {string[]} validation error messages (empty when valid).
|
|
@@ -174,9 +182,7 @@ function validateVersion(upstreamVersion, cacheMax) {
|
|
|
174
182
|
];
|
|
175
183
|
}
|
|
176
184
|
if (cacheMax === null) {
|
|
177
|
-
return [
|
|
178
|
-
`upstreamVersion ${upstreamVersion} but no semver in the cache to confirm`,
|
|
179
|
-
];
|
|
185
|
+
return [];
|
|
180
186
|
}
|
|
181
187
|
if (compareSemver(upstreamVersion, cacheMax) !== 0) {
|
|
182
188
|
return [`upstreamVersion ${upstreamVersion} != cache max ${cacheMax}`];
|
|
@@ -184,6 +190,19 @@ function validateVersion(upstreamVersion, cacheMax) {
|
|
|
184
190
|
return [];
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
/**
|
|
194
|
+
* True iff the artifact pins a semver `upstreamVersion` that this machine has
|
|
195
|
+
* no cached copy to confirm against. A cache-visibility fact, not a contract
|
|
196
|
+
* violation — reported so the push gate can warn without blocking.
|
|
197
|
+
*
|
|
198
|
+
* @param {unknown} upstreamVersion - the artifact's `upstreamVersion`.
|
|
199
|
+
* @param {string | null} cacheMax - resolved max semver, or null.
|
|
200
|
+
* @returns {boolean} whether the version claim is unverifiable here.
|
|
201
|
+
*/
|
|
202
|
+
export function isVersionUnverifiable(upstreamVersion, cacheMax) {
|
|
203
|
+
return cacheMax === null && isValidSemver(upstreamVersion);
|
|
204
|
+
}
|
|
205
|
+
|
|
187
206
|
/**
|
|
188
207
|
* Validate one component entry.
|
|
189
208
|
*
|
|
@@ -430,14 +449,20 @@ function truncate(text) {
|
|
|
430
449
|
* @param {string} routingDir - the routing directory.
|
|
431
450
|
* @param {string} file - the artifact filename (`*.json`).
|
|
432
451
|
* @param {string} cacheRoot - the installed-plugin cache root.
|
|
433
|
-
* @returns {{ file: string, errors: string[] }}
|
|
452
|
+
* @returns {{ file: string, errors: string[], unverifiable: boolean }}
|
|
453
|
+
* the per-file result. `unverifiable` means the version claim could not be
|
|
454
|
+
* confirmed on this machine; it never blocks.
|
|
434
455
|
*/
|
|
435
456
|
function validateFile(routingDir, file, cacheRoot) {
|
|
436
457
|
let artifact;
|
|
437
458
|
try {
|
|
438
459
|
artifact = JSON.parse(fs.readFileSync(path.join(routingDir, file), "utf8"));
|
|
439
460
|
} catch (error) {
|
|
440
|
-
return {
|
|
461
|
+
return {
|
|
462
|
+
errors: [`invalid JSON: ${error.message}`],
|
|
463
|
+
file,
|
|
464
|
+
unverifiable: false,
|
|
465
|
+
};
|
|
441
466
|
}
|
|
442
467
|
const hasIds =
|
|
443
468
|
artifact !== null &&
|
|
@@ -455,13 +480,21 @@ function validateFile(routingDir, file, cacheRoot) {
|
|
|
455
480
|
filename: file,
|
|
456
481
|
mdExists,
|
|
457
482
|
});
|
|
458
|
-
|
|
483
|
+
const upstreamVersion =
|
|
484
|
+
artifact !== null && typeof artifact === "object"
|
|
485
|
+
? artifact.upstreamVersion
|
|
486
|
+
: undefined;
|
|
487
|
+
return {
|
|
488
|
+
errors,
|
|
489
|
+
file,
|
|
490
|
+
unverifiable: isVersionUnverifiable(upstreamVersion, cacheMax),
|
|
491
|
+
};
|
|
459
492
|
}
|
|
460
493
|
|
|
461
494
|
/**
|
|
462
495
|
* Assemble the machine-readable report.
|
|
463
496
|
*
|
|
464
|
-
* @param {ReadonlyArray<{ file: string, errors: string[] }>} results - per-file results.
|
|
497
|
+
* @param {ReadonlyArray<{ file: string, errors: string[], unverifiable?: boolean }>} results - per-file results.
|
|
465
498
|
* @param {{ cacheRoot: string, routingDir: string }} opts - resolved options.
|
|
466
499
|
* @returns {Record<string, unknown>} the report object.
|
|
467
500
|
*/
|
|
@@ -475,6 +508,7 @@ export function buildReport(results, opts) {
|
|
|
475
508
|
summary: {
|
|
476
509
|
invalid,
|
|
477
510
|
scanned: results.length,
|
|
511
|
+
unverifiable: results.filter(r => r.unverifiable === true).length,
|
|
478
512
|
valid: results.length - invalid,
|
|
479
513
|
},
|
|
480
514
|
};
|
|
@@ -483,20 +517,27 @@ export function buildReport(results, opts) {
|
|
|
483
517
|
/**
|
|
484
518
|
* Render the human-readable report.
|
|
485
519
|
*
|
|
486
|
-
* @param {{ results: ReadonlyArray<{ file: string, errors: string[] }>, summary: { scanned: number, valid: number, invalid: number } }} report - report object.
|
|
520
|
+
* @param {{ results: ReadonlyArray<{ file: string, errors: string[], unverifiable?: boolean }>, summary: { scanned: number, valid: number, invalid: number, unverifiable: number } }} report - report object.
|
|
487
521
|
* @returns {string} the rendered report.
|
|
488
522
|
*/
|
|
489
523
|
function humanReport(report) {
|
|
490
|
-
const lines = report.results.map(r =>
|
|
491
|
-
r.errors.length
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
524
|
+
const lines = report.results.map(r => {
|
|
525
|
+
if (r.errors.length > 0) {
|
|
526
|
+
return `✗ ${r.file}\n${r.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
527
|
+
}
|
|
528
|
+
return r.unverifiable === true
|
|
529
|
+
? `⚠ ${r.file} (version unverifiable: plugin not in this machine's cache)`
|
|
530
|
+
: `✓ ${r.file}`;
|
|
531
|
+
});
|
|
495
532
|
const s = report.summary;
|
|
533
|
+
const unverifiable =
|
|
534
|
+
s.unverifiable > 0
|
|
535
|
+
? `, ${s.unverifiable} unverifiable on this machine`
|
|
536
|
+
: "";
|
|
496
537
|
return [
|
|
497
538
|
...lines,
|
|
498
539
|
"",
|
|
499
|
-
`${s.valid}/${s.scanned} routing artifacts valid, ${s.invalid} invalid`,
|
|
540
|
+
`${s.valid}/${s.scanned} routing artifacts valid, ${s.invalid} invalid${unverifiable}`,
|
|
500
541
|
].join("\n");
|
|
501
542
|
}
|
|
502
543
|
|