@codyswann/lisa 4.42.5 → 4.42.7
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/README.md +4 -0
- package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +7 -0
- package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +12 -2
- package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +4 -0
- package/all/copy-overwrite/scripts/lisa-environment-prepare.mjs +2 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +1 -1
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +31 -3
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +4 -0
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +13 -0
- package/dist/configs/vitest/base.d.ts +1 -1
- package/dist/configs/vitest/base.d.ts.map +1 -1
- package/dist/configs/vitest/base.js +1 -0
- package/dist/configs/vitest/base.js.map +1 -1
- package/dist/configs/vitest/cdk.d.ts +2 -2
- package/dist/configs/vitest/cdk.d.ts.map +1 -1
- package/dist/configs/vitest/cdk.js +5 -2
- package/dist/configs/vitest/cdk.js.map +1 -1
- package/dist/configs/vitest/coverage-reports-directory.d.ts +47 -0
- package/dist/configs/vitest/coverage-reports-directory.d.ts.map +1 -0
- package/dist/configs/vitest/coverage-reports-directory.js +131 -0
- package/dist/configs/vitest/coverage-reports-directory.js.map +1 -0
- package/dist/configs/vitest/harper-fabric.d.ts +2 -2
- package/dist/configs/vitest/harper-fabric.d.ts.map +1 -1
- package/dist/configs/vitest/harper-fabric.js +5 -2
- package/dist/configs/vitest/harper-fabric.js.map +1 -1
- package/dist/configs/vitest/nestjs.d.ts +2 -2
- package/dist/configs/vitest/nestjs.d.ts.map +1 -1
- package/dist/configs/vitest/nestjs.js +5 -2
- package/dist/configs/vitest/nestjs.js.map +1 -1
- package/dist/configs/vitest/phaser.d.ts +2 -2
- package/dist/configs/vitest/phaser.d.ts.map +1 -1
- package/dist/configs/vitest/phaser.js +5 -2
- package/dist/configs/vitest/phaser.js.map +1 -1
- package/dist/configs/vitest/typescript.d.ts +2 -2
- package/dist/configs/vitest/typescript.d.ts.map +1 -1
- package/dist/configs/vitest/typescript.js +5 -2
- package/dist/configs/vitest/typescript.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +30 -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 +32 -25
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +1 -1
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +26 -7
- package/package.json +5 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
- package/plugins/lisa/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa/hooks/threshold-ratchet.mjs +6 -0
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +31 -3
- package/plugins/lisa/scripts/design-variable-ids.mjs +2 -0
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-variable-ids.mjs +2 -0
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
- 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 +1 -1
- package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +6 -0
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +31 -3
- package/plugins/lisa-copilot/scripts/design-variable-ids.mjs +2 -0
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +1 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +6 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +31 -3
- package/plugins/lisa-cursor/scripts/design-variable-ids.mjs +2 -0
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -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/scripts/ingest-git.mjs +2 -0
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +2 -0
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +2 -0
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +2 -0
- package/plugins/src/base/hooks/parity-safety-net.sh +1 -1
- package/plugins/src/base/hooks/threshold-ratchet.mjs +6 -0
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +31 -3
- package/plugins/src/base/scripts/design-variable-ids.mjs +2 -0
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
- package/plugins/src/wiki/scripts/ingest-git.mjs +2 -0
- package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
- package/scripts/check-merge-driver-registration.mjs +2 -0
- package/scripts/check-probe-absence-direction.mjs +611 -0
- package/scripts/install-generated-artifact-merge-driver.mjs +7 -1
- package/scripts/lib/shell-guard-refusal-coverage.mjs +18 -19
- package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +4 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-probe-absence-direction — refuse a probe that spends a FAILURE as an
|
|
4
|
+
* ABSENCE without saying which way the absence pushes the gate
|
|
5
|
+
* (CodySwannGT/lisa#3848).
|
|
6
|
+
*
|
|
7
|
+
* ## The defect
|
|
8
|
+
*
|
|
9
|
+
* `gh pr view --repo OWNER/NAME --json url,state` with no positional argument
|
|
10
|
+
* is a USAGE ERROR — `argument required when using the --repo flag`, exit 1 —
|
|
11
|
+
* not a lookup that returned no rows. The work-item validator's pull-request
|
|
12
|
+
* lookup passed exactly that combination and read the exit 1 as "this branch
|
|
13
|
+
* has no pull request", so the two gates that need the pull request (the
|
|
14
|
+
* `Work-Item:` line in its body, and the `[lisa-pr-link]` backlink) were never
|
|
15
|
+
* checked at push on any branch. The concrete call was fixed in #3833; the
|
|
16
|
+
* shape it is an instance of is what this file refuses.
|
|
17
|
+
*
|
|
18
|
+
* It was the fourth instrument in one working session to fail in the shape of a
|
|
19
|
+
* fact about its subject — a search hitting its `--limit` read as a complete
|
|
20
|
+
* result set, a jobs endpoint answering from the wrong attempt, an audit
|
|
21
|
+
* printing nothing read as clean, and this. A fifth arrived inside the
|
|
22
|
+
* diagnostic built to catch the other four.
|
|
23
|
+
*
|
|
24
|
+
* ## Direction is the property, not shape
|
|
25
|
+
*
|
|
26
|
+
* `if (result.status !== 0) return undefined;` is the same nine tokens whether
|
|
27
|
+
* it is correct or catastrophic. What separates them is which way the failure
|
|
28
|
+
* pushes the outcome:
|
|
29
|
+
*
|
|
30
|
+
* fail-closed the failure makes the gate STRICTER — no exemption granted,
|
|
31
|
+
* no range excluded, no allowance read. Losing the answer costs
|
|
32
|
+
* a false positive, never a missed one. A legitimate design.
|
|
33
|
+
* neutral the failure changes no gate outcome — the value is reported,
|
|
34
|
+
* formatted, or logged, and no enforcement reads it.
|
|
35
|
+
* fail-open the failure makes the gate LOOSER. This is the defect. There
|
|
36
|
+
* is no rationale that makes it acceptable, so the marker is
|
|
37
|
+
* accepted by the grammar and REFUSED by the check: writing it
|
|
38
|
+
* is how you say "I looked, and it is wrong", not how you
|
|
39
|
+
* register an exemption.
|
|
40
|
+
*
|
|
41
|
+
* Grepping for the pattern and filing every hit is therefore useless — the
|
|
42
|
+
* population is large and mostly correct. What is missing is the direction, and
|
|
43
|
+
* it has to be recorded AT THE LINE, because that is the only place a reviewer
|
|
44
|
+
* later can check it against the code rather than against a table that has gone
|
|
45
|
+
* stale. (One went stale inside #3848's own description: a call site classified
|
|
46
|
+
* "neutral" was neutral only because #3833 had already landed. A classification
|
|
47
|
+
* of call sites looks timeless and is not.)
|
|
48
|
+
*
|
|
49
|
+
* ## What counts as a finding
|
|
50
|
+
*
|
|
51
|
+
* A site is INSPECTED when all three hold:
|
|
52
|
+
*
|
|
53
|
+
* 1. A failure branch: `if (<failure condition>) return <absence>;` (on one
|
|
54
|
+
* line or two), or `catch { return <absence>; }`. A failure condition
|
|
55
|
+
* reads a child process's verdict — `.status !== 0`, `.error`, `!x.ok` —
|
|
56
|
+
* or is a `catch` clause, which is the same fact arriving as a throw.
|
|
57
|
+
* 2. An ABSENCE value: `undefined`, `null`, `[]`, `{}`, `""`, `false`, `0`,
|
|
58
|
+
* `new Map()`, `new Set()`. These are the values a caller reads as a
|
|
59
|
+
* legitimate negative answer.
|
|
60
|
+
* 3. The enclosing function actually ASKS SOMETHING EXTERNAL — it spawns a
|
|
61
|
+
* child, shells out, or fetches. A `JSON.parse` that throws is a malformed
|
|
62
|
+
* input, not a lookup that could not be performed, and including it would
|
|
63
|
+
* bury the findings that matter under hundreds that do not.
|
|
64
|
+
*
|
|
65
|
+
* An inspected site is a FINDING when it carries no direction marker, when its
|
|
66
|
+
* marker has no rationale, or when its marker says `fail-open`.
|
|
67
|
+
*
|
|
68
|
+
* ## The marker
|
|
69
|
+
*
|
|
70
|
+
* probe-direction: fail-closed — losing the ref skips the exclusion, so more
|
|
71
|
+
* commits are examined, never fewer.
|
|
72
|
+
*
|
|
73
|
+
* It may sit in the comment block above the failure branch, as a trailing
|
|
74
|
+
* comment on the branch or its return, or in the enclosing function's JSDoc —
|
|
75
|
+
* whichever is where a reader of that site would look. The rationale is not
|
|
76
|
+
* decoration: `fail-closed` with no sentence saying what gets stricter is the
|
|
77
|
+
* same unchecked assertion the marker exists to replace, so it is refused.
|
|
78
|
+
*
|
|
79
|
+
* ## Why it fails at zero inspected
|
|
80
|
+
*
|
|
81
|
+
* An empty inspection and a clean tree print the same tick, and this repository
|
|
82
|
+
* has shipped guards that reported success while inert often enough to have a
|
|
83
|
+
* rule about it. The count of sites actually parsed is part of the report and a
|
|
84
|
+
* count of zero is exit 2, not exit 0 — a glob that matches nothing, a root
|
|
85
|
+
* that does not exist, and a parser that silently stopped all reach that
|
|
86
|
+
* branch. That is this check declining to commit its own defect.
|
|
87
|
+
*
|
|
88
|
+
* ## Declared blind spots
|
|
89
|
+
*
|
|
90
|
+
* Named rather than hidden, because "the sweep found nothing" and "the sweep
|
|
91
|
+
* does not look there" are the two facts this whole file exists to keep apart.
|
|
92
|
+
* They are printed on every run; see `BLIND_SPOTS`.
|
|
93
|
+
*
|
|
94
|
+
* Determinism: Node built-ins only, no network, no clock, no `Math.random`.
|
|
95
|
+
* The scanned root is a parameter so the suite can point it at a fixture tree
|
|
96
|
+
* holding a known offender.
|
|
97
|
+
*
|
|
98
|
+
* CLI:
|
|
99
|
+
* node scripts/check-probe-absence-direction.mjs [--json] [root]
|
|
100
|
+
*
|
|
101
|
+
* Exit codes (mirroring the sibling check-* scripts):
|
|
102
|
+
* 0 — sites were inspected and every one declares a safe direction.
|
|
103
|
+
* 1 — >=1 finding.
|
|
104
|
+
* 2 — operational error: unknown flag, unreadable root, or ZERO sites
|
|
105
|
+
* inspected.
|
|
106
|
+
*
|
|
107
|
+
* @module scripts/check-probe-absence-direction
|
|
108
|
+
*/
|
|
109
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
110
|
+
import path from "node:path";
|
|
111
|
+
import process from "node:process";
|
|
112
|
+
|
|
113
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One refused site.
|
|
117
|
+
* @typedef {object} ProbeFinding
|
|
118
|
+
* @property {string} file - Repo-relative path.
|
|
119
|
+
* @property {number} line - One-based line of the failure branch.
|
|
120
|
+
* @property {string} shape - `catch` or `guarded return`.
|
|
121
|
+
* @property {string} statement - The branch as written.
|
|
122
|
+
* @property {string | undefined} direction - The marker's direction, if any.
|
|
123
|
+
* @property {string} reason - Why the site is refused.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A completed sweep.
|
|
128
|
+
* @typedef {object} ProbeReport
|
|
129
|
+
* @property {number} inspected - Absence-on-failure sites parsed.
|
|
130
|
+
* @property {number} files - Files read.
|
|
131
|
+
* @property {ProbeFinding[]} findings - Sites that are refused.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** Directories whose JavaScript Lisa authors, ships, or runs as enforcement. */
|
|
135
|
+
export const SCANNED_ROOTS = Object.freeze([
|
|
136
|
+
"all",
|
|
137
|
+
"cdk",
|
|
138
|
+
"expo",
|
|
139
|
+
"harper-fabric",
|
|
140
|
+
"nestjs",
|
|
141
|
+
"npm-package",
|
|
142
|
+
"phaser",
|
|
143
|
+
"plugins/src",
|
|
144
|
+
"rails",
|
|
145
|
+
"scripts",
|
|
146
|
+
"typescript",
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* What this sweep does NOT look at, and why.
|
|
151
|
+
*
|
|
152
|
+
* Printed on every run. A reader who needs to know whether a surface is covered
|
|
153
|
+
* must not have to infer it from an empty finding list — that inference is the
|
|
154
|
+
* defect being refused, one level up.
|
|
155
|
+
*/
|
|
156
|
+
export const BLIND_SPOTS = Object.freeze([
|
|
157
|
+
"src/** and tests/** — Lisa's own TypeScript. It is compiled and unit-tested here rather than executed inside a host's git hook, so a probe there fails in front of a suite, not in front of a gate.",
|
|
158
|
+
"*.sh and workflow `run:` blocks — a shell probe conflating failure with absence is `check:pipeline-status-reads`' subject (#3090); duplicating it here would report one defect twice.",
|
|
159
|
+
"generated plugin payloads (plugins/lisa*) — copies of plugins/src, where the annotation belongs; flagging both would ask for the same marker twice and let the copy drift into being the one that is fixed.",
|
|
160
|
+
"absences delivered by ASSIGNMENT rather than `return` (`let x = null; try { x = probe(); } catch {}`) — detectable only with a real parser, so the sweep declares it rather than pretending the shape does not exist.",
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
/** Directory names never descended into: nothing inside is authored here. */
|
|
164
|
+
const SKIPPED_DIRECTORIES = Object.freeze([
|
|
165
|
+
"node_modules",
|
|
166
|
+
"dist",
|
|
167
|
+
".git",
|
|
168
|
+
"coverage",
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
/** File extensions read as JavaScript. */
|
|
172
|
+
const JAVASCRIPT_EXTENSIONS = Object.freeze([".mjs", ".cjs", ".js"]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Values a caller reads as a legitimate negative answer.
|
|
176
|
+
*
|
|
177
|
+
* `false` and `0` are here for the same reason `null` is: `runProbe()` handing
|
|
178
|
+
* back `false` because the command could not be started is indistinguishable,
|
|
179
|
+
* at the call site, from it handing back `false` because the probe genuinely
|
|
180
|
+
* said no.
|
|
181
|
+
*/
|
|
182
|
+
const ABSENCE_VALUES = Object.freeze([
|
|
183
|
+
"undefined",
|
|
184
|
+
"null",
|
|
185
|
+
"[]",
|
|
186
|
+
"{}",
|
|
187
|
+
'""',
|
|
188
|
+
"''",
|
|
189
|
+
"false",
|
|
190
|
+
"0",
|
|
191
|
+
"new Map()",
|
|
192
|
+
"new Set()",
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
/** A `return` of an absence value and nothing else. */
|
|
196
|
+
const ABSENCE_RETURN =
|
|
197
|
+
/^\s*return\s+(?<value>undefined|null|\[\]|\{\}|""|''|false|0|new Map\(\)|new Set\(\))\s*;?\s*$/u;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Conditions that read a child process's verdict.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately narrow. `if (!rows.length)` is an empty RESULT, which is the
|
|
203
|
+
* honest absence this check exists to protect; only a verdict about whether the
|
|
204
|
+
* command ran belongs here.
|
|
205
|
+
*/
|
|
206
|
+
const FAILURE_CONDITIONS = Object.freeze([
|
|
207
|
+
/\.status\s*!==?\s*0/u,
|
|
208
|
+
/\bstatus\s*!==?\s*0/u,
|
|
209
|
+
/\.error\b/u,
|
|
210
|
+
/!\s*\w+(?:\.\w+)*\.ok\b/u,
|
|
211
|
+
/\.ok\s*===?\s*false/u,
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
/** Evidence that the enclosing function asks something outside this process. */
|
|
215
|
+
const EXTERNAL_PROBES = Object.freeze([
|
|
216
|
+
/\b(?:spawnSync|spawn|execSync|execFileSync|execFile)\s*\(/u,
|
|
217
|
+
/\b(?:boundedSpawnSync|boundedChildOutput)\s*\(/u,
|
|
218
|
+
/\bfetch\w*\s*\(/u,
|
|
219
|
+
/\ballowFailure\b/u,
|
|
220
|
+
/\b(?:run|git|gh|probe|capture|exec)\s*\(/u,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
/** Where a function whose body holds the branch may begin. */
|
|
224
|
+
const FUNCTION_STARTS = Object.freeze([
|
|
225
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/u,
|
|
226
|
+
/^\s*(?:export\s+)?(?:const|let)\s+\w+\s*=\s*(?:async\s*)?(?:function\b|\()/u,
|
|
227
|
+
/^\s{0,4}(?:async\s+)?\w+\s*\([^)]*\)\s*\{/u,
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Whether any pattern in a set matches.
|
|
232
|
+
*
|
|
233
|
+
* The sets are written as several small regexes rather than one alternation
|
|
234
|
+
* because a 58-point regex is unreviewable, and an unreviewable matcher in a
|
|
235
|
+
* check about unreviewable assumptions would be its own joke.
|
|
236
|
+
* @param {readonly RegExp[]} patterns - Patterns to try.
|
|
237
|
+
* @param {string} text - Text to match.
|
|
238
|
+
* @returns {boolean} True when one matches.
|
|
239
|
+
*/
|
|
240
|
+
function matchesAny(patterns, text) {
|
|
241
|
+
return patterns.some(pattern => pattern.test(text));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The marker, its direction, and the rationale that has to follow it. */
|
|
245
|
+
const DIRECTION_MARKER =
|
|
246
|
+
/probe-direction:\s*(?<direction>fail-closed|fail-open|neutral)\s*(?<rationale>.*)$/u;
|
|
247
|
+
|
|
248
|
+
/** Directions that may stand. `fail-open` is the defect, so it is not here. */
|
|
249
|
+
export const PERMITTED_DIRECTIONS = Object.freeze(["fail-closed", "neutral"]);
|
|
250
|
+
|
|
251
|
+
/** Shortest rationale accepted. A word is not a reason. */
|
|
252
|
+
const MINIMUM_RATIONALE = 12;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether a line is comment-only.
|
|
256
|
+
* @param {string} line - Source line.
|
|
257
|
+
* @returns {boolean} True for `//`, `/*`, `*` and `*\/` lines.
|
|
258
|
+
*/
|
|
259
|
+
function isCommentLine(line) {
|
|
260
|
+
const trimmed = line.trim();
|
|
261
|
+
return (
|
|
262
|
+
trimmed.startsWith("//") ||
|
|
263
|
+
trimmed.startsWith("/*") ||
|
|
264
|
+
trimmed.startsWith("*")
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The contiguous comment block immediately above a line.
|
|
270
|
+
* @param {readonly string[]} lines - File lines.
|
|
271
|
+
* @param {number} index - Zero-based line the block sits above.
|
|
272
|
+
* @returns {string[]} The block, or an empty array.
|
|
273
|
+
*/
|
|
274
|
+
function commentBlockAbove(lines, index) {
|
|
275
|
+
const block = [];
|
|
276
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
277
|
+
const line = lines[cursor];
|
|
278
|
+
if (line.trim() === "") continue;
|
|
279
|
+
if (!isCommentLine(line)) break;
|
|
280
|
+
block.push(line);
|
|
281
|
+
}
|
|
282
|
+
return block;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The line index where the function holding a branch begins.
|
|
287
|
+
* @param {readonly string[]} lines - File lines.
|
|
288
|
+
* @param {number} index - Zero-based branch line.
|
|
289
|
+
* @returns {number} Zero-based start line, or 0 when none is recognised.
|
|
290
|
+
*/
|
|
291
|
+
export function enclosingFunctionStart(lines, index) {
|
|
292
|
+
for (let cursor = index; cursor >= 0; cursor -= 1) {
|
|
293
|
+
if (matchesAny(FUNCTION_STARTS, lines[cursor])) return cursor;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether the code leading up to a branch asks something external.
|
|
300
|
+
* @param {readonly string[]} lines - File lines.
|
|
301
|
+
* @param {number} start - Zero-based function start.
|
|
302
|
+
* @param {number} index - Zero-based branch line.
|
|
303
|
+
* @returns {boolean} True when the branch reports on an external lookup.
|
|
304
|
+
*/
|
|
305
|
+
export function probesSomethingExternal(lines, start, index) {
|
|
306
|
+
return matchesAny(EXTERNAL_PROBES, lines.slice(start, index + 1).join("\n"));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Every line a direction marker for this branch may legitimately appear on.
|
|
311
|
+
*
|
|
312
|
+
* Three places, because three different shapes read naturally in three
|
|
313
|
+
* different spots: a one-line guard wants a trailing or preceding comment, a
|
|
314
|
+
* `catch` inside a small helper wants the helper's JSDoc, and a branch buried
|
|
315
|
+
* in a long function wants the comment right above it.
|
|
316
|
+
* @param {readonly string[]} lines - File lines.
|
|
317
|
+
* @param {{branch: number, end: number}} site - Zero-based branch and last line.
|
|
318
|
+
* @returns {string[]} Candidate lines holding the marker.
|
|
319
|
+
*/
|
|
320
|
+
export function annotationWindow(lines, site) {
|
|
321
|
+
const start = enclosingFunctionStart(lines, site.branch);
|
|
322
|
+
return [
|
|
323
|
+
...commentBlockAbove(lines, site.branch),
|
|
324
|
+
...commentBlockAbove(lines, start),
|
|
325
|
+
...lines.slice(site.branch, site.end + 1),
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Read the direction marker out of a window.
|
|
331
|
+
* @param {readonly string[]} window - Candidate lines.
|
|
332
|
+
* @returns {{direction: string, rationale: string} | undefined} The marker.
|
|
333
|
+
*/
|
|
334
|
+
export function readDirection(window) {
|
|
335
|
+
for (const line of window) {
|
|
336
|
+
const match = DIRECTION_MARKER.exec(line);
|
|
337
|
+
if (!match) continue;
|
|
338
|
+
return {
|
|
339
|
+
direction: match.groups.direction,
|
|
340
|
+
rationale: match.groups.rationale.replace(/^[\s—–-]+/u, "").trim(),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Why a marker is unacceptable, or undefined when it stands.
|
|
348
|
+
* @param {{direction: string, rationale: string} | undefined} marker - Marker.
|
|
349
|
+
* @returns {string | undefined} The refusal reason.
|
|
350
|
+
*/
|
|
351
|
+
export function refusalFor(marker) {
|
|
352
|
+
if (!marker) {
|
|
353
|
+
return "no `probe-direction:` marker — a failure spent as an absence has to say which way the absence pushes the gate";
|
|
354
|
+
}
|
|
355
|
+
if (!PERMITTED_DIRECTIONS.includes(marker.direction)) {
|
|
356
|
+
return "marked `fail-open` — a failure that makes a gate LOOSER is the defect itself; restructure the probe so a caller cannot spend the failure as an answer";
|
|
357
|
+
}
|
|
358
|
+
if (marker.rationale.length < MINIMUM_RATIONALE) {
|
|
359
|
+
return `marked \`${marker.direction}\` with no rationale — say what gets stricter (or what reads the value) so the claim can be checked against the code`;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The next line that is neither blank nor comment-only.
|
|
366
|
+
*
|
|
367
|
+
* A `catch` whose return is separated from it by the very marker this check
|
|
368
|
+
* demands must still be RECOGNISED as a catch. Requiring the return on the
|
|
369
|
+
* literally-next line would mean annotating a site removed it from the
|
|
370
|
+
* population — the check would go green because it had stopped looking, which
|
|
371
|
+
* is the defect it exists to refuse, committed by the remedy.
|
|
372
|
+
* @param {readonly string[]} lines - File lines.
|
|
373
|
+
* @param {number} index - Zero-based line to search after.
|
|
374
|
+
* @returns {number} Zero-based index, or -1 at end of file.
|
|
375
|
+
*/
|
|
376
|
+
export function nextCodeLine(lines, index) {
|
|
377
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
378
|
+
if (lines[cursor].trim() === "") continue;
|
|
379
|
+
if (isCommentLine(lines[cursor])) continue;
|
|
380
|
+
return cursor;
|
|
381
|
+
}
|
|
382
|
+
return -1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Locate the absence-yielding failure branches in one file's lines.
|
|
387
|
+
* @param {readonly string[]} lines - File lines.
|
|
388
|
+
* @returns {{branch: number, end: number, shape: string, statement: string}[]} Sites.
|
|
389
|
+
*/
|
|
390
|
+
export function absenceBranches(lines) {
|
|
391
|
+
const sites = [];
|
|
392
|
+
lines.forEach((line, index) => {
|
|
393
|
+
// A comment that QUOTES the shape is not the shape. This check's own
|
|
394
|
+
// header documents `catch { return null; }` in prose, and reading that as a
|
|
395
|
+
// call site is the same category error the check exists to refuse — a
|
|
396
|
+
// string about a thing taken for the thing.
|
|
397
|
+
if (isCommentLine(line)) return;
|
|
398
|
+
const inline =
|
|
399
|
+
/^\s*(?:\}\s*)?if\s*\((?<condition>.+)\)\s*return\s+\S+/u.exec(line);
|
|
400
|
+
if (inline && matchesAny(FAILURE_CONDITIONS, inline.groups.condition)) {
|
|
401
|
+
const value = /return\s+(?<value>.+?)\s*;?\s*$/u.exec(line)?.groups.value;
|
|
402
|
+
if (ABSENCE_VALUES.includes(value)) {
|
|
403
|
+
sites.push({
|
|
404
|
+
branch: index,
|
|
405
|
+
end: index,
|
|
406
|
+
shape: "guarded return",
|
|
407
|
+
statement: line.trim(),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const returnAt = nextCodeLine(lines, index);
|
|
413
|
+
const next = returnAt === -1 ? "" : lines[returnAt];
|
|
414
|
+
if (!ABSENCE_RETURN.test(next)) return;
|
|
415
|
+
if (
|
|
416
|
+
/^\s*if\s*\(.+\)\s*$/u.test(line) &&
|
|
417
|
+
matchesAny(FAILURE_CONDITIONS, line)
|
|
418
|
+
) {
|
|
419
|
+
sites.push({
|
|
420
|
+
branch: index,
|
|
421
|
+
end: returnAt,
|
|
422
|
+
shape: "guarded return",
|
|
423
|
+
statement: `${line.trim()} ${next.trim()}`,
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (/catch\s*(?:\([^)]*\))?\s*\{\s*$/u.test(line)) {
|
|
428
|
+
sites.push({
|
|
429
|
+
branch: index,
|
|
430
|
+
end: returnAt,
|
|
431
|
+
shape: "catch",
|
|
432
|
+
statement: `${line.trim()} ${next.trim()}`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return [...sites, ...inlineCatchBranches(lines)];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The `catch { return null; }` form, whose return shares the catch's line.
|
|
441
|
+
* @param {readonly string[]} lines - File lines.
|
|
442
|
+
* @returns {{branch: number, end: number, shape: string, statement: string}[]} Sites.
|
|
443
|
+
*/
|
|
444
|
+
function inlineCatchBranches(lines) {
|
|
445
|
+
const sites = [];
|
|
446
|
+
lines.forEach((line, index) => {
|
|
447
|
+
if (isCommentLine(line)) return;
|
|
448
|
+
const match =
|
|
449
|
+
/catch\s*(?:\([^)]*\))?\s*\{\s*return\s+(?<value>.+?)\s*;?\s*\}/u.exec(
|
|
450
|
+
line
|
|
451
|
+
);
|
|
452
|
+
if (!match || !ABSENCE_VALUES.includes(match.groups.value)) return;
|
|
453
|
+
sites.push({
|
|
454
|
+
branch: index,
|
|
455
|
+
end: index,
|
|
456
|
+
shape: "catch",
|
|
457
|
+
statement: line.trim(),
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
return sites;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Inspect one file.
|
|
465
|
+
* @param {string} text - File contents.
|
|
466
|
+
* @param {string} file - Repo-relative path, for reporting.
|
|
467
|
+
* @returns {ProbeReport} Result for this file.
|
|
468
|
+
*/
|
|
469
|
+
export function inspectSource(text, file) {
|
|
470
|
+
const lines = text.split("\n");
|
|
471
|
+
const result = { inspected: 0, findings: [] };
|
|
472
|
+
for (const site of absenceBranches(lines)) {
|
|
473
|
+
const start = enclosingFunctionStart(lines, site.branch);
|
|
474
|
+
if (!probesSomethingExternal(lines, start, site.branch)) continue;
|
|
475
|
+
result.inspected += 1;
|
|
476
|
+
const marker = readDirection(annotationWindow(lines, site));
|
|
477
|
+
const refusal = refusalFor(marker);
|
|
478
|
+
if (!refusal) continue;
|
|
479
|
+
result.findings.push({
|
|
480
|
+
file,
|
|
481
|
+
line: site.branch + 1,
|
|
482
|
+
shape: site.shape,
|
|
483
|
+
statement: site.statement,
|
|
484
|
+
direction: marker?.direction,
|
|
485
|
+
reason: refusal,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Every JavaScript file under a directory.
|
|
493
|
+
* @param {string} absolute - Directory to walk.
|
|
494
|
+
* @param {string} repoRoot - Root the reported paths are relative to.
|
|
495
|
+
* @returns {{absolute: string, relative: string}[]} Files in walk order.
|
|
496
|
+
*/
|
|
497
|
+
export function collectFiles(absolute, repoRoot) {
|
|
498
|
+
const files = [];
|
|
499
|
+
for (const entry of readdirSync(absolute, { withFileTypes: true })) {
|
|
500
|
+
if (SKIPPED_DIRECTORIES.includes(entry.name)) continue;
|
|
501
|
+
const child = path.join(absolute, entry.name);
|
|
502
|
+
if (entry.isDirectory()) {
|
|
503
|
+
files.push(...collectFiles(child, repoRoot));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (!JAVASCRIPT_EXTENSIONS.includes(path.extname(entry.name))) continue;
|
|
507
|
+
files.push({ absolute: child, relative: path.relative(repoRoot, child) });
|
|
508
|
+
}
|
|
509
|
+
return files;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Run the sweep over a tree.
|
|
514
|
+
* @param {string} repoRoot - Absolute path of the tree to inspect.
|
|
515
|
+
* @param {readonly string[]} [roots] - Sub-directories to scan.
|
|
516
|
+
* @returns {ProbeReport} Report.
|
|
517
|
+
*/
|
|
518
|
+
export function sweep(repoRoot, roots = SCANNED_ROOTS) {
|
|
519
|
+
const report = { inspected: 0, files: 0, findings: [] };
|
|
520
|
+
for (const root of roots) {
|
|
521
|
+
const absolute = path.join(repoRoot, root);
|
|
522
|
+
try {
|
|
523
|
+
if (!statSync(absolute).isDirectory()) continue;
|
|
524
|
+
} catch {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
for (const file of collectFiles(absolute, repoRoot)) {
|
|
528
|
+
report.files += 1;
|
|
529
|
+
const result = inspectSource(
|
|
530
|
+
readFileSync(file.absolute, "utf8"),
|
|
531
|
+
file.relative
|
|
532
|
+
);
|
|
533
|
+
report.inspected += result.inspected;
|
|
534
|
+
report.findings.push(...result.findings);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
report.findings.sort(
|
|
538
|
+
(left, right) =>
|
|
539
|
+
left.file.localeCompare(right.file) || left.line - right.line
|
|
540
|
+
);
|
|
541
|
+
return report;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Render the human-readable report.
|
|
546
|
+
* @param {ProbeReport} report - Result.
|
|
547
|
+
* @returns {string} The report text.
|
|
548
|
+
*/
|
|
549
|
+
export function formatReport(report) {
|
|
550
|
+
const lines = [
|
|
551
|
+
`check:probe-absence-direction — inspected ${report.inspected} absence-on-failure site(s) across ${report.files} file(s).`,
|
|
552
|
+
" Not audited by this sweep:",
|
|
553
|
+
...BLIND_SPOTS.map(spot => ` · ${spot}`),
|
|
554
|
+
];
|
|
555
|
+
if (report.inspected === 0) {
|
|
556
|
+
lines.push(
|
|
557
|
+
" ✖ ZERO sites inspected. A sweep that parsed nothing cannot report a clean tree; treating this as a failure, not an all-clear."
|
|
558
|
+
);
|
|
559
|
+
return lines.join("\n");
|
|
560
|
+
}
|
|
561
|
+
for (const finding of report.findings) {
|
|
562
|
+
lines.push(
|
|
563
|
+
` ✖ ${finding.file}:${finding.line} (${finding.shape})`,
|
|
564
|
+
` ${finding.statement}`,
|
|
565
|
+
` ${finding.reason}.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (report.findings.length === 0) {
|
|
569
|
+
lines.push(
|
|
570
|
+
" ✔ Every probe that spends a failure as an absence declares which way the absence pushes the gate."
|
|
571
|
+
);
|
|
572
|
+
return lines.join("\n");
|
|
573
|
+
}
|
|
574
|
+
lines.push(
|
|
575
|
+
"",
|
|
576
|
+
"Fix: record the direction at the line, in the comment above the branch, on the branch itself, or in the enclosing function's JSDoc —",
|
|
577
|
+
" // probe-direction: fail-closed — an unresolvable ref excludes nothing, so the range examined grows rather than shrinks.",
|
|
578
|
+
" // probe-direction: neutral — the value is printed in the report; no gate reads it.",
|
|
579
|
+
"`fail-open` is not an exemption. If the failure makes a gate looser, the probe has to stop being able to hand a failure back as an answer:",
|
|
580
|
+
"return a three-state result (found / absent / could-not-ask), or let the failure throw."
|
|
581
|
+
);
|
|
582
|
+
return lines.join("\n");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* CLI entry point.
|
|
587
|
+
* @returns {void}
|
|
588
|
+
*/
|
|
589
|
+
export function main() {
|
|
590
|
+
const args = process.argv.slice(2);
|
|
591
|
+
const unknown = args.find(arg => arg.startsWith("--") && arg !== "--json");
|
|
592
|
+
if (unknown) {
|
|
593
|
+
console.error(`check:probe-absence-direction: unknown flag ${unknown}`);
|
|
594
|
+
process.exitCode = 2;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const json = args.includes("--json");
|
|
598
|
+
const repoRoot = path.resolve(args.find(arg => !arg.startsWith("--")) ?? ".");
|
|
599
|
+
const report = sweep(repoRoot);
|
|
600
|
+
console.log(
|
|
601
|
+
json
|
|
602
|
+
? JSON.stringify({ ...report, blindSpots: BLIND_SPOTS }, null, 2)
|
|
603
|
+
: formatReport(report)
|
|
604
|
+
);
|
|
605
|
+
if (report.inspected === 0) process.exitCode = 2;
|
|
606
|
+
else if (report.findings.length > 0) process.exitCode = 1;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (invokedAsScript(import.meta.url)) {
|
|
610
|
+
main();
|
|
611
|
+
}
|
|
@@ -116,7 +116,13 @@ function mappedPaths(root) {
|
|
|
116
116
|
}
|
|
117
117
|
if (patterns.length === 0) return [];
|
|
118
118
|
const attr = git(["check-attr", "merge", "--", ...patterns], root);
|
|
119
|
-
|
|
119
|
+
// probe-direction: fail-closed — when `check-attr` cannot be asked, fall back
|
|
120
|
+
// to the patterns `.gitattributes` actually declares. Returning [] instead
|
|
121
|
+
// made the caller print "nothing in this working tree maps to the driver —
|
|
122
|
+
// nothing to register" and exit 0, so a git that failed was reported as a
|
|
123
|
+
// checkout that needs nothing. The fallback costs a redundant registration;
|
|
124
|
+
// the empty list cost a silently skipped one.
|
|
125
|
+
if (!attr.ok) return patterns;
|
|
120
126
|
return attr.stdout
|
|
121
127
|
.split("\n")
|
|
122
128
|
.filter(line => line.endsWith(`merge: ${GENERATED_ARTIFACT_DRIVER}`))
|
|
@@ -117,26 +117,25 @@ export function guardPopulation(root) {
|
|
|
117
117
|
.filter(file => file.length > 0)
|
|
118
118
|
.filter(file => !NOT_GUARDS.some(prefix => file.startsWith(prefix)))
|
|
119
119
|
.sort((left, right) => left.localeCompare(right));
|
|
120
|
-
return tracked.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
return tracked.map(file => {
|
|
121
|
+
// probe-direction: fail-closed — an unreadable TRACKED guard RAISES rather
|
|
122
|
+
// than dropping out of the roster. `git ls-files` has already proved the
|
|
123
|
+
// path is tracked, so a read failure is "could not ask", and the previous
|
|
124
|
+
// `catch { return []; }` spent it as "this guard does not exist" — which
|
|
125
|
+
// silently shrinks the population the coverage check is measured against,
|
|
126
|
+
// so a guard stops being covered with nothing saying so. This is the
|
|
127
|
+
// population, not a sample: it must be all of it or an error.
|
|
128
|
+
const bytes = readFileSync(path.join(root, file));
|
|
127
129
|
const source = bytes.toString("utf8");
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
/\bexit[ \t]+2\b/u.test(source),
|
|
138
|
-
},
|
|
139
|
-
];
|
|
130
|
+
return {
|
|
131
|
+
path: file,
|
|
132
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
133
|
+
size: bytes.length,
|
|
134
|
+
canRefuse: /\bexit[ \t]+[1-9]\d*\b/u.test(source),
|
|
135
|
+
toolBoundary:
|
|
136
|
+
(file.includes("/hooks/") || /tool_input|"file_path"/u.test(source)) &&
|
|
137
|
+
/\bexit[ \t]+2\b/u.test(source),
|
|
138
|
+
};
|
|
140
139
|
});
|
|
141
140
|
}
|
|
142
141
|
|
|
@@ -124,6 +124,10 @@ function git(args, cwd) {
|
|
|
124
124
|
timeout: 30_000,
|
|
125
125
|
});
|
|
126
126
|
} catch {
|
|
127
|
+
// probe-direction: fail-closed — null never reaches a verdict as "no
|
|
128
|
+
// weakening found". `changePlan` turns it into `undeterminable`, which
|
|
129
|
+
// REFUSES, and `isPromotion` falls to the strict reading that keeps the
|
|
130
|
+
// allow list pinned to the baseline.
|
|
127
131
|
return null;
|
|
128
132
|
}
|
|
129
133
|
}
|
|
@@ -169,6 +173,8 @@ function resolvePlan(mode, root, baseRef, onlyFiles) {
|
|
|
169
173
|
try {
|
|
170
174
|
return fs.readFileSync(path.join(root, f), "utf-8");
|
|
171
175
|
} catch {
|
|
176
|
+
// probe-direction: fail-closed — an unreadable current file compares as
|
|
177
|
+
// absent against the baseline, which is the strict side of the ratchet.
|
|
172
178
|
return null;
|
|
173
179
|
}
|
|
174
180
|
},
|