@codyswann/lisa 4.45.7 → 4.45.8
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/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +21 -2
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +68 -0
- package/all/merge/.claude/settings.json +10 -0
- package/dist/configs/vitest/scratch-global-setup.d.ts +9 -3
- package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
- package/dist/configs/vitest/scratch-global-setup.js +23 -10
- package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts +48 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts.map +1 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js +194 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +9 -1
- package/plugins/lisa/.codex-plugin/hooks.json +8 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +68 -0
- 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 +9 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/hooks.json +3 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +68 -0
- 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/plugins/src/base/.claude-plugin/plugin.json +8 -0
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +68 -0
- package/scripts/check-shell-guard-refusal-coverage.mjs +49 -3
- package/scripts/lib/shell-guard-trace.mjs +26 -8
- package/scripts/lisa-enforcement-fallback.sh +21 -2
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +244 -3
|
@@ -155,6 +155,7 @@
|
|
|
155
155
|
* @module scripts/lisa-mutation
|
|
156
156
|
*/
|
|
157
157
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
158
|
+
import { createHash } from "node:crypto";
|
|
158
159
|
import fs from "node:fs";
|
|
159
160
|
import os from "node:os";
|
|
160
161
|
import path from "node:path";
|
|
@@ -445,6 +446,242 @@ export const uninstrumentableGuards = files =>
|
|
|
445
446
|
);
|
|
446
447
|
});
|
|
447
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Environment variable naming the shell-guard evidence trace to read.
|
|
451
|
+
*
|
|
452
|
+
* The same variable the tracer writes to, so a project that already produces a
|
|
453
|
+
* trace hands it to this gate without a second contract.
|
|
454
|
+
*/
|
|
455
|
+
export const SHELL_EVIDENCE_TRACE_VAR = "LISA_SHELL_GUARD_TRACE";
|
|
456
|
+
|
|
457
|
+
/** Where this gate looks for a trace nobody named explicitly. */
|
|
458
|
+
export const DEFAULT_SHELL_EVIDENCE_FILE = ".lisa/shell-guard-trace.jsonl";
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* What the evidence source says about one shell guard.
|
|
462
|
+
*
|
|
463
|
+
* `notComputed` is the load-bearing member and the reason this vocabulary
|
|
464
|
+
* exists. Until CodySwannGT/lisa#3931 this branch printed "Check that one
|
|
465
|
+
* exists; nothing here did" as a STRING LITERAL, unconditionally — prose
|
|
466
|
+
* asserting a search that never ran. The two answers it collapsed have opposite
|
|
467
|
+
* meanings: "I looked and found nothing" is a finding a reviewer must act on,
|
|
468
|
+
* and "I did not look" is a gap in this gate. A vocabulary without a word for
|
|
469
|
+
* the second inevitably prints the first.
|
|
470
|
+
* @type {Readonly<Record<string, string>>}
|
|
471
|
+
*/
|
|
472
|
+
export const SHELL_EVIDENCE = Object.freeze({
|
|
473
|
+
evidenced: "evidenced",
|
|
474
|
+
allowOnly: "allow-only",
|
|
475
|
+
unevidenced: "unevidenced",
|
|
476
|
+
stale: "stale-observation",
|
|
477
|
+
notComputed: "not-computed",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Parse a shell-guard trace into the statuses observed per script.
|
|
482
|
+
*
|
|
483
|
+
* The trace is JSONL, one `{script, status, origin, sha256?}` record per
|
|
484
|
+
* guard/status pair, appended by the tracer the suites load. Unparseable lines
|
|
485
|
+
* are skipped rather than fatal: a trace is an observation log written during a
|
|
486
|
+
* test run, and one torn append must not turn every other observation into a
|
|
487
|
+
* refusal to answer.
|
|
488
|
+
*
|
|
489
|
+
* This deliberately re-derives only the narrow question this gate asks — "what
|
|
490
|
+
* did the run see THIS file do" — rather than the full population judgement in
|
|
491
|
+
* `scripts/lib/shell-guard-refusal-coverage.mjs`, which needs the tracked-guard
|
|
492
|
+
* roster and is not shipped to consumers. The two must agree on the record
|
|
493
|
+
* shape and nothing else.
|
|
494
|
+
* @param {string} jsonl - Raw trace content.
|
|
495
|
+
* @returns {Map<string, {statuses: Set<number>, hashes: Set<string>}>} Observations by script.
|
|
496
|
+
*/
|
|
497
|
+
export const parseShellGuardTrace = jsonl => {
|
|
498
|
+
/** @type {Map<string, {statuses: Set<number>, hashes: Set<string>}>} */
|
|
499
|
+
const observed = new Map();
|
|
500
|
+
for (const line of String(jsonl ?? "").split("\n")) {
|
|
501
|
+
if (line.trim().length === 0) continue;
|
|
502
|
+
let record;
|
|
503
|
+
try {
|
|
504
|
+
record = JSON.parse(line);
|
|
505
|
+
} catch {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (typeof record?.script !== "string") continue;
|
|
509
|
+
const script = normalizePath(record.script);
|
|
510
|
+
const entry = observed.get(script) ?? {
|
|
511
|
+
statuses: new Set(),
|
|
512
|
+
hashes: new Set(),
|
|
513
|
+
};
|
|
514
|
+
if (typeof record.status === "number") entry.statuses.add(record.status);
|
|
515
|
+
if (typeof record.sha256 === "string") entry.hashes.add(record.sha256);
|
|
516
|
+
observed.set(script, entry);
|
|
517
|
+
}
|
|
518
|
+
return observed;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The sha256 of a file on disk, or null when it cannot be read.
|
|
523
|
+
* @param {string} cwd - Project root.
|
|
524
|
+
* @param {string} file - Repository-relative path.
|
|
525
|
+
* @returns {string | null} Hex digest, or null.
|
|
526
|
+
*/
|
|
527
|
+
export const fileDigest = (cwd, file) => {
|
|
528
|
+
try {
|
|
529
|
+
return createHash("sha256")
|
|
530
|
+
.update(fs.readFileSync(path.join(cwd, file)))
|
|
531
|
+
.digest("hex");
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Load the shell-guard evidence source, or report that there is none.
|
|
539
|
+
*
|
|
540
|
+
* Absence is a first-class answer, not an error. Most consumers of this gate
|
|
541
|
+
* never wire a tracer, and a gate that treated "no trace here" as "no test
|
|
542
|
+
* anywhere" would reproduce exactly the defect it is fixing.
|
|
543
|
+
* @param {string} cwd - Project root.
|
|
544
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment to read.
|
|
545
|
+
* @returns {{source: string, observed: ReturnType<typeof parseShellGuardTrace>} | {source: null, reason: string}}
|
|
546
|
+
* The observations, or why none could be loaded.
|
|
547
|
+
*/
|
|
548
|
+
export const readShellGuardEvidence = (cwd, environment = process.env) => {
|
|
549
|
+
const named = environment[SHELL_EVIDENCE_TRACE_VAR];
|
|
550
|
+
const candidate =
|
|
551
|
+
named && named.length > 0 ? named : DEFAULT_SHELL_EVIDENCE_FILE;
|
|
552
|
+
const resolved = path.isAbsolute(candidate)
|
|
553
|
+
? candidate
|
|
554
|
+
: path.join(cwd, candidate);
|
|
555
|
+
let raw;
|
|
556
|
+
try {
|
|
557
|
+
raw = fs.readFileSync(resolved, "utf8");
|
|
558
|
+
} catch {
|
|
559
|
+
return {
|
|
560
|
+
source: null,
|
|
561
|
+
reason: named
|
|
562
|
+
? `${SHELL_EVIDENCE_TRACE_VAR} names ${candidate}, which could not be read`
|
|
563
|
+
: `no trace at ${DEFAULT_SHELL_EVIDENCE_FILE} and ${SHELL_EVIDENCE_TRACE_VAR} is unset`,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return { source: resolved, observed: parseShellGuardTrace(raw) };
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* What one run observed a single shell guard doing.
|
|
571
|
+
*
|
|
572
|
+
* A hash recorded beside an observation is checked against the file as it
|
|
573
|
+
* stands now: an observation of a DIFFERENT version of the guard is evidence
|
|
574
|
+
* about that version and about nothing else, so a drifted guard reports as
|
|
575
|
+
* stale rather than inheriting its predecessor's coverage. A trace whose
|
|
576
|
+
* records carry no hash cannot be checked this way and is taken at face value —
|
|
577
|
+
* which is stated here rather than assumed, because it is the one place this
|
|
578
|
+
* classification can be too generous.
|
|
579
|
+
* @param {object} input - Everything the classification depends on.
|
|
580
|
+
* @param {string} input.file - Repository-relative guard path.
|
|
581
|
+
* @param {ReturnType<typeof parseShellGuardTrace> | null} input.observed - Observations, or null.
|
|
582
|
+
* @param {string | null} input.currentDigest - The guard's digest now, or null.
|
|
583
|
+
* @returns {{state: string, detail: string}} The verdict and its sentence.
|
|
584
|
+
*/
|
|
585
|
+
export const classifyShellGuardEvidence = ({
|
|
586
|
+
file,
|
|
587
|
+
observed,
|
|
588
|
+
currentDigest,
|
|
589
|
+
}) => {
|
|
590
|
+
if (observed === null) {
|
|
591
|
+
return {
|
|
592
|
+
state: SHELL_EVIDENCE.notComputed,
|
|
593
|
+
detail: "driving-test evidence was NOT COMPUTED here",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const entry = observed.get(normalizePath(file));
|
|
597
|
+
if (entry === undefined) {
|
|
598
|
+
return {
|
|
599
|
+
state: SHELL_EVIDENCE.unevidenced,
|
|
600
|
+
detail: "UNEVIDENCED — the run observed no test executing it",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (
|
|
604
|
+
entry.hashes.size > 0 &&
|
|
605
|
+
currentDigest !== null &&
|
|
606
|
+
!entry.hashes.has(currentDigest)
|
|
607
|
+
) {
|
|
608
|
+
return {
|
|
609
|
+
state: SHELL_EVIDENCE.stale,
|
|
610
|
+
detail:
|
|
611
|
+
"UNEVIDENCED — the only observations are of a different version of this file",
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const statuses = [...entry.statuses].sort((left, right) => left - right);
|
|
615
|
+
const refusals = statuses.filter(status => status !== 0);
|
|
616
|
+
if (refusals.length === 0) {
|
|
617
|
+
return {
|
|
618
|
+
state: SHELL_EVIDENCE.allowOnly,
|
|
619
|
+
detail:
|
|
620
|
+
"ALLOWS-ONLY — driven, but never observed refusing; replace it with `exit 0` and its suite still passes",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (!statuses.includes(0)) {
|
|
624
|
+
return {
|
|
625
|
+
state: SHELL_EVIDENCE.allowOnly,
|
|
626
|
+
detail: `REFUSES-ONLY — observed exiting ${refusals.join("/")} but never allowing ordinary work; the control on the other side is missing`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
state: SHELL_EVIDENCE.evidenced,
|
|
631
|
+
detail: `evidenced — driven to exit ${refusals.join("/")} and exit 0`,
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Describe the driving-test evidence for every uninstrumentable file.
|
|
637
|
+
* @param {readonly string[]} files - Repository-relative guard paths.
|
|
638
|
+
* @param {{source: string, observed: ReturnType<typeof parseShellGuardTrace>} | {source: null, reason: string}} evidence
|
|
639
|
+
* The loaded evidence source, or the reason there is none.
|
|
640
|
+
* @param {(file: string) => string | null} digestOf - Current digest lookup.
|
|
641
|
+
* @returns {{lines: string[], verdicts: {file: string, state: string}[], summary: string}}
|
|
642
|
+
* Per-file lines, the verdicts behind them, and the closing sentence.
|
|
643
|
+
*/
|
|
644
|
+
export const describeShellGuardEvidence = (files, evidence, digestOf) => {
|
|
645
|
+
const observed = evidence.source === null ? null : evidence.observed;
|
|
646
|
+
const verdicts = files.map(file => ({
|
|
647
|
+
file,
|
|
648
|
+
...classifyShellGuardEvidence({
|
|
649
|
+
file,
|
|
650
|
+
observed,
|
|
651
|
+
currentDigest: digestOf(file),
|
|
652
|
+
}),
|
|
653
|
+
}));
|
|
654
|
+
const lines = verdicts.map(
|
|
655
|
+
verdict => ` • ${verdict.file} — ${verdict.detail}`
|
|
656
|
+
);
|
|
657
|
+
return { lines, verdicts, summary: shellEvidenceSummary(verdicts, evidence) };
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The one sentence that replaces the unconditional claim.
|
|
662
|
+
* @param {{file: string, state: string}[]} verdicts - Per-file verdicts.
|
|
663
|
+
* @param {{source: string} | {source: null, reason: string}} evidence - Evidence source.
|
|
664
|
+
* @returns {string} The closing sentence.
|
|
665
|
+
*/
|
|
666
|
+
const shellEvidenceSummary = (verdicts, evidence) => {
|
|
667
|
+
if (evidence.source === null) {
|
|
668
|
+
return (
|
|
669
|
+
" This gate did NOT check whether those tests exist: " +
|
|
670
|
+
`${"reason" in evidence ? evidence.reason : "no evidence source"}.\n` +
|
|
671
|
+
' Read the line above as "not measured", never as "nothing exists".'
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const unproven = verdicts.filter(
|
|
675
|
+
verdict => verdict.state !== SHELL_EVIDENCE.evidenced
|
|
676
|
+
);
|
|
677
|
+
return unproven.length === 0
|
|
678
|
+
? ` Checked against ${evidence.source}: every file above was observed being driven\n` +
|
|
679
|
+
" to a refusal AND to an allow."
|
|
680
|
+
: ` Checked against ${evidence.source}: ${unproven.length} of ${verdicts.length} file(s) above\n` +
|
|
681
|
+
" have NO driving-test evidence in this run. A shell guard nothing drives onto a\n" +
|
|
682
|
+
" refusal path can fail open silently and no gate here will see it.";
|
|
683
|
+
};
|
|
684
|
+
|
|
448
685
|
/**
|
|
449
686
|
* Whether Stryker can parse a file at all, by extension.
|
|
450
687
|
* @param {string} file - Repository-relative path.
|
|
@@ -2228,20 +2465,24 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
|
|
|
2228
2465
|
// Always name the blind part of the diff, even when another selected target
|
|
2229
2466
|
// lets Stryker continue. A mixed run measures only the selected files; its
|
|
2230
2467
|
// score is not evidence about a shell guard changed beside them.
|
|
2231
|
-
const
|
|
2468
|
+
const evidence = describeShellGuardEvidence(
|
|
2469
|
+
scope.uninstrumentable,
|
|
2470
|
+
readShellGuardEvidence(cwd),
|
|
2471
|
+
file => fileDigest(cwd, file)
|
|
2472
|
+
);
|
|
2232
2473
|
console.log(
|
|
2233
2474
|
`⚪ ${OUTCOMES.uninstrumentableLanguage}\n` +
|
|
2234
2475
|
` ${scope.changed} file(s) changed vs ${since}; ${scope.selectedFiles} of them are mutate targets\n` +
|
|
2235
2476
|
` under the patterns from ${declaration.source}, and ${scope.uninstrumentable.length} of them\n` +
|
|
2236
2477
|
` ${scope.uninstrumentable.length === 1 ? "is" : "are"} in a language Stryker cannot instrument in ANY configuration:\n` +
|
|
2237
|
-
`${
|
|
2478
|
+
`${evidence.lines.join("\n")}\n` +
|
|
2238
2479
|
" NO mutant COULD be generated for these files. The mutation result\n" +
|
|
2239
2480
|
" below, if one runs, covers only the selected targets and says nothing\n" +
|
|
2240
2481
|
" for these files, so this gate is silent about them by construction —\n" +
|
|
2241
2482
|
" adding them to `mutate` would abort the run, not measure them.\n" +
|
|
2242
2483
|
" Their only evidence is a driving test that runs the script against a\n" +
|
|
2243
2484
|
" payload table and asserts the blocked/allowed verdict, with a control\n" +
|
|
2244
|
-
|
|
2485
|
+
` on both sides.\n${evidence.summary}`
|
|
2245
2486
|
);
|
|
2246
2487
|
if (scope.selected.length === 0 && scope.noCurrentLines.length === 0)
|
|
2247
2488
|
return finish(OUTCOMES.uninstrumentableLanguage, 0);
|