@orangepro/orangepro-mcp 0.2.42 → 0.2.44

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.
@@ -13,7 +13,7 @@
13
13
  * No key ⇒ writes NO files, mints NO proof, returns explicit guidance.
14
14
  */
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
- import { basename, dirname, posix, resolve, sep } from "node:path";
16
+ import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
17
17
  import { generateTests, readDeclaredDeps, unresolvedLocalImports } from "./generate/generator.js";
18
18
  import { GENERATED_DIR, runHintsFor } from "./generate/runHints.js";
19
19
  import { rankRiskGaps } from "./score/risk.js";
@@ -21,12 +21,12 @@ import { resolveProviderConfig } from "./localConfig.js";
21
21
  import { buildProvider, DeterministicProvider } from "./generate/providers.js";
22
22
  import { resolveContained } from "./reprove/paths.js";
23
23
  import { buildRtm } from "./rtm.js";
24
- import { loadLedger } from "./ledger.js";
24
+ import { loadLedger, targetLanguage } from "./ledger.js";
25
25
  import { reportProgress } from "./util/progress.js";
26
26
  import { loadGraph, workspacePaths } from "./workspace.js";
27
27
  import { systemClock } from "./util/time.js";
28
28
  import { redactSecrets } from "./util/redact.js";
29
- import { classifyBaselineFailure, EXPERIMENTAL_SQLITE_TEST_ENV, IMPORT_TIME_CATEGORIES, isNeedsSetupCategory, readEnginesNode, targetNeedsExperimentalSqlite } from "./proofRunnability.js";
29
+ import { classifyBaselineFailure, EXPERIMENTAL_SQLITE_TEST_ENV, isNeedsSetupCategory, readEnginesNode, targetNeedsExperimentalSqlite } from "./proofRunnability.js";
30
30
  /** Real dynamic proof is profile-gated; only wired runner targets are attemptable. */
31
31
  function isTsJsFile(file) {
32
32
  return /\.[cm]?[jt]sx?$/i.test(file);
@@ -372,7 +372,74 @@ const GEN_WINDOW = 5;
372
372
  // file (every eligible symbol × every importing test) cannot starve the shared budget before
373
373
  // a provable hard-edge symbol is tried. Small K; promote to a flag if a repo needs a wider sweep.
374
374
  const EXISTING_LANE_MAX_WEAK_PER_SYMBOL = 3;
375
+ const RUNNER_ROOT_MARKERS = {
376
+ typescript: ["package.json"],
377
+ javascript: ["package.json"],
378
+ python: ["pyproject.toml", "setup.cfg", "setup.py", "pytest.ini", ".pytest.ini", "tox.ini"],
379
+ go: ["go.mod"],
380
+ java: ["pom.xml", "build.gradle", "build.gradle.kts"],
381
+ rs: ["Cargo.toml"]
382
+ };
383
+ /**
384
+ * The nearest language runner root for a target, bounded by the analyzed source root.
385
+ * A nested root is accepted only when the selected existing test is inside it too;
386
+ * otherwise proof execution falls back to the analyzed root, matching the oracle's
387
+ * containment rule rather than inventing a cross-project plan.
388
+ */
389
+ export function proofRunnerRoot(sourceRoot, targetRel, testRel) {
390
+ const stop = resolve(sourceRoot);
391
+ const targetAbs = resolve(stop, targetRel);
392
+ if (targetAbs !== stop && !targetAbs.startsWith(stop + sep))
393
+ return ".";
394
+ const language = targetLanguage(`sym:${targetRel}#target`);
395
+ const markers = RUNNER_ROOT_MARKERS[language] ?? [];
396
+ let dir = dirname(targetAbs);
397
+ let found = stop;
398
+ for (;;) {
399
+ if (markers.some((marker) => existsSync(join(dir, marker)))) {
400
+ found = dir;
401
+ break;
402
+ }
403
+ if (dir === stop)
404
+ break;
405
+ const parent = dirname(dir);
406
+ if (parent === dir || (parent !== stop && !parent.startsWith(stop + sep)))
407
+ break;
408
+ dir = parent;
409
+ }
410
+ if (testRel && found !== stop) {
411
+ const testFile = testRel.split("::", 1)[0] ?? testRel;
412
+ const testAbs = resolve(stop, testFile);
413
+ if ((testAbs !== stop && !testAbs.startsWith(stop + sep))
414
+ || (testAbs !== found && !testAbs.startsWith(found + sep)))
415
+ found = stop;
416
+ }
417
+ const rel = relative(stop, found).split(sep).join("/");
418
+ return rel || ".";
419
+ }
420
+ /** Dominant-language order from all denominator-eligible code behaviors. */
421
+ export function proofLanguageOrder(graph) {
422
+ const counts = new Map();
423
+ const first = new Map();
424
+ for (const node of graph.nodes) {
425
+ if (node.kind !== "CodeSymbol" || node.denominator_eligible !== true)
426
+ continue;
427
+ const language = targetLanguage(node.external_id);
428
+ if (!first.has(language))
429
+ first.set(language, first.size);
430
+ counts.set(language, (counts.get(language) ?? 0) + 1);
431
+ }
432
+ return [...counts.keys()].sort((a, b) => (counts.get(b) - counts.get(a)) || (first.get(a) - first.get(b)));
433
+ }
375
434
  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.";
435
+ /** Only failures proven to apply across a runner project may quarantine its siblings. */
436
+ const PROJECT_WIDE_BLOCKERS = new Set([
437
+ "engine_mismatch",
438
+ "tsconfig_missing",
439
+ "runner_missing",
440
+ "module_root_missing",
441
+ "go_package_build_failure"
442
+ ]);
376
443
  export function isRoastSurvivor(attempt) {
377
444
  return attempt.classification === "non_killing" && attempt.mutant_status === "associated_survived";
378
445
  }
@@ -434,7 +501,13 @@ function fileReaderFor(root) {
434
501
  function classifyProof(result, ctx) {
435
502
  if ("status" in result && result.status === "unrunnable") {
436
503
  // Setup did not run (env non-event) — nothing was minted, target needs setup.
437
- return { classification: "needs_setup", reason: result.reason };
504
+ const reason = result.reason;
505
+ const category = /runner binary not found|unsupported or unknown test runner/i.test(reason)
506
+ ? "runner_missing"
507
+ : /no go\.mod found|no pom\.xml|no maven or gradle|build\.gradle|no python project root/i.test(reason)
508
+ ? "module_root_missing"
509
+ : undefined;
510
+ return { classification: "needs_setup", reason, category };
438
511
  }
439
512
  const dyn = result;
440
513
  const record = dyn.record;
@@ -442,8 +515,16 @@ function classifyProof(result, ctx) {
442
515
  return { classification: "proven" };
443
516
  const cert = record.dynamic_proof;
444
517
  if (cert && cert.baseline_green === false) {
518
+ const failureSummary = dyn.oracle.baseline?.failureSummary;
519
+ if (ctx.targetFileRel.endsWith(".go") && /^#\s+\S+/m.test(failureSummary ?? "")) {
520
+ return {
521
+ classification: "needs_setup",
522
+ reason: "The Go package did not compile before the selected test could run.",
523
+ category: "go_package_build_failure"
524
+ };
525
+ }
445
526
  const { category, reason } = classifyBaselineFailure({
446
- failureSummary: dyn.oracle.baseline?.failureSummary,
527
+ failureSummary,
447
528
  enginesNode: readEnginesNode(ctx.sourceRoot, ctx.targetFileRel),
448
529
  runnerNode: ctx.runnerNode ?? process.version
449
530
  });
@@ -457,27 +538,30 @@ function mutantStatusOf(result) {
457
538
  return "unrunnable";
458
539
  return result.record.dynamic_proof?.mutant_status;
459
540
  }
460
- /**
461
- * R-1 sibling-dedup key: a baseline-red import-time failure is a deterministic property of
462
- * loading the TARGET FILE with a given runner, independent of which test runs it — so
463
- * same-file siblings share it. Keyed on the TARGET FILE only (every caller pins runner to
464
- * undefined): the sole deduped cause is engine_mismatch, a package-level fact both lanes
465
- * classify against the SAME process.version, so the runner must not be in the key. The old
466
- * {runner, target file} key split the cache across lanes (lane 1 wrote "auto <file>", the
467
- * generation lane read "<runner> <file>" -> never matched), silently disabling cross-lane
468
- * dedup. NEVER merges across different files or a different failure class.
469
- */
470
- function dedupKey(runner, targetFileRel) {
471
- return `${runner ?? "auto"}\u0000${targetFileRel}`;
541
+ function projectBlockKey(language, projectRoot) {
542
+ return `${language}\u0000${projectRoot}`;
472
543
  }
473
- /** A same-file sibling deduped WITHOUT re-running: shares the first attempt's redacted reason. */
474
- function dedupedAttempt(targetSymbol, testPath, targetFileRel, blocked) {
544
+ function projectBlockKeys(language, projectRoot, targetFileRel) {
545
+ return [
546
+ projectBlockKey(language, projectRoot),
547
+ `${projectBlockKey(language, projectRoot)}\u0000package:${dirname(targetFileRel).split(sep).join("/")}`
548
+ ];
549
+ }
550
+ function classifiedProjectBlockKey(category, language, projectRoot, targetFileRel) {
551
+ return category === "go_package_build_failure"
552
+ ? projectBlockKeys(language, projectRoot, targetFileRel)[1]
553
+ : projectBlockKey(language, projectRoot);
554
+ }
555
+ /** A same-project candidate skipped WITHOUT re-running after a classified project-wide failure. */
556
+ function dedupedAttempt(targetSymbol, testPath, projectRoot, blocked) {
475
557
  return {
476
558
  target_symbol: targetSymbol,
477
559
  test_path: testPath,
478
560
  classification: "needs_setup",
479
- reason: `${blocked.reason} (shared root cause with a sibling in ${targetFileRel}; not re-run).`,
561
+ reason: `${blocked.reason} (shared project/toolchain cause in ${blocked.scopeLabel}; not re-run).`,
480
562
  category: blocked.category,
563
+ project_root: projectRoot,
564
+ blocked_by: blocked.blockedBy,
481
565
  deduped: true
482
566
  };
483
567
  }
@@ -616,7 +700,7 @@ export function existingAssociatedTests(graph, nodeById) {
616
700
  * the budget. Order within each tier follows the Map's insertion order (graph node/edge order),
617
701
  * so the result is deterministic.
618
702
  */
619
- export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING_LANE_MAX_WEAK_PER_SYMBOL) {
703
+ export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING_LANE_MAX_WEAK_PER_SYMBOL, schedule) {
620
704
  const hard = [];
621
705
  const weak = [];
622
706
  for (const [symId, tests] of testsBySymbol) {
@@ -628,7 +712,60 @@ export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING
628
712
  weak.push({ symId, testRel: t.test, hard: false });
629
713
  }
630
714
  }
631
- return [...hard, ...weak];
715
+ if (!schedule)
716
+ return [...hard, ...weak];
717
+ const languageOrder = proofLanguageOrder(schedule.graph);
718
+ const languageRank = new Map(languageOrder.map((language, index) => [language, index]));
719
+ const decorate = (attempt, index) => {
720
+ const node = schedule.nodeById.get(attempt.symId);
721
+ const targetRel = node ? symbolFileOf(node) : attempt.symId.replace(/^sym:/, "").split("#")[0];
722
+ return {
723
+ ...attempt,
724
+ language: targetLanguage(attempt.symId),
725
+ projectRoot: proofRunnerRoot(schedule.sourceRoot, targetRel, attempt.testRel),
726
+ index
727
+ };
728
+ };
729
+ const stableProjectOrder = (attempts) => {
730
+ const decorated = attempts.map(decorate);
731
+ const firstProject = new Map();
732
+ for (const item of decorated) {
733
+ const key = `${item.language}\u0000${item.projectRoot}`;
734
+ if (!firstProject.has(key))
735
+ firstProject.set(key, firstProject.size);
736
+ }
737
+ return decorated
738
+ .sort((a, b) => ((languageRank.get(a.language) ?? Number.MAX_SAFE_INTEGER) - (languageRank.get(b.language) ?? Number.MAX_SAFE_INTEGER))
739
+ || ((firstProject.get(`${a.language}\u0000${a.projectRoot}`) ?? 0) - (firstProject.get(`${b.language}\u0000${b.projectRoot}`) ?? 0))
740
+ || (a.index - b.index))
741
+ .map(({ index: _index, ...attempt }) => attempt);
742
+ };
743
+ return [...stableProjectOrder(hard), ...stableProjectOrder(weak)];
744
+ }
745
+ /**
746
+ * Stable scheduling view over the existing ORS-ranked candidate list. This changes
747
+ * only which runner receives a scarce proof attempt first; it never changes scores,
748
+ * evidence tiers, or the persisted priority-gap order.
749
+ */
750
+ export function orderRankedProofCandidates(candidates, graph, sourceRoot) {
751
+ const languageOrder = proofLanguageOrder(graph);
752
+ if (languageOrder.length <= 1)
753
+ return candidates;
754
+ const languageRank = new Map(languageOrder.map((language, index) => [language, index]));
755
+ const firstProject = new Map();
756
+ const decorated = candidates.map((candidate, index) => {
757
+ const language = targetLanguage(candidate.id);
758
+ const projectRoot = proofRunnerRoot(sourceRoot, candidate.file);
759
+ const projectKey = `${language}\u0000${projectRoot}`;
760
+ if (!firstProject.has(projectKey))
761
+ firstProject.set(projectKey, firstProject.size);
762
+ return { candidate, index, language, projectKey };
763
+ });
764
+ return decorated
765
+ .sort((a, b) => ((languageRank.get(a.language) ?? Number.MAX_SAFE_INTEGER) - (languageRank.get(b.language) ?? Number.MAX_SAFE_INTEGER))
766
+ || ((firstProject.get(a.projectKey) ?? 0) - (firstProject.get(b.projectKey) ?? 0))
767
+ || (a.index - b.index))
768
+ .map(({ candidate }) => candidate);
632
769
  }
633
770
  /**
634
771
  * PR 1.5 lane — prove the repo's OWN existing tests, NO provider key. For each eligible
@@ -642,7 +779,7 @@ export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING
642
779
  * — the generation lane gets whatever this lane leaves unspent, so TOTAL attempts
643
780
  * (existing + generation) never exceed the budget.
644
781
  */
645
- function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, budget) {
782
+ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, projectBlocked, budget) {
646
783
  const attempts = [];
647
784
  const needsSetup = [];
648
785
  const provenSymbols = new Set();
@@ -651,8 +788,12 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
651
788
  const changed = opts.changedFiles && opts.changedFiles.length > 0 ? new Set(opts.changedFiles) : null;
652
789
  const reader = fileReaderFor(sourceRoot); // R-2: source scan for the node:sqlite env profile
653
790
  // Fix 3: hard TESTED_BY/COVERS pairs first, weak MAY_* pairs after and capped per symbol.
654
- const queue = orderExistingAttempts(existingAssociatedTests(graph, nodeById));
655
- for (const { symId, testRel, hard, testName } of queue) {
791
+ const queue = orderExistingAttempts(existingAssociatedTests(graph, nodeById), EXISTING_LANE_MAX_WEAK_PER_SYMBOL, {
792
+ graph,
793
+ nodeById,
794
+ sourceRoot
795
+ });
796
+ for (const { symId, testRel, hard, testName, language, projectRoot } of queue) {
656
797
  if (attempted >= budget)
657
798
  break;
658
799
  const node = nodeById.get(symId);
@@ -671,10 +812,10 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
671
812
  if (provenSymbols.has(symId))
672
813
  continue;
673
814
  const targetFileRel = symbolFileOf(node);
674
- // R-1 sibling dedup: a prior same-file attempt hit a package-level env root cause
675
- // (engine_mismatch: runner Node outside the declared engines range). Every sibling in this
676
- // file fails baseline identically → mark it
677
- // needs_setup WITHOUT re-running (and WITHOUT consuming the attempt budget).
815
+ const targetLanguageName = language ?? targetLanguage(symId);
816
+ const runnerRoot = projectRoot ?? proofRunnerRoot(sourceRoot, targetFileRel, testRel);
817
+ // A prior candidate in this exact runner project hit a classified project-wide
818
+ // toolchain/environment failure. Preserve the budget and explain the skip.
678
819
  const isPython = isPythonFile(targetFileRel);
679
820
  const candidateTestRels = isPython
680
821
  ? pytestNodeidsForTarget(sourceRoot, testRel, testName).filter((candidate) => isRunnableTestForTarget(node, candidate))
@@ -682,9 +823,10 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
682
823
  if (candidateTestRels.length === 0)
683
824
  continue;
684
825
  const proofTestRel = candidateTestRels[0];
685
- const blocked = importTimeBlocked.get(dedupKey(undefined, targetFileRel));
826
+ const blockedKey = projectBlockKeys(targetLanguageName, runnerRoot, targetFileRel).find((key) => projectBlocked.has(key));
827
+ const blocked = blockedKey ? projectBlocked.get(blockedKey) : undefined;
686
828
  if (blocked) {
687
- const attempt = dedupedAttempt(symId, proofTestRel, targetFileRel, blocked);
829
+ const attempt = dedupedAttempt(symId, proofTestRel, runnerRoot, blocked);
688
830
  attempts.push(attempt);
689
831
  needsSetup.push(attempt);
690
832
  continue;
@@ -745,7 +887,8 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
745
887
  target_symbol: symId,
746
888
  test_path: displayTest,
747
889
  classification: "needs_setup",
748
- reason: `Proof could not run: ${redactSecrets(errMsg(e))}`
890
+ reason: `Proof could not run: ${redactSecrets(errMsg(e))}`,
891
+ project_root: runnerRoot
749
892
  };
750
893
  attempts.push(attempt);
751
894
  needsSetup.push(attempt);
@@ -758,6 +901,7 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
758
901
  classification,
759
902
  reason,
760
903
  category,
904
+ project_root: runnerRoot,
761
905
  mutant_status: mutantStatusOf(result)
762
906
  };
763
907
  attempts.push(attempt);
@@ -768,9 +912,18 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
768
912
  }
769
913
  if (classification === "needs_setup") {
770
914
  needsSetup.push(attempt);
771
- // Cache an import-time root cause so same-file siblings dedup instead of re-running.
772
- if (category && IMPORT_TIME_CATEGORIES.has(category)) {
773
- importTimeBlocked.set(dedupKey(undefined, targetFileRel), { category, reason: reason ?? "" });
915
+ // Quarantine only a classified project-wide root cause. Target/test-specific
916
+ // red baselines and surviving mutants never suppress siblings.
917
+ if (category && PROJECT_WIDE_BLOCKERS.has(category)) {
918
+ const scopeLabel = category === "go_package_build_failure"
919
+ ? dirname(targetFileRel).split(sep).join("/") || "."
920
+ : runnerRoot;
921
+ projectBlocked.set(classifiedProjectBlockKey(category, targetLanguageName, runnerRoot, targetFileRel), {
922
+ category,
923
+ reason: reason ?? "",
924
+ blockedBy: symId,
925
+ scopeLabel
926
+ });
774
927
  }
775
928
  }
776
929
  // non_killing → keep trying this symbol's other associated tests, if any.
@@ -810,16 +963,17 @@ export async function autoProve(root, opts, deps) {
810
963
  .filter((r) => r.evidence_tier === "proven")
811
964
  .map((r) => r.code_symbol)
812
965
  .filter(Boolean));
813
- // R-1: shared sibling-dedup cache of import-time baseline failures. Spans BOTH lanes so a
814
- // node:sqlite-style root cause found once is never re-run across same-file siblings.
815
- const importTimeBlocked = new Map();
966
+ // Shared, run-local quarantine for classified project-wide toolchain failures.
967
+ // Keyed by language + bounded runner root; never populated by a test-specific red
968
+ // baseline, an unknown failure, or a surviving mutant.
969
+ const projectBlocked = new Map();
816
970
  // ONE unified dynamic-proof budget for the whole pass (existing-first → then generation).
817
971
  // Default 5 ("dynamically prove top 5"); `--auto-limit N` overrides it, clamped to
818
972
  // MAX_AUTO_LIMIT. The existing lane consumes from this budget and the generation lane gets
819
973
  // only the remainder, so TOTAL attempts (existing + generation) are ≤ budget.
820
974
  const autoLimit = Math.max(1, Math.min(MAX_AUTO_LIMIT, Math.floor(opts.autoLimit ?? DEFAULT_AUTO_LIMIT)));
821
975
  // ── Lane 1: existing associated tests — NO key required, runs FIRST (PR 1.5). ──
822
- const ex = proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, autoLimit);
976
+ const ex = proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, projectBlocked, autoLimit);
823
977
  if (opts.existingOnly) {
824
978
  const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "no-targets";
825
979
  return {
@@ -872,6 +1026,7 @@ export async function autoProve(root, opts, deps) {
872
1026
  const changed = new Set(opts.changedFiles);
873
1027
  candidates = candidates.filter((g) => changed.has(g.file));
874
1028
  }
1029
+ candidates = orderRankedProofCandidates(candidates, graph, sourceRoot);
875
1030
  const attempts = [];
876
1031
  const needsSetup = [];
877
1032
  const skipped = [];
@@ -917,14 +1072,24 @@ export async function autoProve(root, opts, deps) {
917
1072
  }
918
1073
  const { target_symbol, replacement, runner } = hint.prove_run.args;
919
1074
  const targetFileRel = symbolFile(target_symbol);
920
- // R-1 sibling dedup (cross-lane): a same-file target already hit an import-time env root
921
- // cause → a fresh generated test importing the same module fails identically. Skip it
922
- // WITHOUT generating/writing/running or consuming the attempt budget.
923
- const blocked = importTimeBlocked.get(dedupKey(undefined, targetFileRel));
1075
+ const language = targetLanguage(target_symbol);
1076
+ const projectRoot = proofRunnerRoot(sourceRoot, targetFileRel);
1077
+ // Cross-lane quarantine: a classified project-wide runner failure already
1078
+ // explains why this generated candidate cannot run. Skip without spending budget.
1079
+ const blockedKey = projectBlockKeys(language, projectRoot, targetFileRel).find((key) => projectBlocked.has(key));
1080
+ const blocked = blockedKey ? projectBlocked.get(blockedKey) : undefined;
924
1081
  if (blocked) {
925
- const attempt = dedupedAttempt(target_symbol, "", targetFileRel, blocked);
1082
+ const attempt = dedupedAttempt(target_symbol, "", projectRoot, blocked);
926
1083
  attempts.push(attempt);
927
1084
  needsSetup.push(attempt);
1085
+ skipped.push({
1086
+ target_symbol,
1087
+ title: test.title,
1088
+ reason: attempt.reason ?? "Skipped after a project-wide proof failure.",
1089
+ language,
1090
+ project_root: projectRoot,
1091
+ blocked_by: blocked.blockedBy
1092
+ });
928
1093
  continue;
929
1094
  }
930
1095
  const filename = basename(hint.prove_run.args.test_path);
@@ -984,7 +1149,8 @@ export async function autoProve(root, opts, deps) {
984
1149
  target_symbol,
985
1150
  test_path: writeRel,
986
1151
  classification: "needs_setup",
987
- reason: `Proof could not run: ${redactSecrets(errMsg(e))}`
1152
+ reason: `Proof could not run: ${redactSecrets(errMsg(e))}`,
1153
+ project_root: projectRoot
988
1154
  };
989
1155
  attempts.push(attempt);
990
1156
  needsSetup.push(attempt);
@@ -997,6 +1163,7 @@ export async function autoProve(root, opts, deps) {
997
1163
  classification,
998
1164
  reason,
999
1165
  category,
1166
+ project_root: projectRoot,
1000
1167
  mutant_status: mutantStatusOf(result)
1001
1168
  };
1002
1169
  attempts.push(attempt);
@@ -1004,9 +1171,16 @@ export async function autoProve(root, opts, deps) {
1004
1171
  proven++;
1005
1172
  else if (classification === "needs_setup") {
1006
1173
  needsSetup.push(attempt);
1007
- // Cache an import-time root cause so same-file siblings dedup instead of re-running.
1008
- if (category && IMPORT_TIME_CATEGORIES.has(category)) {
1009
- importTimeBlocked.set(dedupKey(undefined, targetFileRel), { category, reason: reason ?? "" });
1174
+ if (category && PROJECT_WIDE_BLOCKERS.has(category)) {
1175
+ const scopeLabel = category === "go_package_build_failure"
1176
+ ? dirname(targetFileRel).split(sep).join("/") || "."
1177
+ : projectRoot;
1178
+ projectBlocked.set(classifiedProjectBlockKey(category, language, projectRoot, targetFileRel), {
1179
+ category,
1180
+ reason: reason ?? "",
1181
+ blockedBy: target_symbol,
1182
+ scopeLabel
1183
+ });
1010
1184
  }
1011
1185
  }
1012
1186
  // non_killing stays in `attempts` only — an honest skip, never Proven.
@@ -9,9 +9,9 @@
9
9
  * The graph is built directly by OrangePro; it does not depend on any
10
10
  * third-party graph product or format.
11
11
  */
12
- // v2: Go method symbol ids are receiver-qualified (`sym:file.go#Recv.M`) — old
13
- // graphs hold bare-name method ids and must force-rebuild (loadGraph hard-fails).
14
- export const LOCAL_GRAPH_SCHEMA_VERSION = "orangepro.local_graph.v2";
12
+ // v3: Python method symbol ids are owner-qualified (`sym:file.py#Class.method`) —
13
+ // old graphs hold collision-prone bare method ids and must force-rebuild.
14
+ export const LOCAL_GRAPH_SCHEMA_VERSION = "orangepro.local_graph.v3";
15
15
  /** Node kinds that map to behaviors/requirements for scoring + gaps + generation. */
16
16
  export const BEHAVIOR_KINDS = new Set([
17
17
  "Requirement",
@@ -23,7 +23,7 @@ import { workspacePaths } from "./workspace.js";
23
23
  import { redactSecrets } from "./util/redact.js";
24
24
  import { targetLanguage } from "./ledger.js";
25
25
  import { readEnginesNode, satisfiesNodeRange } from "./proofRunnability.js";
26
- export const PROOF_ATTEMPTS_SCHEMA_VERSION = "orangepro.proof_attempts.v1";
26
+ export const PROOF_ATTEMPTS_SCHEMA_VERSION = "orangepro.proof_attempts.v2";
27
27
  export const PROOF_ATTEMPTS_FILE = "proof-attempts.json";
28
28
  export const PROOF_DOCTOR_SCHEMA_VERSION = "orangepro.proof_doctor.v1";
29
29
  /** Languages with a shipped dynamic-proof profile. Everything else is honest "not yet". */
@@ -115,12 +115,17 @@ export function distillProofAttempts(auto, meta) {
115
115
  category: a.category,
116
116
  reason: a.reason ? redactSecrets(a.reason) : undefined,
117
117
  deduped: a.deduped,
118
- language: targetLanguage(a.target_symbol)
118
+ language: targetLanguage(a.target_symbol),
119
+ project_root: a.project_root,
120
+ blocked_by: a.blocked_by
119
121
  })),
120
122
  skipped: auto.skipped.map((s) => ({
121
123
  target_symbol: s.target_symbol,
122
124
  title: s.title,
123
- reason: redactSecrets(s.reason)
125
+ reason: redactSecrets(s.reason),
126
+ language: s.language,
127
+ project_root: s.project_root,
128
+ blocked_by: s.blocked_by
124
129
  }))
125
130
  };
126
131
  }
@@ -3,7 +3,7 @@ import { ORS_VERSION } from "./score/risk.js";
3
3
  import { ORANGEPRO_VERSION } from "./version.js";
4
4
  import { hashString } from "./util/hash.js";
5
5
  export const ARTIFACT_IDENTITY_VERSION = "orangepro.artifact_identity.v1";
6
- export const ANALYZER_VERSION = "orangepro.analyzer.v1";
6
+ export const ANALYZER_VERSION = "orangepro.analyzer.v3";
7
7
  export const PROOF_ORACLE_VERSION = "orangepro.targeted_mutation_oracle.v1";
8
8
  function stable(value) {
9
9
  if (value === null || typeof value !== "object")
@@ -446,6 +446,7 @@ export function rankRiskGaps(graph, opts = {}) {
446
446
  // worklist but do not rescale an unchanged peer.
447
447
  const normalizationSymbols = graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true && !n.stale && rankEligible(n) && !isSuppressed(n.external_id));
448
448
  const symbols = normalizationSymbols.filter((n) => !confirmed.has(n.external_id));
449
+ const worklistSymbols = symbols.filter((n) => n.properties.ranking_exclusion_reason !== "python_structural_container");
449
450
  const symbolIds = new Set(normalizationSymbols.map((s) => s.external_id));
450
451
  const symbolsByFile = new Map();
451
452
  for (const s of normalizationSymbols) {
@@ -603,7 +604,7 @@ export function rankRiskGaps(graph, opts = {}) {
603
604
  for (const s of syms)
604
605
  legacyIncoming.set(s.external_id, (legacyIncoming.get(s.external_id) ?? 0) + share);
605
606
  }
606
- return symbols
607
+ return worklistSymbols
607
608
  .map((s) => {
608
609
  const file = symbolFile(s);
609
610
  const incoming_refs = legacyIncoming.get(s.external_id) ?? 0;
@@ -649,7 +650,7 @@ export function rankRiskGaps(graph, opts = {}) {
649
650
  const rawById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, rawScores[idx]]));
650
651
  const pById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, pScores[idx]]));
651
652
  const iById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, iScores[idx]]));
652
- const ranked = symbols
653
+ const ranked = worklistSymbols
653
654
  .map((s) => {
654
655
  const file = symbolFile(s);
655
656
  const incoming_refs = Math.round((incoming.get(s.external_id) ?? 0) * 10) / 10;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangepro/orangepro-mcp",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "private": false,
5
5
  "description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
6
6
  "license": "MIT",
@@ -101,7 +101,7 @@
101
101
  "smoke:generate-prove-v5": "npm run build && node scripts/smoke-generate-prove-v5.mjs",
102
102
  "smoke:db-sqljs": "npm run build && node scripts/smoke-db-sqljs-prove.mjs",
103
103
  "release": "node scripts/release.mjs",
104
- "prepublishOnly": "npm run build && npm test"
104
+ "prepublishOnly": "npm run typecheck && npm run build && npm test"
105
105
  },
106
106
  "engines": {
107
107
  "node": ">=20"