agent-roast 0.10.1 → 0.11.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/README.md +37 -12
- package/dist/{chunk-FFKEBW6D.js → chunk-WMHB65AL.js} +196 -8
- package/dist/cli.js +51 -9
- package/dist/index.d.ts +59 -1
- package/dist/index.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ review AI-assisted changes, but it cannot tell who wrote a change, and the measu
|
|
|
28
28
|
show these moves are not specific to AI.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
+
npx agent-roast replay # your last 50 commits, one at a time
|
|
31
32
|
npx agent-roast review --base origin/main # a pull request
|
|
32
33
|
npx agent-roast --staged # what you are about to commit
|
|
33
34
|
```
|
|
@@ -42,7 +43,10 @@ npx agent-roast --staged # what you are about to commit
|
|
|
42
43
|
| `test-skip`, `test-focus`, `trivial-assert`, `swallowed-error`, `type-escape`, `stub-abandonment` | the classic shortcuts, compared before/after so old debt does not block | skip, focus and trivial-assert block |
|
|
43
44
|
|
|
44
45
|
Renames, moved tests, parametrization, assertions moved into a new helper, and formatter
|
|
45
|
-
rewraps (prettier, black) are recognized and not reported.
|
|
46
|
+
rewraps (prettier, black) are recognized and not reported. So is a skip that depends on the
|
|
47
|
+
environment: `test.skip(isMobile, 'reason')` and Playwright's other condition forms,
|
|
48
|
+
`if not has_gpu(): pytest.skip(...)`, and a skip inside a fixture or helper. A skip that names a
|
|
49
|
+
test, stands alone in a test body or depends on `CI` is still reported. Intentional cases can be accepted
|
|
46
50
|
one by one with an expiring, reviewed exception (see below). Policy is read from the base
|
|
47
51
|
branch, so a PR cannot switch the rules off for itself.
|
|
48
52
|
|
|
@@ -52,9 +56,9 @@ Measured, not promised; method, corpus and raw numbers are in
|
|
|
52
56
|
[docs/benchmarks.md](docs/benchmarks.md). **Short version: weakened checks are rare, usually
|
|
53
57
|
deliberate, and not specific to AI. The tool finds them; a person decides.**
|
|
54
58
|
|
|
55
|
-
- **Labeled changes** (`benchmarks/cases.mjs`):
|
|
56
|
-
CI/config files are caught and
|
|
57
|
-
helper extraction, raised thresholds, CI restructuring) stay silent. The cases were written
|
|
59
|
+
- **Labeled changes** (`benchmarks/cases.mjs`): 43 cheating edits across JS/TS, Python, Go and
|
|
60
|
+
CI/config files are caught and 36 harmless look-alikes (reformat, rename, move, parametrize,
|
|
61
|
+
helper extraction, raised thresholds, CI restructuring, skips that depend on the environment) stay silent. The cases were written
|
|
58
62
|
together with the detector, so they guard against regressions; they do not prove accuracy.
|
|
59
63
|
Three known gaps are listed in the file and still open: mocking the module under test,
|
|
60
64
|
hardcoding the tested value, and vacuous matchers such as `expect.anything()`.
|
|
@@ -99,8 +103,8 @@ It is a **review aid**: it cannot prove who wrote a change or that code is corre
|
|
|
99
103
|
reads text, not program semantics. A green result is not a quality certificate.
|
|
100
104
|
|
|
101
105
|
**Want to see what it says about your repository?** [docs/pilot.md](docs/pilot.md) has a
|
|
102
|
-
|
|
103
|
-
Counts are enough; no code is needed.
|
|
106
|
+
two-minute replay of your last commits, an advisory workflow that never fails a pull request, and a
|
|
107
|
+
feedback form. Counts are enough; no code is needed.
|
|
104
108
|
|
|
105
109
|
### Quick start
|
|
106
110
|
|
|
@@ -122,6 +126,22 @@ A finding includes its file and line, the evidence, and a concrete next step.
|
|
|
122
126
|
important findings (rules that block first, then the before/after rules, then the noisier
|
|
123
127
|
line patterns).
|
|
124
128
|
|
|
129
|
+
### Replay recent history
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Review each of the last 50 commits on its own, as a pull-request check would have seen it
|
|
133
|
+
npx agent-roast replay
|
|
134
|
+
npx agent-roast replay --last 200 --json # more history, every finding
|
|
135
|
+
npx agent-roast replay --all-rules # also list line patterns such as `as any`
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`replay` answers "what would this have said about our last 50 changes?" before you put it in CI.
|
|
139
|
+
It follows the first-parent history, so a squash, a merge and a direct commit each count as one
|
|
140
|
+
change, reviews each against its parent with the policy at that parent, and lists the commits
|
|
141
|
+
that would have been blocked or drew a warning about a weakened check. It never fails because of a
|
|
142
|
+
finding, and a commit too large to read is reported, not fatal. Line patterns (`type-escape`,
|
|
143
|
+
`swallowed-error`, `stub-abandonment`) are counted and listed only with `--all-rules`.
|
|
144
|
+
|
|
125
145
|
### Context review for pull requests
|
|
126
146
|
|
|
127
147
|
```bash
|
|
@@ -153,7 +173,7 @@ severity but does not enable rules disabled by base policy.
|
|
|
153
173
|
|
|
154
174
|
```bash
|
|
155
175
|
# Advisory pilot: inspect findings before deciding what should block the team
|
|
156
|
-
agent-roast review --base origin/main --
|
|
176
|
+
agent-roast review --base origin/main --advisory
|
|
157
177
|
# Gate the shortcuts the team agrees matter
|
|
158
178
|
agent-roast review --base origin/main --fail-on test-skip,trivial-assert,swallowed-error
|
|
159
179
|
```
|
|
@@ -180,12 +200,12 @@ regressions) may appear in the report/summary rather than as an inline PR annota
|
|
|
180
200
|
Use a separate SARIF category for introduced-findings review; it does not represent
|
|
181
201
|
a full scan of all outstanding alerts.
|
|
182
202
|
|
|
183
|
-
Install 0.
|
|
203
|
+
Install 0.11.0: `npx agent-roast@0.11.0 review --base origin/main`. For local development,
|
|
184
204
|
run `npm run build` and use `node dist/cli.js`.
|
|
185
205
|
|
|
186
206
|
### Precise exceptions (since 0.8.0)
|
|
187
207
|
|
|
188
|
-
Use `npx agent-roast@0.
|
|
208
|
+
Use `npx agent-roast@0.11.0 check --format json`, or build this checkout with
|
|
189
209
|
`npm run build` and run `node dist/cli.js`.
|
|
190
210
|
|
|
191
211
|
Some skipped integrations or abstract stubs are intentional. In 0.8.0 you can accept
|
|
@@ -421,7 +441,9 @@ jobs:
|
|
|
421
441
|
|
|
422
442
|
To pilot the Action without blocking on findings, set `advisory: true`. It passes
|
|
423
443
|
an explicit empty fail-on list; invalid configuration, Git errors and incomplete
|
|
424
|
-
scans still fail. The `v1` Action installs 0.
|
|
444
|
+
scans still fail. The `v1` Action installs 0.11.0. The CLI equivalent is `--advisory`
|
|
445
|
+
(`--fail-on ''` does the same in POSIX shells, but cmd.exe and Windows PowerShell 5.1 do not pass
|
|
446
|
+
an empty argument through).
|
|
425
447
|
|
|
426
448
|
> **Security Note:** `agent-roast` runs in an isolated runner directory (`$RUNNER_TEMP`) with `--ignore-scripts` to neutralize malicious `.npmrc` files in untrusted pull requests. `pull_request_target` is not supported.
|
|
427
449
|
|
|
@@ -474,7 +496,10 @@ the added diff in legacy audit modes are not fully parsed. Custom test API alias
|
|
|
474
496
|
computed property calls and type/import resolution are not handled. Panic loops are an observational history heuristic.
|
|
475
497
|
Test registration options such as Node's `test('name', { skip: true }, fn)` and
|
|
476
498
|
`{ only: true }` are currently not inspected; use explicit `.skip`/`.only` APIs and
|
|
477
|
-
the runner's CI safeguards.
|
|
499
|
+
the runner's CI safeguards. A skip that depends on the environment (`test.skip(isMobile, 'reason')`,
|
|
500
|
+
`pytest.skip` under an `if`, or in a fixture) is not reported, because such skips are overwhelmingly
|
|
501
|
+
platform or service gates; one that hides a failing test behind a condition is therefore missed unless
|
|
502
|
+
the condition mentions `CI`. Regexp masking is contextual, not a full JavaScript
|
|
478
503
|
parser. Formatting across contiguous lines is supported for casts, empty catches
|
|
479
504
|
and placeholder errors, but legacy added-line audits cannot recover missing hunks.
|
|
480
505
|
The discipline score summarizes added history; it does not certify current code quality.
|
|
@@ -509,7 +534,7 @@ node scripts/verify-user-workflows.mjs dist/cli.js
|
|
|
509
534
|
against current code. Regression tests use disposable Git repositories. The programmatic
|
|
510
535
|
`auditPullRequest({ staged: true })` API retains the legacy added-lines contract;
|
|
511
536
|
use `reviewStagedChanges()` for the new context comparison and `ReviewResult`.
|
|
512
|
-
The composite Action installs exactly 0.
|
|
537
|
+
The composite Action installs exactly 0.11.0. For local development, use
|
|
513
538
|
`node dist/cli.js --staged` after building.
|
|
514
539
|
|
|
515
540
|
### Author & Support
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path8 from "path";
|
|
5
5
|
|
|
6
6
|
// src/git.ts
|
|
7
7
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -1161,11 +1161,57 @@ function matchIgnorePath(filePath, ignorePatterns) {
|
|
|
1161
1161
|
// src/metrics.ts
|
|
1162
1162
|
var PYTHON_TEST_SKIP_PATTERNS = [
|
|
1163
1163
|
{ regex: /@pytest\s*\.\s*mark\s*\.\s*skip\b/, name: "skipped pytest (@pytest.mark.skip)" },
|
|
1164
|
-
{ regex: /(?<![\w.])pytest\s*\.\s*skip\s*\(/, name: "imperative pytest skip (pytest.skip)" },
|
|
1165
|
-
{ regex: /@unittest\s*\.\s*skip\s*\(/, name: "skipped unittest (@unittest.skip)" },
|
|
1166
|
-
{ regex: /\bself\s*\.\s*skipTest\s*\(/, name: "imperative unittest skip (self.skipTest)" }
|
|
1164
|
+
{ regex: /(?<![\w.])pytest\s*\.\s*skip\s*\(/, name: "imperative pytest skip (pytest.skip)", imperative: true },
|
|
1165
|
+
{ regex: /@unittest\s*\.\s*skip\s*\(/, name: "skipped unittest (@unittest.skip)", imperative: false },
|
|
1166
|
+
{ regex: /\bself\s*\.\s*skipTest\s*\(/, name: "imperative unittest skip (self.skipTest)", imperative: true }
|
|
1167
1167
|
];
|
|
1168
|
+
function isGuardedSkip(lines) {
|
|
1169
|
+
const indentOf = (text) => text.length - text.trimStart().length;
|
|
1170
|
+
const mentionsCi = (raw) => /\bCI\b|\bGITHUB_ACTIONS\b/.test(raw);
|
|
1171
|
+
const own = lines[lines.length - 1];
|
|
1172
|
+
if (/^\s*(?:if|elif|else)\b.*:\s*\S/.test(own.code)) return !mentionsCi(own.raw);
|
|
1173
|
+
let limit = indentOf(own.code);
|
|
1174
|
+
for (let i = lines.length - 2; i >= 0 && lines.length - 1 - i <= 200; i--) {
|
|
1175
|
+
if (lines[i].number !== lines[i + 1].number - 1) return false;
|
|
1176
|
+
const { code, raw } = lines[i];
|
|
1177
|
+
if (!code.trim() || indentOf(code) >= limit) continue;
|
|
1178
|
+
const keyword = /^\s*(async\s+def|def|class|if|elif|else|try|except|finally|with|for|while)\b/.exec(code)?.[1];
|
|
1179
|
+
if (keyword === "def" || keyword === "async def") return !/^\s*(?:async\s+)?def\s+test/.test(code);
|
|
1180
|
+
if (keyword === "class") return false;
|
|
1181
|
+
if (keyword === "if" || keyword === "elif" || keyword === "else" || keyword === "try" || keyword === "except" || keyword === "finally") return !mentionsCi(raw);
|
|
1182
|
+
limit = indentOf(code) + (keyword ? 0 : 1);
|
|
1183
|
+
}
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1168
1186
|
var JS_TEST_API = /(?<![\w$.])(?:(?:it|test|describe|suite)(?:\s*\.\s*(?:concurrent|serial|parallel|describe|failing|sequential))*\s*\.\s*(skip|todo|only)|((?:xit|xtest|xdescribe|fit|fdescribe)))\b((?:\s*\.\s*(?:each|failing|concurrent|serial|parallel|sequential))*)\s*(?=\(|\u0001)/g;
|
|
1187
|
+
function isConditionalSkip(source, raw, open2) {
|
|
1188
|
+
if (source[open2] !== "(") return false;
|
|
1189
|
+
const args = [];
|
|
1190
|
+
let depth = 0;
|
|
1191
|
+
let start = open2 + 1;
|
|
1192
|
+
for (let i = start; i < source.length && i - open2 <= 2e3; i++) {
|
|
1193
|
+
const char = source[i];
|
|
1194
|
+
if (char === "\0") return false;
|
|
1195
|
+
if ("([{".includes(char)) depth++;
|
|
1196
|
+
else if (")]}".includes(char)) {
|
|
1197
|
+
if (depth) {
|
|
1198
|
+
depth--;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (char !== ")") return false;
|
|
1202
|
+
args.push(raw.slice(start, i).trim());
|
|
1203
|
+
while (args.length && !args.at(-1)) args.pop();
|
|
1204
|
+
const isString = (arg) => /^(['"`])[\s\S]*\1$/.test(arg);
|
|
1205
|
+
if (!args.length || isString(args[0]) || args[0] === "true") return false;
|
|
1206
|
+
if (args.length > 1 && !isString(args.at(-1))) return false;
|
|
1207
|
+
return !/\bCI\b|\bisCI\b|GITHUB_ACTIONS/.test(args[0]);
|
|
1208
|
+
} else if (char === "," && !depth) {
|
|
1209
|
+
args.push(raw.slice(start, i).trim());
|
|
1210
|
+
start = i + 1;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1169
1215
|
var JS_COMMENT_DIRECTIVE_PATTERNS = [
|
|
1170
1216
|
{ regex: /^\s*\/\/\s*@ts-ignore\b/, name: "compiler ignore (@ts-ignore)" },
|
|
1171
1217
|
{ regex: /^\s*\/\/\s*@ts-expect-error\b/, name: "compiler suppression (@ts-expect-error)" },
|
|
@@ -1271,6 +1317,7 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
1271
1317
|
if (python && !isComment) {
|
|
1272
1318
|
for (const rule of PYTHON_TEST_SKIP_PATTERNS) {
|
|
1273
1319
|
if (rule.regex.test(cleanedLine)) {
|
|
1320
|
+
if (rule.imperative && isGuardedSkip(maskedLines)) break;
|
|
1274
1321
|
infractions.push({
|
|
1275
1322
|
type: "test-skip",
|
|
1276
1323
|
file: file.path,
|
|
@@ -1376,6 +1423,7 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
1376
1423
|
if (match[2] && !isTestFile(file.path)) continue;
|
|
1377
1424
|
if (/\bfunction$/.test(source.slice(Math.max(0, previous - 8), previous + 1))) continue;
|
|
1378
1425
|
if (source[match.index + match[0].length] === "" && !/\.\s*each\s*$/.test(match[3])) continue;
|
|
1426
|
+
if (match[1] === "skip" && !match[3] && isConditionalSkip(source, raw, match.index + match[0].length)) continue;
|
|
1379
1427
|
const focus = match[1] === "only" || match[2] === "fit" || match[2] === "fdescribe";
|
|
1380
1428
|
infractions.push({
|
|
1381
1429
|
type: focus ? "test-focus" : "test-skip",
|
|
@@ -1887,6 +1935,9 @@ var RULE_PRIORITY = {
|
|
|
1887
1935
|
"expectation-rewritten": 1,
|
|
1888
1936
|
"test-removed": 1
|
|
1889
1937
|
};
|
|
1938
|
+
function isCheckRule(type) {
|
|
1939
|
+
return RULE_PRIORITY[type] !== void 0;
|
|
1940
|
+
}
|
|
1890
1941
|
function findingRank(type, failedRules) {
|
|
1891
1942
|
return failedRules.includes(type) ? -1 : RULE_PRIORITY[type] ?? 2;
|
|
1892
1943
|
}
|
|
@@ -2530,6 +2581,20 @@ async function requireLocalObjects2(cwd, signal) {
|
|
|
2530
2581
|
if (!(error instanceof GitError && error.code === 1)) throw error;
|
|
2531
2582
|
}
|
|
2532
2583
|
}
|
|
2584
|
+
async function unresolvedRefHint(cwd, ref, signal) {
|
|
2585
|
+
try {
|
|
2586
|
+
const count = async (...args) => Number((await readGit(cwd, ["rev-list", "--count", ...args, "HEAD"], signal, 64)).toString().trim());
|
|
2587
|
+
if ((await readGit(cwd, ["rev-parse", "--is-shallow-repository"], signal, 64)).toString().trim() === "true") {
|
|
2588
|
+
return `This clone is shallow (commits available: ${await count()}), so the ref may lie outside it. Run 'git fetch --unshallow' (in GitHub Actions: actions/checkout with fetch-depth: 0) or check the name.`;
|
|
2589
|
+
}
|
|
2590
|
+
const back = /^HEAD~(\d+)$/.exec(ref);
|
|
2591
|
+
const depth = back ? await count("--first-parent") : 0;
|
|
2592
|
+
if (back && Number(back[1]) >= depth) return `HEAD has only ${depth} commit${depth === 1 ? "" : "s"} on its main line${depth > 1 ? `; try HEAD~${depth - 1}` : ""}.`;
|
|
2593
|
+
} catch {
|
|
2594
|
+
assertNotAborted(signal);
|
|
2595
|
+
}
|
|
2596
|
+
return "Check that it names a commit, branch or tag of this repository.";
|
|
2597
|
+
}
|
|
2533
2598
|
async function reviewRefs(cwd, base, head, signal) {
|
|
2534
2599
|
const resolve2 = async (ref) => {
|
|
2535
2600
|
if (!ref.trim()) throw new ConfigError("Review refs must not be empty.");
|
|
@@ -2537,7 +2602,7 @@ async function reviewRefs(cwd, base, head, signal) {
|
|
|
2537
2602
|
return (await readGit(cwd, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], signal, 1024)).toString().trim();
|
|
2538
2603
|
} catch (error) {
|
|
2539
2604
|
assertNotAborted(signal);
|
|
2540
|
-
throw new ConfigError(`Cannot resolve review ref '${ref}'.
|
|
2605
|
+
throw new ConfigError(`Cannot resolve review ref '${ref}'. ${await unresolvedRefHint(cwd, ref, signal)} ${error instanceof Error ? error.message : String(error)}`);
|
|
2541
2606
|
}
|
|
2542
2607
|
};
|
|
2543
2608
|
await requireLocalObjects2(cwd, signal);
|
|
@@ -3374,6 +3439,125 @@ async function compareSourceChanges(options) {
|
|
|
3374
3439
|
return result;
|
|
3375
3440
|
}
|
|
3376
3441
|
|
|
3442
|
+
// src/replay.ts
|
|
3443
|
+
import path7 from "path";
|
|
3444
|
+
var MAX_COMMITS = 1e3;
|
|
3445
|
+
var CONCURRENCY = 4;
|
|
3446
|
+
var LISTED_COMMITS = 15;
|
|
3447
|
+
var LISTED_PER_RULE = 2;
|
|
3448
|
+
var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3449
|
+
function clipEvidence(text, limit = 200) {
|
|
3450
|
+
if (text.length <= limit) return text;
|
|
3451
|
+
const arrow = text.indexOf(" \u2192 ");
|
|
3452
|
+
const clip = (part, size) => part.length > size ? `${part.slice(0, size - 1)}\u2026` : part;
|
|
3453
|
+
if (arrow < 0) return clip(text, limit);
|
|
3454
|
+
const side = Math.floor((limit - 3) / 2);
|
|
3455
|
+
return `${clip(text.slice(0, arrow), side)} \u2192 ${clip(text.slice(arrow + 3), side)}`;
|
|
3456
|
+
}
|
|
3457
|
+
async function replayHistory(options = {}) {
|
|
3458
|
+
assertNotAborted(options.signal);
|
|
3459
|
+
const cwd = path7.resolve(options.cwd || process.cwd());
|
|
3460
|
+
const last = options.last ?? 50;
|
|
3461
|
+
if (!Number.isInteger(last) || last < 1 || last > MAX_COMMITS) throw new ConfigError(`Replay needs a whole number of commits between 1 and ${MAX_COMMITS}.`);
|
|
3462
|
+
const format = options.format ?? "terminal";
|
|
3463
|
+
if (format !== "terminal" && format !== "json") throw new ConfigError("Replay supports the terminal and json formats.");
|
|
3464
|
+
let log;
|
|
3465
|
+
try {
|
|
3466
|
+
log = (await readGit(cwd, ["log", "--first-parent", "-n", String(last), "--format=%H%x1f%P%x1f%cI%x1f%s"], options.signal)).toString("utf8");
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
assertNotAborted(options.signal);
|
|
3469
|
+
throw new ConfigError(`Cannot read the commit history. ${error instanceof Error ? error.message : String(error)}`);
|
|
3470
|
+
}
|
|
3471
|
+
const entries = log.split("\n").filter(Boolean).map((line) => {
|
|
3472
|
+
const [sha, parents, date, ...subject] = line.split("");
|
|
3473
|
+
return { sha, base: parents.split(" ")[0], date, subject: subject.join(" ") };
|
|
3474
|
+
});
|
|
3475
|
+
if (!entries.length) throw new ConfigError("The repository has no commits to replay.");
|
|
3476
|
+
const reviewable = entries.filter((entry) => entry.base);
|
|
3477
|
+
const commits = new Array(reviewable.length);
|
|
3478
|
+
let next = 0;
|
|
3479
|
+
let done = 0;
|
|
3480
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, reviewable.length) }, async () => {
|
|
3481
|
+
while (next < reviewable.length) {
|
|
3482
|
+
const index = next++;
|
|
3483
|
+
commits[index] = await replayCommit(cwd, reviewable[index], options.signal);
|
|
3484
|
+
options.onProgress?.(++done, reviewable.length);
|
|
3485
|
+
}
|
|
3486
|
+
}));
|
|
3487
|
+
if (commits.length && commits.every((commit) => commit.status === "failed")) throw new ConfigError(`No commit could be reviewed. ${commits[0].error}`);
|
|
3488
|
+
const summary = { commits: commits.length, blocked: 0, warned: 0, noted: 0, clean: 0, untouched: 0, incomplete: 0, failed: 0, findingsByRule: {} };
|
|
3489
|
+
for (const commit of commits) {
|
|
3490
|
+
summary[commit.status]++;
|
|
3491
|
+
for (const finding2 of commit.findings) summary.findingsByRule[finding2.type] = (summary.findingsByRule[finding2.type] ?? 0) + 1;
|
|
3492
|
+
}
|
|
3493
|
+
const shallow = (await readGit(cwd, ["rev-parse", "--is-shallow-repository"], options.signal, 64).catch(() => Buffer.from(""))).toString().trim() === "true";
|
|
3494
|
+
const result = { schemaVersion: "1.0.0", mode: "replay", head: entries[0].sha, requested: last, commitsWithoutParent: entries.length - reviewable.length, shallow, summary, commits, exitCode: 0, output: "" };
|
|
3495
|
+
result.output = format === "json" ? JSON.stringify({ ...result, output: void 0 }, null, 2) : formatReplayTerminal(result, options.allRules);
|
|
3496
|
+
return result;
|
|
3497
|
+
}
|
|
3498
|
+
async function replayCommit(cwd, entry, signal) {
|
|
3499
|
+
const identity = { sha: entry.sha, date: entry.date, subject: entry.subject };
|
|
3500
|
+
try {
|
|
3501
|
+
const review = await reviewChanges({ cwd, base: entry.base, head: entry.sha, format: "json", signal });
|
|
3502
|
+
const status = review.failedRules.length ? "blocked" : !review.complete ? "incomplete" : review.infractions.some((finding2) => isCheckRule(finding2.type)) ? "warned" : review.infractions.length ? "noted" : review.filesInspected ? "clean" : "untouched";
|
|
3503
|
+
return { ...identity, status, failedRules: review.failedRules, findings: mostImportantFirst(review.infractions, review.failedRules), filesInspected: review.filesInspected, skipped: review.skippedFiles };
|
|
3504
|
+
} catch (error) {
|
|
3505
|
+
assertNotAborted(signal);
|
|
3506
|
+
return { ...identity, status: "failed", failedRules: [], findings: [], filesInspected: 0, skipped: [], error: error instanceof Error ? error.message : String(error) };
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
function formatReplayTerminal(result, allRules = false) {
|
|
3510
|
+
const { summary } = result;
|
|
3511
|
+
const lines = [
|
|
3512
|
+
"",
|
|
3513
|
+
`agent-roast \u2014 replay of the last ${plural(summary.commits, "commit")} (up to ${result.head.slice(0, 7)})`,
|
|
3514
|
+
" Each commit is reviewed on its own against its parent, as a pull-request check would have seen it."
|
|
3515
|
+
];
|
|
3516
|
+
const shallowHint = result.shallow ? [" This clone is shallow, so only the commits it holds were replayed. Run 'git fetch --unshallow' to replay more."] : [];
|
|
3517
|
+
if (!summary.commits) return [...lines, " There is no earlier commit to compare with, so nothing was reviewed.", ...shallowHint, ""].join("\n");
|
|
3518
|
+
lines.push(...shallowHint);
|
|
3519
|
+
lines.push(
|
|
3520
|
+
` ${plural(summary.commits - summary.untouched - summary.failed, "commit")} changed source, tests or CI config \xB7 ${summary.untouched} did not`,
|
|
3521
|
+
` Would have blocked: ${plural(summary.blocked, "commit")} \xB7 warnings on ${summary.warned} \xB7 line patterns only on ${summary.noted} \xB7 nothing to report on ${summary.clean}`
|
|
3522
|
+
);
|
|
3523
|
+
if (summary.incomplete + summary.failed) lines.push(` Not reviewed in full: ${plural(summary.incomplete + summary.failed, "commit")}`);
|
|
3524
|
+
const listed = result.commits.filter((commit) => commit.status === "blocked" || commit.status === "warned" || allRules && commit.status === "noted");
|
|
3525
|
+
const ordered = [...listed.filter((commit) => commit.status === "blocked"), ...listed.filter((commit) => commit.status !== "blocked")];
|
|
3526
|
+
for (const commit of ordered.slice(0, LISTED_COMMITS)) {
|
|
3527
|
+
lines.push("", ` ${commit.status === "blocked" ? "BLOCK" : "WARN "} ${commit.sha.slice(0, 7)} ${commit.date.slice(0, 10)} ${terminalText(commit.subject).slice(0, 80)}`);
|
|
3528
|
+
const shown = commit.findings.filter((finding2) => allRules || isCheckRule(finding2.type));
|
|
3529
|
+
for (const rule of new Set(shown.map((finding2) => finding2.type))) {
|
|
3530
|
+
const group = shown.filter((finding2) => finding2.type === rule);
|
|
3531
|
+
for (const finding2 of group.slice(0, LISTED_PER_RULE)) {
|
|
3532
|
+
lines.push(
|
|
3533
|
+
` ${commit.failedRules.includes(rule) ? "FAIL" : "WARN"} [${rule}] ${terminalText(finding2.file || "unknown")}:${finding2.lineNumber}`,
|
|
3534
|
+
` ${clipEvidence(terminalText(finding2.lineSnippet || finding2.reason))}`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
if (group.length > LISTED_PER_RULE) lines.push(` \u2026 and ${group.length - LISTED_PER_RULE} more [${rule}] in this commit`);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
if (ordered.length > LISTED_COMMITS) lines.push("", ` \u2026 ${plural(ordered.length - LISTED_COMMITS, "more commit")} with findings; use --json for all of them.`);
|
|
3541
|
+
if (!ordered.length) lines.push("", " Nothing to report on these commits.");
|
|
3542
|
+
if (!allRules) {
|
|
3543
|
+
const hidden = result.commits.flatMap((commit) => commit.findings.filter((finding2) => !isCheckRule(finding2.type)));
|
|
3544
|
+
if (hidden.length) {
|
|
3545
|
+
const byRule = Object.entries(hidden.reduce((counts, finding2) => ({ ...counts, [finding2.type]: (counts[finding2.type] ?? 0) + 1 }), {})).sort((a, b) => b[1] - a[1]);
|
|
3546
|
+
lines.push("", ` Line patterns not listed: ${plural(hidden.length, "finding")} (${byRule.slice(0, 3).map(([rule, count]) => `${rule} ${count}`).join(", ")}). Add --all-rules to list them.`);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
for (const commit of result.commits.filter((commit2) => commit2.status === "incomplete" || commit2.status === "failed").slice(0, 5)) {
|
|
3550
|
+
lines.push(` Not reviewed in full: ${commit.sha.slice(0, 7)} ${terminalText(commit.subject).slice(0, 60)} \u2014 ${terminalText(commit.error ?? `${plural(commit.skipped.length, "changed file")} could not be compared`)}`);
|
|
3551
|
+
}
|
|
3552
|
+
lines.push(
|
|
3553
|
+
"",
|
|
3554
|
+
" Findings are review candidates, not verdicts: `agent-roast explain <rule>` says what a rule means.",
|
|
3555
|
+
" To run this on every pull request: `agent-roast init --ci`.",
|
|
3556
|
+
""
|
|
3557
|
+
);
|
|
3558
|
+
return lines.join("\n");
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3377
3561
|
// src/index.ts
|
|
3378
3562
|
var InvalidDetectorError = class extends Error {
|
|
3379
3563
|
constructor(message) {
|
|
@@ -3382,9 +3566,9 @@ var InvalidDetectorError = class extends Error {
|
|
|
3382
3566
|
}
|
|
3383
3567
|
};
|
|
3384
3568
|
async function roastRepository(options = {}) {
|
|
3385
|
-
const cwd =
|
|
3569
|
+
const cwd = path8.resolve(options.cwd || process.cwd());
|
|
3386
3570
|
const since = options.since || "90 days ago";
|
|
3387
|
-
const repoName =
|
|
3571
|
+
const repoName = path8.basename(cwd);
|
|
3388
3572
|
const collector = new MetricsCollector({ all: options.all, ignorePaths: options.ignorePaths, rules: options.rules, panicLoopWindowMinutes: options.panicLoopWindowMinutes });
|
|
3389
3573
|
const gitStream = streamGitLog(cwd, since);
|
|
3390
3574
|
await parseGitLogStream(gitStream, (commit, stats) => {
|
|
@@ -3399,7 +3583,7 @@ async function roastRepository(options = {}) {
|
|
|
3399
3583
|
return { summary, output };
|
|
3400
3584
|
}
|
|
3401
3585
|
async function auditPullRequest(options) {
|
|
3402
|
-
const cwd =
|
|
3586
|
+
const cwd = path8.resolve(options.cwd || process.cwd());
|
|
3403
3587
|
const format = options.format || "terminal";
|
|
3404
3588
|
const failOn = (options.failOn || ["test-skip"]).map((s) => s.trim()).filter(Boolean);
|
|
3405
3589
|
for (const det of failOn) {
|
|
@@ -3522,6 +3706,8 @@ export {
|
|
|
3522
3706
|
generateShareUrl,
|
|
3523
3707
|
formatTerminalCard,
|
|
3524
3708
|
formatPrTerminalCard,
|
|
3709
|
+
isCheckRule,
|
|
3710
|
+
mostImportantFirst,
|
|
3525
3711
|
formatGithubAnnotations,
|
|
3526
3712
|
formatGithubStepSummary,
|
|
3527
3713
|
formatPrMarkdownComment,
|
|
@@ -3543,6 +3729,8 @@ export {
|
|
|
3543
3729
|
compareFindings,
|
|
3544
3730
|
reviewChanges,
|
|
3545
3731
|
reviewStagedChanges,
|
|
3732
|
+
replayHistory,
|
|
3733
|
+
formatReplayTerminal,
|
|
3546
3734
|
InvalidDetectorError,
|
|
3547
3735
|
roastRepository,
|
|
3548
3736
|
auditPullRequest
|
package/dist/cli.js
CHANGED
|
@@ -17,12 +17,13 @@ import {
|
|
|
17
17
|
initProject,
|
|
18
18
|
loadConfig,
|
|
19
19
|
readGit,
|
|
20
|
+
replayHistory,
|
|
20
21
|
reviewChanges,
|
|
21
22
|
reviewStagedChanges,
|
|
22
23
|
roastRepository,
|
|
23
24
|
terminalText,
|
|
24
25
|
writeBaseline
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-WMHB65AL.js";
|
|
26
27
|
|
|
27
28
|
// src/cli.ts
|
|
28
29
|
import { Command } from "commander";
|
|
@@ -32,6 +33,13 @@ import * as path from "path";
|
|
|
32
33
|
var require2 = createRequire(import.meta.url);
|
|
33
34
|
var pkg = require2("../package.json");
|
|
34
35
|
var errorMessage = (error) => terminalText(error instanceof Error ? error.message : String(error));
|
|
36
|
+
function failOnOption(options) {
|
|
37
|
+
if (options.advisory) {
|
|
38
|
+
if (options.failOn !== void 0) throw new ConfigError("--advisory cannot be combined with --fail-on.");
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return options.failOn?.split(",").map((rule) => rule.trim()).filter(Boolean);
|
|
42
|
+
}
|
|
35
43
|
async function runContextReview(repoPath, options, staged) {
|
|
36
44
|
const controller = new AbortController();
|
|
37
45
|
const abort = () => controller.abort();
|
|
@@ -40,7 +48,7 @@ async function runContextReview(repoPath, options, staged) {
|
|
|
40
48
|
try {
|
|
41
49
|
const common = {
|
|
42
50
|
cwd: repoPath,
|
|
43
|
-
failOn: options
|
|
51
|
+
failOn: failOnOption(options),
|
|
44
52
|
ignoreExceptions: options.ignoreExceptions,
|
|
45
53
|
format: options.json ? "json" : options.format,
|
|
46
54
|
signal: controller.signal
|
|
@@ -71,7 +79,7 @@ removed-count=${result.removedFindings.length}
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
var program = new Command();
|
|
74
|
-
program.name("agent-roast").description("Check current code, review changes and audit Git history for coding shortcuts").version(pkg.version || "0.4.0").argument("[path]", "Path to git repository (default: current directory)", ".").option("-a, --all", "Audit ALL commits regardless of AI attribution tags", false).option("-s, --since <time>", "Time window for git analysis", "90 days ago").option("-v, --verbose", "Show commit SHAs and line snippets for all infractions", false).option("--json", "Output results as raw JSON", false).option("--staged", "Compare full HEAD/index source and gate new staged findings (including the first commit)", false).option("--base <ref>", "Base git ref/branch for PR mode audit (e.g. origin/main)").option("--head <ref>", "Head git ref/SHA for PR mode audit (default: HEAD)", "HEAD").option("--ignore-exceptions", "Report findings even when accepted by reviewed, unexpired exceptions", false).option("--fail-on <rules>", "Comma-separated detectors that trigger CI failure").option("--fail-under <score>", "Fail CI if agent score is below threshold (0-100)").option("--format <format>", "Output format: terminal, json, github, markdown (check/review also support sarif)", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
|
|
82
|
+
program.name("agent-roast").description("Check current code, review changes and audit Git history for coding shortcuts").version(pkg.version || "0.4.0").argument("[path]", "Path to git repository (default: current directory)", ".").option("-a, --all", "Audit ALL commits regardless of AI attribution tags", false).option("-s, --since <time>", "Time window for git analysis", "90 days ago").option("-v, --verbose", "Show commit SHAs and line snippets for all infractions", false).option("--json", "Output results as raw JSON", false).option("--staged", "Compare full HEAD/index source and gate new staged findings (including the first commit)", false).option("--base <ref>", "Base git ref/branch for PR mode audit (e.g. origin/main)").option("--head <ref>", "Head git ref/SHA for PR mode audit (default: HEAD)", "HEAD").option("--ignore-exceptions", "Report findings even when accepted by reviewed, unexpired exceptions", false).option("--fail-on <rules>", "Comma-separated detectors that trigger CI failure").option("--advisory", "Report findings without failing the run, the same as an empty --fail-on (setup errors and incomplete scans still fail)", false).option("--fail-under <score>", "Fail CI if agent score is below threshold (0-100)").option("--format <format>", "Output format: terminal, json, github, markdown (check/review also support sarif)", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
|
|
75
83
|
try {
|
|
76
84
|
if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
|
|
77
85
|
console.warn(
|
|
@@ -93,8 +101,8 @@ program.name("agent-roast").description("Check current code, review changes and
|
|
|
93
101
|
if (options.failUnder !== void 0 && (options.failUnder.trim() === "" || !Number.isFinite(Number(options.failUnder)) || Number(options.failUnder) < 0 || Number(options.failUnder) > 100)) {
|
|
94
102
|
throw new ConfigError("--fail-under must be a number between 0 and 100.");
|
|
95
103
|
}
|
|
96
|
-
if (!options.base && !options.staged && options.failOn !== void 0) {
|
|
97
|
-
throw new ConfigError("--fail-on requires --base or --staged
|
|
104
|
+
if (!options.base && !options.staged && (options.failOn !== void 0 || options.advisory)) {
|
|
105
|
+
throw new ConfigError(`${options.advisory ? "--advisory" : "--fail-on"} requires --base or --staged.`);
|
|
98
106
|
}
|
|
99
107
|
if (!options.base && !options.staged && !options.json && !["terminal", "json"].includes(options.format)) {
|
|
100
108
|
throw new ConfigError("Repository audits support terminal or json format; use --base or --staged for github/markdown.");
|
|
@@ -102,7 +110,7 @@ program.name("agent-roast").description("Check current code, review changes and
|
|
|
102
110
|
const configRoot = gitOutputPath((await readGit(path.resolve(repoPath), ["rev-parse", "--show-toplevel"])).toString("utf8"));
|
|
103
111
|
const config = loadConfig(configRoot);
|
|
104
112
|
if (options.base || options.staged) {
|
|
105
|
-
const failOnRules = options
|
|
113
|
+
const failOnRules = failOnOption(options) ?? config.failOn;
|
|
106
114
|
const chosenFormat = options.json ? "json" : options.format || "terminal";
|
|
107
115
|
const prResult = await auditPullRequest({
|
|
108
116
|
cwd: repoPath,
|
|
@@ -176,7 +184,7 @@ has-supported-changes=${prResult.hasSupportedChanges}
|
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
186
|
});
|
|
179
|
-
program.command("review").description("Compare full changed source at merge-base and head; gate only new findings").argument("[path]", "Path to the Git repository", ".").addHelpText("after", "\nRequired: --base <trusted-ref> Optional: --head <ref> (default HEAD), or use --staged for HEAD/index\nOutput: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules
|
|
187
|
+
program.command("review").description("Compare full changed source at merge-base and head; gate only new findings").argument("[path]", "Path to the Git repository", ".").addHelpText("after", "\nRequired: --base <trusted-ref> Optional: --head <ref> (default HEAD), or use --staged for HEAD/index\nOutput: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>, --advisory\nExamples:\n agent-roast review --base origin/main --format markdown\n agent-roast review --base HEAD~50 --advisory").action(async (repoPath) => {
|
|
180
188
|
try {
|
|
181
189
|
const options = program.opts();
|
|
182
190
|
if (options.staged ? options.base || program.getOptionValueSource("head") === "cli" : !options.base) {
|
|
@@ -191,7 +199,41 @@ program.command("review").description("Compare full changed source at merge-base
|
|
|
191
199
|
process.exitCode = 2;
|
|
192
200
|
}
|
|
193
201
|
});
|
|
194
|
-
program.command("
|
|
202
|
+
program.command("replay").description("Review each of the newest commits on its own, to see what a pull-request check would have said").argument("[path]", "Path to the Git repository", ".").option("--last <n>", "How many of the newest commits to review (default 50, at most 1000)").option("--all-rules", "List line-pattern warnings (type-escape, swallowed-error, stub-abandonment) as well", false).addHelpText("after", "\nOutput: --format terminal|json, --json\nExamples:\n agent-roast replay\n agent-roast replay --last 200 --json").action(async (repoPath, opts) => {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const abort = () => controller.abort();
|
|
205
|
+
process.once("SIGINT", abort);
|
|
206
|
+
process.once("SIGTERM", abort);
|
|
207
|
+
try {
|
|
208
|
+
const global = program.opts();
|
|
209
|
+
if (global.base || global.staged || global.failOn !== void 0 || global.advisory || global.failUnder !== void 0 || global.all || global.ignoreExceptions || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
|
|
210
|
+
throw new ConfigError("replay reviews each commit on its own; --base, --head, --staged, --fail-on, --advisory, --fail-under, --all, --since and --ignore-exceptions do not apply.");
|
|
211
|
+
}
|
|
212
|
+
const format = global.json ? "json" : global.format;
|
|
213
|
+
if (format !== "terminal" && format !== "json") throw new ConfigError("replay supports --format terminal or json.");
|
|
214
|
+
const last = opts.last === void 0 ? 50 : Number(opts.last);
|
|
215
|
+
if (!/^\d+$/.test(opts.last ?? "50") || last < 1 || last > 1e3) throw new ConfigError("--last must be a whole number between 1 and 1000.");
|
|
216
|
+
const progress = process.stderr.isTTY && format === "terminal";
|
|
217
|
+
const result = await replayHistory({
|
|
218
|
+
cwd: repoPath,
|
|
219
|
+
last,
|
|
220
|
+
allRules: opts.allRules,
|
|
221
|
+
format,
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
onProgress: progress ? (done, total) => process.stderr.write(`\r reviewed ${done}/${total} commits`) : void 0
|
|
224
|
+
});
|
|
225
|
+
if (progress) process.stderr.write("\r\x1B[K");
|
|
226
|
+
console.log(result.output);
|
|
227
|
+
process.exitCode = result.exitCode;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(`agent-roast replay error: ${errorMessage(error)}`);
|
|
230
|
+
process.exitCode = 2;
|
|
231
|
+
} finally {
|
|
232
|
+
process.removeListener("SIGINT", abort);
|
|
233
|
+
process.removeListener("SIGTERM", abort);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
program.command("check").description("Check current tracked and non-ignored untracked source files").argument("[path]", "Path to a git repository", ".").option("--baseline <file>", "Report only findings not accepted in this baseline").option("--write-baseline <file>", "Save current findings for adoption (refuses to overwrite)").addHelpText("after", "\nOutput options: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>, --advisory\nExamples:\n agent-roast check\n agent-roast check --write-baseline .agentroast-baseline.json\n agent-roast check --baseline .agentroast-baseline.json --format json").action(async (repoPath, opts) => {
|
|
195
237
|
try {
|
|
196
238
|
const global = program.opts();
|
|
197
239
|
if (global.base || global.staged || global.failUnder !== void 0 || global.all || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
|
|
@@ -202,7 +244,7 @@ program.command("check").description("Check current tracked and non-ignored untr
|
|
|
202
244
|
cwd: repoPath,
|
|
203
245
|
baseline: opts.baseline,
|
|
204
246
|
ignoreExceptions: global.ignoreExceptions,
|
|
205
|
-
failOn: global
|
|
247
|
+
failOn: failOnOption(global),
|
|
206
248
|
format: global.json ? "json" : global.format
|
|
207
249
|
});
|
|
208
250
|
if (opts.writeBaseline) {
|
package/dist/index.d.ts
CHANGED
|
@@ -317,6 +317,15 @@ declare function formatTerminalCard(summary: MetricSummary, options: {
|
|
|
317
317
|
verbose?: boolean;
|
|
318
318
|
}): string;
|
|
319
319
|
declare function formatPrTerminalCard(result: PrAuditResult): string;
|
|
320
|
+
/** Rules about the checks themselves (tests, assertions, quality gates), as opposed to line patterns such as `as any`. */
|
|
321
|
+
declare function isCheckRule(type: string): boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Puts the findings that matter first, so a long list of warnings cannot push them below a
|
|
324
|
+
* display cap. The order inside each rank is unchanged.
|
|
325
|
+
*/
|
|
326
|
+
declare function mostImportantFirst<T extends {
|
|
327
|
+
type: string;
|
|
328
|
+
}>(findings: readonly T[], failedRules: readonly string[]): T[];
|
|
320
329
|
declare function formatGithubAnnotations(result: Pick<PrAuditResult, "infractions" | "failedRules">): string;
|
|
321
330
|
/**
|
|
322
331
|
* Generates GitHub Step Summary Markdown
|
|
@@ -410,6 +419,55 @@ declare function compareFindings(before: Infraction[], after: Infraction[], newP
|
|
|
410
419
|
declare function reviewChanges(options: ReviewOptions): Promise<ReviewResult>;
|
|
411
420
|
declare function reviewStagedChanges(options?: StagedReviewOptions): Promise<ReviewResult>;
|
|
412
421
|
|
|
422
|
+
type ReplayStatus = "blocked" | "warned" | "noted" | "clean" | "untouched" | "incomplete" | "failed";
|
|
423
|
+
interface ReplayCommit {
|
|
424
|
+
sha: string;
|
|
425
|
+
date: string;
|
|
426
|
+
subject: string;
|
|
427
|
+
/** blocked: a rule that blocks by default fired; warned: a check rule fired without blocking; noted: only line patterns. */
|
|
428
|
+
status: ReplayStatus;
|
|
429
|
+
failedRules: string[];
|
|
430
|
+
findings: Infraction[];
|
|
431
|
+
filesInspected: number;
|
|
432
|
+
skipped: {
|
|
433
|
+
file: string;
|
|
434
|
+
reason: string;
|
|
435
|
+
}[];
|
|
436
|
+
error?: string;
|
|
437
|
+
}
|
|
438
|
+
interface ReplayOptions {
|
|
439
|
+
cwd?: string;
|
|
440
|
+
/** How many of the newest commits to review (default 50). */
|
|
441
|
+
last?: number;
|
|
442
|
+
/** List the line-pattern findings (type-escape, swallowed-error, ...) as well as the check rules. */
|
|
443
|
+
allRules?: boolean;
|
|
444
|
+
format?: "terminal" | "json";
|
|
445
|
+
signal?: AbortSignal;
|
|
446
|
+
onProgress?: (done: number, total: number) => void;
|
|
447
|
+
}
|
|
448
|
+
interface ReplayResult {
|
|
449
|
+
schemaVersion: string;
|
|
450
|
+
mode: "replay";
|
|
451
|
+
head: string;
|
|
452
|
+
requested: number;
|
|
453
|
+
/** Commits without a parent (the first commit, or the edge of a shallow clone) have nothing to compare with. */
|
|
454
|
+
commitsWithoutParent: number;
|
|
455
|
+
/** A shallow clone holds only part of the history, so fewer commits than requested may have been replayed. */
|
|
456
|
+
shallow: boolean;
|
|
457
|
+
summary: Record<ReplayStatus | "commits", number> & {
|
|
458
|
+
findingsByRule: Record<string, number>;
|
|
459
|
+
};
|
|
460
|
+
commits: ReplayCommit[];
|
|
461
|
+
exitCode: number;
|
|
462
|
+
output: string;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Reviews each of the newest commits of the first-parent history on its own against its parent, which is what a
|
|
466
|
+
* pull-request check would have seen: one change per squash, merge or direct commit. Reads Git objects only.
|
|
467
|
+
*/
|
|
468
|
+
declare function replayHistory(options?: ReplayOptions): Promise<ReplayResult>;
|
|
469
|
+
declare function formatReplayTerminal(result: ReplayResult, allRules?: boolean): string;
|
|
470
|
+
|
|
413
471
|
/** Offline SARIF 2.1.0, with relative URIs and explicit incomplete-run status.
|
|
414
472
|
* Snippets are omitted to avoid embedding source code in integration artifacts. */
|
|
415
473
|
declare function formatSarif(report: CheckResult | ReviewResult): string;
|
|
@@ -452,4 +510,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
|
|
|
452
510
|
}>;
|
|
453
511
|
declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
|
|
454
512
|
|
|
455
|
-
export { type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, MAX_CONFIG_BYTES, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, detectAiAttribution, findMergeBase, findingFingerprint, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, initProject, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, reviewChanges, reviewStagedChanges, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, unquoteGitPath, validateExceptions, writeBaseline };
|
|
513
|
+
export { type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, MAX_CONFIG_BYTES, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReplayCommit, type ReplayOptions, type ReplayResult, type ReplayStatus, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, detectAiAttribution, findMergeBase, findingFingerprint, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReplayTerminal, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, initProject, isCheckRule, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, mostImportantFirst, parseDiff, parseGitLogStream, replayHistory, resolveCommitSha, reviewChanges, reviewStagedChanges, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, unquoteGitPath, validateExceptions, writeBaseline };
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
formatGithubStepSummary,
|
|
34
34
|
formatPrMarkdownComment,
|
|
35
35
|
formatPrTerminalCard,
|
|
36
|
+
formatReplayTerminal,
|
|
36
37
|
formatReviewMarkdown,
|
|
37
38
|
formatReviewTerminal,
|
|
38
39
|
formatSarif,
|
|
@@ -45,13 +46,16 @@ import {
|
|
|
45
46
|
getScoreColor,
|
|
46
47
|
gitOutputPath,
|
|
47
48
|
initProject,
|
|
49
|
+
isCheckRule,
|
|
48
50
|
isCodeFile,
|
|
49
51
|
isTestFile,
|
|
50
52
|
loadBaseline,
|
|
51
53
|
loadConfig,
|
|
52
54
|
matchIgnorePath,
|
|
55
|
+
mostImportantFirst,
|
|
53
56
|
parseDiff,
|
|
54
57
|
parseGitLogStream,
|
|
58
|
+
replayHistory,
|
|
55
59
|
resolveCommitSha,
|
|
56
60
|
reviewChanges,
|
|
57
61
|
reviewStagedChanges,
|
|
@@ -63,7 +67,7 @@ import {
|
|
|
63
67
|
unquoteGitPath,
|
|
64
68
|
validateExceptions,
|
|
65
69
|
writeBaseline
|
|
66
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-WMHB65AL.js";
|
|
67
71
|
export {
|
|
68
72
|
CONFIG_FILENAMES,
|
|
69
73
|
ConfigError,
|
|
@@ -98,6 +102,7 @@ export {
|
|
|
98
102
|
formatGithubStepSummary,
|
|
99
103
|
formatPrMarkdownComment,
|
|
100
104
|
formatPrTerminalCard,
|
|
105
|
+
formatReplayTerminal,
|
|
101
106
|
formatReviewMarkdown,
|
|
102
107
|
formatReviewTerminal,
|
|
103
108
|
formatSarif,
|
|
@@ -110,13 +115,16 @@ export {
|
|
|
110
115
|
getScoreColor,
|
|
111
116
|
gitOutputPath,
|
|
112
117
|
initProject,
|
|
118
|
+
isCheckRule,
|
|
113
119
|
isCodeFile,
|
|
114
120
|
isTestFile,
|
|
115
121
|
loadBaseline,
|
|
116
122
|
loadConfig,
|
|
117
123
|
matchIgnorePath,
|
|
124
|
+
mostImportantFirst,
|
|
118
125
|
parseDiff,
|
|
119
126
|
parseGitLogStream,
|
|
127
|
+
replayHistory,
|
|
120
128
|
resolveCommitSha,
|
|
121
129
|
reviewChanges,
|
|
122
130
|
reviewStagedChanges,
|
package/package.json
CHANGED