@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,400 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
2
|
+
async function postJson(fetchImpl, url, headers, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetchImpl(url, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
signal: controller.signal,
|
|
9
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
10
|
+
body: JSON.stringify(body)
|
|
11
|
+
});
|
|
12
|
+
const text = await res.text();
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
const err = new Error(`Model provider HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
15
|
+
err.status = res.status;
|
|
16
|
+
err.bodyText = text;
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
return text ? JSON.parse(text) : {};
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
// Map the cryptic AbortError ("This operation was aborted") to an actionable
|
|
23
|
+
// timeout message — the #1 cause is a reasoning model thinking past the cap.
|
|
24
|
+
if (e?.name === "AbortError") {
|
|
25
|
+
throw new Error(`Model call timed out after ${Math.round(timeoutMs / 1000)}s. Reasoning models (gpt-5/o-series) can spend ` +
|
|
26
|
+
`minutes on hidden reasoning before responding; retry, or try a smaller --limit or a different model.`);
|
|
27
|
+
}
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Newer OpenAI models (GPT-5.x, o-series reasoning models) require
|
|
35
|
+
// `max_completion_tokens` instead of `max_tokens` and reject a non-default
|
|
36
|
+
// `temperature`. Used only to SEED the request; the adapter still self-corrects
|
|
37
|
+
// from the API's own error, so this list need not be exhaustive or current.
|
|
38
|
+
const REASONING_MODEL = /(?:gpt-5|^o[0-9]|[-/]o[0-9])/i;
|
|
39
|
+
/**
|
|
40
|
+
* Reasoning models spend minutes on hidden reasoning before the (non-streaming)
|
|
41
|
+
* response returns — the 60s default killed live gpt-5 runs mid-generation
|
|
42
|
+
* ("This operation was aborted"), and the starvation retry's 4x budget makes
|
|
43
|
+
* calls even longer. 10 minutes is a ceiling, not an expectation.
|
|
44
|
+
*/
|
|
45
|
+
const REASONING_TIMEOUT_MS = 600_000;
|
|
46
|
+
/** Per-call timeout: generous for reasoning models, default for the rest. */
|
|
47
|
+
export function providerTimeoutMs(model) {
|
|
48
|
+
return REASONING_MODEL.test(model) ? REASONING_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* From a 400, determine which token param the model actually WANTS. OpenAI's error
|
|
52
|
+
* is explicit — "'max_tokens' is not supported ... Use 'max_completion_tokens'
|
|
53
|
+
* instead" — so we honor the "Use X instead" directive instead of guessing from the
|
|
54
|
+
* offending param name. That avoids flipping a reasoning model onto `max_tokens`
|
|
55
|
+
* (which always fails) when the first `max_completion_tokens` call errored for an
|
|
56
|
+
* unrelated reason. Returns null when the 400 is not about the token param.
|
|
57
|
+
*/
|
|
58
|
+
function wantedTokenParam(e) {
|
|
59
|
+
const err = e;
|
|
60
|
+
if (err?.status !== 400 || !err.bodyText)
|
|
61
|
+
return null;
|
|
62
|
+
const text = err.bodyText;
|
|
63
|
+
if (/use\s+['"`]?max_completion_tokens/i.test(text))
|
|
64
|
+
return "max_completion_tokens";
|
|
65
|
+
if (/use\s+['"`]?max_tokens\b/i.test(text))
|
|
66
|
+
return "max_tokens";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** True when a 400 says the model rejects a non-default `temperature`. */
|
|
70
|
+
function temperatureRejected(e) {
|
|
71
|
+
const err = e;
|
|
72
|
+
if (err?.status !== 400 || !err.bodyText)
|
|
73
|
+
return false;
|
|
74
|
+
return /\btemperature\b/i.test(err.bodyText) && /unsupported|does not support|only the default/i.test(err.bodyText);
|
|
75
|
+
}
|
|
76
|
+
/** OpenAI-compatible Chat Completions adapter (OpenAI, Azure-compatible, local gateways). */
|
|
77
|
+
export class OpenAICompatibleProvider {
|
|
78
|
+
cfg;
|
|
79
|
+
fetchImpl;
|
|
80
|
+
providerName = "openai";
|
|
81
|
+
constructor(cfg, fetchImpl = fetch) {
|
|
82
|
+
this.cfg = cfg;
|
|
83
|
+
this.fetchImpl = fetchImpl;
|
|
84
|
+
}
|
|
85
|
+
get modelName() {
|
|
86
|
+
return this.cfg.model;
|
|
87
|
+
}
|
|
88
|
+
async complete(req) {
|
|
89
|
+
const modern = REASONING_MODEL.test(this.cfg.model);
|
|
90
|
+
// Reasoning models spend tokens on hidden reasoning, so give them more room.
|
|
91
|
+
let maxTokens = req.maxTokens ?? (modern ? 4000 : 900);
|
|
92
|
+
// Seed from the model name, then self-correct from the API's explicit directive.
|
|
93
|
+
let tokenParam = modern ? "max_completion_tokens" : "max_tokens";
|
|
94
|
+
let sendTemperature = !modern;
|
|
95
|
+
const tried = new Set();
|
|
96
|
+
for (;;) {
|
|
97
|
+
const body = {
|
|
98
|
+
model: this.cfg.model,
|
|
99
|
+
[tokenParam]: maxTokens,
|
|
100
|
+
messages: [
|
|
101
|
+
{ role: "system", content: req.system },
|
|
102
|
+
{ role: "user", content: req.user }
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
if (sendTemperature)
|
|
106
|
+
body.temperature = req.temperature ?? 0.2;
|
|
107
|
+
try {
|
|
108
|
+
const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/chat/completions`, { Authorization: `Bearer ${this.cfg.apiKey ?? ""}` }, body, providerTimeoutMs(this.cfg.model)));
|
|
109
|
+
const choice = data.choices?.[0];
|
|
110
|
+
const content = choice?.message?.content ?? "";
|
|
111
|
+
// Reasoning starvation: the model spent the ENTIRE completion budget on
|
|
112
|
+
// hidden reasoning and returned no visible content (finish_reason
|
|
113
|
+
// "length"). Long grounded prompts trigger this on gpt-5/o-series at the
|
|
114
|
+
// 4000-token seed. Retry ONCE with 4x the budget; if it still comes back
|
|
115
|
+
// empty, return "" and let the caller refuse to emit an empty test.
|
|
116
|
+
if (!content.trim() && choice?.finish_reason === "length" && !tried.has("starved")) {
|
|
117
|
+
tried.add("starved");
|
|
118
|
+
maxTokens = maxTokens * 4;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
return content;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
// Switch to the token param the API explicitly asked for (once) — never guess,
|
|
125
|
+
// so a reasoning model is never flipped onto max_tokens by an unrelated 400.
|
|
126
|
+
const want = wantedTokenParam(e);
|
|
127
|
+
if (want && want !== tokenParam && !tried.has("token-swap")) {
|
|
128
|
+
tried.add("token-swap");
|
|
129
|
+
tokenParam = want;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (temperatureRejected(e) && sendTemperature && !tried.has("temp-drop")) {
|
|
133
|
+
tried.add("temp-drop");
|
|
134
|
+
sendTemperature = false;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Ollama local-model adapter (no API key). */
|
|
143
|
+
export class OllamaProvider {
|
|
144
|
+
cfg;
|
|
145
|
+
fetchImpl;
|
|
146
|
+
providerName = "ollama";
|
|
147
|
+
constructor(cfg, fetchImpl = fetch) {
|
|
148
|
+
this.cfg = cfg;
|
|
149
|
+
this.fetchImpl = fetchImpl;
|
|
150
|
+
}
|
|
151
|
+
get modelName() {
|
|
152
|
+
return this.cfg.model;
|
|
153
|
+
}
|
|
154
|
+
async complete(req) {
|
|
155
|
+
const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/api/chat`, {}, {
|
|
156
|
+
model: this.cfg.model,
|
|
157
|
+
stream: false,
|
|
158
|
+
messages: [
|
|
159
|
+
{ role: "system", content: req.system },
|
|
160
|
+
{ role: "user", content: req.user }
|
|
161
|
+
]
|
|
162
|
+
}));
|
|
163
|
+
return data.message?.content ?? "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Anthropic Messages API adapter. */
|
|
167
|
+
export class AnthropicProvider {
|
|
168
|
+
cfg;
|
|
169
|
+
fetchImpl;
|
|
170
|
+
providerName = "anthropic";
|
|
171
|
+
constructor(cfg, fetchImpl = fetch) {
|
|
172
|
+
this.cfg = cfg;
|
|
173
|
+
this.fetchImpl = fetchImpl;
|
|
174
|
+
}
|
|
175
|
+
get modelName() {
|
|
176
|
+
return this.cfg.model;
|
|
177
|
+
}
|
|
178
|
+
async complete(req) {
|
|
179
|
+
const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/messages`, { "x-api-key": this.cfg.apiKey ?? "", "anthropic-version": "2023-06-01" }, {
|
|
180
|
+
model: this.cfg.model,
|
|
181
|
+
max_tokens: req.maxTokens ?? 900,
|
|
182
|
+
temperature: req.temperature ?? 0.2,
|
|
183
|
+
system: req.system,
|
|
184
|
+
messages: [{ role: "user", content: req.user }]
|
|
185
|
+
}));
|
|
186
|
+
return (data.content ?? []).map((c) => c.text ?? "").join("");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Deterministic, offline provider. Used when no BYOK credentials are present
|
|
191
|
+
* (tests + offline internal validation). It renders the grounded prompt into a
|
|
192
|
+
* framework-hinted scaffold whose specificity tracks the evidence it was given —
|
|
193
|
+
* so a grounded prompt yields concrete assertions while a bare raw-prompt
|
|
194
|
+
* baseline collapses to a generic smoke test. Clearly labeled as deterministic.
|
|
195
|
+
*/
|
|
196
|
+
export class DeterministicProvider {
|
|
197
|
+
providerName = "deterministic";
|
|
198
|
+
modelName = "orangepro-local-deterministic-v0";
|
|
199
|
+
async complete(req) {
|
|
200
|
+
return renderDeterministic(req.user);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function extractSection(text, label) {
|
|
204
|
+
const lines = text.split(/\r?\n/);
|
|
205
|
+
const out = [];
|
|
206
|
+
let inSection = false;
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
if (new RegExp(`^${label}:`, "i").test(line.trim())) {
|
|
209
|
+
inSection = true;
|
|
210
|
+
const inline = line.split(":").slice(1).join(":").trim();
|
|
211
|
+
if (inline)
|
|
212
|
+
out.push(inline);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (inSection) {
|
|
216
|
+
// A section ends at the next ALL-CAPS header — with or without a trailing
|
|
217
|
+
// parenthetical (e.g. "EXISTING TESTS (already covered ...):"). Without the
|
|
218
|
+
// `(` alternative, prompt-v1 sections bled into the previous extraction.
|
|
219
|
+
if (/^[A-Z][A-Z _]+\s*[(:]/.test(line.trim()))
|
|
220
|
+
break;
|
|
221
|
+
const item = line.replace(/^[-*\d.\s]+/, "").trim();
|
|
222
|
+
if (item)
|
|
223
|
+
out.push(item);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function snakeIdent(text) {
|
|
229
|
+
const s = text.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
230
|
+
return s || "behavior";
|
|
231
|
+
}
|
|
232
|
+
function pascalIdent(text) {
|
|
233
|
+
const s = text
|
|
234
|
+
.split(/[^a-z0-9]+/i)
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
237
|
+
.join("");
|
|
238
|
+
return /^[A-Za-z]/.test(s) ? s : "Behavior";
|
|
239
|
+
}
|
|
240
|
+
function goStringLiteral(text) {
|
|
241
|
+
return JSON.stringify(text);
|
|
242
|
+
}
|
|
243
|
+
function camelIdent(text) {
|
|
244
|
+
const p = pascalIdent(text);
|
|
245
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
246
|
+
}
|
|
247
|
+
function renderDeterministic(user) {
|
|
248
|
+
const behavior = extractSection(user, "BEHAVIOR")[0] || "the target behavior";
|
|
249
|
+
const framework = (extractSection(user, "FRAMEWORK")[0] || "vitest").toLowerCase();
|
|
250
|
+
const criteria = extractSection(user, "ACCEPTANCE CRITERIA");
|
|
251
|
+
const assertions = criteria.length
|
|
252
|
+
? criteria
|
|
253
|
+
: ["the primary expected outcome is observable"]; // generic fallback for raw baseline
|
|
254
|
+
return renderSkeleton(framework, behavior, assertions);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The offline stand-in's body: VALID, runnable framework code (NOT Markdown).
|
|
258
|
+
* It is a grounded skeleton — names the behavior, lists the acceptance criteria as
|
|
259
|
+
* a comment, and includes one trivially-passing placeholder assertion so the file
|
|
260
|
+
* compiles and runs. A real BYOK model replaces the placeholder with concrete
|
|
261
|
+
* assertions; imports for the grounded arm are prepended by synthesizeImports.
|
|
262
|
+
*/
|
|
263
|
+
function renderSkeleton(framework, behavior, assertions) {
|
|
264
|
+
// Escape for embedding in a double-quoted JS string: backslash FIRST, then quote,
|
|
265
|
+
// so titles with Windows paths / regex (\d, \U) or quotes can't emit invalid code.
|
|
266
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
267
|
+
const title = esc(behavior);
|
|
268
|
+
const first = esc(assertions[0] ?? "behaves as specified");
|
|
269
|
+
const ac = assertions.join("; ");
|
|
270
|
+
if (framework.includes("pytest") || framework.includes("python")) {
|
|
271
|
+
return [
|
|
272
|
+
`def test_${snakeIdent(behavior)}():`,
|
|
273
|
+
// Behavior + acceptance go in line comments (no docstring) so backslashes or
|
|
274
|
+
// embedded quotes in the title can never break the Python source.
|
|
275
|
+
` # ${behavior}`,
|
|
276
|
+
` # acceptance: ${ac}`,
|
|
277
|
+
" # TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
278
|
+
" assert True # placeholder — replace with a concrete assertion",
|
|
279
|
+
""
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
if (framework.includes("go")) {
|
|
283
|
+
return [
|
|
284
|
+
"package main",
|
|
285
|
+
"",
|
|
286
|
+
"import \"testing\"",
|
|
287
|
+
"",
|
|
288
|
+
`func Test${pascalIdent(behavior)}(t *testing.T) {`,
|
|
289
|
+
`\t// ${behavior}`,
|
|
290
|
+
`\t// acceptance: ${ac}`,
|
|
291
|
+
"\t// TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
292
|
+
"\tif false {",
|
|
293
|
+
`\t\tt.Fatalf("expected %q", ${goStringLiteral(first)})`,
|
|
294
|
+
"\t}",
|
|
295
|
+
"}",
|
|
296
|
+
""
|
|
297
|
+
].join("\n");
|
|
298
|
+
}
|
|
299
|
+
if (framework.includes("junit") || framework.includes("java")) {
|
|
300
|
+
const className = `${pascalIdent(behavior)}Test`;
|
|
301
|
+
if (framework.includes("junit4")) {
|
|
302
|
+
return [
|
|
303
|
+
"import org.junit.Test;",
|
|
304
|
+
"",
|
|
305
|
+
"import static org.junit.Assert.assertTrue;",
|
|
306
|
+
"",
|
|
307
|
+
`class ${className} {`,
|
|
308
|
+
" @Test",
|
|
309
|
+
` public void ${camelIdent(behavior)}() {`,
|
|
310
|
+
` // ${behavior}`,
|
|
311
|
+
` // acceptance: ${ac}`,
|
|
312
|
+
" // TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
313
|
+
` assertTrue(${JSON.stringify(`expected ${first}`)}, true);`,
|
|
314
|
+
" }",
|
|
315
|
+
"}",
|
|
316
|
+
""
|
|
317
|
+
].join("\n");
|
|
318
|
+
}
|
|
319
|
+
return [
|
|
320
|
+
"import org.junit.jupiter.api.Test;",
|
|
321
|
+
"",
|
|
322
|
+
"import static org.junit.jupiter.api.Assertions.assertTrue;",
|
|
323
|
+
"",
|
|
324
|
+
`class ${className} {`,
|
|
325
|
+
" @Test",
|
|
326
|
+
` void ${camelIdent(behavior)}() {`,
|
|
327
|
+
` // ${behavior}`,
|
|
328
|
+
` // acceptance: ${ac}`,
|
|
329
|
+
" // TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
330
|
+
` assertTrue(true, ${JSON.stringify(`expected ${first}`)});`,
|
|
331
|
+
" }",
|
|
332
|
+
"}",
|
|
333
|
+
""
|
|
334
|
+
].join("\n");
|
|
335
|
+
}
|
|
336
|
+
if (framework.includes("playwright")) {
|
|
337
|
+
return [
|
|
338
|
+
`test("${title}", async ({ page }) => {`,
|
|
339
|
+
` // acceptance: ${ac}`,
|
|
340
|
+
" // TODO: navigate (page.goto), interact, then assert the real outcome.",
|
|
341
|
+
" expect(true).toBeTruthy(); // placeholder — replace with a concrete assertion",
|
|
342
|
+
"});",
|
|
343
|
+
""
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
if (framework.includes("cypress")) {
|
|
347
|
+
return [
|
|
348
|
+
`describe("${title}", () => {`,
|
|
349
|
+
` it("${first}", () => {`,
|
|
350
|
+
` // acceptance: ${ac}`,
|
|
351
|
+
" // TODO: drive the UI with cy.* and assert the real outcome.",
|
|
352
|
+
' cy.wrap(true).should("eq", true); // placeholder — replace with a concrete assertion',
|
|
353
|
+
" });",
|
|
354
|
+
"});",
|
|
355
|
+
""
|
|
356
|
+
].join("\n");
|
|
357
|
+
}
|
|
358
|
+
if (framework.includes("ava")) {
|
|
359
|
+
return [
|
|
360
|
+
'import test from "ava";',
|
|
361
|
+
"",
|
|
362
|
+
`test("${title}", (t) => {`,
|
|
363
|
+
` // acceptance: ${ac}`,
|
|
364
|
+
" // TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
365
|
+
" t.true(true); // placeholder — replace with a concrete assertion",
|
|
366
|
+
"});",
|
|
367
|
+
""
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
// jest / vitest / mocha / generic
|
|
371
|
+
return [
|
|
372
|
+
`describe("${title}", () => {`,
|
|
373
|
+
` it("${first}", () => {`,
|
|
374
|
+
` // acceptance: ${ac}`,
|
|
375
|
+
" // TODO: arrange + act on the subject under test, then assert the real outcome.",
|
|
376
|
+
" expect(true).toBe(true); // placeholder — replace with a concrete assertion",
|
|
377
|
+
" });",
|
|
378
|
+
"});",
|
|
379
|
+
""
|
|
380
|
+
].join("\n");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Construct a real BYOK provider from a resolved config. Requires a non-null
|
|
384
|
+
* config and throws on an unknown provider — it never silently falls back to the
|
|
385
|
+
* deterministic stand-in. The deterministic provider must be requested
|
|
386
|
+
* explicitly (`new DeterministicProvider()` at the call site) so the BYOK
|
|
387
|
+
* contract cannot be reintroduced by accident.
|
|
388
|
+
*/
|
|
389
|
+
export function buildProvider(cfg, fetchImpl = fetch) {
|
|
390
|
+
switch (cfg.provider) {
|
|
391
|
+
case "openai":
|
|
392
|
+
return new OpenAICompatibleProvider(cfg, fetchImpl);
|
|
393
|
+
case "ollama":
|
|
394
|
+
return new OllamaProvider(cfg, fetchImpl);
|
|
395
|
+
case "anthropic":
|
|
396
|
+
return new AnthropicProvider(cfg, fetchImpl);
|
|
397
|
+
default:
|
|
398
|
+
throw new Error(`Unknown model provider: ${String(cfg.provider)}. Use provider="deterministic" for the offline stand-in.`);
|
|
399
|
+
}
|
|
400
|
+
}
|