@codyswann/lisa 4.38.1 → 4.38.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/block-direct-issue-create.sh +140 -4
- 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 +11 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +9 -0
- package/package.json +6 -5
- package/plugins/lisa/.claude-plugin/plugin.json +10 -5
- package/plugins/lisa/.codex-plugin/hooks.json +9 -4
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/lisa/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa/hooks/threshold-ratchet-families.mjs +8 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +140 -4
- 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 +10 -5
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +8 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/lisa-cursor/hooks/hooks.json +3 -4
- package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +8 -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 +9 -4
- package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +13 -3
- package/plugins/src/base/hooks/block-direct-issue-create.sh +140 -4
- package/plugins/src/base/hooks/threshold-ratchet-families.mjs +8 -0
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +8 -0
- package/scripts/check-typecheck-tests.mjs +147 -0
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +8 -0
|
@@ -138,10 +138,6 @@
|
|
|
138
138
|
"type": "command",
|
|
139
139
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-instruction-file-edits.sh"
|
|
140
140
|
},
|
|
141
|
-
{
|
|
142
|
-
"type": "command",
|
|
143
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
|
|
144
|
-
},
|
|
145
141
|
{
|
|
146
142
|
"type": "command",
|
|
147
143
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-blind-automerge.sh"
|
|
@@ -178,6 +174,15 @@
|
|
|
178
174
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-verification-gate.sh"
|
|
179
175
|
}
|
|
180
176
|
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"matcher": "",
|
|
180
|
+
"hooks": [
|
|
181
|
+
{
|
|
182
|
+
"type": "command",
|
|
183
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-direct-issue-create.sh"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
181
186
|
}
|
|
182
187
|
],
|
|
183
188
|
"Stop": [
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
# block-direct-issue-create.sh beside it (same delegation pattern as
|
|
9
9
|
# block-instruction-file-edits.agy.sh).
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# Bash-only by construction, and
|
|
13
|
-
#
|
|
11
|
+
# SCOPE GAP, recorded rather than silently dropped (AGENTS.md). This used to
|
|
12
|
+
# read "no scope gap here: the canonical guard is Bash-only by construction, and
|
|
13
|
+
# `run_command` is exactly the surface it needs." That stopped being true in
|
|
14
|
+
# CodySwannGT/lisa#3753: the canonical guard now covers a SECOND substrate —
|
|
15
|
+
# structured tool payloads, where a creation arrives as named fields rather than
|
|
16
|
+
# as a command line — because the filing contract was enforced on the shell and
|
|
17
|
+
# unenforced on every tool call beside it.
|
|
18
|
+
#
|
|
19
|
+
# This adapter forwards `run_command` and nothing else, so on agy that second
|
|
20
|
+
# substrate is still uncovered. Closing it needs agy's own tool-call envelope
|
|
21
|
+
# (which tool names its creation surface, and where the arguments land), and
|
|
22
|
+
# that is a different piece of work from translating one protocol. The shell
|
|
23
|
+
# surface agy does expose receives the whole guard, exactly as before.
|
|
14
24
|
#
|
|
15
25
|
# Fail-open on missing runtimes: a missed refusal costs one under-declared
|
|
16
26
|
# ticket, and `lisa-repair-intake` already sweeps for exactly that. Failing
|
|
@@ -79,6 +79,36 @@
|
|
|
79
79
|
# settings env block, CI config). An escape the governed agent reaches by
|
|
80
80
|
# typing one more token in front of the command it was just refused is not an
|
|
81
81
|
# escape hatch — it is the prose problem with extra steps.
|
|
82
|
+
#
|
|
83
|
+
# ## What this hook costs, and the number that would change the decision
|
|
84
|
+
#
|
|
85
|
+
# Registered `matcher: ""` since CodySwannGT/lisa#3753 — it runs on EVERY tool
|
|
86
|
+
# call, not only Bash. Measured on a developer machine at load ~8-26, 50
|
|
87
|
+
# invocations per row:
|
|
88
|
+
#
|
|
89
|
+
# 16.7 ms enforce-team-first.sh already `matcher: ""` today
|
|
90
|
+
# 17.5 ms enforce-verification-gate.sh already `matcher: ""` today
|
|
91
|
+
# 11.5 ms this guard, non-Bash payload what the broad matcher adds
|
|
92
|
+
# 112.3 ms this guard, Bash payload what Bash calls already paid
|
|
93
|
+
#
|
|
94
|
+
# So the broad registration was not novel — two hooks were already there, this
|
|
95
|
+
# one is cheaper than either, and it raises an existing ~34 ms per-call baseline
|
|
96
|
+
# by about a third.
|
|
97
|
+
#
|
|
98
|
+
# THIS IS A THRESHOLD, NOT A PRECEDENT. The argument is about the TOTAL cost of
|
|
99
|
+
# the broad set, so it does not transfer to the next guard: four more hooks
|
|
100
|
+
# moved to `matcher: ""` would put ~90 ms on every tool call and the answer
|
|
101
|
+
# flips. **Before widening any other hook's matcher, re-run the measurement and
|
|
102
|
+
# reconsider if the broad set totals more than ~100 ms per tool call, or if any
|
|
103
|
+
# single broad hook exceeds ~50 ms.** Do not cite these numbers for a fifth
|
|
104
|
+
# hook; measure again, because the number that matters is the sum.
|
|
105
|
+
#
|
|
106
|
+
# The cost is also irreducible rather than sloppy. A 9-line script containing
|
|
107
|
+
# only this file's preamble measured 11.7 ms, and padding it to this file's
|
|
108
|
+
# length changed nothing (11.5 ms) — it is process spawn plus one `jq`, not
|
|
109
|
+
# parsing. Moving the early exit further up the file buys nothing; the exit is
|
|
110
|
+
# already ahead of config resolution, which is what keeps the figure at 11.5
|
|
111
|
+
# rather than at the 112 ms the Bash path pays.
|
|
82
112
|
set -euo pipefail
|
|
83
113
|
|
|
84
114
|
input="$(cat)"
|
|
@@ -99,13 +129,72 @@ for required in jq python3; do
|
|
|
99
129
|
done
|
|
100
130
|
|
|
101
131
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
132
|
+
|
|
133
|
+
# ── The two substrates ────────────────────────────────────────────────────
|
|
134
|
+
#
|
|
135
|
+
# A creation reaches a tracker two ways, and until CodySwannGT/lisa#3753 this
|
|
136
|
+
# guard saw only one of them. It was registered `matcher: "Bash"` AND gated
|
|
137
|
+
# here on `tool_name != "Bash"`, so an MCP tool call was refused entry twice
|
|
138
|
+
# over. MEASURED before the change, driving the guard with synthetic payloads:
|
|
139
|
+
#
|
|
140
|
+
# ALLOW mcp__linear-server__create_issue, undeclared <- the defect
|
|
141
|
+
# ALLOW mcp__github__create_issue, undeclared <- the defect
|
|
142
|
+
# REFUSE the same creation as `gh issue create` shell text <- control
|
|
143
|
+
# ALLOW an ordinary Read tool call <- control
|
|
144
|
+
#
|
|
145
|
+
# The two controls are what make those ALLOWs mean anything: the refusal proves
|
|
146
|
+
# the guard is live, the Read proves it is not simply refusing everything.
|
|
147
|
+
#
|
|
148
|
+
# **Widening the matcher alone would have changed nothing** — the call would
|
|
149
|
+
# arrive and this gate would still exit 0. Anyone who "fixed" #3753 by editing
|
|
150
|
+
# `plugin.json` would have seen no test fail and shipped a guard that still
|
|
151
|
+
# allows every MCP filing. The registration and this gate had to move together.
|
|
152
|
+
#
|
|
153
|
+
# The SHELL path below is untouched. A structured payload has no command line
|
|
154
|
+
# to tokenise, so it gets its own small classifier rather than being forced
|
|
155
|
+
# through a parser built for shell text.
|
|
156
|
+
structured_call=""
|
|
102
157
|
if [ "$tool_name" != "Bash" ]; then
|
|
103
|
-
|
|
158
|
+
# The cheap shape gate, deliberately BEFORE config resolution.
|
|
159
|
+
#
|
|
160
|
+
# This hook now runs on EVERY tool call, so the cost of the path that does
|
|
161
|
+
# nothing is the cost of the whole change. Config resolution spawns two jq
|
|
162
|
+
# processes per lookup and is what makes the Bash path ~112 ms; putting it
|
|
163
|
+
# ahead of this gate would charge that to every Read and Grep. So the
|
|
164
|
+
# not-a-creation exit happens here, on a shell `case` with no subprocess.
|
|
165
|
+
#
|
|
166
|
+
# Matched on SHAPE, not on an enumerated list of server tool names. Server
|
|
167
|
+
# names are supplied by whoever wrote the server and change when one is
|
|
168
|
+
# added or renamed, so a list would pass every row anyone thought of and
|
|
169
|
+
# miss the first one nobody did — the failure mode this repository's guard
|
|
170
|
+
# suites are written to defeat.
|
|
171
|
+
#
|
|
172
|
+
# RESIDUAL, stated rather than hidden: a server whose creation tool is named
|
|
173
|
+
# without a create-verb or without a tracker noun (`mcp__x__file_work`) is
|
|
174
|
+
# not recognised and is allowed. That is a fail-open, and it is the honest
|
|
175
|
+
# cost of refusing to enumerate. Add shapes here as they are measured.
|
|
176
|
+
case "$tool_name" in
|
|
177
|
+
Bash | Read | Write | Edit | MultiEdit | Glob | Grep | Task | TodoWrite)
|
|
178
|
+
exit 0
|
|
179
|
+
;;
|
|
180
|
+
esac
|
|
181
|
+
case "$tool_name" in
|
|
182
|
+
*[Cc]reate* | *[Nn]ew* | *[Aa]dd* | *[Ff]ile*) ;;
|
|
183
|
+
*) exit 0 ;;
|
|
184
|
+
esac
|
|
185
|
+
case "$tool_name" in
|
|
186
|
+
*[Ii]ssue* | *[Tt]icket* | *[Tt]ask* | *[Ss]tory* | *[Bb]ug* | *[Ee]pic* | *[Ww]ork*) ;;
|
|
187
|
+
*) exit 0 ;;
|
|
188
|
+
esac
|
|
189
|
+
structured_call="1"
|
|
104
190
|
fi
|
|
105
191
|
|
|
106
|
-
command_str="
|
|
107
|
-
if [ -z "$
|
|
108
|
-
|
|
192
|
+
command_str=""
|
|
193
|
+
if [ -z "$structured_call" ]; then
|
|
194
|
+
command_str="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
195
|
+
if [ -z "$command_str" ]; then
|
|
196
|
+
exit 0
|
|
197
|
+
fi
|
|
109
198
|
fi
|
|
110
199
|
|
|
111
200
|
project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
@@ -1859,6 +1948,53 @@ print("ALLOW")
|
|
|
1859
1948
|
PY
|
|
1860
1949
|
|
|
1861
1950
|
set +e
|
|
1951
|
+
# ── The structured substrate ──────────────────────────────────────────────
|
|
1952
|
+
#
|
|
1953
|
+
# An MCP call carries named fields, not a command line, so `scan()` — which
|
|
1954
|
+
# tokenises shell text — has nothing to parse. The declaration is read from the
|
|
1955
|
+
# payload instead, and the recogniser is deliberately small: this path decides
|
|
1956
|
+
# ONE question (is a build-ready role or a human-gate marker present anywhere
|
|
1957
|
+
# in the submitted fields), where the shell path has to answer "which of these
|
|
1958
|
+
# tokens is a client, an endpoint, a flag value, or a file it executes".
|
|
1959
|
+
#
|
|
1960
|
+
# Every string in the payload is searched, at any depth, because a role lands
|
|
1961
|
+
# in a different field on every tracker — `labels[]` on GitHub, `stateId` or a
|
|
1962
|
+
# workflow state name on Linear, a transition `id` on JIRA — and enumerating
|
|
1963
|
+
# field names per vendor is the same brittleness as enumerating tool names.
|
|
1964
|
+
# Over-collecting is safe here: the values are compared against ONE configured
|
|
1965
|
+
# role string, so an unrelated field cannot accidentally satisfy it.
|
|
1966
|
+
if [ -n "$structured_call" ]; then
|
|
1967
|
+
# The operator's ambient escape works on both substrates. There is no inline
|
|
1968
|
+
# form to disqualify it here — a structured call has no shell in which to
|
|
1969
|
+
# assign one — so the override is simply honoured.
|
|
1970
|
+
if [ -n "$ambient_override" ]; then
|
|
1971
|
+
exit 0
|
|
1972
|
+
fi
|
|
1973
|
+
|
|
1974
|
+
structured_declaration="$(
|
|
1975
|
+
printf '%s' "$input" |
|
|
1976
|
+
jq -r --arg role "$ready_role" '
|
|
1977
|
+
[(.tool_input // {}) | .. | strings] as $values
|
|
1978
|
+
| if ($values | index($role)) then "role"
|
|
1979
|
+
elif ($values | map(select(contains("[lisa-human-gate]"))) | length) > 0 then "gate"
|
|
1980
|
+
else "" end
|
|
1981
|
+
' 2>/dev/null || printf 'UNREADABLE'
|
|
1982
|
+
)"
|
|
1983
|
+
|
|
1984
|
+
# Fail closed on a creation-shaped call whose payload cannot be read. Silence
|
|
1985
|
+
# about a filing the guard could not inspect is the defect this whole ticket
|
|
1986
|
+
# is about, one substrate over.
|
|
1987
|
+
if [ "$structured_declaration" = "UNREADABLE" ]; then
|
|
1988
|
+
refuse "an unreadable $tool_name payload that reads as a tracker creation" \
|
|
1989
|
+
"$ready_role" ""
|
|
1990
|
+
fi
|
|
1991
|
+
|
|
1992
|
+
if [ -z "$structured_declaration" ]; then
|
|
1993
|
+
refuse "a tracker creation through $tool_name" "$ready_role" ""
|
|
1994
|
+
fi
|
|
1995
|
+
exit 0
|
|
1996
|
+
fi
|
|
1997
|
+
|
|
1862
1998
|
verdict="$(
|
|
1863
1999
|
printf '%s' "$classifier" |
|
|
1864
2000
|
LISA_GUARD_COMMAND="$command_str" \
|
|
@@ -77,6 +77,14 @@ export const FAMILIES = [
|
|
|
77
77
|
match: /(^|\/)\.lisa\.config\.json$/,
|
|
78
78
|
kind: "allow-list",
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
// The #3811 quarantine. `allow-list` is exactly right here: the list may
|
|
82
|
+
// shrink freely, and ADDING a file — re-authorising type debt that the
|
|
83
|
+
// gate would otherwise refuse — is the change that needs a human to see it.
|
|
84
|
+
id: "typecheck-quarantine",
|
|
85
|
+
match: /(^|\/)typecheck-quarantine\.json$/,
|
|
86
|
+
kind: "allow-list",
|
|
87
|
+
},
|
|
80
88
|
{
|
|
81
89
|
id: "lighthouse",
|
|
82
90
|
match: /(^|\/)(?:lighthouserc-config|lighthouserc|\.lighthouserc)\.json$/,
|
|
@@ -80,6 +80,14 @@ export const FAMILIES = [
|
|
|
80
80
|
match: /(^|\/)\.lisa\.config\.json$/,
|
|
81
81
|
kind: "allow-list",
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
// The #3811 quarantine. `allow-list` is exactly right here: the list may
|
|
85
|
+
// shrink freely, and ADDING a file — re-authorising type debt that the
|
|
86
|
+
// gate would otherwise refuse — is the change that needs a human to see it.
|
|
87
|
+
id: "typecheck-quarantine",
|
|
88
|
+
match: /(^|\/)typecheck-quarantine\.json$/,
|
|
89
|
+
kind: "allow-list",
|
|
90
|
+
},
|
|
83
91
|
{
|
|
84
92
|
id: "lighthouse",
|
|
85
93
|
match: /(^|\/)(?:lighthouserc-config|lighthouserc|\.lighthouserc)\.json$/,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Type-check `tests/` and hold the result to a quarantine that can only shrink.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. The required `type-correctness` gate compiles `tsconfig.json`,
|
|
6
|
+
* whose `include` is `src/**\/*`, so it compiled 0 of 1385 test files and reported
|
|
7
|
+
* success — a required gate permitting exactly what it forbids
|
|
8
|
+
* (CodySwannGT/lisa#3811). Test files here are not incidental: they hold fixture
|
|
9
|
+
* builders, shape assertions and harnesses other suites import.
|
|
10
|
+
*
|
|
11
|
+
* WHY A FILE-LEVEL QUARANTINE RATHER THAN A STAGED ROLLOUT. Turning the program
|
|
12
|
+
* on surfaces a backlog that predates this gate, so something has to hold it.
|
|
13
|
+
* The tempting option is to widen the program one directory at a time. That is
|
|
14
|
+
* the defect surviving in miniature: with a directory scope, a brand-new file
|
|
15
|
+
* under an unreached directory is unchecked on the day it is written. A
|
|
16
|
+
* file-level quarantine inverts it — **every new or renamed file is checked
|
|
17
|
+
* immediately**, and only the files that were already failing are exempt.
|
|
18
|
+
*
|
|
19
|
+
* IT RATCHETS IN BOTH DIRECTIONS, which is what stops it becoming permanent:
|
|
20
|
+
*
|
|
21
|
+
* - an error in a file NOT in the quarantine fails the gate: that is new debt;
|
|
22
|
+
* - a quarantined file with NO errors fails the gate too, asking to be
|
|
23
|
+
* removed from the list. Without that rule the list would record files that
|
|
24
|
+
* were fixed years ago and quietly re-authorise them if they regressed.
|
|
25
|
+
*
|
|
26
|
+
* `typecheck-quarantine.json` is additionally watched by the existing
|
|
27
|
+
* threshold-ratchet as an `allow-list` family, so *adding* an entry is flagged
|
|
28
|
+
* by a gate somebody already reads.
|
|
29
|
+
*
|
|
30
|
+
* WHAT IS DELIBERATELY NOT DONE HERE: the declared `lib` is not widened. tsc
|
|
31
|
+
* emits "Try changing the 'lib' compiler option to 'es2023' or later" on the
|
|
32
|
+
* ES2023 array APIs, and taking that suggestion would have erased the
|
|
33
|
+
* measurement proving this gap cost something — `toSorted` appeared 0 times in
|
|
34
|
+
* the type-checked `src/` and 52 times in the unchecked `tests/`. Those call
|
|
35
|
+
* sites were rewritten to ES2022 equivalents instead.
|
|
36
|
+
* @module scripts/check-typecheck-tests
|
|
37
|
+
*/
|
|
38
|
+
import { spawnSync } from "node:child_process";
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
40
|
+
import * as path from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
|
|
43
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
|
+
const QUARANTINE = path.join(ROOT, "typecheck-quarantine.json");
|
|
45
|
+
const PROJECT = path.join(ROOT, "tsconfig.tests.json");
|
|
46
|
+
|
|
47
|
+
/** Matches `path/to/file.ts(12,34): error TS1234: message`. */
|
|
48
|
+
const ERROR_LINE = /^(?<file>[^(]+)\((?<line>\d+),\d+\): error (?<code>TS\d+):/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the quarantined paths.
|
|
52
|
+
* @returns {{files: string[], reason: string}} The parsed quarantine.
|
|
53
|
+
*/
|
|
54
|
+
function readQuarantine() {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(QUARANTINE, "utf-8"));
|
|
57
|
+
return { files: parsed.files ?? [], reason: parsed.reason ?? "" };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(
|
|
60
|
+
`check-typecheck-tests: cannot read ${path.relative(ROOT, QUARANTINE)} — ${error.message}`
|
|
61
|
+
);
|
|
62
|
+
console.error(
|
|
63
|
+
"That file is what makes this gate green, so an unreadable one is a refusal, not a pass."
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { files: quarantined } = readQuarantine();
|
|
70
|
+
const exempt = new Set(quarantined);
|
|
71
|
+
|
|
72
|
+
const tsc = spawnSync(
|
|
73
|
+
process.execPath,
|
|
74
|
+
[
|
|
75
|
+
path.join(ROOT, "node_modules", "typescript", "bin", "tsc"),
|
|
76
|
+
"--noEmit",
|
|
77
|
+
"-p",
|
|
78
|
+
PROJECT,
|
|
79
|
+
],
|
|
80
|
+
{ cwd: ROOT, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (tsc.error) {
|
|
84
|
+
console.error(
|
|
85
|
+
`check-typecheck-tests: could not run tsc — ${tsc.error.message}`
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = `${tsc.stdout ?? ""}\n${tsc.stderr ?? ""}`.split("\n");
|
|
91
|
+
/** @type {Map<string, number>} */
|
|
92
|
+
const errorsByFile = new Map();
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const match = ERROR_LINE.exec(line.trim());
|
|
95
|
+
if (!match?.groups) continue;
|
|
96
|
+
const file = match.groups.file.replace(/\\/g, "/");
|
|
97
|
+
errorsByFile.set(file, (errorsByFile.get(file) ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const offenders = [...errorsByFile.keys()]
|
|
101
|
+
.filter(file => !exempt.has(file))
|
|
102
|
+
.sort();
|
|
103
|
+
const cleared = quarantined.filter(file => !errorsByFile.has(file)).sort();
|
|
104
|
+
|
|
105
|
+
const totalErrors = [...errorsByFile.values()].reduce((sum, n) => sum + n, 0);
|
|
106
|
+
console.log(
|
|
107
|
+
`type-correctness (tests): ${errorsByFile.size} file(s) with ${totalErrors} error(s); ` +
|
|
108
|
+
`${quarantined.length} quarantined.`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (offenders.length > 0) {
|
|
112
|
+
console.error("");
|
|
113
|
+
console.error(
|
|
114
|
+
`❌ ${offenders.length} file(s) outside the quarantine have type errors:`
|
|
115
|
+
);
|
|
116
|
+
for (const file of offenders) {
|
|
117
|
+
console.error(` ${file} (${errorsByFile.get(file)})`);
|
|
118
|
+
}
|
|
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
|
+
|
|
128
|
+
if (cleared.length > 0) {
|
|
129
|
+
console.error("");
|
|
130
|
+
console.error(
|
|
131
|
+
`❌ ${cleared.length} quarantined file(s) now type-check and must leave the list:`
|
|
132
|
+
);
|
|
133
|
+
for (const file of cleared) console.error(` ${file}`);
|
|
134
|
+
console.error("");
|
|
135
|
+
console.error(
|
|
136
|
+
"Remove them from typecheck-quarantine.json. A list that keeps entries"
|
|
137
|
+
);
|
|
138
|
+
console.error(
|
|
139
|
+
"after they are fixed would re-authorise them if they ever regressed."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (offenders.length > 0 || cleared.length > 0) process.exit(1);
|
|
144
|
+
|
|
145
|
+
console.log(
|
|
146
|
+
"✅ No type errors outside the quarantine, and no stale quarantine entries."
|
|
147
|
+
);
|
|
@@ -80,6 +80,14 @@ export const FAMILIES = [
|
|
|
80
80
|
match: /(^|\/)\.lisa\.config\.json$/,
|
|
81
81
|
kind: "allow-list",
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
// The #3811 quarantine. `allow-list` is exactly right here: the list may
|
|
85
|
+
// shrink freely, and ADDING a file — re-authorising type debt that the
|
|
86
|
+
// gate would otherwise refuse — is the change that needs a human to see it.
|
|
87
|
+
id: "typecheck-quarantine",
|
|
88
|
+
match: /(^|\/)typecheck-quarantine\.json$/,
|
|
89
|
+
kind: "allow-list",
|
|
90
|
+
},
|
|
83
91
|
{
|
|
84
92
|
id: "lighthouse",
|
|
85
93
|
match: /(^|\/)(?:lighthouserc-config|lighthouserc|\.lighthouserc)\.json$/,
|