@orangepro/orangepro-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,2129 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { LOCAL_GRAPH_SCHEMA_VERSION } from "../graph/ontology.js";
4
+ import { makeCandidateEdge, makeEdge, makeNode, makeProofEdges, makeTestCaseNode } from "../graph/factories.js";
5
+ import { hashString } from "../util/hash.js";
6
+ import { slugify } from "../util/ids.js";
7
+ import { loadIgnore, walkFilesWithMeta } from "../util/walk.js";
8
+ import { buildImportGraph } from "../resolve/importGraph.js";
9
+ import { walkBarrel } from "../resolve/barrelWalker.js";
10
+ import { resetResolverCaches } from "../resolve/resolver.js";
11
+ import { resetExportIndexCache } from "../resolve/exportIndex.js";
12
+ import { baseName, extOf, GENERATED_CODE_REASON, isGeneratedCode, isNonProductPath, NON_PRODUCT_REASON, isTestSupportPath, languageOf, manifestKindOf, roleOf } from "./classify.js";
13
+ import { classifyTestLayer } from "./testLayer.js";
14
+ import { detectFromPackageJson, detectFrameworksFromManifest, detectManifestPackage, frameworkFromConfig } from "./frameworks.js";
15
+ import { extractSymbolsWithMeta, extractTestNames, MAX_SYMBOLS_PER_FILE } from "./symbols.js";
16
+ import { extractTreeSitterStructure, extractTreeSitterSymbols, treeSitterReady, treeSitterStatus } from "./treeSitter/engine.js";
17
+ import { isTreeSitterLanguage } from "./treeSitter/languages.js";
18
+ import { conventionSibling, isConventionLanguage } from "./linkage/conventions.js";
19
+ import { isBoilerplateSymbol, BOILERPLATE_REASON } from "./boilerplate.js";
20
+ import { extractCalls, extractMedusaGeneratedServices, MEDUSA_GENERATED_METHOD_BASES, MEDUSA_INTERNAL_SERVICE_TYPE, medusaGeneratedMethodName } from "./callGraph.js";
21
+ import { buildStructuralClusters } from "./clustering.js";
22
+ import { applyRuntimeCoverage } from "./coverage.js";
23
+ import { rankRiskGaps } from "../score/risk.js";
24
+ import { extractBehaviorContracts } from "./behaviorContracts.js";
25
+ import { enumerateFlows } from "../flows/flowWalker.js";
26
+ /**
27
+ * Symbol extraction router. Non-TS languages use the language-agnostic tree-sitter
28
+ * AST when their grammar has been preloaded; TS/JS stay on the TypeScript compiler
29
+ * path. The regex extractors in symbols.ts remain only as a fallback for the rare
30
+ * case where a tree-sitter grammar failed to preload — they are never the primary
31
+ * path.
32
+ */
33
+ function extractFileSymbols(content, language) {
34
+ if (isTreeSitterLanguage(language) && treeSitterReady(language)) {
35
+ return extractTreeSitterSymbols(content, language);
36
+ }
37
+ return extractSymbolsWithMeta(content, language);
38
+ }
39
+ /**
40
+ * The extractor backend used for `language` RIGHT NOW — part of the parse-cache key
41
+ * so a regex-fallback result (computed before a grammar was preloaded) can never be
42
+ * served once tree-sitter becomes ready: the key flips `rx` -> `ts`, forcing a fresh
43
+ * AST extraction instead of persisting a shallow denominator.
44
+ */
45
+ function extractionBackend(language) {
46
+ if (!isTreeSitterLanguage(language))
47
+ return "tsc"; // TS/JS compiler path (or unextracted)
48
+ return treeSitterReady(language) ? "ts" : "rx"; // tree-sitter AST vs regex fallback
49
+ }
50
+ import { runConfirmer } from "./confirm.js";
51
+ const DETECTOR = "repo_analyzer";
52
+ // Global ceiling on extracted code symbols. A SINGLE counter shared across the walk,
53
+ // so a low value lets whichever language is walked first (e.g. a Go `server/`) eat the
54
+ // whole budget and STARVE later dirs (the React/TS `webapp/` extracted 0 symbols on
55
+ // Mattermost). Set high enough that a large multi-language monorepo extracts every
56
+ // language's behavior surface; the per-file cap (symbols.ts MAX_SYMBOLS_PER_FILE) and
57
+ // the file-scan cap still bound the work. Override with ORANGEPRO_MAX_SYMBOLS.
58
+ const MAX_TOTAL_SYMBOLS = 50000;
59
+ const MAX_INFERRED_SERVICES = 30;
60
+ const DEFAULT_CONFIRM_RISK_SYMBOLS = 500;
61
+ /**
62
+ * High default ceiling on inferred behavior anchors — a pathological-run guard,
63
+ * not a tuning knob. Scan-all is the default; ORANGEPRO_MAX_FLOWS lowers it only
64
+ * to bound an accidental huge run.
65
+ */
66
+ const DEFAULT_MAX_INFERRED_FLOWS = 50_000;
67
+ const BEHAVIOR_SURFACE_PATH_RE = /(^|\/)(api|apis|routes?|router|controllers?|handlers?|jobs?|workers?|queues?|processors?|resolvers?|commands?|cmd|webhooks?|listeners?|subscribers?|consumers?)(\/|$)/i;
68
+ const BEHAVIOR_SERVICE_PATH_RE = /(^|\/)(services?|svc)(\/|$)|(^|\/)[^/]*service\.[^.\/]+$/i;
69
+ const BEHAVIOR_SURFACE_FILE_RE = /(^|\/)[^/]*(api|controller|service|handler|resolver|processor|job|worker|queue|consumer|subscriber|listener|command|gateway|route|router)\.[^.\/]+$/i;
70
+ const BEHAVIOR_SURFACE_NAME_RE = /(^|[.#])(__init__|handle|handler|route|controller|service|resolver|processor|job|worker|queue|consumer|subscriber|listener|command|gateway|execute|process|consume|dispatch|schedule|upload|download|sync|checkout|charge|refund|capture|authorize|login|logout|register|find|search|list|create|update|delete|remove|archive|enable|disable|validate|get|add|sub|sum|total|save|load|render|run|main|root|child|mul|about|behavior)([A-Z0-9_]|$)/i;
71
+ const CLIENT_FACTORY_NAME_RE = /(^|[.#])get[A-Z0-9_].*Client$/;
72
+ const BEHAVIOR_OWNER_RE = /(Service|Controller|Resolver|Handler|Processor|Job|Worker|Queue|Consumer|Subscriber|Listener|Command|Gateway|Route|Router)$/;
73
+ const UTILITY_DIRECTORY_EXCLUDE_RE = /\/(utils?|helpers?|tools?|loaders?|dml|dal|orchestration|codemods?|oas|models?|migrations?|migration-scripts?|instrumentation|medusa-telemetry|medusa-test-utils|eslint-plugin)\//i;
74
+ const FRAMEWORK_INTERNAL_PATH_EXCLUDE_RE = /\/(http\/(?:routes-loader|routes-finder|routes-sorter|middlewares\/bodyparser)|medusa-app-loader|remote-query\/query)(?:\/|$)/i;
75
+ const CLI_PACKAGE_PATH_EXCLUDE_RE = /\/(?:cli\/[^/]+\/src\/(?:commands|core|reporter)|packages\/[^/]+\/src\/commands)\//i;
76
+ const BACKEND_RUNTIME_PATH_EXCLUDE_RE = /\/(?:packages\/core\/framework\/src|packages\/modules\/(?:workflow-engine-[^/]+|link-modules)\/src)(?:\/|$)/i;
77
+ const UI_PRODUCT_PATH_EXCLUDE_RE = /\/(?:packages\/admin\/(?:dashboard|admin-bundler|admin-vite-plugin)\/src|packages\/design-system\/(?:toolbox|icons)\/src|www\/(?:apps|packages)\/[^/]+\/(?:app|src|providers|components|lib))(?:\/|$)/i;
78
+ const SDK_CLIENT_PATH_EXCLUDE_RE = /\/(?:packages\/core\/js-sdk\/src|packages\/[^/]+\/(?:sdk|client)\/src)(?:\/|$)/i;
79
+ const PLUGIN_ADMIN_PATH_EXCLUDE_RE = /\/(?:plugins\/[^/]+\/src\/admin|admin\/routes)\//i;
80
+ const INFRA_METHOD_SUFFIX_RE = /^get[A-Z0-9_].*(Identifier|RegistrationKey|Config|Registry|Options|Settings|Path|Directory|TmpDir|Program|PackageManager|Command|Expression|Recommendation|CircularReferences|PivotTableName|PropertyName|PropertyKey|UnderlyingType|ComputedColumnRegistry|EntityOverrideRegistry|InverseRegistry|RelativeDate|SelectsAndRelations|SetDifference|ResolvedPlugins|Token|Scope|Module|Column|Pivot|Ttl|Timeout|Interval|Size|Limit|Offset|Prefix|Suffix|Pattern|Handler|Resource)$/;
81
+ const BUILD_BOOTSTRAP_PREFIX_RE = /^(load|build|compile)(Modules?|Routes?|Routers?|Plugins?|Config|Schema|Program|Package|Project|Files?|Commands?|Migrations?|Definitions?|Manifest|Artifacts?)([A-Z0-9_]|$)/;
82
+ // NOTE: the broad `.*Provider.*Service` clause was removed — it over-excluded FUNCTIONAL provider
83
+ // services (PaymentProviderService, TaxProviderService, FulfillmentProviderService,
84
+ // NotificationProviderService, AuthProviderService) that own real behaviors (capturePayment,
85
+ // getTaxLines, createFulfillment, ...). Genuine infra providers (CacheProviderService) are already
86
+ // caught by the `(Cache|...).*Service` clause. Add explicit infra prefixes here if a new infra
87
+ // provider is missed — never a `.*Provider.*` catch-all.
88
+ // QueryBuilder anchored at ^ matches QueryBuilder*Service (a query-builder is infra by definition,
89
+ // so ALL its methods — buildQuery/buildResponse/compileExpression/buildWhere/... — are plumbing) but
90
+ // NOT functional OrderBuilderService/QuoteService (they don't start with "QueryBuilder"). This is the
91
+ // owner-level fix for the buildQuery leak — strictly narrower than a forbidden `.*Builder.*Service`.
92
+ const INFRA_SERVICE_NAME_RE = /^(?:(InMemory|Redis|Pg|Mongo|Mikro|TypeOrm|Knex).*Service|(Cache|Caching|EventBus|JobScheduler|RemoteQuery|Index|Search|QueryBuilder).*Service)$/;
93
+ const FRAMEWORK_HOOK_METHOD_RE = /^__(joinerConfig|hooks|definition)$/;
94
+ function handlerSymbolCandidates(contract) {
95
+ if (!contract.handler)
96
+ return [];
97
+ if (contract.controller) {
98
+ return [`${contract.controller}.${contract.handler}`];
99
+ }
100
+ return [contract.handler];
101
+ }
102
+ function behaviorSurfaceExclusionReason(relPath, name, memberOf) {
103
+ const path = `/${relPath.replace(/\\/g, "/")}`;
104
+ const owner = memberOf || (name.includes(".") ? name.slice(0, name.indexOf(".")) : "");
105
+ const methodName = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : name;
106
+ if (UTILITY_DIRECTORY_EXCLUDE_RE.test(path)) {
107
+ return "Utility/model/tooling path — infrastructure plumbing, excluded from the behavior denominator.";
108
+ }
109
+ if (FRAMEWORK_INTERNAL_PATH_EXCLUDE_RE.test(path)) {
110
+ return "Framework-internal route/query plumbing — excluded from the behavior denominator.";
111
+ }
112
+ if (CLI_PACKAGE_PATH_EXCLUDE_RE.test(path)) {
113
+ return "CLI developer tooling path — excluded from the behavior denominator.";
114
+ }
115
+ if (BACKEND_RUNTIME_PATH_EXCLUDE_RE.test(path)) {
116
+ return "Framework/runtime/link plumbing path — excluded from backend behavior denominator.";
117
+ }
118
+ if (UI_PRODUCT_PATH_EXCLUDE_RE.test(path)) {
119
+ return "UI/docs/design-system path — excluded from backend behavior denominator.";
120
+ }
121
+ if (SDK_CLIENT_PATH_EXCLUDE_RE.test(path)) {
122
+ return "SDK/client package path — excluded from backend behavior denominator.";
123
+ }
124
+ if (PLUGIN_ADMIN_PATH_EXCLUDE_RE.test(path)) {
125
+ return "Plugin admin UI path — excluded from backend behavior denominator.";
126
+ }
127
+ if (FRAMEWORK_HOOK_METHOD_RE.test(methodName)) {
128
+ return "Framework lifecycle hook — excluded from the behavior denominator.";
129
+ }
130
+ if (owner && INFRA_SERVICE_NAME_RE.test(owner)) {
131
+ return `Method of infrastructure service ${owner} — excluded from the behavior denominator.`;
132
+ }
133
+ if (INFRA_METHOD_SUFFIX_RE.test(methodName)) {
134
+ return "Infrastructure accessor/registry method — excluded from the behavior denominator.";
135
+ }
136
+ if (BUILD_BOOTSTRAP_PREFIX_RE.test(methodName)) {
137
+ return "Framework/build bootstrap method — excluded from the behavior denominator.";
138
+ }
139
+ return null;
140
+ }
141
+ function behaviorSurfaceReason(relPath, name, memberOf) {
142
+ const exclusionReason = behaviorSurfaceExclusionReason(relPath, name, memberOf);
143
+ if (exclusionReason)
144
+ return null;
145
+ const owner = memberOf || (name.includes(".") ? name.slice(0, name.indexOf(".")) : "");
146
+ const servicePath = BEHAVIOR_SERVICE_PATH_RE.test(relPath);
147
+ if (BEHAVIOR_SURFACE_PATH_RE.test(relPath) || (BEHAVIOR_SURFACE_FILE_RE.test(relPath) && !servicePath)) {
148
+ return "API/service/route/job-adjacent source path — countable behavior surface.";
149
+ }
150
+ if (owner && BEHAVIOR_OWNER_RE.test(owner)) {
151
+ return `Method of ${owner} — service/API/job-adjacent behavior surface.`;
152
+ }
153
+ if (CLIENT_FACTORY_NAME_RE.test(name))
154
+ return null;
155
+ if (servicePath && BEHAVIOR_SURFACE_NAME_RE.test(name)) {
156
+ return "Service-adjacent behavior-like function — countable behavior surface.";
157
+ }
158
+ if (BEHAVIOR_SURFACE_NAME_RE.test(name)) {
159
+ return "Handler/service/job-like symbol name — countable behavior surface.";
160
+ }
161
+ return null;
162
+ }
163
+ const FLOW_LINK_STOPWORDS = new Set([
164
+ "test",
165
+ "tests",
166
+ "spec",
167
+ "should",
168
+ "works",
169
+ "found",
170
+ "from",
171
+ "names",
172
+ "behavior",
173
+ "behaviors",
174
+ "service",
175
+ "controller",
176
+ "handler",
177
+ "route",
178
+ "api",
179
+ "src",
180
+ "app"
181
+ ]);
182
+ function textTokens(text) {
183
+ const spaced = text
184
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
185
+ .toLowerCase()
186
+ .split(/[^a-z0-9]+/g)
187
+ .filter((t) => t.length >= 3 && !FLOW_LINK_STOPWORDS.has(t));
188
+ return new Set(spaced);
189
+ }
190
+ function tokenJaccard(a, b) {
191
+ if (a.size === 0 || b.size === 0)
192
+ return 0;
193
+ let intersection = 0;
194
+ for (const t of a)
195
+ if (b.has(t))
196
+ intersection++;
197
+ return intersection / (a.size + b.size - intersection);
198
+ }
199
+ function selectRiskScopedConfirmCandidates(input) {
200
+ const graph = {
201
+ schema_version: LOCAL_GRAPH_SCHEMA_VERSION,
202
+ workspace: { name: path.basename(input.root), root: input.root, root_hash: hashString(input.root), source_upload_policy: "metadata_only" },
203
+ created_at: new Date(0).toISOString(),
204
+ updated_at: new Date(0).toISOString(),
205
+ sources: [],
206
+ nodes: input.nodes,
207
+ edges: input.edges,
208
+ candidate_edges: input.candidate_edges,
209
+ generation_runs: [],
210
+ generated_tests: [],
211
+ manifest: { generated_at: new Date(0).toISOString(), git: null, files: {} }
212
+ };
213
+ const ranked = rankRiskGaps(graph, { limit: input.riskSymbolLimit, repoRoot: input.root });
214
+ const scoreBySymbol = new Map(ranked.map((gap, index) => [gap.id, { score: gap.risk_score, rank: index }]));
215
+ const fileScore = new Map();
216
+ for (const [file, names] of input.eligibleSymbolsByFile) {
217
+ for (const name of names) {
218
+ const risk = scoreBySymbol.get(`sym:${file}#${name}`);
219
+ if (!risk)
220
+ continue;
221
+ const existing = fileScore.get(file);
222
+ if (!existing || risk.score > existing.score || (risk.score === existing.score && risk.rank < existing.rank)) {
223
+ fileScore.set(file, { ...risk, symbols: 1 });
224
+ }
225
+ else if (existing) {
226
+ existing.symbols++;
227
+ }
228
+ }
229
+ }
230
+ const rankedCandidates = input.candidates
231
+ .filter((candidate) => fileScore.has(candidate.implRel))
232
+ .sort((a, b) => {
233
+ const ar = fileScore.get(a.implRel);
234
+ const br = fileScore.get(b.implRel);
235
+ return ((br?.score ?? 0) - (ar?.score ?? 0) ||
236
+ (ar?.rank ?? Number.MAX_SAFE_INTEGER) - (br?.rank ?? Number.MAX_SAFE_INTEGER) ||
237
+ a.implRel.localeCompare(b.implRel) ||
238
+ a.testRel.localeCompare(b.testRel));
239
+ });
240
+ const selected = [];
241
+ const involved = new Set();
242
+ for (const candidate of rankedCandidates) {
243
+ const next = new Set(involved);
244
+ next.add(candidate.testAbs);
245
+ next.add(candidate.implAbs);
246
+ if (next.size > input.confirmBudget)
247
+ continue;
248
+ selected.push(candidate);
249
+ involved.clear();
250
+ for (const file of next)
251
+ involved.add(file);
252
+ }
253
+ const selectedFiles = new Set(selected.map((candidate) => candidate.implRel));
254
+ let riskSymbols = 0;
255
+ for (const file of selectedFiles)
256
+ riskSymbols += fileScore.get(file)?.symbols ?? 0;
257
+ return { candidates: selected, riskSymbols, involvedFiles: involved.size };
258
+ }
259
+ /**
260
+ * Build an OrangePro-shaped graph fragment from a local checkout/path.
261
+ *
262
+ * Fully deterministic and static — no LLM. Stores metadata only (paths, names,
263
+ * hashes): file content is read in-process but never persisted.
264
+ *
265
+ * Node/property conventions (consumed by score, gaps, generation, pack):
266
+ * - File properties: { language, role, test_layer? }
267
+ * - ConfigFile properties: { role }
268
+ * - TestCase external_id `test:<relPath>`; properties: { framework?, test_layer, file, test_names[] }
269
+ * - CodeSymbol external_id `sym:<relPath>#<name>`; properties: { symbol_kind, file, start_line?, end_line? }
270
+ * - Framework external_id `framework:<name>`; properties: { category, test_layer? }
271
+ * - Package external_id `package:<name>`; properties: { ecosystem, dependencies[] }
272
+ * - Service external_id `service:<area>` (inferred, weak); properties: { area, inferred_from }
273
+ * - UserFlow external_id `flow:<slug>` (inferred, weak); properties: { area, inferred_from, example_behaviors[] }
274
+ */
275
+ export function analyzeRepo(root, opts = {}) {
276
+ // A long-lived process (MCP server) must never resolve with a stale tsconfig
277
+ // or export surface: reset both resolve-layer caches at the start of every run.
278
+ resetResolverCaches();
279
+ resetExportIndexCache();
280
+ const readContent = opts.readContent !== false;
281
+ const maxFlows = Math.max(1, opts.maxInferredFlows ?? DEFAULT_MAX_INFERRED_FLOWS);
282
+ const maxSymbols = Math.max(1, opts.maxSymbols ?? MAX_TOTAL_SYMBOLS);
283
+ const ignore = loadIgnore(root);
284
+ const { files, truncated: filesCapHit, max_files: maxFiles } = walkFilesWithMeta(root, ignore, { maxFiles: opts.maxFiles });
285
+ const warnings = [];
286
+ // Wall-clock budget for the per-file scan (DISCLOSED partial when hit; never silent).
287
+ const now = opts.now ?? (() => Date.now());
288
+ const scanStartMs = now();
289
+ const budgetMs = opts.maxAnalyzeMs != null && opts.maxAnalyzeMs > 0 ? opts.maxAnalyzeMs : null;
290
+ let filesProcessed = 0;
291
+ let budgetStopped = false;
292
+ const nodes = [];
293
+ const edges = [];
294
+ const candidate_edges = [];
295
+ const file_entries = {};
296
+ const repoName = baseName(root) || "workspace";
297
+ const repoScopeId = `repo:${slugify(repoName)}`;
298
+ const combinedHash = hashString(files.map((f) => f.hash).sort().join("|"));
299
+ const source = {
300
+ source_scope_id: repoScopeId,
301
+ source_system: "repo",
302
+ source_type: "local_checkout",
303
+ display_name: repoName,
304
+ content_hash: combinedHash,
305
+ metadata: { file_count: files.length, root_name: repoName }
306
+ };
307
+ const prov = (ref, quote_hash) => ({
308
+ source_scope_id: repoScopeId,
309
+ source_ref: ref,
310
+ quote_hash,
311
+ detector: DETECTOR
312
+ });
313
+ // Root anchor.
314
+ nodes.push(makeNode({
315
+ kind: "TenantStub",
316
+ external_id: "tenant:local",
317
+ title: repoName,
318
+ properties: { local_only: true },
319
+ evidence_strength: "hard",
320
+ review_status: "auto_detected",
321
+ confidence: 1,
322
+ provenance: prov(repoScopeId)
323
+ }));
324
+ const frameworkIds = new Set();
325
+ const packageIds = new Set();
326
+ const serviceIds = new Set();
327
+ const flowIds = new Set();
328
+ let symbolCount = 0;
329
+ let symbolCapHit = false;
330
+ let symbolFilesTruncated = 0;
331
+ let excludedBoilerplate = 0;
332
+ // Phase 4 confirmer inputs: denominator-eligible exported symbol names per code
333
+ // file, and every CodeSymbol external_id that made it into the graph (so a
334
+ // confirmed-but-capped symbol downgrades instead of COVERS-ing the file).
335
+ const eligibleSymbolsByFile = new Map();
336
+ const codeSymbolIds = new Set();
337
+ let testFiles = 0;
338
+ let flowsTruncated = 0;
339
+ let sawPythonTest = false;
340
+ let pytestRef = "";
341
+ // For linking each test file to its likely source sibling by basename stem.
342
+ const codeFilesByStem = new Map();
343
+ // All scanned source-file relPaths — drives precise per-language convention
344
+ // linkage (predict-and-verify a sibling path actually exists in scope).
345
+ const codeFileSet = new Set();
346
+ const generatedCodeFiles = new Set();
347
+ const behaviorContractsByFramework = new Map();
348
+ const behaviorContractsByKind = new Map();
349
+ const behaviorContractsByFile = new Map();
350
+ let behaviorContractsTotal = 0;
351
+ let behaviorContractsHandlerEdges = 0;
352
+ const testFileStems = [];
353
+ // TS/JS test+code files fed to the import-graph resolver (Gate 1).
354
+ const resolveFiles = [];
355
+ // Emitted CodeSymbol names per file — the call graph resolves callers/callees
356
+ // ONLY to symbols that actually became nodes (the "known symbol" invariant).
357
+ const symbolsByFile = new Map();
358
+ // Raw (caller, callee) call pairs per TS/JS code file, resolved after the
359
+ // import graph is built (cross-file calls need its bindings + targets).
360
+ const rawCallsByFile = new Map();
361
+ const medusaGeneratedServicesByFile = new Map();
362
+ // Raw imports/calls for tree-sitter languages. Edges are emitted only after
363
+ // every CodeSymbol exists, so endpoints are known and ambiguity can underlink.
364
+ const nonTsStructureByFile = new Map();
365
+ const goTestStructureByFile = new Map();
366
+ const javaTestStructureByFile = new Map();
367
+ const pythonTestStructureByFile = new Map();
368
+ const goModulesByDir = new Map();
369
+ /** Human-readable feature name from a test/module file (drops test suffixes). */
370
+ const featureName = (relPath) => {
371
+ const cleaned = baseName(relPath)
372
+ .replace(/\.(test|spec)\.[a-z0-9]+$/i, "")
373
+ .replace(/^test_/, "")
374
+ .replace(/_test$/, "")
375
+ .replace(/\.[a-z0-9]+$/i, "");
376
+ const human = cleaned.replace(/[-_.]+/g, " ").trim();
377
+ return human || "behaviors";
378
+ };
379
+ /** Bare module name (lowercased) used to match a test file to its source sibling. */
380
+ const moduleStem = (relPath) => {
381
+ let s = baseName(relPath).replace(/\.[a-z0-9]+$/i, ""); // drop extension
382
+ s = s.replace(/\.(test|spec)$/i, ""); // card.test -> card
383
+ s = s.replace(/^test[_.]/i, ""); // test_card -> card
384
+ s = s.replace(/[_.](test|spec)$/i, ""); // card_test -> card
385
+ return s.toLowerCase();
386
+ };
387
+ const dirOf = (relPath) => {
388
+ const i = relPath.lastIndexOf("/");
389
+ return i >= 0 ? relPath.slice(0, i) : "";
390
+ };
391
+ const readSafe = (absPath) => {
392
+ if (!readContent)
393
+ return null;
394
+ try {
395
+ return readFileSync(absPath, "utf8");
396
+ }
397
+ catch {
398
+ return null;
399
+ }
400
+ };
401
+ const isNonProductFile = (relPath) => isNonProductPath(relPath) || generatedCodeFiles.has(relPath);
402
+ const addFramework = (name, category, test_layer, ref) => {
403
+ const external_id = `framework:${name}`;
404
+ if (!frameworkIds.has(external_id)) {
405
+ frameworkIds.add(external_id);
406
+ nodes.push(makeNode({
407
+ kind: "Framework",
408
+ external_id,
409
+ title: name,
410
+ properties: { category, ...(test_layer ? { test_layer } : {}) },
411
+ evidence_strength: "hard",
412
+ review_status: "auto_detected",
413
+ confidence: 1,
414
+ provenance: prov(ref)
415
+ }));
416
+ }
417
+ };
418
+ const topArea = (relPath) => {
419
+ const parts = relPath.split("/").filter(Boolean);
420
+ const skip = new Set(["src", "app", "lib", "packages", "tests", "test", "e2e", "__tests__", "spec"]);
421
+ for (const part of parts.slice(0, parts.length - 1)) {
422
+ if (!skip.has(part.toLowerCase()))
423
+ return part;
424
+ }
425
+ return parts.length > 1 ? parts[0] : "core";
426
+ };
427
+ // Per-directory evidence tally, to suggest non-evidence dirs for .orangeproignore.
428
+ const dirTally = new Map();
429
+ for (const file of files) {
430
+ // Budget gate: stop the per-file scan once the wall-clock budget is exhausted. The
431
+ // remaining files are recorded as not_analyzed_due_to_budget (disclosed partial).
432
+ if (budgetMs != null && now() - scanStartMs >= budgetMs) {
433
+ budgetStopped = true;
434
+ break;
435
+ }
436
+ filesProcessed++;
437
+ const role = roleOf(file.relPath);
438
+ const dirKey = dirOf(file.relPath) || "(root)";
439
+ const dt = dirTally.get(dirKey) ?? { other: 0, useful: 0 };
440
+ if (role === "other")
441
+ dt.other++;
442
+ else
443
+ dt.useful++;
444
+ dirTally.set(dirKey, dt);
445
+ file_entries[file.relPath] = { hash: file.hash, size: file.size, kind: manifestKindOf(file.relPath) };
446
+ const base = baseName(file.relPath);
447
+ // conftest.py is a strong pytest signal even though it is not a test file.
448
+ if (base === "conftest.py") {
449
+ sawPythonTest = true;
450
+ pytestRef = pytestRef || file.relPath;
451
+ }
452
+ if (role === "config") {
453
+ nodes.push(makeNode({
454
+ kind: "ConfigFile",
455
+ external_id: file.relPath,
456
+ title: base,
457
+ properties: { role: "config" },
458
+ evidence_strength: "hard",
459
+ review_status: "auto_detected",
460
+ confidence: 1,
461
+ provenance: prov(file.relPath),
462
+ content_hash: file.hash
463
+ }));
464
+ const fwFromConfig = frameworkFromConfig(file.relPath);
465
+ if (fwFromConfig) {
466
+ addFramework(fwFromConfig.name, fwFromConfig.category, fwFromConfig.test_layer, file.relPath);
467
+ edges.push(makeEdge({
468
+ from_external_id: `framework:${fwFromConfig.name}`,
469
+ to_external_id: file.relPath,
470
+ relationship_type: "CONFIGURED_BY",
471
+ evidence_strength: "hard",
472
+ review_status: "auto_detected",
473
+ provenance: prov(file.relPath)
474
+ }));
475
+ }
476
+ const content = readSafe(file.absPath);
477
+ if (content && base === "package.json") {
478
+ const { pkg, frameworks } = detectFromPackageJson(file.relPath, content);
479
+ if (pkg && !packageIds.has(`package:${pkg.name}`)) {
480
+ packageIds.add(`package:${pkg.name}`);
481
+ nodes.push(makeNode({
482
+ kind: "Package",
483
+ external_id: `package:${pkg.name}`,
484
+ title: pkg.name,
485
+ properties: { ecosystem: pkg.ecosystem, dependencies: pkg.dependencies },
486
+ evidence_strength: "hard",
487
+ review_status: "auto_detected",
488
+ confidence: 1,
489
+ provenance: prov(file.relPath)
490
+ }));
491
+ }
492
+ for (const fw of frameworks)
493
+ addFramework(fw.name, fw.category, fw.test_layer, file.relPath);
494
+ }
495
+ else if (content) {
496
+ if (base === "go.mod") {
497
+ const m = content.match(/^\s*module\s+(\S+)/m);
498
+ if (m)
499
+ goModulesByDir.set(dirOf(file.relPath), m[1]);
500
+ }
501
+ const pkg = detectManifestPackage(file.relPath, content);
502
+ if (pkg && !packageIds.has(`package:${pkg.name}`)) {
503
+ packageIds.add(`package:${pkg.name}`);
504
+ nodes.push(makeNode({
505
+ kind: "Package",
506
+ external_id: `package:${pkg.name}`,
507
+ title: pkg.name,
508
+ properties: { ecosystem: pkg.ecosystem, dependencies: pkg.dependencies },
509
+ evidence_strength: "hard",
510
+ review_status: "auto_detected",
511
+ confidence: 1,
512
+ provenance: prov(file.relPath)
513
+ }));
514
+ }
515
+ for (const fw of detectFrameworksFromManifest(file.relPath, content)) {
516
+ addFramework(fw.name, fw.category, fw.test_layer, file.relPath);
517
+ if (fw.name === "pytest")
518
+ pytestRef = pytestRef || file.relPath;
519
+ }
520
+ }
521
+ continue;
522
+ }
523
+ // Container File node for code/test/doc roles.
524
+ const language = languageOf(file.relPath);
525
+ nodes.push(makeNode({
526
+ kind: "File",
527
+ external_id: file.relPath,
528
+ title: base,
529
+ properties: { language, role },
530
+ evidence_strength: "hard",
531
+ review_status: "auto_detected",
532
+ confidence: 1,
533
+ provenance: prov(file.relPath),
534
+ content_hash: file.hash
535
+ }));
536
+ if ((role === "test" || role === "code") && (language === "typescript" || language === "javascript")) {
537
+ resolveFiles.push({ abs: file.absPath, rel: file.relPath, role: role === "test" ? "test" : "source" });
538
+ }
539
+ if (role === "test") {
540
+ testFiles++;
541
+ testFileStems.push({ relPath: file.relPath, stem: moduleStem(file.relPath), dir: dirOf(file.relPath) });
542
+ if (extOf(file.relPath) === "py") {
543
+ sawPythonTest = true;
544
+ pytestRef = pytestRef || file.relPath;
545
+ }
546
+ const content = readSafe(file.absPath);
547
+ const { layer, confidence: layerConfidence, signals: layerSignals } = classifyTestLayer(file.relPath, content);
548
+ const testNames = content
549
+ ? opts.parseCache
550
+ ? opts.parseCache.testNames(file.hash, () => extractTestNames(content))
551
+ : extractTestNames(content)
552
+ : [];
553
+ const testExternalId = `test:${file.relPath}`;
554
+ nodes.push(makeTestCaseNode({
555
+ testRel: file.relPath,
556
+ title: base,
557
+ testLayer: layer,
558
+ layerConfidence,
559
+ layerSignals,
560
+ testNames,
561
+ provenance: prov(file.relPath, content ? hashString(testNames.join("\n")) : undefined),
562
+ contentHash: file.hash
563
+ }));
564
+ edges.push(makeEdge({
565
+ from_external_id: testExternalId,
566
+ to_external_id: file.relPath,
567
+ relationship_type: "DEFINED_IN",
568
+ evidence_strength: "hard",
569
+ review_status: "auto_detected",
570
+ provenance: prov(file.relPath)
571
+ }));
572
+ if (language === "go" && content && treeSitterReady("go")) {
573
+ goTestStructureByFile.set(file.relPath, extractTreeSitterStructure(content, "go"));
574
+ }
575
+ if (language === "java" && content && treeSitterReady("java")) {
576
+ javaTestStructureByFile.set(file.relPath, extractTreeSitterStructure(content, "java"));
577
+ }
578
+ if (language === "python" && content && treeSitterReady("python")) {
579
+ pythonTestStructureByFile.set(file.relPath, extractTreeSitterStructure(content, "python"));
580
+ }
581
+ // Inferred behavior anchor (weak) grounded in real test names.
582
+ if (testNames.length > 0) {
583
+ const area = topArea(file.relPath);
584
+ const flowId = `flow:${slugify(area + "-" + base)}`;
585
+ if (!flowIds.has(flowId)) {
586
+ if (flowIds.size >= maxFlows) {
587
+ flowsTruncated++;
588
+ }
589
+ else {
590
+ flowIds.add(flowId);
591
+ const feature = featureName(file.relPath);
592
+ const title = `${feature.charAt(0).toUpperCase()}${feature.slice(1)} (found from test names)`;
593
+ nodes.push(makeNode({
594
+ kind: "UserFlow",
595
+ external_id: flowId,
596
+ title,
597
+ properties: { area, feature, inferred_from: "test_describe", example_behaviors: testNames.slice(0, 6), priority: "unknown" },
598
+ evidence_strength: "weak",
599
+ review_status: "inferred",
600
+ confidence: 0.35,
601
+ provenance: prov(file.relPath, hashString(testNames.join("\n"))),
602
+ behavior_source: "test_inferred",
603
+ denominator_eligible: false,
604
+ denominator_reason: "Inferred from test names — a test cannot witness its own requirement."
605
+ }));
606
+ candidate_edges.push(makeCandidateEdge({
607
+ from_external_id: flowId,
608
+ to_external_id: testExternalId,
609
+ relationship_type: "MAY_BE_TESTED_BY",
610
+ evidence_strength: "weak",
611
+ reason: "Behavior anchor inferred from test names in this file",
612
+ confidence: 0.35,
613
+ provenance: prov(file.relPath)
614
+ }));
615
+ }
616
+ }
617
+ }
618
+ }
619
+ else if (role === "code") {
620
+ const stem = moduleStem(file.relPath);
621
+ const entry = { relPath: file.relPath, dir: dirOf(file.relPath) };
622
+ const list = codeFilesByStem.get(stem);
623
+ if (list)
624
+ list.push(entry);
625
+ else
626
+ codeFilesByStem.set(stem, [entry]);
627
+ codeFileSet.add(file.relPath);
628
+ const area = topArea(file.relPath);
629
+ const serviceId = `service:${slugify(area)}`;
630
+ if (!serviceIds.has(serviceId) && serviceIds.size < MAX_INFERRED_SERVICES && area !== "core") {
631
+ serviceIds.add(serviceId);
632
+ nodes.push(makeNode({
633
+ kind: "Service",
634
+ external_id: serviceId,
635
+ title: area,
636
+ properties: { area, inferred_from: "directory" },
637
+ evidence_strength: "weak",
638
+ review_status: "inferred",
639
+ confidence: 0.3,
640
+ provenance: prov(file.relPath)
641
+ }));
642
+ }
643
+ const content = readSafe(file.absPath);
644
+ const isGenerated = content ? isGeneratedCode(content) : false;
645
+ if (isGenerated)
646
+ generatedCodeFiles.add(file.relPath);
647
+ if (content && (language === "typescript" || language === "javascript") && !isGenerated && !isNonProductPath(file.relPath)) {
648
+ for (const contract of extractBehaviorContracts(content, file.relPath)) {
649
+ if (behaviorSurfaceExclusionReason(file.relPath, contract.handler ?? contract.title))
650
+ continue;
651
+ behaviorContractsTotal++;
652
+ behaviorContractsByFramework.set(contract.framework, (behaviorContractsByFramework.get(contract.framework) ?? 0) + 1);
653
+ behaviorContractsByKind.set(contract.kind, (behaviorContractsByKind.get(contract.kind) ?? 0) + 1);
654
+ const contractsForFile = behaviorContractsByFile.get(file.relPath);
655
+ if (contractsForFile)
656
+ contractsForFile.push(contract);
657
+ else
658
+ behaviorContractsByFile.set(file.relPath, [contract]);
659
+ nodes.push(makeNode({
660
+ kind: "Endpoint",
661
+ external_id: contract.id,
662
+ title: contract.title,
663
+ properties: {
664
+ contract_kind: contract.kind,
665
+ framework: contract.framework,
666
+ method: contract.method,
667
+ path: contract.path,
668
+ file: contract.file,
669
+ source: contract.source,
670
+ ...(contract.handler ? { handler: contract.handler } : {}),
671
+ ...(contract.controller ? { controller: contract.controller } : {})
672
+ },
673
+ evidence_strength: "hard",
674
+ review_status: "auto_detected",
675
+ confidence: 1,
676
+ provenance: prov(file.relPath),
677
+ behavior_source: "contract_entrypoint",
678
+ denominator_eligible: false,
679
+ denominator_reason: "Framework entrypoint contract discovered; tracked separately from the legacy CodeSymbol denominator in v1."
680
+ }));
681
+ edges.push(makeEdge({
682
+ from_external_id: contract.id,
683
+ to_external_id: file.relPath,
684
+ relationship_type: "DEFINED_IN",
685
+ evidence_strength: "hard",
686
+ review_status: "auto_detected",
687
+ provenance: prov(file.relPath)
688
+ }));
689
+ }
690
+ }
691
+ if (content && symbolCount >= maxSymbols && !symbolCapHit) {
692
+ // The global cap can land exactly on a file boundary — the flag must
693
+ // still flip, or every later file is skipped with zero disclosure.
694
+ symbolCapHit = true;
695
+ warnings.push(`Symbol extraction cap (${maxSymbols}) reached; some code symbols omitted. Raise ORANGEPRO_MAX_SYMBOLS to include them.`);
696
+ }
697
+ if (content && symbolCount < maxSymbols) {
698
+ const extraction = opts.parseCache
699
+ ? opts.parseCache.symbols(file.hash, `${language}#${extractionBackend(language)}`, () => extractFileSymbols(content, language))
700
+ : extractFileSymbols(content, language);
701
+ if (extraction.truncated)
702
+ symbolFilesTruncated++;
703
+ for (const sym of extraction.symbols) {
704
+ if (symbolCount >= maxSymbols) {
705
+ symbolCapHit = true;
706
+ warnings.push(`Symbol extraction cap (${maxSymbols}) reached; some code symbols omitted. Raise ORANGEPRO_MAX_SYMBOLS to include them.`);
707
+ break;
708
+ }
709
+ const symId = `sym:${file.relPath}#${sym.name}`;
710
+ // Gate 3 eligibility: an export counts as a code-derived behavior only
711
+ // when it is provably callable behavior surface — fn/class/method, or
712
+ // a const the AST proved is a function. `.d.ts` declares types, not
713
+ // behavior. (evidence is "hard" here by construction; role is "code"
714
+ // because this whole branch is the code-file path.)
715
+ const isDts = /\.d\.[cm]?ts$/.test(file.relPath);
716
+ // Symbols in CI/test-infra paths are kept as nodes but excluded from the
717
+ // behavior denominator (path-based; takes precedence over accessor rules).
718
+ const isInfra = isNonProductPath(file.relPath);
719
+ const isGeneratedSymbol = generatedCodeFiles.has(file.relPath);
720
+ const callableKind = sym.symbol_kind === "function" || sym.symbol_kind === "class" || sym.symbol_kind === "method";
721
+ const nonTsClassBehavior = sym.symbol_kind === "class" && language !== "typescript" && language !== "javascript";
722
+ const behaviorCallableKind = sym.symbol_kind === "function" || sym.symbol_kind === "method" || sym.callable === true || nonTsClassBehavior;
723
+ // Trivial accessors (Java getId/toString, Python __repr__) are real
724
+ // symbols but carry no testable behavior — kept in the graph, dropped
725
+ // from the denominator with disclosure so coverage is not under-claimed.
726
+ const isBoilerplate = !isDts && !isInfra && !isGeneratedSymbol && callableKind && isBoilerplateSymbol(sym.name, language, sym.symbol_kind, sym.trivial_accessor);
727
+ const callableBehaviorCandidate = !isDts && !isInfra && !isGeneratedSymbol && !isBoilerplate && behaviorCallableKind;
728
+ const surfaceExclusionReason = callableBehaviorCandidate
729
+ ? behaviorSurfaceExclusionReason(file.relPath, sym.name, sym.member_of)
730
+ : null;
731
+ const surfaceReason = nonTsClassBehavior
732
+ ? "Non-TS class/constructor behavior surface — countable for language proof."
733
+ : callableBehaviorCandidate && !surfaceExclusionReason
734
+ ? behaviorSurfaceReason(file.relPath, sym.name, sym.member_of)
735
+ : null;
736
+ const eligible = callableBehaviorCandidate && surfaceReason !== null;
737
+ const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null;
738
+ const notEntryPointAdjacent = callableBehaviorCandidate && !eligible && !behaviorSurfaceExcluded;
739
+ if (isBoilerplate)
740
+ excludedBoilerplate++;
741
+ nodes.push(makeNode({
742
+ kind: "CodeSymbol",
743
+ external_id: symId,
744
+ title: sym.name,
745
+ properties: {
746
+ symbol_kind: sym.symbol_kind,
747
+ file: file.relPath,
748
+ ...(sym.start_line ? { start_line: sym.start_line } : {}),
749
+ ...(sym.end_line ? { end_line: sym.end_line } : {}),
750
+ ...(sym.member_of ? { member_of: sym.member_of } : {}),
751
+ ...(sym.callable !== undefined ? { callable_const: sym.callable } : {}),
752
+ ...(surfaceReason ? { behavior_surface: "entrypoint_adjacent" } : {}),
753
+ ...(behaviorSurfaceExcluded ? { denominator_reason_code: "infra_behavior_surface" } : {}),
754
+ ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {})
755
+ },
756
+ evidence_strength: "hard",
757
+ review_status: "auto_detected",
758
+ confidence: 1,
759
+ provenance: prov(file.relPath),
760
+ behavior_source: "code_export",
761
+ denominator_eligible: eligible,
762
+ denominator_reason: isDts
763
+ ? "Type declaration (.d.ts/.d.mts/.d.cts) — no runtime behavior to test."
764
+ : isInfra
765
+ ? NON_PRODUCT_REASON
766
+ : isGeneratedSymbol
767
+ ? GENERATED_CODE_REASON
768
+ : isBoilerplate
769
+ ? BOILERPLATE_REASON
770
+ : eligible && surfaceReason
771
+ ? surfaceReason
772
+ : behaviorSurfaceExcluded && surfaceExclusionReason
773
+ ? surfaceExclusionReason
774
+ : callableBehaviorCandidate
775
+ ? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
776
+ : sym.symbol_kind === "class"
777
+ ? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
778
+ : "Exported const (not provably callable) — excluded from the denominator in v1."
779
+ }));
780
+ edges.push(makeEdge({
781
+ from_external_id: symId,
782
+ to_external_id: file.relPath,
783
+ relationship_type: "DEFINED_IN",
784
+ evidence_strength: "hard",
785
+ review_status: "auto_detected",
786
+ provenance: prov(file.relPath)
787
+ }));
788
+ codeSymbolIds.add(symId);
789
+ const fileSyms = symbolsByFile.get(file.relPath);
790
+ if (fileSyms)
791
+ fileSyms.add(sym.name);
792
+ else
793
+ symbolsByFile.set(file.relPath, new Set([sym.name]));
794
+ if (eligible) {
795
+ const list = eligibleSymbolsByFile.get(file.relPath);
796
+ if (list)
797
+ list.push(sym.name);
798
+ else
799
+ eligibleSymbolsByFile.set(file.relPath, [sym.name]);
800
+ }
801
+ symbolCount++;
802
+ }
803
+ const contractsForFile = behaviorContractsByFile.get(file.relPath) ?? [];
804
+ const fileSymbols = symbolsByFile.get(file.relPath) ?? new Set();
805
+ for (const contract of contractsForFile) {
806
+ const handlerName = handlerSymbolCandidates(contract).find((candidate) => fileSymbols.has(candidate));
807
+ if (!handlerName)
808
+ continue;
809
+ const handlerId = `sym:${file.relPath}#${handlerName}`;
810
+ if (!codeSymbolIds.has(handlerId))
811
+ continue;
812
+ edges.push(makeEdge({
813
+ from_external_id: contract.id,
814
+ to_external_id: handlerId,
815
+ relationship_type: "IMPLEMENTED_IN",
816
+ evidence_strength: "hard",
817
+ review_status: "auto_detected",
818
+ provenance: prov(file.relPath)
819
+ }));
820
+ behaviorContractsHandlerEdges++;
821
+ }
822
+ // Call graph (TS/JS only): capture raw (caller, callee) pairs now while
823
+ // the content is in hand; resolution to CodeSymbols runs after the import
824
+ // graph is built (cross-file calls reuse its bindings + targets).
825
+ // Skip non-product paths (test infra + .github CI) so the call graph
826
+ // consumes only product behavior — same predicate that excludes those
827
+ // symbols from the denominator.
828
+ if ((language === "typescript" || language === "javascript") && !isNonProductFile(file.relPath)) {
829
+ const tsx = /\.(tsx|jsx)$/.test(file.relPath);
830
+ const calls = extractCalls(content, tsx);
831
+ if (calls.length > 0)
832
+ rawCallsByFile.set(file.relPath, calls);
833
+ const generatedServices = extractMedusaGeneratedServices(content, tsx);
834
+ if (generatedServices.length > 0)
835
+ medusaGeneratedServicesByFile.set(file.relPath, generatedServices);
836
+ }
837
+ else if (isTreeSitterLanguage(language) && treeSitterReady(language) && !isNonProductFile(file.relPath)) {
838
+ const structure = extractTreeSitterStructure(content, language);
839
+ nonTsStructureByFile.set(file.relPath, { language, structure });
840
+ }
841
+ }
842
+ }
843
+ }
844
+ const medusaGeneratedByClass = new Map();
845
+ const medusaServiceByRegistration = new Map();
846
+ const lowerCaseFirst = (value) => (value ? `${value[0].toLowerCase()}${value.slice(1)}` : value);
847
+ for (const [rel, services] of medusaGeneratedServicesByFile) {
848
+ for (const service of services) {
849
+ const classId = `sym:${rel}#${service.className}`;
850
+ if (!codeSymbolIds.has(classId))
851
+ continue;
852
+ medusaGeneratedByClass.set(`${rel}#${service.className}`, { rel, service });
853
+ for (const modelKey of service.modelKeys) {
854
+ const registration = `${lowerCaseFirst(modelKey)}Service`;
855
+ const existing = medusaServiceByRegistration.get(registration);
856
+ const item = { rel, service, modelKey };
857
+ if (existing)
858
+ existing.push(item);
859
+ else
860
+ medusaServiceByRegistration.set(registration, [item]);
861
+ }
862
+ for (const modelKey of service.modelKeys) {
863
+ for (const method of MEDUSA_GENERATED_METHOD_BASES) {
864
+ const methodName = medusaGeneratedMethodName(modelKey, method);
865
+ const symbolName = `${service.className}.${methodName}`;
866
+ const symId = `sym:${rel}#${symbolName}`;
867
+ if (codeSymbolIds.has(symId))
868
+ continue;
869
+ codeSymbolIds.add(symId);
870
+ nodes.push(makeNode({
871
+ kind: "CodeSymbol",
872
+ external_id: symId,
873
+ title: symbolName,
874
+ properties: {
875
+ symbol_kind: "GeneratedMethod",
876
+ file: rel,
877
+ member_of: service.className,
878
+ method_base: method,
879
+ model_key: modelKey,
880
+ origin: "framework-derived",
881
+ synthesized: true,
882
+ framework: "medusa"
883
+ },
884
+ evidence_strength: "framework-derived",
885
+ review_status: "auto_detected",
886
+ confidence: 1,
887
+ provenance: prov(rel),
888
+ behavior_source: "code_export",
889
+ denominator_eligible: false,
890
+ denominator_reason: "Framework-derived Medusa service method — synthesized runtime method, excluded from the behavior denominator."
891
+ }));
892
+ }
893
+ }
894
+ }
895
+ }
896
+ // ---- Import graph (parse-only ts.resolveModuleName) — Gate 1 integration ----
897
+ // (1) hard File->IMPORTS->File edges: a resolved internal import is a
898
+ // structural fact, not a guess;
899
+ // (2) resolved test->source MAY_RELATE_TO candidates: the PRIMARY test<->source
900
+ // linkage, replacing the basename-stem guess. Still candidate (never proof):
901
+ // a resolved import proves the test file LOADS the module, not that it
902
+ // exercises + asserts a binding — that upgrade is the Phase-4 confirmer.
903
+ // Targets that are tests or test-support (mocks/fixtures/helpers) are
904
+ // excluded: exercising a helper is not coverage of production behavior.
905
+ // (3) the per-axis resolver gate metrics, persisted with raw counts.
906
+ // Tests with at least one RESOLVED internal runtime non-test import: the
907
+ // resolver understood these files, so the stem fallback is suppressed even
908
+ // when every target was support-filtered — a test that imports only helpers
909
+ // must not get stem-linked to a module it never imports.
910
+ const importResolvedTests = new Set();
911
+ let resolverMetrics;
912
+ if (readContent && resolveFiles.length > 0) {
913
+ const relByAbs = new Map(resolveFiles.map((f) => [path.resolve(f.abs), f.rel]));
914
+ const importGraph = buildImportGraph(resolveFiles.map((f) => ({ path: f.abs, role: f.role })), {
915
+ repoRoot: root,
916
+ resolverCache: opts.resolverCache,
917
+ // Full walked set (incl. tsconfig/package.json/lockfiles + any extended JSON) drives
918
+ // the resolver gate — a config edit anywhere must bust the cache (Codex 5.4.3).
919
+ gateFiles: files.map((f) => path.resolve(root, f.relPath))
920
+ });
921
+ resolverMetrics = importGraph.metrics;
922
+ // Two independent dedups: a (type-only) import must still emit the IMPORTS
923
+ // edge, while the SAME module pair reached later by a runtime import must
924
+ // still produce the test->source linkage.
925
+ const seenImports = new Set();
926
+ const seenTestLinks = new Set();
927
+ // Per-file import bindings (local name → {imported, target file+abs}).
928
+ // Runtime bindings resolve direct calls; type bindings resolve injected-field
929
+ // type annotations. Namespace imports (`* as ns`) are kept separately for
930
+ // import-scoped `ns.method()` MAY_CALL resolution.
931
+ const importBindingsByFile = new Map();
932
+ const typeBindingsByFile = new Map();
933
+ const namespaceBindingsByFile = new Map();
934
+ for (const e of importGraph.edges) {
935
+ if (!e.resolved || e.external || !e.target)
936
+ continue;
937
+ const fromRel = relByAbs.get(path.resolve(e.from));
938
+ const targetRel = relByAbs.get(path.resolve(e.target));
939
+ // Skip targets outside the scanned set (ignored/generated files): no node to link.
940
+ if (!fromRel || !targetRel || fromRel === targetRel)
941
+ continue;
942
+ const targetAbs = path.resolve(e.target);
943
+ for (const b of e.bindings) {
944
+ if (e.importKind === "runtime") {
945
+ if (b.imported === "*") {
946
+ let ns = namespaceBindingsByFile.get(fromRel);
947
+ if (!ns)
948
+ namespaceBindingsByFile.set(fromRel, (ns = new Map()));
949
+ if (!ns.has(b.local))
950
+ ns.set(b.local, { targetRel, targetAbs });
951
+ continue;
952
+ }
953
+ let m = importBindingsByFile.get(fromRel);
954
+ if (!m)
955
+ importBindingsByFile.set(fromRel, (m = new Map()));
956
+ if (!m.has(b.local))
957
+ m.set(b.local, { imported: b.imported, targetRel, targetAbs });
958
+ continue;
959
+ }
960
+ if (b.imported === "*")
961
+ continue;
962
+ let tm = typeBindingsByFile.get(fromRel);
963
+ if (!tm)
964
+ typeBindingsByFile.set(fromRel, (tm = new Map()));
965
+ if (!tm.has(b.local))
966
+ tm.set(b.local, { imported: b.imported, targetRel, targetAbs });
967
+ }
968
+ const pairKey = `${fromRel}|${targetRel}`;
969
+ if (!seenImports.has(pairKey)) {
970
+ seenImports.add(pairKey);
971
+ edges.push(makeEdge({
972
+ from_external_id: fromRel,
973
+ to_external_id: targetRel,
974
+ relationship_type: "IMPORTS",
975
+ evidence_strength: "hard",
976
+ review_status: "auto_detected",
977
+ provenance: prov(fromRel)
978
+ }));
979
+ }
980
+ if (e.fromRole === "test" && e.importKind === "runtime" && e.targetRole !== "test") {
981
+ importResolvedTests.add(fromRel);
982
+ if (!isTestSupportPath(targetRel) && !isNonProductFile(targetRel) && !seenTestLinks.has(pairKey)) {
983
+ seenTestLinks.add(pairKey);
984
+ candidate_edges.push(makeCandidateEdge({
985
+ from_external_id: fromRel,
986
+ to_external_id: targetRel,
987
+ relationship_type: "MAY_RELATE_TO",
988
+ evidence_strength: "candidate",
989
+ reason: `Test resolved-imports this module ("${e.specifier}")`,
990
+ confidence: 0.75,
991
+ provenance: prov(fromRel)
992
+ }));
993
+ }
994
+ }
995
+ }
996
+ const tts = importGraph.metrics.test_to_source;
997
+ if (tts.n > 0 && tts.resolved / tts.n < 0.8) {
998
+ warnings.push(`Static confirmation is not defensible for this scope (test->source resolution ${tts.pct}% < 80%); structural test<->source linkage is reported per-link only.`);
999
+ }
1000
+ // ---- Call graph (Layer 1): symbol→symbol CALLS + MAY_CALL edges ----
1001
+ // STRUCTURAL CONTEXT ONLY — never coverage evidence, never fed to the
1002
+ // confirmer/denominator. Every endpoint is a KNOWN emitted CodeSymbol.
1003
+ // hard CALLS (exact, no guessing):
1004
+ // - free `bar()` → same-file symbol, else an import-resolved binding
1005
+ // whose target file actually defines that name;
1006
+ // - `this.bar()` → a member of the caller's own class, same file;
1007
+ // - `C.bar()` static → a member of the same-file class `C`.
1008
+ // candidate MAY_CALL (heuristic hint, tiered confidence, ambiguity → none):
1009
+ // - 0.65 `obj.method()` → the ONLY emitted `*.method` member in the repo;
1010
+ // - 0.55 `ns.method()` → an export reachable from the namespace-imported module;
1011
+ // - 0.45 `foo()` → a barrel terminal traced by the resolver/export index.
1012
+ const seenCalls = new Set();
1013
+ const seenFrameworkDerivedCalls = new Set();
1014
+ const seenMay = new Set();
1015
+ const emitCall = (callerId, calleeId, fromRel, callVia, resolution) => {
1016
+ if (callerId === calleeId)
1017
+ return; // skip self-recursion edges
1018
+ const key = `${callerId}|${calleeId}`;
1019
+ if (seenCalls.has(key))
1020
+ return;
1021
+ seenCalls.add(key);
1022
+ edges.push(makeEdge({
1023
+ from_external_id: callerId,
1024
+ to_external_id: calleeId,
1025
+ relationship_type: "CALLS",
1026
+ evidence_strength: "hard",
1027
+ review_status: "auto_detected",
1028
+ provenance: prov(fromRel),
1029
+ properties: {
1030
+ call_via: callVia,
1031
+ resolution
1032
+ }
1033
+ }));
1034
+ };
1035
+ const emitFrameworkDerivedCall = (callerId, calleeId, fromRel, resolution, modelKey, methodBase) => {
1036
+ if (callerId === calleeId || !codeSymbolIds.has(callerId) || !codeSymbolIds.has(calleeId))
1037
+ return;
1038
+ const key = `${callerId}|${calleeId}`;
1039
+ if (seenCalls.has(key) || seenFrameworkDerivedCalls.has(key))
1040
+ return;
1041
+ seenFrameworkDerivedCalls.add(key);
1042
+ edges.push(makeEdge({
1043
+ from_external_id: callerId,
1044
+ to_external_id: calleeId,
1045
+ relationship_type: "CALLS",
1046
+ evidence_strength: "framework-derived",
1047
+ review_status: "auto_detected",
1048
+ provenance: prov(fromRel),
1049
+ properties: {
1050
+ call_via: "framework-derived",
1051
+ origin: "medusa-generated-service",
1052
+ resolution,
1053
+ model_key: modelKey,
1054
+ method_base: methodBase
1055
+ }
1056
+ }));
1057
+ };
1058
+ // Heuristic edges are accumulated, then emitted AFTER all exact CALLS so an
1059
+ // exact pair always wins (a pair that is both stays hard, never duplicated).
1060
+ const mayCandidates = [];
1061
+ const queueMay = (callerId, calleeId, fromRel, confidence, reason) => {
1062
+ if (callerId === calleeId)
1063
+ return;
1064
+ mayCandidates.push({ callerId, calleeId, fromRel, confidence, reason });
1065
+ };
1066
+ // Global index of emitted PRODUCT member methods by unqualified name — drives
1067
+ // the 0.65 unique-member tier (exactly one candidate, else no edge).
1068
+ const membersByMethod = new Map();
1069
+ for (const n of nodes) {
1070
+ if (n.kind !== "CodeSymbol" || typeof n.properties.member_of !== "string")
1071
+ continue;
1072
+ const f = n.properties.file;
1073
+ if (typeof f === "string" && isNonProductFile(f))
1074
+ continue; // infra/generated targets excluded
1075
+ const title = n.title ?? "";
1076
+ const dot = title.indexOf(".");
1077
+ if (dot < 0)
1078
+ continue;
1079
+ const method = title.slice(dot + 1);
1080
+ const list = membersByMethod.get(method);
1081
+ if (list)
1082
+ list.push(n.external_id);
1083
+ else
1084
+ membersByMethod.set(method, [n.external_id]);
1085
+ }
1086
+ // Resolve `name` exported from a (possibly barrel) module to an emitted,
1087
+ // non-infra terminal symbol id, or null. Direct local definition first,
1088
+ // else a deterministic resolver-backed barrel walk.
1089
+ const resolveModuleExport = (targetRel, targetAbs, name) => {
1090
+ if (symbolsByFile.get(targetRel)?.has(name) && !isNonProductFile(targetRel))
1091
+ return `sym:${targetRel}#${name}`;
1092
+ const w = walkBarrel(targetAbs, name);
1093
+ if (w.status !== "terminal" || !w.covered || !w.terminalFile || !w.terminalBinding)
1094
+ return null;
1095
+ const termRel = relByAbs.get(path.resolve(w.terminalFile));
1096
+ if (!termRel || isNonProductFile(termRel))
1097
+ return null;
1098
+ return symbolsByFile.get(termRel)?.has(w.terminalBinding) ? `sym:${termRel}#${w.terminalBinding}` : null;
1099
+ };
1100
+ const resolveInjectedMember = (typeBinding, callee) => {
1101
+ if (typeBinding.imported === "default" || typeBinding.imported === "*")
1102
+ return null;
1103
+ if (symbolsByFile.get(typeBinding.targetRel)?.has(typeBinding.imported) && !isNonProductFile(typeBinding.targetRel)) {
1104
+ const member = `${typeBinding.imported}.${callee}`;
1105
+ return symbolsByFile.get(typeBinding.targetRel)?.has(member) ? { id: `sym:${typeBinding.targetRel}#${member}`, resolution: "injected_import" } : null;
1106
+ }
1107
+ const w = walkBarrel(typeBinding.targetAbs, typeBinding.imported);
1108
+ if (w.status !== "terminal" || !w.covered || !w.terminalFile || !w.terminalBinding)
1109
+ return null;
1110
+ const termRel = relByAbs.get(path.resolve(w.terminalFile));
1111
+ if (!termRel || isNonProductFile(termRel))
1112
+ return null;
1113
+ const member = `${w.terminalBinding}.${callee}`;
1114
+ return symbolsByFile.get(termRel)?.has(w.terminalBinding) && symbolsByFile.get(termRel)?.has(member) ? { id: `sym:${termRel}#${member}`, resolution: "injected_barrel" } : null;
1115
+ };
1116
+ for (const [rel, calls] of rawCallsByFile) {
1117
+ const localSyms = symbolsByFile.get(rel);
1118
+ if (!localSyms)
1119
+ continue; // file emitted no symbols → no caller can exist
1120
+ const imports = importBindingsByFile.get(rel);
1121
+ const typeImports = typeBindingsByFile.get(rel);
1122
+ const namespaces = namespaceBindingsByFile.get(rel);
1123
+ for (const c of calls) {
1124
+ if (!localSyms.has(c.caller))
1125
+ continue; // caller must be an emitted symbol
1126
+ const callerId = `sym:${rel}#${c.caller}`;
1127
+ if (c.via === "free") {
1128
+ if (localSyms.has(c.callee)) {
1129
+ emitCall(callerId, `sym:${rel}#${c.callee}`, rel, "free", "same_file"); // same-file
1130
+ }
1131
+ else {
1132
+ const b = imports?.get(c.callee);
1133
+ if (!b)
1134
+ continue;
1135
+ if (symbolsByFile.get(b.targetRel)?.has(b.imported) && !isNonProductFile(b.targetRel)) {
1136
+ emitCall(callerId, `sym:${b.targetRel}#${b.imported}`, rel, "free", "import"); // exact: target defines it
1137
+ }
1138
+ else {
1139
+ // 0.45 — resolver-backed barrel terminal recovery.
1140
+ const w = walkBarrel(b.targetAbs, b.imported);
1141
+ if (w.status === "terminal" && w.covered && w.terminalFile && w.terminalBinding) {
1142
+ const termRel = relByAbs.get(path.resolve(w.terminalFile));
1143
+ if (termRel && !isNonProductFile(termRel) && symbolsByFile.get(termRel)?.has(w.terminalBinding)) {
1144
+ queueMay(callerId, `sym:${termRel}#${w.terminalBinding}`, rel, 0.45, `Resolver-backed barrel MAY_CALL: call matched a terminal export through a barrel.`);
1145
+ }
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+ else if (c.via === "this") {
1151
+ const cls = c.caller.includes(".") ? c.caller.slice(0, c.caller.indexOf(".")) : "";
1152
+ if (cls) {
1153
+ const member = `${cls}.${c.callee}`;
1154
+ if (localSyms.has(member))
1155
+ emitCall(callerId, `sym:${rel}#${member}`, rel, "this", "same_file");
1156
+ }
1157
+ }
1158
+ else if (c.via === "static" && c.qualifier) {
1159
+ const member = `${c.qualifier}.${c.callee}`;
1160
+ if (localSyms.has(c.qualifier) && localSyms.has(member)) {
1161
+ emitCall(callerId, `sym:${rel}#${member}`, rel, "static", "same_file"); // exact: same-file class member
1162
+ }
1163
+ else {
1164
+ const ns = namespaces?.get(c.qualifier);
1165
+ const qImport = imports?.get(c.qualifier);
1166
+ if (ns) {
1167
+ // 0.55 — import-scoped: ns.method() → an export of the imported module.
1168
+ const calleeId = resolveModuleExport(ns.targetRel, ns.targetAbs, c.callee);
1169
+ if (calleeId)
1170
+ queueMay(callerId, calleeId, rel, 0.55, `Import-scoped MAY_CALL: ns.method() matched an export reachable from the imported module.`);
1171
+ }
1172
+ else if (qImport) {
1173
+ // 0.65 — IMPORT-ANCHORED unique member: the qualifier is imported, and
1174
+ // the only emitted `*.method` member is DEFINED IN that imported module.
1175
+ // Anchoring to the import kills generic-name noise (a local `x.get()`
1176
+ // has no import binding for `x`, so it never reaches here).
1177
+ const cands = membersByMethod.get(c.callee);
1178
+ if (cands && cands.length === 1 && cands[0].slice(4, cands[0].indexOf("#")) === qImport.targetRel) {
1179
+ queueMay(callerId, cands[0], rel, 0.65, `Imported-binding member match: the qualifier is imported from the module defining the only *.method symbol.`);
1180
+ }
1181
+ }
1182
+ // A non-imported (local/param) qualifier is NOT anchored — no edge.
1183
+ }
1184
+ }
1185
+ else if (c.via === "injected" && c.injectedType) {
1186
+ const typeBinding = typeImports?.get(c.injectedType) ?? imports?.get(c.injectedType);
1187
+ if (typeBinding) {
1188
+ const targetMember = resolveInjectedMember(typeBinding, c.callee);
1189
+ if (targetMember)
1190
+ emitCall(callerId, targetMember.id, rel, "injected", targetMember.resolution);
1191
+ else {
1192
+ const direct = medusaGeneratedByClass.get(`${typeBinding.targetRel}#${typeBinding.imported}`);
1193
+ if (direct) {
1194
+ if (direct.service.methods.includes(c.callee)) {
1195
+ const targetId = `sym:${direct.rel}#${direct.service.className}.${c.callee}`;
1196
+ const match = direct.service.modelKeys.flatMap((modelKey) => MEDUSA_GENERATED_METHOD_BASES.map((base) => ({ modelKey, base, methodName: medusaGeneratedMethodName(modelKey, base) }))).find(({ methodName }) => methodName === c.callee);
1197
+ if (match && codeSymbolIds.has(targetId)) {
1198
+ emitFrameworkDerivedCall(callerId, targetId, rel, "medusa_concrete_generated_class", match.modelKey, match.base);
1199
+ }
1200
+ }
1201
+ else if (MEDUSA_GENERATED_METHOD_BASES.includes(c.callee)) {
1202
+ const matches = direct.service.modelKeys
1203
+ .map((modelKey) => ({
1204
+ modelKey,
1205
+ methodName: medusaGeneratedMethodName(modelKey, c.callee)
1206
+ }))
1207
+ .filter(({ methodName }) => codeSymbolIds.has(`sym:${direct.rel}#${direct.service.className}.${methodName}`));
1208
+ if (matches.length === 1) {
1209
+ emitFrameworkDerivedCall(callerId, `sym:${direct.rel}#${direct.service.className}.${matches[0].methodName}`, rel, "medusa_concrete_generated_class", matches[0].modelKey, c.callee);
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+ continue;
1215
+ }
1216
+ const member = `${c.injectedType}.${c.callee}`;
1217
+ if (localSyms.has(c.injectedType) && localSyms.has(member)) {
1218
+ emitCall(callerId, `sym:${rel}#${member}`, rel, "injected", "same_file_type");
1219
+ }
1220
+ else if (c.injectedType === MEDUSA_INTERNAL_SERVICE_TYPE && MEDUSA_GENERATED_METHOD_BASES.includes(c.callee) && c.qualifier) {
1221
+ const registration = c.qualifier.endsWith("_") ? c.qualifier.slice(0, -1) : c.qualifier;
1222
+ const matches = medusaServiceByRegistration.get(registration) ?? [];
1223
+ if (matches.length === 1) {
1224
+ const match = matches[0];
1225
+ const methodName = medusaGeneratedMethodName(match.modelKey, c.callee);
1226
+ const targetId = `sym:${match.rel}#${match.service.className}.${methodName}`;
1227
+ if (codeSymbolIds.has(targetId)) {
1228
+ emitFrameworkDerivedCall(callerId, targetId, rel, "medusa_unique_registration", match.modelKey, c.callee);
1229
+ }
1230
+ }
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ // Emit MAY_CALL — skip any pair already a hard CALLS, dedup among themselves.
1236
+ for (const m of mayCandidates) {
1237
+ const key = `${m.callerId}|${m.calleeId}`;
1238
+ if (seenCalls.has(key) || seenMay.has(key))
1239
+ continue;
1240
+ seenMay.add(key);
1241
+ candidate_edges.push(makeCandidateEdge({
1242
+ from_external_id: m.callerId,
1243
+ to_external_id: m.calleeId,
1244
+ relationship_type: "MAY_CALL",
1245
+ evidence_strength: "weak",
1246
+ reason: m.reason,
1247
+ confidence: m.confidence,
1248
+ provenance: prov(m.fromRel)
1249
+ }));
1250
+ }
1251
+ }
1252
+ // ---- Non-TS structural imports/calls (Layer 1) ----
1253
+ // Tree-sitter languages do not feed the TS resolver/confirmer. These edges are
1254
+ // structural context only, emitted only when both endpoints are known and the
1255
+ // language convention resolves without ambiguity. Ambiguous modules underlink.
1256
+ let goProofConfirmedPairs = 0;
1257
+ let goProofAttempted = 0;
1258
+ let javaProofConfirmedPairs = 0;
1259
+ let javaProofAttempted = 0;
1260
+ let pythonProofConfirmedPairs = 0;
1261
+ let pythonProofAttempted = 0;
1262
+ if (nonTsStructureByFile.size > 0) {
1263
+ const nodeByExternalId = new Map(nodes.map((n) => [n.external_id, n]));
1264
+ const codeFilesByLanguage = new Map();
1265
+ const javaClassByFqn = new Map();
1266
+ const javaClassFilesByFqn = new Map();
1267
+ const kotlinSymbolByFqn = new Map();
1268
+ const phpClassByFqn = new Map();
1269
+ const csharpFilesByNamespace = new Map();
1270
+ const csharpClassByFqn = new Map();
1271
+ const csharpUsingNamespacesByFile = new Map();
1272
+ const rustModuleToFiles = new Map();
1273
+ const pythonModuleToFiles = new Map();
1274
+ const goFilesByDir = new Map();
1275
+ const addMulti = (map, key, value) => {
1276
+ let set = map.get(key);
1277
+ if (!set)
1278
+ map.set(key, (set = new Set()));
1279
+ set.add(value);
1280
+ };
1281
+ const rustSourceRoots = files
1282
+ .filter((f) => /\.rs$/i.test(f.relPath) && (baseName(f.relPath) === "main.rs" || baseName(f.relPath) === "lib.rs"))
1283
+ .map((f) => dirOf(f.relPath))
1284
+ .sort((a, b) => b.length - a.length);
1285
+ const rustSourceRootFor = (rel) => rustSourceRoots.find((r) => (r ? rel.startsWith(`${r}/`) : true));
1286
+ const rustModulePath = (sourceRoot, rel) => {
1287
+ const moduleRel = sourceRoot ? rel.slice(sourceRoot.length + 1) : rel;
1288
+ const withoutExt = moduleRel.replace(/\.rs$/i, "");
1289
+ if (withoutExt === "lib" || withoutExt === "main")
1290
+ return "crate";
1291
+ if (withoutExt.endsWith("/mod")) {
1292
+ const parent = withoutExt.slice(0, -"/mod".length);
1293
+ return parent ? `crate.${parent.split("/").join(".")}` : "crate";
1294
+ }
1295
+ if (withoutExt)
1296
+ return `crate.${withoutExt.split("/").join(".")}`;
1297
+ return null;
1298
+ };
1299
+ const rustMapKey = (sourceRoot, modulePath) => `${sourceRoot}\0${modulePath}`;
1300
+ const uniqueRustModuleTarget = (fromRel, modulePath) => {
1301
+ const sourceRoot = rustSourceRootFor(fromRel);
1302
+ return sourceRoot === undefined ? null : uniqueMapTarget(rustModuleToFiles, rustMapKey(sourceRoot, modulePath));
1303
+ };
1304
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
1305
+ let byLang = codeFilesByLanguage.get(language);
1306
+ if (!byLang)
1307
+ codeFilesByLanguage.set(language, (byLang = new Set()));
1308
+ byLang.add(rel);
1309
+ if (language === "go") {
1310
+ const d = dirOf(rel);
1311
+ const list = goFilesByDir.get(d);
1312
+ if (list)
1313
+ list.push(rel);
1314
+ else
1315
+ goFilesByDir.set(d, [rel]);
1316
+ }
1317
+ if (language === "java" && structure.packageName) {
1318
+ for (const name of symbolsByFile.get(rel) ?? []) {
1319
+ const node = nodeByExternalId.get(`sym:${rel}#${name}`);
1320
+ if (node?.properties.symbol_kind === "class") {
1321
+ javaClassByFqn.set(`${structure.packageName}.${name}`, rel);
1322
+ addMulti(javaClassFilesByFqn, `${structure.packageName}.${name}`, rel);
1323
+ }
1324
+ }
1325
+ }
1326
+ if (language === "kotlin" && structure.packageName) {
1327
+ for (const name of structure.topLevelSymbols ?? []) {
1328
+ if (!symbolsByFile.get(rel)?.has(name))
1329
+ continue;
1330
+ addMulti(kotlinSymbolByFqn, `${structure.packageName}.${name}`, rel);
1331
+ }
1332
+ }
1333
+ if (language === "php" && structure.moduleName) {
1334
+ for (const name of symbolsByFile.get(rel) ?? []) {
1335
+ const node = nodeByExternalId.get(`sym:${rel}#${name}`);
1336
+ if (node?.properties.symbol_kind === "class")
1337
+ phpClassByFqn.set(`${structure.moduleName}\\${name}`, rel);
1338
+ }
1339
+ }
1340
+ if (language === "csharp" && structure.moduleName) {
1341
+ addMulti(csharpFilesByNamespace, structure.moduleName, rel);
1342
+ for (const name of symbolsByFile.get(rel) ?? []) {
1343
+ const node = nodeByExternalId.get(`sym:${rel}#${name}`);
1344
+ if (node?.properties.symbol_kind === "class")
1345
+ addMulti(csharpClassByFqn, `${structure.moduleName}.${name}`, rel);
1346
+ }
1347
+ }
1348
+ if (language === "rust") {
1349
+ const sourceRoot = rustSourceRootFor(rel);
1350
+ const modulePath = sourceRoot === undefined ? null : rustModulePath(sourceRoot, rel);
1351
+ if (sourceRoot !== undefined && modulePath)
1352
+ addMulti(rustModuleToFiles, rustMapKey(sourceRoot, modulePath), rel);
1353
+ }
1354
+ if (language === "python") {
1355
+ const withoutExt = rel.replace(/\.py$/i, "");
1356
+ const parts = withoutExt.endsWith("/__init__") ? withoutExt.slice(0, -"/__init__".length).split("/") : withoutExt.split("/");
1357
+ for (let i = 0; i < parts.length; i++) {
1358
+ const mod = parts.slice(i).join(".");
1359
+ if (!mod)
1360
+ continue;
1361
+ let set = pythonModuleToFiles.get(mod);
1362
+ if (!set)
1363
+ pythonModuleToFiles.set(mod, (set = new Set()));
1364
+ set.add(rel);
1365
+ }
1366
+ }
1367
+ }
1368
+ const importBindingsByFile = new Map();
1369
+ const seenImports = new Set(edges.filter((e) => e.relationship_type === "IMPORTS").map((e) => `${e.from_external_id}|${e.to_external_id}`));
1370
+ const seenCalls = new Set(edges.filter((e) => e.relationship_type === "CALLS").map((e) => `${e.from_external_id}|${e.to_external_id}`));
1371
+ const emitImport = (fromRel, targetRel) => {
1372
+ if (fromRel === targetRel || !codeFileSet.has(targetRel) || isNonProductFile(targetRel))
1373
+ return;
1374
+ const key = `${fromRel}|${targetRel}`;
1375
+ if (seenImports.has(key))
1376
+ return;
1377
+ seenImports.add(key);
1378
+ edges.push(makeEdge({
1379
+ from_external_id: fromRel,
1380
+ to_external_id: targetRel,
1381
+ relationship_type: "IMPORTS",
1382
+ evidence_strength: "hard",
1383
+ review_status: "auto_detected",
1384
+ provenance: prov(fromRel)
1385
+ }));
1386
+ };
1387
+ const emitCall = (callerId, calleeId, fromRel) => {
1388
+ if (callerId === calleeId || !codeSymbolIds.has(callerId) || !codeSymbolIds.has(calleeId))
1389
+ return;
1390
+ const key = `${callerId}|${calleeId}`;
1391
+ if (seenCalls.has(key))
1392
+ return;
1393
+ seenCalls.add(key);
1394
+ edges.push(makeEdge({
1395
+ from_external_id: callerId,
1396
+ to_external_id: calleeId,
1397
+ relationship_type: "CALLS",
1398
+ evidence_strength: "hard",
1399
+ review_status: "auto_detected",
1400
+ provenance: prov(fromRel),
1401
+ properties: {
1402
+ call_via: "language_convention",
1403
+ resolution: "tree_sitter_exact"
1404
+ }
1405
+ }));
1406
+ };
1407
+ const addImportBinding = (fromRel, local, target, imported, kind) => {
1408
+ let map = importBindingsByFile.get(fromRel);
1409
+ if (!map)
1410
+ importBindingsByFile.set(fromRel, (map = new Map()));
1411
+ if (!map.has(local))
1412
+ map.set(local, { ...target, imported, kind });
1413
+ };
1414
+ const uniquePythonModule = (mod) => {
1415
+ const set = pythonModuleToFiles.get(mod);
1416
+ return set?.size === 1 ? [...set][0] : null;
1417
+ };
1418
+ const resolveRelativePython = (fromRel, mod) => {
1419
+ const m = mod.match(/^(\.+)(.*)$/);
1420
+ if (!m)
1421
+ return null;
1422
+ const up = m[1].length - 1;
1423
+ const tail = m[2].replace(/^\./, "").split(".").filter(Boolean);
1424
+ const base = dirOf(fromRel).split("/").filter(Boolean);
1425
+ if (up > base.length)
1426
+ return null;
1427
+ const parts = base.slice(0, base.length - up).concat(tail);
1428
+ const file = `${parts.join("/")}.py`;
1429
+ const init = `${parts.join("/")}/__init__.py`;
1430
+ if (codeFileSet.has(file))
1431
+ return file;
1432
+ if (codeFileSet.has(init))
1433
+ return init;
1434
+ return null;
1435
+ };
1436
+ const resolvePythonImport = (fromRel, mod) => (mod.startsWith(".") ? resolveRelativePython(fromRel, mod) : uniquePythonModule(mod));
1437
+ const resolveRelativeModule = (fromRel, mod, exts) => {
1438
+ const raw = mod.replace(/^\.\//, "");
1439
+ const base = path.posix.normalize(path.posix.join(dirOf(fromRel), raw));
1440
+ if (base.startsWith("../"))
1441
+ return null;
1442
+ for (const ext of exts) {
1443
+ const file = base.endsWith(ext) ? base : `${base}${ext}`;
1444
+ if (codeFileSet.has(file) && !isNonProductFile(file))
1445
+ return file;
1446
+ }
1447
+ for (const ext of exts) {
1448
+ const file = `${base}/index${ext}`;
1449
+ if (codeFileSet.has(file) && !isNonProductFile(file))
1450
+ return file;
1451
+ }
1452
+ return null;
1453
+ };
1454
+ const uniqueMapTarget = (map, key) => {
1455
+ const matches = [...(map.get(key) ?? [])].filter((f) => !isNonProductFile(f));
1456
+ return matches.length === 1 ? matches[0] : null;
1457
+ };
1458
+ const symbolKind = (rel, name) => nodeByExternalId.get(`sym:${rel}#${name}`)?.properties.symbol_kind;
1459
+ const guardedMemberTarget = (targetRel, className, methodName) => {
1460
+ if (!targetRel || !symbolsByFile.get(targetRel)?.has(methodName))
1461
+ return null;
1462
+ const classes = [...(symbolsByFile.get(targetRel) ?? [])].filter((name) => symbolKind(targetRel, name) === "class");
1463
+ if (classes.length !== 1 || classes[0] !== className)
1464
+ return null;
1465
+ return symbolKind(targetRel, methodName) === "method" ? targetRel : null;
1466
+ };
1467
+ const guardedCsharpMemberTarget = (fromRel, className, methodName) => {
1468
+ const ownNamespace = nonTsStructureByFile.get(fromRel)?.structure.moduleName;
1469
+ const namespaces = new Set([...(ownNamespace ? [ownNamespace] : []), ...(csharpUsingNamespacesByFile.get(fromRel) ?? [])]);
1470
+ const matches = [...namespaces]
1471
+ .map((ns) => guardedMemberTarget(uniqueMapTarget(csharpClassByFqn, `${ns}.${className}`) ?? undefined, className, methodName))
1472
+ .filter((v) => Boolean(v));
1473
+ return [...new Set(matches)].length === 1 ? matches[0] : null;
1474
+ };
1475
+ const resolveRustImport = (fromRel, mod) => {
1476
+ if (mod.startsWith("./"))
1477
+ return resolveRelativeModule(fromRel, mod, [".rs"]);
1478
+ const target = uniqueRustModuleTarget(fromRel, mod);
1479
+ if (target)
1480
+ return target;
1481
+ const parts = mod.split(".").filter(Boolean);
1482
+ if (parts.length > 1)
1483
+ return uniqueRustModuleTarget(fromRel, parts.slice(0, -1).join("."));
1484
+ return null;
1485
+ };
1486
+ const goModuleForDir = (dir) => {
1487
+ for (;;) {
1488
+ const module = goModulesByDir.get(dir);
1489
+ if (module)
1490
+ return { dir, module };
1491
+ if (!dir)
1492
+ return null;
1493
+ dir = dirOf(dir);
1494
+ }
1495
+ };
1496
+ const goModuleForFile = (rel) => goModuleForDir(dirOf(rel));
1497
+ const resolveGoImportDir = (fromRel, mod) => {
1498
+ const owner = goModuleForFile(fromRel);
1499
+ if (!owner || !mod.startsWith(`${owner.module}/`))
1500
+ return null;
1501
+ const moduleRel = mod.slice(owner.module.length + 1);
1502
+ const dir = path.posix.normalize(owner.dir ? path.posix.join(owner.dir, moduleRel) : moduleRel);
1503
+ if (dir.startsWith("../") || (owner.dir && dir !== owner.dir && !dir.startsWith(`${owner.dir}/`)))
1504
+ return null;
1505
+ if (goModuleForDir(dir)?.module !== owner.module)
1506
+ return null;
1507
+ const filesInPackage = (goFilesByDir.get(dir) ?? []).filter((f) => !isNonProductFile(f));
1508
+ return filesInPackage.length > 0 ? dir : null;
1509
+ };
1510
+ const uniqueGoPackageSymbol = (dir, name) => {
1511
+ const matches = (goFilesByDir.get(dir) ?? []).filter((f) => !isNonProductFile(f) && symbolsByFile.get(f)?.has(name));
1512
+ return matches.length === 1 ? matches[0] : null;
1513
+ };
1514
+ const eligibleGoSymbol = (targetRel, name) => {
1515
+ if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1516
+ return null;
1517
+ const symId = `sym:${targetRel}#${name}`;
1518
+ return codeSymbolIds.has(symId) ? symId : null;
1519
+ };
1520
+ const eligiblePythonSymbol = (targetRel, name) => {
1521
+ if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1522
+ return null;
1523
+ const symId = `sym:${targetRel}#${name}`;
1524
+ return codeSymbolIds.has(symId) ? symId : null;
1525
+ };
1526
+ const uniqueJavaClass = (packageName, className) => {
1527
+ return packageName ? uniqueMapTarget(javaClassFilesByFqn, `${packageName}.${className}`) : null;
1528
+ };
1529
+ const javaClassDeclaresMethod = (targetRel, className, methodName) => {
1530
+ return Boolean(nonTsStructureByFile.get(targetRel)?.structure.javaClasses?.some((c) => c.name === className && c.methods.includes(methodName)));
1531
+ };
1532
+ const eligibleJavaSymbol = (targetRel, name, expectedKind) => {
1533
+ if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1534
+ return null;
1535
+ if (symbolKind(targetRel, name) !== expectedKind)
1536
+ return null;
1537
+ const symId = `sym:${targetRel}#${name}`;
1538
+ return codeSymbolIds.has(symId) ? symId : null;
1539
+ };
1540
+ const resolveGoProofTarget = (testRel, structure, qualifier, callee, shadowed) => {
1541
+ if (!qualifier) {
1542
+ if (shadowed.has(callee) || structure.packageName?.endsWith("_test"))
1543
+ return null;
1544
+ return eligibleGoSymbol(uniqueGoPackageSymbol(dirOf(testRel), callee), callee);
1545
+ }
1546
+ const rootQualifier = qualifier.split(".")[0];
1547
+ if (shadowed.has(rootQualifier))
1548
+ return null;
1549
+ const binding = structure.imports.find((i) => i.local === rootQualifier && i.kind === "module");
1550
+ const targetDir = binding ? resolveGoImportDir(testRel, binding.module) : null;
1551
+ return targetDir ? eligibleGoSymbol(uniqueGoPackageSymbol(targetDir, callee), callee) : null;
1552
+ };
1553
+ const resolveJavaProofTarget = (structure, proof) => {
1554
+ if (new Set(proof.shadowed).has(proof.className))
1555
+ return null;
1556
+ const targetRel = uniqueJavaClass(structure.packageName, proof.className);
1557
+ if (proof.target_kind === "constructor")
1558
+ return eligibleJavaSymbol(targetRel, proof.className, "class");
1559
+ if (!targetRel || !javaClassDeclaresMethod(targetRel, proof.className, proof.callee))
1560
+ return null;
1561
+ return eligibleJavaSymbol(targetRel, proof.callee, "method");
1562
+ };
1563
+ const pythonQualifierRoot = (qualifier) => /^([A-Za-z_]\w*)/.exec(qualifier.split(".")[0] ?? qualifier)?.[1] ?? qualifier;
1564
+ const pythonProofImportBinding = (testRel, structure, local) => {
1565
+ const matches = structure.imports.filter((i) => i.local === local);
1566
+ if (matches.length === 0)
1567
+ return undefined;
1568
+ if (matches.length > 1)
1569
+ return null;
1570
+ const binding = matches[0];
1571
+ if (!binding)
1572
+ return null;
1573
+ const targetRel = resolvePythonImport(testRel, binding.module) ?? undefined;
1574
+ return { targetRel, imported: binding.imported, kind: binding.kind };
1575
+ };
1576
+ const resolvePythonProofTarget = (testRel, structure, qualifier, callee, shadowed) => {
1577
+ const conv = conventionSibling(testRel, "python", codeFileSet);
1578
+ const targetRel = conv?.relPath ?? null;
1579
+ if (!targetRel || isNonProductFile(targetRel))
1580
+ return null;
1581
+ const hasWildcardImport = structure.imports.some((i) => i.imported === "*");
1582
+ if (!qualifier) {
1583
+ if (shadowed.has(callee))
1584
+ return null;
1585
+ const binding = pythonProofImportBinding(testRel, structure, callee);
1586
+ if (binding === null)
1587
+ return null;
1588
+ if (binding) {
1589
+ if (binding.kind !== "named" || binding.targetRel !== targetRel || binding.imported !== callee)
1590
+ return null;
1591
+ return eligiblePythonSymbol(targetRel, callee);
1592
+ }
1593
+ if (hasWildcardImport)
1594
+ return null;
1595
+ return eligiblePythonSymbol(targetRel, callee);
1596
+ }
1597
+ const rootQualifier = pythonQualifierRoot(qualifier);
1598
+ if (shadowed.has(rootQualifier))
1599
+ return null;
1600
+ const binding = pythonProofImportBinding(testRel, structure, rootQualifier);
1601
+ if (binding === null)
1602
+ return null;
1603
+ if (binding?.kind === "module") {
1604
+ return binding.targetRel === targetRel ? eligiblePythonSymbol(targetRel, callee) : null;
1605
+ }
1606
+ if (binding?.kind === "named" && (binding.targetRel !== targetRel || binding.imported !== rootQualifier))
1607
+ return null;
1608
+ if (symbolKind(targetRel, rootQualifier) !== "class")
1609
+ return null;
1610
+ return eligiblePythonSymbol(targetRel, callee);
1611
+ };
1612
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
1613
+ for (const i of structure.imports) {
1614
+ let targetRel = null;
1615
+ let targetDir = null;
1616
+ if (language === "java")
1617
+ targetRel = javaClassByFqn.get(i.module) ?? null;
1618
+ else if (language === "python")
1619
+ targetRel = resolvePythonImport(rel, i.module);
1620
+ else if (language === "go") {
1621
+ targetDir = resolveGoImportDir(rel, i.module);
1622
+ const filesInPackage = targetDir ? (goFilesByDir.get(targetDir) ?? []).filter((f) => !isNonProductFile(f)) : [];
1623
+ targetRel = filesInPackage.length === 1 ? filesInPackage[0] : null;
1624
+ }
1625
+ else if (language === "ruby")
1626
+ targetRel = i.module.startsWith("./") ? resolveRelativeModule(rel, i.module, [".rb"]) : null;
1627
+ else if (language === "kotlin")
1628
+ targetRel = uniqueMapTarget(kotlinSymbolByFqn, i.module);
1629
+ else if (language === "rust")
1630
+ targetRel = resolveRustImport(rel, i.module);
1631
+ else if (language === "php")
1632
+ targetRel = phpClassByFqn.get(i.module) ?? null;
1633
+ else if (language === "csharp") {
1634
+ addMulti(csharpUsingNamespacesByFile, rel, i.module);
1635
+ targetRel = uniqueMapTarget(csharpFilesByNamespace, i.module);
1636
+ }
1637
+ else if (language === "c" || language === "cpp")
1638
+ targetRel = resolveRelativeModule(rel, i.module, [".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx"]);
1639
+ if (!targetRel && !targetDir)
1640
+ continue;
1641
+ if (targetRel)
1642
+ emitImport(rel, targetRel);
1643
+ addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind);
1644
+ }
1645
+ }
1646
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
1647
+ const localSyms = symbolsByFile.get(rel);
1648
+ if (!localSyms)
1649
+ continue;
1650
+ const imports = importBindingsByFile.get(rel);
1651
+ const sameFileKotlinTopLevelFunctions = language === "kotlin"
1652
+ ? new Set((structure.topLevelSymbols ?? []).filter((name) => nodeByExternalId.get(`sym:${rel}#${name}`)?.properties.symbol_kind === "function"))
1653
+ : undefined;
1654
+ for (const c of structure.calls) {
1655
+ if (!localSyms.has(c.caller))
1656
+ continue;
1657
+ const callerId = `sym:${rel}#${c.caller}`;
1658
+ const shadowed = new Set(c.shadowed);
1659
+ if (c.via === "free") {
1660
+ if (shadowed.has(c.callee))
1661
+ continue;
1662
+ if (sameFileKotlinTopLevelFunctions?.has(c.callee)) {
1663
+ emitCall(callerId, `sym:${rel}#${c.callee}`, rel);
1664
+ continue;
1665
+ }
1666
+ if (language === "python" && localSyms.has(c.callee))
1667
+ continue;
1668
+ const b = imports?.get(c.callee);
1669
+ if (language === "php" && b?.targetRel && b.imported && symbolKind(b.targetRel, b.imported) !== "function")
1670
+ continue;
1671
+ if (b?.kind === "named" && b.imported && b.targetRel && symbolsByFile.get(b.targetRel)?.has(b.imported)) {
1672
+ emitCall(callerId, `sym:${b.targetRel}#${b.imported}`, rel);
1673
+ }
1674
+ }
1675
+ else if (c.qualifier) {
1676
+ const rootQualifier = c.qualifier.split(".")[0];
1677
+ if (shadowed.has(rootQualifier))
1678
+ continue;
1679
+ const b = imports?.get(rootQualifier);
1680
+ if (language === "go" && b?.kind === "module" && b.targetDir) {
1681
+ const targetRel = uniqueGoPackageSymbol(b.targetDir, c.callee);
1682
+ if (targetRel)
1683
+ emitCall(callerId, `sym:${targetRel}#${c.callee}`, rel);
1684
+ }
1685
+ else if (language === "rust") {
1686
+ const targetRel = uniqueRustModuleTarget(rel, c.qualifier) ?? (b?.kind === "module" ? b.targetRel : undefined);
1687
+ if (targetRel && symbolsByFile.get(targetRel)?.has(c.callee))
1688
+ emitCall(callerId, `sym:${targetRel}#${c.callee}`, rel);
1689
+ }
1690
+ else if (language === "php" && b?.kind === "named") {
1691
+ const targetRel = guardedMemberTarget(b.targetRel, rootQualifier, c.callee);
1692
+ if (targetRel)
1693
+ emitCall(callerId, `sym:${targetRel}#${c.callee}`, rel);
1694
+ }
1695
+ else if (language === "csharp") {
1696
+ const targetRel = guardedCsharpMemberTarget(rel, rootQualifier, c.callee);
1697
+ if (targetRel)
1698
+ emitCall(callerId, `sym:${targetRel}#${c.callee}`, rel);
1699
+ }
1700
+ else if (b?.kind === "module" && b.targetRel && symbolsByFile.get(b.targetRel)?.has(c.callee)) {
1701
+ emitCall(callerId, `sym:${b.targetRel}#${c.callee}`, rel);
1702
+ }
1703
+ else if (language === "java" && b?.kind === "named" && b.targetRel && symbolsByFile.get(b.targetRel)?.has(c.callee)) {
1704
+ emitCall(callerId, `sym:${b.targetRel}#${c.callee}`, rel);
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+ const seenGoProof = new Set();
1710
+ const proofVerifiedAt = scanStartMs;
1711
+ for (const [testRel, structure] of goTestStructureByFile) {
1712
+ const testExternalId = `test:${testRel}`;
1713
+ for (const proof of structure.goProofCalls ?? []) {
1714
+ goProofAttempted++;
1715
+ const symId = resolveGoProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed));
1716
+ if (!symId)
1717
+ continue;
1718
+ const edgeKey = `${testExternalId}|${symId}`;
1719
+ if (seenGoProof.has(edgeKey))
1720
+ continue;
1721
+ seenGoProof.add(edgeKey);
1722
+ goProofConfirmedPairs++;
1723
+ edges.push(makeEdge({
1724
+ from_external_id: symId,
1725
+ to_external_id: testExternalId,
1726
+ relationship_type: "TESTED_BY",
1727
+ evidence_strength: "hard",
1728
+ review_status: "auto_detected",
1729
+ provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
1730
+ last_verified: proofVerifiedAt
1731
+ }));
1732
+ edges.push(makeEdge({
1733
+ from_external_id: testExternalId,
1734
+ to_external_id: symId,
1735
+ relationship_type: "COVERS",
1736
+ evidence_strength: "hard",
1737
+ review_status: "auto_detected",
1738
+ provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
1739
+ last_verified: proofVerifiedAt
1740
+ }));
1741
+ }
1742
+ }
1743
+ const seenJavaProof = new Set();
1744
+ for (const [testRel, structure] of javaTestStructureByFile) {
1745
+ const testExternalId = `test:${testRel}`;
1746
+ for (const proof of structure.javaProofCalls ?? []) {
1747
+ javaProofAttempted++;
1748
+ const symId = resolveJavaProofTarget(structure, proof);
1749
+ if (!symId)
1750
+ continue;
1751
+ const edgeKey = `${testExternalId}|${symId}`;
1752
+ if (seenJavaProof.has(edgeKey))
1753
+ continue;
1754
+ seenJavaProof.add(edgeKey);
1755
+ javaProofConfirmedPairs++;
1756
+ edges.push(...makeProofEdges({
1757
+ testRel,
1758
+ symId,
1759
+ provenance: prov(testRel, hashString(`${symId}:${proof.assertion}:${proof.className}.${proof.callee}`)),
1760
+ lastVerified: proofVerifiedAt
1761
+ }));
1762
+ }
1763
+ }
1764
+ const seenPythonProof = new Set();
1765
+ for (const [testRel, structure] of pythonTestStructureByFile) {
1766
+ const testExternalId = `test:${testRel}`;
1767
+ for (const proof of structure.pythonProofCalls ?? []) {
1768
+ pythonProofAttempted++;
1769
+ const symId = resolvePythonProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed));
1770
+ if (!symId)
1771
+ continue;
1772
+ const edgeKey = `${testExternalId}|${symId}`;
1773
+ if (seenPythonProof.has(edgeKey))
1774
+ continue;
1775
+ seenPythonProof.add(edgeKey);
1776
+ pythonProofConfirmedPairs++;
1777
+ edges.push(...makeProofEdges({
1778
+ testRel,
1779
+ symId,
1780
+ provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
1781
+ lastVerified: proofVerifiedAt
1782
+ }));
1783
+ }
1784
+ }
1785
+ }
1786
+ // ---- Static assertion candidates (Phase 4 / Gate 2): TypeChecker-derived hard edges ----
1787
+ // Upgrade resolver-derived test->source candidates to HARD TESTED_BY/COVERS
1788
+ // ONLY where the 5-conjunct confirmer proves a runtime, asserted use of a real
1789
+ // exported binding. These edges are static candidate evidence for association
1790
+ // and diagnostics only; public Proven is minted solely from dynamic targeted
1791
+ // proof records in the metadata-only ledger. Stem-derived links are NEVER
1792
+ // confirmation inputs. (v1 narrow scope: a candidate whose target is a re-export
1793
+ // barrel — which holds no LOCAL behavior symbols — is not confirmed here;
1794
+ // barrel-routed confirmation is a documented follow-up. Under-confirming is the
1795
+ // desired failure mode.)
1796
+ let confirmedCoverage;
1797
+ if (readContent && eligibleSymbolsByFile.size > 0) {
1798
+ const absByRel = new Map(resolveFiles.map((f) => [f.rel, f.abs]));
1799
+ const candidates = [];
1800
+ const seenPair = new Set();
1801
+ for (const e of candidate_edges) {
1802
+ // resolver-derived MAY_RELATE_TO only (evidence_strength "candidate"); the
1803
+ // weak stem fallback has not been emitted yet, and would be excluded anyway.
1804
+ if (e.relationship_type !== "MAY_RELATE_TO" || e.evidence_strength !== "candidate")
1805
+ continue;
1806
+ const testRel = e.from_external_id;
1807
+ const implRel = e.to_external_id;
1808
+ if (!eligibleSymbolsByFile.has(implRel))
1809
+ continue;
1810
+ const testAbs = absByRel.get(testRel);
1811
+ const implAbs = absByRel.get(implRel);
1812
+ if (!testAbs || !implAbs)
1813
+ continue;
1814
+ const key = `${testRel}|${implRel}`;
1815
+ if (seenPair.has(key))
1816
+ continue;
1817
+ seenPair.add(key);
1818
+ candidates.push({ testRel, testAbs, implRel, implAbs });
1819
+ }
1820
+ const confirmBudget = Math.max(1, Number(process.env.ORANGEPRO_MAX_CONFIRM_FILES) || 1500);
1821
+ const riskSymbolLimit = Math.max(1, Number(process.env.ORANGEPRO_CONFIRM_RISK_SYMBOLS) || DEFAULT_CONFIRM_RISK_SYMBOLS);
1822
+ const involved = new Set();
1823
+ for (const c of candidates) {
1824
+ involved.add(c.testAbs);
1825
+ involved.add(c.implAbs);
1826
+ }
1827
+ if (candidates.length === 0) {
1828
+ confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
1829
+ }
1830
+ else
1831
+ try {
1832
+ const scoped = involved.size > confirmBudget
1833
+ ? selectRiskScopedConfirmCandidates({
1834
+ candidates,
1835
+ nodes,
1836
+ edges,
1837
+ candidate_edges,
1838
+ root,
1839
+ confirmBudget,
1840
+ riskSymbolLimit,
1841
+ eligibleSymbolsByFile
1842
+ })
1843
+ : { candidates, riskSymbols: 0, involvedFiles: involved.size };
1844
+ if (involved.size > confirmBudget && scoped.candidates.length === 0) {
1845
+ confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: involved.size };
1846
+ 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.`);
1847
+ }
1848
+ else {
1849
+ const result = runConfirmer({ candidates: scoped.candidates, symbolsByImpl: eligibleSymbolsByFile, existingSymIds: codeSymbolIds, anchorFile: root });
1850
+ const proofVerifiedAt = scanStartMs;
1851
+ const seenEdge = new Set();
1852
+ let confirmedPairs = 0;
1853
+ for (const c of result.confirmations) {
1854
+ const edgeKey = `test:${c.testRel}|${c.symId}`;
1855
+ if (seenEdge.has(edgeKey))
1856
+ continue;
1857
+ seenEdge.add(edgeKey);
1858
+ confirmedPairs++;
1859
+ edges.push(...makeProofEdges({
1860
+ testRel: c.testRel,
1861
+ symId: c.symId,
1862
+ provenance: prov(c.testRel, hashString(c.symId)),
1863
+ lastVerified: proofVerifiedAt
1864
+ }));
1865
+ }
1866
+ confirmedCoverage = {
1867
+ confirmed_pairs: confirmedPairs,
1868
+ attempted: result.attempted,
1869
+ capped_downgrades: result.capped_downgrades,
1870
+ skipped_files_budget: involved.size > confirmBudget ? involved.size : 0,
1871
+ ...(involved.size > confirmBudget
1872
+ ? {
1873
+ scoped_by_risk: {
1874
+ candidate_pairs: scoped.candidates.length,
1875
+ involved_files: scoped.involvedFiles,
1876
+ risk_symbols: scoped.riskSymbols,
1877
+ risk_symbol_limit: riskSymbolLimit,
1878
+ file_budget: confirmBudget
1879
+ }
1880
+ }
1881
+ : {})
1882
+ };
1883
+ if (involved.size > confirmBudget) {
1884
+ warnings.push(`Static confirmation scoped: ${involved.size} files exceed the confirmer budget (${confirmBudget}); ran ${scoped.candidates.length} candidate pair(s) from the top ${scoped.riskSymbols} risk-ranked symbol(s). Public Proven still requires dynamic targeted proof.`);
1885
+ }
1886
+ }
1887
+ }
1888
+ catch (err) {
1889
+ // The confirmer is best-effort: an exotic tsconfig (composite/references)
1890
+ // or a TS compiler-API edge must NEVER crash analyze. Degrade to
1891
+ // candidate-only coverage with a disclosure.
1892
+ const msg = err instanceof Error ? err.message : String(err);
1893
+ confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
1894
+ 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.`);
1895
+ }
1896
+ }
1897
+ const nonTsHardProofAttempted = goProofAttempted + javaProofAttempted + pythonProofAttempted;
1898
+ if (nonTsHardProofAttempted > 0) {
1899
+ confirmedCoverage = {
1900
+ confirmed_pairs: (confirmedCoverage?.confirmed_pairs ?? 0) + goProofConfirmedPairs + javaProofConfirmedPairs + pythonProofConfirmedPairs,
1901
+ attempted: (confirmedCoverage?.attempted ?? 0) + nonTsHardProofAttempted,
1902
+ capped_downgrades: confirmedCoverage?.capped_downgrades ?? 0,
1903
+ skipped_files_budget: confirmedCoverage?.skipped_files_budget ?? 0
1904
+ };
1905
+ }
1906
+ // Link remaining test files to a source sibling — SECONDARY, for files the
1907
+ // resolver cannot link (non-TS/JS languages, no resolvable imports). A naming/
1908
+ // path convention, so candidate/weak (never-proof); skipped entirely for any
1909
+ // test file the import graph already linked.
1910
+ //
1911
+ // (a) PER-LANGUAGE CONVENTION (Go `_test.go`, JVM `FooTest`↔`Foo` src/test→
1912
+ // src/main mirror, Python `test_x.py`↔`x.py` tests/ mirror): predict the
1913
+ // exact sibling path and verify it was scanned. High precision, so it wins
1914
+ // over the coarse global stem match. (b) GLOBAL STEM fallback otherwise.
1915
+ for (const t of testFileStems) {
1916
+ if (importResolvedTests.has(t.relPath))
1917
+ continue;
1918
+ const language = languageOf(t.relPath);
1919
+ const conv = conventionSibling(t.relPath, language, codeFileSet);
1920
+ if (conv && !isTestSupportPath(conv.relPath) && !isNonProductFile(conv.relPath)) {
1921
+ candidate_edges.push(makeCandidateEdge({
1922
+ from_external_id: t.relPath,
1923
+ to_external_id: conv.relPath,
1924
+ relationship_type: "MAY_RELATE_TO",
1925
+ evidence_strength: "weak",
1926
+ reason: conv.reason,
1927
+ confidence: conv.confidence,
1928
+ provenance: prov(t.relPath)
1929
+ }));
1930
+ continue;
1931
+ }
1932
+ // For a language with a strong test convention, a non-matching file is a
1933
+ // test-tree helper/shadow, not a behavior test (e.g. src/test/.../Owner.java
1934
+ // with no Test suffix). Under-link rather than resurrect the coarse global
1935
+ // stem matcher's cross-file false links. The stem fallback below stays for
1936
+ // TS/JS (resolver-missed) and unsupported languages only.
1937
+ if (isConventionLanguage(language))
1938
+ continue;
1939
+ const candidates = codeFilesByStem.get(t.stem);
1940
+ if (!candidates || candidates.length === 0)
1941
+ continue;
1942
+ const sameDir = candidates.filter((c) => c.dir === t.dir);
1943
+ let target;
1944
+ let confidence = 0;
1945
+ if (sameDir.length === 1) {
1946
+ target = sameDir[0];
1947
+ confidence = 0.6;
1948
+ }
1949
+ else if (sameDir.length === 0 && candidates.length === 1) {
1950
+ target = candidates[0];
1951
+ confidence = 0.4;
1952
+ }
1953
+ else {
1954
+ continue; // ambiguous (0 or >1 plausible sources) — do not guess
1955
+ }
1956
+ if (isTestSupportPath(target.relPath) || isNonProductFile(target.relPath))
1957
+ continue; // helper/generated file named like the behavior is not coverage
1958
+ candidate_edges.push(makeCandidateEdge({
1959
+ from_external_id: t.relPath,
1960
+ to_external_id: target.relPath,
1961
+ relationship_type: "MAY_RELATE_TO",
1962
+ evidence_strength: "weak",
1963
+ reason: `Test and source share the basename stem "${t.stem}"`,
1964
+ confidence,
1965
+ provenance: prov(t.relPath)
1966
+ }));
1967
+ }
1968
+ // Ensure a pytest framework node when Python tests/conftest exist but no
1969
+ // explicit pytest config/dep was found (Python repos that rely on defaults).
1970
+ if (sawPythonTest && !frameworkIds.has("framework:pytest")) {
1971
+ addFramework("pytest", "test", "unit", pytestRef || repoScopeId);
1972
+ }
1973
+ // Semantic association pass: test names are useful "Associated" evidence when
1974
+ // file-stem/import heuristics miss cross-file behavior tests. This is still
1975
+ // weak candidate evidence only: it never mints hard COVERS/TESTED_BY proof.
1976
+ const existingSemanticTestLinks = new Set(candidate_edges
1977
+ .filter((e) => e.relationship_type === "MAY_BE_TESTED_BY" || e.relationship_type === "MAY_COVER")
1978
+ .map((e) => `${e.from_external_id}|${e.to_external_id}`));
1979
+ const semanticTargets = nodes.filter((n) => n.kind === "UserFlow" || (n.kind === "CodeSymbol" && n.denominator_eligible === true));
1980
+ const testNodes = nodes.filter((n) => n.kind === "TestCase");
1981
+ if (semanticTargets.length > 0 && testNodes.length > 0) {
1982
+ const testTokensById = new Map();
1983
+ const testsByToken = new Map();
1984
+ for (const t of testNodes) {
1985
+ const names = Array.isArray(t.properties.test_names) ? t.properties.test_names.map(String).join(" ") : "";
1986
+ const tokens = textTokens(`${t.title ?? ""} ${names} ${t.properties.file ?? ""}`);
1987
+ testTokensById.set(t.external_id, tokens);
1988
+ for (const token of tokens) {
1989
+ const bucket = testsByToken.get(token);
1990
+ if (bucket)
1991
+ bucket.add(t);
1992
+ else
1993
+ testsByToken.set(token, new Set([t]));
1994
+ }
1995
+ }
1996
+ for (const target of semanticTargets) {
1997
+ const examples = Array.isArray(target.properties.example_behaviors) ? target.properties.example_behaviors.map(String).join(" ") : "";
1998
+ const targetTokens = textTokens(`${target.title ?? ""} ${examples} ${target.properties.feature ?? ""} ${target.properties.area ?? ""} ${target.properties.file ?? ""}`);
1999
+ const candidateTests = new Set();
2000
+ for (const token of targetTokens)
2001
+ for (const test of testsByToken.get(token) ?? [])
2002
+ candidateTests.add(test);
2003
+ const matches = [...candidateTests]
2004
+ .map((test) => ({ test, score: tokenJaccard(targetTokens, testTokensById.get(test.external_id) ?? new Set()) }))
2005
+ .filter((m) => m.score >= 0.28)
2006
+ .sort((a, b) => b.score - a.score || a.test.external_id.localeCompare(b.test.external_id))
2007
+ .slice(0, 3);
2008
+ for (const { test, score } of matches) {
2009
+ const key = `${target.external_id}|${test.external_id}`;
2010
+ if (existingSemanticTestLinks.has(key))
2011
+ continue;
2012
+ existingSemanticTestLinks.add(key);
2013
+ candidate_edges.push(makeCandidateEdge({
2014
+ from_external_id: target.external_id,
2015
+ to_external_id: test.external_id,
2016
+ relationship_type: "MAY_BE_TESTED_BY",
2017
+ evidence_strength: "weak",
2018
+ reason: "Token overlap between behavior name/title and test names; associated only, never proof.",
2019
+ confidence: Math.round(score * 100) / 100,
2020
+ provenance: prov(target.provenance.source_ref ?? repoScopeId)
2021
+ }));
2022
+ }
2023
+ }
2024
+ }
2025
+ if (files.length === 0) {
2026
+ warnings.push("No analyzable files found. Check the path and .orangeproignore rules.");
2027
+ }
2028
+ if (flowsTruncated > 0) {
2029
+ warnings.push(`Inferred-behavior cap (${maxFlows}) reached; ${flowsTruncated} test file(s) were not turned into behavior anchors. Lower ORANGEPRO_MAX_FLOWS only to bound a run; the default scans all.`);
2030
+ }
2031
+ if (symbolFilesTruncated > 0) {
2032
+ warnings.push(`${symbolFilesTruncated} file(s) exceeded the per-file symbol cap (${MAX_SYMBOLS_PER_FILE} exports); exports beyond the cap are NOT in the coverage denominator.`);
2033
+ }
2034
+ if (filesCapHit) {
2035
+ warnings.push(`File-scan cap (${maxFiles}) reached; some files were not scanned. Raise ORANGEPRO_MAX_FILES to include them.`);
2036
+ }
2037
+ const filesNotAnalyzed = files.length - filesProcessed;
2038
+ const elapsedMs = now() - scanStartMs;
2039
+ if (budgetStopped) {
2040
+ warnings.push(`Analyze budget (${budgetMs}ms) reached after ${filesProcessed}/${files.length} files; ${filesNotAnalyzed} file(s) were NOT analyzed. Coverage is over a PARTIAL scan (a floor, not a complete headline). Raise ORANGEPRO_MAX_ANALYZE_MS, or scope with --base.`);
2041
+ }
2042
+ // tree-sitter downgrade disclosure: a configured grammar that FAILED to load (vs
2043
+ // simply never preloaded) means extraction silently fell back to the shallow regex
2044
+ // path for that language — surface it instead of serving a quietly-undercounted
2045
+ // denominator (Codex PR-1 HIGH). Only warns for languages actually present.
2046
+ const tsStatus = treeSitterStatus();
2047
+ let treeSitterDowngraded = [];
2048
+ if (tsStatus.failed.length > 0) {
2049
+ const codeLangs = new Set();
2050
+ for (const n of nodes) {
2051
+ if (n.kind === "File" && n.properties.role === "code" && typeof n.properties.language === "string") {
2052
+ codeLangs.add(n.properties.language);
2053
+ }
2054
+ }
2055
+ treeSitterDowngraded = tsStatus.failed.filter((l) => codeLangs.has(l));
2056
+ if (treeSitterDowngraded.length > 0) {
2057
+ warnings.push(`tree-sitter grammar(s) failed to load for ${treeSitterDowngraded.join(", ")} — symbol extraction fell back to a SHALLOW regex path, so the coverage denominator for ${treeSitterDowngraded.join("/")} is undercounted. Reinstall the package (grammars ship with it) for accurate extraction.`);
2058
+ }
2059
+ }
2060
+ // Directories with no graph evidence at all (only non-code/test/config/doc files)
2061
+ // and a meaningful file count — the safest things to add to .orangeproignore to
2062
+ // speed up and de-noise analysis. Suggest-only; never auto-excluded.
2063
+ const NOISE_DIR_MIN = 25;
2064
+ const exclude_suggestions = [...dirTally.entries()]
2065
+ .filter(([dir, c]) => dir !== "(root)" && c.useful === 0 && c.other >= NOISE_DIR_MIN)
2066
+ .sort((a, b) => b[1].other - a[1].other)
2067
+ .slice(0, 10)
2068
+ .map(([path, c]) => ({ path, files: c.other, reason: `${c.other} files, none with code/test/config/doc evidence` }));
2069
+ const runtimeCoverage = applyRuntimeCoverage(root, files, nodes, goModulesByDir);
2070
+ const structuralClusters = buildStructuralClusters(nodes, edges, candidate_edges);
2071
+ const flows = enumerateFlows({ nodes, edges, candidate_edges, workspaceRoot: root });
2072
+ const analysis = {
2073
+ test_files: testFiles,
2074
+ inferred_flows: flowIds.size,
2075
+ flows_truncated: flowsTruncated,
2076
+ max_inferred_flows: maxFlows,
2077
+ symbol_cap_hit: symbolCapHit,
2078
+ symbol_files_truncated: symbolFilesTruncated,
2079
+ excluded_boilerplate: excludedBoilerplate,
2080
+ files_scanned: filesProcessed,
2081
+ files_cap_hit: filesCapHit,
2082
+ max_files: maxFiles,
2083
+ ...(budgetStopped
2084
+ ? { not_analyzed_due_to_budget: { files_not_analyzed: filesNotAnalyzed, elapsed_ms: elapsedMs, budget_ms: budgetMs } }
2085
+ : {}),
2086
+ ...(opts.parseCache
2087
+ ? { parse_cache: { hits: opts.parseCache.hits, misses: opts.parseCache.misses, hit_rate: opts.parseCache.hitRate() } }
2088
+ : {}),
2089
+ ...(opts.resolverCache
2090
+ ? { resolver_cache: { hits: opts.resolverCache.hits, misses: opts.resolverCache.misses, hit_rate: opts.resolverCache.hitRate() } }
2091
+ : {}),
2092
+ exclude_suggestions,
2093
+ ...(behaviorContractsTotal
2094
+ ? {
2095
+ behavior_contracts: {
2096
+ total: behaviorContractsTotal,
2097
+ by_framework: Object.fromEntries([...behaviorContractsByFramework.entries()].sort(([a], [b]) => a.localeCompare(b))),
2098
+ by_kind: Object.fromEntries([...behaviorContractsByKind.entries()].sort(([a], [b]) => a.localeCompare(b))),
2099
+ handler_edges: behaviorContractsHandlerEdges
2100
+ }
2101
+ }
2102
+ : {}),
2103
+ flows,
2104
+ structural_clusters: structuralClusters,
2105
+ ...(tsStatus.loaded.length || tsStatus.failed.length
2106
+ ? { tree_sitter: { loaded: tsStatus.loaded, failed: tsStatus.failed, downgraded: treeSitterDowngraded } }
2107
+ : {}),
2108
+ ...(confirmedCoverage ? { confirmed_coverage: confirmedCoverage } : {}),
2109
+ ...(runtimeCoverage ? { runtime_coverage: runtimeCoverage } : {}),
2110
+ ...(resolverMetrics
2111
+ ? {
2112
+ resolver_metrics: resolverMetrics,
2113
+ resolver_gate: {
2114
+ axis: "test_to_source",
2115
+ threshold_pct: 80,
2116
+ pct: resolverMetrics.test_to_source.pct,
2117
+ // Raw-count ratio (display pct rounds, e.g. 79.96 -> 80.0) AND a
2118
+ // complete scan: a files-cap-truncated OR budget-stopped run measured
2119
+ // an unknown fraction of the repo, so it can never be declared defensible.
2120
+ defensible: !filesCapHit &&
2121
+ !budgetStopped &&
2122
+ resolverMetrics.test_to_source.n > 0 &&
2123
+ resolverMetrics.test_to_source.resolved / resolverMetrics.test_to_source.n >= 0.8
2124
+ }
2125
+ }
2126
+ : {})
2127
+ };
2128
+ return { nodes, edges, candidate_edges, sources: [source], warnings, file_entries, analysis };
2129
+ }