ennodia 0.3.0 → 0.4.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/CONTRIBUTING.md +2 -19
- package/README.md +51 -31
- package/docs/comparisons/agent-frameworks.md +5 -5
- package/docs/comparisons/autogen.md +4 -4
- package/docs/comparisons/chathub.md +4 -4
- package/docs/comparisons/index.md +4 -4
- package/docs/comparisons/langgraph.md +4 -4
- package/docs/comparisons/mixture-of-agents.md +5 -5
- package/docs/comparisons/model-merging.md +5 -5
- package/docs/comparisons/openrouter.md +4 -4
- package/docs/concepts/compositional-audits.md +8 -6
- package/docs/concepts/data-governance.md +5 -9
- package/docs/concepts/how-ennodia-works.md +2 -2
- package/docs/concepts/interfaces-and-core.md +1 -1
- package/docs/concepts/second-opinions.md +5 -5
- package/docs/evidence/index.md +6 -13
- package/docs/evidence/measurement.md +6 -5
- package/docs/getting-started.md +9 -6
- package/docs/guides/agent-skills.md +6 -5
- package/docs/guides/budgets-and-limits.md +8 -6
- package/docs/guides/pragmatic-mode.md +2 -0
- package/docs/guides/recipes.md +1 -4
- package/docs/guides/running-better-audits.md +7 -7
- package/docs/guides/troubleshooting.md +0 -2
- package/docs/guides/understand-results.md +0 -2
- package/docs/index.md +2 -4
- package/docs/install.md +11 -9
- package/docs/questions.md +56 -0
- package/docs/reference/controlled-english.md +1 -3
- package/docs/reference/ennodia-io.md +7 -7
- package/docs/reference/mcp-tools.md +69 -15
- package/docs/reference/supported-harnesses.md +9 -4
- package/docs/roadmap.md +0 -2
- package/package.json +4 -11
- package/skills/pragmatic/SKILL.md +2 -0
- package/src/cli.ts +31 -15
- package/src/compare.ts +37 -1
- package/src/core.ts +2 -4
- package/src/diagnosis.ts +48 -1
- package/src/harnesses.ts +54 -25
- package/src/index.ts +4 -0
- package/src/runs.ts +17 -8
- package/src/server.ts +55 -2
- package/src/tasks.ts +145 -37
- package/src/version.ts +1 -1
- package/bench/README.md +0 -34
- package/bench/bug-recall/run.ts +0 -339
- package/bench/bug-recall/scorer.ts +0 -424
- package/bench/fixtures/bug-recall/001-missing-await/input.ts +0 -33
- package/bench/fixtures/bug-recall/001-missing-await/oracle.json +0 -28
- package/bench/fixtures/bug-recall/001-missing-await/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/001-missing-await/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/001-missing-await/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/input.ts +0 -40
- package/bench/fixtures/bug-recall/002-timeout-drain/oracle.json +0 -28
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/input.ts +0 -22
- package/bench/fixtures/bug-recall/003-schema-contract/oracle.json +0 -28
- package/bench/fixtures/bug-recall/003-schema-contract/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/input.ts +0 -28
- package/bench/fixtures/bug-recall/004-cancel-propagation/oracle.json +0 -28
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/ennodia-parallel-compare.md +0 -1
- package/bench/results/.gitkeep +0 -1
- package/docs/evidence/building-ennodia.md +0 -66
- package/docs/evidence/docs-drift.md +0 -73
- package/docs/evidence/skill-trial.md +0 -68
- package/docs/reference/benchmarks.md +0 -64
- package/examples/docs-drift/README.md +0 -8
- package/examples/docs-drift/archive/migration.md +0 -4
- package/examples/docs-drift/check.ts +0 -7
- package/examples/docs-drift/config.ts +0 -8
- package/examples/docs-drift/demo.ts +0 -54
- package/examples/docs-drift/recorded-run.json +0 -16
- package/examples/skill-trial/README.md +0 -8
- package/examples/skill-trial/check.ts +0 -15
- package/examples/skill-trial/contract.md +0 -7
- package/examples/skill-trial/recorded-trial.json +0 -42
- package/examples/skill-trial/retry.ts +0 -4
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
export type Severity = "low" | "medium" | "high";
|
|
6
|
-
|
|
7
|
-
export type RequiredFinding = {
|
|
8
|
-
id: string;
|
|
9
|
-
severity: Severity;
|
|
10
|
-
line: number;
|
|
11
|
-
description: string;
|
|
12
|
-
keywords: string[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type FalsePositiveTrap = {
|
|
16
|
-
id: string;
|
|
17
|
-
description: string;
|
|
18
|
-
terms: string[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type Oracle = {
|
|
22
|
-
id: string;
|
|
23
|
-
title: string;
|
|
24
|
-
prompt: string;
|
|
25
|
-
requiredFindings: RequiredFinding[];
|
|
26
|
-
knownFalsePositives: FalsePositiveTrap[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type BenchmarkCase = {
|
|
30
|
-
id: string;
|
|
31
|
-
title: string;
|
|
32
|
-
inputPath: string;
|
|
33
|
-
input: string;
|
|
34
|
-
oracle: Oracle;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type OutputRecord = {
|
|
38
|
-
caseId: string;
|
|
39
|
-
condition: string;
|
|
40
|
-
text: string;
|
|
41
|
-
metadata?: Record<string, unknown>;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type CaseScore = {
|
|
45
|
-
caseId: string;
|
|
46
|
-
condition: string;
|
|
47
|
-
matchedFindingIds: string[];
|
|
48
|
-
missedFindingIds: string[];
|
|
49
|
-
falsePositiveTrapIds: string[];
|
|
50
|
-
claimedFindingCount: number;
|
|
51
|
-
requiredFindingCount: number;
|
|
52
|
-
highFindingCount: number;
|
|
53
|
-
highMatchedCount: number;
|
|
54
|
-
recall: number;
|
|
55
|
-
precision: number;
|
|
56
|
-
f1: number;
|
|
57
|
-
highRecall: number;
|
|
58
|
-
outputChars: number;
|
|
59
|
-
metadata?: Record<string, unknown>;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type ConditionSummary = {
|
|
63
|
-
condition: string;
|
|
64
|
-
cases: number;
|
|
65
|
-
matchedFindings: number;
|
|
66
|
-
requiredFindings: number;
|
|
67
|
-
falsePositiveTraps: number;
|
|
68
|
-
claimedFindings: number;
|
|
69
|
-
highMatchedFindings: number;
|
|
70
|
-
highRequiredFindings: number;
|
|
71
|
-
recall: number;
|
|
72
|
-
precision: number;
|
|
73
|
-
f1: number;
|
|
74
|
-
highRecall: number;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type BenchmarkSummary = {
|
|
78
|
-
benchmark: "multi-model-bug-recall";
|
|
79
|
-
mode: "fixture" | "live";
|
|
80
|
-
createdAt: string;
|
|
81
|
-
scorerHash: string;
|
|
82
|
-
caseScores: CaseScore[];
|
|
83
|
-
conditions: ConditionSummary[];
|
|
84
|
-
wins: {
|
|
85
|
-
condition: string;
|
|
86
|
-
fixtures: number;
|
|
87
|
-
}[];
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const DEFAULT_FIXTURES_ROOT = new URL(
|
|
91
|
-
"../fixtures/bug-recall/",
|
|
92
|
-
import.meta.url,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
export async function loadCases(
|
|
96
|
-
fixturesRoot = DEFAULT_FIXTURES_ROOT,
|
|
97
|
-
): Promise<BenchmarkCase[]> {
|
|
98
|
-
const entries = await readdir(fixturesRoot, { withFileTypes: true });
|
|
99
|
-
const caseDirs = entries
|
|
100
|
-
.filter((entry) => entry.isDirectory())
|
|
101
|
-
.map((entry) => entry.name)
|
|
102
|
-
.sort();
|
|
103
|
-
|
|
104
|
-
const cases: BenchmarkCase[] = [];
|
|
105
|
-
|
|
106
|
-
for (const caseId of caseDirs) {
|
|
107
|
-
const caseUrl = new URL(`${caseId}/`, fixturesRoot);
|
|
108
|
-
const inputUrl = new URL("input.ts", caseUrl);
|
|
109
|
-
const oracleUrl = new URL("oracle.json", caseUrl);
|
|
110
|
-
const input = await Bun.file(inputUrl).text();
|
|
111
|
-
const oracle = await Bun.file(oracleUrl).json() as Oracle;
|
|
112
|
-
validateOracle(caseId, oracle);
|
|
113
|
-
|
|
114
|
-
cases.push({
|
|
115
|
-
id: oracle.id,
|
|
116
|
-
title: oracle.title,
|
|
117
|
-
inputPath: inputUrl.pathname,
|
|
118
|
-
input,
|
|
119
|
-
oracle,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return cases;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function scoreBenchmark(input: {
|
|
127
|
-
cases: BenchmarkCase[];
|
|
128
|
-
outputs: OutputRecord[];
|
|
129
|
-
mode: "fixture" | "live";
|
|
130
|
-
}): Promise<BenchmarkSummary> {
|
|
131
|
-
const caseScores = input.outputs.map((output) => {
|
|
132
|
-
const benchmarkCase = input.cases.find((candidate) =>
|
|
133
|
-
candidate.id === output.caseId
|
|
134
|
-
);
|
|
135
|
-
if (!benchmarkCase) {
|
|
136
|
-
throw new Error(`Unknown benchmark case: ${output.caseId}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return scoreCase(benchmarkCase.oracle, output);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
benchmark: "multi-model-bug-recall",
|
|
144
|
-
mode: input.mode,
|
|
145
|
-
createdAt: new Date().toISOString(),
|
|
146
|
-
scorerHash: await hashScorerSource(),
|
|
147
|
-
caseScores,
|
|
148
|
-
conditions: summarizeConditions(caseScores),
|
|
149
|
-
wins: summarizeWins(input.cases, caseScores),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function scoreCase(oracle: Oracle, output: OutputRecord): CaseScore {
|
|
154
|
-
const claims = extractClaims(output.text);
|
|
155
|
-
const normalizedOutput = normalize(output.text);
|
|
156
|
-
const claimTexts = claims.length > 0
|
|
157
|
-
? claims.map((claim) => normalize(claim))
|
|
158
|
-
: splitFallbackClaims(output.text).map((claim) => normalize(claim));
|
|
159
|
-
const matchTexts = claimTexts.length > 0 ? claimTexts : [normalizedOutput];
|
|
160
|
-
const matchedFindingIds = oracle.requiredFindings
|
|
161
|
-
.filter((finding) => matchesFinding(finding, matchTexts))
|
|
162
|
-
.map((finding) => finding.id);
|
|
163
|
-
const missedFindingIds = oracle.requiredFindings
|
|
164
|
-
.filter((finding) => !matchedFindingIds.includes(finding.id))
|
|
165
|
-
.map((finding) => finding.id);
|
|
166
|
-
const falsePositiveTrapIds = oracle.knownFalsePositives
|
|
167
|
-
.filter((trap) => matchesTerms(trap.terms, [normalizedOutput]))
|
|
168
|
-
.map((trap) => trap.id);
|
|
169
|
-
const highFindings = oracle.requiredFindings.filter((finding) =>
|
|
170
|
-
finding.severity === "high"
|
|
171
|
-
);
|
|
172
|
-
const highMatchedCount = highFindings.filter((finding) =>
|
|
173
|
-
matchedFindingIds.includes(finding.id)
|
|
174
|
-
).length;
|
|
175
|
-
const claimedFindingCount = Math.max(
|
|
176
|
-
claimTexts.length,
|
|
177
|
-
matchedFindingIds.length + falsePositiveTrapIds.length,
|
|
178
|
-
);
|
|
179
|
-
const precision = safeRatio(matchedFindingIds.length, claimedFindingCount);
|
|
180
|
-
const recall = safeRatio(
|
|
181
|
-
matchedFindingIds.length,
|
|
182
|
-
oracle.requiredFindings.length,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
caseId: oracle.id,
|
|
187
|
-
condition: output.condition,
|
|
188
|
-
matchedFindingIds,
|
|
189
|
-
missedFindingIds,
|
|
190
|
-
falsePositiveTrapIds,
|
|
191
|
-
claimedFindingCount,
|
|
192
|
-
requiredFindingCount: oracle.requiredFindings.length,
|
|
193
|
-
highFindingCount: highFindings.length,
|
|
194
|
-
highMatchedCount,
|
|
195
|
-
recall,
|
|
196
|
-
precision,
|
|
197
|
-
f1: f1(precision, recall),
|
|
198
|
-
highRecall: safeRatio(highMatchedCount, highFindings.length),
|
|
199
|
-
outputChars: output.text.length,
|
|
200
|
-
metadata: output.metadata,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function formatConditionTable(summary: BenchmarkSummary): string {
|
|
205
|
-
const rows = [
|
|
206
|
-
"| Condition | Cases | Recall | Precision | F1 | High recall | FP traps |",
|
|
207
|
-
"|---|---:|---:|---:|---:|---:|---:|",
|
|
208
|
-
...summary.conditions.map((condition) =>
|
|
209
|
-
`| ${condition.condition} | ${condition.cases} | ${
|
|
210
|
-
formatPercent(condition.recall)
|
|
211
|
-
} | ${formatPercent(condition.precision)} | ${formatPercent(condition.f1)} | ${
|
|
212
|
-
formatPercent(condition.highRecall)
|
|
213
|
-
} | ${condition.falsePositiveTraps} |`
|
|
214
|
-
),
|
|
215
|
-
];
|
|
216
|
-
|
|
217
|
-
return rows.join("\n");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function buildPrompt(benchmarkCase: BenchmarkCase): string {
|
|
221
|
-
return [
|
|
222
|
-
"You are reviewing one TypeScript fixture for correctness bugs.",
|
|
223
|
-
"Return JSON only with this exact shape:",
|
|
224
|
-
'{"findings":[{"line":12,"summary":"short bug description"}]}',
|
|
225
|
-
"List real correctness, lifecycle, async, cancellation, schema, or boundary bugs.",
|
|
226
|
-
"Do not include style comments or refactors.",
|
|
227
|
-
"",
|
|
228
|
-
`Fixture: ${benchmarkCase.id} - ${benchmarkCase.title}`,
|
|
229
|
-
"```ts",
|
|
230
|
-
benchmarkCase.input.trim(),
|
|
231
|
-
"```",
|
|
232
|
-
].join("\n");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function validateOracle(caseId: string, oracle: Oracle): void {
|
|
236
|
-
if (oracle.id !== caseId) {
|
|
237
|
-
throw new Error(`Oracle id ${oracle.id} does not match directory ${caseId}.`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const finding of oracle.requiredFindings) {
|
|
241
|
-
if (finding.keywords.length === 0) {
|
|
242
|
-
throw new Error(`${caseId}.${finding.id} must declare keywords.`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function summarizeConditions(scores: CaseScore[]): ConditionSummary[] {
|
|
248
|
-
const byCondition = new Map<string, CaseScore[]>();
|
|
249
|
-
for (const score of scores) {
|
|
250
|
-
const group = byCondition.get(score.condition) ?? [];
|
|
251
|
-
group.push(score);
|
|
252
|
-
byCondition.set(score.condition, group);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return [...byCondition.entries()]
|
|
256
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
257
|
-
.map(([condition, group]) => {
|
|
258
|
-
const matchedFindings = sum(group, "matchedFindingIds");
|
|
259
|
-
const requiredFindings = sum(group, "requiredFindingCount");
|
|
260
|
-
const claimedFindings = sum(group, "claimedFindingCount");
|
|
261
|
-
const highMatchedFindings = sum(group, "highMatchedCount");
|
|
262
|
-
const highRequiredFindings = sum(group, "highFindingCount");
|
|
263
|
-
const falsePositiveTraps = sum(group, "falsePositiveTrapIds");
|
|
264
|
-
const precision = safeRatio(matchedFindings, claimedFindings);
|
|
265
|
-
const recall = safeRatio(matchedFindings, requiredFindings);
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
condition,
|
|
269
|
-
cases: group.length,
|
|
270
|
-
matchedFindings,
|
|
271
|
-
requiredFindings,
|
|
272
|
-
falsePositiveTraps,
|
|
273
|
-
claimedFindings,
|
|
274
|
-
highMatchedFindings,
|
|
275
|
-
highRequiredFindings,
|
|
276
|
-
recall,
|
|
277
|
-
precision,
|
|
278
|
-
f1: f1(precision, recall),
|
|
279
|
-
highRecall: safeRatio(highMatchedFindings, highRequiredFindings),
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function summarizeWins(cases: BenchmarkCase[], scores: CaseScore[]) {
|
|
285
|
-
const wins = new Map<string, number>();
|
|
286
|
-
|
|
287
|
-
for (const benchmarkCase of cases) {
|
|
288
|
-
const caseScores = scores.filter((score) => score.caseId === benchmarkCase.id);
|
|
289
|
-
const best = caseScores.reduce<CaseScore | undefined>((winner, score) => {
|
|
290
|
-
if (!winner) {
|
|
291
|
-
return score;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (score.f1 > winner.f1) {
|
|
295
|
-
return score;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
score.f1 === winner.f1 &&
|
|
300
|
-
score.highRecall > winner.highRecall
|
|
301
|
-
) {
|
|
302
|
-
return score;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
score.f1 === winner.f1 &&
|
|
307
|
-
score.highRecall === winner.highRecall &&
|
|
308
|
-
score.falsePositiveTrapIds.length < winner.falsePositiveTrapIds.length
|
|
309
|
-
) {
|
|
310
|
-
return score;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return winner;
|
|
314
|
-
}, undefined);
|
|
315
|
-
|
|
316
|
-
if (best) {
|
|
317
|
-
wins.set(best.condition, (wins.get(best.condition) ?? 0) + 1);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return [...wins.entries()]
|
|
322
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
323
|
-
.map(([condition, fixtures]) => ({ condition, fixtures }));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function extractClaims(text: string): string[] {
|
|
327
|
-
const json = parseJsonPayload(text);
|
|
328
|
-
if (!json || typeof json !== "object") {
|
|
329
|
-
return [];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const findings = (json as { findings?: unknown }).findings;
|
|
333
|
-
if (!Array.isArray(findings)) {
|
|
334
|
-
return [];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return findings
|
|
338
|
-
.map((finding) => {
|
|
339
|
-
if (!finding || typeof finding !== "object") {
|
|
340
|
-
return "";
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const line = (finding as { line?: unknown }).line;
|
|
344
|
-
const summary = (finding as { summary?: unknown }).summary;
|
|
345
|
-
return `${typeof line === "number" ? `line ${line}` : ""} ${
|
|
346
|
-
typeof summary === "string" ? summary : ""
|
|
347
|
-
}`.trim();
|
|
348
|
-
})
|
|
349
|
-
.filter(Boolean);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function parseJsonPayload(text: string): unknown {
|
|
353
|
-
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
354
|
-
const candidate = fenced ?? text.trim();
|
|
355
|
-
const first = candidate.indexOf("{");
|
|
356
|
-
const last = candidate.lastIndexOf("}");
|
|
357
|
-
|
|
358
|
-
if (first < 0 || last < first) {
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
return JSON.parse(candidate.slice(first, last + 1));
|
|
364
|
-
} catch {
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function splitFallbackClaims(text: string): string[] {
|
|
370
|
-
return text
|
|
371
|
-
.split(/\r?\n/)
|
|
372
|
-
.map((line) => line.trim())
|
|
373
|
-
.filter((line) =>
|
|
374
|
-
/^[-*]\s+/.test(line) ||
|
|
375
|
-
/^\d+[.)]\s+/.test(line) ||
|
|
376
|
-
/\bline\s+\d+\b/i.test(line)
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function matchesFinding(finding: RequiredFinding, texts: string[]): boolean {
|
|
381
|
-
return matchesTerms(finding.keywords, texts);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function matchesTerms(terms: string[], texts: string[]): boolean {
|
|
385
|
-
return texts.some((text) =>
|
|
386
|
-
terms.every((term) => text.includes(normalize(term)))
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function normalize(value: string): string {
|
|
391
|
-
return value.toLowerCase().replace(/[_-]/g, " ").replace(/\s+/g, " ").trim();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function safeRatio(numerator: number, denominator: number): number {
|
|
395
|
-
return denominator === 0 ? 0 : numerator / denominator;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function f1(precision: number, recall: number): number {
|
|
399
|
-
return precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function formatPercent(value: number): string {
|
|
403
|
-
return `${Math.round(value * 1000) / 10}%`;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function sum(group: CaseScore[], key: keyof CaseScore): number {
|
|
407
|
-
return group.reduce((total, score) => {
|
|
408
|
-
const value = score[key];
|
|
409
|
-
if (Array.isArray(value)) {
|
|
410
|
-
return total + value.length;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (typeof value === "number") {
|
|
414
|
-
return total + value;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return total;
|
|
418
|
-
}, 0);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async function hashScorerSource(): Promise<string> {
|
|
422
|
-
const source = await Bun.file(fileURLToPath(import.meta.url)).text();
|
|
423
|
-
return createHash("sha256").update(source).digest("hex");
|
|
424
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
type User = {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
email: string;
|
|
5
|
-
active: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type Mailer = {
|
|
9
|
-
send(to: string, body: string): Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
async function fetchUser(userId: string): Promise<User> {
|
|
13
|
-
return {
|
|
14
|
-
id: userId,
|
|
15
|
-
name: "Ada",
|
|
16
|
-
email: "ada@example.com",
|
|
17
|
-
active: true,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function sendWelcomeEmail(
|
|
22
|
-
userId: string,
|
|
23
|
-
mailer: Mailer,
|
|
24
|
-
): Promise<"sent" | "skipped"> {
|
|
25
|
-
const user = fetchUser(userId);
|
|
26
|
-
|
|
27
|
-
if (!user.active) {
|
|
28
|
-
return "skipped";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
await mailer.send(user.email, `Welcome ${user.name}`);
|
|
32
|
-
return "sent";
|
|
33
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "001-missing-await",
|
|
3
|
-
"title": "Missing await before user lookup",
|
|
4
|
-
"prompt": "Review the TypeScript fixture and list correctness bugs.",
|
|
5
|
-
"requiredFindings": [
|
|
6
|
-
{
|
|
7
|
-
"id": "missing-await-fetch-user",
|
|
8
|
-
"severity": "high",
|
|
9
|
-
"line": 24,
|
|
10
|
-
"description": "fetchUser returns a Promise but sendWelcomeEmail reads active, email, and name without awaiting it.",
|
|
11
|
-
"keywords": ["await", "fetchUser"]
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"id": "promise-property-access",
|
|
15
|
-
"severity": "medium",
|
|
16
|
-
"line": 26,
|
|
17
|
-
"description": "The branch reads user.active on a Promise, so the inactive-user skip path is broken.",
|
|
18
|
-
"keywords": ["active", "Promise"]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"knownFalsePositives": [
|
|
22
|
-
{
|
|
23
|
-
"id": "mailer-not-awaited",
|
|
24
|
-
"description": "Claims mailer.send is not awaited even though it is awaited.",
|
|
25
|
-
"terms": ["mailer.send", "not awaited"]
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":24,"summary":"fetchUser returns a Promise and must be awaited before reading active/email/name."},{"line":26,"summary":"Because user is still a Promise, user.active never checks the loaded user's active flag."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":24,"summary":"Missing await before fetchUser means user is a Promise."},{"line":29,"summary":"mailer.send is not awaited."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":24,"summary":"Missing await before fetchUser leaves user as a Promise."},{"line":26,"summary":"The active check reads active on a Promise instead of the loaded User, so inactive users are not skipped."}]}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
type ChildProcess = {
|
|
2
|
-
stdout: AsyncIterable<Uint8Array>;
|
|
3
|
-
stderr: AsyncIterable<Uint8Array>;
|
|
4
|
-
exited: Promise<number>;
|
|
5
|
-
kill(): void;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type Result = {
|
|
9
|
-
exitCode: number;
|
|
10
|
-
stdout: string;
|
|
11
|
-
stderr: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const decoder = new TextDecoder();
|
|
15
|
-
|
|
16
|
-
export async function collectChild(
|
|
17
|
-
child: ChildProcess,
|
|
18
|
-
timeoutMs: number,
|
|
19
|
-
): Promise<Result> {
|
|
20
|
-
let stdout = "";
|
|
21
|
-
let stderr = "";
|
|
22
|
-
|
|
23
|
-
void (async () => {
|
|
24
|
-
for await (const chunk of child.stdout) {
|
|
25
|
-
stdout += decoder.decode(chunk);
|
|
26
|
-
}
|
|
27
|
-
})();
|
|
28
|
-
|
|
29
|
-
void (async () => {
|
|
30
|
-
for await (const chunk of child.stderr) {
|
|
31
|
-
stderr += decoder.decode(chunk);
|
|
32
|
-
}
|
|
33
|
-
})();
|
|
34
|
-
|
|
35
|
-
const timeout = setTimeout(() => child.kill(), timeoutMs);
|
|
36
|
-
const exitCode = await child.exited;
|
|
37
|
-
clearTimeout(timeout);
|
|
38
|
-
|
|
39
|
-
return { exitCode, stdout, stderr };
|
|
40
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "002-timeout-drain",
|
|
3
|
-
"title": "Exit observed before output streams drain",
|
|
4
|
-
"prompt": "Review the TypeScript fixture and list correctness bugs.",
|
|
5
|
-
"requiredFindings": [
|
|
6
|
-
{
|
|
7
|
-
"id": "returns-before-stream-drain",
|
|
8
|
-
"severity": "high",
|
|
9
|
-
"line": 35,
|
|
10
|
-
"description": "collectChild returns after child.exited without awaiting the stdout and stderr reader loops, so captured output can be truncated.",
|
|
11
|
-
"keywords": ["await", "stdout", "stderr"]
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"id": "timeout-resolves-as-normal-exit",
|
|
15
|
-
"severity": "medium",
|
|
16
|
-
"line": 31,
|
|
17
|
-
"description": "The timeout kills the child but the result does not record that the process timed out.",
|
|
18
|
-
"keywords": ["timeout", "kill", "timed out"]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"knownFalsePositives": [
|
|
22
|
-
{
|
|
23
|
-
"id": "leaks-timeout-on-success",
|
|
24
|
-
"description": "Claims the timeout is never cleared even though clearTimeout is called.",
|
|
25
|
-
"terms": ["timeout", "never cleared"]
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":35,"summary":"The function awaits child.exited but never awaits the stdout and stderr reader loops, so output can be returned before it drains."},{"line":31,"summary":"When the timeout fires and kill is called, the returned Result does not mark the process as timed out."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":35,"summary":"Returns after child.exited without awaiting stdout and stderr collection."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":35,"summary":"collectChild must await both stdout and stderr drain loops before returning, otherwise output can be truncated."},{"line":31,"summary":"A timeout only calls kill; the result does not say the process timed out."}]}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
const ListTasksInput = z.object({
|
|
4
|
-
includeEvents: z.boolean().default(false),
|
|
5
|
-
maxEvents: z.number().int().nonnegative().default(25),
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
type Event = {
|
|
9
|
-
at: string;
|
|
10
|
-
message: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function selectEvents(events: Event[], rawInput: unknown): Event[] {
|
|
14
|
-
const input = ListTasksInput.parse(rawInput);
|
|
15
|
-
|
|
16
|
-
if (!input.includeEvents) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const maxEvents = input.maxEvents || 25;
|
|
21
|
-
return events.slice(-maxEvents);
|
|
22
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "003-schema-contract",
|
|
3
|
-
"title": "Boundary value lost by defaulting",
|
|
4
|
-
"prompt": "Review the TypeScript fixture and list correctness bugs.",
|
|
5
|
-
"requiredFindings": [
|
|
6
|
-
{
|
|
7
|
-
"id": "zero-max-events-ignored",
|
|
8
|
-
"severity": "high",
|
|
9
|
-
"line": 19,
|
|
10
|
-
"description": "maxEvents: 0 is valid in the schema, but input.maxEvents || 25 turns it into 25.",
|
|
11
|
-
"keywords": ["maxEvents", "0"]
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"id": "negative-slice-with-default",
|
|
15
|
-
"severity": "low",
|
|
16
|
-
"line": 20,
|
|
17
|
-
"description": "The code relies on slice(-maxEvents), so the zero case should be handled explicitly to return no events.",
|
|
18
|
-
"keywords": ["slice", "0"]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"knownFalsePositives": [
|
|
22
|
-
{
|
|
23
|
-
"id": "schema-allows-negative",
|
|
24
|
-
"description": "Claims the schema allows negative maxEvents even though nonnegative is used.",
|
|
25
|
-
"terms": ["negative", "maxEvents", "allowed"]
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":19,"summary":"maxEvents 0 is valid but input.maxEvents || 25 replaces it with the default."},{"line":20,"summary":"The slice call needs an explicit 0 case so maxEvents 0 returns no events."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":19,"summary":"maxEvents 0 is ignored because || 25 treats it as false."},{"line":4,"summary":"The schema allows negative maxEvents values."}]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"findings":[{"line":19,"summary":"Using input.maxEvents || 25 loses the valid maxEvents 0 boundary value."},{"line":20,"summary":"slice(-maxEvents) should handle 0 explicitly and return an empty event list."}]}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
type Task = {
|
|
2
|
-
id: string;
|
|
3
|
-
status: "running" | "succeeded" | "failed" | "cancelled";
|
|
4
|
-
cancel(): void;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
type Run = {
|
|
8
|
-
id: string;
|
|
9
|
-
status: "running" | "cancelled";
|
|
10
|
-
taskIds: string[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function cancelRun(run: Run, tasks: Map<string, Task>): Run {
|
|
14
|
-
if (run.status !== "running") {
|
|
15
|
-
return run;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
run.status = "cancelled";
|
|
19
|
-
|
|
20
|
-
for (const taskId of run.taskIds) {
|
|
21
|
-
const task = tasks.get(taskId);
|
|
22
|
-
if (!task || task.status !== "running") {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return run;
|
|
28
|
-
}
|