@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.
Files changed (78) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +140 -4
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +11 -6
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +9 -0
  10. package/package.json +6 -5
  11. package/plugins/lisa/.claude-plugin/plugin.json +10 -5
  12. package/plugins/lisa/.codex-plugin/hooks.json +9 -4
  13. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +13 -3
  15. package/plugins/lisa/hooks/block-direct-issue-create.sh +140 -4
  16. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +8 -0
  17. package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +13 -3
  18. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +140 -4
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +10 -5
  26. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +140 -4
  27. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +8 -0
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +140 -4
  30. package/plugins/lisa-cursor/hooks/hooks.json +3 -4
  31. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +8 -0
  32. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-agy/plugin.json +1 -1
  35. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  55. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-agy/plugin.json +1 -1
  60. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  65. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  70. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/src/base/.claude-plugin/plugin.json +9 -4
  73. package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +13 -3
  74. package/plugins/src/base/hooks/block-direct-issue-create.sh +140 -4
  75. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +8 -0
  76. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +8 -0
  77. package/scripts/check-typecheck-tests.mjs +147 -0
  78. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +8 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.38.1",
3
+ "version": "4.38.3",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
- # No scope gap here, unlike the instruction-file adapter: the canonical guard is
12
- # Bash-only by construction, and `run_command` is exactly the surface it needs.
13
- # agy therefore receives the whole guard, not an arm of it.
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
- exit 0
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="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
107
- if [ -z "$command_str" ]; then
108
- exit 0
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$/,