@codyswann/lisa 4.45.2 → 4.45.3
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-hooks/parity-safety-net.sh +115 -12
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -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 +9 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +115 -12
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +115 -12
- 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-copilot/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +115 -12
- 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/hooks/parity-safety-net.sh +115 -12
- package/scripts/check-typecheck-tests.mjs +229 -72
- package/scripts/lib/dependency-tree.mjs +136 -0
- package/typescript/copy-contents/.husky/pre-push +45 -0
|
@@ -405,7 +405,12 @@ readonly FOLLOW_MAX_TOTAL_BYTES=524288
|
|
|
405
405
|
readonly FOLLOW_MAX_FILES=8
|
|
406
406
|
readonly FOLLOW_MAX_DEPTH=3
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
# Effective working directory established by a literal `cd` in the command, and
|
|
409
|
+
# the flag that says a `cd` happened whose target could NOT be read literally.
|
|
410
|
+
# Empty + 0 means no `cd` has been seen and the session's own cwd governs.
|
|
411
|
+
follow_cwd=""
|
|
412
|
+
follow_cwd_unknown=0
|
|
413
|
+
follow_normalized=""
|
|
409
414
|
follow_paths=$'\n'
|
|
410
415
|
follow_names=""
|
|
411
416
|
follow_count=0
|
|
@@ -420,6 +425,10 @@ see is the thing it exists to stop.
|
|
|
420
425
|
|
|
421
426
|
- Name the script by a LITERAL path that exists and is readable — \`bash
|
|
422
427
|
./scripts/x.sh\` rather than \`bash \"\$SCRIPT\"\` — and the guard can classify it.
|
|
428
|
+
- A leading \`cd\` the guard cannot read literally — \`cd \"\$DIR\" && bash x.sh\` —
|
|
429
|
+
leaves it unable to say WHICH copy of \`x.sh\` would run, and a same-named file
|
|
430
|
+
in another directory is a different file. Spell the directory out, or give the
|
|
431
|
+
script its full path.
|
|
423
432
|
- A file past the inspection cap is refused rather than half-scanned, because a
|
|
424
433
|
truncated scan reports a confident ALLOW about text it never read. Split it,
|
|
425
434
|
or run it manually outside the agent.
|
|
@@ -550,15 +559,77 @@ follow_split() {
|
|
|
550
559
|
'
|
|
551
560
|
}
|
|
552
561
|
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
# Collapse `.`, `..` and empty segments so the path a verdict NAMES is the one a
|
|
563
|
+
# reader recognises. Purely lexical, and it cannot move a verdict: every
|
|
564
|
+
# component has already been proven to exist by the `-d` test that follows, and
|
|
565
|
+
# the `-f`/`-r` tests are applied to the collapsed path either way.
|
|
566
|
+
follow_normalize() {
|
|
567
|
+
local p="$1" out="" seg
|
|
568
|
+
local IFS=/
|
|
569
|
+
set -f
|
|
570
|
+
# The word split is the point: `$p` is being addressed one path segment at a
|
|
571
|
+
# time, and the glob guard above keeps a literal `*` from expanding.
|
|
572
|
+
# shellcheck disable=SC2086
|
|
573
|
+
for seg in $p; do
|
|
574
|
+
case "$seg" in
|
|
575
|
+
"" | .) ;;
|
|
576
|
+
..) out="${out%/*}" ;;
|
|
577
|
+
*) out="$out/$seg" ;;
|
|
578
|
+
esac
|
|
579
|
+
done
|
|
580
|
+
set +f
|
|
581
|
+
follow_normalized="${out:-/}"
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Track the working directory a literal `cd` establishes, so a relative executed
|
|
585
|
+
# path resolves against the directory the command will actually have.
|
|
586
|
+
#
|
|
587
|
+
# This REPLACES the session's own cwd rather than adding to it. It used to be an
|
|
588
|
+
# extra base appended AFTER `$PWD`, so `$PWD` always won and
|
|
589
|
+
# `cd <other-checkout> && bash scripts/x.sh` was classified against the session
|
|
590
|
+
# checkout's same-named copy. That is not a resolution failure — it succeeds,
|
|
591
|
+
# against a real and readable file, and the verdict is reported with full
|
|
592
|
+
# confidence about text nobody was about to run (CodySwannGT/lisa#3933). Two
|
|
593
|
+
# checkouts of one repository differ by exactly the work in flight, which is the
|
|
594
|
+
# entire point of the second one, so the copy that would RUN is the only copy
|
|
595
|
+
# whose verdict means anything. Wrong in both directions, and the expensive
|
|
596
|
+
# direction is silent: the dangerous line sits in the copy that would run and
|
|
597
|
+
# the guard reads the pristine one.
|
|
598
|
+
#
|
|
599
|
+
# A target this hook cannot turn into a literal directory marks the cwd UNKNOWN
|
|
600
|
+
# instead of leaving a stale one in place. `follow_locate` then refuses a
|
|
601
|
+
# relative token rather than guessing, which is the honest-refusal doctrine the
|
|
602
|
+
# rest of this block already follows: a confident scan of the wrong file is
|
|
603
|
+
# strictly worse than saying it could not tell.
|
|
604
|
+
follow_cd() {
|
|
605
|
+
local d="$1" home="${HOME:-}"
|
|
606
|
+
# The tilde arms are LITERAL text — the unexpanded spelling the hook was
|
|
607
|
+
# handed — for the reason follow_locate's arms document.
|
|
608
|
+
# shellcheck disable=SC2088
|
|
609
|
+
case "$d" in
|
|
610
|
+
'~') d="$home" ;;
|
|
611
|
+
'~/'*) d="$home/${d#'~/'}" ;;
|
|
612
|
+
esac
|
|
613
|
+
case "$d" in
|
|
614
|
+
"" | -* | *'$'* | *'*'* | *'?'* | *'`'* | *'{'* | *'['*)
|
|
615
|
+
follow_cwd=""
|
|
616
|
+
follow_cwd_unknown=1
|
|
617
|
+
return 0
|
|
618
|
+
;;
|
|
619
|
+
esac
|
|
557
620
|
case "$d" in
|
|
558
|
-
|
|
621
|
+
/*) ;;
|
|
622
|
+
*) d="${follow_cwd:-$PWD}/$d" ;;
|
|
559
623
|
esac
|
|
560
|
-
|
|
561
|
-
|
|
624
|
+
follow_normalize "$d"
|
|
625
|
+
d="$follow_normalized"
|
|
626
|
+
if [ -d "$d" ]; then
|
|
627
|
+
follow_cwd="$d"
|
|
628
|
+
follow_cwd_unknown=0
|
|
629
|
+
else
|
|
630
|
+
follow_cwd=""
|
|
631
|
+
follow_cwd_unknown=1
|
|
632
|
+
fi
|
|
562
633
|
}
|
|
563
634
|
|
|
564
635
|
# Resolve one executed-file operand to a readable regular file, or fail.
|
|
@@ -571,6 +642,7 @@ follow_add_base() {
|
|
|
571
642
|
# this hook cannot evaluate.
|
|
572
643
|
follow_locate() {
|
|
573
644
|
local token="$1" home="${HOME:-}" tmp="${TMPDIR:-/tmp}" proj="${CLAUDE_PROJECT_DIR:-$PWD}" base
|
|
645
|
+
local cwd="${follow_cwd:-$PWD}"
|
|
574
646
|
follow_located=""
|
|
575
647
|
tmp="${tmp%/}"
|
|
576
648
|
[ -n "$tmp" ] || tmp="/tmp"
|
|
@@ -584,8 +656,14 @@ follow_locate() {
|
|
|
584
656
|
'${TMPDIR}' | '${TMPDIR}'/*) token="$tmp${token#'${TMPDIR}'}" ;;
|
|
585
657
|
'$HOME' | '$HOME'/*) token="$home${token#'$HOME'}" ;;
|
|
586
658
|
'${HOME}' | '${HOME}'/*) token="$home${token#'${HOME}'}" ;;
|
|
587
|
-
'$PWD' | '$PWD'/*)
|
|
588
|
-
|
|
659
|
+
'$PWD' | '$PWD'/*)
|
|
660
|
+
[ "$follow_cwd_unknown" -eq 0 ] || return 1
|
|
661
|
+
token="$cwd${token#'$PWD'}"
|
|
662
|
+
;;
|
|
663
|
+
'${PWD}' | '${PWD}'/*)
|
|
664
|
+
[ "$follow_cwd_unknown" -eq 0 ] || return 1
|
|
665
|
+
token="$cwd${token#'${PWD}'}"
|
|
666
|
+
;;
|
|
589
667
|
'$CLAUDE_PROJECT_DIR' | '$CLAUDE_PROJECT_DIR'/*) token="$proj${token#'$CLAUDE_PROJECT_DIR'}" ;;
|
|
590
668
|
'${CLAUDE_PROJECT_DIR}' | '${CLAUDE_PROJECT_DIR}'/*) token="$proj${token#'${CLAUDE_PROJECT_DIR}'}" ;;
|
|
591
669
|
esac
|
|
@@ -597,13 +675,26 @@ follow_locate() {
|
|
|
597
675
|
return 0
|
|
598
676
|
;;
|
|
599
677
|
esac
|
|
678
|
+
# Past this point the token is RELATIVE, so which directory it is resolved
|
|
679
|
+
# from decides which file the verdict is about.
|
|
680
|
+
if [ "$follow_cwd_unknown" -eq 1 ]; then
|
|
681
|
+
return 1
|
|
682
|
+
fi
|
|
683
|
+
if [ -n "$follow_cwd" ]; then
|
|
684
|
+
# A literal `cd` names the directory the shell will be in. It is the only
|
|
685
|
+
# candidate — falling back to `$PWD` here is what made the guard report
|
|
686
|
+
# confidently about a same-named file in another checkout.
|
|
687
|
+
[ -f "$follow_cwd/$token" ] && [ -r "$follow_cwd/$token" ] || return 1
|
|
688
|
+
follow_located="$follow_cwd/$token"
|
|
689
|
+
return 0
|
|
690
|
+
fi
|
|
600
691
|
while IFS= read -r base; do
|
|
601
692
|
[ -n "$base" ] || continue
|
|
602
693
|
if [ -f "$base/$token" ] && [ -r "$base/$token" ]; then
|
|
603
694
|
follow_located="$base/$token"
|
|
604
695
|
return 0
|
|
605
696
|
fi
|
|
606
|
-
done <<<"$PWD"$'\n'"$proj
|
|
697
|
+
done <<<"$PWD"$'\n'"$proj"
|
|
607
698
|
return 1
|
|
608
699
|
}
|
|
609
700
|
|
|
@@ -636,7 +727,15 @@ follow_take() {
|
|
|
636
727
|
follow_names="$follow_names $path"
|
|
637
728
|
followed_text="$followed_text"$'\n'"$content"
|
|
638
729
|
if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
|
|
730
|
+
# A `cd` inside the followed script governs what THAT script goes on to
|
|
731
|
+
# execute, and nothing after it in the command that ran the script — the
|
|
732
|
+
# child inherits this directory but cannot change the parent's. Saving and
|
|
733
|
+
# restoring is what keeps a nested `cd` from silently re-pointing the rest
|
|
734
|
+
# of the outer walk.
|
|
735
|
+
local saved_cwd="$follow_cwd" saved_unknown="$follow_cwd_unknown"
|
|
639
736
|
follow_scan "$content" "$((depth + 1))"
|
|
737
|
+
follow_cwd="$saved_cwd"
|
|
738
|
+
follow_cwd_unknown="$saved_unknown"
|
|
640
739
|
fi
|
|
641
740
|
}
|
|
642
741
|
|
|
@@ -861,7 +960,11 @@ follow_scan() {
|
|
|
861
960
|
cd)
|
|
862
961
|
if [ $((i + 1)) -lt "$n" ]; then
|
|
863
962
|
follow_unwrap "${toks[i + 1]}"
|
|
864
|
-
|
|
963
|
+
follow_cd "$follow_unwrapped"
|
|
964
|
+
else
|
|
965
|
+
# A bare `cd` is `cd "$HOME"`, and leaving the previous directory in
|
|
966
|
+
# place would resolve later tokens from a directory the shell left.
|
|
967
|
+
follow_cd "${HOME:-}"
|
|
865
968
|
fi
|
|
866
969
|
cmd_pos=0
|
|
867
970
|
i=$((i + 1))
|
|
@@ -27,6 +27,33 @@
|
|
|
27
27
|
* threshold-ratchet as an `allow-list` family, so *adding* an entry is flagged
|
|
28
28
|
* by a gate somebody already reads.
|
|
29
29
|
*
|
|
30
|
+
* IT REFUSES TO ANSWER FROM AN UNRESOLVABLE TREE (CodySwannGT/lisa#3913). The
|
|
31
|
+
* second of those ratchet directions is the destructive one: it names files and
|
|
32
|
+
* tells a reader to delete their quarantine entries. With an empty
|
|
33
|
+
* `node_modules` the compiler cannot start, nothing is compiled, no diagnostic
|
|
34
|
+
* is parsed — and "no diagnostic for this file" is exactly the shape of "this
|
|
35
|
+
* file was fixed". The gate reported all 370 quarantined files as cleared and
|
|
36
|
+
* instructed their removal, which is a destructive edit ordered on the strength
|
|
37
|
+
* of a comparison whose inputs never resolved.
|
|
38
|
+
*
|
|
39
|
+
* So there are two ways this refuses instead of reporting, and the second is
|
|
40
|
+
* the general one:
|
|
41
|
+
*
|
|
42
|
+
* - the compiler is not present at the path this spawns it from; and
|
|
43
|
+
* - the compiler signalled failure and produced NOT ONE parseable
|
|
44
|
+
* diagnostic, which also covers a broken tsconfig, an OOM and a crash.
|
|
45
|
+
*
|
|
46
|
+
* Both exit non-zero with the denial first and NO file list, because a gate
|
|
47
|
+
* whose only surface is an exit code cannot invent a third one — see
|
|
48
|
+
* `scripts/lib/dependency-tree.mjs` for why the wording is a contract rather
|
|
49
|
+
* than a string.
|
|
50
|
+
*
|
|
51
|
+
* `--root` EXISTS SO THIS CAN BE POINTED AT A FIXTURE. Deriving the root solely
|
|
52
|
+
* from `import.meta.url` made the failure above reproducible only by copying
|
|
53
|
+
* this file somewhere else, and `check-empty-subject-guards.mjs` reads that
|
|
54
|
+
* token out of this header to decide the gate is probeable at all — without it
|
|
55
|
+
* this sits in that sweep's declared blind spot.
|
|
56
|
+
*
|
|
30
57
|
* WHAT IS DELIBERATELY NOT DONE HERE: the declared `lib` is not widened. tsc
|
|
31
58
|
* emits "Try changing the 'lib' compiler option to 'es2023' or later" on the
|
|
32
59
|
* ES2023 array APIs, and taking that suggestion would have erased the
|
|
@@ -38,110 +65,240 @@
|
|
|
38
65
|
import { spawnSync } from "node:child_process";
|
|
39
66
|
import { readFileSync } from "node:fs";
|
|
40
67
|
import * as path from "node:path";
|
|
68
|
+
import process from "node:process";
|
|
41
69
|
import { fileURLToPath } from "node:url";
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
import {
|
|
72
|
+
cannotMeasure,
|
|
73
|
+
INSTALL_COMMAND,
|
|
74
|
+
missingDependencies,
|
|
75
|
+
ranVacuously,
|
|
76
|
+
} from "./lib/dependency-tree.mjs";
|
|
77
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
78
|
+
|
|
79
|
+
const DEFAULT_ROOT = path.resolve(
|
|
80
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
81
|
+
".."
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The gate's name, as it prints in both the report and the refusal. */
|
|
85
|
+
const GATE = "type-correctness (tests)";
|
|
86
|
+
|
|
87
|
+
/** Where the compiler is spawned from, relative to the root. */
|
|
88
|
+
const TSC = path.join("node_modules", "typescript", "bin", "tsc");
|
|
46
89
|
|
|
47
90
|
/** Matches `path/to/file.ts(12,34): error TS1234: message`. */
|
|
48
91
|
const ERROR_LINE = /^(?<file>[^(]+)\((?<line>\d+),\d+\): error (?<code>TS\d+):/;
|
|
49
92
|
|
|
50
93
|
/**
|
|
51
94
|
* Read the quarantined paths.
|
|
52
|
-
* @
|
|
95
|
+
* @param {string} root - The directory holding the quarantine.
|
|
96
|
+
* @returns {{files: string[], reason: string} | null} The parsed quarantine, or
|
|
97
|
+
* `null` when it could not be read — an unreadable list is a refusal, not a
|
|
98
|
+
* pass, because that file is what makes this gate green.
|
|
53
99
|
*/
|
|
54
|
-
function readQuarantine() {
|
|
100
|
+
function readQuarantine(root) {
|
|
101
|
+
const quarantine = path.join(root, "typecheck-quarantine.json");
|
|
55
102
|
try {
|
|
56
|
-
const parsed = JSON.parse(readFileSync(
|
|
103
|
+
const parsed = JSON.parse(readFileSync(quarantine, "utf-8"));
|
|
57
104
|
return { files: parsed.files ?? [], reason: parsed.reason ?? "" };
|
|
58
105
|
} catch (error) {
|
|
59
106
|
console.error(
|
|
60
|
-
`check-typecheck-tests: cannot read ${path.relative(
|
|
107
|
+
`check-typecheck-tests: cannot read ${path.relative(root, quarantine)} — ${error.message}`
|
|
61
108
|
);
|
|
62
109
|
console.error(
|
|
63
110
|
"That file is what makes this gate green, so an unreadable one is a refusal, not a pass."
|
|
64
111
|
);
|
|
65
|
-
|
|
112
|
+
return null;
|
|
66
113
|
}
|
|
67
114
|
}
|
|
68
115
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error(
|
|
85
|
-
`check-typecheck-tests: could not run tsc — ${tsc.error.message}`
|
|
86
|
-
);
|
|
87
|
-
process.exit(1);
|
|
116
|
+
/**
|
|
117
|
+
* Parse tsc's output into a per-file error count.
|
|
118
|
+
* @param {string} output - Combined stdout and stderr.
|
|
119
|
+
* @returns {Map<string, number>} Errors keyed by forward-slashed path.
|
|
120
|
+
*/
|
|
121
|
+
function parseDiagnostics(output) {
|
|
122
|
+
/** @type {Map<string, number>} */
|
|
123
|
+
const errorsByFile = new Map();
|
|
124
|
+
for (const line of output.split("\n")) {
|
|
125
|
+
const match = ERROR_LINE.exec(line.trim());
|
|
126
|
+
if (!match?.groups) continue;
|
|
127
|
+
const file = match.groups.file.replace(/\\/g, "/");
|
|
128
|
+
errorsByFile.set(file, (errorsByFile.get(file) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return errorsByFile;
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The refusal issued when nothing was compiled.
|
|
135
|
+
*
|
|
136
|
+
* The denial names the destructive remedy explicitly. "Nothing type-checked" is
|
|
137
|
+
* the inference a reader draws from an empty diagnostic set, and the gate's own
|
|
138
|
+
* cleared-entry message is what turns that inference into a deletion — so this
|
|
139
|
+
* has to deny both, not just the first.
|
|
140
|
+
*
|
|
141
|
+
* NO FILE IS NAMED HERE, deliberately. A list of paths is what makes the
|
|
142
|
+
* destructive message actionable, and a refusal that still printed 370 paths
|
|
143
|
+
* would be followed exactly as often as the finding it replaces.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} because - Why the measurement could not happen.
|
|
146
|
+
* @param {number} quarantined - How many entries went unexamined.
|
|
147
|
+
* @param {string} fix - The action that repairs THIS cause. Named per call
|
|
148
|
+
* site rather than fixed, because telling someone to install dependencies
|
|
149
|
+
* they already have is how a refusal gets dismissed as noise.
|
|
150
|
+
* @returns {string} The refusal block.
|
|
151
|
+
*/
|
|
152
|
+
function refuseUnmeasurable(because, quarantined, fix) {
|
|
153
|
+
return cannotMeasure({
|
|
154
|
+
gate: GATE,
|
|
155
|
+
denial: `that any of the ${quarantined} quarantined file(s) now type-check`,
|
|
156
|
+
because,
|
|
157
|
+
remedy:
|
|
158
|
+
`${fix} Do NOT edit typecheck-quarantine.json on the strength of ` +
|
|
159
|
+
`this — no file was compiled, so nothing was established about any ` +
|
|
160
|
+
`entry in it.`,
|
|
161
|
+
});
|
|
98
162
|
}
|
|
99
163
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Run the gate.
|
|
166
|
+
* @param {string} root - Directory to resolve the project and quarantine under.
|
|
167
|
+
* @returns {number} The process exit code.
|
|
168
|
+
*/
|
|
169
|
+
export function main(root) {
|
|
170
|
+
const quarantine = readQuarantine(root);
|
|
171
|
+
if (quarantine === null) return 1;
|
|
172
|
+
const quarantined = quarantine.files;
|
|
173
|
+
const exempt = new Set(quarantined);
|
|
104
174
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
175
|
+
// PRECONDITION. Checked before the spawn rather than inferred from it: the
|
|
176
|
+
// spawn SUCCEEDS with an empty `node_modules` — node starts, and it is node
|
|
177
|
+
// that cannot find the module — so `tsc.error` never fires and the failure
|
|
178
|
+
// arrives as an empty diagnostic set indistinguishable from a clean run.
|
|
179
|
+
const missing = missingDependencies(root, [TSC]);
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
console.error(
|
|
182
|
+
refuseUnmeasurable(
|
|
183
|
+
`the TypeScript compiler is not installed — no file at ${missing[0]}.`,
|
|
184
|
+
quarantined.length,
|
|
185
|
+
`Run \`${INSTALL_COMMAND}\`, then re-run this gate.`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
110
190
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
const tsc = spawnSync(
|
|
192
|
+
process.execPath,
|
|
193
|
+
[
|
|
194
|
+
path.join(root, TSC),
|
|
195
|
+
"--noEmit",
|
|
196
|
+
"-p",
|
|
197
|
+
path.join(root, "tsconfig.tests.json"),
|
|
198
|
+
],
|
|
199
|
+
{ cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
115
200
|
);
|
|
116
|
-
|
|
117
|
-
|
|
201
|
+
|
|
202
|
+
if (tsc.error) {
|
|
203
|
+
console.error(
|
|
204
|
+
`check-typecheck-tests: could not run tsc — ${tsc.error.message}`
|
|
205
|
+
);
|
|
206
|
+
return 1;
|
|
118
207
|
}
|
|
119
|
-
console.error("");
|
|
120
|
-
console.error(
|
|
121
|
-
"Fix them. Do NOT add them to typecheck-quarantine.json — that list"
|
|
122
|
-
);
|
|
123
|
-
console.error(
|
|
124
|
-
"only shrinks, and the threshold-ratchet flags additions to it."
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
208
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.error(
|
|
131
|
-
`❌ ${cleared.length} quarantined file(s) now type-check and must leave the list:`
|
|
209
|
+
const errorsByFile = parseDiagnostics(
|
|
210
|
+
`${tsc.stdout ?? ""}\n${tsc.stderr ?? ""}`
|
|
132
211
|
);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
212
|
+
|
|
213
|
+
// POSTCONDITION, and the general form of the same defect. The compiler
|
|
214
|
+
// signalled failure and produced nothing this gate can read, so the
|
|
215
|
+
// comparison below would run against an empty set — the exact shape of "every
|
|
216
|
+
// quarantined file is fixed". A ZERO exit with no diagnostics is not this: it
|
|
217
|
+
// is a clean run, and it reports normally.
|
|
218
|
+
if (ranVacuously(tsc.status, errorsByFile.size)) {
|
|
219
|
+
const firstLine = `${tsc.stderr ?? ""}${tsc.stdout ?? ""}`
|
|
220
|
+
.split("\n")
|
|
221
|
+
.map(line => line.trim())
|
|
222
|
+
.find(Boolean);
|
|
223
|
+
console.error(
|
|
224
|
+
refuseUnmeasurable(
|
|
225
|
+
`tsc exited ${tsc.status} without emitting one diagnostic this gate ` +
|
|
226
|
+
`could parse, so the comparison had no subject.` +
|
|
227
|
+
`${firstLine ? ` First output line: ${firstLine}` : ""}`,
|
|
228
|
+
quarantined.length,
|
|
229
|
+
"Fix the cause named above, then re-run this gate."
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const offenders = [...errorsByFile.keys()]
|
|
236
|
+
.filter(file => !exempt.has(file))
|
|
237
|
+
.sort();
|
|
238
|
+
const cleared = quarantined.filter(file => !errorsByFile.has(file)).sort();
|
|
239
|
+
|
|
240
|
+
const totalErrors = [...errorsByFile.values()].reduce((sum, n) => sum + n, 0);
|
|
241
|
+
console.log(
|
|
242
|
+
`${GATE}: ${errorsByFile.size} file(s) with ${totalErrors} error(s); ` +
|
|
243
|
+
`${quarantined.length} quarantined.`
|
|
137
244
|
);
|
|
138
|
-
|
|
139
|
-
|
|
245
|
+
|
|
246
|
+
if (offenders.length > 0) {
|
|
247
|
+
console.error("");
|
|
248
|
+
console.error(
|
|
249
|
+
`❌ ${offenders.length} file(s) outside the quarantine have type errors:`
|
|
250
|
+
);
|
|
251
|
+
for (const file of offenders) {
|
|
252
|
+
console.error(` ${file} (${errorsByFile.get(file)})`);
|
|
253
|
+
}
|
|
254
|
+
console.error("");
|
|
255
|
+
console.error(
|
|
256
|
+
"Fix them. Do NOT add them to typecheck-quarantine.json — that list"
|
|
257
|
+
);
|
|
258
|
+
console.error(
|
|
259
|
+
"only shrinks, and the threshold-ratchet flags additions to it."
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (cleared.length > 0) {
|
|
264
|
+
console.error("");
|
|
265
|
+
console.error(
|
|
266
|
+
`❌ ${cleared.length} quarantined file(s) now type-check and must leave the list:`
|
|
267
|
+
);
|
|
268
|
+
for (const file of cleared) console.error(` ${file}`);
|
|
269
|
+
console.error("");
|
|
270
|
+
console.error(
|
|
271
|
+
"Remove them from typecheck-quarantine.json. A list that keeps entries"
|
|
272
|
+
);
|
|
273
|
+
console.error(
|
|
274
|
+
"after they are fixed would re-authorise them if they ever regressed."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (offenders.length > 0 || cleared.length > 0) return 1;
|
|
279
|
+
|
|
280
|
+
console.log(
|
|
281
|
+
"✅ No type errors outside the quarantine, and no stale quarantine entries."
|
|
140
282
|
);
|
|
283
|
+
return 0;
|
|
141
284
|
}
|
|
142
285
|
|
|
143
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Resolve the root from `--root <dir>`, defaulting to the repository.
|
|
288
|
+
* @param {ReadonlyArray<string>} argv - Arguments after the script name.
|
|
289
|
+
* @returns {string} The absolute root.
|
|
290
|
+
*/
|
|
291
|
+
export function rootFrom(argv) {
|
|
292
|
+
const flag = argv.indexOf("--root");
|
|
293
|
+
if (flag === -1) return DEFAULT_ROOT;
|
|
294
|
+
const value = argv[flag + 1];
|
|
295
|
+
if (value === undefined) {
|
|
296
|
+
console.error("check-typecheck-tests: --root requires a value");
|
|
297
|
+
process.exit(2);
|
|
298
|
+
}
|
|
299
|
+
return path.resolve(value);
|
|
300
|
+
}
|
|
144
301
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
302
|
+
if (invokedAsScript(import.meta.url)) {
|
|
303
|
+
process.exit(main(rootFrom(process.argv.slice(2))));
|
|
304
|
+
}
|