@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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-prove orchestration for `opro start` (PR 1 of the auto-drive-to-proven spec).
|
|
3
|
+
*
|
|
4
|
+
* ORCHESTRATION ONLY — this file mints no proof. After analyze, when a provider key
|
|
5
|
+
* exists and the user did not opt out, it: selects the top provable TS/JS CodeSymbol
|
|
6
|
+
* targets (ORS-ranked), generates a runnable test per target via the EXISTING
|
|
7
|
+
* `generateTests`, writes it under `orangepro_generated/`, and runs it through the
|
|
8
|
+
* UNCHANGED `opProveLoop`/dynamic-proof oracle. Proven is minted only by that oracle;
|
|
9
|
+
* a static / non-killing / generation-failed / setup-failed test is an honest skip and
|
|
10
|
+
* NEVER becomes Proven. A later failed attempt cannot clobber a prior Proven (the #162
|
|
11
|
+
* fingerprint-scoped RTM selection guarantees this — this file does not defeat it).
|
|
12
|
+
*
|
|
13
|
+
* No key ⇒ writes NO files, mints NO proof, returns explicit guidance.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { basename, dirname, posix, resolve, sep } from "node:path";
|
|
17
|
+
import { generateTests, readDeclaredDeps, unresolvedLocalImports } from "./generate/generator.js";
|
|
18
|
+
import { GENERATED_DIR, runHintsFor } from "./generate/runHints.js";
|
|
19
|
+
import { rankRiskGaps } from "./score/risk.js";
|
|
20
|
+
import { resolveProviderConfig } from "./localConfig.js";
|
|
21
|
+
import { buildProvider } from "./generate/providers.js";
|
|
22
|
+
import { resolveContained } from "./reprove/paths.js";
|
|
23
|
+
import { buildRtm } from "./rtm.js";
|
|
24
|
+
import { loadLedger } from "./ledger.js";
|
|
25
|
+
import { loadGraph, workspacePaths } from "./workspace.js";
|
|
26
|
+
import { systemClock } from "./util/time.js";
|
|
27
|
+
import { redactSecrets } from "./util/redact.js";
|
|
28
|
+
import { classifyBaselineFailure, EXPERIMENTAL_SQLITE_TEST_ENV, IMPORT_TIME_CATEGORIES, isNeedsSetupCategory, readEnginesNode, targetNeedsExperimentalSqlite } from "./proofRunnability.js";
|
|
29
|
+
/** Real dynamic proof is TS/JS-only; only these targets are attemptable. */
|
|
30
|
+
function isTsJsFile(file) {
|
|
31
|
+
return /\.[cm]?[jt]sx?$/i.test(file);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* SOLE trust barrier for auto-prove target selection. `opDynamicProof` has NO
|
|
35
|
+
* eligibility guard — `resolveTargetSymbol` resolves ANY CodeSymbol and the prove
|
|
36
|
+
* path never reads eligibility — so handing it an excluded infra symbol would mint
|
|
37
|
+
* Proven against plumbing. Auto-prove must therefore refuse anything that is not an
|
|
38
|
+
* entry-point-adjacent behavior surface: eligible (top-level `denominator_eligible`),
|
|
39
|
+
* `behavior_surface === "entrypoint_adjacent"`, and carrying NO `denominator_reason_code`
|
|
40
|
+
* (infra_behavior_surface / not_entry_point_adjacent). TS/JS file, always.
|
|
41
|
+
*/
|
|
42
|
+
export function isEligibleProvableTarget(node) {
|
|
43
|
+
if (!node || node.kind !== "CodeSymbol")
|
|
44
|
+
return false;
|
|
45
|
+
if (node.denominator_eligible !== true)
|
|
46
|
+
return false;
|
|
47
|
+
if (node.properties.behavior_surface !== "entrypoint_adjacent")
|
|
48
|
+
return false;
|
|
49
|
+
if (node.properties.denominator_reason_code != null)
|
|
50
|
+
return false;
|
|
51
|
+
const file = typeof node.properties.file === "string" ? node.properties.file : node.external_id.replace(/^sym:/, "").split("#")[0];
|
|
52
|
+
return isTsJsFile(file);
|
|
53
|
+
}
|
|
54
|
+
const GENERATED_HEADER = "// Generated by OrangePro — do not edit";
|
|
55
|
+
// "Static map first, dynamically prove top 5": ONE unified dynamic-proof budget spans
|
|
56
|
+
// BOTH lanes (existing-tests first, then generation for the remaining eligible gaps).
|
|
57
|
+
// TOTAL attempts (existing + generation) are capped at this budget; `--auto-limit N`
|
|
58
|
+
// overrides it (clamped to MAX_AUTO_LIMIT) for deeper runs (`opro start --auto-limit 25`,
|
|
59
|
+
// `opro prove`, `opro prove-loop`). Static breadth (behaviors/flows/associated/risk) is
|
|
60
|
+
// never gated on this budget — only the dynamic verification pass is.
|
|
61
|
+
const DEFAULT_AUTO_LIMIT = 5;
|
|
62
|
+
const MAX_AUTO_LIMIT = 50;
|
|
63
|
+
/** Generator caps a single call at 5 (MAX_LIMIT); page candidates in windows of that. */
|
|
64
|
+
const GEN_WINDOW = 5;
|
|
65
|
+
// ponytail: cap the weak MAY_*/MAY_RELATE_TO fan-out a single symbol contributes so one hot
|
|
66
|
+
// file (every eligible symbol × every importing test) cannot starve the shared budget before
|
|
67
|
+
// a provable hard-edge symbol is tried. Small K; promote to a flag if a repo needs a wider sweep.
|
|
68
|
+
const EXISTING_LANE_MAX_WEAK_PER_SYMBOL = 3;
|
|
69
|
+
export const NO_KEY_MESSAGE = "No provider key; auto-prove skipped — add OPENAI_API_KEY / ANTHROPIC_API_KEY, or use the OrangePro MCP in your coding agent.";
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the contained absolute path for a generated file, REJECTING any name that
|
|
72
|
+
* escapes `<sourceRoot>/orangepro_generated/`. Exported so the guardrail is unit-tested.
|
|
73
|
+
*/
|
|
74
|
+
export function containedGeneratedPath(sourceRoot, filename) {
|
|
75
|
+
const dir = resolve(sourceRoot, GENERATED_DIR);
|
|
76
|
+
return resolveContained(dir, filename);
|
|
77
|
+
}
|
|
78
|
+
/** The source file a `sym:` target lives in (workspace-relative). */
|
|
79
|
+
function symbolFile(target) {
|
|
80
|
+
return target.slice("sym:".length).split("#")[0] ?? "";
|
|
81
|
+
}
|
|
82
|
+
/** Relative import specifiers (`./x`, `../x`) in import/export/require/dynamic-import positions. */
|
|
83
|
+
const REL_IMPORT_RE = /(\bfrom\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\s*\(\s*)(['"])(\.\.?\/[^'"]*)\2/g;
|
|
84
|
+
/**
|
|
85
|
+
* Re-point a generated test's RELATIVE imports so they resolve from
|
|
86
|
+
* `orangepro_generated/`. The generator grounds imports against the SOURCE dir (the
|
|
87
|
+
* model co-locates the test with the module under test, e.g. `./order.service`), but
|
|
88
|
+
* autoProve writes the test to `orangepro_generated/`. Resolve each `./x` / `../x`
|
|
89
|
+
* against the target source file's directory, then re-express it relative to
|
|
90
|
+
* `orangepro_generated/` (POSIX, leading `./`) — e.g. `./order.service` →
|
|
91
|
+
* `../src/order.service`. Only import/require positions are touched, never plain
|
|
92
|
+
* string literals. TS/JS only (dynamic proof is TS/JS-only).
|
|
93
|
+
*/
|
|
94
|
+
function rewriteRelativeImports(body, targetFileRel, generatedDir) {
|
|
95
|
+
const baseDir = posix.dirname(targetFileRel.split(sep).join("/"));
|
|
96
|
+
return body.replace(REL_IMPORT_RE, (_m, prefix, quote, spec) => {
|
|
97
|
+
const abs = posix.normalize(posix.join(baseDir === "." ? "" : baseDir, spec));
|
|
98
|
+
let rel = posix.relative(generatedDir, abs);
|
|
99
|
+
if (!rel.startsWith("."))
|
|
100
|
+
rel = `./${rel}`;
|
|
101
|
+
return `${prefix}${quote}${rel}${quote}`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function fileReaderFor(root) {
|
|
105
|
+
const absRoot = resolve(root);
|
|
106
|
+
return (relPath) => {
|
|
107
|
+
const abs = resolve(absRoot, relPath);
|
|
108
|
+
if (abs !== absRoot && !abs.startsWith(absRoot + sep))
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
return readFileSync(abs, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* R-1: classify a prove-loop result. A RED baseline is classified from the oracle's
|
|
120
|
+
* already-redacted `baseline.failureSummary` (+ the target's declared engines.node vs the
|
|
121
|
+
* runner Node) into a SPECIFIC reason. logic_failure is a genuine failing test on the real
|
|
122
|
+
* code → an honest non-proof, NEVER needs_setup. No proof is minted here.
|
|
123
|
+
*/
|
|
124
|
+
function classifyProof(result, ctx) {
|
|
125
|
+
if ("status" in result && result.status === "unrunnable") {
|
|
126
|
+
// Setup did not run (env non-event) — nothing was minted, target needs setup.
|
|
127
|
+
return { classification: "needs_setup", reason: result.reason };
|
|
128
|
+
}
|
|
129
|
+
const dyn = result;
|
|
130
|
+
const record = dyn.record;
|
|
131
|
+
if (record.closed)
|
|
132
|
+
return { classification: "proven" };
|
|
133
|
+
const cert = record.dynamic_proof;
|
|
134
|
+
if (cert && cert.baseline_green === false) {
|
|
135
|
+
const { category, reason } = classifyBaselineFailure({
|
|
136
|
+
failureSummary: dyn.oracle.baseline?.failureSummary,
|
|
137
|
+
enginesNode: readEnginesNode(ctx.sourceRoot, ctx.targetFileRel),
|
|
138
|
+
runnerNode: ctx.runnerNode ?? process.version
|
|
139
|
+
});
|
|
140
|
+
const classification = isNeedsSetupCategory(category) ? "needs_setup" : "non_killing";
|
|
141
|
+
return { classification, reason: redactSecrets(reason), category };
|
|
142
|
+
}
|
|
143
|
+
return { classification: "non_killing", reason: record.reason ?? "Mutant survived; the test does not assert on the target's real behavior." };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* R-1 sibling-dedup key: a baseline-red import-time failure is a deterministic property of
|
|
147
|
+
* loading the TARGET FILE with a given runner, independent of which test runs it — so
|
|
148
|
+
* same-file siblings share it. Keyed on the TARGET FILE only (every caller pins runner to
|
|
149
|
+
* undefined): the sole deduped cause is engine_mismatch, a package-level fact both lanes
|
|
150
|
+
* classify against the SAME process.version, so the runner must not be in the key. The old
|
|
151
|
+
* {runner, target file} key split the cache across lanes (lane 1 wrote "auto <file>", the
|
|
152
|
+
* generation lane read "<runner> <file>" -> never matched), silently disabling cross-lane
|
|
153
|
+
* dedup. NEVER merges across different files or a different failure class.
|
|
154
|
+
*/
|
|
155
|
+
function dedupKey(runner, targetFileRel) {
|
|
156
|
+
return `${runner ?? "auto"}\u0000${targetFileRel}`;
|
|
157
|
+
}
|
|
158
|
+
/** A same-file sibling deduped WITHOUT re-running: shares the first attempt's redacted reason. */
|
|
159
|
+
function dedupedAttempt(targetSymbol, testPath, targetFileRel, blocked) {
|
|
160
|
+
return {
|
|
161
|
+
target_symbol: targetSymbol,
|
|
162
|
+
test_path: testPath,
|
|
163
|
+
classification: "needs_setup",
|
|
164
|
+
reason: `${blocked.reason} (shared root cause with a sibling in ${targetFileRel}; not re-run).`,
|
|
165
|
+
category: blocked.category,
|
|
166
|
+
deduped: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* R-2: the exact experimental-builtin env profile for a target that references node:sqlite,
|
|
171
|
+
* else undefined. Auto-applied only on a confident source reference; forwarded through the
|
|
172
|
+
* EXISTING test_env path (opProveLoop → oracle --test-env, spike-allowlisted). It only makes
|
|
173
|
+
* the baseline RUNNABLE — never asserts, mocks, or mints Proven. Ambient NODE_OPTIONS is never
|
|
174
|
+
* forwarded (this is an explicit fixed flag, not process.env).
|
|
175
|
+
*/
|
|
176
|
+
function experimentalSqliteTestEnv(reader, targetFileRel) {
|
|
177
|
+
if (!targetNeedsExperimentalSqlite(reader, targetFileRel))
|
|
178
|
+
return undefined;
|
|
179
|
+
// Only inject where the runner Node actually accepts the flag in NODE_OPTIONS. On Node < 22.5 the
|
|
180
|
+
// flag is rejected outright ("--experimental-sqlite is not allowed in NODE_OPTIONS", exit 9), which
|
|
181
|
+
// would make the baseline unrunnable for a spurious reason; there node:sqlite is unavailable anyway,
|
|
182
|
+
// so we skip injection and let R-1 classify it honestly as an experimental-builtin needs_setup.
|
|
183
|
+
// The oracle spawns the runner with this same Node (process.execPath), so this check is authoritative.
|
|
184
|
+
if (!process.allowedNodeEnvironmentFlags.has("--experimental-sqlite"))
|
|
185
|
+
return undefined;
|
|
186
|
+
return [EXPERIMENTAL_SQLITE_TEST_ENV];
|
|
187
|
+
}
|
|
188
|
+
const zeroSummary = (status, ran, reason) => ({
|
|
189
|
+
ran,
|
|
190
|
+
status,
|
|
191
|
+
reason,
|
|
192
|
+
attempted: 0,
|
|
193
|
+
proven: 0,
|
|
194
|
+
needs_setup: [],
|
|
195
|
+
skipped: [],
|
|
196
|
+
generated_files: [],
|
|
197
|
+
attempts: []
|
|
198
|
+
});
|
|
199
|
+
function symbolFileOf(node) {
|
|
200
|
+
return typeof node.properties.file === "string" ? node.properties.file : node.external_id.replace(/^sym:/, "").split("#")[0];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Map each ELIGIBLE CodeSymbol to the existing test files statically linked to it — the
|
|
204
|
+
* repo's OWN tests. Sources are exactly the edges RTM/coverage read for the "Associated"
|
|
205
|
+
* tier:
|
|
206
|
+
* - hard TESTED_BY/COVERS edges (the confirmer's sym↔TestCase links, `graph.edges`);
|
|
207
|
+
* - weak MAY_BE_TESTED_BY/MAY_COVER candidate edges (sym↔TestCase, `graph.candidate_edges`);
|
|
208
|
+
* - weak MAY_RELATE_TO candidate edges (a test file resolved-imports a source FILE) —
|
|
209
|
+
* the broadest signal, mapped to every eligible symbol in that source file.
|
|
210
|
+
* The static edge only SELECTS a target+test pair for the oracle — it is never itself proof,
|
|
211
|
+
* and a symbol the test does not actually exercise simply survives the mutant (honest skip).
|
|
212
|
+
* AI-suggested candidate edges are excluded (a model guess is not an existing test).
|
|
213
|
+
*/
|
|
214
|
+
export function existingAssociatedTests(graph, nodeById) {
|
|
215
|
+
const out = new Map();
|
|
216
|
+
const testFileOf = (id) => {
|
|
217
|
+
const n = nodeById.get(id);
|
|
218
|
+
if (!n || n.kind !== "TestCase")
|
|
219
|
+
return null;
|
|
220
|
+
return typeof n.properties.file === "string" ? n.properties.file : id.replace(/^test:/, "");
|
|
221
|
+
};
|
|
222
|
+
// `hard` = TESTED_BY/COVERS (the confirmer's structural links); weak = MAY_* candidate
|
|
223
|
+
// edges. Hard is recorded before weak (graph.edges scanned first), so a test already
|
|
224
|
+
// linked hard is never downgraded; a later weak dup only upgrades an existing weak to hard.
|
|
225
|
+
const add = (symId, testRel, hard) => {
|
|
226
|
+
if (!isEligibleProvableTarget(nodeById.get(symId)))
|
|
227
|
+
return;
|
|
228
|
+
const list = out.get(symId);
|
|
229
|
+
if (!list) {
|
|
230
|
+
out.set(symId, [{ test: testRel, hard }]);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const existing = list.find((t) => t.test === testRel);
|
|
234
|
+
if (existing) {
|
|
235
|
+
if (hard)
|
|
236
|
+
existing.hard = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
list.push({ test: testRel, hard });
|
|
240
|
+
};
|
|
241
|
+
// A sym↔test edge joins one TestCase endpoint to one CodeSymbol endpoint; resolve
|
|
242
|
+
// whichever side is the symbol so both edge directions are handled uniformly.
|
|
243
|
+
const link = (a, b, hard) => {
|
|
244
|
+
const tb = testFileOf(b);
|
|
245
|
+
if (tb && nodeById.get(a)?.kind === "CodeSymbol")
|
|
246
|
+
return add(a, tb, hard);
|
|
247
|
+
const ta = testFileOf(a);
|
|
248
|
+
if (ta && nodeById.get(b)?.kind === "CodeSymbol")
|
|
249
|
+
add(b, ta, hard);
|
|
250
|
+
};
|
|
251
|
+
// Eligible symbols grouped by source file, for the file-level MAY_RELATE_TO expansion.
|
|
252
|
+
const eligibleByFile = new Map();
|
|
253
|
+
for (const n of graph.nodes) {
|
|
254
|
+
if (!isEligibleProvableTarget(n))
|
|
255
|
+
continue;
|
|
256
|
+
const f = symbolFileOf(n);
|
|
257
|
+
const list = eligibleByFile.get(f);
|
|
258
|
+
if (list)
|
|
259
|
+
list.push(n.external_id);
|
|
260
|
+
else
|
|
261
|
+
eligibleByFile.set(f, [n.external_id]);
|
|
262
|
+
}
|
|
263
|
+
// MAY_RELATE_TO endpoints are plain workspace-relative file paths; a test file's node id
|
|
264
|
+
// is `test:${relPath}`. Return the test relPath for whichever endpoint is a TestCase file.
|
|
265
|
+
const testRelForFile = (relPath) => {
|
|
266
|
+
const n = nodeById.get(`test:${relPath}`);
|
|
267
|
+
return n && n.kind === "TestCase" ? relPath : null;
|
|
268
|
+
};
|
|
269
|
+
for (const e of graph.edges) {
|
|
270
|
+
if (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS")
|
|
271
|
+
link(e.from_external_id, e.to_external_id, true);
|
|
272
|
+
}
|
|
273
|
+
for (const e of graph.candidate_edges ?? []) {
|
|
274
|
+
if (e.review_status === "ai_suggested")
|
|
275
|
+
continue;
|
|
276
|
+
if (e.relationship_type === "MAY_BE_TESTED_BY" || e.relationship_type === "MAY_COVER") {
|
|
277
|
+
link(e.from_external_id, e.to_external_id, false);
|
|
278
|
+
}
|
|
279
|
+
else if (e.relationship_type === "MAY_RELATE_TO") {
|
|
280
|
+
const fromTest = testRelForFile(e.from_external_id);
|
|
281
|
+
const testRel = fromTest ?? testRelForFile(e.to_external_id);
|
|
282
|
+
const sourceRel = fromTest ? e.to_external_id : e.from_external_id;
|
|
283
|
+
if (testRel)
|
|
284
|
+
for (const symId of eligibleByFile.get(sourceRel) ?? [])
|
|
285
|
+
add(symId, testRel, false);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Fix 3 — deterministic hard-first, weak-capped attempt order. Every hard TESTED_BY/COVERS
|
|
292
|
+
* pair precedes ANY weak MAY_* pair (a provable hard-edge symbol is tried before weak fan-out
|
|
293
|
+
* burns the shared budget), and each symbol contributes at most `maxWeakPerSymbol` weak pairs
|
|
294
|
+
* so one hot MAY_RELATE_TO file (its every eligible symbol × every importing test) can't starve
|
|
295
|
+
* the budget. Order within each tier follows the Map's insertion order (graph node/edge order),
|
|
296
|
+
* so the result is deterministic.
|
|
297
|
+
*/
|
|
298
|
+
export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING_LANE_MAX_WEAK_PER_SYMBOL) {
|
|
299
|
+
const hard = [];
|
|
300
|
+
const weak = [];
|
|
301
|
+
for (const [symId, tests] of testsBySymbol) {
|
|
302
|
+
let weakCount = 0;
|
|
303
|
+
for (const t of tests) {
|
|
304
|
+
if (t.hard)
|
|
305
|
+
hard.push({ symId, testRel: t.test, hard: true });
|
|
306
|
+
else if (weakCount++ < maxWeakPerSymbol)
|
|
307
|
+
weak.push({ symId, testRel: t.test, hard: false });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return [...hard, ...weak];
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* PR 1.5 lane — prove the repo's OWN existing tests, NO provider key. For each eligible
|
|
314
|
+
* target with an existing associated test, run the UNCHANGED `opProveLoop` with the test
|
|
315
|
+
* IN ITS ORIGINAL LOCATION (never copied/relocated — that avoids the generation lane's
|
|
316
|
+
* import-grounding pitfall) and a null-sentinel mutant. Closes → Proven; survives /
|
|
317
|
+
* crashes-pre-assert / setup-fails → honest skip (never Proven, and #162 best-ever
|
|
318
|
+
* selection means it can never clobber a prior Proven). Consumes from the SHARED unified
|
|
319
|
+
* budget (`budget`) — the generation lane gets whatever this lane leaves unspent, so
|
|
320
|
+
* TOTAL attempts (existing + generation) never exceed the budget.
|
|
321
|
+
*/
|
|
322
|
+
function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, budget) {
|
|
323
|
+
const attempts = [];
|
|
324
|
+
const needsSetup = [];
|
|
325
|
+
const provenSymbols = new Set();
|
|
326
|
+
let proven = 0;
|
|
327
|
+
let attempted = 0;
|
|
328
|
+
const changed = opts.changedFiles && opts.changedFiles.length > 0 ? new Set(opts.changedFiles) : null;
|
|
329
|
+
const reader = fileReaderFor(sourceRoot); // R-2: source scan for the node:sqlite env profile
|
|
330
|
+
// Fix 3: hard TESTED_BY/COVERS pairs first, weak MAY_* pairs after and capped per symbol.
|
|
331
|
+
const queue = orderExistingAttempts(existingAssociatedTests(graph, nodeById));
|
|
332
|
+
for (const { symId, testRel } of queue) {
|
|
333
|
+
if (attempted >= budget)
|
|
334
|
+
break;
|
|
335
|
+
const node = nodeById.get(symId);
|
|
336
|
+
// Redundant with existingAssociatedTests' own filter, but the eligibility barrier is
|
|
337
|
+
// the sole guard against handing plumbing to the guard-less prove path — assert it here too.
|
|
338
|
+
if (!isEligibleProvableTarget(node))
|
|
339
|
+
continue;
|
|
340
|
+
if (changed && !changed.has(symbolFileOf(node)))
|
|
341
|
+
continue;
|
|
342
|
+
// Fix 1: already Proven for the CURRENT code (fingerprint match under #162) → skip. Re-closing
|
|
343
|
+
// it would overstate newly-proven and append a redundant closed cert every run.
|
|
344
|
+
if (alreadyProven.has(symId))
|
|
345
|
+
continue;
|
|
346
|
+
// Proven earlier THIS run — one closing existing test per symbol is enough.
|
|
347
|
+
if (provenSymbols.has(symId))
|
|
348
|
+
continue;
|
|
349
|
+
const targetFileRel = symbolFileOf(node);
|
|
350
|
+
// R-1 sibling dedup: a prior same-file attempt hit a package-level env root cause
|
|
351
|
+
// (engine_mismatch: runner Node outside the declared engines range). Every sibling in this
|
|
352
|
+
// file fails baseline identically → mark it
|
|
353
|
+
// needs_setup WITHOUT re-running (and WITHOUT consuming the attempt budget).
|
|
354
|
+
const blocked = importTimeBlocked.get(dedupKey(undefined, targetFileRel));
|
|
355
|
+
if (blocked) {
|
|
356
|
+
const attempt = dedupedAttempt(symId, testRel, targetFileRel, blocked);
|
|
357
|
+
attempts.push(attempt);
|
|
358
|
+
needsSetup.push(attempt);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
attempted++;
|
|
362
|
+
// R-2: inject NODE_OPTIONS=--experimental-sqlite via the existing test_env path when the
|
|
363
|
+
// target references node:sqlite. Makes the baseline runnable only; never mints Proven.
|
|
364
|
+
const testEnv = experimentalSqliteTestEnv(reader, targetFileRel);
|
|
365
|
+
let result;
|
|
366
|
+
try {
|
|
367
|
+
result = proveLoop(root,
|
|
368
|
+
// link_node_modules: the isolated proof copy excludes node_modules; without linking,
|
|
369
|
+
// any target/test importing a repo dependency fails baseline → needs_setup. Linking only
|
|
370
|
+
// makes real tests runnable — Proven still requires the dynamic oracle's sentinel kill.
|
|
371
|
+
{ target_symbol: symId, source: sourceRoot, test_path: testRel, replacement: "return null;", link_node_modules: true, ...(testEnv ? { test_env: testEnv } : {}), run_id: `auto-prove-existing-${attempted}` }, proveDeps);
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
const attempt = {
|
|
375
|
+
target_symbol: symId,
|
|
376
|
+
test_path: testRel,
|
|
377
|
+
classification: "needs_setup",
|
|
378
|
+
reason: `Proof could not run: ${redactSecrets(errMsg(e))}`
|
|
379
|
+
};
|
|
380
|
+
attempts.push(attempt);
|
|
381
|
+
needsSetup.push(attempt);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const { classification, reason, category } = classifyProof(result, { sourceRoot, targetFileRel });
|
|
385
|
+
const attempt = { target_symbol: symId, test_path: testRel, classification, reason, category };
|
|
386
|
+
attempts.push(attempt);
|
|
387
|
+
if (classification === "proven") {
|
|
388
|
+
proven++;
|
|
389
|
+
provenSymbols.add(symId); // its remaining queued tests are skipped above
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (classification === "needs_setup") {
|
|
393
|
+
needsSetup.push(attempt);
|
|
394
|
+
// Cache an import-time root cause so same-file siblings dedup instead of re-running.
|
|
395
|
+
if (category && IMPORT_TIME_CATEGORIES.has(category)) {
|
|
396
|
+
importTimeBlocked.set(dedupKey(undefined, targetFileRel), { category, reason: reason ?? "" });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// non_killing → keep trying this symbol's other associated tests, if any.
|
|
400
|
+
}
|
|
401
|
+
return { attempts, needsSetup, proven, attempted, provenSymbols };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Drive prove for the top provable TS/JS targets. Two lanes: (1) prove the repo's OWN
|
|
405
|
+
* existing associated tests — NO key, runs first (PR 1.5); (2) key-gated generate → prove
|
|
406
|
+
* to fill the remaining gaps (PR 1). The existing oracle is the sole proof judge for both;
|
|
407
|
+
* generation writes are contained under orangepro_generated/.
|
|
408
|
+
*/
|
|
409
|
+
export async function autoProve(root, opts, deps) {
|
|
410
|
+
if (opts.noAuto)
|
|
411
|
+
return zeroSummary("disabled", false, "--no-auto: auto-prove disabled; analyze-only.");
|
|
412
|
+
const clock = deps.clock ?? systemClock;
|
|
413
|
+
const proveLoop = deps.proveLoop;
|
|
414
|
+
const proveDeps = {
|
|
415
|
+
clock,
|
|
416
|
+
env: deps.env,
|
|
417
|
+
dynamicProofRunner: deps.dynamicProofRunner,
|
|
418
|
+
coverageRunner: deps.coverageRunner,
|
|
419
|
+
analyze: deps.analyze,
|
|
420
|
+
aiProvider: deps.aiProvider
|
|
421
|
+
};
|
|
422
|
+
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
423
|
+
const sourceRoot = resolve(graph.workspace.root);
|
|
424
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.external_id, n]));
|
|
425
|
+
// Fix 1: symbols already Proven for the CURRENT code. `buildRtm(graph, ledger)` IS the
|
|
426
|
+
// #162 fingerprint-scoped decision that sets a row's tier to "proven" (via
|
|
427
|
+
// selectLedgerBySymbol), so we reuse it verbatim rather than re-deriving fingerprints.
|
|
428
|
+
// Both lanes skip these so res.proven counts only NEWLY-minted proofs this run and no
|
|
429
|
+
// redundant closed cert is appended. A changed file → different fingerprint → not in
|
|
430
|
+
// this set → still attempted (re-prove for the new code).
|
|
431
|
+
const alreadyProven = new Set(buildRtm(graph, loadLedger(root)).rows
|
|
432
|
+
.filter((r) => r.evidence_tier === "proven")
|
|
433
|
+
.map((r) => r.code_symbol)
|
|
434
|
+
.filter(Boolean));
|
|
435
|
+
// R-1: shared sibling-dedup cache of import-time baseline failures. Spans BOTH lanes so a
|
|
436
|
+
// node:sqlite-style root cause found once is never re-run across same-file siblings.
|
|
437
|
+
const importTimeBlocked = new Map();
|
|
438
|
+
// ONE unified dynamic-proof budget for the whole pass (existing-first → then generation).
|
|
439
|
+
// Default 5 ("dynamically prove top 5"); `--auto-limit N` overrides it, clamped to
|
|
440
|
+
// MAX_AUTO_LIMIT. The existing lane consumes from this budget and the generation lane gets
|
|
441
|
+
// only the remainder, so TOTAL attempts (existing + generation) are ≤ budget.
|
|
442
|
+
const autoLimit = Math.max(1, Math.min(MAX_AUTO_LIMIT, Math.floor(opts.autoLimit ?? DEFAULT_AUTO_LIMIT)));
|
|
443
|
+
// ── Lane 1: existing associated tests — NO key required, runs FIRST (PR 1.5). ──
|
|
444
|
+
const ex = proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, autoLimit);
|
|
445
|
+
// Key gate applies ONLY to the generation lane. No provider key ⇒ generation is skipped
|
|
446
|
+
// (no files, no fake proof) with explicit guidance; the existing-tests lane still counts.
|
|
447
|
+
const providerConfig = resolveProviderConfig(deps.env, { provider: opts.provider, model: opts.model });
|
|
448
|
+
if (!providerConfig) {
|
|
449
|
+
const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "skipped-no-key";
|
|
450
|
+
return {
|
|
451
|
+
ran: ex.attempted > 0,
|
|
452
|
+
status,
|
|
453
|
+
reason: NO_KEY_MESSAGE,
|
|
454
|
+
attempted: ex.attempted,
|
|
455
|
+
proven: ex.proven,
|
|
456
|
+
needs_setup: ex.needsSetup,
|
|
457
|
+
skipped: [],
|
|
458
|
+
generated_files: [],
|
|
459
|
+
attempts: ex.attempts
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const provider = buildProvider(providerConfig);
|
|
463
|
+
const generate = deps.generate ?? generateTests;
|
|
464
|
+
const reader = fileReaderFor(sourceRoot);
|
|
465
|
+
// Generation gets only the budget the existing-tests lane left unspent, so existing +
|
|
466
|
+
// generation attempts total ≤ autoLimit. Exhausted budget ⇒ genBudget 0 ⇒ no provider call.
|
|
467
|
+
const genBudget = Math.max(0, autoLimit - ex.attempted);
|
|
468
|
+
// Candidates = ORS-ranked provable CodeSymbols. rankRiskGaps ranks by OrangePro Risk
|
|
469
|
+
// Score and excludes hard-confirmed symbols; we ALSO enforce the eligibility barrier
|
|
470
|
+
// explicitly (isEligibleProvableTarget) at selection so an excluded infra symbol can
|
|
471
|
+
// NEVER reach the guard-less prove path, and drop any symbol the existing-tests lane
|
|
472
|
+
// already proved so generation fills only the remaining gaps. PR mode scopes to the
|
|
473
|
+
// eligible symbols whose file is in the changed set (changed_files → symbols, NOT
|
|
474
|
+
// affected_behaviors — those are Requirement/UserFlow/BusinessRule ids opDynamicProof
|
|
475
|
+
// cannot prove).
|
|
476
|
+
let candidates = rankRiskGaps(graph, { repoRoot: sourceRoot, limit: 500 }).filter((g) => isEligibleProvableTarget(nodeById.get(g.id)) && !ex.provenSymbols.has(g.id) && !alreadyProven.has(g.id));
|
|
477
|
+
if (opts.changedFiles && opts.changedFiles.length > 0) {
|
|
478
|
+
const changed = new Set(opts.changedFiles);
|
|
479
|
+
candidates = candidates.filter((g) => changed.has(g.file));
|
|
480
|
+
}
|
|
481
|
+
const attempts = [];
|
|
482
|
+
const needsSetup = [];
|
|
483
|
+
const skipped = [];
|
|
484
|
+
const generatedFiles = [];
|
|
485
|
+
const declaredDeps = readDeclaredDeps(sourceRoot);
|
|
486
|
+
let proven = 0;
|
|
487
|
+
let attempted = 0;
|
|
488
|
+
for (let start = 0; start < candidates.length && attempted < genBudget; start += GEN_WINDOW) {
|
|
489
|
+
const window = candidates.slice(start, start + GEN_WINDOW);
|
|
490
|
+
const need = genBudget - attempted;
|
|
491
|
+
const windowIds = window.map((g) => g.id);
|
|
492
|
+
const gen = await generate(graph, { target_ids: windowIds, limit: Math.min(windowIds.length, need) }, provider, reader, clock);
|
|
493
|
+
const tests = gen.generated_tests;
|
|
494
|
+
// Global start offset so filenames stay unique across windows — runHintsFor
|
|
495
|
+
// otherwise resets its index to 0 per window and same-slug targets collide.
|
|
496
|
+
const hints = runHintsFor(tests, sourceRoot, start);
|
|
497
|
+
for (let i = 0; i < tests.length && attempted < genBudget; i++) {
|
|
498
|
+
const test = tests[i];
|
|
499
|
+
const hint = hints[i];
|
|
500
|
+
if (!hint.prove_run) {
|
|
501
|
+
// Non-TS/JS target or no resolvable code symbol → not attemptable.
|
|
502
|
+
skipped.push({
|
|
503
|
+
target_symbol: hint.target_symbol_external_id,
|
|
504
|
+
title: test.title,
|
|
505
|
+
reason: hint.target_symbol_external_id
|
|
506
|
+
? "Target is not TS/JS; dynamic proof supports TS/JS CodeSymbol targets only."
|
|
507
|
+
: "No resolvable TS/JS code-symbol target to prove."
|
|
508
|
+
});
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const { target_symbol, replacement, runner } = hint.prove_run.args;
|
|
512
|
+
const targetFileRel = symbolFile(target_symbol);
|
|
513
|
+
// R-1 sibling dedup (cross-lane): a same-file target already hit an import-time env root
|
|
514
|
+
// cause → a fresh generated test importing the same module fails identically. Skip it
|
|
515
|
+
// WITHOUT generating/writing/running or consuming the attempt budget.
|
|
516
|
+
const blocked = importTimeBlocked.get(dedupKey(undefined, targetFileRel));
|
|
517
|
+
if (blocked) {
|
|
518
|
+
const attempt = dedupedAttempt(target_symbol, "", targetFileRel, blocked);
|
|
519
|
+
attempts.push(attempt);
|
|
520
|
+
needsSetup.push(attempt);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const filename = basename(hint.prove_run.args.test_path);
|
|
524
|
+
let abs;
|
|
525
|
+
try {
|
|
526
|
+
abs = containedGeneratedPath(sourceRoot, filename);
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
skipped.push({ target_symbol, title: test.title, reason: `Generated path rejected (escapes ${GENERATED_DIR}/): ${redactSecrets(errMsg(e))}` });
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const writeRel = `${GENERATED_DIR}/${filename}`;
|
|
533
|
+
// The generator grounds imports at the source/suggested dir; autoProve writes
|
|
534
|
+
// to orangepro_generated/. Re-point relative specifiers at the target source
|
|
535
|
+
// file so they resolve from the write location (`./x` → `../src/x`).
|
|
536
|
+
const body = rewriteRelativeImports(test.body, symbolFile(target_symbol), GENERATED_DIR);
|
|
537
|
+
// A "non-runnable" verdict is usually just import LOCATION, which the rewrite
|
|
538
|
+
// fixes. Re-validate against the ACTUAL write location: skip only if locals
|
|
539
|
+
// still won't resolve (a genuine setup gap the oracle could never honestly close).
|
|
540
|
+
if (test.runnable === false && unresolvedLocalImports(body, abs, sourceRoot, declaredDeps).length > 0) {
|
|
541
|
+
skipped.push({
|
|
542
|
+
target_symbol,
|
|
543
|
+
title: test.title,
|
|
544
|
+
reason: `Generator returned a non-runnable draft${test.unresolved_reason ? `: ${test.unresolved_reason}` : "."}`
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (existsSync(abs)) {
|
|
549
|
+
// Guardrail: never overwrite an existing file.
|
|
550
|
+
skipped.push({ target_symbol, title: test.title, reason: `Existing file ${writeRel} not overwritten.` });
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
554
|
+
writeFileSync(abs, `${GENERATED_HEADER}\n${body}${body.endsWith("\n") ? "" : "\n"}`, "utf8");
|
|
555
|
+
generatedFiles.push(writeRel);
|
|
556
|
+
attempted++;
|
|
557
|
+
// R-2: inject the node:sqlite env profile when the TARGET source references the builtin.
|
|
558
|
+
const testEnv = experimentalSqliteTestEnv(reader, targetFileRel);
|
|
559
|
+
let result;
|
|
560
|
+
try {
|
|
561
|
+
result = proveLoop(root, {
|
|
562
|
+
target_symbol,
|
|
563
|
+
source: sourceRoot,
|
|
564
|
+
test_path: writeRel,
|
|
565
|
+
replacement,
|
|
566
|
+
runner,
|
|
567
|
+
// See lane 1: link node_modules so a generated test importing a repo dep can boot.
|
|
568
|
+
link_node_modules: true,
|
|
569
|
+
...(testEnv ? { test_env: testEnv } : {}),
|
|
570
|
+
run_id: `auto-prove-${start + i + 1}`
|
|
571
|
+
}, proveDeps);
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
const attempt = {
|
|
575
|
+
target_symbol,
|
|
576
|
+
test_path: writeRel,
|
|
577
|
+
classification: "needs_setup",
|
|
578
|
+
reason: `Proof could not run: ${redactSecrets(errMsg(e))}`
|
|
579
|
+
};
|
|
580
|
+
attempts.push(attempt);
|
|
581
|
+
needsSetup.push(attempt);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const { classification, reason, category } = classifyProof(result, { sourceRoot, targetFileRel });
|
|
585
|
+
const attempt = { target_symbol, test_path: writeRel, classification, reason, category };
|
|
586
|
+
attempts.push(attempt);
|
|
587
|
+
if (classification === "proven")
|
|
588
|
+
proven++;
|
|
589
|
+
else if (classification === "needs_setup") {
|
|
590
|
+
needsSetup.push(attempt);
|
|
591
|
+
// Cache an import-time root cause so same-file siblings dedup instead of re-running.
|
|
592
|
+
if (category && IMPORT_TIME_CATEGORIES.has(category)) {
|
|
593
|
+
importTimeBlocked.set(dedupKey(undefined, targetFileRel), { category, reason: reason ?? "" });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// non_killing stays in `attempts` only — an honest skip, never Proven.
|
|
597
|
+
}
|
|
598
|
+
// A window that produced nothing (transient generation hiccup) must NOT abandon
|
|
599
|
+
// lower-ranked candidates: the outer loop already terminates at pool exhaustion
|
|
600
|
+
// or when the attempt budget (autoLimit) is reached.
|
|
601
|
+
}
|
|
602
|
+
// Merge the existing-tests lane (ran first, no key) with the generation lane.
|
|
603
|
+
const totalProven = ex.proven + proven;
|
|
604
|
+
const totalAttempted = ex.attempted + attempted;
|
|
605
|
+
const status = totalProven > 0 ? "proven-run" : totalAttempted > 0 ? "ran-no-proof" : "no-targets";
|
|
606
|
+
return {
|
|
607
|
+
ran: true,
|
|
608
|
+
status,
|
|
609
|
+
attempted: totalAttempted,
|
|
610
|
+
proven: totalProven,
|
|
611
|
+
needs_setup: [...ex.needsSetup, ...needsSetup],
|
|
612
|
+
skipped,
|
|
613
|
+
generated_files: generatedFiles,
|
|
614
|
+
attempts: [...ex.attempts, ...attempts]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function errMsg(e) {
|
|
618
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
619
|
+
return msg.split("\n", 1)[0] ?? msg;
|
|
620
|
+
}
|