@orangepro/orangepro-mcp 0.2.40 → 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.
- package/dist/local/analyze/analyzer.js +163 -29
- package/dist/local/analyze/classify.js +5 -1
- package/dist/local/analyze/confirm.js +90 -6
- package/dist/local/analyze/frameworks.js +3 -0
- package/dist/local/flows/flowWalker.js +1 -1
- package/dist/local/operations.js +30 -5
- package/dist/local/proofDoctor.js +5 -4
- package/dist/local/provenance.js +84 -0
- package/dist/local/resolve/importGraph.js +15 -1
- package/dist/local/score/rankEligibility.js +15 -0
- package/dist/local/score/risk.js +96 -24
- package/dist/local/viz/behaviorReportData.js +91 -12
- package/dist/local/viz/behaviorReportHtml.js +36 -7
- package/dist/local/viz/html.js +3 -3
- package/package.json +1 -1
|
@@ -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
|
-
...(
|
|
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
|
-
:
|
|
904
|
-
?
|
|
905
|
-
:
|
|
906
|
-
?
|
|
907
|
-
:
|
|
908
|
-
?
|
|
909
|
-
:
|
|
910
|
-
?
|
|
911
|
-
:
|
|
912
|
-
? "
|
|
913
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
package/dist/local/operations.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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).";
|