@inceptionstack/pi-hard-no 1.2.1 → 1.3.1
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/dismiss.test.ts +20 -13
- package/dismiss.ts +18 -14
- package/index.ts +3 -1
- package/judge.ts +47 -0
- package/orchestrator.ts +5 -1
- package/package.json +1 -1
package/dismiss.test.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
findingKey,
|
|
4
|
+
numberFindings,
|
|
5
|
+
parseDismissals,
|
|
6
|
+
filterSuppressed,
|
|
7
|
+
DismissTracker,
|
|
8
|
+
} from "./dismiss";
|
|
3
9
|
|
|
4
10
|
describe("findingKey", () => {
|
|
5
11
|
it("extracts severity + location from finding line", () => {
|
|
6
|
-
const line =
|
|
12
|
+
const line = "- **Medium:** src/gateway/model.ts:97 — chatId extraction uses wrong index";
|
|
7
13
|
const key = findingKey(line);
|
|
8
14
|
expect(key).toContain("medium:src/gateway/model.ts:97");
|
|
9
15
|
});
|
|
10
16
|
|
|
11
17
|
it("handles numbered finding format (F# prefix)", () => {
|
|
12
|
-
const line =
|
|
18
|
+
const line = "- **F1 Medium:** src/foo.ts:10 — something bad";
|
|
13
19
|
const key = findingKey(line);
|
|
14
20
|
expect(key).toBe("medium:src/foo.ts:10 — something bad");
|
|
15
21
|
});
|
|
@@ -22,7 +28,7 @@ describe("findingKey", () => {
|
|
|
22
28
|
|
|
23
29
|
describe("numberFindings", () => {
|
|
24
30
|
it("numbers finding bullets sequentially", () => {
|
|
25
|
-
const text =
|
|
31
|
+
const text = "- **High:** foo.ts:1 — bug\n- **Low:** bar.ts:2 — nit\nSome other text";
|
|
26
32
|
const { numbered, findings } = numberFindings(text);
|
|
27
33
|
expect(numbered).toContain("**F1 High:**");
|
|
28
34
|
expect(numbered).toContain("**F2 Low:**");
|
|
@@ -31,7 +37,7 @@ describe("numberFindings", () => {
|
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
it("preserves non-finding lines unchanged", () => {
|
|
34
|
-
const text =
|
|
40
|
+
const text = "Header\n\n- **Medium:** x.ts:5 — issue\n\nFooter";
|
|
35
41
|
const { numbered } = numberFindings(text);
|
|
36
42
|
expect(numbered).toContain("Header");
|
|
37
43
|
expect(numbered).toContain("Footer");
|
|
@@ -41,7 +47,8 @@ describe("numberFindings", () => {
|
|
|
41
47
|
|
|
42
48
|
describe("parseDismissals", () => {
|
|
43
49
|
it("parses DISMISS F# with colon separator", () => {
|
|
44
|
-
const text =
|
|
50
|
+
const text =
|
|
51
|
+
"The chatId extraction is intentional.\nDISMISS F1: intentional design for telegram thread format";
|
|
45
52
|
const dismissals = parseDismissals(text);
|
|
46
53
|
expect(dismissals.size).toBe(1);
|
|
47
54
|
expect(dismissals.get(1)).toBe("intentional design for telegram thread format");
|
|
@@ -69,21 +76,21 @@ describe("parseDismissals", () => {
|
|
|
69
76
|
|
|
70
77
|
describe("filterSuppressed", () => {
|
|
71
78
|
it("removes suppressed findings", () => {
|
|
72
|
-
const text =
|
|
73
|
-
const suppressed = new Set([findingKey(
|
|
79
|
+
const text = "- **High:** foo.ts:1 — bug one\n- **Low:** bar.ts:2 — nit two";
|
|
80
|
+
const suppressed = new Set([findingKey("- **High:** foo.ts:1 — bug one")]);
|
|
74
81
|
const result = filterSuppressed(text, suppressed);
|
|
75
82
|
expect(result).not.toContain("bug one");
|
|
76
83
|
expect(result).toContain("nit two");
|
|
77
84
|
});
|
|
78
85
|
|
|
79
86
|
it("returns null when all findings suppressed", () => {
|
|
80
|
-
const text =
|
|
81
|
-
const suppressed = new Set([findingKey(
|
|
87
|
+
const text = "- **High:** foo.ts:1 — bug one";
|
|
88
|
+
const suppressed = new Set([findingKey("- **High:** foo.ts:1 — bug one")]);
|
|
82
89
|
expect(filterSuppressed(text, suppressed)).toBeNull();
|
|
83
90
|
});
|
|
84
91
|
|
|
85
92
|
it("returns original when no suppressions", () => {
|
|
86
|
-
const text =
|
|
93
|
+
const text = "- **Low:** x.ts:5 — something";
|
|
87
94
|
expect(filterSuppressed(text, new Set())).toBe(text);
|
|
88
95
|
});
|
|
89
96
|
});
|
|
@@ -91,7 +98,7 @@ describe("filterSuppressed", () => {
|
|
|
91
98
|
describe("DismissTracker", () => {
|
|
92
99
|
it("tracks dismissals and suppresses after threshold", () => {
|
|
93
100
|
const tracker = new DismissTracker();
|
|
94
|
-
const findings = [
|
|
101
|
+
const findings = ["- **Medium:** src/foo.ts:10 — bad pattern", "- **Low:** src/bar.ts:5 — nit"];
|
|
95
102
|
tracker.setLastFindings(findings);
|
|
96
103
|
|
|
97
104
|
// First dismiss
|
|
@@ -106,7 +113,7 @@ describe("DismissTracker", () => {
|
|
|
106
113
|
|
|
107
114
|
it("reset clears all state", () => {
|
|
108
115
|
const tracker = new DismissTracker();
|
|
109
|
-
tracker.setLastFindings([
|
|
116
|
+
tracker.setLastFindings(["- **High:** x.ts:1 — bug"]);
|
|
110
117
|
tracker.processDismissals("DISMISS F1: nope");
|
|
111
118
|
tracker.processDismissals("DISMISS F1: nope again");
|
|
112
119
|
expect(tracker.getSuppressed().size).toBe(1);
|
package/dismiss.ts
CHANGED
|
@@ -35,16 +35,18 @@ export function numberFindings(text: string): { numbered: string; findings: stri
|
|
|
35
35
|
const findings: string[] = [];
|
|
36
36
|
let counter = 0;
|
|
37
37
|
|
|
38
|
-
const numbered = lines
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
const numbered = lines
|
|
39
|
+
.map((line) => {
|
|
40
|
+
// Match finding bullets: - **Severity:** ...
|
|
41
|
+
const match = line.match(/^(\s*-\s*)\*\*(\w+):\*\*(.*)$/);
|
|
42
|
+
if (match) {
|
|
43
|
+
counter++;
|
|
44
|
+
findings.push(line);
|
|
45
|
+
return `${match[1]}**F${counter} ${match[2]}:**${match[3]}`;
|
|
46
|
+
}
|
|
47
|
+
return line;
|
|
48
|
+
})
|
|
49
|
+
.join("\n");
|
|
48
50
|
|
|
49
51
|
return { numbered, findings };
|
|
50
52
|
}
|
|
@@ -53,7 +55,7 @@ export function numberFindings(text: string): { numbered: string; findings: stri
|
|
|
53
55
|
export function parseDismissals(text: string): Map<number, string> {
|
|
54
56
|
const dismissals = new Map<number, string>();
|
|
55
57
|
// Match: DISMISS F1: reason or DISMISS F1 - reason or DISMISS F1 reason
|
|
56
|
-
const pattern = /DISMISS\s+F(\d+)\s*[
|
|
58
|
+
const pattern = /DISMISS\s+F(\d+)\s*[:–-]\s*(.+)/gi;
|
|
57
59
|
let match;
|
|
58
60
|
while ((match = pattern.exec(text)) !== null) {
|
|
59
61
|
dismissals.set(parseInt(match[1], 10), match[2].trim());
|
|
@@ -66,7 +68,7 @@ export function filterSuppressed(text: string, suppressed: Set<string>): string
|
|
|
66
68
|
if (suppressed.size === 0) return text;
|
|
67
69
|
|
|
68
70
|
const lines = text.split("\n");
|
|
69
|
-
const filtered = lines.filter(line => {
|
|
71
|
+
const filtered = lines.filter((line) => {
|
|
70
72
|
const match = line.match(/^\s*-\s*\*\*\w+:\*\*/);
|
|
71
73
|
if (!match) return true; // keep non-finding lines
|
|
72
74
|
const key = findingKey(line);
|
|
@@ -74,7 +76,7 @@ export function filterSuppressed(text: string, suppressed: Set<string>): string
|
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
// If all findings were suppressed, return null (should be LGTM)
|
|
77
|
-
const remaining = filtered.filter(l => l.match(/^\s*-\s*\*\*/));
|
|
79
|
+
const remaining = filtered.filter((l) => l.match(/^\s*-\s*\*\*/));
|
|
78
80
|
if (remaining.length === 0) return null;
|
|
79
81
|
|
|
80
82
|
return filtered.join("\n");
|
|
@@ -112,7 +114,9 @@ export class DismissTracker {
|
|
|
112
114
|
this.dismissed.set(key, { key, reason, count: 1 });
|
|
113
115
|
}
|
|
114
116
|
count++;
|
|
115
|
-
log(
|
|
117
|
+
log(
|
|
118
|
+
`dismiss: F${fNum} dismissed (${key}) — "${reason}" [count=${this.dismissed.get(key)!.count}]`,
|
|
119
|
+
);
|
|
116
120
|
}
|
|
117
121
|
return count;
|
|
118
122
|
}
|
package/index.ts
CHANGED
|
@@ -706,7 +706,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
706
706
|
|
|
707
707
|
// Process DISMISS markers from agent's response (before running review)
|
|
708
708
|
if (lastAssistant) {
|
|
709
|
-
const textParts = (lastAssistant.content ?? [])
|
|
709
|
+
const textParts = (lastAssistant.content ?? [])
|
|
710
|
+
.filter((b: any) => b.type === "text")
|
|
711
|
+
.map((b: any) => b.text);
|
|
710
712
|
const agentText = textParts.join("\n");
|
|
711
713
|
if (agentText) {
|
|
712
714
|
orchestrator.processDismissals(agentText);
|
package/judge.ts
CHANGED
|
@@ -66,6 +66,7 @@ TAXONOMY (authoritative):
|
|
|
66
66
|
- npm/pnpm/yarn/pip/cargo install, make, cargo build, npm run format, codegen scripts → modifying
|
|
67
67
|
- kill/pkill/systemctl, docker run, docker compose up → modifying
|
|
68
68
|
- sed -i, perl -pi → modifying (in-place edit)
|
|
69
|
+
- perl -e, python -c, node -e, ruby -e (one-liners) → unsure (static analysis cannot determine side effects; caught by detectSubprocessWrapper pre-check)
|
|
69
70
|
- ./script.sh or npm run <unknown> → unsure unless clearly read-only
|
|
70
71
|
- truncated command (e.g. "git commi") → unsure
|
|
71
72
|
- Compound commands with &&, ;, ||, pipes, subshells: ANY modifying part → modifying; ANY unknown/truncated → unsure; otherwise the class of the safest-subset.
|
|
@@ -101,9 +102,47 @@ export function parseJudgeResponse(raw: string): BashClassification {
|
|
|
101
102
|
return "unsure";
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Detect subprocess wrappers (perl -e, python -c, node -e, ruby -e) that cannot
|
|
107
|
+
* be statically analyzed. Returns 'unsure' for these patterns (conservative).
|
|
108
|
+
*
|
|
109
|
+
* Note: False-positives are acceptable here (fail-safe direction). Regexes match
|
|
110
|
+
* broadly to catch unquoted/variable forms (perl -e $code) and even literal strings
|
|
111
|
+
* in other commands (echo "perl -e foo"), since marking those 'unsure' is
|
|
112
|
+
* safer than missing an actual one-liner.
|
|
113
|
+
*/
|
|
114
|
+
function detectSubprocessWrapper(command: string): BashClassification | null {
|
|
115
|
+
if (!command) return null;
|
|
116
|
+
|
|
117
|
+
// perl -e / -E with any argument (quoted, unquoted, or variable)
|
|
118
|
+
if (/\bperl\b.*\s-[eE](?:\s+\S|$)/.test(command)) {
|
|
119
|
+
return "unsure";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// python / python3 -c with any argument
|
|
123
|
+
if (/\bpython\d?\b.*\s-c(?:\s+\S|$)/.test(command)) {
|
|
124
|
+
return "unsure";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// node -e / -E / --eval with any argument
|
|
128
|
+
if (/\bnode\b.*(?:\s-[eE]|--eval)(?:\s+\S|$)/.test(command)) {
|
|
129
|
+
return "unsure";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ruby -e with any argument
|
|
133
|
+
if (/\bruby\b.*\s-e(?:\s+\S|$)/.test(command)) {
|
|
134
|
+
return "unsure";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
/**
|
|
105
141
|
* Run the judge on a single bash command. Always resolves (never rejects);
|
|
106
142
|
* any failure collapses to `unsure` so the caller's skip logic stays safe.
|
|
143
|
+
*
|
|
144
|
+
* Pre-checks for subprocess wrappers (perl -e, python -c, etc.) before calling
|
|
145
|
+
* the LLM, since these patterns cannot be statically analyzed.
|
|
107
146
|
*/
|
|
108
147
|
export async function classifyBashCommand(
|
|
109
148
|
runner: JudgeRunner,
|
|
@@ -111,6 +150,14 @@ export async function classifyBashCommand(
|
|
|
111
150
|
opts: JudgeOptions,
|
|
112
151
|
): Promise<BashClassification> {
|
|
113
152
|
if (!command || typeof command !== "string") return "unsure";
|
|
153
|
+
|
|
154
|
+
// Deterministic pre-check: subprocess wrappers
|
|
155
|
+
const subprocessResult = detectSubprocessWrapper(command);
|
|
156
|
+
if (subprocessResult) {
|
|
157
|
+
log(`judge: detected subprocess wrapper (${command.slice(0, 40)}...) → unsure`);
|
|
158
|
+
return subprocessResult;
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
try {
|
|
115
162
|
const { text } = await runner(command, opts);
|
|
116
163
|
return parseJudgeResponse(text);
|
package/orchestrator.ts
CHANGED
|
@@ -166,7 +166,11 @@ export class ReviewOrchestrator {
|
|
|
166
166
|
// All findings suppressed — treat as LGTM
|
|
167
167
|
if (filtered === null) {
|
|
168
168
|
log("dismiss: all findings suppressed — treating as LGTM");
|
|
169
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
isLgtm: true,
|
|
172
|
+
text: "No issues found (previously dismissed findings suppressed).",
|
|
173
|
+
};
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
// Number remaining findings and track them
|