@orangepro/orangepro-mcp 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 +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- package/scripts/spikes/failure-summary.mjs +29 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { behaviorNodes, nodesByKind } from "../graph/factories.js";
|
|
2
|
+
const AREA_SKIP = new Set(["src", "app", "lib", "packages", "tests", "test", "e2e", "__tests__", "spec"]);
|
|
3
|
+
/** Strength order for diff→behavior provenance: import-graph evidence beats heuristics. */
|
|
4
|
+
const KIND_RANK = { area: 0, stem: 1, import: 2, direct: 3 };
|
|
5
|
+
const stronger = (a, b) => (KIND_RANK[a] >= KIND_RANK[b] ? a : b);
|
|
6
|
+
/**
|
|
7
|
+
* Cap on behaviors surfaced by the coarse directory-area fallback. Precise
|
|
8
|
+
* (test ↔ changed-file) links are never capped; only the area fallback is, so a
|
|
9
|
+
* small PR can't match every behavior in a top-level directory (the old bug: a
|
|
10
|
+
* 5-file PR matched ~794 behaviors).
|
|
11
|
+
*/
|
|
12
|
+
const MAX_AREA_FALLBACK_BEHAVIORS = 50;
|
|
13
|
+
/**
|
|
14
|
+
* HUB damping for the precise import-derived links: a changed file linked to
|
|
15
|
+
* more test files than max(floor, fraction-of-all-tests) is shared plumbing
|
|
16
|
+
* (a constants module, a barrel), not behavior-specific signal — expanding it
|
|
17
|
+
* would flood PR targeting (Mattermost: a one-line constants.tsx edit linked
|
|
18
|
+
* 186 test files -> 393 "affected" behaviors). Hubs are excluded from precise
|
|
19
|
+
* expansion and surfaced as an explanatory action instead.
|
|
20
|
+
*/
|
|
21
|
+
const HUB_FAN_IN_MIN = 20;
|
|
22
|
+
const HUB_FAN_IN_FRACTION = 0.02;
|
|
23
|
+
/** First meaningful path segment, mirroring the analyzer's topArea convention. */
|
|
24
|
+
function topArea(relPath) {
|
|
25
|
+
const parts = relPath.split("/").filter(Boolean);
|
|
26
|
+
for (const part of parts.slice(0, Math.max(0, parts.length - 1))) {
|
|
27
|
+
if (!AREA_SKIP.has(part.toLowerCase()))
|
|
28
|
+
return part;
|
|
29
|
+
}
|
|
30
|
+
return parts.length > 1 ? parts[0] : "core";
|
|
31
|
+
}
|
|
32
|
+
function nodeRole(node) {
|
|
33
|
+
const role = node.properties.role;
|
|
34
|
+
return typeof role === "string" ? role : "";
|
|
35
|
+
}
|
|
36
|
+
function nodeFile(node) {
|
|
37
|
+
const file = node.properties.file;
|
|
38
|
+
return typeof file === "string" ? file : undefined;
|
|
39
|
+
}
|
|
40
|
+
function nodeArea(node) {
|
|
41
|
+
const area = node.properties.area;
|
|
42
|
+
return typeof area === "string" ? area : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Map a set of changed files to the behaviors and tests whose grounding may be
|
|
46
|
+
* invalidated, and produce concrete follow-up actions.
|
|
47
|
+
*
|
|
48
|
+
* Metadata only: emits external ids / titles and human-readable guidance, never
|
|
49
|
+
* source content. The graph is read but never mutated.
|
|
50
|
+
*/
|
|
51
|
+
export function changedImpact(graph, changedFiles, base_ref) {
|
|
52
|
+
const changedSet = new Set(changedFiles);
|
|
53
|
+
// ── affected_tests: TestCase nodes whose properties.file changed,
|
|
54
|
+
// plus File nodes with role 'test' that themselves changed. ──
|
|
55
|
+
const affectedTestSet = new Set();
|
|
56
|
+
const changedTestCaseIds = new Set();
|
|
57
|
+
for (const tc of nodesByKind(graph, "TestCase")) {
|
|
58
|
+
const file = nodeFile(tc);
|
|
59
|
+
if (file && changedSet.has(file)) {
|
|
60
|
+
affectedTestSet.add(tc.title ?? tc.external_id);
|
|
61
|
+
changedTestCaseIds.add(tc.external_id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const fileNode of nodesByKind(graph, "File")) {
|
|
65
|
+
if (nodeRole(fileNode) === "test" && changedSet.has(fileNode.external_id)) {
|
|
66
|
+
affectedTestSet.add(fileNode.title ?? fileNode.external_id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── affected_behaviors: PRECISE (test ↔ changed-file) first; coarse area
|
|
70
|
+
// match only as a capped fallback when nothing precise links. ──
|
|
71
|
+
// A changed CODE file relates to test files via MAY_RELATE_TO (File↔File):
|
|
72
|
+
// primarily RESOLVED test->source imports from the analyzer's import graph,
|
|
73
|
+
// with the name-stem heuristic as a secondary fallback — so PR-scoped impact
|
|
74
|
+
// is import-precise wherever the resolver linked. Links are collected PER
|
|
75
|
+
// changed file (counting only test-file endpoints, so a changed TEST file's
|
|
76
|
+
// own source imports are ignored), then HUB-damped: a changed file whose test
|
|
77
|
+
// fan-in exceeds the threshold is excluded from precise expansion and reported
|
|
78
|
+
// as an action instead.
|
|
79
|
+
const testFilePaths = new Set();
|
|
80
|
+
for (const tc of nodesByKind(graph, "TestCase")) {
|
|
81
|
+
const file = nodeFile(tc);
|
|
82
|
+
if (file)
|
|
83
|
+
testFilePaths.add(file);
|
|
84
|
+
}
|
|
85
|
+
// Each MAY_RELATE_TO carries its provenance in evidence_strength: "candidate"
|
|
86
|
+
// is a RESOLVED test→source import (import-graph evidence); anything else
|
|
87
|
+
// (currently "weak") is the basename-stem name heuristic. Import beats stem for
|
|
88
|
+
// the same pair, so a resolved link is never downgraded to a guess.
|
|
89
|
+
const linksPerChangedFile = new Map();
|
|
90
|
+
const addLink = (changedFile, other, kind) => {
|
|
91
|
+
if (!testFilePaths.has(other))
|
|
92
|
+
return; // only test-file endpoints count
|
|
93
|
+
let m = linksPerChangedFile.get(changedFile);
|
|
94
|
+
if (!m)
|
|
95
|
+
linksPerChangedFile.set(changedFile, (m = new Map()));
|
|
96
|
+
m.set(other, stronger(m.get(other) ?? kind, kind));
|
|
97
|
+
};
|
|
98
|
+
for (const e of graph.candidate_edges) {
|
|
99
|
+
if (e.relationship_type !== "MAY_RELATE_TO")
|
|
100
|
+
continue;
|
|
101
|
+
const kind = e.evidence_strength === "candidate" ? "import" : "stem";
|
|
102
|
+
if (changedSet.has(e.to_external_id))
|
|
103
|
+
addLink(e.to_external_id, e.from_external_id, kind);
|
|
104
|
+
if (changedSet.has(e.from_external_id))
|
|
105
|
+
addLink(e.from_external_id, e.to_external_id, kind);
|
|
106
|
+
}
|
|
107
|
+
const hubThreshold = Math.max(HUB_FAN_IN_MIN, Math.ceil(testFilePaths.size * HUB_FAN_IN_FRACTION));
|
|
108
|
+
// test file -> strongest provenance among the non-hub changed files reaching it.
|
|
109
|
+
const relatedTestFileKind = new Map();
|
|
110
|
+
const hubFiles = [];
|
|
111
|
+
for (const [file, linkedTests] of linksPerChangedFile) {
|
|
112
|
+
if (linkedTests.size > hubThreshold) {
|
|
113
|
+
hubFiles.push({ file, fan_in: linkedTests.size });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const [t, kind] of linkedTests) {
|
|
117
|
+
relatedTestFileKind.set(t, stronger(relatedTestFileKind.get(t) ?? kind, kind));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
hubFiles.sort((a, b) => b.fan_in - a.fan_in);
|
|
121
|
+
// TestCase external_id -> how the diff reached it. A test whose own file is in
|
|
122
|
+
// the diff is "direct" (strongest); otherwise it inherits the import/stem kind
|
|
123
|
+
// of the changed file that links it.
|
|
124
|
+
const testCaseKind = new Map();
|
|
125
|
+
for (const id of changedTestCaseIds)
|
|
126
|
+
testCaseKind.set(id, "direct");
|
|
127
|
+
if (relatedTestFileKind.size) {
|
|
128
|
+
for (const tc of nodesByKind(graph, "TestCase")) {
|
|
129
|
+
const file = nodeFile(tc);
|
|
130
|
+
if (!file)
|
|
131
|
+
continue;
|
|
132
|
+
const kind = relatedTestFileKind.get(file);
|
|
133
|
+
if (!kind)
|
|
134
|
+
continue;
|
|
135
|
+
const prev = testCaseKind.get(tc.external_id);
|
|
136
|
+
testCaseKind.set(tc.external_id, prev ? stronger(prev, kind) : kind);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const linkTestCaseIds = new Set(testCaseKind.keys());
|
|
140
|
+
// Behaviors linked to a changed (or import/stem-related) TestCase via coverage
|
|
141
|
+
// edges, tagged with the strongest provenance among the test cases reaching them.
|
|
142
|
+
const behaviorIds = new Set(behaviorNodes(graph).map((b) => b.external_id));
|
|
143
|
+
const behaviorKind = new Map();
|
|
144
|
+
const link = (a, b) => {
|
|
145
|
+
let tc;
|
|
146
|
+
let beh;
|
|
147
|
+
if (linkTestCaseIds.has(a) && behaviorIds.has(b))
|
|
148
|
+
(tc = a), (beh = b);
|
|
149
|
+
else if (linkTestCaseIds.has(b) && behaviorIds.has(a))
|
|
150
|
+
(tc = b), (beh = a);
|
|
151
|
+
if (!tc || !beh)
|
|
152
|
+
return;
|
|
153
|
+
const kind = testCaseKind.get(tc) ?? "stem";
|
|
154
|
+
const prev = behaviorKind.get(beh);
|
|
155
|
+
behaviorKind.set(beh, prev ? stronger(prev, kind) : kind);
|
|
156
|
+
};
|
|
157
|
+
for (const e of graph.edges) {
|
|
158
|
+
if (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS")
|
|
159
|
+
link(e.from_external_id, e.to_external_id);
|
|
160
|
+
}
|
|
161
|
+
for (const e of graph.candidate_edges) {
|
|
162
|
+
if (e.relationship_type === "MAY_BE_TESTED_BY" || e.relationship_type === "MAY_COVER")
|
|
163
|
+
link(e.from_external_id, e.to_external_id);
|
|
164
|
+
}
|
|
165
|
+
const linkedBehaviors = new Set(behaviorKind.keys());
|
|
166
|
+
// Always include the precise set (behaviors whose tests cover the changed files).
|
|
167
|
+
// Then fall back to the coarse directory area ONLY for changed-file areas that no
|
|
168
|
+
// precise behavior already covers — so a mixed PR doesn't lose its un-test-linked
|
|
169
|
+
// files (per-area, not all-or-nothing), while a precisely-linked area is never
|
|
170
|
+
// re-expanded to every behavior in its directory (the #7 over-match).
|
|
171
|
+
const affectedBehaviorSet = new Set(linkedBehaviors);
|
|
172
|
+
const preciseAreas = new Set();
|
|
173
|
+
for (const b of behaviorNodes(graph)) {
|
|
174
|
+
if (!linkedBehaviors.has(b.external_id))
|
|
175
|
+
continue;
|
|
176
|
+
const a = nodeArea(b);
|
|
177
|
+
if (a)
|
|
178
|
+
preciseAreas.add(a);
|
|
179
|
+
}
|
|
180
|
+
// Hub files are excluded from the area fallback too: their impact is
|
|
181
|
+
// repo-wide, so re-expanding the hub's directory area would re-create the
|
|
182
|
+
// very flood the damping removed (a hub-only PR must not area-match).
|
|
183
|
+
const hubFileSet = new Set(hubFiles.map((h) => h.file));
|
|
184
|
+
const fallbackAreas = new Set();
|
|
185
|
+
for (const relPath of changedFiles) {
|
|
186
|
+
if (hubFileSet.has(relPath))
|
|
187
|
+
continue;
|
|
188
|
+
const a = topArea(relPath);
|
|
189
|
+
if (!preciseAreas.has(a))
|
|
190
|
+
fallbackAreas.add(a);
|
|
191
|
+
}
|
|
192
|
+
let area_truncated = 0;
|
|
193
|
+
if (fallbackAreas.size > 0) {
|
|
194
|
+
const areaMatched = behaviorNodes(graph)
|
|
195
|
+
.filter((b) => {
|
|
196
|
+
const area = nodeArea(b);
|
|
197
|
+
return area !== undefined && fallbackAreas.has(area) && !affectedBehaviorSet.has(b.external_id);
|
|
198
|
+
})
|
|
199
|
+
.map((b) => b.external_id)
|
|
200
|
+
.sort();
|
|
201
|
+
area_truncated = Math.max(0, areaMatched.length - MAX_AREA_FALLBACK_BEHAVIORS);
|
|
202
|
+
for (const id of areaMatched.slice(0, MAX_AREA_FALLBACK_BEHAVIORS)) {
|
|
203
|
+
affectedBehaviorSet.add(id);
|
|
204
|
+
// areaMatched already excludes anything precisely linked, so these are
|
|
205
|
+
// area-only — never overriding a stronger import/stem/direct tag.
|
|
206
|
+
if (!behaviorKind.has(id))
|
|
207
|
+
behaviorKind.set(id, "area");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const area_fallback = fallbackAreas.size > 0;
|
|
211
|
+
// ── recommended_actions, tailored by the kinds of files that changed ──
|
|
212
|
+
const configIds = new Set();
|
|
213
|
+
for (const cfg of nodesByKind(graph, "ConfigFile"))
|
|
214
|
+
configIds.add(cfg.external_id);
|
|
215
|
+
for (const pkg of nodesByKind(graph, "Package"))
|
|
216
|
+
configIds.add(pkg.external_id);
|
|
217
|
+
for (const fileNode of nodesByKind(graph, "File")) {
|
|
218
|
+
if (nodeRole(fileNode) === "config")
|
|
219
|
+
configIds.add(fileNode.external_id);
|
|
220
|
+
}
|
|
221
|
+
const changedConfig = changedFiles.some((f) => configIds.has(f));
|
|
222
|
+
const changedTest = affectedTestSet.size > 0;
|
|
223
|
+
const changedCode = changedFiles.some((f) => {
|
|
224
|
+
const node = graph.nodes.find((n) => n.external_id === f);
|
|
225
|
+
return node !== undefined && node.kind === "File" && nodeRole(node) === "code";
|
|
226
|
+
});
|
|
227
|
+
const recommended_actions = [];
|
|
228
|
+
const sortedBehaviors = [...affectedBehaviorSet].sort();
|
|
229
|
+
for (const behaviorId of sortedBehaviors.slice(0, 10)) {
|
|
230
|
+
recommended_actions.push(`Regenerate or review tests for ${behaviorId}`);
|
|
231
|
+
}
|
|
232
|
+
if (sortedBehaviors.length > 10) {
|
|
233
|
+
recommended_actions.push(`… and ${sortedBehaviors.length - 10} more affected behavior(s) (see affected_behaviors).`);
|
|
234
|
+
}
|
|
235
|
+
const areaAdded = affectedBehaviorSet.size - linkedBehaviors.size;
|
|
236
|
+
if (area_fallback && areaAdded > 0) {
|
|
237
|
+
recommended_actions.push(`Matched ${areaAdded} behavior(s) by directory area for changed files with no test link` +
|
|
238
|
+
(area_truncated ? ` (+${area_truncated} more capped — narrow the diff or add tests for precise targeting).` : "."));
|
|
239
|
+
}
|
|
240
|
+
for (const hub of hubFiles) {
|
|
241
|
+
recommended_actions.push(`${hub.file} is linked to ${hub.fan_in} test files (hub import; threshold ${hubThreshold}) — ` +
|
|
242
|
+
`excluded from precise targeting because its impact is repo-wide, not behavior-specific. ` +
|
|
243
|
+
`Target specific behaviors directly if this file's change is the point of the PR.`);
|
|
244
|
+
}
|
|
245
|
+
if (changedConfig) {
|
|
246
|
+
recommended_actions.push("Config/package changed: re-check framework assumptions (run update)");
|
|
247
|
+
}
|
|
248
|
+
if (changedTest) {
|
|
249
|
+
recommended_actions.push("Changed tests: re-verify behavior coverage confidence");
|
|
250
|
+
}
|
|
251
|
+
if (changedCode && affectedBehaviorSet.size === 0) {
|
|
252
|
+
recommended_actions.push("Code changed: re-run analyze to refresh symbols and coverage");
|
|
253
|
+
}
|
|
254
|
+
// Per-behavior provenance: import-graph evidence (direct/import) vs heuristic
|
|
255
|
+
// (stem/area). Defaults to "area" only if some path added a behavior without a
|
|
256
|
+
// tag (shouldn't happen — every affected id is tagged at its source).
|
|
257
|
+
const affected = [...affectedBehaviorSet].sort();
|
|
258
|
+
const link_kinds = {};
|
|
259
|
+
const counts = { direct: 0, import: 0, stem: 0, area: 0 };
|
|
260
|
+
for (const id of affected) {
|
|
261
|
+
const k = behaviorKind.get(id) ?? "area";
|
|
262
|
+
link_kinds[id] = k;
|
|
263
|
+
counts[k]++;
|
|
264
|
+
}
|
|
265
|
+
if (affected.length > 0) {
|
|
266
|
+
const parts = ["direct", "import", "stem", "area"]
|
|
267
|
+
.filter((k) => counts[k] > 0)
|
|
268
|
+
.map((k) => `${counts[k]} ${k}`);
|
|
269
|
+
recommended_actions.unshift(`Targeting provenance: ${parts.join(", ")} (import-graph evidence is precise; stem/area are heuristic).`);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
status: "ok",
|
|
273
|
+
base_ref,
|
|
274
|
+
changed_files: [...changedFiles].sort(),
|
|
275
|
+
affected_behaviors: affected,
|
|
276
|
+
link_kinds,
|
|
277
|
+
affected_tests: [...affectedTestSet].sort(),
|
|
278
|
+
recommended_actions
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read coarse git provenance for the freshness manifest.
|
|
3
|
+
*
|
|
4
|
+
* Metadata only — commit/branch/dirty flag. Returns null when the workspace is
|
|
5
|
+
* not a git checkout (HEAD cannot be resolved), so freshness still works on
|
|
6
|
+
* plain directories.
|
|
7
|
+
*/
|
|
8
|
+
export function readGitInfo(git) {
|
|
9
|
+
const head = git(["rev-parse", "HEAD"]);
|
|
10
|
+
if (head === null)
|
|
11
|
+
return null;
|
|
12
|
+
const commit = head.trim().slice(0, 40);
|
|
13
|
+
const branchRaw = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
14
|
+
const branch = branchRaw === null ? undefined : branchRaw.trim();
|
|
15
|
+
const statusRaw = git(["status", "--porcelain"]);
|
|
16
|
+
const dirty = statusRaw === null ? undefined : statusRaw.trim().length > 0;
|
|
17
|
+
return {
|
|
18
|
+
commit,
|
|
19
|
+
...(branch !== undefined ? { branch } : {}),
|
|
20
|
+
...(dirty !== undefined ? { dirty } : {})
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Assemble a freshness manifest from per-file entries and git provenance.
|
|
25
|
+
*
|
|
26
|
+
* Copies the file_entries map so the returned manifest never aliases the
|
|
27
|
+
* caller's input.
|
|
28
|
+
*/
|
|
29
|
+
export function buildManifest(file_entries, git, now) {
|
|
30
|
+
return {
|
|
31
|
+
generated_at: now,
|
|
32
|
+
git,
|
|
33
|
+
files: { ...file_entries }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare the graph's stored manifest (previous snapshot) against the current
|
|
3
|
+
* per-file entries and report what changed.
|
|
4
|
+
*
|
|
5
|
+
* changed_files is the union of added (present now, missing before), removed
|
|
6
|
+
* (present before, missing now), and modified (present in both with a differing
|
|
7
|
+
* hash), sorted for stable output.
|
|
8
|
+
*
|
|
9
|
+
* state:
|
|
10
|
+
* - "missing" when the stored manifest has no files (never analyzed),
|
|
11
|
+
* - "fresh" when nothing changed,
|
|
12
|
+
* - "stale" otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export function computeFreshness(graph, currentEntries) {
|
|
15
|
+
const previous = graph.manifest.files;
|
|
16
|
+
const previousKeys = Object.keys(previous);
|
|
17
|
+
const changed = new Set();
|
|
18
|
+
for (const [relPath, entry] of Object.entries(currentEntries)) {
|
|
19
|
+
const prior = previous[relPath];
|
|
20
|
+
if (!prior || prior.hash !== entry.hash)
|
|
21
|
+
changed.add(relPath);
|
|
22
|
+
}
|
|
23
|
+
for (const relPath of previousKeys) {
|
|
24
|
+
if (!(relPath in currentEntries))
|
|
25
|
+
changed.add(relPath);
|
|
26
|
+
}
|
|
27
|
+
const changed_files = [...changed].sort();
|
|
28
|
+
const state = previousKeys.length === 0 ? "missing" : changed_files.length === 0 ? "fresh" : "stale";
|
|
29
|
+
return { state, changed_files };
|
|
30
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { behaviorNodes, priorityRank } from "../graph/factories.js";
|
|
2
|
+
import { structurallyUnconfirmable } from "../graph/confirmable.js";
|
|
3
|
+
/**
|
|
4
|
+
* Test-coverage gap analysis over behavior anchors.
|
|
5
|
+
*
|
|
6
|
+
* Surfaces behaviors that are missing concrete acceptance criteria and/or hard
|
|
7
|
+
* test evidence, so the kit can recommend where to add or generate tests. This
|
|
8
|
+
* is a *readiness* signal built from metadata only — no source code, prompts, or
|
|
9
|
+
* heuristic internals leak into the returned structure.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_LIMIT = 10;
|
|
12
|
+
/** Hard test evidence: explicit TESTED_BY/COVERS edge. */
|
|
13
|
+
function hasHardTestEvidence(graph, externalId) {
|
|
14
|
+
return graph.edges.some((e) => (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS") &&
|
|
15
|
+
(e.from_external_id === externalId || e.to_external_id === externalId));
|
|
16
|
+
}
|
|
17
|
+
/** Weak test evidence: only candidate MAY_BE_TESTED_BY/MAY_COVER. */
|
|
18
|
+
function hasWeakTestEvidence(graph, externalId) {
|
|
19
|
+
return graph.candidate_edges.some((e) => (e.relationship_type === "MAY_BE_TESTED_BY" || e.relationship_type === "MAY_COVER") &&
|
|
20
|
+
(e.from_external_id === externalId || e.to_external_id === externalId));
|
|
21
|
+
}
|
|
22
|
+
function testEvidenceFor(graph, externalId) {
|
|
23
|
+
if (hasHardTestEvidence(graph, externalId))
|
|
24
|
+
return "covered";
|
|
25
|
+
if (hasWeakTestEvidence(graph, externalId))
|
|
26
|
+
return "weak";
|
|
27
|
+
return "none";
|
|
28
|
+
}
|
|
29
|
+
/** Acceptance criteria via explicit edge OR inline properties. */
|
|
30
|
+
function hasAcceptanceCriteria(graph, node) {
|
|
31
|
+
const linked = graph.edges.some((e) => e.relationship_type === "HAS_ACCEPTANCE_CRITERION" && e.from_external_id === node.external_id);
|
|
32
|
+
if (linked)
|
|
33
|
+
return true;
|
|
34
|
+
const inline = node.properties.acceptance_criteria;
|
|
35
|
+
return Array.isArray(inline) && inline.length > 0;
|
|
36
|
+
}
|
|
37
|
+
/** Weakest evidence first, so the most under-tested behaviors surface at the top. */
|
|
38
|
+
const EVIDENCE_RANK = {
|
|
39
|
+
none: 0,
|
|
40
|
+
weak: 1,
|
|
41
|
+
covered: 2
|
|
42
|
+
};
|
|
43
|
+
function reasonFor(testEvidence, hasAc) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (testEvidence === "none")
|
|
46
|
+
parts.push("No test evidence linked");
|
|
47
|
+
else if (testEvidence === "weak")
|
|
48
|
+
parts.push("Only weak (candidate) test evidence linked");
|
|
49
|
+
if (!hasAc)
|
|
50
|
+
parts.push("Missing acceptance criteria");
|
|
51
|
+
return parts.length > 0 ? parts.join("; ") : "Behavior is fully covered";
|
|
52
|
+
}
|
|
53
|
+
function recommendedActionFor(testEvidence, hasAc) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (testEvidence === "none")
|
|
56
|
+
parts.push("Add or generate a test for this behavior");
|
|
57
|
+
else if (testEvidence === "weak")
|
|
58
|
+
parts.push("Confirm or strengthen the candidate test link into hard evidence");
|
|
59
|
+
if (!hasAc)
|
|
60
|
+
parts.push("Add acceptance criteria to make assertions concrete");
|
|
61
|
+
return parts.length > 0 ? parts.join("; ") : "No action needed";
|
|
62
|
+
}
|
|
63
|
+
export function findGaps(graph, opts) {
|
|
64
|
+
const behaviors = behaviorNodes(graph);
|
|
65
|
+
const totalBehaviors = behaviors.length;
|
|
66
|
+
// Behaviors whose only tests are e2e/api are NOT structural gaps — a real test
|
|
67
|
+
// likely exists, we just cannot confirm it by import resolution. Filter them
|
|
68
|
+
// out of the gap list (stays 3-state) and report the count separately.
|
|
69
|
+
const nsc = structurallyUnconfirmable(graph);
|
|
70
|
+
const minRank = opts?.min_priority === undefined ? -1 : priorityRank(opts.min_priority);
|
|
71
|
+
const limit = opts?.limit ?? DEFAULT_LIMIT;
|
|
72
|
+
const gaps = [];
|
|
73
|
+
let notStructurallyConfirmable = 0;
|
|
74
|
+
for (const node of behaviors) {
|
|
75
|
+
const priorityValue = String(node.properties.priority ?? "unknown");
|
|
76
|
+
if (priorityRank(priorityValue) < minRank)
|
|
77
|
+
continue;
|
|
78
|
+
if (nsc.has(node.external_id)) {
|
|
79
|
+
notStructurallyConfirmable++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const testEvidence = testEvidenceFor(graph, node.external_id);
|
|
83
|
+
const hasAc = hasAcceptanceCriteria(graph, node);
|
|
84
|
+
const isGap = testEvidence !== "covered" || hasAc === false;
|
|
85
|
+
if (!isGap)
|
|
86
|
+
continue;
|
|
87
|
+
gaps.push({
|
|
88
|
+
external_id: node.external_id,
|
|
89
|
+
title: node.title ?? node.external_id,
|
|
90
|
+
kind: node.kind,
|
|
91
|
+
priority: priorityValue,
|
|
92
|
+
reason: reasonFor(testEvidence, hasAc),
|
|
93
|
+
has_acceptance_criteria: hasAc,
|
|
94
|
+
test_evidence: testEvidence,
|
|
95
|
+
recommended_action: recommendedActionFor(testEvidence, hasAc)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const sorted = [...gaps].sort((a, b) => {
|
|
99
|
+
const evidenceDelta = EVIDENCE_RANK[a.test_evidence] - EVIDENCE_RANK[b.test_evidence];
|
|
100
|
+
if (evidenceDelta !== 0)
|
|
101
|
+
return evidenceDelta;
|
|
102
|
+
return priorityRank(b.priority) - priorityRank(a.priority);
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
gaps: sorted.slice(0, limit),
|
|
106
|
+
total_behaviors: totalBehaviors,
|
|
107
|
+
...(notStructurallyConfirmable > 0 ? { not_structurally_confirmable: notStructurallyConfirmable } : {}),
|
|
108
|
+
...(totalBehaviors === 0
|
|
109
|
+
? {
|
|
110
|
+
guidance: "No behavior anchors found. Run analyze on a path that contains tests, or add requirements/templates (.csv/.md) via --paths."
|
|
111
|
+
}
|
|
112
|
+
: {})
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const LOCAL_BUCKETS = [
|
|
2
|
+
"happy_path",
|
|
3
|
+
"validation_error",
|
|
4
|
+
"edge_case",
|
|
5
|
+
"integration_flow",
|
|
6
|
+
"security_privacy",
|
|
7
|
+
"regression"
|
|
8
|
+
];
|
|
9
|
+
export const BUCKET_LABEL = {
|
|
10
|
+
happy_path: "happy path",
|
|
11
|
+
validation_error: "validation error",
|
|
12
|
+
edge_case: "edge case",
|
|
13
|
+
integration_flow: "integration flow",
|
|
14
|
+
security_privacy: "security / privacy",
|
|
15
|
+
regression: "regression"
|
|
16
|
+
};
|
|
17
|
+
const SIGNAL_OF = {
|
|
18
|
+
happy_path: "hasExpectedBehavior",
|
|
19
|
+
validation_error: "hasValidationEvidence",
|
|
20
|
+
edge_case: "hasEdgeEvidence",
|
|
21
|
+
integration_flow: "hasIntegrationEvidence",
|
|
22
|
+
security_privacy: "hasSecurityEvidence",
|
|
23
|
+
regression: "hasRegressionEvidence"
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Ordered "slots" per total-test budget. Each slot is a small preference list;
|
|
27
|
+
* the first evidence-justified, not-yet-chosen bucket in a slot fills it. A slot
|
|
28
|
+
* with no justified candidate is skipped (no padding). Caps match the existing
|
|
29
|
+
* generation limits (default 3, max 5).
|
|
30
|
+
*/
|
|
31
|
+
const SLOTS_BY_LIMIT = {
|
|
32
|
+
1: [["happy_path"]],
|
|
33
|
+
2: [["happy_path"], ["validation_error", "edge_case"]],
|
|
34
|
+
3: [["happy_path"], ["validation_error", "edge_case"], ["integration_flow", "regression"]],
|
|
35
|
+
4: [["happy_path"], ["validation_error"], ["edge_case"], ["integration_flow"]],
|
|
36
|
+
5: [["happy_path"], ["validation_error"], ["edge_case"], ["integration_flow"], ["security_privacy", "regression"]]
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Choose up to `limit` local buckets, in priority order, that are justified by
|
|
40
|
+
* the evidence signals. Pure and deterministic. Never pads unjustified buckets.
|
|
41
|
+
*/
|
|
42
|
+
export function selectLocalBuckets(signals, limit) {
|
|
43
|
+
const L = Math.max(1, Math.min(5, Math.floor(limit) || 1));
|
|
44
|
+
const slots = SLOTS_BY_LIMIT[L];
|
|
45
|
+
const chosen = [];
|
|
46
|
+
for (const slot of slots) {
|
|
47
|
+
for (const bucket of slot) {
|
|
48
|
+
if (chosen.includes(bucket))
|
|
49
|
+
continue;
|
|
50
|
+
if (signals[SIGNAL_OF[bucket]]) {
|
|
51
|
+
chosen.push(bucket);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return chosen;
|
|
57
|
+
}
|
|
58
|
+
const VALIDATION_RE = /\b(invalid|errors?|required|reject(ed|s)?|validation|validate|forbidden|not allowed|unauthorized|400|422|constraint|missing (field|value|required)|bad request|must not)\b/;
|
|
59
|
+
const EDGE_RE = /\b(limits?|empty|nulls?|nullable|boundary|missing data|concurren\w*|stale|retr(y|ies)|timeouts?|edge[\s-]?cases?|maximum|minimum|overflow|underflow|zero|negative|out of range|race condition|duplicate)\b/;
|
|
60
|
+
const INTEGRATION_RE = /\b(routes?|endpoints?|api|https?|requests?|responses?|services?|workflows?|flows?|integration|fetch|client|navigat\w*|pages?|screens?|redirect|webhooks?|pipeline|composable|store)\b/;
|
|
61
|
+
const SECURITY_RE = /\b(auth\w*|permissions?|tokens?|secrets?|pii|privacy|sessions?|invites?|oauth|api[\s-]?keys?|roles?|access control|login|logout|passwords?|credentials?|csrf|xss|encrypt\w*)\b/;
|
|
62
|
+
const REGRESSION_RE = /\b(regressions?|bugs?|incidents?|fixed|broke(n)?|known issue|changelog|hotfix|defects?|reproduc\w*)\b/;
|
|
63
|
+
/** Derive bucket signals from a behavior's evidence. Pure. */
|
|
64
|
+
export function deriveBucketSignals(e) {
|
|
65
|
+
return {
|
|
66
|
+
hasExpectedBehavior: e.hasTestableAnchor,
|
|
67
|
+
hasValidationEvidence: VALIDATION_RE.test(e.corpus),
|
|
68
|
+
hasEdgeEvidence: EDGE_RE.test(e.corpus),
|
|
69
|
+
hasIntegrationEvidence: INTEGRATION_RE.test(e.corpus) || e.relatedFiles >= 2 || e.workflowSteps > 0,
|
|
70
|
+
hasSecurityEvidence: SECURITY_RE.test(e.corpus),
|
|
71
|
+
hasRegressionEvidence: REGRESSION_RE.test(e.corpus) || e.testNames > 0 || e.inferredFromTests
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { behaviorNodes } from "../graph/factories.js";
|
|
2
|
+
const JUDGE_SYSTEM = [
|
|
3
|
+
"You are a strict, impartial test-quality judge.",
|
|
4
|
+
"You are given the testing context (target behaviors, acceptance criteria, and the REAL modules/symbols that exist in the repo) and two candidate test suites:",
|
|
5
|
+
"A = prompt-only baseline, B = Local KG (graph-grounded).",
|
|
6
|
+
"Score EACH suite from 0 to 100 on four dimensions:",
|
|
7
|
+
"- completeness: depth and breadth of meaningful, concrete assertions and scenarios for the behavior.",
|
|
8
|
+
"- context_awareness: how well it uses the provided behaviors / acceptance criteria / context.",
|
|
9
|
+
"- accuracy: targets REAL modules/symbols from the provided list; penalize invented or hallucinated imports/APIs.",
|
|
10
|
+
"- domain_specificity: uses the repo's real domain vocabulary rather than generic placeholders.",
|
|
11
|
+
"The two suites may test different things — judge each on its own merits against the context; do NOT require them to match.",
|
|
12
|
+
'Return ONLY JSON: {"baseline":{"completeness":N,"context_awareness":N,"accuracy":N,"domain_specificity":N},"grounded":{"completeness":N,"context_awareness":N,"accuracy":N,"domain_specificity":N},"rationale":"one short sentence"}'
|
|
13
|
+
].join("\n");
|
|
14
|
+
function clampScore(v) {
|
|
15
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return 0;
|
|
18
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
19
|
+
}
|
|
20
|
+
function coerceDims(value) {
|
|
21
|
+
const o = (value && typeof value === "object" ? value : {});
|
|
22
|
+
return {
|
|
23
|
+
completeness: clampScore(o.completeness),
|
|
24
|
+
context_awareness: clampScore(o.context_awareness),
|
|
25
|
+
accuracy: clampScore(o.accuracy),
|
|
26
|
+
domain_specificity: clampScore(o.domain_specificity)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Parse the judge's JSON response (tolerant of code fences / surrounding prose). */
|
|
30
|
+
export function parseJudgeResponse(text) {
|
|
31
|
+
const stripped = text.replace(/```(?:json)?/gi, "");
|
|
32
|
+
const start = stripped.indexOf("{");
|
|
33
|
+
const end = stripped.lastIndexOf("}");
|
|
34
|
+
if (start < 0 || end <= start)
|
|
35
|
+
return null;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(stripped.slice(start, end + 1));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!parsed || typeof parsed !== "object")
|
|
44
|
+
return null;
|
|
45
|
+
const o = parsed;
|
|
46
|
+
if (!o.baseline && !o.grounded)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
baseline: coerceDims(o.baseline),
|
|
50
|
+
grounded: coerceDims(o.grounded),
|
|
51
|
+
rationale: typeof o.rationale === "string" ? o.rationale : undefined
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Compact, metadata-only context for the judge: targeted behaviors + real names. */
|
|
55
|
+
export function buildJudgeContext(graph, groundedTests) {
|
|
56
|
+
const behaviorIds = new Set(behaviorNodes(graph).map((b) => b.external_id));
|
|
57
|
+
const targeted = new Set();
|
|
58
|
+
for (const t of groundedTests)
|
|
59
|
+
for (const id of t.grounding.entity_ids)
|
|
60
|
+
if (behaviorIds.has(id))
|
|
61
|
+
targeted.add(id);
|
|
62
|
+
const lines = ["TARGET BEHAVIORS:"];
|
|
63
|
+
for (const id of targeted) {
|
|
64
|
+
const n = graph.nodes.find((x) => x.external_id === id);
|
|
65
|
+
if (!n)
|
|
66
|
+
continue;
|
|
67
|
+
const ac = Array.isArray(n.properties.acceptance_criteria) ? n.properties.acceptance_criteria : [];
|
|
68
|
+
lines.push(`- ${n.title || id}${ac.length ? ` (acceptance criteria: ${ac.slice(0, 4).map(String).join("; ")})` : ""}`);
|
|
69
|
+
}
|
|
70
|
+
if (targeted.size === 0)
|
|
71
|
+
lines.push("- (none resolved from grounding)");
|
|
72
|
+
// The grounded arm's source refs were validated against the local graph —
|
|
73
|
+
// list them explicitly so the judge can never call them invented. (The global
|
|
74
|
+
// sample below is 40-of-N: on a big repo it almost never contains the
|
|
75
|
+
// targeted files, and the old "treat names outside this list as possibly
|
|
76
|
+
// invented" wording made the judge zero accuracy/domain on that false
|
|
77
|
+
// premise — "ProfilePopover not present in this repo" on Mattermost.)
|
|
78
|
+
const groundedRefs = [];
|
|
79
|
+
for (const t of groundedTests) {
|
|
80
|
+
for (const r of t.grounding.source_refs || [])
|
|
81
|
+
if (!groundedRefs.includes(r))
|
|
82
|
+
groundedRefs.push(r);
|
|
83
|
+
}
|
|
84
|
+
if (groundedRefs.length) {
|
|
85
|
+
lines.push("", "GROUNDED REPO FILES (validated to exist in this repo — never treat these files, or symbols imported from them, as invented):", groundedRefs.slice(0, 12).join(", "));
|
|
86
|
+
}
|
|
87
|
+
const realNames = [];
|
|
88
|
+
for (const n of graph.nodes) {
|
|
89
|
+
if (realNames.length >= 40)
|
|
90
|
+
break;
|
|
91
|
+
if (n.kind === "File")
|
|
92
|
+
realNames.push(n.external_id.split("/").pop() || n.external_id);
|
|
93
|
+
else if (n.kind === "CodeSymbol" && n.title)
|
|
94
|
+
realNames.push(n.title);
|
|
95
|
+
}
|
|
96
|
+
lines.push("", "OTHER REAL MODULES/SYMBOLS (a small sample of a much larger repo — a name absent from this sample is NOT necessarily invented; only penalize clearly generic placeholders):", [...new Set(realNames)].join(", ") || "(none extracted)");
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
99
|
+
/** Ask the model to score both suites. Returns null on any failure (caller falls back). */
|
|
100
|
+
export async function judgeComparison(provider, contextText, baselineCode, groundedCode) {
|
|
101
|
+
const user = [
|
|
102
|
+
"CONTEXT:",
|
|
103
|
+
contextText,
|
|
104
|
+
"",
|
|
105
|
+
"SUITE A — prompt-only baseline:",
|
|
106
|
+
"```",
|
|
107
|
+
baselineCode || "(no tests generated)",
|
|
108
|
+
"```",
|
|
109
|
+
"",
|
|
110
|
+
"SUITE B — Local KG (graph-grounded):",
|
|
111
|
+
"```",
|
|
112
|
+
groundedCode || "(no tests generated)",
|
|
113
|
+
"```",
|
|
114
|
+
"",
|
|
115
|
+
"Score both suites now. Return ONLY the JSON object."
|
|
116
|
+
].join("\n");
|
|
117
|
+
try {
|
|
118
|
+
const raw = await provider.complete({ system: JUDGE_SYSTEM, user, temperature: 0 });
|
|
119
|
+
return parseJudgeResponse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|