@pablozrrrr/jevguard 0.1.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/LICENSE +21 -0
- package/README.md +417 -0
- package/cli/main.js +1813 -0
- package/index.js +2022 -0
- package/package.json +53 -0
- package/skills/jevguard-rules/SKILL.md +158 -0
- package/skills/jevguard-rules/validate-rules.js +991 -0
package/index.js
ADDED
|
@@ -0,0 +1,2022 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/core/src/ports/types.ts
|
|
3
|
+
var SCOPE_CREEP_ANSWER = "scopeCreep";
|
|
4
|
+
var COMPLEXITY_ANSWER = "complexity";
|
|
5
|
+
// packages/core/src/evaluation/build-request.ts
|
|
6
|
+
var NOUL_INSTRUCTIONS = "Answer true when the attributed change violates the rule's Violation, excluding behavior described by the rule's Allowed exception. Answer false when the change does not violate the rule, including allowed behavior. Return the probability from 0 to 1 that the change violates the rule.";
|
|
7
|
+
function buildJevRequest(task, rule, evidence) {
|
|
8
|
+
return {
|
|
9
|
+
kind: "RULE",
|
|
10
|
+
task,
|
|
11
|
+
question: {
|
|
12
|
+
type: "noul",
|
|
13
|
+
instructions: NOUL_INSTRUCTIONS,
|
|
14
|
+
criteria: {
|
|
15
|
+
id: rule.id,
|
|
16
|
+
description: rule.description,
|
|
17
|
+
violation: rule.violation,
|
|
18
|
+
...rule.allowed === null ? {} : { allowed: rule.allowed }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// packages/core/src/evaluation/build-context.ts
|
|
25
|
+
function buildReviewContext(turn, rule, scopedPaths) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "RULE",
|
|
28
|
+
turnId: turn.id,
|
|
29
|
+
ruleId: rule.id,
|
|
30
|
+
severity: rule.severity,
|
|
31
|
+
scopedPaths
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// packages/core/src/evidence/defaults.ts
|
|
35
|
+
var DEFAULT_MAX_DIFF_LENGTH = 1e5;
|
|
36
|
+
var DEFAULT_EVIDENCE_POLICY = {
|
|
37
|
+
maxDiffLength: DEFAULT_MAX_DIFF_LENGTH,
|
|
38
|
+
allowedExtensions: [
|
|
39
|
+
".ts",
|
|
40
|
+
".tsx",
|
|
41
|
+
".mts",
|
|
42
|
+
".cts",
|
|
43
|
+
".js",
|
|
44
|
+
".jsx",
|
|
45
|
+
".mjs",
|
|
46
|
+
".cjs",
|
|
47
|
+
".json",
|
|
48
|
+
".jsonc",
|
|
49
|
+
".md",
|
|
50
|
+
".mdx",
|
|
51
|
+
".txt",
|
|
52
|
+
".yaml",
|
|
53
|
+
".yml",
|
|
54
|
+
".toml",
|
|
55
|
+
".css",
|
|
56
|
+
".scss",
|
|
57
|
+
".sass",
|
|
58
|
+
".less",
|
|
59
|
+
".html",
|
|
60
|
+
".htm",
|
|
61
|
+
".vue",
|
|
62
|
+
".svelte",
|
|
63
|
+
".py",
|
|
64
|
+
".rb",
|
|
65
|
+
".go",
|
|
66
|
+
".rs",
|
|
67
|
+
".java",
|
|
68
|
+
".kt",
|
|
69
|
+
".kts",
|
|
70
|
+
".scala",
|
|
71
|
+
".cs",
|
|
72
|
+
".php",
|
|
73
|
+
".c",
|
|
74
|
+
".h",
|
|
75
|
+
".cc",
|
|
76
|
+
".cpp",
|
|
77
|
+
".hpp",
|
|
78
|
+
".cxx",
|
|
79
|
+
".hh",
|
|
80
|
+
".swift",
|
|
81
|
+
".dart",
|
|
82
|
+
".lua",
|
|
83
|
+
".r",
|
|
84
|
+
".ex",
|
|
85
|
+
".exs",
|
|
86
|
+
".erl",
|
|
87
|
+
".sh",
|
|
88
|
+
".bash",
|
|
89
|
+
".zsh",
|
|
90
|
+
".fish",
|
|
91
|
+
".ps1",
|
|
92
|
+
".bat",
|
|
93
|
+
".cmd",
|
|
94
|
+
".sql",
|
|
95
|
+
".graphql",
|
|
96
|
+
".gql",
|
|
97
|
+
".proto",
|
|
98
|
+
".tf",
|
|
99
|
+
".hcl",
|
|
100
|
+
".xml",
|
|
101
|
+
".svg",
|
|
102
|
+
".csv",
|
|
103
|
+
".tsv",
|
|
104
|
+
".diff",
|
|
105
|
+
".patch"
|
|
106
|
+
],
|
|
107
|
+
deniedFileNames: [
|
|
108
|
+
".env",
|
|
109
|
+
".envrc",
|
|
110
|
+
".netrc",
|
|
111
|
+
"_netrc",
|
|
112
|
+
".npmrc",
|
|
113
|
+
".pypirc",
|
|
114
|
+
".pgpass",
|
|
115
|
+
".git-credentials",
|
|
116
|
+
".htpasswd",
|
|
117
|
+
".dockercfg",
|
|
118
|
+
"id_rsa",
|
|
119
|
+
"id_dsa",
|
|
120
|
+
"id_ecdsa",
|
|
121
|
+
"id_ed25519",
|
|
122
|
+
"credentials",
|
|
123
|
+
"credentials.json",
|
|
124
|
+
"credentials.yaml",
|
|
125
|
+
"credentials.yml",
|
|
126
|
+
"secrets",
|
|
127
|
+
"secrets.json",
|
|
128
|
+
"secrets.yaml",
|
|
129
|
+
"secrets.yml",
|
|
130
|
+
"shadow",
|
|
131
|
+
"master.key"
|
|
132
|
+
],
|
|
133
|
+
deniedExtensions: [
|
|
134
|
+
".pem",
|
|
135
|
+
".key",
|
|
136
|
+
".p12",
|
|
137
|
+
".pfx",
|
|
138
|
+
".pkcs12",
|
|
139
|
+
".jks",
|
|
140
|
+
".keystore",
|
|
141
|
+
".ppk",
|
|
142
|
+
".p8",
|
|
143
|
+
".der"
|
|
144
|
+
],
|
|
145
|
+
deniedDirectoryNames: [".ssh", ".aws", ".gnupg", ".gpg", ".azure", ".kube", ".docker"]
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// packages/core/src/evidence/paths.ts
|
|
149
|
+
function normalizePath(path) {
|
|
150
|
+
return path.replace(/\\/g, "/");
|
|
151
|
+
}
|
|
152
|
+
function pathSegments(path) {
|
|
153
|
+
return normalizePath(path).split("/").filter((segment) => segment !== "");
|
|
154
|
+
}
|
|
155
|
+
function pathBasename(path) {
|
|
156
|
+
const segments = pathSegments(path);
|
|
157
|
+
return segments[segments.length - 1] ?? "";
|
|
158
|
+
}
|
|
159
|
+
function fileExtension(path) {
|
|
160
|
+
const basename = pathBasename(path).toLowerCase();
|
|
161
|
+
const dot = basename.lastIndexOf(".");
|
|
162
|
+
return dot <= 0 ? "" : basename.slice(dot);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// packages/core/src/evidence/safety.ts
|
|
166
|
+
var DENIED_SENSITIVE = { allowed: false, reason: "DENIED_SENSITIVE_PATH" };
|
|
167
|
+
var DENIED_EXTENSION = { allowed: false, reason: "EXTENSION_NOT_ALLOWED" };
|
|
168
|
+
function checkFilePath(path, policy) {
|
|
169
|
+
const basename = pathBasename(path).toLowerCase();
|
|
170
|
+
if (matchesDeniedFileName(basename, policy)) {
|
|
171
|
+
return DENIED_SENSITIVE;
|
|
172
|
+
}
|
|
173
|
+
const segments = pathSegments(path).map((segment) => segment.toLowerCase());
|
|
174
|
+
if (segments.some((segment) => policy.deniedDirectoryNames.includes(segment))) {
|
|
175
|
+
return DENIED_SENSITIVE;
|
|
176
|
+
}
|
|
177
|
+
const extension = fileExtension(path);
|
|
178
|
+
if (policy.deniedExtensions.includes(extension)) {
|
|
179
|
+
return DENIED_SENSITIVE;
|
|
180
|
+
}
|
|
181
|
+
if (!policy.allowedExtensions.includes(extension)) {
|
|
182
|
+
return DENIED_EXTENSION;
|
|
183
|
+
}
|
|
184
|
+
return { allowed: true };
|
|
185
|
+
}
|
|
186
|
+
function matchesDeniedFileName(basename, policy) {
|
|
187
|
+
return policy.deniedFileNames.some((name) => name.startsWith(".") ? basename === name || basename.startsWith(`${name}.`) : basename === name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// packages/core/src/evidence/scope.ts
|
|
191
|
+
function matchesScope(path, scope) {
|
|
192
|
+
return globToRegExp(scope).test(normalizePath(path));
|
|
193
|
+
}
|
|
194
|
+
function globToRegExp(glob) {
|
|
195
|
+
const source = glob.replace(/\\/g, "/");
|
|
196
|
+
let pattern = "";
|
|
197
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
198
|
+
const char = source[index] ?? "";
|
|
199
|
+
if (char === "*") {
|
|
200
|
+
if (source[index + 1] === "*") {
|
|
201
|
+
const followedBySlash = source[index + 2] === "/";
|
|
202
|
+
pattern += followedBySlash ? "(?:.*/)?" : ".*";
|
|
203
|
+
index += followedBySlash ? 2 : 1;
|
|
204
|
+
} else {
|
|
205
|
+
pattern += "[^/]*";
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (char === "?") {
|
|
210
|
+
pattern += "[^/]";
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
pattern += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
+
}
|
|
215
|
+
return new RegExp(`^${pattern}$`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// packages/core/src/evidence/select-evidence.ts
|
|
219
|
+
function selectRuleEvidence(turn, rule, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
220
|
+
if (hasNoAttributedPatch(turn)) {
|
|
221
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
222
|
+
}
|
|
223
|
+
const applicableFiles = selectApplicableFiles(turn.files, rule.scope);
|
|
224
|
+
if (applicableFiles.length === 0) {
|
|
225
|
+
return { status: "SKIPPED", reason: "NO_SCOPE_MATCH" };
|
|
226
|
+
}
|
|
227
|
+
return mountEvidence(applicableFiles, policy);
|
|
228
|
+
}
|
|
229
|
+
function selectTurnEvidence(turn, policy = DEFAULT_EVIDENCE_POLICY) {
|
|
230
|
+
if (hasNoAttributedPatch(turn)) {
|
|
231
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
232
|
+
}
|
|
233
|
+
const attributedFiles = turn.files.filter((file) => file.patch.trim() !== "");
|
|
234
|
+
return mountEvidence(attributedFiles, policy);
|
|
235
|
+
}
|
|
236
|
+
function mountEvidence(files, policy) {
|
|
237
|
+
const diff = assembleDiff(files);
|
|
238
|
+
if (diff.trim() === "") {
|
|
239
|
+
return { status: "SKIPPED", reason: "NO_ATTRIBUTED_PATCH" };
|
|
240
|
+
}
|
|
241
|
+
if (diff.length > policy.maxDiffLength) {
|
|
242
|
+
return { status: "UNAVAILABLE", reason: "OVERSIZED_DIFF" };
|
|
243
|
+
}
|
|
244
|
+
if (files.some((file) => !checkFilePath(file.path, policy).allowed)) {
|
|
245
|
+
return { status: "UNAVAILABLE", reason: "BLOCKED_EVIDENCE" };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
status: "SELECTED",
|
|
249
|
+
evidence: { files: files.map((file) => file.path), diff }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function hasNoAttributedPatch(turn) {
|
|
253
|
+
return turn.files.every((file) => file.patch.trim() === "");
|
|
254
|
+
}
|
|
255
|
+
function selectApplicableFiles(files, scope) {
|
|
256
|
+
if (scope === null) {
|
|
257
|
+
return files;
|
|
258
|
+
}
|
|
259
|
+
return files.filter((file) => matchesScope(file.path, scope));
|
|
260
|
+
}
|
|
261
|
+
function assembleDiff(files) {
|
|
262
|
+
return files.map((file) => file.patch).join(`
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// packages/core/src/gate/probability.ts
|
|
267
|
+
function isProbability(value) {
|
|
268
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// packages/core/src/gate/evaluate-gate.ts
|
|
272
|
+
function evaluateGate(severity, violationProbability, config) {
|
|
273
|
+
if (!isProbability(violationProbability)) {
|
|
274
|
+
return { outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
275
|
+
}
|
|
276
|
+
switch (severity) {
|
|
277
|
+
case "error":
|
|
278
|
+
return evaluateErrorGate(violationProbability, config.thresholds.error);
|
|
279
|
+
case "warning":
|
|
280
|
+
return evaluateWarningGate(violationProbability, config.thresholds.warning);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function evaluateErrorGate(probability, thresholds) {
|
|
284
|
+
if (probability < thresholds.warn) {
|
|
285
|
+
return { outcome: "PASS", violationProbability: probability };
|
|
286
|
+
}
|
|
287
|
+
if (probability < thresholds.fail) {
|
|
288
|
+
return { outcome: "WARN", violationProbability: probability };
|
|
289
|
+
}
|
|
290
|
+
return { outcome: "FAIL", violationProbability: probability };
|
|
291
|
+
}
|
|
292
|
+
function evaluateWarningGate(probability, thresholds) {
|
|
293
|
+
if (probability < thresholds.warn) {
|
|
294
|
+
return { outcome: "PASS", violationProbability: probability };
|
|
295
|
+
}
|
|
296
|
+
return { outcome: "WARN", violationProbability: probability };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// packages/core/src/evaluation/evaluate-with-port.ts
|
|
300
|
+
async function evaluateWithPort(port, request) {
|
|
301
|
+
try {
|
|
302
|
+
const result = await port.evaluate(request);
|
|
303
|
+
return result.kind === "RULE" ? result : { kind: "RULE", status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
304
|
+
} catch {
|
|
305
|
+
return { kind: "RULE", status: "FAILED", reason: "API_ERROR" };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function evaluateBuiltInBatchWithPort(port, request) {
|
|
309
|
+
try {
|
|
310
|
+
const result = await port.evaluate(request);
|
|
311
|
+
return result.kind === "BUILT_IN_BATCH" ? result : { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
312
|
+
} catch {
|
|
313
|
+
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason: "API_ERROR" };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// packages/core/src/evaluation/evaluate-rule.ts
|
|
318
|
+
async function evaluateRule(input, dependencies) {
|
|
319
|
+
const selection = selectRuleEvidence(input.turn, input.rule, input.evidencePolicy);
|
|
320
|
+
if (selection.status === "SKIPPED") {
|
|
321
|
+
const context = buildReviewContext(input.turn, input.rule, []);
|
|
322
|
+
return { ...context, outcome: "SKIPPED", reason: selection.reason };
|
|
323
|
+
}
|
|
324
|
+
if (selection.status === "UNAVAILABLE") {
|
|
325
|
+
const context = buildReviewContext(input.turn, input.rule, []);
|
|
326
|
+
return { ...context, outcome: "UNAVAILABLE", reason: selection.reason };
|
|
327
|
+
}
|
|
328
|
+
const context = buildReviewContext(input.turn, input.rule, selection.evidence.files);
|
|
329
|
+
const request = buildJevRequest(input.turn.task, input.rule, selection.evidence);
|
|
330
|
+
const evaluation = await evaluateWithPort(dependencies.jev, request);
|
|
331
|
+
if (evaluation.status === "FAILED") {
|
|
332
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
333
|
+
}
|
|
334
|
+
const gate = evaluateGate(input.rule.severity, evaluation.noul.violationProbability, input.gateConfig);
|
|
335
|
+
return { ...context, ...gate };
|
|
336
|
+
}
|
|
337
|
+
// packages/core/src/review/aggregate.ts
|
|
338
|
+
var VERDICT_RANK = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
339
|
+
function aggregateReview(turnId, results) {
|
|
340
|
+
const snapshot = [...results];
|
|
341
|
+
return { turnId, results: snapshot, summary: summarize(snapshot) };
|
|
342
|
+
}
|
|
343
|
+
function summarize(results) {
|
|
344
|
+
const counts = countOutcomes(results);
|
|
345
|
+
return {
|
|
346
|
+
verdict: highestVerdict(results),
|
|
347
|
+
hasUnavailable: counts.unavailable > 0,
|
|
348
|
+
counts
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function countOutcomes(results) {
|
|
352
|
+
const counts = { pass: 0, warn: 0, fail: 0, skipped: 0, unavailable: 0 };
|
|
353
|
+
for (const result of results) {
|
|
354
|
+
switch (result.outcome) {
|
|
355
|
+
case "PASS":
|
|
356
|
+
counts.pass += 1;
|
|
357
|
+
break;
|
|
358
|
+
case "WARN":
|
|
359
|
+
counts.warn += 1;
|
|
360
|
+
break;
|
|
361
|
+
case "FAIL":
|
|
362
|
+
counts.fail += 1;
|
|
363
|
+
break;
|
|
364
|
+
case "SKIPPED":
|
|
365
|
+
counts.skipped += 1;
|
|
366
|
+
break;
|
|
367
|
+
case "UNAVAILABLE":
|
|
368
|
+
counts.unavailable += 1;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return counts;
|
|
373
|
+
}
|
|
374
|
+
function highestVerdict(results) {
|
|
375
|
+
let highest = null;
|
|
376
|
+
for (const result of results) {
|
|
377
|
+
if (!isSemanticVerdict(result.outcome)) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (highest === null || VERDICT_RANK[result.outcome] > VERDICT_RANK[highest]) {
|
|
381
|
+
highest = result.outcome;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return highest;
|
|
385
|
+
}
|
|
386
|
+
function isSemanticVerdict(outcome) {
|
|
387
|
+
return outcome === "PASS" || outcome === "WARN" || outcome === "FAIL";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// packages/core/src/evaluation/evaluate-rules.ts
|
|
391
|
+
async function evaluateRules(input, dependencies) {
|
|
392
|
+
const results = [];
|
|
393
|
+
for (const rule of input.rules) {
|
|
394
|
+
results.push(await evaluateRuleResult(input, rule, dependencies));
|
|
395
|
+
}
|
|
396
|
+
return aggregateReview(input.turn.id, results);
|
|
397
|
+
}
|
|
398
|
+
async function evaluateRuleResult(input, rule, dependencies) {
|
|
399
|
+
if (rule.status === "INVALID") {
|
|
400
|
+
return invalidRuleResult(input.turn.id, rule);
|
|
401
|
+
}
|
|
402
|
+
if (input.gateConfig.status === "INVALID") {
|
|
403
|
+
return invalidConfigResult(input.turn, rule.rule);
|
|
404
|
+
}
|
|
405
|
+
const ruleInput = {
|
|
406
|
+
turn: input.turn,
|
|
407
|
+
rule: rule.rule,
|
|
408
|
+
gateConfig: input.gateConfig.config,
|
|
409
|
+
evidencePolicy: input.evidencePolicy
|
|
410
|
+
};
|
|
411
|
+
return evaluateRule(ruleInput, dependencies);
|
|
412
|
+
}
|
|
413
|
+
function invalidRuleResult(turnId, failure) {
|
|
414
|
+
return {
|
|
415
|
+
kind: "RULE",
|
|
416
|
+
turnId,
|
|
417
|
+
ruleId: failure.ruleId,
|
|
418
|
+
severity: null,
|
|
419
|
+
scopedPaths: [],
|
|
420
|
+
outcome: "UNAVAILABLE",
|
|
421
|
+
reason: "INVALID_RULE"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function invalidConfigResult(turn, rule) {
|
|
425
|
+
return {
|
|
426
|
+
...buildReviewContext(turn, rule, []),
|
|
427
|
+
outcome: "UNAVAILABLE",
|
|
428
|
+
reason: "INVALID_CONFIG"
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
// packages/core/src/rules/headings.ts
|
|
432
|
+
var RULE_HEADING = /^##\s+(.*)$/;
|
|
433
|
+
var SECTION_HEADING = /^###\s+(.*)$/;
|
|
434
|
+
function matchRuleHeading(line) {
|
|
435
|
+
const match = RULE_HEADING.exec(line);
|
|
436
|
+
if (match === null) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
return (match[1] ?? "").trim();
|
|
440
|
+
}
|
|
441
|
+
function matchSectionHeading(line) {
|
|
442
|
+
const match = SECTION_HEADING.exec(line);
|
|
443
|
+
if (match === null) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
return (match[1] ?? "").trim();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// packages/core/src/rules/metadata.ts
|
|
450
|
+
var METADATA_ENTRY = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
451
|
+
function parseRuleMetadata(lines) {
|
|
452
|
+
let severity = null;
|
|
453
|
+
let scope = null;
|
|
454
|
+
const seenKeys = new Set;
|
|
455
|
+
for (const line of collectMetadataLines(lines)) {
|
|
456
|
+
const entry = parseMetadataEntry(line);
|
|
457
|
+
if (entry === null) {
|
|
458
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
459
|
+
}
|
|
460
|
+
if (seenKeys.has(entry.key)) {
|
|
461
|
+
return { ok: false, code: "DUPLICATE_METADATA" };
|
|
462
|
+
}
|
|
463
|
+
seenKeys.add(entry.key);
|
|
464
|
+
if (entry.key === "severity") {
|
|
465
|
+
const parsed = parseSeverity(entry.value);
|
|
466
|
+
if (parsed === null) {
|
|
467
|
+
return { ok: false, code: "INVALID_SEVERITY" };
|
|
468
|
+
}
|
|
469
|
+
severity = parsed;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (entry.key === "scope") {
|
|
473
|
+
if (entry.value === "") {
|
|
474
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
475
|
+
}
|
|
476
|
+
scope = entry.value;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
return { ok: false, code: "MALFORMED_METADATA" };
|
|
480
|
+
}
|
|
481
|
+
if (severity === null) {
|
|
482
|
+
return { ok: false, code: "MISSING_SEVERITY" };
|
|
483
|
+
}
|
|
484
|
+
return { ok: true, value: { severity, scope } };
|
|
485
|
+
}
|
|
486
|
+
function collectMetadataLines(lines) {
|
|
487
|
+
const collected = [];
|
|
488
|
+
for (const line of lines) {
|
|
489
|
+
if (matchSectionHeading(line) !== null) {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (line.trim() !== "") {
|
|
493
|
+
collected.push(line);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return collected;
|
|
497
|
+
}
|
|
498
|
+
function parseMetadataEntry(line) {
|
|
499
|
+
const match = METADATA_ENTRY.exec(line);
|
|
500
|
+
if (match === null) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return { key: match[1] ?? "", value: (match[2] ?? "").trim() };
|
|
504
|
+
}
|
|
505
|
+
function parseSeverity(value) {
|
|
506
|
+
switch (value) {
|
|
507
|
+
case "error":
|
|
508
|
+
case "warning":
|
|
509
|
+
return value;
|
|
510
|
+
default:
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// packages/core/src/rules/sections.ts
|
|
516
|
+
var SECTION_NAMES = ["Rule", "Violation", "Allowed"];
|
|
517
|
+
function parseRuleSections(lines) {
|
|
518
|
+
const collected = collectSections(lines);
|
|
519
|
+
if (!collected.ok) {
|
|
520
|
+
return collected;
|
|
521
|
+
}
|
|
522
|
+
return validateSections(collected.value);
|
|
523
|
+
}
|
|
524
|
+
function collectSections(lines) {
|
|
525
|
+
const sections = new Map;
|
|
526
|
+
let current = null;
|
|
527
|
+
let content = [];
|
|
528
|
+
for (const line of lines) {
|
|
529
|
+
const heading = matchSectionHeading(line);
|
|
530
|
+
if (heading === null) {
|
|
531
|
+
if (current !== null) {
|
|
532
|
+
content.push(line);
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (current !== null) {
|
|
537
|
+
sections.set(current, joinContent(content));
|
|
538
|
+
}
|
|
539
|
+
if (!SECTION_NAMES.includes(heading)) {
|
|
540
|
+
return { ok: false, code: "UNKNOWN_SECTION" };
|
|
541
|
+
}
|
|
542
|
+
if (sections.has(heading)) {
|
|
543
|
+
return { ok: false, code: "DUPLICATE_SECTION" };
|
|
544
|
+
}
|
|
545
|
+
current = heading;
|
|
546
|
+
content = [];
|
|
547
|
+
}
|
|
548
|
+
if (current !== null) {
|
|
549
|
+
sections.set(current, joinContent(content));
|
|
550
|
+
}
|
|
551
|
+
return { ok: true, value: sections };
|
|
552
|
+
}
|
|
553
|
+
function validateSections(sections) {
|
|
554
|
+
const rule = sections.get("Rule");
|
|
555
|
+
const violation = sections.get("Violation");
|
|
556
|
+
const allowed = sections.get("Allowed") ?? null;
|
|
557
|
+
if (rule === undefined) {
|
|
558
|
+
return { ok: false, code: "MISSING_RULE" };
|
|
559
|
+
}
|
|
560
|
+
if (violation === undefined) {
|
|
561
|
+
return { ok: false, code: "MISSING_VIOLATION" };
|
|
562
|
+
}
|
|
563
|
+
if (rule === "") {
|
|
564
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
565
|
+
}
|
|
566
|
+
if (violation === "") {
|
|
567
|
+
return { ok: false, code: "EMPTY_SECTION" };
|
|
568
|
+
}
|
|
569
|
+
if (allowed === "") {
|
|
570
|
+
return { ok: false, code: "EMPTY_ALLOWED" };
|
|
571
|
+
}
|
|
572
|
+
if (allowed !== null && allowed === violation) {
|
|
573
|
+
return { ok: false, code: "ALLOWED_EQUALS_VIOLATION" };
|
|
574
|
+
}
|
|
575
|
+
return { ok: true, value: { rule, violation, allowed } };
|
|
576
|
+
}
|
|
577
|
+
function joinContent(lines) {
|
|
578
|
+
return lines.join(`
|
|
579
|
+
`).trim();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// packages/core/src/rules/blocks.ts
|
|
583
|
+
var RULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
584
|
+
function isValidRuleId(id) {
|
|
585
|
+
return RULE_ID.test(id);
|
|
586
|
+
}
|
|
587
|
+
function collectRuleBlocks(text) {
|
|
588
|
+
const blocks = [];
|
|
589
|
+
let current = null;
|
|
590
|
+
for (const line of splitLines(text)) {
|
|
591
|
+
const id = matchRuleHeading(line);
|
|
592
|
+
if (id !== null) {
|
|
593
|
+
current = { id, body: [] };
|
|
594
|
+
blocks.push(current);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (current !== null) {
|
|
598
|
+
current.body.push(line);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return blocks;
|
|
602
|
+
}
|
|
603
|
+
function parseRuleBlock(block, duplicate) {
|
|
604
|
+
if (!isValidRuleId(block.id)) {
|
|
605
|
+
return ruleCandidateFailure("INVALID_ID", null);
|
|
606
|
+
}
|
|
607
|
+
if (duplicate) {
|
|
608
|
+
return ruleCandidateFailure("DUPLICATE_ID", block.id);
|
|
609
|
+
}
|
|
610
|
+
const metadata = parseRuleMetadata(block.body);
|
|
611
|
+
if (!metadata.ok) {
|
|
612
|
+
return ruleCandidateFailure(metadata.code, block.id);
|
|
613
|
+
}
|
|
614
|
+
const sections = parseRuleSections(block.body);
|
|
615
|
+
if (!sections.ok) {
|
|
616
|
+
return ruleCandidateFailure(sections.code, block.id);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
status: "PARSED",
|
|
620
|
+
rule: buildParsedRule(block.id, metadata.value, sections.value)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function ruleCandidateFailure(code, ruleId) {
|
|
624
|
+
return { status: "INVALID", reason: "INVALID_RULE", code, ruleId };
|
|
625
|
+
}
|
|
626
|
+
function splitLines(text) {
|
|
627
|
+
return text.replace(/\r\n?/g, `
|
|
628
|
+
`).split(`
|
|
629
|
+
`);
|
|
630
|
+
}
|
|
631
|
+
function buildParsedRule(id, metadata, sections) {
|
|
632
|
+
return {
|
|
633
|
+
id,
|
|
634
|
+
severity: metadata.severity,
|
|
635
|
+
scope: metadata.scope,
|
|
636
|
+
description: sections.rule,
|
|
637
|
+
violation: sections.violation,
|
|
638
|
+
allowed: sections.allowed
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
// packages/core/src/rules/parse-rules.ts
|
|
642
|
+
function parseRules(text) {
|
|
643
|
+
const blocks = collectRuleBlocks(text);
|
|
644
|
+
if (blocks.length === 0) {
|
|
645
|
+
return [ruleCandidateFailure("MISSING_ID", null)];
|
|
646
|
+
}
|
|
647
|
+
const duplicates = findDuplicateIds(blocks.map((block) => block.id));
|
|
648
|
+
return blocks.map((block) => parseRuleBlock(block, duplicates.has(block.id)));
|
|
649
|
+
}
|
|
650
|
+
function findDuplicateIds(ids) {
|
|
651
|
+
const counts = new Map;
|
|
652
|
+
for (const id of ids) {
|
|
653
|
+
if (!isValidRuleId(id)) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
657
|
+
}
|
|
658
|
+
const duplicates = new Set;
|
|
659
|
+
for (const [id, count] of counts) {
|
|
660
|
+
if (count > 1) {
|
|
661
|
+
duplicates.add(id);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return duplicates;
|
|
665
|
+
}
|
|
666
|
+
// packages/core/src/builtins/complexity.ts
|
|
667
|
+
var COMPLEXITY_CHECK_ID = "COMPLEXITY";
|
|
668
|
+
var COMPLEXITY_GATE_CONFIG = {
|
|
669
|
+
version: 1,
|
|
670
|
+
thresholds: {
|
|
671
|
+
error: { warn: 0.4, fail: 0.7 },
|
|
672
|
+
warning: { warn: 0.5 }
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
var COMPLEXITY_INSTRUCTIONS = "Does this attributed change introduce material complexity that is disproportionate to, or not reasonably necessary for, completing the user's task? Necessary supporting changes, tests, validation, error handling, and complexity explicitly required by the task should not count as unnecessary complexity. Return the probability from 0 to 1 that the change introduces disproportionate or unnecessary complexity.";
|
|
676
|
+
var COMPLEXITY_DESCRIPTION = "Material complexity introduced by the attributed change that is disproportionate to the problem and not reasonably necessary for completing the task.";
|
|
677
|
+
var COMPLEXITY_VIOLATION = "The change adds unnecessary abstractions, layers or indirections without proportional gain, new dependencies without clear need, excessive configuration, premature generalization, or structure materially larger than the problem requires.";
|
|
678
|
+
var COMPLEXITY_ALLOWED = "Complexity explicitly required by the task, and necessary supporting changes, tests, validation, error handling, auxiliary changes, or following an existing codebase abstraction to avoid breaking its pattern.";
|
|
679
|
+
function buildComplexityQuestion() {
|
|
680
|
+
return {
|
|
681
|
+
type: "noul",
|
|
682
|
+
instructions: COMPLEXITY_INSTRUCTIONS,
|
|
683
|
+
criteria: {
|
|
684
|
+
id: COMPLEXITY_CHECK_ID,
|
|
685
|
+
description: COMPLEXITY_DESCRIPTION,
|
|
686
|
+
violation: COMPLEXITY_VIOLATION,
|
|
687
|
+
allowed: COMPLEXITY_ALLOWED
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function buildComplexityContext(turn, scopedPaths) {
|
|
692
|
+
return {
|
|
693
|
+
kind: "BUILT_IN",
|
|
694
|
+
turnId: turn.id,
|
|
695
|
+
ruleId: null,
|
|
696
|
+
checkId: COMPLEXITY_CHECK_ID,
|
|
697
|
+
severity: "warning",
|
|
698
|
+
scopedPaths
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// packages/core/src/builtins/scope-creep.ts
|
|
703
|
+
var SCOPE_CREEP_CHECK_ID = "SCOPE-CREEP";
|
|
704
|
+
var SCOPE_CREEP_GATE_CONFIG = {
|
|
705
|
+
version: 1,
|
|
706
|
+
thresholds: {
|
|
707
|
+
error: { warn: 0.65, fail: 0.9 },
|
|
708
|
+
warning: { warn: 0.6 }
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
var SCOPE_CREEP_INSTRUCTIONS = "Answer true when the attributed change contains scope creep: material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task. Answer false when every change is requested work, or supporting, testing, or documentation work reasonably necessary to complete the requested work, including immaterial incidental edits. Return the probability from 0 to 1 that the change is scope creep.";
|
|
712
|
+
var SCOPE_CREEP_DESCRIPTION = "A change is scope creep when it introduces material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete it.";
|
|
713
|
+
var SCOPE_CREEP_VIOLATION = "The change adds material functional, behavioral, architectural, dependency, configuration, documentation, or refactoring work that the task did not request and that is not reasonably necessary to complete the task.";
|
|
714
|
+
var SCOPE_CREEP_ALLOWED = "Changes the task requested, and supporting, testing, and documentation changes reasonably necessary to complete the requested work, including immaterial incidental edits.";
|
|
715
|
+
function buildScopeCreepQuestion() {
|
|
716
|
+
return {
|
|
717
|
+
type: "noul",
|
|
718
|
+
instructions: SCOPE_CREEP_INSTRUCTIONS,
|
|
719
|
+
criteria: {
|
|
720
|
+
id: SCOPE_CREEP_CHECK_ID,
|
|
721
|
+
description: SCOPE_CREEP_DESCRIPTION,
|
|
722
|
+
violation: SCOPE_CREEP_VIOLATION,
|
|
723
|
+
allowed: SCOPE_CREEP_ALLOWED
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function buildScopeCreepContext(turn, scopedPaths) {
|
|
728
|
+
return {
|
|
729
|
+
kind: "BUILT_IN",
|
|
730
|
+
turnId: turn.id,
|
|
731
|
+
ruleId: null,
|
|
732
|
+
checkId: SCOPE_CREEP_CHECK_ID,
|
|
733
|
+
severity: "error",
|
|
734
|
+
scopedPaths
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// packages/core/src/builtins/evaluate-builtins.ts
|
|
739
|
+
function buildBuiltInBatchRequest(task, evidence) {
|
|
740
|
+
return {
|
|
741
|
+
kind: "BUILT_IN_BATCH",
|
|
742
|
+
task,
|
|
743
|
+
questions: {
|
|
744
|
+
[SCOPE_CREEP_ANSWER]: buildScopeCreepQuestion(),
|
|
745
|
+
[COMPLEXITY_ANSWER]: buildComplexityQuestion()
|
|
746
|
+
},
|
|
747
|
+
change: { files: evidence.files, diff: evidence.diff }
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
async function evaluateBuiltIns(input, dependencies) {
|
|
751
|
+
const selection = selectTurnEvidence(input.turn, input.evidencePolicy);
|
|
752
|
+
if (selection.status === "SKIPPED") {
|
|
753
|
+
return {
|
|
754
|
+
scopeCreep: {
|
|
755
|
+
...buildScopeCreepContext(input.turn, []),
|
|
756
|
+
outcome: "SKIPPED",
|
|
757
|
+
reason: selection.reason
|
|
758
|
+
},
|
|
759
|
+
complexity: {
|
|
760
|
+
...buildComplexityContext(input.turn, []),
|
|
761
|
+
outcome: "SKIPPED",
|
|
762
|
+
reason: selection.reason
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (selection.status === "UNAVAILABLE") {
|
|
767
|
+
return {
|
|
768
|
+
scopeCreep: {
|
|
769
|
+
...buildScopeCreepContext(input.turn, []),
|
|
770
|
+
outcome: "UNAVAILABLE",
|
|
771
|
+
reason: selection.reason
|
|
772
|
+
},
|
|
773
|
+
complexity: {
|
|
774
|
+
...buildComplexityContext(input.turn, []),
|
|
775
|
+
outcome: "UNAVAILABLE",
|
|
776
|
+
reason: selection.reason
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const evidence = selection.evidence;
|
|
781
|
+
const request = buildBuiltInBatchRequest(input.turn.task, evidence);
|
|
782
|
+
const evaluation = await evaluateBuiltInBatchWithPort(dependencies.jev, request);
|
|
783
|
+
const scopeCreepContext = buildScopeCreepContext(input.turn, evidence.files);
|
|
784
|
+
const complexityContext = buildComplexityContext(input.turn, evidence.files);
|
|
785
|
+
if (evaluation.status === "FAILED") {
|
|
786
|
+
return {
|
|
787
|
+
scopeCreep: { ...scopeCreepContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" },
|
|
788
|
+
complexity: { ...complexityContext, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" }
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
scopeCreep: gateScopeCreep(evaluation.answers.scopeCreep, scopeCreepContext),
|
|
793
|
+
complexity: gateComplexity(evaluation.answers.complexity, complexityContext)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function gateScopeCreep(answer, context) {
|
|
797
|
+
if (answer.status === "FAILED") {
|
|
798
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
...context,
|
|
802
|
+
...evaluateGate("error", answer.noul.violationProbability, SCOPE_CREEP_GATE_CONFIG)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function gateComplexity(answer, context) {
|
|
806
|
+
if (answer.status === "FAILED") {
|
|
807
|
+
return { ...context, outcome: "UNAVAILABLE", reason: "JEV_FAILURE" };
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
...context,
|
|
811
|
+
...evaluateGate("warning", answer.noul.violationProbability, COMPLEXITY_GATE_CONFIG)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
// packages/core/src/concurrency/fifo-port.ts
|
|
815
|
+
function createFifoJevPort(port, config) {
|
|
816
|
+
const maxConcurrency = config.maxConcurrency;
|
|
817
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
818
|
+
throw new RangeError("maxConcurrency must be a positive integer");
|
|
819
|
+
}
|
|
820
|
+
const queue = [];
|
|
821
|
+
let active = 0;
|
|
822
|
+
function drain() {
|
|
823
|
+
while (active < maxConcurrency && queue.length > 0) {
|
|
824
|
+
const pending = queue.shift();
|
|
825
|
+
if (pending === undefined) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
active += 1;
|
|
829
|
+
start(pending);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function start(pending) {
|
|
833
|
+
Promise.resolve().then(() => port.evaluate(pending.request)).then((result) => {
|
|
834
|
+
active -= 1;
|
|
835
|
+
pending.settle(result);
|
|
836
|
+
drain();
|
|
837
|
+
}, (error) => {
|
|
838
|
+
active -= 1;
|
|
839
|
+
pending.fail(error);
|
|
840
|
+
drain();
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
evaluate(request) {
|
|
845
|
+
return new Promise((settle, fail) => {
|
|
846
|
+
queue.push({ request, settle, fail });
|
|
847
|
+
drain();
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
// packages/core/src/gate/defaults.ts
|
|
853
|
+
var DEFAULT_GATE_CONFIG = {
|
|
854
|
+
version: 1,
|
|
855
|
+
thresholds: {
|
|
856
|
+
error: { warn: 0.4, fail: 0.7 },
|
|
857
|
+
warning: { warn: 0.6 }
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
// packages/core/src/gate/config.ts
|
|
861
|
+
function resolveGateConfig(value) {
|
|
862
|
+
if (value === null || value === undefined) {
|
|
863
|
+
return { status: "VALID", config: DEFAULT_GATE_CONFIG };
|
|
864
|
+
}
|
|
865
|
+
if (!isRecord(value) || value.version !== 1) {
|
|
866
|
+
return invalid();
|
|
867
|
+
}
|
|
868
|
+
const thresholds = value.thresholds;
|
|
869
|
+
if (!isRecord(thresholds)) {
|
|
870
|
+
return invalid();
|
|
871
|
+
}
|
|
872
|
+
const error = thresholds.error;
|
|
873
|
+
const warning = thresholds.warning;
|
|
874
|
+
if (!isRecord(error) || !isRecord(warning)) {
|
|
875
|
+
return invalid();
|
|
876
|
+
}
|
|
877
|
+
const errorWarn = error.warn;
|
|
878
|
+
const errorFail = error.fail;
|
|
879
|
+
const warningWarn = warning.warn;
|
|
880
|
+
if (!isProbability(errorWarn) || !isProbability(errorFail) || !isProbability(warningWarn)) {
|
|
881
|
+
return invalid();
|
|
882
|
+
}
|
|
883
|
+
if (errorWarn >= errorFail) {
|
|
884
|
+
return invalid();
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
status: "VALID",
|
|
888
|
+
config: {
|
|
889
|
+
version: 1,
|
|
890
|
+
thresholds: {
|
|
891
|
+
error: { warn: errorWarn, fail: errorFail },
|
|
892
|
+
warning: { warn: warningWarn }
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function isRecord(value) {
|
|
898
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
899
|
+
}
|
|
900
|
+
function invalid() {
|
|
901
|
+
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
902
|
+
}
|
|
903
|
+
// packages/opencode-adapter/src/attribution/locate-turn.ts
|
|
904
|
+
function findCompletedAssistant(records) {
|
|
905
|
+
for (let index = records.length - 1;index >= 0; index -= 1) {
|
|
906
|
+
const record = records[index];
|
|
907
|
+
if (record !== undefined && isCompletedAssistant(record.info)) {
|
|
908
|
+
return record.info;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
function findDirectParentRecord(records, assistant) {
|
|
914
|
+
const parentID = assistant.parentID;
|
|
915
|
+
if (typeof parentID !== "string" || parentID === "") {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
const record = records.find((item) => item.info.id === parentID);
|
|
919
|
+
if (record === undefined || record.info.role !== "user") {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
return record;
|
|
923
|
+
}
|
|
924
|
+
function isCompletedAssistant(info) {
|
|
925
|
+
if (info.role !== "assistant") {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
if (typeof info.parentID !== "string" || info.parentID === "") {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
if (info.error !== undefined && info.error !== null) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
if (info.summary === true) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
return Number.isFinite(info.time?.completed);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// packages/opencode-adapter/src/attribution/normalize-patch.ts
|
|
941
|
+
function normalizeFileDiff(diff) {
|
|
942
|
+
const path = typeof diff.file === "string" ? diff.file.trim() : "";
|
|
943
|
+
if (path === "") {
|
|
944
|
+
return { status: "INCOMPLETE" };
|
|
945
|
+
}
|
|
946
|
+
if (typeof diff.patch === "string" && diff.patch.trim() !== "") {
|
|
947
|
+
return { status: "PATCH", file: { path, patch: diff.patch } };
|
|
948
|
+
}
|
|
949
|
+
const before = diff.before;
|
|
950
|
+
const after = diff.after;
|
|
951
|
+
if (typeof before !== "string" || typeof after !== "string") {
|
|
952
|
+
return { status: "INCOMPLETE" };
|
|
953
|
+
}
|
|
954
|
+
if (before === after) {
|
|
955
|
+
return { status: "EMPTY" };
|
|
956
|
+
}
|
|
957
|
+
return { status: "PATCH", file: { path, patch: buildReplacementPatch(path, before, after) } };
|
|
958
|
+
}
|
|
959
|
+
function buildReplacementPatch(path, before, after) {
|
|
960
|
+
const beforeLines = splitLines2(before);
|
|
961
|
+
const afterLines = splitLines2(after);
|
|
962
|
+
const fromPath = beforeLines.length === 0 ? "/dev/null" : `a/${path}`;
|
|
963
|
+
const toPath = afterLines.length === 0 ? "/dev/null" : `b/${path}`;
|
|
964
|
+
const header = `--- ${fromPath}
|
|
965
|
+
+++ ${toPath}`;
|
|
966
|
+
const range = `@@ -${hunkStart(beforeLines.length)},${beforeLines.length} +${hunkStart(afterLines.length)},${afterLines.length} @@`;
|
|
967
|
+
const removed = beforeLines.map((line) => `-${line}`);
|
|
968
|
+
const added = afterLines.map((line) => `+${line}`);
|
|
969
|
+
return [header, range, ...removed, ...added, ""].join(`
|
|
970
|
+
`);
|
|
971
|
+
}
|
|
972
|
+
function splitLines2(text) {
|
|
973
|
+
if (text === "") {
|
|
974
|
+
return [];
|
|
975
|
+
}
|
|
976
|
+
const lines = text.split(`
|
|
977
|
+
`);
|
|
978
|
+
if (lines[lines.length - 1] === "") {
|
|
979
|
+
lines.pop();
|
|
980
|
+
}
|
|
981
|
+
return lines;
|
|
982
|
+
}
|
|
983
|
+
function hunkStart(lineCount) {
|
|
984
|
+
return lineCount === 0 ? 0 : 1;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// packages/opencode-adapter/src/attribution/task.ts
|
|
988
|
+
function extractTask(parts) {
|
|
989
|
+
const task = parts.filter(isTextPart).map((part) => part.text).join(`
|
|
990
|
+
`);
|
|
991
|
+
return task.trim() === "" ? null : task;
|
|
992
|
+
}
|
|
993
|
+
function isTextPart(part) {
|
|
994
|
+
return part.type === "text" && typeof part.text === "string";
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// packages/opencode-adapter/src/attribution/attribute-turn.ts
|
|
998
|
+
async function attributeTurn(sessionID, dependencies) {
|
|
999
|
+
const facade = dependencies.facade;
|
|
1000
|
+
const deduplicator = dependencies.deduplicator;
|
|
1001
|
+
const records = await readMessages(facade, sessionID);
|
|
1002
|
+
if (records === null) {
|
|
1003
|
+
return unavailable("MESSAGE_READ_FAILURE");
|
|
1004
|
+
}
|
|
1005
|
+
const assistant = findCompletedAssistant(records);
|
|
1006
|
+
if (assistant === null) {
|
|
1007
|
+
return { status: "NONE" };
|
|
1008
|
+
}
|
|
1009
|
+
if (deduplicator.hasProcessed(assistant.id)) {
|
|
1010
|
+
return { status: "ALREADY_PROCESSED" };
|
|
1011
|
+
}
|
|
1012
|
+
const parent = findDirectParentRecord(records, assistant);
|
|
1013
|
+
if (parent === null) {
|
|
1014
|
+
return unavailable("MISSING_PARENT_MESSAGE");
|
|
1015
|
+
}
|
|
1016
|
+
const task = extractTask(parent.parts);
|
|
1017
|
+
if (task === null) {
|
|
1018
|
+
return unavailable("EMPTY_TASK");
|
|
1019
|
+
}
|
|
1020
|
+
const diffs = await readDiff(facade, sessionID, parent.info.id);
|
|
1021
|
+
if (diffs === null) {
|
|
1022
|
+
return unavailable("DIFF_READ_FAILURE");
|
|
1023
|
+
}
|
|
1024
|
+
const files = normalizeDiffs(diffs);
|
|
1025
|
+
if (files === null) {
|
|
1026
|
+
return unavailable("INCOMPLETE_FILE_DIFF");
|
|
1027
|
+
}
|
|
1028
|
+
deduplicator.markProcessed(assistant.id);
|
|
1029
|
+
const turn = { id: assistant.id, task, files };
|
|
1030
|
+
return { status: "ATTRIBUTED", turn };
|
|
1031
|
+
}
|
|
1032
|
+
async function readMessages(facade, sessionID) {
|
|
1033
|
+
try {
|
|
1034
|
+
return await facade.listMessages({ sessionID });
|
|
1035
|
+
} catch {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async function readDiff(facade, sessionID, messageID) {
|
|
1040
|
+
try {
|
|
1041
|
+
return await facade.fetchDiff({ sessionID, messageID });
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function normalizeDiffs(diffs) {
|
|
1047
|
+
const files = [];
|
|
1048
|
+
for (const diff of diffs) {
|
|
1049
|
+
const result = normalizeFileDiff(diff);
|
|
1050
|
+
if (result.status === "INCOMPLETE") {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
if (result.status === "PATCH") {
|
|
1054
|
+
files.push(result.file);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return files;
|
|
1058
|
+
}
|
|
1059
|
+
function unavailable(reason) {
|
|
1060
|
+
return { status: "UNAVAILABLE", reason };
|
|
1061
|
+
}
|
|
1062
|
+
// packages/opencode-adapter/src/attribution/deduplicator.ts
|
|
1063
|
+
class InMemoryTurnDeduplicator {
|
|
1064
|
+
processed = new Set;
|
|
1065
|
+
hasProcessed(assistantMessageID) {
|
|
1066
|
+
return this.processed.has(assistantMessageID);
|
|
1067
|
+
}
|
|
1068
|
+
markProcessed(assistantMessageID) {
|
|
1069
|
+
this.processed.add(assistantMessageID);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// packages/opencode-adapter/src/attribution/opencode.ts
|
|
1073
|
+
function createOpenCodeSessionFacade(client) {
|
|
1074
|
+
return {
|
|
1075
|
+
async listMessages(input) {
|
|
1076
|
+
const result = await client.session.messages({ path: { id: input.sessionID } });
|
|
1077
|
+
if (result.data === undefined || result.error !== undefined) {
|
|
1078
|
+
throw new Error("OpenCode session messages unavailable");
|
|
1079
|
+
}
|
|
1080
|
+
return result.data;
|
|
1081
|
+
},
|
|
1082
|
+
async fetchDiff(input) {
|
|
1083
|
+
const result = await client.session.diff({
|
|
1084
|
+
path: { id: input.sessionID },
|
|
1085
|
+
query: { messageID: input.messageID }
|
|
1086
|
+
});
|
|
1087
|
+
if (result.data === undefined || result.error !== undefined) {
|
|
1088
|
+
throw new Error("OpenCode session diff unavailable");
|
|
1089
|
+
}
|
|
1090
|
+
return result.data;
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
// packages/opencode-adapter/src/credentials/constants.ts
|
|
1095
|
+
var CREDENTIAL_SERVICE = "jevguard";
|
|
1096
|
+
var CREDENTIAL_ACCOUNT = "typesafe-api-key";
|
|
1097
|
+
var API_KEY_ENVIRONMENT_VARIABLE = "TYPESAFE_API_KEY";
|
|
1098
|
+
|
|
1099
|
+
// packages/opencode-adapter/src/credentials/secret.ts
|
|
1100
|
+
function normalizeSecret(value) {
|
|
1101
|
+
if (value === undefined) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
const trimmed = value.trim();
|
|
1105
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// packages/opencode-adapter/src/credentials/provider.ts
|
|
1109
|
+
function createCredentialProvider(dependencies) {
|
|
1110
|
+
return {
|
|
1111
|
+
async resolve() {
|
|
1112
|
+
const override = normalizeSecret(dependencies.environment.get(API_KEY_ENVIRONMENT_VARIABLE));
|
|
1113
|
+
if (override !== null) {
|
|
1114
|
+
return { status: "AVAILABLE", source: "ENVIRONMENT", apiKey: override };
|
|
1115
|
+
}
|
|
1116
|
+
const stored = await readStored(dependencies.store);
|
|
1117
|
+
switch (stored.status) {
|
|
1118
|
+
case "FOUND": {
|
|
1119
|
+
const secret = normalizeSecret(stored.apiKey);
|
|
1120
|
+
if (secret === null) {
|
|
1121
|
+
return { status: "UNAVAILABLE", reason: "MISSING_CREDENTIAL" };
|
|
1122
|
+
}
|
|
1123
|
+
return { status: "AVAILABLE", source: "STORE", apiKey: secret };
|
|
1124
|
+
}
|
|
1125
|
+
case "NOT_FOUND":
|
|
1126
|
+
return { status: "UNAVAILABLE", reason: "MISSING_CREDENTIAL" };
|
|
1127
|
+
case "FAILED":
|
|
1128
|
+
return { status: "UNAVAILABLE", reason: stored.reason };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
async function readStored(store) {
|
|
1134
|
+
try {
|
|
1135
|
+
return await store.read();
|
|
1136
|
+
} catch {
|
|
1137
|
+
return { status: "FAILED", reason: "STORE_READ_FAILURE" };
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// packages/opencode-adapter/src/credentials/environment.ts
|
|
1141
|
+
function createProcessEnvironment() {
|
|
1142
|
+
return {
|
|
1143
|
+
get(name) {
|
|
1144
|
+
return process.env[name];
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
// packages/opencode-adapter/src/credentials/keyring-store.ts
|
|
1149
|
+
var KEYRING_ENTRY_OPTIONS = { linux: { store: "secret-service" } };
|
|
1150
|
+
function createKeyringEntry(EntryConstructor) {
|
|
1151
|
+
return new EntryConstructor(CREDENTIAL_SERVICE, CREDENTIAL_ACCOUNT, KEYRING_ENTRY_OPTIONS);
|
|
1152
|
+
}
|
|
1153
|
+
function createKeyringCredentialStore() {
|
|
1154
|
+
return {
|
|
1155
|
+
read: readCredential,
|
|
1156
|
+
write: writeCredential,
|
|
1157
|
+
delete: deleteCredential
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async function readCredential() {
|
|
1161
|
+
try {
|
|
1162
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
1163
|
+
const value = await createKeyringEntry(AsyncEntry).getPassword();
|
|
1164
|
+
if (value === undefined || value.length === 0) {
|
|
1165
|
+
return { status: "NOT_FOUND" };
|
|
1166
|
+
}
|
|
1167
|
+
return { status: "FOUND", apiKey: value };
|
|
1168
|
+
} catch {
|
|
1169
|
+
return { status: "FAILED", reason: "STORE_READ_FAILURE" };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function writeCredential(apiKey) {
|
|
1173
|
+
try {
|
|
1174
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
1175
|
+
await createKeyringEntry(AsyncEntry).setPassword(apiKey);
|
|
1176
|
+
return { status: "SAVED" };
|
|
1177
|
+
} catch {
|
|
1178
|
+
return { status: "FAILED", reason: "STORE_WRITE_FAILURE" };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function deleteCredential() {
|
|
1182
|
+
try {
|
|
1183
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
1184
|
+
const deleted = await createKeyringEntry(AsyncEntry).deletePassword();
|
|
1185
|
+
return deleted ? { status: "DELETED" } : { status: "NOT_FOUND" };
|
|
1186
|
+
} catch {
|
|
1187
|
+
return { status: "FAILED", reason: "STORE_DELETE_FAILURE" };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// packages/opencode-adapter/src/credentials/login.ts
|
|
1191
|
+
async function login(input) {
|
|
1192
|
+
const prompted = await readPrompt(input.prompt);
|
|
1193
|
+
const collected = collectSecret(prompted);
|
|
1194
|
+
if (collected.status !== "READ") {
|
|
1195
|
+
return collected;
|
|
1196
|
+
}
|
|
1197
|
+
const written = await writeSecret(input.store, collected.value);
|
|
1198
|
+
if (written.status === "FAILED") {
|
|
1199
|
+
return { status: "FAILED", reason: "STORE_WRITE_FAILURE" };
|
|
1200
|
+
}
|
|
1201
|
+
return { status: "SAVED" };
|
|
1202
|
+
}
|
|
1203
|
+
async function readPrompt(prompt) {
|
|
1204
|
+
try {
|
|
1205
|
+
return await prompt.read("TypeSafe API key: ");
|
|
1206
|
+
} catch {
|
|
1207
|
+
return { status: "FAILED", reason: "PROMPT_FAILURE" };
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function writeSecret(store, value) {
|
|
1211
|
+
try {
|
|
1212
|
+
return await store.write(value);
|
|
1213
|
+
} catch {
|
|
1214
|
+
return { status: "FAILED", reason: "STORE_WRITE_FAILURE" };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function collectSecret(prompted) {
|
|
1218
|
+
if (prompted.status === "ABORTED") {
|
|
1219
|
+
return { status: "ABORTED" };
|
|
1220
|
+
}
|
|
1221
|
+
if (prompted.status === "FAILED") {
|
|
1222
|
+
return { status: "FAILED", reason: prompted.reason };
|
|
1223
|
+
}
|
|
1224
|
+
const secret = normalizeSecret(prompted.value);
|
|
1225
|
+
if (secret === null) {
|
|
1226
|
+
return { status: "FAILED", reason: "EMPTY_SECRET" };
|
|
1227
|
+
}
|
|
1228
|
+
return { status: "READ", value: secret };
|
|
1229
|
+
}
|
|
1230
|
+
// packages/opencode-adapter/src/credentials/prompt.ts
|
|
1231
|
+
function createSecretPrompt() {
|
|
1232
|
+
return {
|
|
1233
|
+
read: readSecret
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async function readSecret(message) {
|
|
1237
|
+
if (process.stdin.isTTY !== true) {
|
|
1238
|
+
return { status: "FAILED", reason: "NOT_A_TTY" };
|
|
1239
|
+
}
|
|
1240
|
+
try {
|
|
1241
|
+
const { default: password } = await import("@inquirer/password");
|
|
1242
|
+
const value = await password({ message, mask: true });
|
|
1243
|
+
return { status: "READ", value };
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
1246
|
+
return { status: "ABORTED" };
|
|
1247
|
+
}
|
|
1248
|
+
return { status: "FAILED", reason: "PROMPT_FAILURE" };
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// packages/opencode-adapter/src/jev/transport.ts
|
|
1252
|
+
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
1253
|
+
|
|
1254
|
+
// packages/opencode-adapter/src/jev/types.ts
|
|
1255
|
+
var JEV_MODEL = "jev-latest";
|
|
1256
|
+
|
|
1257
|
+
// packages/opencode-adapter/src/jev/transport.ts
|
|
1258
|
+
var DEFAULT_ALLOWED_CRITERION = "The change does not violate the rule's Violation.";
|
|
1259
|
+
function buildTypeSafeClientConfig(apiKey) {
|
|
1260
|
+
return { apiKey, defaultModel: JEV_MODEL, logLevel: "off" };
|
|
1261
|
+
}
|
|
1262
|
+
function createJevTransport(dependencies) {
|
|
1263
|
+
return {
|
|
1264
|
+
async evaluate(request) {
|
|
1265
|
+
const credential = await resolveCredential(dependencies.credentials);
|
|
1266
|
+
if (credential.status === "UNAVAILABLE") {
|
|
1267
|
+
return failedFor(request, toFailureReason(credential.reason));
|
|
1268
|
+
}
|
|
1269
|
+
let client;
|
|
1270
|
+
try {
|
|
1271
|
+
client = dependencies.createClient(credential.apiKey);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return failedFor(request, "API_ERROR");
|
|
1274
|
+
}
|
|
1275
|
+
return evaluateWithClient(client, request);
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function createTypeSafeJevTransport(credentials) {
|
|
1280
|
+
return createJevTransport({ credentials, createClient: createTypeSafeSystemOneClient });
|
|
1281
|
+
}
|
|
1282
|
+
function createTypeSafeSystemOneClient(apiKey) {
|
|
1283
|
+
const client = new TypeSafeClient(buildTypeSafeClientConfig(apiKey));
|
|
1284
|
+
return {
|
|
1285
|
+
systemOne: (request) => client.systemOne(request)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function resolveCredential(provider) {
|
|
1289
|
+
try {
|
|
1290
|
+
return await provider.resolve();
|
|
1291
|
+
} catch {
|
|
1292
|
+
return { status: "UNAVAILABLE", reason: "STORE_READ_FAILURE" };
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function toFailureReason(reason) {
|
|
1296
|
+
return reason === "MISSING_CREDENTIAL" ? "MISSING_CREDENTIAL" : "API_ERROR";
|
|
1297
|
+
}
|
|
1298
|
+
async function evaluateWithClient(client, request) {
|
|
1299
|
+
switch (request.kind) {
|
|
1300
|
+
case "RULE":
|
|
1301
|
+
return evaluateRuleWithClient(client, request);
|
|
1302
|
+
case "BUILT_IN_BATCH":
|
|
1303
|
+
return evaluateBatchWithClient(client, request);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async function evaluateRuleWithClient(client, request) {
|
|
1307
|
+
let response;
|
|
1308
|
+
try {
|
|
1309
|
+
response = await client.systemOne(toRuleSystemOneRequest(request));
|
|
1310
|
+
} catch {
|
|
1311
|
+
return ruleFailed("API_ERROR");
|
|
1312
|
+
}
|
|
1313
|
+
const violationProbability = readViolationProbability(response);
|
|
1314
|
+
if (violationProbability === null) {
|
|
1315
|
+
return ruleFailed("INVALID_RESPONSE");
|
|
1316
|
+
}
|
|
1317
|
+
return { kind: "RULE", status: "EVALUATED", noul: { violationProbability } };
|
|
1318
|
+
}
|
|
1319
|
+
async function evaluateBatchWithClient(client, request) {
|
|
1320
|
+
let response;
|
|
1321
|
+
try {
|
|
1322
|
+
response = await client.systemOne(toBatchSystemOneRequest(request));
|
|
1323
|
+
} catch {
|
|
1324
|
+
return batchFailed("API_ERROR");
|
|
1325
|
+
}
|
|
1326
|
+
return readBatchAnswers(response);
|
|
1327
|
+
}
|
|
1328
|
+
function toRuleSystemOneRequest(request) {
|
|
1329
|
+
return {
|
|
1330
|
+
state: {
|
|
1331
|
+
task: request.task,
|
|
1332
|
+
rule: toCriterion(request.question.criteria),
|
|
1333
|
+
change: toChange(request.change)
|
|
1334
|
+
},
|
|
1335
|
+
questions: {
|
|
1336
|
+
violation: toTypeSafeQuestion(request.question.instructions, request.question.criteria)
|
|
1337
|
+
},
|
|
1338
|
+
model: JEV_MODEL
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function toBatchSystemOneRequest(request) {
|
|
1342
|
+
const scopeCreep = request.questions.scopeCreep;
|
|
1343
|
+
const complexity = request.questions.complexity;
|
|
1344
|
+
return {
|
|
1345
|
+
state: {
|
|
1346
|
+
task: request.task,
|
|
1347
|
+
checks: toChecks(scopeCreep.criteria, complexity.criteria),
|
|
1348
|
+
change: toChange(request.change)
|
|
1349
|
+
},
|
|
1350
|
+
questions: {
|
|
1351
|
+
[SCOPE_CREEP_ANSWER]: toTypeSafeQuestion(scopeCreep.instructions, scopeCreep.criteria),
|
|
1352
|
+
[COMPLEXITY_ANSWER]: toTypeSafeQuestion(complexity.instructions, complexity.criteria)
|
|
1353
|
+
},
|
|
1354
|
+
model: JEV_MODEL
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function toChecks(scopeCreep, complexity) {
|
|
1358
|
+
return {
|
|
1359
|
+
scopeCreep: toCriterion(scopeCreep),
|
|
1360
|
+
complexity: toCriterion(complexity)
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function toChange(change) {
|
|
1364
|
+
return { files: [...change.files], diff: change.diff };
|
|
1365
|
+
}
|
|
1366
|
+
function toTypeSafeQuestion(instructions, criteria) {
|
|
1367
|
+
return {
|
|
1368
|
+
type: "noul",
|
|
1369
|
+
instructions,
|
|
1370
|
+
criteria: {
|
|
1371
|
+
true: criteria.violation,
|
|
1372
|
+
false: criteria.allowed ?? DEFAULT_ALLOWED_CRITERION
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function toCriterion(criteria) {
|
|
1377
|
+
return {
|
|
1378
|
+
id: criteria.id,
|
|
1379
|
+
description: criteria.description,
|
|
1380
|
+
violation: criteria.violation,
|
|
1381
|
+
...criteria.allowed === undefined ? {} : { allowed: criteria.allowed }
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function readViolationProbability(response) {
|
|
1385
|
+
if (!isRecord2(response)) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
const answers = response.answers;
|
|
1389
|
+
if (!isRecord2(answers)) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
const violation = answers.violation;
|
|
1393
|
+
if (!isRecord2(violation) || violation.type !== "noul") {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
const noul = violation.noul;
|
|
1397
|
+
return isProbability2(noul) ? noul : null;
|
|
1398
|
+
}
|
|
1399
|
+
function readBatchAnswers(response) {
|
|
1400
|
+
if (!isRecord2(response)) {
|
|
1401
|
+
return batchFailed("INVALID_RESPONSE");
|
|
1402
|
+
}
|
|
1403
|
+
const answers = response.answers;
|
|
1404
|
+
if (!isRecord2(answers)) {
|
|
1405
|
+
return batchFailed("INVALID_RESPONSE");
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
kind: "BUILT_IN_BATCH",
|
|
1409
|
+
status: "EVALUATED",
|
|
1410
|
+
answers: {
|
|
1411
|
+
[SCOPE_CREEP_ANSWER]: readAnswer(answers, SCOPE_CREEP_ANSWER),
|
|
1412
|
+
[COMPLEXITY_ANSWER]: readAnswer(answers, COMPLEXITY_ANSWER)
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function readAnswer(answers, key) {
|
|
1417
|
+
const answer = answers[key];
|
|
1418
|
+
if (!isRecord2(answer) || answer.type !== "noul") {
|
|
1419
|
+
return { status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
1420
|
+
}
|
|
1421
|
+
const noul = answer.noul;
|
|
1422
|
+
if (!isProbability2(noul)) {
|
|
1423
|
+
return { status: "FAILED", reason: "INVALID_RESPONSE" };
|
|
1424
|
+
}
|
|
1425
|
+
return { status: "EVALUATED", noul: { violationProbability: noul } };
|
|
1426
|
+
}
|
|
1427
|
+
function isProbability2(value) {
|
|
1428
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
1429
|
+
}
|
|
1430
|
+
function isRecord2(value) {
|
|
1431
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1432
|
+
}
|
|
1433
|
+
function ruleFailed(reason) {
|
|
1434
|
+
return { kind: "RULE", status: "FAILED", reason };
|
|
1435
|
+
}
|
|
1436
|
+
function batchFailed(reason) {
|
|
1437
|
+
return { kind: "BUILT_IN_BATCH", status: "FAILED", reason };
|
|
1438
|
+
}
|
|
1439
|
+
function failedFor(request, reason) {
|
|
1440
|
+
return request.kind === "RULE" ? ruleFailed(reason) : batchFailed(reason);
|
|
1441
|
+
}
|
|
1442
|
+
// packages/opencode-adapter/src/policy/config-yaml.ts
|
|
1443
|
+
import { parseDocument, visit } from "yaml";
|
|
1444
|
+
function parsePolicyConfig(text) {
|
|
1445
|
+
const document = parseDocument(text, { strict: true, uniqueKeys: true });
|
|
1446
|
+
if (document.errors.length > 0 || document.warnings.length > 0) {
|
|
1447
|
+
return invalid2();
|
|
1448
|
+
}
|
|
1449
|
+
if (document.contents === null || containsAliasAnchorOrTag(document)) {
|
|
1450
|
+
return invalid2();
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
return { status: "PARSED", value: document.toJS({ maxAliasCount: 0 }) };
|
|
1454
|
+
} catch {
|
|
1455
|
+
return invalid2();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function containsAliasAnchorOrTag(document) {
|
|
1459
|
+
let unsafe = false;
|
|
1460
|
+
visit(document, {
|
|
1461
|
+
Alias() {
|
|
1462
|
+
unsafe = true;
|
|
1463
|
+
return visit.BREAK;
|
|
1464
|
+
},
|
|
1465
|
+
Node(_key, node) {
|
|
1466
|
+
if (node.anchor !== undefined || node.tag !== undefined) {
|
|
1467
|
+
unsafe = true;
|
|
1468
|
+
return visit.BREAK;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
return unsafe;
|
|
1473
|
+
}
|
|
1474
|
+
function invalid2() {
|
|
1475
|
+
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1476
|
+
}
|
|
1477
|
+
// packages/opencode-adapter/src/policy/file-source.ts
|
|
1478
|
+
import { readFile } from "fs/promises";
|
|
1479
|
+
import { join } from "path";
|
|
1480
|
+
function createPolicyFileLoader(dependencies) {
|
|
1481
|
+
return {
|
|
1482
|
+
async load() {
|
|
1483
|
+
const rules = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "rules.md"));
|
|
1484
|
+
if (rules.status === "FAILED") {
|
|
1485
|
+
return { status: "FAILED", reason: "RULES_READ_FAILURE" };
|
|
1486
|
+
}
|
|
1487
|
+
const config = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "config.yaml"));
|
|
1488
|
+
if (config.status === "FAILED") {
|
|
1489
|
+
return { status: "FAILED", reason: "CONFIG_READ_FAILURE" };
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
status: "LOADED",
|
|
1493
|
+
source: { rules: rules.value, config: config.value }
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function createNodePolicyFileSystem() {
|
|
1499
|
+
return {
|
|
1500
|
+
readFile: (path) => readFile(path, "utf8")
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
async function readOptionalFile(fileSystem, path) {
|
|
1504
|
+
try {
|
|
1505
|
+
return { status: "READ", value: await fileSystem.readFile(path) };
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
if (isMissingFileError(error)) {
|
|
1508
|
+
return { status: "READ", value: null };
|
|
1509
|
+
}
|
|
1510
|
+
return { status: "FAILED" };
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function isMissingFileError(error) {
|
|
1514
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1515
|
+
}
|
|
1516
|
+
// packages/opencode-adapter/src/presentation/display.ts
|
|
1517
|
+
function displayOutcome(counts) {
|
|
1518
|
+
if (counts.fail > 0) {
|
|
1519
|
+
return "FAIL";
|
|
1520
|
+
}
|
|
1521
|
+
if (counts.unavailable > 0) {
|
|
1522
|
+
return "UNAVAILABLE";
|
|
1523
|
+
}
|
|
1524
|
+
if (counts.warn > 0) {
|
|
1525
|
+
return "WARN";
|
|
1526
|
+
}
|
|
1527
|
+
if (counts.pass > 0) {
|
|
1528
|
+
return "PASS";
|
|
1529
|
+
}
|
|
1530
|
+
return "SKIPPED";
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// packages/opencode-adapter/src/presentation/log.ts
|
|
1534
|
+
function toReviewLogEntry(review) {
|
|
1535
|
+
return {
|
|
1536
|
+
turnId: review.turnId,
|
|
1537
|
+
summary: {
|
|
1538
|
+
highestVerdict: review.summary.verdict,
|
|
1539
|
+
hasUnavailable: review.summary.hasUnavailable,
|
|
1540
|
+
counts: { ...review.summary.counts }
|
|
1541
|
+
},
|
|
1542
|
+
results: review.results.map(toReviewLogResult)
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function toReviewLogResult(result) {
|
|
1546
|
+
switch (result.kind) {
|
|
1547
|
+
case "RULE":
|
|
1548
|
+
return ruleLogResult(result);
|
|
1549
|
+
case "BUILT_IN":
|
|
1550
|
+
return builtInLogResult(result);
|
|
1551
|
+
case "REVIEW":
|
|
1552
|
+
return reviewLevelLogResult(result);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function logLevelFor(outcome) {
|
|
1556
|
+
switch (outcome) {
|
|
1557
|
+
case "PASS":
|
|
1558
|
+
case "SKIPPED":
|
|
1559
|
+
return "info";
|
|
1560
|
+
case "WARN":
|
|
1561
|
+
return "warn";
|
|
1562
|
+
case "FAIL":
|
|
1563
|
+
case "UNAVAILABLE":
|
|
1564
|
+
return "error";
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function reviewLogMessage(entry) {
|
|
1568
|
+
return `JevGuard ${displayOutcome(entry.summary.counts)} for turn ${entry.turnId}`;
|
|
1569
|
+
}
|
|
1570
|
+
function ruleLogResult(result) {
|
|
1571
|
+
const identity = {
|
|
1572
|
+
kind: "RULE",
|
|
1573
|
+
ruleId: result.ruleId,
|
|
1574
|
+
severity: result.severity,
|
|
1575
|
+
scopedPaths: [...result.scopedPaths]
|
|
1576
|
+
};
|
|
1577
|
+
switch (result.outcome) {
|
|
1578
|
+
case "PASS":
|
|
1579
|
+
case "WARN":
|
|
1580
|
+
case "FAIL":
|
|
1581
|
+
return {
|
|
1582
|
+
...identity,
|
|
1583
|
+
outcome: result.outcome,
|
|
1584
|
+
violationProbability: result.violationProbability
|
|
1585
|
+
};
|
|
1586
|
+
case "SKIPPED":
|
|
1587
|
+
case "UNAVAILABLE":
|
|
1588
|
+
return { ...identity, outcome: result.outcome, reason: result.reason };
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function builtInLogResult(result) {
|
|
1592
|
+
const identity = {
|
|
1593
|
+
kind: "BUILT_IN",
|
|
1594
|
+
checkId: result.checkId,
|
|
1595
|
+
severity: result.severity,
|
|
1596
|
+
scopedPaths: [...result.scopedPaths]
|
|
1597
|
+
};
|
|
1598
|
+
switch (result.outcome) {
|
|
1599
|
+
case "PASS":
|
|
1600
|
+
case "WARN":
|
|
1601
|
+
case "FAIL":
|
|
1602
|
+
return {
|
|
1603
|
+
...identity,
|
|
1604
|
+
outcome: result.outcome,
|
|
1605
|
+
violationProbability: result.violationProbability
|
|
1606
|
+
};
|
|
1607
|
+
case "SKIPPED":
|
|
1608
|
+
case "UNAVAILABLE":
|
|
1609
|
+
return { ...identity, outcome: result.outcome, reason: result.reason };
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function reviewLevelLogResult(result) {
|
|
1613
|
+
return { kind: "REVIEW", outcome: result.outcome, reason: result.reason };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// packages/opencode-adapter/src/presentation/toast.ts
|
|
1617
|
+
function toReviewToast(review) {
|
|
1618
|
+
const outcome = displayOutcome(review.summary.counts);
|
|
1619
|
+
return {
|
|
1620
|
+
title: `JevGuard ${outcome}`,
|
|
1621
|
+
message: countSummary(review.summary.counts),
|
|
1622
|
+
variant: toastVariantFor(outcome)
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function toastVariantFor(outcome) {
|
|
1626
|
+
switch (outcome) {
|
|
1627
|
+
case "PASS":
|
|
1628
|
+
return "success";
|
|
1629
|
+
case "WARN":
|
|
1630
|
+
return "warning";
|
|
1631
|
+
case "FAIL":
|
|
1632
|
+
case "UNAVAILABLE":
|
|
1633
|
+
return "error";
|
|
1634
|
+
case "SKIPPED":
|
|
1635
|
+
return "info";
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function countSummary(counts) {
|
|
1639
|
+
return `pass ${counts.pass} \xB7 warn ${counts.warn} \xB7 fail ${counts.fail} \xB7 skipped ${counts.skipped} \xB7 unavailable ${counts.unavailable}`;
|
|
1640
|
+
}
|
|
1641
|
+
// packages/opencode-adapter/src/presentation/opencode.ts
|
|
1642
|
+
var SERVICE_NAME = "jevguard";
|
|
1643
|
+
var TOAST_DURATION_MS = 5000;
|
|
1644
|
+
function createOpenCodePresentationClient(client) {
|
|
1645
|
+
return {
|
|
1646
|
+
app: {
|
|
1647
|
+
log: (input) => client.app.log({ body: input.body })
|
|
1648
|
+
},
|
|
1649
|
+
tui: {
|
|
1650
|
+
showToast: (input) => client.tui.showToast({ body: input.body })
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function createOpenCodeLogSink(client) {
|
|
1655
|
+
return {
|
|
1656
|
+
async write(entry) {
|
|
1657
|
+
try {
|
|
1658
|
+
const result = await client.app.log({
|
|
1659
|
+
body: {
|
|
1660
|
+
service: SERVICE_NAME,
|
|
1661
|
+
level: logLevelFor(displayOutcome(entry.summary.counts)),
|
|
1662
|
+
message: reviewLogMessage(entry),
|
|
1663
|
+
extra: toLogExtra(entry)
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
return result.error === undefined ? "DELIVERED" : "FAILED";
|
|
1667
|
+
} catch {
|
|
1668
|
+
return "FAILED";
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function createOpenCodeToastSink(client) {
|
|
1674
|
+
return {
|
|
1675
|
+
async show(toast) {
|
|
1676
|
+
try {
|
|
1677
|
+
const result = await client.tui.showToast({
|
|
1678
|
+
body: {
|
|
1679
|
+
title: toast.title,
|
|
1680
|
+
message: toast.message,
|
|
1681
|
+
variant: toast.variant,
|
|
1682
|
+
duration: TOAST_DURATION_MS
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
return result.error === undefined ? "DELIVERED" : "FAILED";
|
|
1686
|
+
} catch {
|
|
1687
|
+
return "FAILED";
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function toLogExtra(entry) {
|
|
1693
|
+
return {
|
|
1694
|
+
turnId: entry.turnId,
|
|
1695
|
+
summary: {
|
|
1696
|
+
highestVerdict: entry.summary.highestVerdict,
|
|
1697
|
+
hasUnavailable: entry.summary.hasUnavailable,
|
|
1698
|
+
counts: { ...entry.summary.counts }
|
|
1699
|
+
},
|
|
1700
|
+
results: entry.results.map(toResultExtra)
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function toResultExtra(result) {
|
|
1704
|
+
return { ...toIdentityExtra(result), ...toOutcomeExtra(result) };
|
|
1705
|
+
}
|
|
1706
|
+
function toIdentityExtra(result) {
|
|
1707
|
+
switch (result.kind) {
|
|
1708
|
+
case "RULE":
|
|
1709
|
+
return {
|
|
1710
|
+
kind: result.kind,
|
|
1711
|
+
ruleId: result.ruleId,
|
|
1712
|
+
severity: result.severity,
|
|
1713
|
+
scopedPaths: [...result.scopedPaths]
|
|
1714
|
+
};
|
|
1715
|
+
case "BUILT_IN":
|
|
1716
|
+
return {
|
|
1717
|
+
kind: result.kind,
|
|
1718
|
+
checkId: result.checkId,
|
|
1719
|
+
severity: result.severity,
|
|
1720
|
+
scopedPaths: [...result.scopedPaths]
|
|
1721
|
+
};
|
|
1722
|
+
case "REVIEW":
|
|
1723
|
+
return { kind: result.kind };
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function toOutcomeExtra(result) {
|
|
1727
|
+
switch (result.outcome) {
|
|
1728
|
+
case "PASS":
|
|
1729
|
+
case "WARN":
|
|
1730
|
+
case "FAIL":
|
|
1731
|
+
return { outcome: result.outcome, violationProbability: result.violationProbability };
|
|
1732
|
+
case "SKIPPED":
|
|
1733
|
+
case "UNAVAILABLE":
|
|
1734
|
+
return { outcome: result.outcome, reason: result.reason };
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// packages/opencode-adapter/src/presentation/index.ts
|
|
1739
|
+
function createReviewPresenter(ports) {
|
|
1740
|
+
return {
|
|
1741
|
+
async present(review) {
|
|
1742
|
+
const log = await deliverLog(ports.log, toReviewLogEntry(review));
|
|
1743
|
+
const toast = await deliverToast(ports.toast, toReviewToast(review));
|
|
1744
|
+
return { log, toast };
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
async function deliverLog(sink, entry) {
|
|
1749
|
+
try {
|
|
1750
|
+
return await sink.write(entry);
|
|
1751
|
+
} catch {
|
|
1752
|
+
return "FAILED";
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async function deliverToast(sink, toast) {
|
|
1756
|
+
try {
|
|
1757
|
+
return await sink.show(toast);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return "FAILED";
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
// packages/plugin/src/idle-event.ts
|
|
1763
|
+
function idleSessionID(event) {
|
|
1764
|
+
if (!isRecord3(event) || event.type !== "session.status") {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
const properties = event.properties;
|
|
1768
|
+
if (!isRecord3(properties)) {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
const status = properties.status;
|
|
1772
|
+
if (!isRecord3(status) || status.type !== "idle") {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
const sessionID = properties.sessionID;
|
|
1776
|
+
return typeof sessionID === "string" && sessionID !== "" ? sessionID : null;
|
|
1777
|
+
}
|
|
1778
|
+
function isRecord3(value) {
|
|
1779
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// packages/plugin/src/present.ts
|
|
1783
|
+
async function presentReview(presenter, review) {
|
|
1784
|
+
try {
|
|
1785
|
+
await presenter.present(review);
|
|
1786
|
+
} catch {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// packages/plugin/src/review-result.ts
|
|
1792
|
+
function reviewLevelUnavailable(turnId, reason) {
|
|
1793
|
+
return {
|
|
1794
|
+
kind: "REVIEW",
|
|
1795
|
+
turnId,
|
|
1796
|
+
ruleId: null,
|
|
1797
|
+
severity: null,
|
|
1798
|
+
scopedPaths: [],
|
|
1799
|
+
outcome: "UNAVAILABLE",
|
|
1800
|
+
reason
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function unavailableReview(turnId, reason) {
|
|
1804
|
+
return aggregateReview(turnId, [reviewLevelUnavailable(turnId, reason)]);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// packages/plugin/src/review.ts
|
|
1808
|
+
async function reviewAttributedTurn(turn, dependencies) {
|
|
1809
|
+
const loaded = await loadPolicy(dependencies.policy);
|
|
1810
|
+
const builtInInput = { turn, evidencePolicy: DEFAULT_EVIDENCE_POLICY };
|
|
1811
|
+
const [ruleResults, builtIns] = await Promise.all([
|
|
1812
|
+
evaluateRuleLane(turn, loaded, dependencies),
|
|
1813
|
+
evaluateBuiltIns(builtInInput, { jev: dependencies.jev })
|
|
1814
|
+
]);
|
|
1815
|
+
const review = aggregateReview(turn.id, [
|
|
1816
|
+
...ruleResults,
|
|
1817
|
+
builtIns.scopeCreep,
|
|
1818
|
+
builtIns.complexity
|
|
1819
|
+
]);
|
|
1820
|
+
await presentReview(dependencies.presenter, review);
|
|
1821
|
+
}
|
|
1822
|
+
async function evaluateRuleLane(turn, loaded, dependencies) {
|
|
1823
|
+
if (loaded === null) {
|
|
1824
|
+
return [reviewLevelUnavailable(turn.id, "INVALID_RULE")];
|
|
1825
|
+
}
|
|
1826
|
+
if (loaded.status === "FAILED") {
|
|
1827
|
+
const reason = loaded.reason === "CONFIG_READ_FAILURE" ? "INVALID_CONFIG" : "INVALID_RULE";
|
|
1828
|
+
return [reviewLevelUnavailable(turn.id, reason)];
|
|
1829
|
+
}
|
|
1830
|
+
const rules = loaded.source.rules;
|
|
1831
|
+
if (rules === null) {
|
|
1832
|
+
return [reviewLevelUnavailable(turn.id, "INVALID_RULE")];
|
|
1833
|
+
}
|
|
1834
|
+
const parsedRules = parseRules(rules);
|
|
1835
|
+
const gateConfig = readGateConfig(loaded.source.config);
|
|
1836
|
+
const review = await evaluateRules({
|
|
1837
|
+
turn,
|
|
1838
|
+
rules: parsedRules,
|
|
1839
|
+
gateConfig,
|
|
1840
|
+
evidencePolicy: DEFAULT_EVIDENCE_POLICY
|
|
1841
|
+
}, { jev: dependencies.jev });
|
|
1842
|
+
return review.results;
|
|
1843
|
+
}
|
|
1844
|
+
async function loadPolicy(policy) {
|
|
1845
|
+
try {
|
|
1846
|
+
return await policy.load();
|
|
1847
|
+
} catch {
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function readGateConfig(config) {
|
|
1852
|
+
const value = readConfigValue(config);
|
|
1853
|
+
if (value.status === "INVALID") {
|
|
1854
|
+
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1855
|
+
}
|
|
1856
|
+
return resolveGateConfig(value.value);
|
|
1857
|
+
}
|
|
1858
|
+
function readConfigValue(config) {
|
|
1859
|
+
if (config === null) {
|
|
1860
|
+
return { status: "VALUE", value: null };
|
|
1861
|
+
}
|
|
1862
|
+
const parsed = parsePolicyConfig(config);
|
|
1863
|
+
return parsed.status === "PARSED" ? { status: "VALUE", value: parsed.value } : { status: "INVALID" };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// packages/plugin/src/runtime.ts
|
|
1867
|
+
function createPluginRuntime(dependencies) {
|
|
1868
|
+
let attributionTail = Promise.resolve();
|
|
1869
|
+
const backgroundJobs = new Set;
|
|
1870
|
+
function track(work) {
|
|
1871
|
+
const contained = work.then(ignore, ignore);
|
|
1872
|
+
backgroundJobs.add(contained);
|
|
1873
|
+
contained.then(() => {
|
|
1874
|
+
backgroundJobs.delete(contained);
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
function enqueueAttribution(sessionID) {
|
|
1878
|
+
const run = attributionTail.then(() => processIdle(sessionID));
|
|
1879
|
+
attributionTail = run.then(ignore, ignore);
|
|
1880
|
+
}
|
|
1881
|
+
async function processIdle(sessionID) {
|
|
1882
|
+
try {
|
|
1883
|
+
const attribution = await attributeTurn(sessionID, {
|
|
1884
|
+
facade: dependencies.facade,
|
|
1885
|
+
deduplicator: dependencies.deduplicator
|
|
1886
|
+
});
|
|
1887
|
+
switch (attribution.status) {
|
|
1888
|
+
case "NONE":
|
|
1889
|
+
case "ALREADY_PROCESSED":
|
|
1890
|
+
return;
|
|
1891
|
+
case "UNAVAILABLE":
|
|
1892
|
+
track(presentReview(dependencies.presenter, unavailableReview(sessionID, "MISSING_ATTRIBUTED_DIFF")));
|
|
1893
|
+
return;
|
|
1894
|
+
case "ATTRIBUTED":
|
|
1895
|
+
track(reviewAttributedTurn(attribution.turn, dependencies));
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
} catch {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
async function drain() {
|
|
1903
|
+
while (true) {
|
|
1904
|
+
await attributionTail;
|
|
1905
|
+
if (backgroundJobs.size === 0) {
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
await Promise.all([...backgroundJobs]);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return {
|
|
1912
|
+
hooks: {
|
|
1913
|
+
event: (input) => {
|
|
1914
|
+
const sessionID = idleSessionID(input.event);
|
|
1915
|
+
if (sessionID === null) {
|
|
1916
|
+
return Promise.resolve();
|
|
1917
|
+
}
|
|
1918
|
+
enqueueAttribution(sessionID);
|
|
1919
|
+
return Promise.resolve();
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
drain
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function ignore() {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// packages/plugin/src/skills/config-hook.ts
|
|
1930
|
+
var SKILLS_KEY = "skills";
|
|
1931
|
+
var PATHS_KEY = "paths";
|
|
1932
|
+
function isRecord4(value) {
|
|
1933
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1934
|
+
}
|
|
1935
|
+
function isMutableArray(value) {
|
|
1936
|
+
return Array.isArray(value);
|
|
1937
|
+
}
|
|
1938
|
+
function appendSkillPath(config, skillDirectory) {
|
|
1939
|
+
if (!isRecord4(config)) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const skills = config[SKILLS_KEY];
|
|
1943
|
+
if (skills === undefined) {
|
|
1944
|
+
config[SKILLS_KEY] = { [PATHS_KEY]: [skillDirectory] };
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (!isRecord4(skills)) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const paths = skills[PATHS_KEY];
|
|
1951
|
+
if (paths === undefined) {
|
|
1952
|
+
skills[PATHS_KEY] = [skillDirectory];
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (!isMutableArray(paths)) {
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (paths.some((entry) => entry === skillDirectory)) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
paths.push(skillDirectory);
|
|
1962
|
+
}
|
|
1963
|
+
function createConfigHook(options) {
|
|
1964
|
+
return async (input) => {
|
|
1965
|
+
if (options.skillDirectory === null) {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
appendSkillPath(input, options.skillDirectory);
|
|
1970
|
+
} catch {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// packages/plugin/src/skills/skill-directory.ts
|
|
1977
|
+
import { existsSync } from "fs";
|
|
1978
|
+
import { join as join2 } from "path";
|
|
1979
|
+
import { fileURLToPath } from "url";
|
|
1980
|
+
var SKILL_ENTRY_FILE = "SKILL.md";
|
|
1981
|
+
var SKILL_DIRECTORY_CANDIDATES = ["./skills/jevguard-rules", "../skills/jevguard-rules"];
|
|
1982
|
+
function resolveSkillDirectory(moduleUrl, exists) {
|
|
1983
|
+
for (const candidate of SKILL_DIRECTORY_CANDIDATES) {
|
|
1984
|
+
const directory = fileURLToPath(new URL(candidate, moduleUrl));
|
|
1985
|
+
if (exists(join2(directory, SKILL_ENTRY_FILE))) {
|
|
1986
|
+
return directory;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
function resolveInstalledSkillDirectory(moduleUrl) {
|
|
1992
|
+
return resolveSkillDirectory(moduleUrl, existsSync);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// packages/plugin/src/plugin.ts
|
|
1996
|
+
var DEFAULT_JEV_MAX_CONCURRENCY = 2;
|
|
1997
|
+
var JevGuardPlugin = async (input) => {
|
|
1998
|
+
const presentationClient = createOpenCodePresentationClient(input.client);
|
|
1999
|
+
const dependencies = {
|
|
2000
|
+
facade: createOpenCodeSessionFacade(input.client),
|
|
2001
|
+
deduplicator: new InMemoryTurnDeduplicator,
|
|
2002
|
+
policy: createPolicyFileLoader({
|
|
2003
|
+
root: input.worktree,
|
|
2004
|
+
fileSystem: createNodePolicyFileSystem()
|
|
2005
|
+
}),
|
|
2006
|
+
presenter: createReviewPresenter({
|
|
2007
|
+
log: createOpenCodeLogSink(presentationClient),
|
|
2008
|
+
toast: createOpenCodeToastSink(presentationClient)
|
|
2009
|
+
}),
|
|
2010
|
+
jev: createFifoJevPort(createTypeSafeJevTransport(createCredentialProvider({
|
|
2011
|
+
environment: createProcessEnvironment(),
|
|
2012
|
+
store: createKeyringCredentialStore()
|
|
2013
|
+
})), { maxConcurrency: DEFAULT_JEV_MAX_CONCURRENCY })
|
|
2014
|
+
};
|
|
2015
|
+
return {
|
|
2016
|
+
...createPluginRuntime(dependencies).hooks,
|
|
2017
|
+
config: createConfigHook({ skillDirectory: resolveInstalledSkillDirectory(import.meta.url) })
|
|
2018
|
+
};
|
|
2019
|
+
};
|
|
2020
|
+
export {
|
|
2021
|
+
JevGuardPlugin
|
|
2022
|
+
};
|