@orangepro/orangepro-mcp 0.2.29 → 0.2.31

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.
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
+ const COVERAGE_SUITE_MANIFEST = ".orangepro/coverage-suites.json";
3
4
  const GO_COVERAGE_CANDIDATES = new Set([
4
5
  "coverage.out",
5
6
  "cover.out",
@@ -47,6 +48,48 @@ function symbolLanguage(n) {
47
48
  return "java";
48
49
  return "other";
49
50
  }
51
+ function normalizedArtifactPath(value) {
52
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "");
53
+ if (!normalized || path.posix.isAbsolute(normalized) || normalized.split("/").includes(".."))
54
+ return null;
55
+ return normalized;
56
+ }
57
+ function coverageSuiteManifest(root) {
58
+ const out = new Map();
59
+ try {
60
+ const raw = JSON.parse(readFileSync(path.join(root, COVERAGE_SUITE_MANIFEST), "utf8"));
61
+ for (const [artifactPath, value] of Object.entries(raw.artifacts ?? {})) {
62
+ const rel = normalizedArtifactPath(artifactPath);
63
+ if (!rel || !value || typeof value !== "object")
64
+ continue;
65
+ const suite = value.suite;
66
+ if (suite !== "unit" && suite !== "integration")
67
+ continue;
68
+ const command = value.command;
69
+ out.set(rel, { suite, ...(typeof command === "string" && command.trim() ? { command: command.trim() } : {}) });
70
+ }
71
+ }
72
+ catch {
73
+ // Missing or malformed provenance is fail-visible as unclassified coverage.
74
+ }
75
+ return out;
76
+ }
77
+ function inferredCoverageSuite(rel) {
78
+ const normalized = `/${rel.toLowerCase().replace(/[^a-z0-9/._-]+/g, "-")}/`;
79
+ if (/(?:^|[/_.-])(integration|e2e|end-to-end)(?:[/_.-]|$)/.test(normalized))
80
+ return "integration";
81
+ if (/(?:^|[/_.-])unit(?:[/_.-]|$)/.test(normalized))
82
+ return "unit";
83
+ return null;
84
+ }
85
+ export function coverageSuiteForArtifact(root, rel) {
86
+ const normalized = normalizedArtifactPath(rel) ?? rel.replace(/\\/g, "/");
87
+ const declared = coverageSuiteManifest(root).get(normalized);
88
+ if (declared)
89
+ return { suite: declared.suite, suite_source: "manifest", ...(declared.command ? { command: declared.command } : {}) };
90
+ const inferred = inferredCoverageSuite(normalized);
91
+ return inferred ? { suite: inferred, suite_source: "inferred" } : { suite: "unclassified", suite_source: "unclassified" };
92
+ }
50
93
  function artifactCandidates(root, files) {
51
94
  const out = new Map();
52
95
  const add = (candidate) => {
@@ -79,7 +122,11 @@ function artifactCandidates(root, files) {
79
122
  catch {
80
123
  /* no generated coverage dir */
81
124
  }
82
- return [...out.values()].sort((a, b) => a.path.localeCompare(b.path));
125
+ return [...out.values()]
126
+ .map((candidate) => {
127
+ return { ...candidate, ...coverageSuiteForArtifact(root, candidate.path) };
128
+ })
129
+ .sort((a, b) => a.path.localeCompare(b.path));
83
130
  }
84
131
  function discoverNestedCoverageArtifacts(root) {
85
132
  const out = [];
@@ -109,7 +156,7 @@ function discoverNestedCoverageArtifacts(root) {
109
156
  }
110
157
  };
111
158
  visit("");
112
- return out;
159
+ return out.map((candidate) => ({ ...candidate, ...coverageSuiteForArtifact(root, candidate.path) }));
113
160
  }
114
161
  function goModuleForDir(dir, goModulesByDir) {
115
162
  for (;;) {
@@ -424,13 +471,14 @@ function parseJacocoXml(root, rel, codeFiles) {
424
471
  };
425
472
  }
426
473
  function parseCoverageArtifact(root, candidate, goModulesByDir, codeFiles) {
427
- if (candidate.format === "go-coverprofile")
428
- return parseGoCoverprofile(root, candidate.path, goModulesByDir, codeFiles);
429
- if (candidate.format === "lcov")
430
- return parseLcov(root, candidate.path, codeFiles);
431
- if (candidate.format === "coverage-py")
432
- return parseCoveragePyXml(root, candidate.path, codeFiles);
433
- return parseJacocoXml(root, candidate.path, codeFiles);
474
+ const parsed = candidate.format === "go-coverprofile"
475
+ ? parseGoCoverprofile(root, candidate.path, goModulesByDir, codeFiles)
476
+ : candidate.format === "lcov"
477
+ ? parseLcov(root, candidate.path, codeFiles)
478
+ : candidate.format === "coverage-py"
479
+ ? parseCoveragePyXml(root, candidate.path, codeFiles)
480
+ : parseJacocoXml(root, candidate.path, codeFiles);
481
+ return { ...parsed, ranges: parsed.ranges.map((range) => ({ ...range, suite: candidate.suite })) };
434
482
  }
435
483
  function overlaps(aStart, aEnd, bStart, bEnd) {
436
484
  return aStart <= bEnd && bStart <= aEnd;
@@ -451,7 +499,14 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
451
499
  skippedArtifacts.push(skipped);
452
500
  if (ranges.length === 0)
453
501
  continue;
454
- const stat = artifactStats.get(artifact.path) ?? { files: new Set(), covered_ranges: 0, format: artifact.format };
502
+ const stat = artifactStats.get(artifact.path) ?? {
503
+ files: new Set(),
504
+ covered_ranges: 0,
505
+ format: artifact.format,
506
+ suite: artifact.suite,
507
+ suite_source: artifact.suite_source,
508
+ ...(artifact.command ? { command: artifact.command } : {})
509
+ };
455
510
  for (const range of ranges) {
456
511
  const list = rangesByFile.get(range.file);
457
512
  if (list)
@@ -467,6 +522,9 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
467
522
  return undefined;
468
523
  const byLanguage = {};
469
524
  const coveredSymbols = new Set();
525
+ const unitSymbols = new Set();
526
+ const integrationSymbols = new Set();
527
+ const unclassifiedSymbols = new Set();
470
528
  let totalEligible = 0;
471
529
  let symbolsWithSpans = 0;
472
530
  const ingestedLanguages = new Set([...artifactStats.values()].map((s) => languageForFormat(s.format)));
@@ -491,12 +549,20 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
491
549
  if (overlapping.length === 0)
492
550
  continue;
493
551
  const formats = [...new Set(overlapping.map((r) => r.format))].sort();
552
+ const suites = [...new Set(overlapping.map((r) => r.suite ?? "unclassified"))].sort();
494
553
  coveredSymbols.add(n.external_id);
554
+ if (suites.includes("unit"))
555
+ unitSymbols.add(n.external_id);
556
+ if (suites.includes("integration"))
557
+ integrationSymbols.add(n.external_id);
558
+ if (suites.includes("unclassified"))
559
+ unclassifiedSymbols.add(n.external_id);
495
560
  bucket.covered++;
496
561
  n.properties = {
497
562
  ...n.properties,
498
563
  runtime_covered: true,
499
- runtime_coverage_formats: formats
564
+ runtime_coverage_formats: formats,
565
+ runtime_coverage_suites: suites
500
566
  };
501
567
  }
502
568
  for (const bucket of Object.values(byLanguage))
@@ -505,6 +571,9 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
505
571
  artifacts: [...artifactStats.entries()].map(([path, s]) => ({
506
572
  path,
507
573
  format: s.format,
574
+ suite: s.suite,
575
+ suite_source: s.suite_source,
576
+ ...(s.command ? { command: s.command } : {}),
508
577
  files: s.files.size,
509
578
  covered_ranges: s.covered_ranges
510
579
  })),
@@ -513,6 +582,18 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
513
582
  symbols_with_spans: symbolsWithSpans,
514
583
  covered_symbols: coveredSymbols.size,
515
584
  covered_pct: pct(coveredSymbols.size, totalEligible),
516
- by_language: byLanguage
585
+ by_language: byLanguage,
586
+ by_suite: {
587
+ unit: unitSymbols.size,
588
+ integration: integrationSymbols.size,
589
+ overlap: [...unitSymbols].filter((id) => integrationSymbols.has(id)).length,
590
+ unclassified: unclassifiedSymbols.size,
591
+ union: coveredSymbols.size,
592
+ unit_pct: pct(unitSymbols.size, totalEligible),
593
+ integration_pct: pct(integrationSymbols.size, totalEligible),
594
+ overlap_pct: pct([...unitSymbols].filter((id) => integrationSymbols.has(id)).length, totalEligible),
595
+ unclassified_pct: pct(unclassifiedSymbols.size, totalEligible),
596
+ union_pct: pct(coveredSymbols.size, totalEligible)
597
+ }
517
598
  };
518
599
  }
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { reportProgress } from "../util/progress.js";
5
+ import { coverageSuiteForArtifact } from "./coverage.js";
5
6
  const DEFAULT_TIMEOUT_MS = 120_000;
6
7
  const DEFAULT_BUDGET_MS = 600_000;
7
8
  const GENERATED_COVERAGE_DIR = ".orangepro/coverage";
@@ -129,6 +130,10 @@ export function prepareRuntimeCoverage(root, opts = {}) {
129
130
  warnings.push("No local coverage generators found, so OrangePro did not generate runtime coverage.");
130
131
  }
131
132
  const artifacts = detectCoverageArtifacts(absRoot);
133
+ const unclassified = artifacts.filter((artifact) => artifact.ingestible && artifact.suite === "unclassified");
134
+ if (unclassified.length > 0) {
135
+ warnings.push(`${unclassified.length} ingestible coverage artifact(s) have no unit/integration provenance. Add ${COVERAGE_SUITE_MANIFEST_HELP} so OrangePro reports the suites separately.`);
136
+ }
132
137
  const suggested_commands = suggestCoverageCommands(absRoot);
133
138
  return {
134
139
  root: absRoot,
@@ -139,10 +144,12 @@ export function prepareRuntimeCoverage(root, opts = {}) {
139
144
  warnings
140
145
  };
141
146
  }
147
+ const COVERAGE_SUITE_MANIFEST_HELP = ".orangepro/coverage-suites.json with an artifacts map, for example {\"coverage/unit.coverprofile\":{\"suite\":\"unit\",\"command\":\"make unit-test-coverage\"}}";
142
148
  export function detectCoverageArtifacts(root) {
143
149
  const absRoot = path.resolve(root);
144
150
  const out = new Map();
145
- const add = (info) => {
151
+ const add = (raw) => {
152
+ const info = { ...raw, ...coverageSuiteForArtifact(absRoot, raw.path) };
146
153
  const abs = path.join(absRoot, info.path);
147
154
  if (!existsSync(abs))
148
155
  return;
@@ -18,7 +18,7 @@ import { generateTests, readDeclaredDeps, unresolvedLocalImports } from "./gener
18
18
  import { GENERATED_DIR, runHintsFor } from "./generate/runHints.js";
19
19
  import { rankRiskGaps } from "./score/risk.js";
20
20
  import { resolveProviderConfig } from "./localConfig.js";
21
- import { buildProvider } from "./generate/providers.js";
21
+ import { buildProvider, DeterministicProvider } from "./generate/providers.js";
22
22
  import { resolveContained } from "./reprove/paths.js";
23
23
  import { buildRtm } from "./rtm.js";
24
24
  import { loadLedger } from "./ledger.js";
@@ -834,10 +834,12 @@ export async function autoProve(root, opts, deps) {
834
834
  attempts: ex.attempts
835
835
  };
836
836
  }
837
- // Key gate applies ONLY to the generation lane. No provider key ⇒ generation is skipped
838
- // (no files, no fake proof) with explicit guidance; the existing-tests lane still counts.
839
- const providerConfig = resolveProviderConfig(deps.env, { provider: opts.provider, model: opts.model });
840
- if (!providerConfig) {
837
+ // Provider gate applies ONLY to the generation lane. Explicit deterministic mode is a
838
+ // valid offline provider; otherwise a real provider key/config is required.
839
+ const deterministic = opts.provider === "deterministic"
840
+ || /^(1|true|yes)$/i.test(String(deps.env.ORANGEPRO_ALLOW_DETERMINISTIC ?? ""));
841
+ const providerConfig = deterministic ? null : resolveProviderConfig(deps.env, { provider: opts.provider, model: opts.model });
842
+ if (!providerConfig && !deterministic) {
841
843
  const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "skipped-no-key";
842
844
  return {
843
845
  ran: ex.attempted > 0,
@@ -851,7 +853,7 @@ export async function autoProve(root, opts, deps) {
851
853
  attempts: ex.attempts
852
854
  };
853
855
  }
854
- const provider = buildProvider(providerConfig);
856
+ const provider = deterministic ? new DeterministicProvider() : buildProvider(providerConfig);
855
857
  const generate = deps.generate ?? generateTests;
856
858
  const reader = fileReaderFor(sourceRoot);
857
859
  // Generation gets only the budget the existing-tests lane left unspent, so existing +
package/dist/local/cli.js CHANGED
@@ -78,7 +78,7 @@ const HELP = `opro — OrangePro (local-first, BYOK, metadata-only artifacts)
78
78
 
79
79
  Usage:
80
80
  opro # one-command start: analyze, optional AI links + flows, report, RTM, agent handoff
81
- opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--prompt-version v5] [--json]
81
+ opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--proof-limit 5] [--generate-limit 20] [--prompt-version v5] [--json]
82
82
  opro roast [path] [--limit 5] [--json] # keyless: find passing tests whose targeted mutant still survives
83
83
  opro init
84
84
  opro setup # interactive: choose a default model provider + model (saved locally)
@@ -199,6 +199,8 @@ async function main() {
199
199
  aiAll: asBool(flags["ai-all"], false),
200
200
  aiFlows: !asBool(flags["no-ai-flows"], false),
201
201
  autoLimit: numericFlag(flags["auto-limit"]),
202
+ proofLimit: numericFlag(flags["proof-limit"]),
203
+ generateLimit: numericFlag(flags["generate-limit"]),
202
204
  noAuto: asBool(flags["no-auto"], false),
203
205
  promptVersion: flags["prompt-version"] === "v5" ? "v5" : undefined,
204
206
  provider: typeof flags.provider === "string" ? flags.provider : undefined,
@@ -225,6 +227,10 @@ async function main() {
225
227
  out(" Proof next: no behavior is dynamically proven yet; run from a coding agent with a model key and follow the generated-test proof handoff.");
226
228
  }
227
229
  const ap = res.auto_prove;
230
+ const gen = res.generation;
231
+ out(` Test generation: ${gen.status} — ${gen.runnable}/${gen.generated} runnable, ${Math.max(0, gen.drafts - gen.runnable)} blocked draft(s)`);
232
+ if (gen.reason)
233
+ out(` generation reason: ${gen.reason}`);
228
234
  if (ap.status === "skipped-no-key") {
229
235
  out(` Dynamic proof: skipped — ${ap.reason ?? "no provider key"}`);
230
236
  }
@@ -527,7 +533,9 @@ async function main() {
527
533
  if (res.artifacts.length) {
528
534
  out(" found:");
529
535
  for (const a of res.artifacts) {
530
- out(` ${a.path} (${a.language}, ${a.format}${a.ingestible ? ", ingestible now" : ", detected only"})`);
536
+ out(` ${a.path} (${a.language}, ${a.format}, suite=${a.suite} [${a.suite_source}]${a.ingestible ? ", ingestible now" : ", detected only"})`);
537
+ if (a.command)
538
+ out(` command: ${a.command}`);
531
539
  }
532
540
  }
533
541
  else {
@@ -13,6 +13,8 @@
13
13
  export const VALUE_FLAGS = new Set([
14
14
  "agent-pass",
15
15
  "auto-limit",
16
+ "proof-limit",
17
+ "generate-limit",
16
18
  "base",
17
19
  "client",
18
20
  "coverage-timeout-ms",
@@ -0,0 +1,30 @@
1
+ /** Classify only high-confidence generator/validator diagnostics. Ambiguous
2
+ * failures stay unknown rather than being mislabeled as a repo setup problem. */
3
+ export function classifyGeneratedDraftBlocker(reason) {
4
+ const text = reason ?? "";
5
+ if (/timed?\s*out|timeout|ETIMEDOUT|signal:\s*killed/i.test(text))
6
+ return "validation_timeout";
7
+ if (/(?:^|\b)(?:go|gofmt|python3|pytest|mvn|gradle|node|npm|vitest|jest|mocha)(?:\b[^.\n]*)?(?:not found|ENOENT|not installed)/i.test(text) ||
8
+ /(?:test runner|toolchain).*(?:missing|not found|not installed)/i.test(text))
9
+ return "toolchain_or_runner";
10
+ if (/no required module provides package|cannot find module|module not found|unresolved import|imports module-path package/i.test(text)) {
11
+ return "unresolved_import";
12
+ }
13
+ if (/syntax check failed|undefined:|unknown field|redeclared in this block|expected (?:declaration|operand|'[^']+'|"[^"]+"|[^ ]+),? found|literal not terminated|cannot assign/i.test(text))
14
+ return "generated_code";
15
+ return "unknown";
16
+ }
17
+ export function generatedDraftRemediation(reason) {
18
+ switch (classifyGeneratedDraftBlocker(reason)) {
19
+ case "generated_code":
20
+ return "Regenerate or repair the draft using symbols and APIs that exist in this package; installing dependencies will not fix this compiler error.";
21
+ case "unresolved_import":
22
+ return "Verify that the generated import path exists and is already declared by this repo; install dependencies only when the repo expects that import, then regenerate.";
23
+ case "toolchain_or_runner":
24
+ return "Install or configure the named toolchain/test runner, then re-run `opro start`.";
25
+ case "validation_timeout":
26
+ return "Warm the dependency cache or raise the relevant OrangePro validation timeout, then re-run `opro start`.";
27
+ default:
28
+ return "Review the named blocker, repair or regenerate the draft as needed, then re-run `opro start`; do not assume dependencies are missing.";
29
+ }
30
+ }
@@ -19,6 +19,7 @@ import { languageOf } from "../analyze/classify.js";
19
19
  import { buildGroundedUserPrompt, buildRawUserPrompt, buildSystemPrompt, PROMPT_VERSION } from "./prompt.js";
20
20
  import { buildBatchGenerationSystemPromptV5, buildBatchGenerationUserPromptV5, buildPlanningRepairSystemPromptV5, buildPlanningRepairUserPromptV5, buildPlanningSystemPromptV5, buildPlanningUserPromptV5, hasRepairableScenarioStructure, parseBatchGeneratedTests, parsePlannedScenariosStrict, scenarioTiesBackToRaw, PROMPT_VERSION_V5 } from "./promptV5.js";
21
21
  import { BUCKET_LABEL, deriveBucketSignals, selectLocalBuckets } from "./buckets.js";
22
+ import { generatedDraftRemediation } from "./draftGuidance.js";
22
23
  const MAX_LIMIT = 5;
23
24
  const DEFAULT_LIMIT = 3;
24
25
  const MAX_RELATED_FILES = 4;
@@ -26,7 +27,11 @@ const SYMBOL_EXCERPT_CONTEXT_LINES = 3;
26
27
  const MAX_EXCERPT_CHARS = 8000;
27
28
  const MAX_TARGET_TYPE_EXCERPT_CHARS = 5000;
28
29
  const STATIC_CHECK_TIMEOUT_MS = 3000;
29
- const GO_COMPILE_CHECK_TIMEOUT_MS = 20000;
30
+ // Large Go monorepos can spend tens of seconds populating a cold module cache
31
+ // before the target package is compiled. Keep the check authoritative instead
32
+ // of downgrading valid generated tests while dependencies are still downloading.
33
+ const GO_COMPILE_CHECK_TIMEOUT_MS = 120000;
34
+ const V5_GENERATION_MAX_TOKENS = 4000;
30
35
  /** A source ref that names a test file (used to place a generated test next to it). */
31
36
  const TEST_REF_RE = /(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)test\.[cm]?[jt]sx?$)|(_test\.[a-z]+$)|(_spec\.[a-z]+$)|((^|\/)test_[^/]+\.[a-z]+$)/i;
32
37
  function areaOf(relPath) {
@@ -518,6 +523,9 @@ export function gatherContext(graph, behavior, framework, fileReader) {
518
523
  // Subject imports are TS/JS lines — feeding them to a pytest/go target would
519
524
  // produce an unparseable test in the other language.
520
525
  const fwLower = framework.toLowerCase();
526
+ const goContext = fwLower.includes("go")
527
+ ? goGenerationImportContext(graph.workspace.root, [...relatedFiles, ...testFiles], fileReader)
528
+ : { go_import_paths: [], go_module_paths: [], go_target_package_paths: [] };
521
529
  const subjectImports = fwLower.includes("pytest") || fwLower.includes("python") || fwLower.includes("go") || fwLower.includes("junit") || fwLower.includes("java")
522
530
  ? []
523
531
  : subjectImportsFor(graph, testFiles);
@@ -561,6 +569,9 @@ export function gatherContext(graph, behavior, framework, fileReader) {
561
569
  // MISSING, never a re-derivation of a test that already exists.
562
570
  existing_tests: examples.slice(0, 10),
563
571
  subject_imports: subjectImports,
572
+ ...(goContext.go_import_paths.length ? { go_import_paths: goContext.go_import_paths } : {}),
573
+ ...(goContext.go_module_paths.length ? { go_module_paths: goContext.go_module_paths } : {}),
574
+ ...(goContext.go_target_package_paths.length ? { go_target_package_paths: goContext.go_target_package_paths } : {}),
564
575
  ...(flowChain ? { flow_chain: flowChain } : {})
565
576
  };
566
577
  // entity_ids are graph external_ids: the behavior plus related file paths. A
@@ -1578,10 +1589,6 @@ function goStaticIssue(body) {
1578
1589
  if (!/\bfunc\s+Test[A-Za-z0-9_]*\s*\(\s*t\s+\*testing\.T\s*\)/m.test(body)) {
1579
1590
  return "Go test is missing a func Test...(t *testing.T) entrypoint.";
1580
1591
  }
1581
- const external = imports.filter((spec) => spec.split("/")[0].includes("."));
1582
- if (external.length) {
1583
- return `Go test imports module-path package(s) ${external.join(", ")}; OrangePro cannot verify module imports resolve from the generated file. Prefer same-package or stdlib-only code.`;
1584
- }
1585
1592
  if (!commandAvailable("gofmt"))
1586
1593
  return "gofmt not found; cannot verify Go syntax.";
1587
1594
  const { dir, file } = writeTempStaticFile("go", body);
@@ -1611,26 +1618,58 @@ function findGoModuleRoot(startDir, workspaceRoot) {
1611
1618
  }
1612
1619
  return null;
1613
1620
  }
1614
- function goCompileIssue(body, workspaceRoot, relatedFiles) {
1621
+ function unusedImportLineNumbers(output, tempRel) {
1622
+ const escaped = tempRel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1623
+ const diagnostic = new RegExp(`(?:^|\\n)(?:\\./)?${escaped}:(\\d+):\\d+:\\s+[^\\n]*\\bimported\\b[^\\n]*\\bnot used\\b`, "g");
1624
+ return new Set([...output.matchAll(diagnostic)].map((match) => Number(match[1])).filter(Number.isFinite));
1625
+ }
1626
+ function removeLines(body, lineNumbers) {
1627
+ if (!lineNumbers.size)
1628
+ return body;
1629
+ return body
1630
+ .split(/\r?\n/)
1631
+ .filter((_line, index) => !lineNumbers.has(index + 1))
1632
+ .join("\n");
1633
+ }
1634
+ function goCompile(body, workspaceRoot, relatedFiles) {
1635
+ const externalImports = goImportSpecs(body).filter((spec) => spec.split("/")[0].includes("."));
1636
+ const unverifiedImportIssue = externalImports.length
1637
+ ? `Go test imports module-path package(s) ${externalImports.join(", ")}, but OrangePro could not run the target-package compile check to verify they resolve.`
1638
+ : null;
1615
1639
  const sourceFile = relatedFiles.find((rel) => /\.go$/i.test(rel) && !TEST_REF_RE.test(rel));
1616
1640
  if (!sourceFile)
1617
- return null;
1641
+ return { body, issue: unverifiedImportIssue };
1618
1642
  const packageDir = path.resolve(workspaceRoot, path.dirname(sourceFile));
1619
1643
  if (!existsSync(packageDir) || !findGoModuleRoot(packageDir, workspaceRoot))
1620
- return null;
1644
+ return { body, issue: unverifiedImportIssue };
1621
1645
  if (!commandAvailable("go"))
1622
- return "go not found; cannot verify generated Go test compiles.";
1646
+ return { body, issue: "go not found; cannot verify generated Go test compiles." };
1623
1647
  const tempRel = `orangepro_compile_${process.pid}_${shortHash(body)}_test.go`;
1624
1648
  const tempFile = path.join(packageDir, tempRel);
1649
+ let checkedBody = body;
1625
1650
  try {
1626
- writeFileSync(tempFile, body);
1651
+ writeFileSync(tempFile, checkedBody);
1627
1652
  execFileSync("go", ["test", "-run", "^$", "."], { cwd: packageDir, stdio: "pipe", timeout: GO_COMPILE_CHECK_TIMEOUT_MS });
1628
- return null;
1653
+ return { body: checkedBody, issue: null };
1629
1654
  }
1630
1655
  catch (e) {
1631
1656
  const err = e;
1632
1657
  const output = `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}`;
1633
- return `Go compile check failed: ${shortStaticDiag(output || err.message || "unknown error")}`;
1658
+ const withoutUnusedImports = removeLines(checkedBody, unusedImportLineNumbers(output, tempRel));
1659
+ if (withoutUnusedImports !== checkedBody) {
1660
+ checkedBody = withoutUnusedImports;
1661
+ try {
1662
+ writeFileSync(tempFile, checkedBody);
1663
+ execFileSync("go", ["test", "-run", "^$", "."], { cwd: packageDir, stdio: "pipe", timeout: GO_COMPILE_CHECK_TIMEOUT_MS });
1664
+ return { body: checkedBody, issue: null };
1665
+ }
1666
+ catch (retryError) {
1667
+ const retry = retryError;
1668
+ const retryOutput = `${retry.stdout?.toString() ?? ""}${retry.stderr?.toString() ?? ""}`;
1669
+ return { body: checkedBody, issue: `Go compile check failed: ${shortStaticDiag(retryOutput || retry.message || "unknown error")}` };
1670
+ }
1671
+ }
1672
+ return { body: checkedBody, issue: `Go compile check failed: ${shortStaticDiag(output || err.message || "unknown error")}` };
1634
1673
  }
1635
1674
  finally {
1636
1675
  rmSync(tempFile, { force: true });
@@ -1659,6 +1698,37 @@ function goImportSpecs(body) {
1659
1698
  }
1660
1699
  return specs;
1661
1700
  }
1701
+ function goGenerationImportContext(workspaceRoot, files, fileReader) {
1702
+ const goFiles = files.filter((file) => /\.go$/i.test(file));
1703
+ const moduleRoots = dedupe(goFiles
1704
+ .map((file) => findGoModuleRoot(path.resolve(workspaceRoot, path.dirname(file)), workspaceRoot))
1705
+ .filter((root) => Boolean(root)));
1706
+ const modulePathByRoot = new Map(moduleRoots.map((root) => {
1707
+ try {
1708
+ return [root, readFileSync(path.join(root, "go.mod"), "utf8").match(/^\s*module\s+(\S+)/m)?.[1] ?? ""];
1709
+ }
1710
+ catch {
1711
+ return [root, ""];
1712
+ }
1713
+ }));
1714
+ const go_module_paths = dedupe([...modulePathByRoot.values()].filter(Boolean));
1715
+ const go_target_package_paths = dedupe(goFiles
1716
+ .map((file) => {
1717
+ const sourceDir = path.resolve(workspaceRoot, path.dirname(file));
1718
+ const moduleRoot = findGoModuleRoot(sourceDir, workspaceRoot);
1719
+ const modulePath = moduleRoot ? modulePathByRoot.get(moduleRoot) : "";
1720
+ if (!moduleRoot || !modulePath)
1721
+ return "";
1722
+ const packageDir = path.relative(moduleRoot, sourceDir).split(path.sep).join("/");
1723
+ return packageDir && packageDir !== "." ? `${modulePath}/${packageDir}` : modulePath;
1724
+ })
1725
+ .filter(Boolean));
1726
+ const targetPaths = new Set(go_target_package_paths);
1727
+ const go_import_paths = dedupe(goFiles
1728
+ .flatMap((file) => goImportSpecs(fileReader(file) ?? ""))
1729
+ .filter((spec) => spec.split("/")[0].includes(".") && !targetPaths.has(spec))).slice(0, 40);
1730
+ return { go_import_paths, go_module_paths, go_target_package_paths };
1731
+ }
1662
1732
  function hasBalancedBraces(body) {
1663
1733
  let depth = 0;
1664
1734
  let stringQuote = null;
@@ -2176,8 +2246,13 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2176
2246
  body = applyPythonSrcLayoutImports(body, relatedFiles);
2177
2247
  }
2178
2248
  body = ensureFrameworkScaffold(body, framework);
2249
+ let compileIssue = null;
2250
+ if (framework.toLowerCase().includes("go")) {
2251
+ const checked = goCompile(body, graph.workspace.root, relatedFiles);
2252
+ body = checked.body;
2253
+ compileIssue = checked.issue;
2254
+ }
2179
2255
  const staticIssue = staticFormatIssue(body, framework);
2180
- const compileIssue = framework.toLowerCase().includes("go") ? goCompileIssue(body, graph.workspace.root, relatedFiles) : null;
2181
2256
  const runnable = hasAssertion(body, framework) && !staticIssue && !compileIssue;
2182
2257
  generated.push({
2183
2258
  id: `${run_id}-t${generated.length + 1}`,
@@ -2309,7 +2384,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2309
2384
  reportProgress(`Generating "${gc.ctx.behavior_title}" [v5 batch: ${selected.length}]…`);
2310
2385
  completions.push(await provider.complete({
2311
2386
  system: buildBatchGenerationSystemPromptV5(),
2312
- user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: selected })
2387
+ user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: selected }),
2388
+ maxTokens: V5_GENERATION_MAX_TOKENS
2313
2389
  }));
2314
2390
  }
2315
2391
  catch (e) {
@@ -2319,7 +2395,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2319
2395
  try {
2320
2396
  completions.push(await provider.complete({
2321
2397
  system: buildBatchGenerationSystemPromptV5(),
2322
- user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: [scenario] })
2398
+ user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: [scenario] }),
2399
+ maxTokens: V5_GENERATION_MAX_TOKENS
2323
2400
  }));
2324
2401
  }
2325
2402
  catch (singleErr) {
@@ -2433,7 +2510,12 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2433
2510
  }
2434
2511
  }
2435
2512
  const importErrors = isResolverFramework(framework) ? unresolvedLocalImports(body, genTestAbs, graph.workspace.root, declaredDeps) : [];
2436
- const compileIssue = framework.toLowerCase().includes("go") ? goCompileIssue(body, graph.workspace.root, relatedFiles) : null;
2513
+ let compileIssue = null;
2514
+ if (framework.toLowerCase().includes("go")) {
2515
+ const checked = goCompile(body, graph.workspace.root, relatedFiles);
2516
+ body = checked.body;
2517
+ compileIssue = checked.issue;
2518
+ }
2437
2519
  const runnable = isRunnable(body, framework, import_provenance, importErrors) && !compileIssue;
2438
2520
  if (!runnable) {
2439
2521
  const reason = unresolved_reason ?? compileIssue ?? runnableFailureReason(body, framework, import_provenance, importErrors, declaredDeps);
@@ -2463,7 +2545,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2463
2545
  "",
2464
2546
  // Concise blocker: first clause only — the full remedy is one line.
2465
2547
  `Blocked by: ${reason.split(" — ")[0]}`,
2466
- "Fix: install this repo's dependencies / configure the test runner, then re-run \`opro start\`."
2548
+ `Fix: ${generatedDraftRemediation(reason)}`
2467
2549
  ].join("\n"), gc.ctx.source_excerpts, "//").body;
2468
2550
  generated.push({
2469
2551
  id: `${run_id}-t${generated.length + 1}`,
@@ -2651,7 +2733,12 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2651
2733
  // from where the test will live. A test whose own import won't load is never
2652
2734
  // marked runnable.
2653
2735
  const importErrors = isResolverFramework(framework) ? unresolvedLocalImports(body, genTestAbs, graph.workspace.root, declaredDeps) : [];
2654
- const compileIssue = framework.toLowerCase().includes("go") ? goCompileIssue(body, graph.workspace.root, relatedFiles) : null;
2736
+ let compileIssue = null;
2737
+ if (framework.toLowerCase().includes("go")) {
2738
+ const checked = goCompile(body, graph.workspace.root, relatedFiles);
2739
+ body = checked.body;
2740
+ compileIssue = checked.issue;
2741
+ }
2655
2742
  const runnable = isRunnable(body, framework, import_provenance, importErrors) && !compileIssue;
2656
2743
  if (!runnable && !unresolved_reason) {
2657
2744
  unresolved_reason = compileIssue ?? runnableFailureReason(body, framework, import_provenance, importErrors, declaredDeps);
@@ -32,8 +32,10 @@ export function buildSystemPrompt() {
32
32
  "- Framework format rules:",
33
33
  " - pytest: output a valid Python file with pytest-style `def test_...` functions and `assert` statements.",
34
34
  " - Go: output a same-package `_test.go` body. Include `package <same package>`, `import \"testing\"`,",
35
- " and `func Test...(t *testing.T)`. Avoid third-party or module-path imports; use stdlib-only unless",
36
- " the evidence gives an existing same-package helper.",
35
+ " and `func Test...(t *testing.T)`. Do not import packages you do not use. Prefer stdlib and exact",
36
+ " OBSERVED GO IMPORT PATHS; never derive or invent a package subpath from a module name.",
37
+ " Do not qualify lowercase (unexported) identifiers from imported packages. Bare same-package",
38
+ " identifiers may be unexported because the generated test is rewritten into the target package.",
37
39
  " - Java/JUnit: output a complete `.java` file, not a method fragment. Include a `class <Name>Test { ... }`,",
38
40
  " the requested JUnit version's `@Test` import, and a JUnit assertion.",
39
41
  " - TS/JS: output valid framework code for the named framework and use complete imports.",
@@ -78,6 +80,21 @@ export function buildGroundedUserPrompt(ctx, bucket) {
78
80
  for (const imp of ctx.subject_imports)
79
81
  lines.push(imp);
80
82
  }
83
+ if (ctx.go_module_paths?.length) {
84
+ lines.push("GO MODULE IDENTITIES (module roots only — never guess package subpaths from these):");
85
+ for (const modulePath of ctx.go_module_paths)
86
+ lines.push(`- ${modulePath}`);
87
+ }
88
+ if (ctx.go_import_paths?.length) {
89
+ lines.push("OBSERVED GO IMPORT PATHS (exact repo-evidenced package paths; prefer these for non-stdlib imports):");
90
+ for (const importPath of ctx.go_import_paths)
91
+ lines.push(`- ${importPath}`);
92
+ }
93
+ if (ctx.go_target_package_paths?.length) {
94
+ lines.push("TARGET GO PACKAGE PATHS (do not import these; the test is rewritten into the same package):");
95
+ for (const targetPath of ctx.go_target_package_paths)
96
+ lines.push(`- ${targetPath}`);
97
+ }
81
98
  if (ctx.source_excerpts.length) {
82
99
  lines.push("SOURCE EXCERPTS:");
83
100
  lines.push("Use these for understanding only; do not copy their lines verbatim into the test body.");
@@ -97,8 +114,10 @@ export function buildGroundedUserPrompt(ctx, bucket) {
97
114
  }
98
115
  else if (fw.includes("go")) {
99
116
  lines.push("- Emit same-package Go test code only: `package ...`, `import \"testing\"`, and `func Test...(t *testing.T)`.");
100
- lines.push("- Do not import third-party or module-path packages such as `github.com/...`; prefer stdlib-only tests.");
101
- lines.push("- Do not use testify/assert/require unless an existing same-package helper in the evidence clearly requires it.");
117
+ lines.push("- Do not import packages you do not use.");
118
+ lines.push("- Prefer stdlib and exact OBSERVED GO IMPORT PATHS. Never invent a package path or derive a subpath from a GO MODULE IDENTITY.");
119
+ lines.push("- Never import a TARGET GO PACKAGE PATH; call its identifiers directly because this is a same-package test.");
120
+ lines.push("- Do not qualify lowercase (unexported) identifiers from imported packages; bare same-package identifiers may be unexported.");
102
121
  }
103
122
  else if (fw.includes("junit") || fw.includes("java")) {
104
123
  if (fw.includes("junit4")) {
@@ -33,7 +33,15 @@ export function getFrameworkRules(framework) {
33
33
  return "Python: `def test_...` with `assert`. Use `# Concern:` and `# Technique:` comments.";
34
34
  }
35
35
  if (fw.includes("go")) {
36
- return "Go: same-package `_test.go`, `func Test...(t *testing.T)`. Stdlib preferred.";
36
+ return [
37
+ "Go: same-package `_test.go` file. `func Test...(t *testing.T)`.",
38
+ "MUST start with `package <name>` matching the package under test.",
39
+ "MUST import `\"testing\"`; do not import any package you do not use.",
40
+ "Prefer stdlib and exact OBSERVED GO IMPORT PATHS. Never invent a package path or derive a subpath from a GO MODULE IDENTITY.",
41
+ "Never import a TARGET GO PACKAGE PATH; call its identifiers directly because this is a same-package test.",
42
+ "Do not qualify lowercase (unexported) identifiers from imported packages; bare same-package identifiers may be unexported.",
43
+ "Prefer stdlib `testing` over testify unless testify appears in OBSERVED GO IMPORT PATHS."
44
+ ].join(" ");
37
45
  }
38
46
  if (fw.includes("junit4") || fw.includes("java4")) {
39
47
  return "JUnit 4: `import org.junit.Test;` + `import static org.junit.Assert.*;`. Complete .java file.";
@@ -179,6 +187,21 @@ export function buildBatchGenerationUserPromptV5(ctx) {
179
187
  lines.push(`ACTORS: ${ctx.actors.join(", ")}`);
180
188
  lines.push(`FRAMEWORK: ${ctx.framework} | LAYER: ${ctx.test_layer}`);
181
189
  lines.push(`FRAMEWORK RULES: ${getFrameworkRules(ctx.framework)}`);
190
+ if (ctx.go_module_paths?.length) {
191
+ lines.push("GO MODULE IDENTITIES (module roots only — never guess package subpaths from these):");
192
+ for (const modulePath of ctx.go_module_paths)
193
+ lines.push(` ${modulePath}`);
194
+ }
195
+ if (ctx.go_import_paths?.length) {
196
+ lines.push("OBSERVED GO IMPORT PATHS (exact repo-evidenced package paths):");
197
+ for (const importPath of ctx.go_import_paths)
198
+ lines.push(` ${importPath}`);
199
+ }
200
+ if (ctx.go_target_package_paths?.length) {
201
+ lines.push("TARGET GO PACKAGE PATHS (do not import; this is a same-package test):");
202
+ for (const targetPath of ctx.go_target_package_paths)
203
+ lines.push(` ${targetPath}`);
204
+ }
182
205
  lines.push("");
183
206
  if (ctx.flow_chain?.length) {
184
207
  lines.push("FLOW CHAIN:");