@codyswann/lisa 4.51.0 → 4.52.1

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 (65) hide show
  1. package/dist/cli/doctor-coderabbit.d.ts +1 -0
  2. package/dist/cli/doctor-coderabbit.d.ts.map +1 -1
  3. package/dist/cli/doctor-coderabbit.js +64 -5
  4. package/dist/cli/doctor-coderabbit.js.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  6. package/dist/core/lisa-owned-hash-ledger.js +2 -0
  7. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  8. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +5 -3
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +117 -4
  13. package/package.json +4 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/plugin.json +1 -1
  27. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  47. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-agy/plugin.json +1 -1
  52. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  57. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  62. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  64. package/scripts/generate-two-channel-couplings.ts +215 -7
  65. package/scripts/two-channel-couplings.json +94 -0
@@ -228,9 +228,50 @@ export function buildDeliveryInventory(
228
228
  );
229
229
  }
230
230
 
231
+ /**
232
+ * The only part of a report these two functions read.
233
+ *
234
+ * Narrower than the full report on purpose: they need a key and a verdict per
235
+ * entry and nothing else, and a parameter that asked for the whole measurement
236
+ * would force every caller — including a test — to build one it does not use.
237
+ */
238
+ interface CouplingKeySource {
239
+ readonly entries: readonly {
240
+ readonly key: string;
241
+ readonly verdict: string;
242
+ }[];
243
+ }
244
+
245
+ /** One coupling's answer to "can this notice a STALE artifact?" (#3687). */
246
+ export interface StalenessClassification {
247
+ /**
248
+ * What the coupling CAN detect today — an observation, not a decision.
249
+ *
250
+ * `version-handshake` asks the artifact for a contract version and compares
251
+ * it. `capability-probe` asks for one subcommand or flag and infers age from
252
+ * whether it answers, which sees staleness at exactly one point and is blind
253
+ * above that floor — the CodySwannGT/lisa#3477 defect. `existence-only`
254
+ * cannot see age at all.
255
+ */
256
+ readonly detects: "version-handshake" | "capability-probe" | "existence-only";
257
+ /**
258
+ * Why the current level is acceptable. Required unless a handshake exists.
259
+ *
260
+ * This is the DECISION half, kept separate from `detects` on purpose: a
261
+ * coupling that cannot see age and is fine that way (an advisory read, an
262
+ * artifact carrying no verdict-bearing logic) is a different fact from one
263
+ * that cannot see age and should. Collapsing them into a single
264
+ * "not-needed" token would lose exactly the distinction the sweep exists to
265
+ * record, and would let an unexamined default wear the same word as a
266
+ * considered exemption.
267
+ */
268
+ readonly reason?: string;
269
+ }
270
+
231
271
  /** The ledger's hand-authored half plus its derived half. */
232
272
  interface Ledger {
233
273
  readonly ratified: Readonly<Record<string, string>>;
274
+ readonly staleness: Readonly<Record<string, StalenessClassification>>;
234
275
  readonly inspected: TwoChannelReport["inspected"];
235
276
  readonly counts: TwoChannelReport["counts"];
236
277
  readonly couplings: readonly {
@@ -259,6 +300,156 @@ function readRatified(ledgerPath: string): Readonly<Record<string, string>> {
259
300
  return parsed?.ratified ?? {};
260
301
  }
261
302
 
303
+ /**
304
+ * Read the staleness classifications already recorded (#3687).
305
+ *
306
+ * Exactly the shape `readRatified` uses, and for the same reason: the ledger is
307
+ * a DERIVED artifact regenerated in-commit, so a hand-authored field survives
308
+ * only if the generator reads the previous version before rewriting it. This is
309
+ * the established local precedent for persisting a human decision inside a
310
+ * generated file, which is why the classification lives here rather than in a
311
+ * new sidecar nobody would remember to update.
312
+ * @param ledgerPath - Absolute path to the ledger
313
+ * @returns The classifications, keyed `<workflow>::<path>`
314
+ */
315
+ function readStaleness(
316
+ ledgerPath: string
317
+ ): Readonly<Record<string, StalenessClassification>> {
318
+ if (!existsSync(ledgerPath)) return {};
319
+ const parsed = JSON.parse(readFileSync(ledgerPath, "utf8")) as
320
+ | Partial<Ledger>
321
+ | undefined;
322
+ return parsed?.staleness ?? {};
323
+ }
324
+
325
+ /**
326
+ * The verdict for a coupling the package channel already covers.
327
+ *
328
+ * Those are omitted from the ledger, so every question this file asks about a
329
+ * live coupling has to exclude them — named once so the three sites cannot
330
+ * disagree about which verdict that is.
331
+ */
332
+ const PACKAGE_BACKED = "package-backed";
333
+
334
+ /** The `detects` value meaning the coupling asks for a contract version. */
335
+ const VERSION_HANDSHAKE = "version-handshake";
336
+
337
+ /** The `detects` value meaning it infers age from one subcommand or flag. */
338
+ const CAPABILITY_PROBE = "capability-probe";
339
+
340
+ /** The `detects` value meaning it can see presence and nothing else. */
341
+ const EXISTENCE_ONLY = "existence-only";
342
+
343
+ /** The `detects` values a classification may carry. */
344
+ const DETECTS = new Set([VERSION_HANDSHAKE, CAPABILITY_PROBE, EXISTENCE_ONLY]);
345
+
346
+ /**
347
+ * The floor a stated reason must clear to be a reason rather than a label.
348
+ *
349
+ * A floor, not a proof — no check tells a real argument from filler. What it
350
+ * buys is that "n/a", "ok" and an empty string cannot stand in for the decision
351
+ * this field exists to record.
352
+ */
353
+ const MIN_REASON_CHARACTERS = 40;
354
+
355
+ /**
356
+ * Every problem with the staleness half, as operator-readable paragraphs.
357
+ *
358
+ * THREE refusals, and the first is the one that makes this a standing property
359
+ * rather than a one-time sweep: a coupling with no classification fails, so
360
+ * entry 25 cannot silently inherit "nobody looked". A sweep decays the moment
361
+ * somebody adds an entry; a required field does not.
362
+ * @param report - The measurement
363
+ * @param staleness - The recorded classifications
364
+ * @returns Problem paragraphs, empty when the staleness half is sound
365
+ */
366
+ export function stalenessProblems(
367
+ report: CouplingKeySource,
368
+ staleness: Readonly<Record<string, StalenessClassification>>
369
+ ): readonly string[] {
370
+ const live = new Set(
371
+ report.entries
372
+ .filter(entry => entry.verdict !== PACKAGE_BACKED)
373
+ .map(entry => entry.key)
374
+ );
375
+ const missing = [...live]
376
+ .filter(key => staleness[key] === undefined)
377
+ .map(
378
+ key =>
379
+ `\nUNCLASSIFIED COUPLING ${key}\n No staleness classification. Every ` +
380
+ `coupling must record whether it can detect a STALE artifact, not only ` +
381
+ `an absent one — absence skips and posts nothing, while present-and-old ` +
382
+ `posts a verdict from superseded logic that every reader believes. Add ` +
383
+ `a "staleness" entry with "detects" (version-handshake | ` +
384
+ `capability-probe | existence-only) and, unless it is a handshake, a ` +
385
+ `"reason" saying why that is acceptable here.`
386
+ );
387
+ const orphaned = Object.keys(staleness)
388
+ .filter(key => !live.has(key))
389
+ .map(
390
+ key =>
391
+ `\nSTALE CLASSIFICATION ${key}\n Nothing reads this path any more, so ` +
392
+ `the classification is an unexamined judgement the next matching path ` +
393
+ `would inherit. Delete it from ${LEDGER_PATH}.`
394
+ );
395
+ const malformed = Object.entries(staleness)
396
+ .filter(([key]) => live.has(key))
397
+ .flatMap(([key, value]) => describeMalformed(key, value));
398
+ return [...missing, ...orphaned, ...malformed];
399
+ }
400
+
401
+ /**
402
+ * Why one recorded classification is not usable, if it is not.
403
+ * @param key - The coupling key
404
+ * @param value - The recorded classification
405
+ * @returns Problem paragraphs for this entry
406
+ */
407
+ function describeMalformed(
408
+ key: string,
409
+ value: StalenessClassification
410
+ ): readonly string[] {
411
+ if (!DETECTS.has(value?.detects)) {
412
+ return [
413
+ `\nMALFORMED CLASSIFICATION ${key}\n "detects" is ` +
414
+ `${JSON.stringify(value?.detects)}; expected one of ` +
415
+ `${[...DETECTS].join(", ")}.`,
416
+ ];
417
+ }
418
+ if (value.detects === VERSION_HANDSHAKE) return [];
419
+ const reason = typeof value.reason === "string" ? value.reason.trim() : "";
420
+ return reason.length >= MIN_REASON_CHARACTERS
421
+ ? []
422
+ : [
423
+ `\nUNREASONED CLASSIFICATION ${key}\n "detects" is ` +
424
+ `"${value.detects}", which cannot see how old the artifact is, and no ` +
425
+ `"reason" of at least ${MIN_REASON_CHARACTERS} characters says why ` +
426
+ `that is acceptable here. A recorded decision and an unexamined ` +
427
+ `default must not look the same.`,
428
+ ];
429
+ }
430
+
431
+ /**
432
+ * The one-line fleet coverage sentence (#3687).
433
+ * @param report - The measurement
434
+ * @param staleness - The recorded classifications
435
+ * @returns A sentence naming how much of the fleet can see age
436
+ */
437
+ export function stalenessCoverage(
438
+ report: CouplingKeySource,
439
+ staleness: Readonly<Record<string, StalenessClassification>>
440
+ ): string {
441
+ const live = report.entries.filter(entry => entry.verdict !== PACKAGE_BACKED);
442
+ const tally = (kind: string): number =>
443
+ live.filter(entry => staleness[entry.key]?.detects === kind).length;
444
+ const handshake = tally(VERSION_HANDSHAKE);
445
+ return (
446
+ ` staleness detection: ${handshake} of ${live.length} coupling(s) can see ` +
447
+ `how old the artifact is (${VERSION_HANDSHAKE}); ${tally(CAPABILITY_PROBE)} ` +
448
+ `detect it at one floor only (${CAPABILITY_PROBE}); ${tally(EXISTENCE_ONLY)} ` +
449
+ `detect absence only (${EXISTENCE_ONLY}).`
450
+ );
451
+ }
452
+
262
453
  /**
263
454
  * Render the ledger a report implies.
264
455
  *
@@ -272,14 +463,16 @@ function readRatified(ledgerPath: string): Readonly<Record<string, string>> {
272
463
  */
273
464
  function toLedger(
274
465
  report: TwoChannelReport,
275
- ratified: Readonly<Record<string, string>>
466
+ ratified: Readonly<Record<string, string>>,
467
+ staleness: Readonly<Record<string, StalenessClassification>>
276
468
  ): Ledger {
277
469
  return {
278
470
  ratified,
471
+ staleness,
279
472
  inspected: report.inspected,
280
473
  counts: report.counts,
281
474
  couplings: report.entries
282
- .filter(entry => entry.verdict !== "package-backed")
475
+ .filter(entry => entry.verdict !== PACKAGE_BACKED)
283
476
  .map(entry => ({
284
477
  key: entry.key,
285
478
  workflow: entry.workflow,
@@ -302,6 +495,7 @@ function toLedger(
302
495
  export function measure(root: string): {
303
496
  readonly report: TwoChannelReport;
304
497
  readonly ratified: Readonly<Record<string, string>>;
498
+ readonly staleness: Readonly<Record<string, StalenessClassification>>;
305
499
  } {
306
500
  const workflowsDir = path.join(root, WORKFLOWS_DIR);
307
501
  const inventory = buildDeliveryInventory(root);
@@ -342,6 +536,7 @@ export function measure(root: string): {
342
536
  ratified: readRatified(path.join(root, LEDGER_PATH)),
343
537
  }),
344
538
  ratified: readRatified(path.join(root, LEDGER_PATH)),
539
+ staleness: readStaleness(path.join(root, LEDGER_PATH)),
345
540
  };
346
541
  }
347
542
 
@@ -350,7 +545,10 @@ export function measure(root: string): {
350
545
  * @param report - The measurement
351
546
  * @returns The report text
352
547
  */
353
- function humanReport(report: TwoChannelReport): string {
548
+ function humanReport(
549
+ report: TwoChannelReport,
550
+ staleness: Readonly<Record<string, StalenessClassification>>
551
+ ): string {
354
552
  const counts = report.inspected;
355
553
  const header =
356
554
  `two-channel delivery: inspected ${counts.workflows} reusable workflow(s), ` +
@@ -370,7 +568,12 @@ function humanReport(report: TwoChannelReport): string {
370
568
  `inherit. Delete it from ${LEDGER_PATH}.`
371
569
  )
372
570
  .join("");
373
- return `${header}\n${verdicts}${findings}${stale}`;
571
+ // The coverage line answers the question the registry could not answer at
572
+ // all before this: "how many of our couplings can detect staleness?" — in one
573
+ // line, without opening 24 workflows.
574
+ const coverage = stalenessCoverage(report, staleness);
575
+ const unclassified = stalenessProblems(report, staleness).join("");
576
+ return `${header}\n${verdicts}\n${coverage}${findings}${stale}${unclassified}`;
374
577
  }
375
578
 
376
579
  /**
@@ -384,9 +587,9 @@ export function main(argv: readonly string[]): number {
384
587
  process.stderr.write(`error: --root does not exist: ${options.root}\n`);
385
588
  return EXIT_OPERATIONAL;
386
589
  }
387
- const { report, ratified } = measure(options.root);
590
+ const { report, ratified, staleness } = measure(options.root);
388
591
  process.stdout.write(
389
- `${options.json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
592
+ `${options.json ? JSON.stringify(report, null, 2) : humanReport(report, staleness)}\n`
390
593
  );
391
594
  if (!report.measured) {
392
595
  process.stderr.write(
@@ -397,7 +600,7 @@ export function main(argv: readonly string[]): number {
397
600
  return EXIT_OPERATIONAL;
398
601
  }
399
602
  const ledgerPath = path.join(options.root, LEDGER_PATH);
400
- const rendered = `${JSON.stringify(toLedger(report, ratified), null, 2)}\n`;
603
+ const rendered = `${JSON.stringify(toLedger(report, ratified, staleness), null, 2)}\n`;
401
604
  const current = existsSync(ledgerPath)
402
605
  ? readFileSync(ledgerPath, "utf8")
403
606
  : "";
@@ -413,9 +616,14 @@ export function main(argv: readonly string[]): number {
413
616
  mkdirSync(path.dirname(ledgerPath), { recursive: true });
414
617
  writeFileSync(ledgerPath, rendered);
415
618
  }
619
+ // The staleness half fails in BOTH modes, exactly as findings do. A gate you
620
+ // can regenerate your way past is not a gate: regenerating records a coupling,
621
+ // it does not classify one, and `toLedger` carries the map through untouched
622
+ // rather than inventing a default for a new entry (#3687).
416
623
  const failed =
417
624
  report.findings.length > 0 ||
418
625
  report.staleRatifications.length > 0 ||
626
+ stalenessProblems(report, staleness).length > 0 ||
419
627
  ledgerStale;
420
628
  return failed ? 1 : 0;
421
629
  }
@@ -3,6 +3,100 @@
3
3
  "quality.yml::scripts/check-learnings-budget.ts": "Lisa-source-only branch, and the else arm is the delivery. The step tests for BOTH scripts/check-learnings-budget.ts and src/core/learnings-budget-check.ts, which co-exist only in this repository; every consumer takes the else arm and runs the prover as `bunx @codyswann/lisa@<their own declared version>`, which is the PACKAGE channel and not a caller-tree read at all. A trailing `grep -qE \"learnings budget (passed|saturated)|no learnings file\"` then refuses a run that printed no verdict, so the arm cannot pass vacuously. Ratified because the caller-tree read is unreachable outside this repository, not because the gap is tolerated.",
4
4
  "quality.yml::scripts/generate-nightly-e2e-guard-certificate.mjs": "Lisa-source-only branch. Both reusable jobs require `github.repository == 'CodySwannGT/lisa'` before the step can start, and the run body repeats that exact repository guard before checking this artifact path. Ordinary and private host callers therefore skip the step without reading or fetching from their own tree. The scoped branch fetches only the immutable refs needed to behavior-test Lisa's own retained certificate inputs. Ratified because the caller-tree read is unreachable outside this repository, not because an undelivered host dependency is tolerated."
5
5
  },
6
+ "staleness": {
7
+ "create-github-issue-on-failure.yml::scripts/nightly-e2e-provider-action.mjs": {
8
+ "detects": "existence-only",
9
+ "reason": "The read files a tracker issue after a nightly failure; it posts no required check and gates no merge. A stale copy files a slightly wrong issue body, which a human reads and corrects, rather than turning a merge green or red from superseded logic. The harm this classification exists to catch is a verdict nobody can see is old, and this coupling produces no verdict."
10
+ },
11
+ "create-jira-issue-on-failure.yml::scripts/nightly-e2e-provider-action.mjs": {
12
+ "detects": "existence-only",
13
+ "reason": "Same provider action as the GitHub arm, same reasoning: it files a tracker issue after a nightly failure, posts no required check, and gates no merge. A stale copy misformats a report a human reads; it cannot produce a merge verdict derived from superseded logic."
14
+ },
15
+ "create-linear-issue-on-failure.yml::scripts/nightly-e2e-provider-action.mjs": {
16
+ "detects": "existence-only",
17
+ "reason": "Same provider action as the GitHub and Jira arms, same reasoning: it files a tracker issue after a nightly failure, posts no required check, and gates no merge. A stale copy degrades a report rather than a verdict."
18
+ },
19
+ "create-sentry-issue-on-failure.yml::scripts/nightly-e2e-provider-action.mjs": {
20
+ "detects": "existence-only",
21
+ "reason": "Same provider action as the other three failure arms, same reasoning: it files an issue after a nightly failure, posts no required check, and gates no merge. A stale copy degrades a report rather than a verdict."
22
+ },
23
+ "maestro-native-e2e.yml::scripts/classify-maestro-failures.mjs": {
24
+ "detects": "existence-only",
25
+ "reason": "Classifies native E2E failures for the run summary so an operator can tell an infrastructure flake from a product regression. It labels a failure that has already been decided elsewhere; it does not decide one. A stale classifier mislabels a red the operator is already looking at, which is visible in the same output."
26
+ },
27
+ "nightly-e2e-health.yml::scripts/check-nightly-e2e-health.mjs": {
28
+ "detects": "version-handshake"
29
+ },
30
+ "nightly-e2e-health.yml::scripts/nightly-e2e-suites.schema.json": {
31
+ "detects": "existence-only",
32
+ "reason": "A JSON schema has no executable surface, so it cannot answer a contract-version call at all — no handshake is constructible here. It is covered transitively rather than left unguarded: the schema and check-nightly-e2e-health.mjs ship on the same typescript/copy-overwrite lane and are read by the same workflow, and that checker DOES handshake, so a tree old enough to carry a stale schema carries a stale checker and the existing handshake fails first."
33
+ },
34
+ "nightly-e2e-report.yml::scripts/check-nightly-e2e-health.mjs": {
35
+ "detects": "version-handshake"
36
+ },
37
+ "nightly-e2e-tracking.yml::scripts/reconcile-nightly-e2e-tracking.mjs": {
38
+ "detects": "existence-only",
39
+ "reason": "Reconciles the nightly tracking issue — opens, refreshes or closes it to match the latest health verdict. It consumes a verdict rather than producing one, so a stale copy mismanages a tracking issue a human reads and cannot make a merge gate green."
40
+ },
41
+ "publish-to-npm.yml::scripts/check-npm-publish-landed.mjs": {
42
+ "detects": "existence-only",
43
+ "reason": "A GENUINE GAP, recorded rather than waved through. It verifies a publish actually landed, so a stale copy can attest to a release that did not happen. It runs post-merge in the release workflow and posts no merge-blocking required context, which is why it is not the first handshake to build — but it is verdict-bearing and belongs at the front of the queue when this classification is next acted on."
44
+ },
45
+ "publish-to-npm.yml::scripts/check-release-package-identity.mjs": {
46
+ "detects": "existence-only",
47
+ "reason": "A GENUINE GAP, recorded rather than waved through, and the sibling of the publish-landed read: it attests that the published package is the one intended, so a stale copy can attest to an identity it no longer checks the same way. Post-merge and not merge-blocking, so it queues behind the merge gates rather than ahead of them."
48
+ },
49
+ "quality-rails.yml::scripts/check-threshold-ratchet.mjs": {
50
+ "detects": "existence-only",
51
+ "reason": "Same prover as the quality.yml read and the same GENUINE GAP, reached from the Rails caller tree. Recorded separately because the two callers can drift apart: a handshake added to one workflow does not reach the other, which is how a fleet ends up half-covered."
52
+ },
53
+ "quality-rails.yml::scripts/lisa-mutation.sh": {
54
+ "detects": "existence-only",
55
+ "reason": "A shell wrapper that shapes a mutation-testing invocation; the verdict comes from Stryker and its configured thresholds, not from this file. A stale wrapper passes older flags, which fails loudly as a runner usage error rather than producing a wrong mutation score."
56
+ },
57
+ "quality-rails.yml::scripts/lisa-scratch-run.sh": {
58
+ "detects": "existence-only",
59
+ "reason": "A shell wrapper that isolates a run's scratch namespace. It carries no verdict-bearing logic at all — it prepares a directory and execs the real command — so there is no superseded judgement for a stale copy to apply."
60
+ },
61
+ "quality-rails.yml::scripts/lisa-work-item.mjs": {
62
+ "detects": "version-handshake"
63
+ },
64
+ "quality.yml::scripts/check-e2e-coverage.mjs": {
65
+ "detects": "existence-only",
66
+ "reason": "Guarded by an existence test and scoped to Expo consumers. It reports route coverage against a declared threshold; a stale copy computes coverage by an older rule and says which rule it used in its own output, so the number it prints is interpretable rather than silently wrong."
67
+ },
68
+ "quality.yml::scripts/check-learnings-budget.ts": {
69
+ "detects": "existence-only",
70
+ "reason": "Already ratified as a Lisa-source-only branch: every consumer takes the else arm and runs the prover from the PACKAGE channel at their own declared version, which is a version pin rather than an unversioned copy. The two-channel skew this field is about does not arise on the path consumers actually take."
71
+ },
72
+ "quality.yml::scripts/check-skipped-required-checks.mjs": {
73
+ "detects": "existence-only",
74
+ "reason": "A GENUINE GAP and the sharpest one in this registry: it is the vacuity and review-evidence prover, so a stale copy posts a review verdict from superseded logic on a merge gate. Its own workflow resolves a second in-repo address when scripts/ has no copy, which bounds the absent case but says nothing about age. Highest-value handshake candidate here."
75
+ },
76
+ "quality.yml::scripts/check-state-classification.mjs": {
77
+ "detects": "existence-only",
78
+ "reason": "Guarded by an existence test and advisory in effect: it classifies demo/fixture state against a declared contract and reports what it found. A stale copy reports against an older contract version, which surfaces as a classification mismatch in its own output rather than as a green merge gate, so the failure is self-announcing."
79
+ },
80
+ "quality.yml::scripts/check-threshold-ratchet.mjs": {
81
+ "detects": "existence-only",
82
+ "reason": "A GENUINE GAP. The ratchet decides whether a threshold moved the wrong way, so a stale copy can pass a loosened threshold it does not yet know how to refuse. It is declared optional at push in this repository's own gate set, which lowers the blast radius but does not remove it."
83
+ },
84
+ "quality.yml::scripts/check-verification-coverage.mjs": {
85
+ "detects": "existence-only",
86
+ "reason": "Bespoke and conditional — it requires a verification-spec delta on feat/fix changes. A stale copy applies an older definition of what counts as a spec delta, which produces a visible disagreement with the spec in the pull request rather than a silent pass."
87
+ },
88
+ "quality.yml::scripts/generate-nightly-e2e-guard-certificate.mjs": {
89
+ "detects": "existence-only",
90
+ "reason": "Already ratified as undelivered: no lane ships this path, so no consumer tree carries a copy that could be stale. The staleness question is vacuous here for the same reason the delivery question was, and it is recorded rather than left blank so the entry is not mistaken for one nobody examined."
91
+ },
92
+ "quality.yml::scripts/lisa-floor-collisions.mjs": {
93
+ "detects": "existence-only",
94
+ "reason": "Reports collisions between declared security floors. A stale copy knows a smaller set of floors, so it under-reports rather than mis-decides, and the floors it does know it still checks correctly. Under-reporting is the benign direction for a collision detector."
95
+ },
96
+ "quality.yml::scripts/lisa-work-item.mjs": {
97
+ "detects": "version-handshake"
98
+ }
99
+ },
6
100
  "inspected": {
7
101
  "workflows": 23,
8
102
  "steps": 719,