agent-roast 0.2.1 → 0.4.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 +51 -5
- package/dist/{chunk-XUNF5YCA.js → chunk-O4SB2FCT.js} +460 -30
- package/dist/cli.js +95 -6
- package/dist/index.d.ts +84 -5
- package/dist/index.js +27 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,17 +18,29 @@
|
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
`agent-roast` scans recorded git diffs for shortcuts taken by AI coding assistants (Claude Code, Cursor, Codex, Antigravity, Aider). It checks for skipped tests, compiler type escapes (`as any`), swallowed errors, and panic revert loops, then outputs a discipline score and line proofs. 100% offline, zero API keys, zero telemetry.
|
|
21
|
+
`agent-roast` scans recorded git diffs for shortcuts taken by AI coding assistants (Claude Code, Cursor, Codex, Antigravity, Aider). It checks for skipped tests, fake tautological assertions, abandoned stubs, compiler type escapes (`as any`), swallowed errors, and panic revert loops, then outputs a discipline score and line proofs. 100% offline, zero API keys, zero telemetry.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
+
# Initialize in your repository (creates .agentroastrc.json)
|
|
25
|
+
npx agent-roast init
|
|
26
|
+
npx agent-roast init --ci # generate .github/workflows/agent-roast.yml
|
|
27
|
+
npx agent-roast init --hook # install .git/hooks/pre-commit
|
|
28
|
+
|
|
24
29
|
# Audit repository history
|
|
25
30
|
npx agent-roast # scan AI-attributed commits (last 90 days)
|
|
26
31
|
npx agent-roast --all # scan entire repository history
|
|
27
32
|
npx agent-roast --verbose # show commit SHAs and line proofs
|
|
33
|
+
npx agent-roast --fail-under 70 # exit code 1 if score is below 70
|
|
28
34
|
|
|
29
35
|
# Audit pull request diffs (CI gate)
|
|
30
36
|
npx agent-roast --base origin/main --head HEAD
|
|
31
|
-
npx agent-roast --base origin/main --head HEAD --fail-on test-skip,
|
|
37
|
+
npx agent-roast --base origin/main --head HEAD --fail-on test-skip,trivial-assert,stub-abandonment --format github
|
|
38
|
+
npx agent-roast --base origin/main --format markdown # rich markdown for PR comments
|
|
39
|
+
|
|
40
|
+
# Generate AI discipline badge for your README
|
|
41
|
+
npx agent-roast badge # prints Shields.io Markdown snippet
|
|
42
|
+
npx agent-roast badge --out ./badge.svg # exports standalone SVG badge
|
|
43
|
+
npx agent-roast badge --json # Shields.io dynamic endpoint payload
|
|
32
44
|
```
|
|
33
45
|
|
|
34
46
|
What a report looks like (a real repository audit, trimmed):
|
|
@@ -50,6 +62,12 @@ INFRACTIONS DETECTED (42)
|
|
|
50
62
|
● test-skip (6) — disabled or skipped test suites
|
|
51
63
|
e.g. tests/test_outbound.py:18 — pytest.skip(...)
|
|
52
64
|
|
|
65
|
+
● trivial-assert (2) — tautological assertions faking test coverage
|
|
66
|
+
e.g. tests/auth.test.ts:31 — expect(true).toBe(true)
|
|
67
|
+
|
|
68
|
+
● stub-abandonment (1) — incomplete stubs left behind
|
|
69
|
+
e.g. src/payment.ts:18 — throw new Error("Not implemented")
|
|
70
|
+
|
|
53
71
|
● swallowed-error (2) — empty catch blocks or except: pass
|
|
54
72
|
```
|
|
55
73
|
|
|
@@ -59,12 +77,38 @@ INFRACTIONS DETECTED (42)
|
|
|
59
77
|
|
|
60
78
|
---
|
|
61
79
|
|
|
80
|
+
### Configuration (`.agentroastrc.json`)
|
|
81
|
+
|
|
82
|
+
Configure custom ignore paths, rule severities, and CI thresholds with a `.agentroastrc.json` file in your repository root:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"$schema": "https://raw.githubusercontent.com/Nari-Ab/agent-roast/main/schema.json",
|
|
87
|
+
"ignorePaths": [
|
|
88
|
+
"**/*.min.js",
|
|
89
|
+
"**/vendor/**",
|
|
90
|
+
"**/dist/**",
|
|
91
|
+
"**/fixtures/**"
|
|
92
|
+
],
|
|
93
|
+
"rules": {
|
|
94
|
+
"hallucinated-package": "error",
|
|
95
|
+
"empty-catch": "warn",
|
|
96
|
+
"trivial-assert": "warn",
|
|
97
|
+
"stub-abandonment": "warn",
|
|
98
|
+
"mock-leak": "warn"
|
|
99
|
+
},
|
|
100
|
+
"scoreThreshold": 70
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
62
106
|
### GitHub Actions Integration
|
|
63
107
|
|
|
64
|
-
Add `agent-roast` as a gate in your pull request workflow (`.github/workflows/
|
|
108
|
+
Add `agent-roast` as a gate in your pull request workflow (`.github/workflows/agent-roast.yml`):
|
|
65
109
|
|
|
66
110
|
```yaml
|
|
67
|
-
name: Agent
|
|
111
|
+
name: Agent Roast
|
|
68
112
|
on:
|
|
69
113
|
pull_request:
|
|
70
114
|
branches: [main]
|
|
@@ -83,7 +127,7 @@ jobs:
|
|
|
83
127
|
- name: Run agent-roast audit
|
|
84
128
|
uses: Nari-Ab/agent-roast@v1
|
|
85
129
|
with:
|
|
86
|
-
fail-on: test-skip
|
|
130
|
+
fail-on: test-skip,trivial-assert,stub-abandonment
|
|
87
131
|
format: github
|
|
88
132
|
```
|
|
89
133
|
|
|
@@ -96,6 +140,8 @@ jobs:
|
|
|
96
140
|
Only **added lines** (`+`) in source code files are scanned — deleted lines, file moves, and comments never trigger false alarms.
|
|
97
141
|
|
|
98
142
|
- **Skipped tests (`test-skip`):** `it.skip`, `describe.skip`, `xit`, `pytest.skip`, `test.todo` added to silence failing tests. (Default blocking gate).
|
|
143
|
+
- **Trivial assertions (`trivial-assert`):** Tautological or dummy assertions added to fabricate test green checks (`expect(true).toBe(true)`, `expect(1).toBe(1)`, `assert True`, `self.assertTrue(True)`).
|
|
144
|
+
- **Stub abandonment (`stub-abandonment`):** Dummy stubs left unimplemented in committed code (`throw new Error("Not implemented")`, `raise NotImplementedError`, Rust `todo!()` / `unimplemented!()`).
|
|
99
145
|
- **Type escapes (`type-escape`):** `as any`, `as unknown as`, `as never`, `// @ts-ignore`, `// @ts-nocheck`, `// @ts-expect-error`, `# type: ignore`, `# noqa`, `eslint-disable`.
|
|
100
146
|
- **Swallowed errors (`swallowed-error`):** Empty `catch {}` or `except: pass` blocks hiding runtime failures.
|
|
101
147
|
- **Panic fix loops (`panic-loop`):** Heuristic chain analysis of rapid-fire fix/revert commits by the same author within 15 minutes touching overlapping files. *(Observational metric for repository audits; not a PR blocker)*.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path3 from "path";
|
|
5
5
|
|
|
6
6
|
// src/git.ts
|
|
7
7
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -136,12 +136,12 @@ async function parseGitLogStream(stream, onCommit) {
|
|
|
136
136
|
{ skippedLongLines, skippedUnknownFiles }
|
|
137
137
|
);
|
|
138
138
|
};
|
|
139
|
-
return new Promise((
|
|
139
|
+
return new Promise((resolve2, reject) => {
|
|
140
140
|
let settled = false;
|
|
141
141
|
const safeResolve = (val) => {
|
|
142
142
|
if (!settled) {
|
|
143
143
|
settled = true;
|
|
144
|
-
|
|
144
|
+
resolve2(val);
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
const safeReject = (err) => {
|
|
@@ -542,6 +542,90 @@ var GitResolutionError = class extends Error {
|
|
|
542
542
|
}
|
|
543
543
|
};
|
|
544
544
|
|
|
545
|
+
// src/config.ts
|
|
546
|
+
import * as fs from "fs";
|
|
547
|
+
import * as path from "path";
|
|
548
|
+
var DEFAULT_CONFIG = {
|
|
549
|
+
failOn: ["test-skip", "trivial-assert"],
|
|
550
|
+
ignorePaths: [
|
|
551
|
+
"**/fixtures/**",
|
|
552
|
+
"**/__fixtures__/**",
|
|
553
|
+
"**/testdata/**",
|
|
554
|
+
"**/mocks/**",
|
|
555
|
+
"**/test_data/**",
|
|
556
|
+
"**/__snapshots__/**",
|
|
557
|
+
"**/sample/**",
|
|
558
|
+
"**/samples/**"
|
|
559
|
+
],
|
|
560
|
+
scoreThreshold: 70,
|
|
561
|
+
rules: {
|
|
562
|
+
"test-skip": "error",
|
|
563
|
+
"trivial-assert": "error",
|
|
564
|
+
"swallowed-error": "warn",
|
|
565
|
+
"stub-abandonment": "warn",
|
|
566
|
+
"type-escape": "warn"
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var CONFIG_FILENAMES = [
|
|
570
|
+
".agentroastrc.json",
|
|
571
|
+
".agentroastrc",
|
|
572
|
+
"agent-roast.config.json"
|
|
573
|
+
];
|
|
574
|
+
function loadConfig(cwd = process.cwd()) {
|
|
575
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
576
|
+
const filePath = path.join(cwd, filename);
|
|
577
|
+
if (fs.existsSync(filePath)) {
|
|
578
|
+
try {
|
|
579
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
580
|
+
const parsed = JSON.parse(raw);
|
|
581
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
582
|
+
return {
|
|
583
|
+
...DEFAULT_CONFIG,
|
|
584
|
+
...parsed,
|
|
585
|
+
failOn: Array.isArray(parsed.failOn) ? parsed.failOn : DEFAULT_CONFIG.failOn,
|
|
586
|
+
ignorePaths: Array.isArray(parsed.ignorePaths) ? Array.from(/* @__PURE__ */ new Set([...DEFAULT_CONFIG.ignorePaths || [], ...parsed.ignorePaths])) : DEFAULT_CONFIG.ignorePaths,
|
|
587
|
+
rules: {
|
|
588
|
+
...DEFAULT_CONFIG.rules,
|
|
589
|
+
...parsed.rules || {}
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
} catch (err) {
|
|
594
|
+
console.warn(`Warning: Failed to parse ${filename}: ${err.message}. Using default configuration.`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return { ...DEFAULT_CONFIG };
|
|
599
|
+
}
|
|
600
|
+
function matchIgnorePath(filePath, ignorePatterns) {
|
|
601
|
+
if (!ignorePatterns || ignorePatterns.length === 0) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
605
|
+
for (const pattern of ignorePatterns) {
|
|
606
|
+
if (!pattern) continue;
|
|
607
|
+
const cleanPattern = pattern.replace(/\\/g, "/");
|
|
608
|
+
let regexPattern = cleanPattern.replace(/[.+^${}()|[\]]/g, "\\$&").replace(/\*\*\//g, "(?:^|.*?/)").replace(/\/\*\*/g, "(?:/.*)?").replace(/\*/g, "[^/]*");
|
|
609
|
+
if (!regexPattern.startsWith("^") && !regexPattern.startsWith("(?:^")) {
|
|
610
|
+
regexPattern = "(?:^|/)" + regexPattern;
|
|
611
|
+
}
|
|
612
|
+
if (!regexPattern.endsWith("$")) {
|
|
613
|
+
regexPattern = regexPattern + "(?:$|/)";
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const rx = new RegExp(regexPattern, "i");
|
|
617
|
+
if (rx.test(normalized)) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
if (normalized.includes(cleanPattern.replace(/\*/g, ""))) {
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
545
629
|
// src/metrics.ts
|
|
546
630
|
var TEST_SKIP_PATTERNS = [
|
|
547
631
|
{ regex: /(?:it|test|describe)\.skip\b/, name: "skipped test (it.skip / describe.skip)" },
|
|
@@ -567,6 +651,21 @@ var SWALLOWED_ERROR_PATTERNS = [
|
|
|
567
651
|
{ regex: /catch\s*(?:\([^)]*\))?\s*\{\s*\}/, name: "empty catch block (catch {})" },
|
|
568
652
|
{ regex: /except(?:\s+[^:]*)?:\s*pass\b/, name: "silent python error swallow (except: pass)" }
|
|
569
653
|
];
|
|
654
|
+
var TRIVIAL_ASSERT_PATTERNS = [
|
|
655
|
+
{ regex: /expect\s*\(\s*(?:true|false|1|0|null|undefined)\s*\)\.toBe\s*\(\s*(?:true|false|1|0|null|undefined)\s*\)/, name: "trivial assertion (expect(val).toBe(val))" },
|
|
656
|
+
{ regex: /expect\s*\(\s*(?:true|false|1|0|null|undefined)\s*\)\.toEqual\s*\(\s*(?:true|false|1|0|null|undefined)\s*\)/, name: "trivial assertion (expect(val).toEqual(val))" },
|
|
657
|
+
{ regex: /\bassert\s+(?:True|False|1\s*==\s*1)\b/, name: "trivial python assert (assert True / 1 == 1)" },
|
|
658
|
+
{ regex: /self\.assertTrue\s*\(\s*(?:True|1)\s*\)/, name: "trivial assert (self.assertTrue(True))" },
|
|
659
|
+
{ regex: /self\.assertEqual\s*\(\s*(?:1\s*,\s*1|True\s*,\s*True)\s*\)/, name: "trivial assert (self.assertEqual(1, 1))" },
|
|
660
|
+
{ regex: /assert\.(?:strictEqual|equal)\s*\(\s*(?:true\s*,\s*true|1\s*,\s*1)\s*\)/, name: "trivial assert (assert.equal(1, 1))" }
|
|
661
|
+
];
|
|
662
|
+
var STUB_ABANDONMENT_PATTERNS = [
|
|
663
|
+
{ regex: /\bthrow\s+new\s+Error\s*\(\s*["'](?:TODO|Not implemented|to be implemented|stub|implement me)/i, name: "unimplemented stub (throw new Error('Not implemented'))" },
|
|
664
|
+
{ regex: /\braise\s+NotImplementedError\b/, name: "unimplemented stub (raise NotImplementedError)" },
|
|
665
|
+
{ regex: /\braise\s+NotImplemented\b/, name: "unimplemented stub (raise NotImplemented)" },
|
|
666
|
+
{ regex: /\btodo!\s*\(\s*\)/, name: "unimplemented rust stub (todo!())" },
|
|
667
|
+
{ regex: /\bunimplemented!\s*\(\s*\)/, name: "unimplemented rust stub (unimplemented!())" }
|
|
668
|
+
];
|
|
570
669
|
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
571
670
|
".ts",
|
|
572
671
|
".tsx",
|
|
@@ -588,21 +687,28 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
588
687
|
".c"
|
|
589
688
|
]);
|
|
590
689
|
var IGNORED_PATH_REGEX = /(?:^|\/)(?:fixtures|__fixtures__|testdata|mocks|test_data|__snapshots__|sample[s]?)\//i;
|
|
591
|
-
|
|
690
|
+
var TEST_PATH_REGEX = /(?:^|\/)(?:tests?|__tests__|specs?)\/|[._-](?:test|spec)\.[a-z0-9]+$/i;
|
|
691
|
+
function isCodeFile(filePath, customIgnorePaths) {
|
|
592
692
|
if (IGNORED_PATH_REGEX.test(filePath)) {
|
|
593
693
|
return false;
|
|
594
694
|
}
|
|
695
|
+
if (customIgnorePaths && matchIgnorePath(filePath, customIgnorePaths)) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
595
698
|
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
596
699
|
return CODE_EXTENSIONS.has(ext);
|
|
597
700
|
}
|
|
598
|
-
function
|
|
701
|
+
function isTestFile(filePath) {
|
|
702
|
+
return TEST_PATH_REGEX.test(filePath);
|
|
703
|
+
}
|
|
704
|
+
function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ */ new Date()).toISOString(), options = {}) {
|
|
599
705
|
const infractions = [];
|
|
600
706
|
let hasSupportedChanges = false;
|
|
601
707
|
let filesInspected = 0;
|
|
602
708
|
let linesAdded = 0;
|
|
603
709
|
for (const file of files) {
|
|
604
710
|
if (file.deleted || file.path === "unknown") continue;
|
|
605
|
-
if (!isCodeFile(file.path)) continue;
|
|
711
|
+
if (!isCodeFile(file.path, options.ignorePaths)) continue;
|
|
606
712
|
hasSupportedChanges = true;
|
|
607
713
|
filesInspected++;
|
|
608
714
|
linesAdded += file.addedLines.length;
|
|
@@ -675,6 +781,38 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
675
781
|
}
|
|
676
782
|
}
|
|
677
783
|
}
|
|
784
|
+
if (!isComment && isTestFile(file.path)) {
|
|
785
|
+
for (const rule of TRIVIAL_ASSERT_PATTERNS) {
|
|
786
|
+
if (rule.regex.test(cleanedLine)) {
|
|
787
|
+
infractions.push({
|
|
788
|
+
type: "trivial-assert",
|
|
789
|
+
file: file.path,
|
|
790
|
+
lineNumber: added.lineNumber,
|
|
791
|
+
lineSnippet: rawLine.trim(),
|
|
792
|
+
commitHash,
|
|
793
|
+
commitDate,
|
|
794
|
+
reason: rule.name
|
|
795
|
+
});
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (!isComment) {
|
|
801
|
+
for (const rule of STUB_ABANDONMENT_PATTERNS) {
|
|
802
|
+
if (rule.regex.test(rawLine)) {
|
|
803
|
+
infractions.push({
|
|
804
|
+
type: "stub-abandonment",
|
|
805
|
+
file: file.path,
|
|
806
|
+
lineNumber: added.lineNumber,
|
|
807
|
+
lineSnippet: rawLine.trim(),
|
|
808
|
+
commitHash,
|
|
809
|
+
commitDate,
|
|
810
|
+
reason: rule.name
|
|
811
|
+
});
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
678
816
|
}
|
|
679
817
|
}
|
|
680
818
|
return { infractions, hasSupportedChanges, filesInspected, linesAdded };
|
|
@@ -697,12 +835,16 @@ var MetricsCollector = class {
|
|
|
697
835
|
typeEscapes = [];
|
|
698
836
|
swallowedErrors = [];
|
|
699
837
|
panicLoops = [];
|
|
838
|
+
trivialAsserts = [];
|
|
839
|
+
stubAbandonments = [];
|
|
700
840
|
skippedLongLines = 0;
|
|
701
841
|
skippedUnknownFiles = 0;
|
|
702
842
|
activePanicChain = null;
|
|
703
843
|
includeAll;
|
|
844
|
+
ignorePaths;
|
|
704
845
|
constructor(options = {}) {
|
|
705
846
|
this.includeAll = options.all ?? false;
|
|
847
|
+
this.ignorePaths = options.ignorePaths;
|
|
706
848
|
}
|
|
707
849
|
recordSkipped(stats) {
|
|
708
850
|
if (stats.skippedLongLines) this.skippedLongLines += stats.skippedLongLines;
|
|
@@ -788,8 +930,10 @@ var MetricsCollector = class {
|
|
|
788
930
|
let commitSkips = 0;
|
|
789
931
|
let commitEscapes = 0;
|
|
790
932
|
let commitSwallowed = 0;
|
|
933
|
+
let commitTrivial = 0;
|
|
934
|
+
let commitStubs = 0;
|
|
791
935
|
for (const file of commit.files) {
|
|
792
|
-
if (!isCodeFile(file.path)) {
|
|
936
|
+
if (!isCodeFile(file.path, this.ignorePaths)) {
|
|
793
937
|
continue;
|
|
794
938
|
}
|
|
795
939
|
for (const added of file.addedLines) {
|
|
@@ -865,6 +1009,40 @@ var MetricsCollector = class {
|
|
|
865
1009
|
}
|
|
866
1010
|
}
|
|
867
1011
|
}
|
|
1012
|
+
if (!isComment && isTestFile(file.path) && commitTrivial < MAX_INFRACTIONS_PER_COMMIT) {
|
|
1013
|
+
for (const rule of TRIVIAL_ASSERT_PATTERNS) {
|
|
1014
|
+
if (rule.regex.test(cleanedLine)) {
|
|
1015
|
+
this.trivialAsserts.push({
|
|
1016
|
+
type: "trivial-assert",
|
|
1017
|
+
file: file.path,
|
|
1018
|
+
lineNumber: added.lineNumber,
|
|
1019
|
+
lineSnippet: rawLine.trim(),
|
|
1020
|
+
commitHash: commit.hash,
|
|
1021
|
+
commitDate: commit.date,
|
|
1022
|
+
reason: rule.name
|
|
1023
|
+
});
|
|
1024
|
+
commitTrivial++;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (!isComment && commitStubs < MAX_INFRACTIONS_PER_COMMIT) {
|
|
1030
|
+
for (const rule of STUB_ABANDONMENT_PATTERNS) {
|
|
1031
|
+
if (rule.regex.test(rawLine)) {
|
|
1032
|
+
this.stubAbandonments.push({
|
|
1033
|
+
type: "stub-abandonment",
|
|
1034
|
+
file: file.path,
|
|
1035
|
+
lineNumber: added.lineNumber,
|
|
1036
|
+
lineSnippet: rawLine.trim(),
|
|
1037
|
+
commitHash: commit.hash,
|
|
1038
|
+
commitDate: commit.date,
|
|
1039
|
+
reason: rule.name
|
|
1040
|
+
});
|
|
1041
|
+
commitStubs++;
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
868
1046
|
}
|
|
869
1047
|
}
|
|
870
1048
|
}
|
|
@@ -872,7 +1050,7 @@ var MetricsCollector = class {
|
|
|
872
1050
|
this.flushPanicChain();
|
|
873
1051
|
const linesSample = this.includeAll ? this.totalLinesAdded : this.aiLinesAdded;
|
|
874
1052
|
const isSufficientData = linesSample >= MIN_SAMPLE_LOC;
|
|
875
|
-
const codeInfractionPoints = this.testSkips.length * 15 + this.swallowedErrors.length * 10 + this.typeEscapes.length * 3;
|
|
1053
|
+
const codeInfractionPoints = this.testSkips.length * 15 + this.trivialAsserts.length * 15 + this.swallowedErrors.length * 10 + this.stubAbandonments.length * 8 + this.typeEscapes.length * 3;
|
|
876
1054
|
let panicInfractionPoints = 0;
|
|
877
1055
|
for (const loop of this.panicLoops) {
|
|
878
1056
|
const countMatch = loop.reason.match(/chain of (\d+) quick fixes/);
|
|
@@ -901,16 +1079,24 @@ var MetricsCollector = class {
|
|
|
901
1079
|
}
|
|
902
1080
|
} else {
|
|
903
1081
|
const testSkipPoints = this.testSkips.length * 15;
|
|
1082
|
+
const trivialPoints = this.trivialAsserts.length * 15;
|
|
904
1083
|
const panicPoints = this.panicLoops.length * 8;
|
|
1084
|
+
const stubPoints = this.stubAbandonments.length * 8;
|
|
905
1085
|
const escapePoints = this.typeEscapes.length * 3;
|
|
906
1086
|
const swallowPoints = this.swallowedErrors.length * 10;
|
|
907
|
-
const maxPoints = Math.max(testSkipPoints, panicPoints, escapePoints, swallowPoints);
|
|
1087
|
+
const maxPoints = Math.max(testSkipPoints, trivialPoints, panicPoints, stubPoints, escapePoints, swallowPoints);
|
|
908
1088
|
if (maxPoints === panicPoints && this.panicLoops.length >= 2) {
|
|
909
1089
|
archetype = "The Panic Looper";
|
|
910
1090
|
archetypeDescription = "Prone to rapid-fire fix and revert thrashing cycles when wrestling stubborn bugs.";
|
|
911
1091
|
} else if (maxPoints === testSkipPoints && this.testSkips.length >= 3) {
|
|
912
1092
|
archetype = "The Silent Vandal";
|
|
913
1093
|
archetypeDescription = "When test suites push back, tends to disable or skip tests to keep pipelines green.";
|
|
1094
|
+
} else if (maxPoints === trivialPoints && this.trivialAsserts.length >= 2) {
|
|
1095
|
+
archetype = "The Fake Asserter";
|
|
1096
|
+
archetypeDescription = "Masks broken tests by replacing real assertions with trivial expect(true).toBe(true) or assert True.";
|
|
1097
|
+
} else if (maxPoints === stubPoints && this.stubAbandonments.length >= 2) {
|
|
1098
|
+
archetype = "The Stub Abandoner";
|
|
1099
|
+
archetypeDescription = "Rushes through implementations by leaving unimplemented placeholders and TODO throws.";
|
|
914
1100
|
} else if (maxPoints === escapePoints && this.typeEscapes.length >= 5) {
|
|
915
1101
|
archetype = "The Any Architect";
|
|
916
1102
|
archetypeDescription = "Leans on compiler bypasses (`as any`, `@ts-ignore`) rather than strict type modeling.";
|
|
@@ -936,6 +1122,8 @@ var MetricsCollector = class {
|
|
|
936
1122
|
typeEscapes: this.typeEscapes,
|
|
937
1123
|
swallowedErrors: this.swallowedErrors,
|
|
938
1124
|
panicLoops: this.panicLoops,
|
|
1125
|
+
trivialAsserts: this.trivialAsserts,
|
|
1126
|
+
stubAbandonments: this.stubAbandonments,
|
|
939
1127
|
rawScore: totalInfractionPoints,
|
|
940
1128
|
score,
|
|
941
1129
|
isSufficientData,
|
|
@@ -986,26 +1174,26 @@ function formatTerminalCard(summary, options) {
|
|
|
986
1174
|
);
|
|
987
1175
|
lines.push(` ${pc.dim(summary.archetypeDescription)}`);
|
|
988
1176
|
lines.push("");
|
|
989
|
-
const totalInfractions = summary.testSkips
|
|
1177
|
+
const totalInfractions = (summary.testSkips?.length || 0) + (summary.typeEscapes?.length || 0) + (summary.swallowedErrors?.length || 0) + (summary.panicLoops?.length || 0) + (summary.trivialAsserts?.length || 0) + (summary.stubAbandonments?.length || 0);
|
|
990
1178
|
lines.push(pc.bold(` INFRACTIONS (${totalInfractions})`));
|
|
991
1179
|
lines.push(pc.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
992
|
-
if (summary.panicLoops.length > 0) {
|
|
1180
|
+
if (summary.panicLoops && summary.panicLoops.length > 0) {
|
|
993
1181
|
lines.push(
|
|
994
1182
|
` ${pc.magenta("\u25CF")} ${pc.bold("panic-loop")} (${summary.panicLoops.length}) ${pc.dim("consecutive quick fixes < 15m apart")}`
|
|
995
1183
|
);
|
|
996
1184
|
const sample = summary.panicLoops[0];
|
|
997
1185
|
lines.push(` ${pc.dim(sample.reason)}`);
|
|
998
1186
|
}
|
|
999
|
-
if (summary.
|
|
1187
|
+
if (summary.trivialAsserts && summary.trivialAsserts.length > 0) {
|
|
1000
1188
|
lines.push(
|
|
1001
|
-
` ${pc.
|
|
1189
|
+
` ${pc.red("\u25CF")} ${pc.bold("trivial-assert")} (${summary.trivialAsserts.length}) ${pc.dim("vacuous test assertions (expect(true).toBe(true), assert True)")}`
|
|
1002
1190
|
);
|
|
1003
|
-
const sample = summary.
|
|
1191
|
+
const sample = summary.trivialAsserts[0];
|
|
1004
1192
|
if (sample.file) {
|
|
1005
1193
|
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
1006
1194
|
}
|
|
1007
1195
|
}
|
|
1008
|
-
if (summary.testSkips.length > 0) {
|
|
1196
|
+
if (summary.testSkips && summary.testSkips.length > 0) {
|
|
1009
1197
|
lines.push(
|
|
1010
1198
|
` ${pc.red("\u25CF")} ${pc.bold("test-skip")} (${summary.testSkips.length}) ${pc.dim("skipped or disabled tests")}`
|
|
1011
1199
|
);
|
|
@@ -1014,7 +1202,7 @@ function formatTerminalCard(summary, options) {
|
|
|
1014
1202
|
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
1015
1203
|
}
|
|
1016
1204
|
}
|
|
1017
|
-
if (summary.swallowedErrors.length > 0) {
|
|
1205
|
+
if (summary.swallowedErrors && summary.swallowedErrors.length > 0) {
|
|
1018
1206
|
lines.push(
|
|
1019
1207
|
` ${pc.red("\u25CF")} ${pc.bold("swallowed-error")} (${summary.swallowedErrors.length}) ${pc.dim("empty catch blocks or except: pass")}`
|
|
1020
1208
|
);
|
|
@@ -1023,6 +1211,24 @@ function formatTerminalCard(summary, options) {
|
|
|
1023
1211
|
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
1024
1212
|
}
|
|
1025
1213
|
}
|
|
1214
|
+
if (summary.stubAbandonments && summary.stubAbandonments.length > 0) {
|
|
1215
|
+
lines.push(
|
|
1216
|
+
` ${pc.yellow("\u25CF")} ${pc.bold("stub-abandonment")} (${summary.stubAbandonments.length}) ${pc.dim("unimplemented placeholders (throw new Error('TODO'), NotImplementedError)")}`
|
|
1217
|
+
);
|
|
1218
|
+
const sample = summary.stubAbandonments[0];
|
|
1219
|
+
if (sample.file) {
|
|
1220
|
+
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (summary.typeEscapes && summary.typeEscapes.length > 0) {
|
|
1224
|
+
lines.push(
|
|
1225
|
+
` ${pc.yellow("\u25CF")} ${pc.bold("type-escape")} (${summary.typeEscapes.length}) ${pc.dim("compiler bypasses (as any, @ts-ignore)")}`
|
|
1226
|
+
);
|
|
1227
|
+
const sample = summary.typeEscapes[0];
|
|
1228
|
+
if (sample.file) {
|
|
1229
|
+
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1026
1232
|
if (totalInfractions === 0) {
|
|
1027
1233
|
lines.push(` ${pc.green("\u25CF")} ${pc.dim("zero infractions detected across all inspected lines.")}`);
|
|
1028
1234
|
}
|
|
@@ -1040,10 +1246,12 @@ function formatTerminalCard(summary, options) {
|
|
|
1040
1246
|
lines.push("");
|
|
1041
1247
|
}
|
|
1042
1248
|
const allInfractions = [
|
|
1043
|
-
...summary.testSkips,
|
|
1044
|
-
...summary.
|
|
1045
|
-
...summary.swallowedErrors,
|
|
1046
|
-
...summary.
|
|
1249
|
+
...summary.testSkips || [],
|
|
1250
|
+
...summary.trivialAsserts || [],
|
|
1251
|
+
...summary.swallowedErrors || [],
|
|
1252
|
+
...summary.stubAbandonments || [],
|
|
1253
|
+
...summary.typeEscapes || [],
|
|
1254
|
+
...summary.panicLoops || []
|
|
1047
1255
|
];
|
|
1048
1256
|
if (allInfractions.length > 0) {
|
|
1049
1257
|
lines.push(pc.bold(" EVIDENCE (First 10 occurrences):"));
|
|
@@ -1109,7 +1317,7 @@ function formatPrTerminalCard(result) {
|
|
|
1109
1317
|
}
|
|
1110
1318
|
function sortInfractionsDeterministic(infractions) {
|
|
1111
1319
|
const getLevelRank = (type) => {
|
|
1112
|
-
return type === "test-skip" || type === "swallowed-error" ? 0 : 1;
|
|
1320
|
+
return type === "test-skip" || type === "trivial-assert" || type === "swallowed-error" ? 0 : 1;
|
|
1113
1321
|
};
|
|
1114
1322
|
return [...infractions].sort((a, b) => {
|
|
1115
1323
|
const rankDiff = getLevelRank(a.type) - getLevelRank(b.type);
|
|
@@ -1129,7 +1337,7 @@ function formatGithubAnnotations(result) {
|
|
|
1129
1337
|
let omittedCount = 0;
|
|
1130
1338
|
for (const inf of sorted) {
|
|
1131
1339
|
if (!inf.file || inf.file === "unknown") continue;
|
|
1132
|
-
const isError = result.failedRules.includes(inf.type) || inf.type === "test-skip" || inf.type === "swallowed-error";
|
|
1340
|
+
const isError = result.failedRules.includes(inf.type) || inf.type === "test-skip" || inf.type === "trivial-assert" || inf.type === "swallowed-error";
|
|
1133
1341
|
const line = inf.lineNumber || 1;
|
|
1134
1342
|
const title = inf.reason.replace(/[\r\n]/g, " ");
|
|
1135
1343
|
const msg = (inf.lineSnippet || inf.reason).replace(/[\r\n]/g, " ");
|
|
@@ -1189,15 +1397,222 @@ function formatGithubStepSummary(result) {
|
|
|
1189
1397
|
}
|
|
1190
1398
|
return lines.join("\n");
|
|
1191
1399
|
}
|
|
1400
|
+
function formatPrMarkdownComment(result) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
const titleEmoji = result.passed ? "\u2705" : "\u274C";
|
|
1403
|
+
const titleStatus = result.passed ? "PASSED" : "BLOCKED";
|
|
1404
|
+
lines.push(`## \u{1F6E1}\uFE0F agent-roast audit: ${titleStatus} ${titleEmoji}`);
|
|
1405
|
+
lines.push("");
|
|
1406
|
+
if (!result.hasSupportedChanges) {
|
|
1407
|
+
lines.push("No supported source file changes detected in this pull request.");
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
lines.push("---");
|
|
1410
|
+
lines.push("*Audited 100% offline with [agent-roast](https://github.com/Nari-Ab/agent-roast)*");
|
|
1411
|
+
return lines.join("\n");
|
|
1412
|
+
}
|
|
1413
|
+
const statusText = result.passed ? "\u2705 Passed" : `\u274C Blocked on ${result.failedRules.join(", ")}`;
|
|
1414
|
+
lines.push("| Status | Defect Density | Files Inspected | Lines Added | Infractions |");
|
|
1415
|
+
lines.push("| :--- | :--- | :--- | :--- | :--- |");
|
|
1416
|
+
lines.push(
|
|
1417
|
+
`| ${statusText} | ${result.defectDensityPer100Lines} / 100 LOC | ${result.filesInspected} | +${result.linesAdded} | ${result.infractions.length} |`
|
|
1418
|
+
);
|
|
1419
|
+
lines.push("");
|
|
1420
|
+
if (result.infractions.length > 0) {
|
|
1421
|
+
lines.push("<details>");
|
|
1422
|
+
lines.push(`<summary><b>View Detected Infractions (${result.infractions.length})</b></summary>`);
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
lines.push("| Type | File:Line | Description | Snippet |");
|
|
1425
|
+
lines.push("| :--- | :--- | :--- | :--- |");
|
|
1426
|
+
const sorted = sortInfractionsDeterministic(result.infractions);
|
|
1427
|
+
for (const inf of sorted) {
|
|
1428
|
+
const loc = inf.file ? `\`${inf.file}:${inf.lineNumber || 1}\`` : "*history*";
|
|
1429
|
+
const snippet = inf.lineSnippet ? `\`${inf.lineSnippet.replace(/\|/g, "\\|")}\`` : "-";
|
|
1430
|
+
lines.push(`| \`${inf.type}\` | ${loc} | ${inf.reason} | ${snippet} |`);
|
|
1431
|
+
}
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push("</details>");
|
|
1434
|
+
lines.push("");
|
|
1435
|
+
} else {
|
|
1436
|
+
lines.push("\u2728 **Clean pull request**: Zero AI shortcuts, bypassed tests, or compiler type escapes detected.");
|
|
1437
|
+
lines.push("");
|
|
1438
|
+
}
|
|
1439
|
+
lines.push("---");
|
|
1440
|
+
lines.push("*Audited 100% offline with [agent-roast](https://github.com/Nari-Ab/agent-roast)*");
|
|
1441
|
+
return lines.join("\n");
|
|
1442
|
+
}
|
|
1192
1443
|
|
|
1193
1444
|
// src/types.ts
|
|
1194
1445
|
var VALID_DETECTORS = [
|
|
1195
1446
|
"test-skip",
|
|
1196
1447
|
"type-escape",
|
|
1197
1448
|
"swallowed-error",
|
|
1198
|
-
"panic-loop"
|
|
1449
|
+
"panic-loop",
|
|
1450
|
+
"trivial-assert",
|
|
1451
|
+
"stub-abandonment"
|
|
1199
1452
|
];
|
|
1200
1453
|
|
|
1454
|
+
// src/init.ts
|
|
1455
|
+
import * as fs2 from "fs";
|
|
1456
|
+
import * as path2 from "path";
|
|
1457
|
+
var DEFAULT_CONFIG_TEMPLATE = {
|
|
1458
|
+
$schema: "https://raw.githubusercontent.com/Nari-Ab/agent-roast/main/schema.json",
|
|
1459
|
+
ignorePaths: [
|
|
1460
|
+
"**/*.min.js",
|
|
1461
|
+
"**/vendor/**",
|
|
1462
|
+
"**/dist/**",
|
|
1463
|
+
"**/build/**",
|
|
1464
|
+
"**/fixtures/**"
|
|
1465
|
+
],
|
|
1466
|
+
rules: {
|
|
1467
|
+
"hallucinated-package": "error",
|
|
1468
|
+
"empty-catch": "warn",
|
|
1469
|
+
"trivial-assert": "warn",
|
|
1470
|
+
"stub-abandonment": "warn",
|
|
1471
|
+
"mock-leak": "warn"
|
|
1472
|
+
},
|
|
1473
|
+
scoreThreshold: 70
|
|
1474
|
+
};
|
|
1475
|
+
var GITHUB_WORKFLOW_TEMPLATE = `name: Agent Roast
|
|
1476
|
+
on:
|
|
1477
|
+
pull_request:
|
|
1478
|
+
branches: [main, master]
|
|
1479
|
+
push:
|
|
1480
|
+
branches: [main, master]
|
|
1481
|
+
|
|
1482
|
+
jobs:
|
|
1483
|
+
audit:
|
|
1484
|
+
name: Audit AI Shortcuts
|
|
1485
|
+
runs-on: ubuntu-latest
|
|
1486
|
+
steps:
|
|
1487
|
+
- name: Checkout repository
|
|
1488
|
+
uses: actions/checkout@v4
|
|
1489
|
+
with:
|
|
1490
|
+
fetch-depth: 0
|
|
1491
|
+
|
|
1492
|
+
- name: Setup Node.js
|
|
1493
|
+
uses: actions/setup-node@v4
|
|
1494
|
+
with:
|
|
1495
|
+
node-version: 20
|
|
1496
|
+
|
|
1497
|
+
- name: Run agent-roast PR check
|
|
1498
|
+
if: github.event_name == 'pull_request'
|
|
1499
|
+
run: npx agent-roast --base \${{ github.event.pull_request.base.sha }} --head \${{ github.event.pull_request.head.sha }} --format github
|
|
1500
|
+
|
|
1501
|
+
- name: Run agent-roast repo audit
|
|
1502
|
+
if: github.event_name != 'pull_request'
|
|
1503
|
+
run: npx agent-roast --fail-under 70
|
|
1504
|
+
`;
|
|
1505
|
+
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
1506
|
+
# Run agent-roast pre-commit verification
|
|
1507
|
+
npx agent-roast --fail-under 70
|
|
1508
|
+
`;
|
|
1509
|
+
async function initProject(options = {}) {
|
|
1510
|
+
const cwd = path2.resolve(options.cwd || process.cwd());
|
|
1511
|
+
const created = [];
|
|
1512
|
+
const skipped = [];
|
|
1513
|
+
const configPath = path2.join(cwd, ".agentroastrc.json");
|
|
1514
|
+
const altConfigPath = path2.join(cwd, "agent-roast.config.json");
|
|
1515
|
+
if (fs2.existsSync(configPath) || fs2.existsSync(altConfigPath)) {
|
|
1516
|
+
skipped.push(".agentroastrc.json");
|
|
1517
|
+
} else {
|
|
1518
|
+
fs2.writeFileSync(
|
|
1519
|
+
configPath,
|
|
1520
|
+
JSON.stringify(DEFAULT_CONFIG_TEMPLATE, null, 2) + "\n",
|
|
1521
|
+
"utf-8"
|
|
1522
|
+
);
|
|
1523
|
+
created.push(".agentroastrc.json");
|
|
1524
|
+
}
|
|
1525
|
+
if (options.ci) {
|
|
1526
|
+
const workflowDir = path2.join(cwd, ".github", "workflows");
|
|
1527
|
+
const workflowPath = path2.join(workflowDir, "agent-roast.yml");
|
|
1528
|
+
if (fs2.existsSync(workflowPath)) {
|
|
1529
|
+
skipped.push(".github/workflows/agent-roast.yml");
|
|
1530
|
+
} else {
|
|
1531
|
+
fs2.mkdirSync(workflowDir, { recursive: true });
|
|
1532
|
+
fs2.writeFileSync(workflowPath, GITHUB_WORKFLOW_TEMPLATE, "utf-8");
|
|
1533
|
+
created.push(".github/workflows/agent-roast.yml");
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (options.hook) {
|
|
1537
|
+
const gitDir = path2.join(cwd, ".git");
|
|
1538
|
+
if (!fs2.existsSync(gitDir) || !fs2.statSync(gitDir).isDirectory()) {
|
|
1539
|
+
skipped.push(".git/hooks/pre-commit (no .git directory found)");
|
|
1540
|
+
} else {
|
|
1541
|
+
const hooksDir = path2.join(gitDir, "hooks");
|
|
1542
|
+
const hookPath = path2.join(hooksDir, "pre-commit");
|
|
1543
|
+
if (fs2.existsSync(hookPath)) {
|
|
1544
|
+
skipped.push(".git/hooks/pre-commit");
|
|
1545
|
+
} else {
|
|
1546
|
+
fs2.mkdirSync(hooksDir, { recursive: true });
|
|
1547
|
+
fs2.writeFileSync(hookPath, PRE_COMMIT_HOOK_TEMPLATE, {
|
|
1548
|
+
encoding: "utf-8",
|
|
1549
|
+
mode: 493
|
|
1550
|
+
});
|
|
1551
|
+
created.push(".git/hooks/pre-commit");
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return { created, skipped };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/badge.ts
|
|
1559
|
+
function getScoreColor(score) {
|
|
1560
|
+
if (score === null || isNaN(score)) {
|
|
1561
|
+
return { name: "lightgrey", hex: "#9f9f9f" };
|
|
1562
|
+
}
|
|
1563
|
+
if (score >= 90) return { name: "brightgreen", hex: "#4c1" };
|
|
1564
|
+
if (score >= 80) return { name: "green", hex: "#97ca00" };
|
|
1565
|
+
if (score >= 70) return { name: "yellowgreen", hex: "#a4a61d" };
|
|
1566
|
+
if (score >= 60) return { name: "yellow", hex: "#dfb317" };
|
|
1567
|
+
return { name: "red", hex: "#e05d44" };
|
|
1568
|
+
}
|
|
1569
|
+
function generateSvgBadge(score, label = "AI Discipline") {
|
|
1570
|
+
const value = score !== null && !isNaN(score) ? `${score}/100` : "N/A";
|
|
1571
|
+
const color = getScoreColor(score);
|
|
1572
|
+
const labelWidth = Math.round(label.length * 6.8 + 14);
|
|
1573
|
+
const valueWidth = Math.round(value.length * 7.2 + 14);
|
|
1574
|
+
const totalWidth = labelWidth + valueWidth;
|
|
1575
|
+
const labelX = Math.round(labelWidth / 2);
|
|
1576
|
+
const valueX = Math.round(labelWidth + valueWidth / 2);
|
|
1577
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${value}">
|
|
1578
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
1579
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
1580
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
1581
|
+
</linearGradient>
|
|
1582
|
+
<clipPath id="r">
|
|
1583
|
+
<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
|
|
1584
|
+
</clipPath>
|
|
1585
|
+
<g clip-path="url(#r)">
|
|
1586
|
+
<rect width="${labelWidth}" height="20" fill="#555"/>
|
|
1587
|
+
<rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color.hex}"/>
|
|
1588
|
+
<rect width="${totalWidth}" height="20" fill="url(#s)"/>
|
|
1589
|
+
</g>
|
|
1590
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
|
|
1591
|
+
<text aria-hidden="true" x="${labelX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${(labelWidth - 14) * 10}">${label}</text>
|
|
1592
|
+
<text x="${labelX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${(labelWidth - 14) * 10}">${label}</text>
|
|
1593
|
+
<text aria-hidden="true" x="${valueX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${(valueWidth - 14) * 10}">${value}</text>
|
|
1594
|
+
<text x="${valueX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${(valueWidth - 14) * 10}">${value}</text>
|
|
1595
|
+
</g>
|
|
1596
|
+
</svg>`;
|
|
1597
|
+
}
|
|
1598
|
+
function generateShieldsMarkdown(score, label = "AI Discipline", repoUrl = "https://github.com/Nari-Ab/agent-roast") {
|
|
1599
|
+
const value = score !== null && !isNaN(score) ? `${score}%2F100` : "N%2FA";
|
|
1600
|
+
const color = getScoreColor(score);
|
|
1601
|
+
const encodedLabel = encodeURIComponent(label);
|
|
1602
|
+
const badgeUrl = `https://img.shields.io/badge/${encodedLabel}-${value}-${color.name}?style=flat-square`;
|
|
1603
|
+
return `[](${repoUrl})`;
|
|
1604
|
+
}
|
|
1605
|
+
function generateShieldsJson(score, label = "AI Discipline") {
|
|
1606
|
+
const value = score !== null && !isNaN(score) ? `${score}/100` : "N/A";
|
|
1607
|
+
const color = getScoreColor(score);
|
|
1608
|
+
return {
|
|
1609
|
+
schemaVersion: 1,
|
|
1610
|
+
label,
|
|
1611
|
+
message: value,
|
|
1612
|
+
color: color.name
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1201
1616
|
// src/index.ts
|
|
1202
1617
|
var InvalidDetectorError = class extends Error {
|
|
1203
1618
|
constructor(message) {
|
|
@@ -1206,10 +1621,10 @@ var InvalidDetectorError = class extends Error {
|
|
|
1206
1621
|
}
|
|
1207
1622
|
};
|
|
1208
1623
|
async function roastRepository(options = {}) {
|
|
1209
|
-
const cwd =
|
|
1624
|
+
const cwd = path3.resolve(options.cwd || process.cwd());
|
|
1210
1625
|
const since = options.since || "90 days ago";
|
|
1211
|
-
const repoName =
|
|
1212
|
-
const collector = new MetricsCollector({ all: options.all });
|
|
1626
|
+
const repoName = path3.basename(cwd);
|
|
1627
|
+
const collector = new MetricsCollector({ all: options.all, ignorePaths: options.ignorePaths });
|
|
1213
1628
|
const gitStream = streamGitLog(cwd, since);
|
|
1214
1629
|
await parseGitLogStream(gitStream, (commit, stats) => {
|
|
1215
1630
|
collector.processCommit(commit);
|
|
@@ -1223,7 +1638,7 @@ async function roastRepository(options = {}) {
|
|
|
1223
1638
|
return { summary, output };
|
|
1224
1639
|
}
|
|
1225
1640
|
async function auditPullRequest(options) {
|
|
1226
|
-
const cwd =
|
|
1641
|
+
const cwd = path3.resolve(options.cwd || process.cwd());
|
|
1227
1642
|
const format = options.format || "terminal";
|
|
1228
1643
|
const failOn = (options.failOn || ["test-skip"]).map((s) => s.trim()).filter(Boolean);
|
|
1229
1644
|
for (const det of failOn) {
|
|
@@ -1238,15 +1653,15 @@ async function auditPullRequest(options) {
|
|
|
1238
1653
|
const mergeBaseSha = findMergeBase(cwd, baseSha, headSha);
|
|
1239
1654
|
const diffStream = streamGitDiff(cwd, mergeBaseSha, headSha);
|
|
1240
1655
|
let rawDiff = "";
|
|
1241
|
-
await new Promise((
|
|
1656
|
+
await new Promise((resolve2, reject) => {
|
|
1242
1657
|
diffStream.on("data", (chunk) => {
|
|
1243
1658
|
rawDiff += chunk.toString();
|
|
1244
1659
|
});
|
|
1245
|
-
diffStream.on("end", () =>
|
|
1660
|
+
diffStream.on("end", () => resolve2());
|
|
1246
1661
|
diffStream.on("error", reject);
|
|
1247
1662
|
});
|
|
1248
1663
|
const { files, skippedLongLines, skippedUnknownFiles } = parseDiff(rawDiff);
|
|
1249
|
-
const audit = auditFileDiffs(files, headSha);
|
|
1664
|
+
const audit = auditFileDiffs(files, headSha, (/* @__PURE__ */ new Date()).toISOString(), { ignorePaths: options.ignorePaths });
|
|
1250
1665
|
const infractions = audit.infractions;
|
|
1251
1666
|
const defectDensityPer100Lines = audit.linesAdded > 0 ? Math.round(infractions.length / audit.linesAdded * 100 * 100) / 100 : 0;
|
|
1252
1667
|
const failedRules = [...new Set(
|
|
@@ -1275,6 +1690,8 @@ async function auditPullRequest(options) {
|
|
|
1275
1690
|
result.output = JSON.stringify(result, null, 2);
|
|
1276
1691
|
} else if (format === "github") {
|
|
1277
1692
|
result.output = formatGithubAnnotations(result);
|
|
1693
|
+
} else if (format === "markdown") {
|
|
1694
|
+
result.output = formatPrMarkdownComment(result);
|
|
1278
1695
|
} else {
|
|
1279
1696
|
result.output = formatPrTerminalCard(result);
|
|
1280
1697
|
}
|
|
@@ -1296,7 +1713,11 @@ export {
|
|
|
1296
1713
|
resolveCommitSha,
|
|
1297
1714
|
findMergeBase,
|
|
1298
1715
|
GitResolutionError,
|
|
1716
|
+
DEFAULT_CONFIG,
|
|
1717
|
+
loadConfig,
|
|
1718
|
+
matchIgnorePath,
|
|
1299
1719
|
isCodeFile,
|
|
1720
|
+
isTestFile,
|
|
1300
1721
|
auditFileDiffs,
|
|
1301
1722
|
stripStringAndRegexLiterals,
|
|
1302
1723
|
MetricsCollector,
|
|
@@ -1305,7 +1726,16 @@ export {
|
|
|
1305
1726
|
formatPrTerminalCard,
|
|
1306
1727
|
formatGithubAnnotations,
|
|
1307
1728
|
formatGithubStepSummary,
|
|
1729
|
+
formatPrMarkdownComment,
|
|
1308
1730
|
VALID_DETECTORS,
|
|
1731
|
+
DEFAULT_CONFIG_TEMPLATE,
|
|
1732
|
+
GITHUB_WORKFLOW_TEMPLATE,
|
|
1733
|
+
PRE_COMMIT_HOOK_TEMPLATE,
|
|
1734
|
+
initProject,
|
|
1735
|
+
getScoreColor,
|
|
1736
|
+
generateSvgBadge,
|
|
1737
|
+
generateShieldsMarkdown,
|
|
1738
|
+
generateShieldsJson,
|
|
1309
1739
|
InvalidDetectorError,
|
|
1310
1740
|
roastRepository,
|
|
1311
1741
|
auditPullRequest
|
package/dist/cli.js
CHANGED
|
@@ -5,25 +5,38 @@ import {
|
|
|
5
5
|
InvalidDetectorError,
|
|
6
6
|
auditPullRequest,
|
|
7
7
|
formatGithubStepSummary,
|
|
8
|
+
generateShieldsJson,
|
|
9
|
+
generateShieldsMarkdown,
|
|
10
|
+
generateSvgBadge,
|
|
11
|
+
initProject,
|
|
12
|
+
loadConfig,
|
|
8
13
|
roastRepository
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-O4SB2FCT.js";
|
|
10
15
|
|
|
11
16
|
// src/cli.ts
|
|
12
17
|
import { Command } from "commander";
|
|
13
18
|
import { createRequire } from "module";
|
|
14
19
|
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
15
21
|
var require2 = createRequire(import.meta.url);
|
|
16
22
|
var pkg = require2("../package.json");
|
|
17
23
|
var program = new Command();
|
|
18
|
-
program.name("agent-roast").description("Audit your git history or PR diffs for AI coding shortcuts and panic loops").version(pkg.version || "0.
|
|
24
|
+
program.name("agent-roast").description("Audit your git history or PR diffs for AI coding shortcuts and panic loops").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("--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("--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", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
|
|
19
25
|
try {
|
|
20
26
|
if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
|
|
21
27
|
console.warn(
|
|
22
28
|
"Warning: pull_request_target runs with elevated repository privileges. agent-roast recommends running on 'pull_request'."
|
|
23
29
|
);
|
|
24
30
|
}
|
|
31
|
+
const config = await loadConfig(repoPath);
|
|
25
32
|
if (options.base) {
|
|
26
|
-
|
|
33
|
+
let failOnRules;
|
|
34
|
+
if (options.failOn) {
|
|
35
|
+
failOnRules = options.failOn.split(",").map((s) => s.trim()).filter(Boolean);
|
|
36
|
+
} else {
|
|
37
|
+
const errorRules = Object.entries(config.rules || {}).filter(([_, level]) => level === "error").map(([rule]) => rule);
|
|
38
|
+
failOnRules = errorRules.length > 0 ? errorRules : ["test-skip"];
|
|
39
|
+
}
|
|
27
40
|
const chosenFormat = options.json ? "json" : options.format || "terminal";
|
|
28
41
|
const prResult = await auditPullRequest({
|
|
29
42
|
cwd: repoPath,
|
|
@@ -32,7 +45,8 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
|
|
|
32
45
|
failOn: failOnRules,
|
|
33
46
|
format: chosenFormat,
|
|
34
47
|
verbose: options.verbose,
|
|
35
|
-
authors: options.authors
|
|
48
|
+
authors: options.authors,
|
|
49
|
+
ignorePaths: config.ignorePaths
|
|
36
50
|
});
|
|
37
51
|
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
38
52
|
try {
|
|
@@ -65,14 +79,20 @@ has-supported-changes=${prResult.hasSupportedChanges}
|
|
|
65
79
|
since: options.since,
|
|
66
80
|
all: options.all,
|
|
67
81
|
verbose: options.verbose,
|
|
68
|
-
json: options.json
|
|
82
|
+
json: options.json,
|
|
83
|
+
ignorePaths: config.ignorePaths
|
|
69
84
|
});
|
|
70
85
|
if (options.json) {
|
|
71
86
|
console.log(JSON.stringify(result.summary, null, 2));
|
|
72
87
|
} else {
|
|
73
88
|
console.log(result.output);
|
|
74
89
|
}
|
|
75
|
-
|
|
90
|
+
const scoreThreshold = options.failUnder !== void 0 ? Number(options.failUnder) : config.scoreThreshold;
|
|
91
|
+
if (scoreThreshold !== void 0 && !isNaN(scoreThreshold) && result.summary.score !== null && result.summary.score < scoreThreshold) {
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
} else {
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
}
|
|
76
96
|
}
|
|
77
97
|
} catch (err) {
|
|
78
98
|
if (err instanceof GitResolutionError || err instanceof InvalidDetectorError || err instanceof GitError || err.name === "GitError") {
|
|
@@ -84,4 +104,73 @@ has-supported-changes=${prResult.hasSupportedChanges}
|
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
});
|
|
107
|
+
program.command("init").description("Initialize .agentroastrc.json and CI/hook configurations").argument("[dir]", "Directory to initialize", ".").option("-y, --yes", "Skip interactive prompts and use defaults", false).option("--ci", "Generate GitHub Actions workflow file", false).option("--hook", "Install Git pre-commit hook", false).action(async (targetDir, opts) => {
|
|
108
|
+
try {
|
|
109
|
+
const res = await initProject({
|
|
110
|
+
cwd: targetDir,
|
|
111
|
+
yes: opts.yes,
|
|
112
|
+
ci: opts.ci,
|
|
113
|
+
hook: opts.hook
|
|
114
|
+
});
|
|
115
|
+
if (res.created.length > 0) {
|
|
116
|
+
console.log(`Initialized agent-roast in ${targetDir}:`);
|
|
117
|
+
for (const file of res.created) {
|
|
118
|
+
console.log(` + Created ${file}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (res.skipped.length > 0) {
|
|
122
|
+
for (const file of res.skipped) {
|
|
123
|
+
console.log(` - Skipped ${file} (already exists or not applicable)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (res.created.length === 0 && res.skipped.length === 0) {
|
|
127
|
+
console.log("No files were created.");
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`Failed to initialize agent-roast: ${err.message}`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
program.command("badge").description("Generate an AI discipline badge (SVG, Markdown, or Shields JSON)").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("--label <text>", "Badge label text", "AI Discipline").option("--out <file>", "Output SVG file path (e.g. ./badge.svg)").action(async (repoPath, opts) => {
|
|
135
|
+
try {
|
|
136
|
+
const rootOpts = program.opts();
|
|
137
|
+
const isJson = Boolean(opts.json || rootOpts.json);
|
|
138
|
+
const isAll = Boolean(opts.all || rootOpts.all);
|
|
139
|
+
const since = opts.since || rootOpts.since || "90 days ago";
|
|
140
|
+
const config = await loadConfig(repoPath);
|
|
141
|
+
const result = await roastRepository({
|
|
142
|
+
cwd: repoPath,
|
|
143
|
+
since,
|
|
144
|
+
all: isAll,
|
|
145
|
+
ignorePaths: config.ignorePaths
|
|
146
|
+
});
|
|
147
|
+
const score = result.summary.score;
|
|
148
|
+
const label = opts.label || "AI Discipline";
|
|
149
|
+
if (isJson) {
|
|
150
|
+
const payload = generateShieldsJson(score, label);
|
|
151
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
152
|
+
process.exitCode = 0;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (opts.out) {
|
|
156
|
+
const svg = generateSvgBadge(score, label);
|
|
157
|
+
const outPath = path.resolve(opts.out);
|
|
158
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
159
|
+
fs.writeFileSync(outPath, svg, "utf-8");
|
|
160
|
+
console.log(`Generated SVG badge at: ${opts.out}`);
|
|
161
|
+
process.exitCode = 0;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const markdown = generateShieldsMarkdown(score, label);
|
|
165
|
+
console.log(`
|
|
166
|
+
Badge for README.md:
|
|
167
|
+
|
|
168
|
+
${markdown}
|
|
169
|
+
`);
|
|
170
|
+
process.exitCode = 0;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`Error generating badge: ${err.message}`);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
87
176
|
program.parse(process.argv);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface CommitInfo {
|
|
|
9
9
|
isAiAttributed: boolean;
|
|
10
10
|
aiSignatures: string[];
|
|
11
11
|
}
|
|
12
|
-
type DetectorType = "test-skip" | "type-escape" | "swallowed-error" | "panic-loop";
|
|
12
|
+
type DetectorType = "test-skip" | "type-escape" | "swallowed-error" | "panic-loop" | "trivial-assert" | "stub-abandonment";
|
|
13
13
|
declare const VALID_DETECTORS: readonly DetectorType[];
|
|
14
14
|
interface Infraction {
|
|
15
15
|
type: DetectorType;
|
|
@@ -30,6 +30,8 @@ interface MetricSummary {
|
|
|
30
30
|
typeEscapes: Infraction[];
|
|
31
31
|
swallowedErrors: Infraction[];
|
|
32
32
|
panicLoops: Infraction[];
|
|
33
|
+
trivialAsserts: Infraction[];
|
|
34
|
+
stubAbandonments: Infraction[];
|
|
33
35
|
rawScore: number;
|
|
34
36
|
score: number | null;
|
|
35
37
|
isSufficientData: boolean;
|
|
@@ -45,15 +47,18 @@ interface RoastOptions {
|
|
|
45
47
|
verbose?: boolean;
|
|
46
48
|
json?: boolean;
|
|
47
49
|
authors?: boolean;
|
|
50
|
+
ignorePaths?: string[];
|
|
51
|
+
scoreThreshold?: number;
|
|
48
52
|
}
|
|
49
53
|
interface PrAuditOptions {
|
|
50
54
|
cwd?: string;
|
|
51
55
|
base: string;
|
|
52
56
|
head: string;
|
|
53
57
|
failOn?: string[];
|
|
54
|
-
format?: "terminal" | "json" | "github";
|
|
58
|
+
format?: "terminal" | "json" | "github" | "markdown";
|
|
55
59
|
verbose?: boolean;
|
|
56
60
|
authors?: boolean;
|
|
61
|
+
ignorePaths?: string[];
|
|
57
62
|
}
|
|
58
63
|
interface PrAuditResult {
|
|
59
64
|
schemaVersion: string;
|
|
@@ -139,11 +144,14 @@ declare class GitResolutionError extends Error {
|
|
|
139
144
|
constructor(message: string);
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
declare function isCodeFile(filePath: string): boolean;
|
|
147
|
+
declare function isCodeFile(filePath: string, customIgnorePaths?: string[]): boolean;
|
|
148
|
+
declare function isTestFile(filePath: string): boolean;
|
|
143
149
|
/**
|
|
144
150
|
* Audits a collection of file diffs (e.g. for PR mode) against detector rules.
|
|
145
151
|
*/
|
|
146
|
-
declare function auditFileDiffs(files: ParsedFileDiff[], commitHash?: string, commitDate?: string
|
|
152
|
+
declare function auditFileDiffs(files: ParsedFileDiff[], commitHash?: string, commitDate?: string, options?: {
|
|
153
|
+
ignorePaths?: string[];
|
|
154
|
+
}): {
|
|
147
155
|
infractions: Infraction[];
|
|
148
156
|
hasSupportedChanges: boolean;
|
|
149
157
|
filesInspected: number;
|
|
@@ -163,12 +171,16 @@ declare class MetricsCollector {
|
|
|
163
171
|
private typeEscapes;
|
|
164
172
|
private swallowedErrors;
|
|
165
173
|
private panicLoops;
|
|
174
|
+
private trivialAsserts;
|
|
175
|
+
private stubAbandonments;
|
|
166
176
|
private skippedLongLines;
|
|
167
177
|
private skippedUnknownFiles;
|
|
168
178
|
private activePanicChain;
|
|
169
179
|
private includeAll;
|
|
180
|
+
private ignorePaths?;
|
|
170
181
|
constructor(options?: {
|
|
171
182
|
all?: boolean;
|
|
183
|
+
ignorePaths?: string[];
|
|
172
184
|
});
|
|
173
185
|
recordSkipped(stats: {
|
|
174
186
|
skippedLongLines?: number;
|
|
@@ -194,6 +206,73 @@ declare function formatGithubAnnotations(result: PrAuditResult): string;
|
|
|
194
206
|
* Generates GitHub Step Summary Markdown
|
|
195
207
|
*/
|
|
196
208
|
declare function formatGithubStepSummary(result: PrAuditResult): string;
|
|
209
|
+
/**
|
|
210
|
+
* Generates rich GitHub PR Comment Markdown (for PR comments / sticky bots)
|
|
211
|
+
*/
|
|
212
|
+
declare function formatPrMarkdownComment(result: PrAuditResult): string;
|
|
213
|
+
|
|
214
|
+
interface AgentRoastConfig {
|
|
215
|
+
/** Detector rules to fail CI on in PR mode */
|
|
216
|
+
failOn?: string[];
|
|
217
|
+
/** Glob patterns or prefixes to ignore from analysis */
|
|
218
|
+
ignorePaths?: string[];
|
|
219
|
+
/** Minimum passing score for full repository audits (0-100) */
|
|
220
|
+
scoreThreshold?: number;
|
|
221
|
+
/** Granular rule configuration (error | warn | off) */
|
|
222
|
+
rules?: Record<string, "error" | "warn" | "off">;
|
|
223
|
+
/** Maximum minutes between fix commits before flagging panic-loop */
|
|
224
|
+
panicLoopWindowMinutes?: number;
|
|
225
|
+
}
|
|
226
|
+
declare const DEFAULT_CONFIG: AgentRoastConfig;
|
|
227
|
+
/**
|
|
228
|
+
* Discovers and parses repository configuration file.
|
|
229
|
+
* Returns DEFAULT_CONFIG if no configuration file is found or if parsing fails.
|
|
230
|
+
*/
|
|
231
|
+
declare function loadConfig(cwd?: string): AgentRoastConfig;
|
|
232
|
+
/**
|
|
233
|
+
* Matches a relative or normalized file path against an array of glob ignore patterns.
|
|
234
|
+
*/
|
|
235
|
+
declare function matchIgnorePath(filePath: string, ignorePatterns?: string[]): boolean;
|
|
236
|
+
|
|
237
|
+
interface InitOptions {
|
|
238
|
+
cwd?: string;
|
|
239
|
+
yes?: boolean;
|
|
240
|
+
ci?: boolean;
|
|
241
|
+
hook?: boolean;
|
|
242
|
+
}
|
|
243
|
+
interface InitResult {
|
|
244
|
+
created: string[];
|
|
245
|
+
skipped: string[];
|
|
246
|
+
}
|
|
247
|
+
declare const DEFAULT_CONFIG_TEMPLATE: {
|
|
248
|
+
$schema: string;
|
|
249
|
+
ignorePaths: string[];
|
|
250
|
+
rules: {
|
|
251
|
+
"hallucinated-package": string;
|
|
252
|
+
"empty-catch": string;
|
|
253
|
+
"trivial-assert": string;
|
|
254
|
+
"stub-abandonment": string;
|
|
255
|
+
"mock-leak": string;
|
|
256
|
+
};
|
|
257
|
+
scoreThreshold: number;
|
|
258
|
+
};
|
|
259
|
+
declare const GITHUB_WORKFLOW_TEMPLATE = "name: Agent Roast\non:\n pull_request:\n branches: [main, master]\n push:\n branches: [main, master]\n\njobs:\n audit:\n name: Audit AI Shortcuts\n runs-on: ubuntu-latest\n steps:\n - name: Checkout repository\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: 20\n\n - name: Run agent-roast PR check\n if: github.event_name == 'pull_request'\n run: npx agent-roast --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }} --format github\n\n - name: Run agent-roast repo audit\n if: github.event_name != 'pull_request'\n run: npx agent-roast --fail-under 70\n";
|
|
260
|
+
declare const PRE_COMMIT_HOOK_TEMPLATE = "#!/bin/sh\n# Run agent-roast pre-commit verification\nnpx agent-roast --fail-under 70\n";
|
|
261
|
+
declare function initProject(options?: InitOptions): Promise<InitResult>;
|
|
262
|
+
|
|
263
|
+
interface ShieldsEndpointResponse {
|
|
264
|
+
schemaVersion: 1;
|
|
265
|
+
label: string;
|
|
266
|
+
message: string;
|
|
267
|
+
color: string;
|
|
268
|
+
}
|
|
269
|
+
declare function getScoreColor(score: number | null): {
|
|
270
|
+
name: string;
|
|
271
|
+
hex: string;
|
|
272
|
+
};
|
|
273
|
+
declare function generateSvgBadge(score: number | null, label?: string): string;
|
|
274
|
+
declare function generateShieldsMarkdown(score: number | null, label?: string, repoUrl?: string): string;
|
|
275
|
+
declare function generateShieldsJson(score: number | null, label?: string): ShieldsEndpointResponse;
|
|
197
276
|
|
|
198
277
|
declare class InvalidDetectorError extends Error {
|
|
199
278
|
constructor(message: string);
|
|
@@ -204,4 +283,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
|
|
|
204
283
|
}>;
|
|
205
284
|
declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
|
|
206
285
|
|
|
207
|
-
export { type AiAttributionResult, type CommitInfo, type DetectorType, FIELD_SEPARATOR, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, type Infraction, InvalidDetectorError, type MetricSummary, MetricsCollector, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, type RoastOptions, VALID_DETECTORS, auditFileDiffs, auditPullRequest, detectAiAttribution, findMergeBase, formatGithubAnnotations, formatGithubStepSummary, formatPrTerminalCard, formatTerminalCard, generateShareUrl, getHermeticGitEnv, isCodeFile, parseDiff, parseGitLogStream, resolveCommitSha, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath };
|
|
286
|
+
export { type AgentRoastConfig, type AiAttributionResult, type CommitInfo, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, type RoastOptions, type ShieldsEndpointResponse, VALID_DETECTORS, auditFileDiffs, auditPullRequest, detectAiAttribution, findMergeBase, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, initProject, isCodeFile, isTestFile, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_CONFIG,
|
|
4
|
+
DEFAULT_CONFIG_TEMPLATE,
|
|
3
5
|
FIELD_SEPARATOR,
|
|
6
|
+
GITHUB_WORKFLOW_TEMPLATE,
|
|
4
7
|
GitError,
|
|
5
8
|
GitResolutionError,
|
|
6
9
|
HERMETIC_GIT_FLAGS,
|
|
7
10
|
InvalidDetectorError,
|
|
8
11
|
MetricsCollector,
|
|
12
|
+
PRE_COMMIT_HOOK_TEMPLATE,
|
|
9
13
|
RECORD_SEPARATOR,
|
|
10
14
|
VALID_DETECTORS,
|
|
11
15
|
auditFileDiffs,
|
|
@@ -14,11 +18,20 @@ import {
|
|
|
14
18
|
findMergeBase,
|
|
15
19
|
formatGithubAnnotations,
|
|
16
20
|
formatGithubStepSummary,
|
|
21
|
+
formatPrMarkdownComment,
|
|
17
22
|
formatPrTerminalCard,
|
|
18
23
|
formatTerminalCard,
|
|
19
24
|
generateShareUrl,
|
|
25
|
+
generateShieldsJson,
|
|
26
|
+
generateShieldsMarkdown,
|
|
27
|
+
generateSvgBadge,
|
|
20
28
|
getHermeticGitEnv,
|
|
29
|
+
getScoreColor,
|
|
30
|
+
initProject,
|
|
21
31
|
isCodeFile,
|
|
32
|
+
isTestFile,
|
|
33
|
+
loadConfig,
|
|
34
|
+
matchIgnorePath,
|
|
22
35
|
parseDiff,
|
|
23
36
|
parseGitLogStream,
|
|
24
37
|
resolveCommitSha,
|
|
@@ -27,14 +40,18 @@ import {
|
|
|
27
40
|
streamGitLog,
|
|
28
41
|
stripStringAndRegexLiterals,
|
|
29
42
|
unquoteGitPath
|
|
30
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-O4SB2FCT.js";
|
|
31
44
|
export {
|
|
45
|
+
DEFAULT_CONFIG,
|
|
46
|
+
DEFAULT_CONFIG_TEMPLATE,
|
|
32
47
|
FIELD_SEPARATOR,
|
|
48
|
+
GITHUB_WORKFLOW_TEMPLATE,
|
|
33
49
|
GitError,
|
|
34
50
|
GitResolutionError,
|
|
35
51
|
HERMETIC_GIT_FLAGS,
|
|
36
52
|
InvalidDetectorError,
|
|
37
53
|
MetricsCollector,
|
|
54
|
+
PRE_COMMIT_HOOK_TEMPLATE,
|
|
38
55
|
RECORD_SEPARATOR,
|
|
39
56
|
VALID_DETECTORS,
|
|
40
57
|
auditFileDiffs,
|
|
@@ -43,11 +60,20 @@ export {
|
|
|
43
60
|
findMergeBase,
|
|
44
61
|
formatGithubAnnotations,
|
|
45
62
|
formatGithubStepSummary,
|
|
63
|
+
formatPrMarkdownComment,
|
|
46
64
|
formatPrTerminalCard,
|
|
47
65
|
formatTerminalCard,
|
|
48
66
|
generateShareUrl,
|
|
67
|
+
generateShieldsJson,
|
|
68
|
+
generateShieldsMarkdown,
|
|
69
|
+
generateSvgBadge,
|
|
49
70
|
getHermeticGitEnv,
|
|
71
|
+
getScoreColor,
|
|
72
|
+
initProject,
|
|
50
73
|
isCodeFile,
|
|
74
|
+
isTestFile,
|
|
75
|
+
loadConfig,
|
|
76
|
+
matchIgnorePath,
|
|
51
77
|
parseDiff,
|
|
52
78
|
parseGitLogStream,
|
|
53
79
|
resolveCommitSha,
|