@codyswann/lisa 3.6.2 → 3.8.0
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/dist/cli/doctor-worktree-hygiene.d.ts +40 -0
- package/dist/cli/doctor-worktree-hygiene.d.ts.map +1 -0
- package/dist/cli/doctor-worktree-hygiene.js +123 -0
- package/dist/cli/doctor-worktree-hygiene.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +6 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +24 -11
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/required-checks.json +9 -1
- package/nestjs/create-only/.github/required-checks.json +9 -1
- package/package.json +1 -1
- 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 +29 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-pull-request-review/SKILL.md +50 -2
- package/plugins/lisa/rules/eager/falsifiable-checks.md +3 -1
- package/plugins/lisa/rules/reference/derived-branch-plan.md +18 -2
- package/plugins/lisa/rules/reference/falsifiable-checks.md +57 -5
- package/plugins/lisa/scripts/design-source-gate.mjs +11 -2
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
- package/plugins/lisa/skills/lisa-pull-request-review/SKILL.md +50 -2
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-source-gate.mjs +11 -2
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
- package/plugins/lisa-agy/skills/lisa-pull-request-review/SKILL.md +50 -2
- 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/rules/eager/falsifiable-checks.md +3 -1
- package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +18 -2
- package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +57 -5
- package/plugins/lisa-copilot/scripts/design-source-gate.mjs +11 -2
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
- package/plugins/lisa-copilot/skills/lisa-pull-request-review/SKILL.md +50 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +18 -2
- package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +57 -5
- package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +3 -1
- package/plugins/lisa-cursor/scripts/design-source-gate.mjs +11 -2
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
- package/plugins/lisa-cursor/skills/lisa-pull-request-review/SKILL.md +50 -2
- 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/rules/eager/falsifiable-checks.md +3 -1
- package/plugins/src/base/rules/reference/derived-branch-plan.md +18 -2
- package/plugins/src/base/rules/reference/falsifiable-checks.md +57 -5
- package/plugins/src/base/scripts/design-source-gate.mjs +11 -2
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
- package/plugins/src/base/skills/lisa-pull-request-review/SKILL.md +50 -2
- package/typescript/copy-overwrite/ast-grep/rules/no-missing-fs-extra-namespace-member-js.yml +71 -0
- package/typescript/copy-overwrite/ast-grep/rules/no-missing-fs-extra-namespace-member.yml +90 -0
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +402 -5
- package/typescript/create-only/.github/required-checks.json +13 -3
- package/typescript/package-lisa/package.lisa.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# This file is managed by Lisa.
|
|
2
|
+
# Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
3
|
+
#
|
|
4
|
+
# JavaScript-language twin of `no-missing-fs-extra-namespace-member.yml`.
|
|
5
|
+
#
|
|
6
|
+
# ast-grep binds one parser per rule: `language: typescript` scans only
|
|
7
|
+
# .ts/.mts/.cts, so .js/.mjs/.cjs would otherwise be an unguarded gap — and a
|
|
8
|
+
# guard with a silent hole is the failure mode this rule exists to prevent.
|
|
9
|
+
# Build scripts and tooling in TypeScript projects are routinely .mjs, and
|
|
10
|
+
# fs-extra is exactly the kind of dependency they reach for.
|
|
11
|
+
#
|
|
12
|
+
# The `constraints` block MUST stay byte-identical to the TypeScript rule.
|
|
13
|
+
# `tests/unit/config/ast-grep-template.test.ts` asserts both files carry the
|
|
14
|
+
# same allow list AND that it still equals the set real Node reports, so these
|
|
15
|
+
# two cannot drift apart or drift away from reality unnoticed.
|
|
16
|
+
#
|
|
17
|
+
# See the TypeScript rule for the full rationale on why this is an allow list
|
|
18
|
+
# rather than a deny list, and why the set is stable across fs-extra 11.x.
|
|
19
|
+
id: no-missing-fs-extra-namespace-member-js
|
|
20
|
+
language: javascript
|
|
21
|
+
severity: error
|
|
22
|
+
message: |
|
|
23
|
+
`fs-extra` does not expose this member on its ESM namespace object.
|
|
24
|
+
|
|
25
|
+
`import * as fse from "fs-extra"` resolves to fs-extra's CommonJS entry, so
|
|
26
|
+
Node derives the namespace keys with cjs-module-lexer. Only fs-extra's own
|
|
27
|
+
directly-assigned functions survive that. Absent members include every JSON
|
|
28
|
+
helper (readJson, readJSON, writeJson, outputJson, and their Sync forms),
|
|
29
|
+
every node:fs passthrough (readFile, writeFile, readdir, stat, existsSync,
|
|
30
|
+
appendFile, ...), and copySync, moveSync, removeSync, pathExistsSync and
|
|
31
|
+
outputFileSync.
|
|
32
|
+
|
|
33
|
+
The call is `undefined` at run time and throws a TypeError, and unit tests
|
|
34
|
+
cannot see it — Vitest and Jest resolve the import through CJS interop and
|
|
35
|
+
hand back the default export's properties.
|
|
36
|
+
|
|
37
|
+
Fix by importing the concrete API instead:
|
|
38
|
+
- file I/O -> `readFile` / `writeFile` from `node:fs/promises`
|
|
39
|
+
- JSON -> read the file, then `JSON.parse` / `JSON.stringify`
|
|
40
|
+
- whole module -> `import fse from "fs-extra"`. The DEFAULT export does
|
|
41
|
+
carry every member and is deliberately not flagged here.
|
|
42
|
+
# Test trees are excluded on purpose — see the TypeScript rule for the full
|
|
43
|
+
# rationale. In short: the defect class is "works under a bundler, `undefined`
|
|
44
|
+
# under real Node", so it can only bite code Node executes directly, and the
|
|
45
|
+
# runtime control in the Lisa monorepo already scopes itself to source trees
|
|
46
|
+
# for exactly that reason. Keep this list in step with the TypeScript rule.
|
|
47
|
+
ignores:
|
|
48
|
+
- '**/tests/**'
|
|
49
|
+
- '**/test/**'
|
|
50
|
+
- '**/__tests__/**'
|
|
51
|
+
- '**/__mocks__/**'
|
|
52
|
+
- '**/*.test.js'
|
|
53
|
+
- '**/*.test.mjs'
|
|
54
|
+
- '**/*.test.cjs'
|
|
55
|
+
- '**/*.test.jsx'
|
|
56
|
+
- '**/*.spec.js'
|
|
57
|
+
- '**/*.spec.mjs'
|
|
58
|
+
- '**/*.spec.cjs'
|
|
59
|
+
- '**/*.spec.jsx'
|
|
60
|
+
rule:
|
|
61
|
+
all:
|
|
62
|
+
- pattern: $NS.$MEMBER
|
|
63
|
+
- inside:
|
|
64
|
+
kind: program
|
|
65
|
+
stopBy: end
|
|
66
|
+
has:
|
|
67
|
+
pattern: import * as $NS from "fs-extra"
|
|
68
|
+
constraints:
|
|
69
|
+
MEMBER:
|
|
70
|
+
not:
|
|
71
|
+
regex: ^(copy|createFile|createFileSync|createLink|createLinkSync|createSymlink|createSymlinkSync|default|emptyDir|emptyDirSync|emptydir|emptydirSync|ensureDir|ensureDirSync|ensureFile|ensureFileSync|ensureLink|ensureLinkSync|ensureSymlink|ensureSymlinkSync|exists|mkdirp|mkdirpSync|mkdirs|mkdirsSync|move|outputFile|pathExists|read|readv|remove|write|writev)$
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# This file is managed by Lisa.
|
|
2
|
+
# Do not edit directly — changes will be overwritten on the next `lisa` run.
|
|
3
|
+
#
|
|
4
|
+
# Bans `fs-extra` namespace members that do not exist under real Node ESM.
|
|
5
|
+
#
|
|
6
|
+
# The allow list below is an ALLOW list, not a deny list, and that is
|
|
7
|
+
# deliberate. A deny list fails open: any absent member nobody thought to
|
|
8
|
+
# enumerate passes silently, which is the exact rot this rule exists to
|
|
9
|
+
# prevent. An allow list fails closed — an unrecognised member is flagged
|
|
10
|
+
# loudly and a human decides.
|
|
11
|
+
#
|
|
12
|
+
# The list is the set of keys real Node reports for
|
|
13
|
+
# `import * as fse from "fs-extra"`. It is byte-identical across fs-extra
|
|
14
|
+
# 11.1.1, 11.3.3, 11.3.4 and 11.3.5, because the bare specifier always routes
|
|
15
|
+
# through the package `exports` map to the CommonJS entry (`./lib/index.js`)
|
|
16
|
+
# and Node derives the namespace keys with cjs-module-lexer from a re-export
|
|
17
|
+
# shape that has not changed. `lib/esm.mjs` — which does export the JSON
|
|
18
|
+
# helpers — is reachable only via the `fs-extra/esm` subpath, which is a
|
|
19
|
+
# different import.
|
|
20
|
+
#
|
|
21
|
+
# Anti-rot: `tests/unit/core/fs-extra-namespace-callsites.test.ts` in the Lisa
|
|
22
|
+
# monorepo derives this same set from real Node at run time, and
|
|
23
|
+
# `tests/unit/config/ast-grep-template.test.ts` asserts the list below still
|
|
24
|
+
# equals it. If fs-extra ever changes its namespace surface, that pairing goes
|
|
25
|
+
# red centrally instead of this list silently drifting in every host project.
|
|
26
|
+
id: no-missing-fs-extra-namespace-member
|
|
27
|
+
language: typescript
|
|
28
|
+
severity: error
|
|
29
|
+
message: |
|
|
30
|
+
`fs-extra` does not expose this member on its ESM namespace object.
|
|
31
|
+
|
|
32
|
+
`import * as fse from "fs-extra"` resolves to fs-extra's CommonJS entry, so
|
|
33
|
+
Node derives the namespace keys with cjs-module-lexer. Only fs-extra's own
|
|
34
|
+
directly-assigned functions survive that. Absent members include every JSON
|
|
35
|
+
helper (readJson, readJSON, writeJson, outputJson, and their Sync forms),
|
|
36
|
+
every node:fs passthrough (readFile, writeFile, readdir, stat, existsSync,
|
|
37
|
+
appendFile, ...), and copySync, moveSync, removeSync, pathExistsSync and
|
|
38
|
+
outputFileSync.
|
|
39
|
+
|
|
40
|
+
The call is `undefined` at run time and throws a TypeError. Unit tests are
|
|
41
|
+
structurally blind to it: Vitest and Jest resolve the import through CJS
|
|
42
|
+
interop and hand back the default export's properties, so the call is a
|
|
43
|
+
function in every test and `undefined` in production. Lisa shipped this
|
|
44
|
+
defect three times in one release window (#2482, #2487) — once as a crash
|
|
45
|
+
that stranded seven merged PRs, twice inside error-swallowing `catch` blocks
|
|
46
|
+
where the TypeError replaced the error being handled.
|
|
47
|
+
|
|
48
|
+
Fix by importing the concrete API instead:
|
|
49
|
+
- file I/O -> `readFile` / `writeFile` from `node:fs/promises`
|
|
50
|
+
- JSON -> read the file, then `JSON.parse` / `JSON.stringify`
|
|
51
|
+
- whole module -> `import fse from "fs-extra"`. The DEFAULT export does
|
|
52
|
+
carry every member and is deliberately not flagged here.
|
|
53
|
+
# Test trees are excluded on purpose, matching the scope the runtime control
|
|
54
|
+
# already chose. The defect class is "works under a bundler, `undefined` under
|
|
55
|
+
# real Node" — so it can only bite code that Node executes directly. Vitest and
|
|
56
|
+
# Jest resolve `import * as fse from "fs-extra"` through CJS interop and hand
|
|
57
|
+
# back the default export's properties, which is precisely why test files can
|
|
58
|
+
# call `fse.writeJson` forever without ever being wrong.
|
|
59
|
+
#
|
|
60
|
+
# `tests/unit/core/fs-extra-namespace-callsites.test.ts` scopes itself to
|
|
61
|
+
# ["src", "scripts"] for this reason — "trees whose code is executed by Node
|
|
62
|
+
# directly, not through a bundler". This rule matches that scope rather than
|
|
63
|
+
# inventing a second, wider one. Path globs are used instead of a src/scripts
|
|
64
|
+
# allowlist because host project layouts vary (apps/, packages/, lib/) while
|
|
65
|
+
# test directory conventions do not.
|
|
66
|
+
ignores:
|
|
67
|
+
- '**/tests/**'
|
|
68
|
+
- '**/test/**'
|
|
69
|
+
- '**/__tests__/**'
|
|
70
|
+
- '**/__mocks__/**'
|
|
71
|
+
- '**/*.test.ts'
|
|
72
|
+
- '**/*.test.mts'
|
|
73
|
+
- '**/*.test.cts'
|
|
74
|
+
- '**/*.test.tsx'
|
|
75
|
+
- '**/*.spec.ts'
|
|
76
|
+
- '**/*.spec.mts'
|
|
77
|
+
- '**/*.spec.cts'
|
|
78
|
+
- '**/*.spec.tsx'
|
|
79
|
+
rule:
|
|
80
|
+
all:
|
|
81
|
+
- pattern: $NS.$MEMBER
|
|
82
|
+
- inside:
|
|
83
|
+
kind: program
|
|
84
|
+
stopBy: end
|
|
85
|
+
has:
|
|
86
|
+
pattern: import * as $NS from "fs-extra"
|
|
87
|
+
constraints:
|
|
88
|
+
MEMBER:
|
|
89
|
+
not:
|
|
90
|
+
regex: ^(copy|createFile|createFileSync|createLink|createLinkSync|createSymlink|createSymlinkSync|default|emptyDir|emptyDirSync|emptydir|emptydirSync|ensureDir|ensureDirSync|ensureFile|ensureFileSync|ensureLink|ensureLinkSync|ensureSymlink|ensureSymlinkSync|exists|mkdirp|mkdirpSync|mkdirs|mkdirsSync|move|outputFile|pathExists|read|readv|remove|write|writev)$
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* check-skipped-required-checks — refuse a
|
|
4
|
-
*
|
|
3
|
+
* check-skipped-required-checks — refuse a required status check that satisfies
|
|
4
|
+
* without proving anything.
|
|
5
5
|
*
|
|
6
6
|
* Shipped by Lisa (copy-overwrite). Generalized from tunnl's TUN-402 guard: the
|
|
7
7
|
* logic is Lisa's and gets updated fleet-wide, the two REVIEWED SNAPSHOTS it
|
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* node scripts/check-skipped-required-checks.mjs [rootDir] [--remote] [--json]
|
|
13
|
+
* node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
|
|
14
|
+
*
|
|
15
|
+
* ## The family this guard covers
|
|
16
|
+
*
|
|
17
|
+
* **Required-and-red is loud; required-and-vacuous is not; advisory-and-stale is
|
|
18
|
+
* invisible.** All three are the same defect wearing different clothes — a gate
|
|
19
|
+
* that reports satisfied without having proven anything — and the useful
|
|
20
|
+
* question is never "did the check pass" but "did the check do anything".
|
|
21
|
+
*
|
|
22
|
+
* Two of the three live here:
|
|
23
|
+
*
|
|
24
|
+
* - **Skipped** (`--remote` / offline arm, below): GitHub counts a `skipped`
|
|
25
|
+
* required check as SATISFIED, so a `skip_jobs` token makes the gate
|
|
26
|
+
* decorative. Static, offline, BLOCKING.
|
|
27
|
+
* - **Vacuous** (`--pr` arm): the check really ran and really reported
|
|
28
|
+
* `success`, having done no work — measured on CodeRabbit posting
|
|
29
|
+
* `success — "Review rate limited"`. Live, per-PR, REPORTING ONLY.
|
|
13
30
|
*
|
|
14
31
|
* ## Where this runs
|
|
15
32
|
*
|
|
@@ -96,6 +113,57 @@
|
|
|
96
113
|
* network and `gh` auth on every run would flake, and a flaky guard gets
|
|
97
114
|
* skipped — which reintroduces exactly the false-green class this file refuses.
|
|
98
115
|
*
|
|
116
|
+
* ## `--pr` — the VACUOUS arm, and why it only ever reports
|
|
117
|
+
*
|
|
118
|
+
* Measured (CodySwannGT/lisa#2497): `CodeRabbit` was in this repository's
|
|
119
|
+
* required set, and on PRs #2483 and #2484 it posted `success` with the
|
|
120
|
+
* description `Review rate limited` having performed ZERO reviews. Both merged
|
|
121
|
+
* on that green, both carried security-relevant changes, both shipped in tag
|
|
122
|
+
* `v3.5.1`. Branch protection recorded "reviewed" for work nothing reviewed.
|
|
123
|
+
*
|
|
124
|
+
* The failure is silent by construction, and this is the whole point:
|
|
125
|
+
*
|
|
126
|
+
* ```
|
|
127
|
+
* gh pr checks <PR> | grep -i coderabbit
|
|
128
|
+
* CodeRabbit pass 0 Review rate limited <- hollow
|
|
129
|
+
* CodeRabbit pass 1 Review completed <- real
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* **The status column says `pass` either way. Only the description
|
|
133
|
+
* distinguishes them.** So anything gating on such a check must read the
|
|
134
|
+
* description, and `--pr` is the machine-readable form of that one-line triage.
|
|
135
|
+
*
|
|
136
|
+
* This arm NEVER blocks — `NEVER_BLOCKING`, enforced regardless of the
|
|
137
|
+
* declaration's `enforcement` mode. Two independent reasons, both load-bearing:
|
|
138
|
+
*
|
|
139
|
+
* 1. A review bot's availability can depend on an org-wide SPENDING CAP. A
|
|
140
|
+
* blocking check that fires on a billing state makes merges hostage to
|
|
141
|
+
* accounting, which is a worse gate than the one it replaces.
|
|
142
|
+
* 2. Whether a review bot belongs in the required set at all is a governance
|
|
143
|
+
* decision an owner has to make. Shipping the gate before the decision
|
|
144
|
+
* would pre-empt it. Detection is what is uncontroversial; act on it.
|
|
145
|
+
*
|
|
146
|
+
* ## Proof is matched STRICTLY, no-work LOOSELY
|
|
147
|
+
*
|
|
148
|
+
* The two description lists are deliberately asymmetric, because their errors
|
|
149
|
+
* are not symmetric:
|
|
150
|
+
*
|
|
151
|
+
* - A `proof` phrase must match the whole description (case-insensitive,
|
|
152
|
+
* trimmed). Matching here GRANTS CREDIT, and a loose match that grants
|
|
153
|
+
* credit is exactly the false green this file exists to refuse.
|
|
154
|
+
* - A `no_work` phrase matches as a substring. Matching here DENIES credit,
|
|
155
|
+
* so breadth is safe — and it survives a vendor appending detail
|
|
156
|
+
* (`Review rate limited (retry in 12m)`).
|
|
157
|
+
*
|
|
158
|
+
* Anything matching neither is `unproven` — reported, never silently passed. A
|
|
159
|
+
* vocabulary nobody enumerated must not read as a pass.
|
|
160
|
+
*
|
|
161
|
+
* Unlike `required_contexts`, this vocabulary is NOT repo-specific: `Review
|
|
162
|
+
* rate limited` is the vendor's own product string, identical in every
|
|
163
|
+
* repository. That is why shipping it as a default is safe where shipping a
|
|
164
|
+
* guessed ruleset was not (#2476) — and why a wrong guess here costs one line
|
|
165
|
+
* of report rather than a red build.
|
|
166
|
+
*
|
|
99
167
|
* ## Exact string equality, everywhere
|
|
100
168
|
*
|
|
101
169
|
* Every comparison here is `===`. Repos routinely carry confusable pairs — an
|
|
@@ -170,6 +238,55 @@ export const VIOLATIONS = Object.freeze({
|
|
|
170
238
|
badExemption: "exemption_without_valid_ticket",
|
|
171
239
|
remoteDrift: "ruleset_snapshot_drift",
|
|
172
240
|
whitespace: "whitespace_in_skip_token",
|
|
241
|
+
vacuous: "vacuous_required_check",
|
|
242
|
+
unproven: "unproven_required_check",
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The shipped description vocabulary for review-bot style checks.
|
|
247
|
+
*
|
|
248
|
+
* `proof` is matched STRICTLY (whole description, case-insensitive, trimmed)
|
|
249
|
+
* because a match grants credit. `no_work` is matched LOOSELY (substring)
|
|
250
|
+
* because a match denies it. See the header for why that asymmetry is the safe
|
|
251
|
+
* direction.
|
|
252
|
+
*
|
|
253
|
+
* Every string here was read off a real check on a real PR in this fleet, not
|
|
254
|
+
* invented: `Review rate limited` (#2483, #2484, #2495), `Review approved`
|
|
255
|
+
* (#2350). A repository may extend either list per check without losing these.
|
|
256
|
+
*/
|
|
257
|
+
export const REVIEW_DESCRIPTION_DEFAULTS = Object.freeze({
|
|
258
|
+
proof: Object.freeze([
|
|
259
|
+
"review approved",
|
|
260
|
+
"review completed",
|
|
261
|
+
"changes requested",
|
|
262
|
+
"comments posted",
|
|
263
|
+
]),
|
|
264
|
+
no_work: Object.freeze([
|
|
265
|
+
"rate limited",
|
|
266
|
+
"review queued",
|
|
267
|
+
"review skipped",
|
|
268
|
+
"skipped",
|
|
269
|
+
"queued",
|
|
270
|
+
"waiting",
|
|
271
|
+
"in progress",
|
|
272
|
+
"no review",
|
|
273
|
+
"disabled",
|
|
274
|
+
"quota",
|
|
275
|
+
"billing",
|
|
276
|
+
]),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Verdicts `classifyCheckDescription` returns.
|
|
281
|
+
*
|
|
282
|
+
* `unproven` is the FALLBACK on purpose: the absence of a recognised phrase is
|
|
283
|
+
* the absence of evidence, and this file's whole thesis is that those are not
|
|
284
|
+
* the same as a pass.
|
|
285
|
+
*/
|
|
286
|
+
export const DESCRIPTION_VERDICTS = Object.freeze({
|
|
287
|
+
proved: "proved",
|
|
288
|
+
noWork: "no-work",
|
|
289
|
+
unproven: "unproven",
|
|
173
290
|
});
|
|
174
291
|
|
|
175
292
|
/** Enforcement modes a declaration may select. */
|
|
@@ -195,6 +312,50 @@ const ALWAYS_BLOCKING = Object.freeze([
|
|
|
195
312
|
VIOLATIONS.remoteDrift,
|
|
196
313
|
]);
|
|
197
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Violation kinds that NEVER fail the build, in any enforcement mode.
|
|
317
|
+
*
|
|
318
|
+
* The vacuity arm reports and stops there. A required check can go hollow
|
|
319
|
+
* because a vendor hit an org-wide SPENDING CAP, and a gate that reddens every
|
|
320
|
+
* PR the moment a bill goes unpaid is a worse gate than the one it is
|
|
321
|
+
* criticising. Whether such a check belongs in the required set at all is a
|
|
322
|
+
* governance decision an owner makes in an admin console, not one this script
|
|
323
|
+
* may pre-empt by turning its own finding into a blocker.
|
|
324
|
+
*
|
|
325
|
+
* Detection is the uncontroversial half, and it is the half that was missing:
|
|
326
|
+
* nothing anywhere could previously tell "the check reported success" apart
|
|
327
|
+
* from "the check did anything".
|
|
328
|
+
*
|
|
329
|
+
* This list is checked BEFORE `ALWAYS_BLOCKING` and before the enforcement
|
|
330
|
+
* mode, so deleting the `enforcement` key cannot silently arm it.
|
|
331
|
+
*/
|
|
332
|
+
export const NEVER_BLOCKING = Object.freeze([
|
|
333
|
+
VIOLATIONS.vacuous,
|
|
334
|
+
VIOLATIONS.unproven,
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Reads `--name=value` or `--name value` out of argv.
|
|
339
|
+
*
|
|
340
|
+
* Returns `undefined` for an absent flag and for `--name` with no value, so a
|
|
341
|
+
* typo cannot be read as an empty PR number and silently examine nothing.
|
|
342
|
+
*
|
|
343
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
344
|
+
* @param {string} name - The flag, including its leading dashes
|
|
345
|
+
* @returns {string|undefined} The value, or undefined
|
|
346
|
+
*/
|
|
347
|
+
export function readFlagValue(argv, name) {
|
|
348
|
+
const inline = argv.find(arg => arg.startsWith(`${name}=`));
|
|
349
|
+
if (inline !== undefined) {
|
|
350
|
+
const value = inline.slice(name.length + 1).trim();
|
|
351
|
+
return value === "" ? undefined : value;
|
|
352
|
+
}
|
|
353
|
+
const at = argv.indexOf(name);
|
|
354
|
+
if (at === -1) return undefined;
|
|
355
|
+
const next = argv[at + 1];
|
|
356
|
+
return next === undefined || next.startsWith("--") ? undefined : next;
|
|
357
|
+
}
|
|
358
|
+
|
|
198
359
|
/**
|
|
199
360
|
* True when a line is a whole-line YAML comment.
|
|
200
361
|
*
|
|
@@ -403,6 +564,30 @@ export function loadDeclaration(rootDir) {
|
|
|
403
564
|
);
|
|
404
565
|
}
|
|
405
566
|
}
|
|
567
|
+
|
|
568
|
+
// Same reasoning for the vacuity declarations: a non-object entry would read
|
|
569
|
+
// as "declared" and then yield an empty vocabulary, quietly examining the
|
|
570
|
+
// check against defaults the author thought they had overridden.
|
|
571
|
+
for (const [name, entry] of Object.entries(
|
|
572
|
+
declaration.evidence_bearing_checks ?? {}
|
|
573
|
+
)) {
|
|
574
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`check-skipped-required-checks: the declaration for \`${name}\` in \`evidence_bearing_checks\` must be an object — use \`{}\` to accept the shipped description vocabulary.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
for (const list of ["proof", "no_work"]) {
|
|
580
|
+
if (entry[list] === undefined) continue;
|
|
581
|
+
if (
|
|
582
|
+
!Array.isArray(entry[list]) ||
|
|
583
|
+
entry[list].some(phrase => typeof phrase !== "string")
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`check-skipped-required-checks: \`evidence_bearing_checks.${name}.${list}\` must be an array of description strings.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
406
591
|
return declaration;
|
|
407
592
|
}
|
|
408
593
|
|
|
@@ -616,6 +801,184 @@ export function evaluateSkippedRequiredChecks(
|
|
|
616
801
|
return { violations, checked: skipped.length };
|
|
617
802
|
}
|
|
618
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Decides whether a check's description proves the check did any work.
|
|
806
|
+
*
|
|
807
|
+
* @param {string|undefined} description - The check's description, verbatim
|
|
808
|
+
* @param {{proof?: ReadonlyArray<string>, no_work?: ReadonlyArray<string>}} [vocabulary] -
|
|
809
|
+
* Per-check additions. Merged WITH the shipped defaults rather than replacing
|
|
810
|
+
* them, so a repository naming one extra proof phrase does not silently lose
|
|
811
|
+
* the no-work list that catches the measured defect.
|
|
812
|
+
* @returns {string} One of `DESCRIPTION_VERDICTS`
|
|
813
|
+
*/
|
|
814
|
+
export function classifyCheckDescription(description, vocabulary = {}) {
|
|
815
|
+
const text = (description ?? "").trim().toLowerCase();
|
|
816
|
+
if (text === "") return DESCRIPTION_VERDICTS.unproven;
|
|
817
|
+
|
|
818
|
+
// No-work is tested FIRST. The lists are asserted non-overlapping in the
|
|
819
|
+
// suite, so order cannot change a verdict today — testing the denying rule
|
|
820
|
+
// first means a future overlap fails safe (denied) rather than granting
|
|
821
|
+
// credit, which is the direction that matters.
|
|
822
|
+
const noWork = [
|
|
823
|
+
...REVIEW_DESCRIPTION_DEFAULTS.no_work,
|
|
824
|
+
...(vocabulary.no_work ?? []),
|
|
825
|
+
];
|
|
826
|
+
if (noWork.some(phrase => text.includes(phrase.trim().toLowerCase()))) {
|
|
827
|
+
return DESCRIPTION_VERDICTS.noWork;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const proof = [
|
|
831
|
+
...REVIEW_DESCRIPTION_DEFAULTS.proof,
|
|
832
|
+
...(vocabulary.proof ?? []),
|
|
833
|
+
];
|
|
834
|
+
if (proof.some(phrase => text === phrase.trim().toLowerCase())) {
|
|
835
|
+
return DESCRIPTION_VERDICTS.proved;
|
|
836
|
+
}
|
|
837
|
+
return DESCRIPTION_VERDICTS.unproven;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Reports every declared evidence-bearing check that satisfied without proving
|
|
842
|
+
* it did work.
|
|
843
|
+
*
|
|
844
|
+
* A check is examined only when the repository named it in
|
|
845
|
+
* `evidence_bearing_checks` — matched by EXACT name, like every other
|
|
846
|
+
* comparison in this file. Most CI jobs ship an empty description, so
|
|
847
|
+
* flagging them all would bury the one finding that matters, and the obvious
|
|
848
|
+
* fix for a noisy guard is to delete it.
|
|
849
|
+
*
|
|
850
|
+
* Four outcomes per declared check:
|
|
851
|
+
*
|
|
852
|
+
* - Green + a `proof` description → nothing. This is the case the whole
|
|
853
|
+
* machine exists to reach.
|
|
854
|
+
* - Green + a `no_work` description → `vacuous_required_check`. The measured
|
|
855
|
+
* #2483/#2484 defect.
|
|
856
|
+
* - Green + anything else → `unproven_required_check`. Not an accusation: a
|
|
857
|
+
* statement that this run produced no evidence either way.
|
|
858
|
+
* - Absent entirely → `unproven_required_check`. Measured on #2493/#2491/
|
|
859
|
+
* #2488, where the bot posted no context at all; "no unresolved review
|
|
860
|
+
* threads" there means nobody looked, not that nothing was wrong.
|
|
861
|
+
*
|
|
862
|
+
* A RED check is deliberately ignored. Required-and-red is the loud case and
|
|
863
|
+
* needs no help from here; reporting it too would make this arm indistinguish-
|
|
864
|
+
* able from ordinary CI noise.
|
|
865
|
+
*
|
|
866
|
+
* `required_contexts` changes only the WORDING — whether branch protection
|
|
867
|
+
* actually recorded this hollow green as a satisfied gate. When that snapshot
|
|
868
|
+
* is untrusted the finding still stands; the guard just declines to claim
|
|
869
|
+
* required-ness it has not transcribed.
|
|
870
|
+
*
|
|
871
|
+
* @param {object} declaration - The per-repo declaration
|
|
872
|
+
* @param {ReadonlyArray<{name: string, state: string, bucket?: string, description?: string}>} checks -
|
|
873
|
+
* Checks as `gh pr checks --json name,state,bucket,description` returns them
|
|
874
|
+
* @param {{trustRequiredContexts?: boolean}} [options] - Set
|
|
875
|
+
* `trustRequiredContexts: false` to stop asserting whether a check is required
|
|
876
|
+
* @returns {{violations: object[], checked: number}} Violations and how many declared checks were examined
|
|
877
|
+
*/
|
|
878
|
+
export function evaluateVacuousChecks(declaration, checks, options = {}) {
|
|
879
|
+
const declared = declaration.evidence_bearing_checks ?? {};
|
|
880
|
+
const trustRequired = options.trustRequiredContexts !== false;
|
|
881
|
+
const required = new Set(declaration.required_contexts ?? []);
|
|
882
|
+
const violations = [];
|
|
883
|
+
let checked = 0;
|
|
884
|
+
|
|
885
|
+
for (const [name, entry] of Object.entries(declared)) {
|
|
886
|
+
checked += 1;
|
|
887
|
+
const vocabulary = typeof entry === "object" && entry !== null ? entry : {};
|
|
888
|
+
const found = checks.find(check => check.name === name);
|
|
889
|
+
|
|
890
|
+
if (found === undefined) {
|
|
891
|
+
violations.push({
|
|
892
|
+
kind: VIOLATIONS.unproven,
|
|
893
|
+
token: name,
|
|
894
|
+
message: `\`${name}\` is declared evidence-bearing but did not report on this pull request at all. A report of "no unresolved review threads" from this PR means NOBODY LOOKED, not that nothing was wrong — say which one you observed. (If the context was renamed, fix \`evidence_bearing_checks\`; names are compared byte for byte.)`,
|
|
895
|
+
});
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const state = String(found.state ?? "").toUpperCase();
|
|
900
|
+
if (state === "FAILURE" || state === "ERROR") continue;
|
|
901
|
+
|
|
902
|
+
const verdict = classifyCheckDescription(found.description, vocabulary);
|
|
903
|
+
if (verdict === DESCRIPTION_VERDICTS.proved && state === "SUCCESS") {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const requiredNote = !trustRequired
|
|
908
|
+
? " Whether it is ruleset-required is NOT KNOWN here — `required_contexts` has not been transcribed, so this cannot say what the merge gate recorded."
|
|
909
|
+
: required.has(name)
|
|
910
|
+
? " This context IS ruleset-required, so branch protection recorded a satisfied review gate for a review that did not happen."
|
|
911
|
+
: " This context is not in `required_contexts`, so no merge gate was falsified — but nothing reviewed this either.";
|
|
912
|
+
|
|
913
|
+
violations.push(
|
|
914
|
+
verdict === DESCRIPTION_VERDICTS.noWork && state === "SUCCESS"
|
|
915
|
+
? {
|
|
916
|
+
kind: VIOLATIONS.vacuous,
|
|
917
|
+
token: name,
|
|
918
|
+
contexts: [name],
|
|
919
|
+
message: `\`${name}\` reported ${state} with the description ${JSON.stringify(found.description ?? "")}, which says it DID NO WORK.${requiredNote} \`gh pr checks\` prints \`pass\` for this exactly as it does for a real review — the description is the only thing that tells them apart. Treat this PR as UNREVIEWED.`,
|
|
920
|
+
}
|
|
921
|
+
: {
|
|
922
|
+
kind: VIOLATIONS.unproven,
|
|
923
|
+
token: name,
|
|
924
|
+
message: `\`${name}\` reported ${state} with the description ${JSON.stringify(found.description ?? "")}, which proves neither that it reviewed anything nor that it did not.${requiredNote} Read the check itself before treating this PR as reviewed, or add the phrase to \`evidence_bearing_checks.${name}.proof\` once you have confirmed what it means.`,
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return { violations, checked };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Reads one pull request's checks, descriptions included.
|
|
934
|
+
*
|
|
935
|
+
* `--json` is what makes this usable: the plain `gh pr checks` table is the
|
|
936
|
+
* human triage, and the description column is the load-bearing one, but only
|
|
937
|
+
* the JSON form survives being parsed. Both CheckRuns and legacy commit
|
|
938
|
+
* StatusContexts come back through this single call — CodeRabbit posts the
|
|
939
|
+
* latter, which `gh pr view --json statusCheckRollup` returns WITHOUT a
|
|
940
|
+
* description, so that route cannot see the defect at all.
|
|
941
|
+
*
|
|
942
|
+
* A non-zero exit is expected and ignored: `gh pr checks` exits 8 while checks
|
|
943
|
+
* are pending and 1 when any check failed, and both are perfectly readable
|
|
944
|
+
* states for this arm. Only unparseable output is an error.
|
|
945
|
+
*
|
|
946
|
+
* @param {string|number} pr - Pull request number or URL
|
|
947
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
948
|
+
* @returns {Array<{name: string, state: string, bucket?: string, description?: string}>} The checks
|
|
949
|
+
* @throws {Error} When `gh` is unavailable or its output cannot be parsed
|
|
950
|
+
*/
|
|
951
|
+
export function fetchPullRequestChecks(pr, repo) {
|
|
952
|
+
const args = [
|
|
953
|
+
"pr",
|
|
954
|
+
"checks",
|
|
955
|
+
String(pr),
|
|
956
|
+
"--json",
|
|
957
|
+
"name,state,bucket,description",
|
|
958
|
+
];
|
|
959
|
+
if (repo) args.push("--repo", repo);
|
|
960
|
+
let raw;
|
|
961
|
+
try {
|
|
962
|
+
raw = execFileSync("gh", args, { encoding: "utf8" });
|
|
963
|
+
} catch (error) {
|
|
964
|
+
raw = typeof error?.stdout === "string" ? error.stdout : "";
|
|
965
|
+
if (raw.trim() === "") {
|
|
966
|
+
throw new Error(
|
|
967
|
+
`check-skipped-required-checks: could not read checks for PR ${pr}${repo ? ` in ${repo}` : ""} — ${error instanceof Error ? error.message : String(error)}`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const parsed = JSON.parse(raw);
|
|
973
|
+
if (!Array.isArray(parsed)) throw new TypeError("not an array");
|
|
974
|
+
return parsed;
|
|
975
|
+
} catch (error) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`check-skipped-required-checks: \`gh pr checks --json\` returned output this cannot parse (${error instanceof Error ? error.message : String(error)}). Refusing to report "nothing vacuous" from output nobody read.`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
619
982
|
/**
|
|
620
983
|
* Fetches the live required contexts for every declared ruleset.
|
|
621
984
|
*
|
|
@@ -693,6 +1056,7 @@ export function runGuard(argv) {
|
|
|
693
1056
|
const positional = argv.filter(arg => !arg.startsWith("--"));
|
|
694
1057
|
const rootDir = positional[0] ?? process.cwd();
|
|
695
1058
|
const declaration = loadDeclaration(rootDir);
|
|
1059
|
+
const pr = readFlagValue(argv, "--pr");
|
|
696
1060
|
const collected = collectSkipJobTokens(rootDir, declaration.workflows);
|
|
697
1061
|
const remote = argv.includes("--remote");
|
|
698
1062
|
const live = remote
|
|
@@ -714,6 +1078,21 @@ export function runGuard(argv) {
|
|
|
714
1078
|
...compareRulesetBaseline(declaration.required_contexts, live)
|
|
715
1079
|
);
|
|
716
1080
|
}
|
|
1081
|
+
|
|
1082
|
+
// The vacuity arm is layered ON TOP of the offline run rather than replacing
|
|
1083
|
+
// it: it is a third variant of one family, so it belongs in one report. Its
|
|
1084
|
+
// findings are `NEVER_BLOCKING`, so adding them cannot change the exit code
|
|
1085
|
+
// the offline arm would have produced on its own.
|
|
1086
|
+
const vacuity =
|
|
1087
|
+
pr === undefined
|
|
1088
|
+
? undefined
|
|
1089
|
+
: evaluateVacuousChecks(
|
|
1090
|
+
declaration,
|
|
1091
|
+
fetchPullRequestChecks(pr, readFlagValue(argv, "--repo")),
|
|
1092
|
+
{ trustRequiredContexts: trust.trusted }
|
|
1093
|
+
);
|
|
1094
|
+
if (vacuity !== undefined) violations.push(...vacuity.violations);
|
|
1095
|
+
|
|
717
1096
|
return {
|
|
718
1097
|
violations,
|
|
719
1098
|
checked: result.checked,
|
|
@@ -721,6 +1100,8 @@ export function runGuard(argv) {
|
|
|
721
1100
|
enforcement: declaration.enforcement ?? "error",
|
|
722
1101
|
trust,
|
|
723
1102
|
recipe: transcriptionRecipe(declaration),
|
|
1103
|
+
pr,
|
|
1104
|
+
evidenceChecked: vacuity?.checked ?? 0,
|
|
724
1105
|
};
|
|
725
1106
|
}
|
|
726
1107
|
|
|
@@ -774,9 +1155,10 @@ function main(argv) {
|
|
|
774
1155
|
* @returns {boolean} True when it blocks
|
|
775
1156
|
*/
|
|
776
1157
|
const blocks = violation =>
|
|
777
|
-
!
|
|
1158
|
+
!NEVER_BLOCKING.includes(violation.kind) &&
|
|
1159
|
+
(!warnOnly || ALWAYS_BLOCKING.includes(violation.kind));
|
|
778
1160
|
const blocking = result.violations.filter(blocks);
|
|
779
|
-
const lines = ["## 🔒
|
|
1161
|
+
const lines = ["## 🔒 Required checks that prove nothing", ""];
|
|
780
1162
|
|
|
781
1163
|
// The refusal comes FIRST and replaces the verdict. Printing "✅ none
|
|
782
1164
|
// silences a required check" from a snapshot nobody transcribed is the one
|
|
@@ -803,7 +1185,12 @@ function main(argv) {
|
|
|
803
1185
|
if (result.violations.length === 0) {
|
|
804
1186
|
if (result.trust.trusted) {
|
|
805
1187
|
lines.push(
|
|
806
|
-
`✅ ${result.checked} \`skip_jobs\` token(s) examined; none silences a ruleset-required status check
|
|
1188
|
+
`✅ ${result.checked} \`skip_jobs\` token(s) examined; none silences a ruleset-required status check.`,
|
|
1189
|
+
...(result.pr === undefined
|
|
1190
|
+
? []
|
|
1191
|
+
: [
|
|
1192
|
+
`✅ ${result.evidenceChecked} evidence-bearing check(s) examined on PR #${result.pr}; each proved it did work.`,
|
|
1193
|
+
])
|
|
807
1194
|
);
|
|
808
1195
|
} else {
|
|
809
1196
|
lines.push(
|
|
@@ -827,6 +1214,16 @@ function main(argv) {
|
|
|
827
1214
|
`This declaration sets \`"enforcement": "warn"\`, so everything above except a proven false green (\`${VIOLATIONS.suppressesRequired}\`) is reported without failing the build. Review each finding, fix or declare it, then delete the \`enforcement\` key so this guard can block.`
|
|
828
1215
|
);
|
|
829
1216
|
}
|
|
1217
|
+
if (
|
|
1218
|
+
result.violations.some(violation =>
|
|
1219
|
+
NEVER_BLOCKING.includes(violation.kind)
|
|
1220
|
+
)
|
|
1221
|
+
) {
|
|
1222
|
+
lines.push(
|
|
1223
|
+
"",
|
|
1224
|
+
`\`${VIOLATIONS.vacuous}\` and \`${VIOLATIONS.unproven}\` are REPORT-ONLY in every enforcement mode — they never fail a build. A required check can go hollow because a vendor hit an org-wide spending cap, and reddening every PR on a billing state would be a worse gate than the one being criticised. What they change is what you may CLAIM: a PR carrying either finding has not been shown to be reviewed, so do not record it as reviewed.`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
830
1227
|
}
|
|
831
1228
|
const report = `${lines.join("\n")}\n`;
|
|
832
1229
|
process.stdout.write(report);
|