@codyswann/lisa 4.24.0 → 4.24.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.
Files changed (68) hide show
  1. package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +218 -0
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +7 -4
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/package.json +3 -3
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-git-submit-pr/SKILL.md +12 -0
  13. package/plugins/lisa/skills/lisa-git-submit-pr/SKILL.md +12 -0
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-agy/skills/lisa-git-submit-pr/SKILL.md +12 -0
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/skills/lisa-git-submit-pr/SKILL.md +12 -0
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cursor/skills/lisa-git-submit-pr/SKILL.md +12 -0
  25. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-agy/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  48. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-agy/plugin.json +1 -1
  53. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  58. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  63. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/src/base/skills/lisa-git-submit-pr/SKILL.md +12 -0
  66. package/scripts/check-security-floors.mjs +290 -31
  67. package/scripts/two-channel-couplings.json +1 -1
  68. package/typescript/copy-overwrite/.prettierignore +1 -0
@@ -164,16 +164,21 @@ function trackedManifests() {
164
164
 
165
165
  /**
166
166
  * Every version constraint declared across the template manifests.
167
- * @returns {{found: Map<string, Array<{file: string, path: string, spec: string}>>,
167
+ * @returns {{found: Map<string, Array<{file: string, path: string, spec: string,
168
+ * lowest: number[]}>>,
168
169
  * unresolved: Array<{file: string, path: string, name: string, spec: string}>,
169
- * scanned: string[], unscanned: string[]|null}}
170
- * Constraints by package name, self-references pointing at nothing, the
171
- * manifests read, and any tracked manifest the patterns did not reach.
170
+ * unparseable: Array<{file: string, path: string, name: string, spec: string,
171
+ * reason: string}>, scanned: string[], unscanned: string[]|null}}
172
+ * Constraints by package name with their resolved floors, self-references
173
+ * pointing at nothing, floors that resolve to no lower bound, the manifests
174
+ * read, and any tracked manifest the patterns did not reach.
172
175
  */
173
176
  export function collectFloors() {
174
177
  const found = new Map();
175
178
  /** `$name` references pointing at nothing this manifest declares. */
176
179
  const unresolved = [];
180
+ /** Floors carrying a version that resolves to no lower bound. */
181
+ const unparseable = [];
177
182
  const files = globSync(MANIFEST_PATTERNS).filter(
178
183
  file => !NOT_A_MANIFEST.some(fragment => file.includes(fragment))
179
184
  );
@@ -184,6 +189,30 @@ export function collectFloors() {
184
189
  const scanned = new Set(files);
185
190
  const unscanned =
186
191
  tracked === null ? null : tracked.filter(file => !scanned.has(file));
192
+ /**
193
+ * File one floor: comparable ones under their package, the rest as gaps.
194
+ *
195
+ * Resolved HERE, at collection, rather than in the audit loop, for two
196
+ * reasons beyond tidiness. It runs before any network call, so a floor with
197
+ * no lower bound is reported even on a run that is entirely rate-limited —
198
+ * the case where a silent skip is hardest to notice. And it makes every site
199
+ * in `found` carry a resolved floor, which removes the audit's null check
200
+ * instead of leaving a `continue` that a later reader has to re-litigate.
201
+ * @param {string} name npm package name.
202
+ * @param {string} file Manifest the floor was declared in.
203
+ * @param {string} path Governance group and section, dotted.
204
+ * @param {string} spec The version constraint to resolve.
205
+ */
206
+ const record = (name, file, path, spec) => {
207
+ const { version, reason } = resolveLowerBound(spec);
208
+ if (version === null) {
209
+ unparseable.push({ file, path, name, spec, reason });
210
+ return;
211
+ }
212
+ if (!found.has(name)) found.set(name, []);
213
+ found.get(name).push({ file, path, spec, lowest: version });
214
+ };
215
+
187
216
  for (const file of files) {
188
217
  let manifest;
189
218
  try {
@@ -221,33 +250,20 @@ export function collectFloors() {
221
250
  continue;
222
251
  }
223
252
  if (!/\d/.test(target)) continue;
224
- if (!found.has(name)) found.set(name, []);
225
- found
226
- .get(name)
227
- .push({ file, path: `${group}.${section}`, spec: target });
253
+ record(name, file, `${group}.${section}`, target);
228
254
  continue;
229
255
  }
230
256
  // A literal carrying no digits — "workspace:*", "latest" — is not a
231
- // floor and there is nothing to compare.
257
+ // floor and there is nothing to compare. Kept ahead of `record` so
258
+ // these stay a deliberate non-floor rather than becoming a gap
259
+ // report: they are not failed attempts to state a floor.
232
260
  if (!/\d/.test(spec)) continue;
233
- if (!found.has(name)) found.set(name, []);
234
- found.get(name).push({ file, path: `${group}.${section}`, spec });
261
+ record(name, file, `${group}.${section}`, spec);
235
262
  }
236
263
  }
237
264
  }
238
265
  }
239
- return { found, unresolved, scanned: files, unscanned };
240
- }
241
-
242
- /**
243
- * Lowest version a spec permits, as a comparable tuple.
244
- * @param {string} spec A range like ">=5.0.7", "^1.2.3", "~2.0.0".
245
- * @returns {number[]|null} [major, minor, patch], or null if unparseable.
246
- */
247
- export function lowestPermitted(spec) {
248
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
249
- if (!match) return null;
250
- return match.slice(1, 4).map(Number);
266
+ return { found, unresolved, unparseable, scanned: files, unscanned };
251
267
  }
252
268
 
253
269
  /**
@@ -263,6 +279,200 @@ function compare(left, right) {
263
279
  return 0;
264
280
  }
265
281
 
282
+ /** The comparator an npm term may lead with. */
283
+ const TERM_OPERATOR = /^(>=|<=|>|<|=|\^|~)/;
284
+
285
+ /**
286
+ * The version half of a term, anchored end to end.
287
+ *
288
+ * Anchored deliberately. The predecessor was an unanchored
289
+ * `/(\d+)\.(\d+)\.(\d+)/` scan, and a scan cannot tell a version from a number
290
+ * that merely follows one: on `^1.2.3-alpha.4` a global sweep finds `1.2.3`
291
+ * and then finds `4` in the prerelease tag and reads it as a second term. An
292
+ * anchored match either consumes the whole thing or rejects it, which is the
293
+ * property that lets a rejection mean "report this" instead of "guess".
294
+ *
295
+ * The operator and any prerelease or build tag are removed before this runs,
296
+ * rather than being alternatives inside it. Splitting the three concerns keeps
297
+ * each pattern simple enough to read, and the tag is discarded regardless
298
+ * because every comparison in this file is on the [major, minor, patch] tuple.
299
+ */
300
+ const TERM_VERSION = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/;
301
+
302
+ /**
303
+ * Lower bound of one comparator term.
304
+ *
305
+ * Three outcomes, kept distinct because collapsing any two of them is how a
306
+ * floor goes unchecked:
307
+ *
308
+ * - `bound` — the term states a lowest permitted version.
309
+ * - `floorless` — the term is an upper bound (`<8`, `<=2.0.0`). It states no
310
+ * floor at all; it permits every release beneath it.
311
+ * - `unreadable` — this check cannot tell which of the two it is.
312
+ * @param {string} term A single comparator, e.g. ">=1.2.3" or "^8".
313
+ * @returns {{kind: string, version?: number[]}} The classification.
314
+ */
315
+ function termLowerBound(term) {
316
+ const operator = TERM_OPERATOR.exec(term)?.[1] ?? "";
317
+ if (operator === "<" || operator === "<=") return { kind: "floorless" };
318
+ // A prerelease or build tag is the only place a `-` or `+` survives this
319
+ // far: hyphen RANGES were split before the term was formed. Taking the head
320
+ // is what stops `-alpha.4` contributing a `4` to the version.
321
+ const [core] = term.slice(operator.length).split(/[-+]/);
322
+
323
+ const match = TERM_VERSION.exec(core);
324
+ if (!match) return { kind: "unreadable" };
325
+ // `x` and `*` are wildcards, so they and everything after them are
326
+ // unspecified. `1.x.5` is not a version anyone means; truncating at the
327
+ // first wildcard is what makes `1.x` and `1` resolve identically.
328
+ const specified = [];
329
+ for (const part of [match[1], match[2], match[3]]) {
330
+ if (part === undefined || /^[xX*]$/.test(part)) break;
331
+ specified.push(Number(part));
332
+ }
333
+ // An absent minor or patch is zero, not missing: `^8` is `>=8.0.0`. Leaving
334
+ // them undefined would put NaN into the tuple, and NaN compares as neither
335
+ // higher nor lower than anything — it disables the comparison it feeds
336
+ // rather than failing it.
337
+ const lower = [...specified, 0, 0].slice(0, 3);
338
+ if (operator !== ">") return { kind: "bound", version: lower };
339
+
340
+ // npm expands a STRICT comparator before comparing: `>1` is `>=2.0.0` and
341
+ // `>1.2` is `>=1.3.0`. Reading either as merely 1.0.0 or 1.2.0 invents
342
+ // versions the range does not permit, which in this file means comparing a
343
+ // release the floor already excludes.
344
+ const lastSpecified = specified.length - 1;
345
+ lower[lastSpecified] += 1;
346
+ for (let index = lastSpecified + 1; index < 3; index += 1) lower[index] = 0;
347
+ return { kind: "bound", version: lower };
348
+ }
349
+
350
+ /**
351
+ * Lower bound of one `||` branch: the HIGHEST floor its terms state.
352
+ *
353
+ * A branch is a conjunction — `>=1.2.3 <2.0.0` permits a version only if every
354
+ * term admits it — so the binding floor is the strongest one present. Terms
355
+ * that state no floor (`<2.0.0`) constrain the ceiling and are passed over;
356
+ * a branch made only of those has no floor.
357
+ * @param {string} branch One branch of a `||` range.
358
+ * @returns {{kind: string, version?: number[]}} The classification.
359
+ */
360
+ function branchLowerBound(branch) {
361
+ const trimmed = branch.trim();
362
+ if (trimmed === "") return { kind: "unreadable" };
363
+ // A hyphen range's floor is its LEFT side. Both sides are bare versions, so
364
+ // measuring every term and keeping the highest would return `2.0.0` for
365
+ // `1.2.3 - 2.0.0` — the ceiling reported as the floor, which is the one
366
+ // direction that clears a vulnerable release instead of flagging it.
367
+ const [beneathAnyCeiling] = trimmed.split(/\s+-\s+/);
368
+ // semver permits whitespace between an operator and its version, so `>= 8`
369
+ // is `>=8`. Closing the gap before splitting on whitespace stops a legal
370
+ // range being split into a bare operator plus a bare version — which would
371
+ // read as unreadable and report a floor that is in fact perfectly clear.
372
+ const terms = beneathAnyCeiling
373
+ .replace(/(>=|<=|>|<|=|\^|~)\s+/g, "$1")
374
+ .split(/\s+/)
375
+ .filter(Boolean);
376
+ let strongest = null;
377
+ for (const term of terms) {
378
+ const resolved = termLowerBound(term);
379
+ if (resolved.kind === "unreadable") return { kind: "unreadable" };
380
+ if (resolved.kind === "floorless") continue;
381
+ if (strongest === null || compare(resolved.version, strongest) > 0) {
382
+ strongest = resolved.version;
383
+ }
384
+ }
385
+ return strongest === null
386
+ ? { kind: "floorless" }
387
+ : { kind: "bound", version: strongest };
388
+ }
389
+
390
+ /**
391
+ * Lowest version a spec permits, with the reason when there is none.
392
+ *
393
+ * The reason is produced here rather than by a second pass, so the message a
394
+ * reader sees and the decision the audit makes come from the same parse and
395
+ * cannot disagree.
396
+ *
397
+ * Note what `[0, 0, 0]` would mean if this returned it for an upper-bound-only
398
+ * spec, which is how the sibling collision checker in the templates models a
399
+ * floorless range: {@link withinRange} would compare 0.0.0 against an advisory
400
+ * range like `>= 4.0.0, < 5.0.8`, find it outside, and report the floor CLEAN.
401
+ * A spec that permits every vulnerable release would come back green. The two
402
+ * files are therefore deliberately NOT unified — there, zero means "weakest
403
+ * possible, loses every comparison"; here, no comparison at that end exists,
404
+ * so a floorless spec has to be reported instead of compared.
405
+ * @param {string} spec A range like ">=5.0.7", "^8", "~2.0", "^7 || ^8".
406
+ * @returns {{version: number[]|null, reason: string|null}} Bound, or reason.
407
+ */
408
+ function resolveLowerBound(spec) {
409
+ const raw = typeof spec === "string" ? spec.trim() : "";
410
+ if (raw === "") return { version: null, reason: "is empty" };
411
+ // An alias versions a DIFFERENT package. Its number cannot be compared
412
+ // against advisories filed for this name, and comparing it anyway would
413
+ // answer confidently about the wrong package.
414
+ if (/^npm:/i.test(raw)) {
415
+ return {
416
+ version: null,
417
+ reason:
418
+ "is an alias, so its version belongs to another package and cannot be compared against this one's advisories",
419
+ };
420
+ }
421
+
422
+ const branches = raw.split("||").map(branchLowerBound);
423
+ if (branches.some(branch => branch.kind === "unreadable")) {
424
+ return {
425
+ version: null,
426
+ reason: "is not a version range this check knows how to read",
427
+ };
428
+ }
429
+ // A disjunction permits the union of its branches, so one floorless branch
430
+ // makes the whole spec floorless: `<8 || ^9` permits everything below 8.
431
+ // Filtering that branch out would let the strongest branch speak for the
432
+ // weakest, reporting a floor the spec does not have.
433
+ if (branches.some(branch => branch.kind === "floorless")) {
434
+ return {
435
+ version: null,
436
+ reason:
437
+ "sets only an upper bound, so it permits every earlier release including vulnerable ones",
438
+ };
439
+ }
440
+ return {
441
+ version: branches.reduce(
442
+ (lowest, branch) =>
443
+ compare(branch.version, lowest) < 0 ? branch.version : lowest,
444
+ branches[0].version
445
+ ),
446
+ reason: null,
447
+ };
448
+ }
449
+
450
+ /**
451
+ * Lowest version a spec permits, as a comparable tuple.
452
+ *
453
+ * Partial and wildcard constraints resolve to their floor — `^8` is `8.0.0`,
454
+ * `~8.2` is `8.2.0`, `5.x` is `5.0.0` — rather than to null. Requiring all
455
+ * three components meant every such floor came back null, and the audit's
456
+ * `if (!lowest) continue` then skipped it in silence (lisa#3438).
457
+ *
458
+ * `null` now means only "no floor was read here" and never "nothing to
459
+ * check". Callers must report it; {@link lowerBoundGap} supplies the wording.
460
+ * @param {string} spec A range like ">=5.0.7", "^8", "~2.0", "^7 || ^8".
461
+ * @returns {number[]|null} [major, minor, patch], or null when there is none.
462
+ */
463
+ export function lowestPermitted(spec) {
464
+ return resolveLowerBound(spec).version;
465
+ }
466
+
467
+ /**
468
+ * Why a spec has no comparable floor, in words an operator can act on.
469
+ * @param {string} spec A version constraint.
470
+ * @returns {string|null} The reason, or null when the spec does resolve.
471
+ */
472
+ export function lowerBoundGap(spec) {
473
+ return resolveLowerBound(spec).reason;
474
+ }
475
+
266
476
  /**
267
477
  * Whether a version falls inside an advisory's vulnerable range.
268
478
  * @param {number[]} version Candidate version.
@@ -346,10 +556,17 @@ async function advisoriesFor(name) {
346
556
  /**
347
557
  * Audit every collected floor.
348
558
  * @returns {Promise<{problems: Array, unreachable: Array, unresolved: Array,
349
- * unscanned: string[]|null, manifests: number, checked: number}>} Findings.
559
+ * unparseable: Array, unscanned: string[]|null, manifests: number,
560
+ * checked: number}>} Findings.
350
561
  */
351
562
  async function audit() {
352
- const { found: floors, unresolved, scanned, unscanned } = collectFloors();
563
+ const {
564
+ found: floors,
565
+ unresolved,
566
+ unparseable,
567
+ scanned,
568
+ unscanned,
569
+ } = collectFloors();
353
570
  const problems = [];
354
571
  const unreachable = [];
355
572
  for (const [name, sites] of floors) {
@@ -364,9 +581,15 @@ async function audit() {
364
581
  for (const vulnerability of advisory.vulnerabilities ?? []) {
365
582
  if (vulnerability.package?.name !== name) continue;
366
583
  for (const site of sites) {
367
- const lowest = lowestPermitted(site.spec);
368
- if (!lowest) continue;
369
- if (!withinRange(lowest, vulnerability.vulnerable_version_range)) {
584
+ // No null check here, deliberately. This loop used to open with
585
+ // `const lowest = lowestPermitted(site.spec); if (!lowest) continue;`
586
+ // — the skip that made an unreadable floor indistinguishable from a
587
+ // floor that passed. Every site in `found` now carries a floor
588
+ // resolved at collection time, and the ones that resolve to nothing
589
+ // are in `unparseable`, where they are reported and gate `--strict`.
590
+ if (
591
+ !withinRange(site.lowest, vulnerability.vulnerable_version_range)
592
+ ) {
370
593
  continue;
371
594
  }
372
595
  problems.push({
@@ -385,6 +608,7 @@ async function audit() {
385
608
  problems,
386
609
  unreachable,
387
610
  unresolved,
611
+ unparseable,
388
612
  unscanned,
389
613
  manifests: scanned.length,
390
614
  checked: floors.size,
@@ -394,13 +618,28 @@ async function audit() {
394
618
  async function main() {
395
619
  const strict = process.argv.includes("--strict");
396
620
  const asJson = process.argv.includes("--json");
397
- const { problems, unreachable, unresolved, unscanned, manifests, checked } =
398
- await audit();
621
+ const {
622
+ problems,
623
+ unreachable,
624
+ unresolved,
625
+ unparseable,
626
+ unscanned,
627
+ manifests,
628
+ checked,
629
+ } = await audit();
399
630
 
400
631
  if (asJson) {
401
632
  console.log(
402
633
  JSON.stringify(
403
- { problems, unreachable, unresolved, unscanned, manifests, checked },
634
+ {
635
+ problems,
636
+ unreachable,
637
+ unresolved,
638
+ unparseable,
639
+ unscanned,
640
+ manifests,
641
+ checked,
642
+ },
404
643
  null,
405
644
  2
406
645
  )
@@ -443,6 +682,24 @@ async function main() {
443
682
  );
444
683
  }
445
684
 
685
+ // A floor whose version cannot be resolved to a lowest permitted release is
686
+ // a floor this audit did not compare. It used to be passed over in silence,
687
+ // which put it in the output as nothing at all — and nothing at all is what
688
+ // a floor that passed also looks like.
689
+ if (!asJson && unparseable.length > 0) {
690
+ console.log(
691
+ `\n> **${unparseable.length} floor(s) could NOT be checked.** Each states a version this audit could not read as a lowest permitted release, so none of them were compared against the advisory database. This is not a clean result for them.`
692
+ );
693
+ for (const entry of unparseable) {
694
+ console.log(
695
+ `> - \`${entry.name}: ${entry.spec}\` in ${entry.file} ${entry.path} — ${entry.reason}`
696
+ );
697
+ }
698
+ console.log(
699
+ "> Fix by rewriting each one as a constraint with a lower bound — `>=x.y.z`, `^x.y.z`, `~x.y.z`, or an exact version."
700
+ );
701
+ }
702
+
446
703
  // A manifest the patterns never opened is the same failure as a package the
447
704
  // advisory API never answered for: unexamined, and reported clean unless the
448
705
  // difference is stated. Printed whether or not anything else went wrong.
@@ -488,11 +745,13 @@ async function main() {
488
745
  // does: it is a floor nobody checked, and letting it pass reports it clean.
489
746
  // An unscanned tracked manifest gates for the same reason an inconclusive
490
747
  // lookup does: it is a floor nobody checked, and passing reports it clean.
748
+ // A floor with no readable lower bound gates for that reason a fourth time.
491
749
  if (
492
750
  strict &&
493
751
  (problems.length > 0 ||
494
752
  unreachable.length > 0 ||
495
753
  unresolved.length > 0 ||
754
+ unparseable.length > 0 ||
496
755
  (unscanned !== null && unscanned.length > 0))
497
756
  ) {
498
757
  process.exitCode = 1;
@@ -7,7 +7,7 @@
7
7
  "workflows": 23,
8
8
  "steps": 639,
9
9
  "couplings": 27,
10
- "inventory": 58
10
+ "inventory": 59
11
11
  },
12
12
  "counts": {
13
13
  "package-backed": 10,
@@ -65,6 +65,7 @@ amplify
65
65
  scripts/bdd-matrix.mjs
66
66
  scripts/check-bdd-coverage.mjs
67
67
  scripts/check-conflict-markers.mjs
68
+ scripts/check-orphaned-branches.mjs
68
69
  scripts/check-e2e-coverage.mjs
69
70
  scripts/check-lighthouse-details.mjs
70
71
  scripts/check-nightly-e2e-health.mjs