@orangepro/orangepro-mcp 0.2.41 → 0.2.42

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.
@@ -10,6 +10,7 @@ import { buildImportGraph } from "../resolve/importGraph.js";
10
10
  import { walkBarrel } from "../resolve/barrelWalker.js";
11
11
  import { resetResolverCaches } from "../resolve/resolver.js";
12
12
  import { resetExportIndexCache } from "../resolve/exportIndex.js";
13
+ import { builtInRankExclusion } from "../score/rankEligibility.js";
13
14
  import { baseName, extOf, GENERATED_CODE_REASON, isGeneratedCode, isNonProductPath, NON_PRODUCT_REASON, isTestSupportPath, languageOf, manifestKindOf, roleOf } from "./classify.js";
14
15
  import { classifyTestLayer } from "./testLayer.js";
15
16
  import { detectFromPackageJson, detectFrameworksFromManifest, detectManifestPackage, frameworkFromConfig } from "./frameworks.js";
@@ -142,6 +143,59 @@ function behaviorSurfaceReason(relPath, name, memberOf) {
142
143
  return null;
143
144
  }
144
145
  const RUNTIME_SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/i;
146
+ /**
147
+ * Return qualified `Class.run` symbols whose class extends a CLI command base
148
+ * imported from a known runtime package. This is deliberately import-scoped:
149
+ * a local class merely named Command/SfCommand never becomes an entry point.
150
+ */
151
+ function cliEntrySymbols(content) {
152
+ const out = new Map();
153
+ const sourceFile = ts.createSourceFile("cli-entry.tsx", content, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
154
+ const importedBases = new Map();
155
+ for (const stmt of sourceFile.statements) {
156
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier) || !stmt.importClause || stmt.importClause.isTypeOnly)
157
+ continue;
158
+ const source = stmt.moduleSpecifier.text;
159
+ const framework = source === "@oclif/core" ? "oclif" :
160
+ source === "@salesforce/sf-plugins-core" ? "salesforce-cli" :
161
+ source === "commander" ? "commander" :
162
+ undefined;
163
+ if (!framework)
164
+ continue;
165
+ const bindings = stmt.importClause.namedBindings;
166
+ if (!bindings || !ts.isNamedImports(bindings))
167
+ continue;
168
+ for (const element of bindings.elements) {
169
+ if (element.isTypeOnly)
170
+ continue;
171
+ const imported = element.propertyName?.text ?? element.name.text;
172
+ if (imported === "Command" || imported === "SfCommand")
173
+ importedBases.set(element.name.text, framework);
174
+ }
175
+ }
176
+ for (const stmt of sourceFile.statements) {
177
+ if (!ts.isClassDeclaration(stmt) || !stmt.name)
178
+ continue;
179
+ let framework;
180
+ for (const clause of stmt.heritageClauses ?? []) {
181
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
182
+ continue;
183
+ for (const type of clause.types) {
184
+ if (ts.isIdentifier(type.expression))
185
+ framework = importedBases.get(type.expression.text) ?? framework;
186
+ }
187
+ }
188
+ if (!framework)
189
+ continue;
190
+ const hasRun = stmt.members.some((member) => ts.isMethodDeclaration(member) &&
191
+ !!member.body &&
192
+ ts.isIdentifier(member.name) &&
193
+ member.name.text === "run");
194
+ if (hasRun)
195
+ out.set(`${stmt.name.text}.run`, framework);
196
+ }
197
+ return out;
198
+ }
145
199
  /**
146
200
  * Resolve package-level public entry modules from package metadata and the two
147
201
  * conventional source entry paths. This gives libraries a semantic behavior
@@ -487,6 +541,20 @@ export function analyzeRepo(root, opts = {}) {
487
541
  if (set.size < 32)
488
542
  set.add(name);
489
543
  };
544
+ // Stronger than an unanchored receiver name: the qualifier is a runtime
545
+ // binding imported from a bare external module. Preserve that provenance so
546
+ // risk scoring can recognize replacement operations without treating every
547
+ // local `replace*` method as destructive.
548
+ const importedStaticCalleesByCaller = new Map();
549
+ const recordImportedStaticCallee = (callerId, name) => {
550
+ let set = importedStaticCalleesByCaller.get(callerId);
551
+ if (!set) {
552
+ set = new Set();
553
+ importedStaticCalleesByCaller.set(callerId, set);
554
+ }
555
+ if (set.size < 32)
556
+ set.add(name);
557
+ };
490
558
  // Raw (caller, callee) call pairs per TS/JS code file, resolved after the
491
559
  // import graph is built (cross-file calls need its bindings + targets).
492
560
  const rawCallsByFile = new Map();
@@ -830,6 +898,7 @@ export function analyzeRepo(root, opts = {}) {
830
898
  const extraction = opts.parseCache
831
899
  ? opts.parseCache.symbols(file.hash, `${language}#${extractionBackend(language)}`, () => extractFileSymbols(content, language))
832
900
  : extractFileSymbols(content, language);
901
+ const cliEntries = language === "typescript" || language === "javascript" ? cliEntrySymbols(content) : new Map();
833
902
  if (extraction.truncated)
834
903
  symbolFilesTruncated++;
835
904
  for (const sym of extraction.symbols) {
@@ -860,15 +929,18 @@ export function analyzeRepo(root, opts = {}) {
860
929
  const surfaceExclusionReason = callableBehaviorCandidate
861
930
  ? behaviorSurfaceExclusionReason(file.relPath, sym.name, sym.member_of)
862
931
  : null;
932
+ const cliFramework = cliEntries.get(sym.name);
933
+ const cliDenominatorOverride = Boolean(cliFramework) && callableBehaviorCandidate;
863
934
  const surfaceReason = nonTsClassBehavior
864
935
  ? "Non-TS class/constructor behavior surface — countable for language proof."
865
936
  : callableBehaviorCandidate && !surfaceExclusionReason
866
937
  ? behaviorSurfaceReason(file.relPath, sym.name, sym.member_of)
867
938
  : null;
868
939
  const publicApiEntry = callableBehaviorCandidate && !surfaceExclusionReason && publicEntryFiles.has(file.relPath);
869
- const eligible = callableBehaviorCandidate && (surfaceReason !== null || publicApiEntry);
870
- const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null;
940
+ const eligible = cliDenominatorOverride || (callableBehaviorCandidate && (surfaceReason !== null || publicApiEntry));
941
+ const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null && !cliDenominatorOverride;
871
942
  const notEntryPointAdjacent = callableBehaviorCandidate && !eligible && !behaviorSurfaceExcluded;
943
+ const rankingExclusion = builtInRankExclusion(file.relPath);
872
944
  if (isBoilerplate)
873
945
  excludedBoilerplate++;
874
946
  nodes.push(makeNode({
@@ -881,10 +953,27 @@ export function analyzeRepo(root, opts = {}) {
881
953
  ...(sym.start_line ? { start_line: sym.start_line } : {}),
882
954
  ...(sym.end_line ? { end_line: sym.end_line } : {}),
883
955
  ...(sym.member_of ? { member_of: sym.member_of } : {}),
956
+ ...(cliFramework ? { cli_entrypoint: true, entry_lane: "cli", cli_framework: cliFramework } : {}),
884
957
  ...(sym.callable !== undefined ? { callable_const: sym.callable } : {}),
885
- ...(surfaceReason ? { behavior_surface: "entrypoint_adjacent" } : publicApiEntry ? { behavior_surface: "public_api_entry" } : {}),
958
+ ...(cliDenominatorOverride
959
+ ? {
960
+ behavior_surface: "cli_command_entry",
961
+ denominator_reason_code: "cli_command_entry",
962
+ ...(surfaceExclusionReason ? { denominator_override_from: surfaceExclusionReason } : {})
963
+ }
964
+ : surfaceReason
965
+ ? { behavior_surface: "entrypoint_adjacent" }
966
+ : publicApiEntry
967
+ ? { behavior_surface: "public_api_entry" }
968
+ : {}),
886
969
  ...(behaviorSurfaceExcluded ? { denominator_reason_code: "infra_behavior_surface" } : {}),
887
- ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {})
970
+ ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {}),
971
+ ...(rankingExclusion
972
+ ? {
973
+ ranking_exclusion_reason_code: rankingExclusion.code,
974
+ ranking_exclusion_reason: rankingExclusion.reason
975
+ }
976
+ : {})
888
977
  },
889
978
  evidence_strength: "hard",
890
979
  review_status: "auto_detected",
@@ -900,17 +989,19 @@ export function analyzeRepo(root, opts = {}) {
900
989
  ? GENERATED_CODE_REASON
901
990
  : isBoilerplate
902
991
  ? BOILERPLATE_REASON
903
- : eligible && surfaceReason
904
- ? surfaceReason
905
- : publicApiEntry
906
- ? "Callable export from a package public entry module — countable library behavior surface."
907
- : behaviorSurfaceExcluded && surfaceExclusionReason
908
- ? surfaceExclusionReason
909
- : callableBehaviorCandidate
910
- ? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
911
- : sym.symbol_kind === "class"
912
- ? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
913
- : "Exported const (not provably callable) — excluded from the denominator in v1."
992
+ : cliDenominatorOverride
993
+ ? `Verified ${cliFramework} CLI command run() entry — countable behavior surface${surfaceExclusionReason ? `; overrides: ${surfaceExclusionReason}` : "."}`
994
+ : eligible && surfaceReason
995
+ ? surfaceReason
996
+ : publicApiEntry
997
+ ? "Callable export from a package public entry module — countable library behavior surface."
998
+ : behaviorSurfaceExcluded && surfaceExclusionReason
999
+ ? surfaceExclusionReason
1000
+ : callableBehaviorCandidate
1001
+ ? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
1002
+ : sym.symbol_kind === "class"
1003
+ ? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
1004
+ : "Exported const (not provably callable) — excluded from the denominator in v1."
914
1005
  }));
915
1006
  edges.push(makeEdge({
916
1007
  from_external_id: symId,
@@ -1075,10 +1166,18 @@ export function analyzeRepo(root, opts = {}) {
1075
1166
  const importBindingsByFile = new Map();
1076
1167
  const typeBindingsByFile = new Map();
1077
1168
  const namespaceBindingsByFile = new Map();
1169
+ const externalRuntimeBindingsByFile = new Map();
1078
1170
  for (const e of importGraph.edges) {
1171
+ const fromRel = relByAbs.get(path.resolve(e.from));
1172
+ if (fromRel && e.importKind === "runtime" && e.category === "bare-external") {
1173
+ let bindings = externalRuntimeBindingsByFile.get(fromRel);
1174
+ if (!bindings)
1175
+ externalRuntimeBindingsByFile.set(fromRel, (bindings = new Set()));
1176
+ for (const binding of e.bindings)
1177
+ bindings.add(binding.local);
1178
+ }
1079
1179
  if (!e.resolved || e.external || !e.target)
1080
1180
  continue;
1081
- const fromRel = relByAbs.get(path.resolve(e.from));
1082
1181
  const targetRel = relByAbs.get(path.resolve(e.target));
1083
1182
  // Skip targets outside the scanned set (ignored/generated files): no node to link.
1084
1183
  if (!fromRel || !targetRel || fromRel === targetRel)
@@ -1264,6 +1363,7 @@ export function analyzeRepo(root, opts = {}) {
1264
1363
  const imports = importBindingsByFile.get(rel);
1265
1364
  const typeImports = typeBindingsByFile.get(rel);
1266
1365
  const namespaces = namespaceBindingsByFile.get(rel);
1366
+ const externalRuntimeBindings = externalRuntimeBindingsByFile.get(rel);
1267
1367
  for (const c of calls) {
1268
1368
  if (!localSyms.has(c.caller))
1269
1369
  continue; // caller must be an emitted symbol
@@ -1323,6 +1423,9 @@ export function analyzeRepo(root, opts = {}) {
1323
1423
  queueMay(callerId, cands[0], rel, 0.65, `Imported-binding member match: the qualifier is imported from the module defining the only *.method symbol.`);
1324
1424
  }
1325
1425
  }
1426
+ if (externalRuntimeBindings?.has(c.qualifier)) {
1427
+ recordImportedStaticCallee(callerId, `${c.qualifier}.${c.callee}`);
1428
+ }
1326
1429
  // A non-imported (local/param) qualifier is NOT anchored — no edge.
1327
1430
  if (!ns && !qImport)
1328
1431
  recordExternalCallee(callerId, `${c.qualifier}.${c.callee}`); // round two: retain by name
@@ -1961,16 +2064,6 @@ export function analyzeRepo(root, opts = {}) {
1961
2064
  }
1962
2065
  }
1963
2066
  }
1964
- // Fix B: persist retained callee names on their caller symbols (names only).
1965
- if (externalCalleesByCaller.size > 0) {
1966
- const byId = new Map(nodes.map((n) => [n.external_id, n]));
1967
- for (const [callerId, names] of externalCalleesByCaller) {
1968
- const n = byId.get(callerId);
1969
- if (!n)
1970
- continue;
1971
- n.properties = { ...(n.properties ?? {}), external_callees: [...names].sort() };
1972
- }
1973
- }
1974
2067
  const seenGoProof = new Set();
1975
2068
  const proofVerifiedAt = scanStartMs;
1976
2069
  for (const [testRel, structure] of goTestStructureByFile) {
@@ -2075,6 +2168,25 @@ export function analyzeRepo(root, opts = {}) {
2075
2168
  }
2076
2169
  }
2077
2170
  }
2171
+ // Persist retained callees for every language mix. This must live outside the
2172
+ // non-TS conditional: a TypeScript-only repository still needs its unresolved
2173
+ // external and import-anchored static calls available to risk scoring.
2174
+ if (externalCalleesByCaller.size > 0 || importedStaticCalleesByCaller.size > 0) {
2175
+ const byId = new Map(nodes.map((n) => [n.external_id, n]));
2176
+ const callerIds = new Set([...externalCalleesByCaller.keys(), ...importedStaticCalleesByCaller.keys()]);
2177
+ for (const callerId of callerIds) {
2178
+ const n = byId.get(callerId);
2179
+ if (!n)
2180
+ continue;
2181
+ const external = externalCalleesByCaller.get(callerId);
2182
+ const importedStatic = importedStaticCalleesByCaller.get(callerId);
2183
+ n.properties = {
2184
+ ...(n.properties ?? {}),
2185
+ ...(external?.size ? { external_callees: [...external].sort() } : {}),
2186
+ ...(importedStatic?.size ? { imported_static_callees: [...importedStatic].sort() } : {})
2187
+ };
2188
+ }
2189
+ }
2078
2190
  // ---- Static assertion candidates (Phase 4 / Gate 2): TypeChecker-derived hard edges ----
2079
2191
  // Upgrade resolver-derived test->source candidates to HARD TESTED_BY/COVERS
2080
2192
  // ONLY where the 5-conjunct confirmer proves a runtime, asserted use of a real
@@ -2157,7 +2269,7 @@ export function analyzeRepo(root, opts = {}) {
2157
2269
  involved.add(c.implAbs);
2158
2270
  }
2159
2271
  if (candidates.length === 0) {
2160
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2272
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2161
2273
  }
2162
2274
  else
2163
2275
  try {
@@ -2174,7 +2286,7 @@ export function analyzeRepo(root, opts = {}) {
2174
2286
  })
2175
2287
  : { candidates, riskSymbols: 0, involvedFiles: involved.size };
2176
2288
  if (involved.size > confirmBudget && scoped.candidates.length === 0) {
2177
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: involved.size };
2289
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: involved.size };
2178
2290
  warnings.push(`Static confirmation skipped: ${involved.size} files exceed the confirmer budget (${confirmBudget}), and no high-risk subset fit. Use \`--base <ref>\` for PR-scoped confirmation, or raise ORANGEPRO_MAX_CONFIRM_FILES.`);
2179
2291
  }
2180
2292
  else {
@@ -2195,8 +2307,28 @@ export function analyzeRepo(root, opts = {}) {
2195
2307
  lastVerified: proofVerifiedAt
2196
2308
  }));
2197
2309
  }
2310
+ let associatedPairs = 0;
2311
+ for (const c of result.associations) {
2312
+ const edgeKey = `test:${c.testRel}|${c.symId}`;
2313
+ if (seenEdge.has(edgeKey))
2314
+ continue;
2315
+ seenEdge.add(edgeKey);
2316
+ associatedPairs++;
2317
+ edges.push(...makeProofEdges({
2318
+ testRel: c.testRel,
2319
+ symId: c.symId,
2320
+ provenance: prov(c.testRel, hashString(`${c.symId}:runtime-invocation`)),
2321
+ lastVerified: proofVerifiedAt,
2322
+ properties: {
2323
+ static_association: "runtime_invocation",
2324
+ assertion_observed: false,
2325
+ association_reason: c.reason
2326
+ }
2327
+ }));
2328
+ }
2198
2329
  confirmedCoverage = {
2199
2330
  confirmed_pairs: confirmedPairs,
2331
+ associated_pairs: associatedPairs,
2200
2332
  attempted: result.attempted,
2201
2333
  capped_downgrades: result.capped_downgrades,
2202
2334
  skipped_files_budget: involved.size > confirmBudget ? involved.size : 0,
@@ -2222,7 +2354,7 @@ export function analyzeRepo(root, opts = {}) {
2222
2354
  // or a TS compiler-API edge must NEVER crash analyze. Degrade to
2223
2355
  // candidate-only coverage with a disclosure.
2224
2356
  const msg = err instanceof Error ? err.message : String(err);
2225
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2357
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2226
2358
  warnings.push(`Static confirmation skipped: the structural confirmer could not run on this project (${msg.slice(0, 160)}). Coverage is reported from candidate links only.`);
2227
2359
  }
2228
2360
  }
@@ -2230,6 +2362,7 @@ export function analyzeRepo(root, opts = {}) {
2230
2362
  if (nonTsHardProofAttempted > 0) {
2231
2363
  confirmedCoverage = {
2232
2364
  confirmed_pairs: (confirmedCoverage?.confirmed_pairs ?? 0) + goProofConfirmedPairs + javaProofConfirmedPairs + pythonProofConfirmedPairs,
2365
+ associated_pairs: confirmedCoverage?.associated_pairs ?? 0,
2233
2366
  attempted: (confirmedCoverage?.attempted ?? 0) + nonTsHardProofAttempted,
2234
2367
  capped_downgrades: confirmedCoverage?.capped_downgrades ?? 0,
2235
2368
  skipped_files_budget: confirmedCoverage?.skipped_files_budget ?? 0
@@ -2412,6 +2545,7 @@ export function analyzeRepo(root, opts = {}) {
2412
2545
  files_scanned: filesProcessed,
2413
2546
  files_cap_hit: filesCapHit,
2414
2547
  max_files: maxFiles,
2548
+ max_symbols: maxSymbols,
2415
2549
  ...(budgetStopped
2416
2550
  ? { not_analyzed_due_to_budget: { files_not_analyzed: filesNotAnalyzed, elapsed_ms: elapsedMs, budget_ms: budgetMs } }
2417
2551
  : {}),
@@ -75,10 +75,14 @@ export function languageOf(relPath) {
75
75
  export function isTestFile(relPath) {
76
76
  const p = relPath.toLowerCase();
77
77
  const file = relPath.split("/").pop() || relPath;
78
+ // CLI frameworks commonly use `src/commands/<topic>/test/` as a PRODUCT
79
+ // command namespace (for example `agent test run`), not as a test suite.
80
+ // Explicit test filenames and real test roots remain authoritative.
81
+ const cliTestCommandNamespace = /(^|\/)src\/commands\/(?:[^/]+\/)*test\//.test(p);
78
82
  return (/\.(test|spec)\.[a-z]+$/.test(p) ||
79
83
  /(^|\/)test\.[cm]?[jt]sx?$/.test(p) ||
80
84
  /(^|\/)__tests__\//.test(p) ||
81
- /(^|\/)(tests?|e2e|cypress|spec)\//.test(p) ||
85
+ (!cliTestCommandNamespace && /(^|\/)(tests?|e2e|cypress|spec)\//.test(p)) ||
82
86
  /(^|\/)(unit|integration|functional|acceptance)tests?\//.test(p) ||
83
87
  /_test\.(py|go)$/.test(p) ||
84
88
  /test_.*\.py$/.test(p) ||
@@ -572,6 +572,21 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
572
572
  return out;
573
573
  }
574
574
  const implNorm = norm(implAbsPath);
575
+ const implSf = program.getSourceFile(implNorm);
576
+ const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
577
+ const cliCommandClasses = new Set();
578
+ let defaultCliCommandClass = null;
579
+ for (const statement of implSf?.statements ?? []) {
580
+ if (!ts.isClassDeclaration(statement) || !statement.name)
581
+ continue;
582
+ const extendsCliCommand = statement.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword &&
583
+ clause.types.some((t) => /(?:^|\.)(?:Sf)?Command$/.test(t.expression.getText(implSf)))) ?? false;
584
+ if (!extendsCliCommand || !memberTargetsByClass.has(statement.name.text))
585
+ continue;
586
+ cliCommandClasses.add(statement.name.text);
587
+ if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword))
588
+ defaultCliCommandClass = statement.name.text;
589
+ }
575
590
  // Resolve an identifier's binding to a terminal export name in implAbsPath, or
576
591
  // null. Follows aliases (renames, re-export barrels) to the defining file.
577
592
  const resolvedTargetName = (id) => {
@@ -918,6 +933,7 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
918
933
  const constructorBindings = new Map();
919
934
  const prototypeBindings = new Map();
920
935
  const mutatorAliases = new Map();
936
+ const loadedModuleBindings = new Map();
921
937
  const unwrapExpression = (expr) => {
922
938
  let current = expr;
923
939
  while (ts.isParenthesizedExpression(current) ||
@@ -930,6 +946,38 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
930
946
  }
931
947
  return current;
932
948
  };
949
+ const loadedModuleFromEsmock = (expr) => {
950
+ const value = unwrapExpression(expr);
951
+ if (!ts.isCallExpression(value) ||
952
+ !ts.isIdentifier(value.expression) ||
953
+ !importedFromPkg(value.expression, ["esmock"]) ||
954
+ value.arguments.length === 0) {
955
+ return null;
956
+ }
957
+ const spec = unwrapExpression(value.arguments[0]);
958
+ if (!ts.isStringLiteralLike(spec))
959
+ return null;
960
+ const resolved = resolveImport(spec.text, testAbsPath);
961
+ if (!resolved.resolvedFileName || norm(resolved.resolvedFileName) !== implNorm)
962
+ return null;
963
+ return {
964
+ src: { file: implNorm, importedName: "default", typeOnly: false },
965
+ defaultClassName: defaultCliCommandClass
966
+ };
967
+ };
968
+ const loadedDefaultClassBinding = (expr) => {
969
+ const value = unwrapExpression(expr);
970
+ if (!ts.isPropertyAccessExpression(value) || value.name.text !== "default")
971
+ return null;
972
+ const receiver = unwrapExpression(value.expression);
973
+ if (!ts.isIdentifier(receiver))
974
+ return null;
975
+ const sym = checker.getSymbolAtLocation(receiver);
976
+ const loaded = sym ? loadedModuleBindings.get(sym) : undefined;
977
+ if (!loaded?.defaultClassName)
978
+ return null;
979
+ return { className: loaded.defaultClassName, src: loaded.src, fromNest: false };
980
+ };
933
981
  const numericIndex = (expr) => {
934
982
  const index = unwrapExpression(expr);
935
983
  if (ts.isNumericLiteral(index))
@@ -1159,7 +1207,13 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
1159
1207
  if (ts.isIdentifier(receiver)) {
1160
1208
  const sym = checker.getSymbolAtLocation(receiver);
1161
1209
  const binding = sym ? instanceBindings.get(sym) : undefined;
1162
- return binding ?? null;
1210
+ if (binding)
1211
+ return binding;
1212
+ const directClass = classNameFromToken(receiver);
1213
+ if (directClass && cliCommandClasses.has(directClass)) {
1214
+ return { className: directClass, src: importSourceOf(receiver), fromNest: false };
1215
+ }
1216
+ return null;
1163
1217
  }
1164
1218
  if (ts.isConditionalExpression(receiver)) {
1165
1219
  return sameClassBinding([bindingFromNestInstanceExpr(receiver.whenTrue), bindingFromNestInstanceExpr(receiver.whenFalse)]);
@@ -1824,6 +1878,9 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
1824
1878
  };
1825
1879
  const classBindingFromExpr = (expr, requireNest) => {
1826
1880
  const value = unwrapExpression(expr);
1881
+ const loadedDefault = loadedDefaultClassBinding(value);
1882
+ if (loadedDefault)
1883
+ return loadedDefault;
1827
1884
  if (ts.isIdentifier(value)) {
1828
1885
  const sym = checker.getSymbolAtLocation(value);
1829
1886
  const binding = sym ? instanceBindings.get(sym) : undefined;
@@ -1856,6 +1913,22 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
1856
1913
  if (sym)
1857
1914
  instanceBindings.set(sym, binding);
1858
1915
  };
1916
+ const maybeBindLoadedModule = (name, expr) => {
1917
+ const loaded = loadedModuleFromEsmock(expr);
1918
+ if (!loaded)
1919
+ return;
1920
+ const sym = checker.getSymbolAtLocation(name);
1921
+ if (sym)
1922
+ loadedModuleBindings.set(sym, loaded);
1923
+ if (!loaded.defaultClassName)
1924
+ return;
1925
+ const targetName = `${loaded.defaultClassName}.run`;
1926
+ if (!nameSet.has(targetName))
1927
+ return;
1928
+ const st = stateFor(targetName);
1929
+ st.importedRuntime = true;
1930
+ st.importSources.push(loaded.src);
1931
+ };
1859
1932
  const maybeBindDerivedIdentity = (name, expr) => {
1860
1933
  bindMutatorAlias(name, expr);
1861
1934
  const sym = checker.getSymbolAtLocation(name);
@@ -2065,6 +2138,7 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
2065
2138
  markTupleMutationSeed(node.initializer);
2066
2139
  if (ts.isIdentifier(node.name)) {
2067
2140
  recordContainerBindings(node.name, node.initializer);
2141
+ maybeBindLoadedModule(node.name, node.initializer);
2068
2142
  maybeBindInstance(node.name, node.initializer);
2069
2143
  maybeBindDerivedIdentity(node.name, node.initializer);
2070
2144
  }
@@ -2395,7 +2469,10 @@ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
2395
2469
  out.set(name, {
2396
2470
  verdict: "inferred",
2397
2471
  reason: "the behavior is exercised but no assertion consumes its result or observes its rendered effect via the real binding",
2398
- rejected_conjunct: 5
2472
+ rejected_conjunct: 5,
2473
+ ...(st.runtimeUses.some((u) => u.src != null && cleanBindingSource(u.src) && !srcMocked(u.src))
2474
+ ? { association_signal: "runtime_invocation" }
2475
+ : {})
2399
2476
  });
2400
2477
  }
2401
2478
  return out;
@@ -2546,10 +2623,11 @@ export function resolveSpecifier(spec, fromFile, repoRoot, wsPkgs) {
2546
2623
  export function runConfirmer(input) {
2547
2624
  const { candidates, symbolsByImpl, existingSymIds, anchorFile } = input;
2548
2625
  const confirmations = [];
2626
+ const associations = [];
2549
2627
  let attempted = 0;
2550
2628
  let capped_downgrades = 0;
2551
2629
  if (candidates.length === 0)
2552
- return { confirmations, attempted, capped_downgrades };
2630
+ return { confirmations, associations, attempted, capped_downgrades };
2553
2631
  const absFiles = new Set();
2554
2632
  for (const c of candidates) {
2555
2633
  absFiles.add(c.testAbs);
@@ -2564,7 +2642,9 @@ export function runConfirmer(input) {
2564
2642
  const verdicts = confirmBehaviors(ctx, c.testAbs, c.implAbs, names);
2565
2643
  for (const [name, v] of verdicts) {
2566
2644
  attempted++;
2567
- if (v.verdict !== "confirmed")
2645
+ const isConfirmed = v.verdict === "confirmed";
2646
+ const isAssociated = v.verdict === "inferred" && v.association_signal === "runtime_invocation";
2647
+ if (!isConfirmed && !isAssociated)
2568
2648
  continue;
2569
2649
  const symId = `sym:${c.implRel}#${name}`;
2570
2650
  if (!existingSymIds.has(symId)) {
@@ -2575,8 +2655,12 @@ export function runConfirmer(input) {
2575
2655
  if (seen.has(key))
2576
2656
  continue;
2577
2657
  seen.add(key);
2578
- confirmations.push({ testRel: c.testRel, implRel: c.implRel, behaviorName: name, symId, reason: v.reason });
2658
+ const edge = { testRel: c.testRel, implRel: c.implRel, behaviorName: name, symId, reason: v.reason };
2659
+ if (isConfirmed)
2660
+ confirmations.push(edge);
2661
+ else
2662
+ associations.push(edge);
2579
2663
  }
2580
2664
  }
2581
- return { confirmations, attempted, capped_downgrades };
2665
+ return { confirmations, associations, attempted, capped_downgrades };
2582
2666
  }
@@ -30,6 +30,9 @@ const RUNTIME_HINTS = {
30
30
  "django": "Django",
31
31
  "flask": "Flask",
32
32
  "spring-boot": "Spring Boot",
33
+ "@oclif/core": "oclif",
34
+ "@salesforce/sf-plugins-core": "Salesforce CLI",
35
+ "commander": "Commander",
33
36
  };
34
37
  const CONFIG_FRAMEWORK_HINTS = [
35
38
  { match: /^vitest\.config\.[tj]s$/, name: "vitest", layer: "unit" },
@@ -24,7 +24,7 @@ export function behaviorEntries(nodes, edges) {
24
24
  n.denominator_eligible === true &&
25
25
  n.stale !== true &&
26
26
  !endpointStarts.has(n.external_id) &&
27
- !incomingEligibleCalls.has(n.external_id))
27
+ (n.properties.cli_entrypoint === true || !incomingEligibleCalls.has(n.external_id)))
28
28
  .map((n) => ({
29
29
  external_id: n.external_id,
30
30
  kind: "Behavior",
@@ -20,7 +20,7 @@ import { enrichFromContent } from "./enrich/index.js";
20
20
  import { scoreGraph } from "./score/score.js";
21
21
  import { doctorGraph } from "./score/doctor.js";
22
22
  import { findGaps } from "./gaps/gaps.js";
23
- import { rankPriorityGaps, rankRiskGaps } from "./score/risk.js";
23
+ import { inspectRiskInputHealth, rankPriorityGaps, rankRiskGaps, riskHistoryFingerprint } from "./score/risk.js";
24
24
  import { generateTests } from "./generate/generator.js";
25
25
  import { classifyGeneratedDraftBlocker } from "./generate/draftGuidance.js";
26
26
  import { autoProve, NO_KEY_MESSAGE, isEligibleProvableTarget } from "./autoProve.js";
@@ -46,6 +46,7 @@ import { prepareRuntimeCoverage } from "./analyze/coverageArtifacts.js";
46
46
  import { appendLedgerRecord, canReproveLanguage, loadLedger, ledgerStats, proofEdgesFor, reproveTarget, resolveTargetSymbol, targetFingerprint, targetLanguage } from "./ledger.js";
47
47
  import { buildRtm, provenSymbolIds, renderRtmCsv, renderRtmMarkdown } from "./rtm.js";
48
48
  import { buildProofDoctor, distillProofAttempts, loadProofAttempts, proofAttemptsFresh, writeProofAttempts } from "./proofDoctor.js";
49
+ import { buildArtifactIdentity, PROOF_ORACLE_VERSION } from "./provenance.js";
49
50
  import { tryScopedReprove } from "./reprove/scoped.js";
50
51
  import { resolveContained, toWorkspaceRel } from "./reprove/paths.js";
51
52
  import { hashBuffer } from "./util/hash.js";
@@ -490,7 +491,7 @@ function buildGraphFromFragments(workspaceRoot, scanRoot, analyzeFragment, enric
490
491
  const sources = [...analyzeFragment.sources, ...enrichFragments.flatMap((f) => f.sources)];
491
492
  const manifest = buildManifest(analyzeFragment.file_entries, readGitInfo(gitRunner(scanRoot)), now);
492
493
  const prunedEdges = pruneDanglingEdges(nodes, edges);
493
- return {
494
+ const graph = {
494
495
  schema_version: LOCAL_GRAPH_SCHEMA_VERSION,
495
496
  workspace: {
496
497
  name: config.workspace_name || deriveWorkspaceName(scanRoot),
@@ -516,6 +517,22 @@ function buildGraphFromFragments(workspaceRoot, scanRoot, analyzeFragment, enric
516
517
  confirmed_by_layer: confirmedCoverageByLayer({ nodes, edges: prunedEdges })
517
518
  }
518
519
  };
520
+ graph.artifact_identity = artifactIdentityFor(graph, scanRoot);
521
+ return graph;
522
+ }
523
+ function artifactIdentityFor(graph, repoRoot) {
524
+ const health = inspectRiskInputHealth(repoRoot);
525
+ const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
526
+ return buildArtifactIdentity(graph, {
527
+ configHash: loadRiskConfig(repoRoot).hash,
528
+ history: {
529
+ state: health.history,
530
+ churnWindow: health.churnWindow,
531
+ churnAvailable: health.churnAvailable,
532
+ commitDate: health.commitDate,
533
+ inputFingerprint: riskHistoryFingerprint(repoRoot, codeFiles, health.churnWindow)
534
+ }
535
+ });
519
536
  }
520
537
  // ── operations ───────────────────────────────────────────────────────
521
538
  export function opInit(root, deps = defaultDeps()) {
@@ -970,6 +987,7 @@ export function opRecordRun(root, opts, deps = defaultDeps()) {
970
987
  model: opts.model,
971
988
  prompt_version: opts.prompt_version,
972
989
  language,
990
+ artifact_identity: artifactIdentityFor(preGraph, preGraph.workspace.root),
973
991
  status,
974
992
  reprove_mode: reproveMode,
975
993
  reprove_reason: reproveReason,
@@ -1213,6 +1231,7 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
1213
1231
  const closed = dynamicProofSucceeded(oracle);
1214
1232
  const baselineGreen = oracle.baseline?.exitCode === 0 && oracle.baseline?.timedOut !== true;
1215
1233
  const mutantFailedAssertion = oracle.mutant?.assertionFailure === true && oracle.mutant?.timedOut !== true;
1234
+ const artifactIdentity = artifactIdentityFor(graph, sourceRoot);
1216
1235
  const dynamicProof = {
1217
1236
  proof_kind: "dynamic_targeted",
1218
1237
  baseline_green: baselineGreen,
@@ -1223,7 +1242,9 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
1223
1242
  sentinel: oracle.replacementMode ?? replacementMode,
1224
1243
  runner: oracle.runner ?? runner,
1225
1244
  test_path: oracle.test ?? testRel,
1226
- mutant_status: oracle.status
1245
+ mutant_status: oracle.status,
1246
+ oracle_version: PROOF_ORACLE_VERSION,
1247
+ run_fingerprint: artifactIdentity.run_fingerprint
1227
1248
  };
1228
1249
  const result = appendLedgerRecord(root, {
1229
1250
  run_id: opts.run_id,
@@ -1235,6 +1256,7 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
1235
1256
  evidence_ids: [],
1236
1257
  language,
1237
1258
  dynamic_proof: dynamicProof,
1259
+ artifact_identity: artifactIdentity,
1238
1260
  target_fingerprint: targetFingerprint(graph, target),
1239
1261
  status: closed ? "reproven" : "unproven",
1240
1262
  reason: oracle.reason ?? (closed ? "Dynamic targeted proof closed." : "Dynamic targeted proof did not close."),
@@ -1623,7 +1645,9 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1623
1645
  // Auto-prove (PR 1): key-gated generate→prove for the top provable TS/JS targets.
1624
1646
  // Runs BEFORE the report/RTM writes below so they reflect any freshly-minted Proven.
1625
1647
  // Proof is minted only by opProveLoop's UNCHANGED oracle; no key ⇒ no files, no proof.
1626
- reportProgress("auto-prove: driving generate → prove on the top provable targets", { current: 6, total: 8 });
1648
+ reportProgress(opts.noAuto
1649
+ ? "auto-prove: disabled (--no-auto)"
1650
+ : "auto-prove: driving generate → prove on the top provable targets", { current: 6, total: 8 });
1627
1651
  let autoProveResult;
1628
1652
  try {
1629
1653
  autoProveResult = await autoProve(root, {
@@ -2279,7 +2303,7 @@ export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-co
2279
2303
  data.delta = computeReportDelta(prev, data);
2280
2304
  }
2281
2305
  catch {
2282
- data.delta = null;
2306
+ // buildBehaviorReportData already provides an explicit first_run state.
2283
2307
  }
2284
2308
  const html = renderBehaviorReport(data);
2285
2309
  const htmlPath = resolve(root, outputPath);
@@ -2404,6 +2428,7 @@ function incrementalMerge(root, old, changedFiles, now, maxInferredFlows, maxFil
2404
2428
  confirmed_by_layer: confirmedCoverageByLayer({ nodes, edges: prunedEdges })
2405
2429
  }
2406
2430
  };
2431
+ graph.artifact_identity = artifactIdentityFor(graph, root);
2407
2432
  return { graph, updated_entities: updatedEntities, stale_generated_tests: staleCount, warnings: freshFragment.warnings };
2408
2433
  }
2409
2434
  const NO_DIFF_GUIDANCE = "No changed files found. Run this on a feature branch, pass base_ref, or use the test-gaps tool for baseline opportunities (CLI: opro gaps; MCP: orangepro_find_test_gaps).";