@stupify/cli 0.0.1 → 0.0.3
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 +60 -0
- package/dist/analysis.d.ts +14 -0
- package/dist/analysis.js +276 -0
- package/dist/batcher.d.ts +3 -0
- package/dist/batcher.js +142 -0
- package/dist/cache.d.ts +2 -0
- package/dist/cache.js +59 -0
- package/dist/candidate-context.d.ts +2 -0
- package/dist/candidate-context.js +40 -0
- package/dist/checks.d.ts +3 -0
- package/dist/checks.js +131 -0
- package/dist/command.d.ts +2 -0
- package/dist/command.js +183 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +53 -0
- package/dist/counter-scout.d.ts +14 -0
- package/dist/counter-scout.js +97 -0
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +10 -0
- package/dist/experiment.d.ts +1 -0
- package/dist/experiment.js +225 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.js +219 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +24 -0
- package/dist/model.js +281 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +197 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.js +101 -0
- package/dist/repomix-provider.d.ts +4 -0
- package/dist/repomix-provider.js +145 -0
- package/dist/sem-provider.d.ts +2 -0
- package/dist/sem-provider.js +221 -0
- package/dist/stupify.d.ts +2 -0
- package/dist/stupify.js +387 -0
- package/dist/trace.d.ts +29 -0
- package/dist/trace.js +64 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +6 -0
- package/package.json +42 -5
- package/src/analysis.ts +408 -0
- package/src/batcher.ts +198 -0
- package/src/cache.ts +65 -0
- package/src/candidate-context.ts +43 -0
- package/src/checks.ts +132 -0
- package/src/command.ts +218 -0
- package/src/constants.ts +56 -0
- package/src/counter-scout.ts +119 -0
- package/src/diff.ts +9 -0
- package/src/experiment.ts +317 -0
- package/src/git.ts +228 -0
- package/src/index.ts +1 -0
- package/src/model.ts +360 -0
- package/src/prompts.ts +234 -0
- package/src/render.ts +107 -0
- package/src/repomix-provider.ts +163 -0
- package/src/sem-provider.ts +255 -0
- package/src/stupify.ts +598 -0
- package/src/trace.ts +103 -0
- package/src/types.ts +264 -0
- package/bin/stupify.mjs +0 -3
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
declare const brand: unique symbol;
|
|
2
|
+
type Brand<Value, Name extends string> = Value & {
|
|
3
|
+
readonly [brand]: Name;
|
|
4
|
+
};
|
|
5
|
+
export type SourceId = Brand<string, "SourceId">;
|
|
6
|
+
export type CheckId = Brand<string, "CheckId">;
|
|
7
|
+
export declare function sourceId(value: string): SourceId;
|
|
8
|
+
export declare function checkId(value: string): CheckId;
|
|
9
|
+
export type Engine = "raw-diff" | "sem";
|
|
10
|
+
export type ScoutMode = "llm" | "counter";
|
|
11
|
+
export type AuditContextMode = "none" | "repomix";
|
|
12
|
+
export type AuditPromptName = "strict" | "high_bar";
|
|
13
|
+
type AnalyzeOptions = Readonly<{
|
|
14
|
+
checkIds: readonly string[] | null;
|
|
15
|
+
json: boolean;
|
|
16
|
+
model: ModelId;
|
|
17
|
+
engine: Engine;
|
|
18
|
+
scout: ScoutMode;
|
|
19
|
+
auditContext: AuditContextMode;
|
|
20
|
+
auditPrompt: AuditPromptName;
|
|
21
|
+
debugSem: boolean;
|
|
22
|
+
debugTargets: boolean;
|
|
23
|
+
maxCandidates: number;
|
|
24
|
+
auditBatchSize: number;
|
|
25
|
+
maxAuditInputTokens: number;
|
|
26
|
+
auditConcurrency: number;
|
|
27
|
+
}>;
|
|
28
|
+
export type Command = Readonly<{
|
|
29
|
+
kind: "help";
|
|
30
|
+
}> | Readonly<{
|
|
31
|
+
kind: "experiment";
|
|
32
|
+
configPath: string;
|
|
33
|
+
}> | (Readonly<{
|
|
34
|
+
kind: "since";
|
|
35
|
+
since: string;
|
|
36
|
+
}> & AnalyzeOptions) | (Readonly<{
|
|
37
|
+
kind: "stdin";
|
|
38
|
+
}> & AnalyzeOptions) | (Readonly<{
|
|
39
|
+
kind: "commit";
|
|
40
|
+
commit: string;
|
|
41
|
+
}> & AnalyzeOptions) | (Readonly<{
|
|
42
|
+
kind: "commits";
|
|
43
|
+
count: number;
|
|
44
|
+
}> & AnalyzeOptions);
|
|
45
|
+
type ControlCommand = Readonly<{
|
|
46
|
+
kind: "help";
|
|
47
|
+
}> | Readonly<{
|
|
48
|
+
kind: "experiment";
|
|
49
|
+
configPath: string;
|
|
50
|
+
}>;
|
|
51
|
+
export type AnalyzeCommand = Exclude<Command, ControlCommand>;
|
|
52
|
+
export type StupifyCheck = Readonly<{
|
|
53
|
+
id: CheckId;
|
|
54
|
+
name: string;
|
|
55
|
+
question: string;
|
|
56
|
+
lookFor: readonly string[];
|
|
57
|
+
ignoreWhen: readonly string[];
|
|
58
|
+
enabledByDefault?: boolean;
|
|
59
|
+
examples?: Readonly<{
|
|
60
|
+
match?: readonly string[];
|
|
61
|
+
noMatch?: readonly string[];
|
|
62
|
+
}>;
|
|
63
|
+
}>;
|
|
64
|
+
export type FindingCandidate = Readonly<{
|
|
65
|
+
checkId: string;
|
|
66
|
+
why: string;
|
|
67
|
+
proof: string;
|
|
68
|
+
}>;
|
|
69
|
+
export type Finding = Readonly<{
|
|
70
|
+
sourceId: SourceId;
|
|
71
|
+
checkId: CheckId;
|
|
72
|
+
why: string;
|
|
73
|
+
proof: string;
|
|
74
|
+
}>;
|
|
75
|
+
export type FindingsResult = Readonly<{
|
|
76
|
+
findings: readonly Finding[];
|
|
77
|
+
summary?: string;
|
|
78
|
+
}>;
|
|
79
|
+
export type AuditReviewStats = Readonly<{
|
|
80
|
+
totalTargets: number;
|
|
81
|
+
finding: number;
|
|
82
|
+
clean: number;
|
|
83
|
+
uncertain: number;
|
|
84
|
+
invalid: number;
|
|
85
|
+
}>;
|
|
86
|
+
export type AuditReviewResult = FindingsResult & Readonly<{
|
|
87
|
+
stats: AuditReviewStats;
|
|
88
|
+
}>;
|
|
89
|
+
export type NetDiffStats = Readonly<{
|
|
90
|
+
filesChanged: number;
|
|
91
|
+
additions: number;
|
|
92
|
+
deletions: number;
|
|
93
|
+
}>;
|
|
94
|
+
export type NetDiff = Readonly<{
|
|
95
|
+
id: SourceId;
|
|
96
|
+
label: string;
|
|
97
|
+
base: string;
|
|
98
|
+
target: string;
|
|
99
|
+
text: string;
|
|
100
|
+
stats: NetDiffStats;
|
|
101
|
+
}>;
|
|
102
|
+
export type SourceRange = Readonly<{
|
|
103
|
+
id: SourceId;
|
|
104
|
+
label: string;
|
|
105
|
+
base: string;
|
|
106
|
+
target: string;
|
|
107
|
+
stats: NetDiffStats;
|
|
108
|
+
}>;
|
|
109
|
+
export type DiffHunk = Readonly<{
|
|
110
|
+
pointer: string;
|
|
111
|
+
batchId: string;
|
|
112
|
+
fileId: string;
|
|
113
|
+
hunkId: string;
|
|
114
|
+
filePath: string;
|
|
115
|
+
lineCount: number;
|
|
116
|
+
text: string;
|
|
117
|
+
}>;
|
|
118
|
+
export type DiffBatch = Readonly<{
|
|
119
|
+
id: string;
|
|
120
|
+
hunks: readonly DiffHunk[];
|
|
121
|
+
text: string;
|
|
122
|
+
}>;
|
|
123
|
+
export type CandidateContext = Readonly<{
|
|
124
|
+
pointer: string;
|
|
125
|
+
text: string;
|
|
126
|
+
}>;
|
|
127
|
+
export type SemChange = Readonly<{
|
|
128
|
+
entityId: string;
|
|
129
|
+
entityName: string;
|
|
130
|
+
entityType: string;
|
|
131
|
+
filePath: string;
|
|
132
|
+
changeType: string;
|
|
133
|
+
beforeContent: string | null;
|
|
134
|
+
afterContent: string | null;
|
|
135
|
+
}>;
|
|
136
|
+
export type SemChangeSummary = Readonly<{
|
|
137
|
+
added: number;
|
|
138
|
+
deleted: number;
|
|
139
|
+
modified: number;
|
|
140
|
+
moved: number;
|
|
141
|
+
renamed: number;
|
|
142
|
+
fileCount: number;
|
|
143
|
+
total: number;
|
|
144
|
+
}>;
|
|
145
|
+
export type SemChangeSet = Readonly<{
|
|
146
|
+
id: SourceId;
|
|
147
|
+
label: string;
|
|
148
|
+
base: string;
|
|
149
|
+
target: string;
|
|
150
|
+
contextCwd: string;
|
|
151
|
+
cleanup: () => Promise<void>;
|
|
152
|
+
changes: readonly SemChange[];
|
|
153
|
+
summary: SemChangeSummary;
|
|
154
|
+
}>;
|
|
155
|
+
export type SemCandidate = Readonly<{
|
|
156
|
+
sourceId: SourceId;
|
|
157
|
+
targetId: string;
|
|
158
|
+
entityId: string;
|
|
159
|
+
checkId: CheckId;
|
|
160
|
+
reason: string;
|
|
161
|
+
}>;
|
|
162
|
+
export type SemContext = Readonly<{
|
|
163
|
+
targetId: string;
|
|
164
|
+
entityId: string;
|
|
165
|
+
entityName: string;
|
|
166
|
+
entityKind: string;
|
|
167
|
+
changeKind: string;
|
|
168
|
+
checkId: CheckId;
|
|
169
|
+
reason: string;
|
|
170
|
+
filePath?: string;
|
|
171
|
+
text: string;
|
|
172
|
+
}>;
|
|
173
|
+
export type DebugTarget = Readonly<{
|
|
174
|
+
targetId: string;
|
|
175
|
+
checkId: CheckId;
|
|
176
|
+
entityId: string;
|
|
177
|
+
entityKind?: string;
|
|
178
|
+
changeKind?: string;
|
|
179
|
+
scoutReason?: string;
|
|
180
|
+
sourceLabel?: string;
|
|
181
|
+
}>;
|
|
182
|
+
export type SemContextPack = Readonly<{
|
|
183
|
+
provider: "repomix";
|
|
184
|
+
filePaths: readonly string[];
|
|
185
|
+
totalCharacters: number;
|
|
186
|
+
totalTokens: number;
|
|
187
|
+
text: string;
|
|
188
|
+
}>;
|
|
189
|
+
export type AnalysisRun = Readonly<{
|
|
190
|
+
engine: Engine;
|
|
191
|
+
auditContext: AuditContextMode;
|
|
192
|
+
auditPrompt: AuditPromptName;
|
|
193
|
+
mode: AnalyzeCommand["kind"];
|
|
194
|
+
modelId: ModelId;
|
|
195
|
+
checkIds: readonly CheckId[];
|
|
196
|
+
sourceId: SourceId;
|
|
197
|
+
label: string;
|
|
198
|
+
stats: NetDiffStats;
|
|
199
|
+
batchesScanned: number;
|
|
200
|
+
candidateCount: number;
|
|
201
|
+
targetsByCheck?: Readonly<Record<string, number>>;
|
|
202
|
+
entitiesScanned: number;
|
|
203
|
+
auditedCandidateCount: number;
|
|
204
|
+
scoutModelCalls: number;
|
|
205
|
+
auditModelCalls: number;
|
|
206
|
+
timingsMs: Readonly<{
|
|
207
|
+
diff: number;
|
|
208
|
+
modelLoad: number;
|
|
209
|
+
search: number;
|
|
210
|
+
audit: number;
|
|
211
|
+
total: number;
|
|
212
|
+
}>;
|
|
213
|
+
warnings: readonly string[];
|
|
214
|
+
auditStats?: AuditReviewStats;
|
|
215
|
+
debugTargets?: readonly DebugTarget[];
|
|
216
|
+
traceEvents?: readonly TraceEvent[];
|
|
217
|
+
}>;
|
|
218
|
+
export type TraceEvent = Readonly<{
|
|
219
|
+
name: string;
|
|
220
|
+
ms: number;
|
|
221
|
+
count?: number;
|
|
222
|
+
detail?: string;
|
|
223
|
+
}>;
|
|
224
|
+
export type AnalysisReport = Readonly<{
|
|
225
|
+
run: AnalysisRun;
|
|
226
|
+
result: FindingsResult;
|
|
227
|
+
}>;
|
|
228
|
+
export type ModelId = "gemma-4-e2b" | "gemma-4-e4b" | "gemma-4-26b-a4b" | "qwen3-4b-magicquant" | "qwen2.5-coder-1.5b" | "qwen2.5-coder-7b" | "qwen2.5-coder-32b";
|
|
229
|
+
export type ModelConfig = Readonly<{
|
|
230
|
+
id: ModelId;
|
|
231
|
+
name: string;
|
|
232
|
+
size: string;
|
|
233
|
+
file: string;
|
|
234
|
+
url: string;
|
|
235
|
+
}>;
|
|
236
|
+
export {};
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stupify/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Local-only diagnostic CLI for checking whether AI is making you dumber.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
5
7
|
"bin": {
|
|
6
|
-
"stupify": "
|
|
8
|
+
"stupify": "dist/stupify.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+ssh://git@github.com/Octember/stupif.ai.git",
|
|
13
|
+
"directory": "packages/cli"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://stupif.ai",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Octember/stupif.ai/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"cli",
|
|
22
|
+
"developer-tools",
|
|
23
|
+
"local-first",
|
|
24
|
+
"code-review"
|
|
25
|
+
],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
7
32
|
},
|
|
8
33
|
"files": [
|
|
9
|
-
"
|
|
34
|
+
"dist",
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"package.json"
|
|
10
38
|
],
|
|
11
|
-
"
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"prepack": "bun run build",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
43
|
+
"smoke": "bun run build && node ./dist/stupify.js --help"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@ataraxy-labs/sem": "^0.3.24",
|
|
47
|
+
"repomix": "^1.14.0"
|
|
48
|
+
}
|
|
12
49
|
}
|
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { auditPrompt, findingsAuditPrompt, scoutPrompt, semScoutPrompt } from "./prompts.ts";
|
|
2
|
+
import { cachedJson, fingerprint } from "./cache.ts";
|
|
3
|
+
import type { LocalModel } from "./model.ts";
|
|
4
|
+
import type {
|
|
5
|
+
CandidateContext,
|
|
6
|
+
CheckId,
|
|
7
|
+
DiffBatch,
|
|
8
|
+
AuditPromptName,
|
|
9
|
+
AuditReviewResult,
|
|
10
|
+
Finding,
|
|
11
|
+
FindingsResult,
|
|
12
|
+
NetDiff,
|
|
13
|
+
SemCandidate,
|
|
14
|
+
SemChangeSet,
|
|
15
|
+
SemContext,
|
|
16
|
+
SemContextPack,
|
|
17
|
+
SourceId,
|
|
18
|
+
StupifyCheck,
|
|
19
|
+
} from "./types.ts";
|
|
20
|
+
|
|
21
|
+
export async function scoutBatch(
|
|
22
|
+
model: LocalModel,
|
|
23
|
+
batch: DiffBatch,
|
|
24
|
+
checks: readonly StupifyCheck[],
|
|
25
|
+
sourceLabel: string,
|
|
26
|
+
): Promise<readonly string[]> {
|
|
27
|
+
const raw = await runJsonPrompt(model, scoutPrompt(batch, checks, sourceLabel), scoutSchema(batch), 0);
|
|
28
|
+
return uncheckedCandidates(raw);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function auditCandidates(
|
|
32
|
+
model: LocalModel,
|
|
33
|
+
diff: NetDiff,
|
|
34
|
+
contexts: readonly CandidateContext[],
|
|
35
|
+
checks: readonly StupifyCheck[],
|
|
36
|
+
): Promise<FindingsResult> {
|
|
37
|
+
if (contexts.length === 0) return { findings: [], summary: "No candidate regions found." };
|
|
38
|
+
|
|
39
|
+
const raw = await runJsonPrompt(model, auditPrompt(contexts, checks, diff.label), auditSchema(contexts), 0);
|
|
40
|
+
return uncheckedRawAuditResult(raw, diff.id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function scoutSemChanges(
|
|
44
|
+
model: LocalModel,
|
|
45
|
+
changeSet: SemChangeSet,
|
|
46
|
+
checks: readonly StupifyCheck[],
|
|
47
|
+
maxCandidates: number,
|
|
48
|
+
): Promise<readonly SemCandidate[]> {
|
|
49
|
+
const raw = await runJsonPrompt(
|
|
50
|
+
model,
|
|
51
|
+
semScoutPrompt(changeSet, checks, maxCandidates),
|
|
52
|
+
semScoutSchema(changeSet, checks, maxCandidates),
|
|
53
|
+
0,
|
|
54
|
+
);
|
|
55
|
+
return uncheckedSemCandidates(raw, changeSet.id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runFindingsAudit(
|
|
59
|
+
model: LocalModel,
|
|
60
|
+
changeSet: SemChangeSet,
|
|
61
|
+
contexts: readonly SemContext[],
|
|
62
|
+
pack: SemContextPack,
|
|
63
|
+
checks: readonly StupifyCheck[],
|
|
64
|
+
request = findingsAuditRequest(changeSet, contexts, pack, checks),
|
|
65
|
+
): Promise<AuditReviewResult> {
|
|
66
|
+
if (contexts.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
findings: [],
|
|
69
|
+
summary: "No candidate entities found.",
|
|
70
|
+
stats: { totalTargets: 0, finding: 0, clean: 0, uncertain: 0, invalid: 0 },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const raw = await runJsonPrompt(
|
|
75
|
+
model,
|
|
76
|
+
request.prompt,
|
|
77
|
+
request.schema,
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
return uncheckedFindingsAuditResult(raw, changeSet.id, contexts);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function findingsAuditRequest(
|
|
84
|
+
changeSet: SemChangeSet,
|
|
85
|
+
contexts: readonly SemContext[],
|
|
86
|
+
pack: SemContextPack,
|
|
87
|
+
checks: readonly StupifyCheck[],
|
|
88
|
+
promptName: AuditPromptName = "strict",
|
|
89
|
+
): Readonly<{ prompt: string; schema: unknown }> {
|
|
90
|
+
return {
|
|
91
|
+
prompt: findingsAuditPrompt(contexts, pack, checks, changeSet.label, promptName),
|
|
92
|
+
schema: findingsAuditSchema(contexts),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function countPromptTokens(model: LocalModel, prompt: string): Promise<number> {
|
|
97
|
+
const cached = await cachedJson(
|
|
98
|
+
"prompt-tokens",
|
|
99
|
+
fingerprint({
|
|
100
|
+
version: 1,
|
|
101
|
+
modelId: model.id,
|
|
102
|
+
profile: model.profile,
|
|
103
|
+
prompt,
|
|
104
|
+
}),
|
|
105
|
+
async () => {
|
|
106
|
+
const response = await fetch(`${model.baseUrl}/tokenize`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "content-type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ content: prompt }),
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`llama-server tokenize failed: HTTP ${response.status} ${await response.text()}`);
|
|
113
|
+
}
|
|
114
|
+
const body = await response.json() as { tokens?: unknown };
|
|
115
|
+
if (!Array.isArray(body.tokens)) throw new Error("llama-server tokenize returned no tokens.");
|
|
116
|
+
return { count: body.tokens.length };
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
return cached.count;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findingsAuditSchema(contexts: readonly SemContext[]): unknown {
|
|
123
|
+
const targetIds = contexts.map((context) => context.targetId);
|
|
124
|
+
const findingItem = {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
targetId: { type: "string", enum: targetIds },
|
|
128
|
+
why: { type: "string" },
|
|
129
|
+
proof: { type: "string" },
|
|
130
|
+
},
|
|
131
|
+
required: ["targetId", "why", "proof"],
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
};
|
|
134
|
+
const uncertainItem = {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
targetId: { type: "string", enum: targetIds },
|
|
138
|
+
why: { type: "string" },
|
|
139
|
+
},
|
|
140
|
+
required: ["targetId", "why"],
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
findings: {
|
|
147
|
+
type: "array",
|
|
148
|
+
items: findingItem,
|
|
149
|
+
},
|
|
150
|
+
uncertain: {
|
|
151
|
+
type: "array",
|
|
152
|
+
items: uncertainItem,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function auditSchema(contexts: readonly CandidateContext[]): unknown {
|
|
160
|
+
return auditSchemaFromProofs(contexts.map((context) => context.pointer));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function auditSchemaFromProofs(proofs: readonly string[]): unknown {
|
|
164
|
+
return {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
findings: {
|
|
168
|
+
type: "array",
|
|
169
|
+
items: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
checkId: { type: "string" },
|
|
173
|
+
why: { type: "string" },
|
|
174
|
+
proof: { type: "string", enum: proofs },
|
|
175
|
+
},
|
|
176
|
+
required: ["checkId", "why", "proof"],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
summary: { type: "string" },
|
|
181
|
+
},
|
|
182
|
+
required: ["findings", "summary"],
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function semScoutSchema(
|
|
188
|
+
changeSet: SemChangeSet,
|
|
189
|
+
checks: readonly StupifyCheck[],
|
|
190
|
+
maxCandidates: number,
|
|
191
|
+
): unknown {
|
|
192
|
+
return {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
targets: {
|
|
196
|
+
type: "array",
|
|
197
|
+
maxItems: maxCandidates,
|
|
198
|
+
items: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
entityId: { type: "string", enum: changeSet.changes.map((change) => change.entityId) },
|
|
202
|
+
checkId: { type: "string", enum: checks.map((check) => check.id) },
|
|
203
|
+
reason: { type: "string" },
|
|
204
|
+
},
|
|
205
|
+
required: ["entityId", "checkId", "reason"],
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
required: ["targets"],
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function scoutSchema(batch: DiffBatch): unknown {
|
|
216
|
+
return {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
candidates: {
|
|
220
|
+
type: "array",
|
|
221
|
+
maxItems: 3,
|
|
222
|
+
items: { type: "string", enum: batch.hunks.map((hunk) => hunk.pointer) },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
required: ["candidates"],
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type RawScoutOutput = Readonly<{ candidates?: readonly string[] }>;
|
|
231
|
+
type RawAuditOutput = Readonly<{
|
|
232
|
+
findings?: readonly RawFinding[];
|
|
233
|
+
summary?: string;
|
|
234
|
+
}>;
|
|
235
|
+
type RawSemScoutOutput = Readonly<{
|
|
236
|
+
targets?: readonly RawSemCandidate[];
|
|
237
|
+
candidates?: readonly RawSemCandidate[];
|
|
238
|
+
}>;
|
|
239
|
+
type RawSemCandidate = Readonly<{
|
|
240
|
+
targetId?: string;
|
|
241
|
+
entityId?: string;
|
|
242
|
+
checkId?: string;
|
|
243
|
+
checkIds?: readonly string[];
|
|
244
|
+
reason?: string;
|
|
245
|
+
}>;
|
|
246
|
+
type RawFinding = Readonly<{
|
|
247
|
+
checkId?: string;
|
|
248
|
+
why?: string;
|
|
249
|
+
proof?: string;
|
|
250
|
+
}>;
|
|
251
|
+
type RawFindingReview = RawFinding & Readonly<{ targetId?: string }>;
|
|
252
|
+
type RawFindingsAuditOutput = Readonly<{
|
|
253
|
+
findings?: readonly RawFindingReview[];
|
|
254
|
+
uncertain?: readonly RawFindingReview[];
|
|
255
|
+
}>;
|
|
256
|
+
|
|
257
|
+
function uncheckedCandidates(value: unknown): readonly string[] {
|
|
258
|
+
return [...((value as RawScoutOutput).candidates ?? [])];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function uncheckedRawAuditResult(value: unknown, sourceId: SourceId): FindingsResult {
|
|
262
|
+
const output = value as RawAuditOutput;
|
|
263
|
+
const findings = (output.findings ?? []).map((finding): Finding => ({
|
|
264
|
+
sourceId,
|
|
265
|
+
checkId: (finding.checkId ?? "") as CheckId,
|
|
266
|
+
why: finding.why ?? "",
|
|
267
|
+
proof: finding.proof ?? "",
|
|
268
|
+
}));
|
|
269
|
+
return { findings, summary: output.summary ?? defaultSummary(findings.length) };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function uncheckedSemCandidates(value: unknown, sourceId: SourceId): readonly SemCandidate[] {
|
|
273
|
+
const output = value as RawSemScoutOutput;
|
|
274
|
+
const rawTargets = output.targets ?? output.candidates ?? [];
|
|
275
|
+
return rawTargets.flatMap((candidate) => {
|
|
276
|
+
if (candidate.checkId) {
|
|
277
|
+
return [{
|
|
278
|
+
sourceId,
|
|
279
|
+
targetId: candidate.targetId ?? "",
|
|
280
|
+
entityId: candidate.entityId ?? "",
|
|
281
|
+
checkId: candidate.checkId as CheckId,
|
|
282
|
+
reason: candidate.reason ?? "",
|
|
283
|
+
}];
|
|
284
|
+
}
|
|
285
|
+
return (candidate.checkIds ?? []).map((checkId) => ({
|
|
286
|
+
sourceId,
|
|
287
|
+
targetId: candidate.targetId ?? "",
|
|
288
|
+
entityId: candidate.entityId ?? "",
|
|
289
|
+
checkId: checkId as CheckId,
|
|
290
|
+
reason: candidate.reason ?? "",
|
|
291
|
+
}));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function uncheckedFindingsAuditResult(
|
|
296
|
+
value: unknown,
|
|
297
|
+
sourceId: SourceId,
|
|
298
|
+
contexts: readonly SemContext[],
|
|
299
|
+
): AuditReviewResult {
|
|
300
|
+
const output = value as RawFindingsAuditOutput;
|
|
301
|
+
const targetsById = new Map(contexts.map((context) => [context.targetId, context]));
|
|
302
|
+
const findings = (output.findings ?? []).map((finding): Finding => {
|
|
303
|
+
const target = targetsById.get(finding.targetId ?? "");
|
|
304
|
+
return {
|
|
305
|
+
sourceId,
|
|
306
|
+
checkId: (target?.checkId ?? "") as CheckId,
|
|
307
|
+
why: finding.why ?? "",
|
|
308
|
+
proof: finding.proof ?? "",
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
const uncertain = output.uncertain?.length ?? 0;
|
|
312
|
+
const totalTargets = contexts.length;
|
|
313
|
+
return {
|
|
314
|
+
findings,
|
|
315
|
+
summary: defaultSummary(findings.length),
|
|
316
|
+
stats: {
|
|
317
|
+
totalTargets,
|
|
318
|
+
finding: findings.length,
|
|
319
|
+
clean: Math.max(0, totalTargets - findings.length - uncertain),
|
|
320
|
+
uncertain,
|
|
321
|
+
invalid: 0,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function defaultSummary(findingCount: number): string {
|
|
327
|
+
return findingCount === 0
|
|
328
|
+
? "No clear judgment-offload signal found."
|
|
329
|
+
: `${findingCount} finding review${findingCount === 1 ? "" : "s"} accepted.`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function runJsonPrompt(
|
|
333
|
+
model: LocalModel,
|
|
334
|
+
prompt: string,
|
|
335
|
+
schema: unknown,
|
|
336
|
+
temperature: number,
|
|
337
|
+
): Promise<unknown> {
|
|
338
|
+
return cachedJson(
|
|
339
|
+
"model-json",
|
|
340
|
+
fingerprint({
|
|
341
|
+
version: 1,
|
|
342
|
+
modelId: model.id,
|
|
343
|
+
profile: model.profile,
|
|
344
|
+
prompt,
|
|
345
|
+
schema,
|
|
346
|
+
temperature,
|
|
347
|
+
}),
|
|
348
|
+
() => runJsonPromptUncached(model, prompt, schema, temperature),
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function runJsonPromptUncached(
|
|
353
|
+
model: LocalModel,
|
|
354
|
+
prompt: string,
|
|
355
|
+
schema: unknown,
|
|
356
|
+
temperature: number,
|
|
357
|
+
): Promise<unknown> {
|
|
358
|
+
const first = await complete(model, prompt, schema, temperature);
|
|
359
|
+
const parsed = parseJson(first);
|
|
360
|
+
if (parsed.ok) return parsed.value;
|
|
361
|
+
|
|
362
|
+
const retry = await complete(model, `${prompt}
|
|
363
|
+
|
|
364
|
+
Your previous response was not valid JSON. Return the requested JSON object only.`, schema, temperature);
|
|
365
|
+
const retryParsed = parseJson(retry);
|
|
366
|
+
if (retryParsed.ok) return retryParsed.value;
|
|
367
|
+
|
|
368
|
+
console.error("Raw model output:");
|
|
369
|
+
console.error(retry);
|
|
370
|
+
throw new Error("Model returned invalid JSON.");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function complete(
|
|
374
|
+
model: LocalModel,
|
|
375
|
+
prompt: string,
|
|
376
|
+
schema: unknown,
|
|
377
|
+
temperature: number,
|
|
378
|
+
): Promise<string> {
|
|
379
|
+
const response = await fetch(`${model.baseUrl}/v1/chat/completions`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: { "content-type": "application/json" },
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
model: model.id,
|
|
384
|
+
messages: [{ role: "user", content: prompt }],
|
|
385
|
+
temperature,
|
|
386
|
+
response_format: {
|
|
387
|
+
type: "json_object",
|
|
388
|
+
schema,
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (!response.ok) throw new Error(`llama-server request failed: HTTP ${response.status} ${await response.text()}`);
|
|
394
|
+
|
|
395
|
+
const body = await response.json() as { choices?: Array<{ message?: { content?: unknown } }> };
|
|
396
|
+
const content = body.choices?.[0]?.message?.content;
|
|
397
|
+
if (typeof content !== "string") throw new Error("llama-server returned no message content.");
|
|
398
|
+
return content;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function parseJson(raw: string): Readonly<{ ok: true; value: unknown }> | Readonly<{ ok: false }> {
|
|
402
|
+
try {
|
|
403
|
+
const value = JSON.parse(raw);
|
|
404
|
+
return { ok: true, value };
|
|
405
|
+
} catch {
|
|
406
|
+
return { ok: false };
|
|
407
|
+
}
|
|
408
|
+
}
|