seer-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 (371) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/LICENSE +176 -0
  3. package/README.md +272 -0
  4. package/README_dev.md +199 -0
  5. package/dist/bundle/ci.d.ts +47 -0
  6. package/dist/bundle/ci.d.ts.map +1 -0
  7. package/dist/bundle/ci.js +113 -0
  8. package/dist/bundle/ci.js.map +1 -0
  9. package/dist/bundle/contract.d.ts +111 -0
  10. package/dist/bundle/contract.d.ts.map +1 -0
  11. package/dist/bundle/contract.js +352 -0
  12. package/dist/bundle/contract.js.map +1 -0
  13. package/dist/bundle/export.d.ts +36 -0
  14. package/dist/bundle/export.d.ts.map +1 -0
  15. package/dist/bundle/export.js +152 -0
  16. package/dist/bundle/export.js.map +1 -0
  17. package/dist/bundle/external.d.ts +66 -0
  18. package/dist/bundle/external.d.ts.map +1 -0
  19. package/dist/bundle/external.js +238 -0
  20. package/dist/bundle/external.js.map +1 -0
  21. package/dist/bundle/format.d.ts +94 -0
  22. package/dist/bundle/format.d.ts.map +1 -0
  23. package/dist/bundle/format.js +42 -0
  24. package/dist/bundle/format.js.map +1 -0
  25. package/dist/bundle/import.d.ts +49 -0
  26. package/dist/bundle/import.d.ts.map +1 -0
  27. package/dist/bundle/import.js +116 -0
  28. package/dist/bundle/import.js.map +1 -0
  29. package/dist/cli/index.d.ts +3 -0
  30. package/dist/cli/index.d.ts.map +1 -0
  31. package/dist/cli/index.js +1402 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/cli/init.d.ts +48 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +284 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/db/schema.d.ts +3 -0
  38. package/dist/db/schema.d.ts.map +1 -0
  39. package/dist/db/schema.js +616 -0
  40. package/dist/db/schema.js.map +1 -0
  41. package/dist/db/store.d.ts +1011 -0
  42. package/dist/db/store.d.ts.map +1 -0
  43. package/dist/db/store.js +3888 -0
  44. package/dist/db/store.js.map +1 -0
  45. package/dist/graph/pagerank.d.ts +9 -0
  46. package/dist/graph/pagerank.d.ts.map +1 -0
  47. package/dist/graph/pagerank.js +47 -0
  48. package/dist/graph/pagerank.js.map +1 -0
  49. package/dist/indexer/architecture.d.ts +72 -0
  50. package/dist/indexer/architecture.d.ts.map +1 -0
  51. package/dist/indexer/architecture.js +112 -0
  52. package/dist/indexer/architecture.js.map +1 -0
  53. package/dist/indexer/behavior.d.ts +75 -0
  54. package/dist/indexer/behavior.d.ts.map +1 -0
  55. package/dist/indexer/behavior.js +395 -0
  56. package/dist/indexer/behavior.js.map +1 -0
  57. package/dist/indexer/boundaries.d.ts +60 -0
  58. package/dist/indexer/boundaries.d.ts.map +1 -0
  59. package/dist/indexer/boundaries.js +366 -0
  60. package/dist/indexer/boundaries.js.map +1 -0
  61. package/dist/indexer/churn.d.ts +15 -0
  62. package/dist/indexer/churn.d.ts.map +1 -0
  63. package/dist/indexer/churn.js +49 -0
  64. package/dist/indexer/churn.js.map +1 -0
  65. package/dist/indexer/classify.d.ts +9 -0
  66. package/dist/indexer/classify.d.ts.map +1 -0
  67. package/dist/indexer/classify.js +90 -0
  68. package/dist/indexer/classify.js.map +1 -0
  69. package/dist/indexer/context.d.ts +176 -0
  70. package/dist/indexer/context.d.ts.map +1 -0
  71. package/dist/indexer/context.js +193 -0
  72. package/dist/indexer/context.js.map +1 -0
  73. package/dist/indexer/continuity.d.ts +67 -0
  74. package/dist/indexer/continuity.d.ts.map +1 -0
  75. package/dist/indexer/continuity.js +288 -0
  76. package/dist/indexer/continuity.js.map +1 -0
  77. package/dist/indexer/detectchanges.d.ts +32 -0
  78. package/dist/indexer/detectchanges.d.ts.map +1 -0
  79. package/dist/indexer/detectchanges.js +74 -0
  80. package/dist/indexer/detectchanges.js.map +1 -0
  81. package/dist/indexer/discovery.d.ts +37 -0
  82. package/dist/indexer/discovery.d.ts.map +1 -0
  83. package/dist/indexer/discovery.js +136 -0
  84. package/dist/indexer/discovery.js.map +1 -0
  85. package/dist/indexer/externaldeps.d.ts +18 -0
  86. package/dist/indexer/externaldeps.d.ts.map +1 -0
  87. package/dist/indexer/externaldeps.js +288 -0
  88. package/dist/indexer/externaldeps.js.map +1 -0
  89. package/dist/indexer/freshness.d.ts +48 -0
  90. package/dist/indexer/freshness.d.ts.map +1 -0
  91. package/dist/indexer/freshness.js +128 -0
  92. package/dist/indexer/freshness.js.map +1 -0
  93. package/dist/indexer/git.d.ts +144 -0
  94. package/dist/indexer/git.d.ts.map +1 -0
  95. package/dist/indexer/git.js +444 -0
  96. package/dist/indexer/git.js.map +1 -0
  97. package/dist/indexer/index.d.ts +145 -0
  98. package/dist/indexer/index.d.ts.map +1 -0
  99. package/dist/indexer/index.js +930 -0
  100. package/dist/indexer/index.js.map +1 -0
  101. package/dist/indexer/modules.d.ts +62 -0
  102. package/dist/indexer/modules.d.ts.map +1 -0
  103. package/dist/indexer/modules.js +293 -0
  104. package/dist/indexer/modules.js.map +1 -0
  105. package/dist/indexer/preflight.d.ts +154 -0
  106. package/dist/indexer/preflight.d.ts.map +1 -0
  107. package/dist/indexer/preflight.js +399 -0
  108. package/dist/indexer/preflight.js.map +1 -0
  109. package/dist/indexer/protoScanner.d.ts +34 -0
  110. package/dist/indexer/protoScanner.d.ts.map +1 -0
  111. package/dist/indexer/protoScanner.js +133 -0
  112. package/dist/indexer/protoScanner.js.map +1 -0
  113. package/dist/indexer/risk.d.ts +115 -0
  114. package/dist/indexer/risk.d.ts.map +1 -0
  115. package/dist/indexer/risk.js +194 -0
  116. package/dist/indexer/risk.js.map +1 -0
  117. package/dist/indexer/serviceHostScanner.d.ts +25 -0
  118. package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
  119. package/dist/indexer/serviceHostScanner.js +95 -0
  120. package/dist/indexer/serviceHostScanner.js.map +1 -0
  121. package/dist/indexer/serviceLinks.d.ts +105 -0
  122. package/dist/indexer/serviceLinks.d.ts.map +1 -0
  123. package/dist/indexer/serviceLinks.js +509 -0
  124. package/dist/indexer/serviceLinks.js.map +1 -0
  125. package/dist/indexer/shapehash.d.ts +98 -0
  126. package/dist/indexer/shapehash.d.ts.map +1 -0
  127. package/dist/indexer/shapehash.js +354 -0
  128. package/dist/indexer/shapehash.js.map +1 -0
  129. package/dist/indexer/skeleton.d.ts +15 -0
  130. package/dist/indexer/skeleton.d.ts.map +1 -0
  131. package/dist/indexer/skeleton.js +136 -0
  132. package/dist/indexer/skeleton.js.map +1 -0
  133. package/dist/indexer/symbolhistory.d.ts +41 -0
  134. package/dist/indexer/symbolhistory.d.ts.map +1 -0
  135. package/dist/indexer/symbolhistory.js +124 -0
  136. package/dist/indexer/symbolhistory.js.map +1 -0
  137. package/dist/indexer/watcher.d.ts +68 -0
  138. package/dist/indexer/watcher.d.ts.map +1 -0
  139. package/dist/indexer/watcher.js +179 -0
  140. package/dist/indexer/watcher.js.map +1 -0
  141. package/dist/mcp/server.d.ts +80 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +1610 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/parser/index.d.ts +8 -0
  146. package/dist/parser/index.d.ts.map +1 -0
  147. package/dist/parser/index.js +33 -0
  148. package/dist/parser/index.js.map +1 -0
  149. package/dist/parser/languages/cpp.d.ts +3 -0
  150. package/dist/parser/languages/cpp.d.ts.map +1 -0
  151. package/dist/parser/languages/cpp.js +350 -0
  152. package/dist/parser/languages/cpp.js.map +1 -0
  153. package/dist/parser/languages/csharp.d.ts +3 -0
  154. package/dist/parser/languages/csharp.d.ts.map +1 -0
  155. package/dist/parser/languages/csharp.js +239 -0
  156. package/dist/parser/languages/csharp.js.map +1 -0
  157. package/dist/parser/languages/go.d.ts +3 -0
  158. package/dist/parser/languages/go.d.ts.map +1 -0
  159. package/dist/parser/languages/go.js +259 -0
  160. package/dist/parser/languages/go.js.map +1 -0
  161. package/dist/parser/languages/java.d.ts +3 -0
  162. package/dist/parser/languages/java.d.ts.map +1 -0
  163. package/dist/parser/languages/java.js +391 -0
  164. package/dist/parser/languages/java.js.map +1 -0
  165. package/dist/parser/languages/python.d.ts +3 -0
  166. package/dist/parser/languages/python.d.ts.map +1 -0
  167. package/dist/parser/languages/python.js +396 -0
  168. package/dist/parser/languages/python.js.map +1 -0
  169. package/dist/parser/languages/rust.d.ts +3 -0
  170. package/dist/parser/languages/rust.d.ts.map +1 -0
  171. package/dist/parser/languages/rust.js +159 -0
  172. package/dist/parser/languages/rust.js.map +1 -0
  173. package/dist/parser/languages/typescript.d.ts +3 -0
  174. package/dist/parser/languages/typescript.d.ts.map +1 -0
  175. package/dist/parser/languages/typescript.js +1442 -0
  176. package/dist/parser/languages/typescript.js.map +1 -0
  177. package/dist/parser/parserContext.d.ts +77 -0
  178. package/dist/parser/parserContext.d.ts.map +1 -0
  179. package/dist/parser/parserContext.js +354 -0
  180. package/dist/parser/parserContext.js.map +1 -0
  181. package/dist/parser/walker.d.ts +81 -0
  182. package/dist/parser/walker.d.ts.map +1 -0
  183. package/dist/parser/walker.js +217 -0
  184. package/dist/parser/walker.js.map +1 -0
  185. package/dist/parser/worker.d.ts +66 -0
  186. package/dist/parser/worker.d.ts.map +1 -0
  187. package/dist/parser/worker.js +129 -0
  188. package/dist/parser/worker.js.map +1 -0
  189. package/dist/parser/workerpool.d.ts +107 -0
  190. package/dist/parser/workerpool.d.ts.map +1 -0
  191. package/dist/parser/workerpool.js +383 -0
  192. package/dist/parser/workerpool.js.map +1 -0
  193. package/dist/scip/format.d.ts +87 -0
  194. package/dist/scip/format.d.ts.map +1 -0
  195. package/dist/scip/format.js +31 -0
  196. package/dist/scip/format.js.map +1 -0
  197. package/dist/scip/import.d.ts +37 -0
  198. package/dist/scip/import.d.ts.map +1 -0
  199. package/dist/scip/import.js +180 -0
  200. package/dist/scip/import.js.map +1 -0
  201. package/dist/types.d.ts +392 -0
  202. package/dist/types.d.ts.map +1 -0
  203. package/dist/types.js +4 -0
  204. package/dist/types.js.map +1 -0
  205. package/docs/architecture.md +105 -0
  206. package/docs/benchmarks/methodology.md +134 -0
  207. package/docs/benchmarks/raw-results.md +71 -0
  208. package/docs/benchmarks.md +74 -0
  209. package/docs/cli.md +148 -0
  210. package/docs/examples/behavior-tests.md +70 -0
  211. package/docs/examples/change-history.md +85 -0
  212. package/docs/examples/pre-edit-context.md +81 -0
  213. package/docs/examples/service-links.md +88 -0
  214. package/docs/examples.md +80 -0
  215. package/docs/faq.md +70 -0
  216. package/docs/internals.md +104 -0
  217. package/docs/languages.md +70 -0
  218. package/docs/limits.md +52 -0
  219. package/docs/mcp.md +199 -0
  220. package/docs/quickstart.md +119 -0
  221. package/docs/testing.md +123 -0
  222. package/docs/tools.md +115 -0
  223. package/package.json +52 -0
  224. package/research-codebase.md +578 -0
  225. package/seer-cli-docs.md +326 -0
  226. package/seer-master-guide.md +246 -0
  227. package/src/bundle/ci.ts +141 -0
  228. package/src/bundle/contract.ts +387 -0
  229. package/src/bundle/export.ts +175 -0
  230. package/src/bundle/external.ts +285 -0
  231. package/src/bundle/format.ts +92 -0
  232. package/src/bundle/import.ts +157 -0
  233. package/src/cli/index.ts +1249 -0
  234. package/src/cli/init.ts +389 -0
  235. package/src/db/schema.ts +614 -0
  236. package/src/db/store.ts +4306 -0
  237. package/src/graph/pagerank.ts +53 -0
  238. package/src/indexer/architecture.ts +148 -0
  239. package/src/indexer/behavior.ts +466 -0
  240. package/src/indexer/boundaries.ts +374 -0
  241. package/src/indexer/churn.ts +58 -0
  242. package/src/indexer/classify.ts +96 -0
  243. package/src/indexer/context.ts +340 -0
  244. package/src/indexer/continuity.ts +322 -0
  245. package/src/indexer/detectchanges.ts +94 -0
  246. package/src/indexer/discovery.ts +176 -0
  247. package/src/indexer/externaldeps.ts +243 -0
  248. package/src/indexer/freshness.ts +166 -0
  249. package/src/indexer/git.ts +453 -0
  250. package/src/indexer/index.ts +1092 -0
  251. package/src/indexer/modules.ts +358 -0
  252. package/src/indexer/preflight.ts +548 -0
  253. package/src/indexer/protoScanner.ts +147 -0
  254. package/src/indexer/risk.ts +304 -0
  255. package/src/indexer/serviceHostScanner.ts +92 -0
  256. package/src/indexer/serviceLinks.ts +543 -0
  257. package/src/indexer/shapehash.ts +370 -0
  258. package/src/indexer/skeleton.ts +169 -0
  259. package/src/indexer/symbolhistory.ts +172 -0
  260. package/src/indexer/watcher.ts +206 -0
  261. package/src/mcp/server.ts +1659 -0
  262. package/src/parser/index.ts +37 -0
  263. package/src/parser/languages/cpp.ts +361 -0
  264. package/src/parser/languages/csharp.ts +235 -0
  265. package/src/parser/languages/go.ts +259 -0
  266. package/src/parser/languages/java.ts +382 -0
  267. package/src/parser/languages/python.ts +370 -0
  268. package/src/parser/languages/rust.ts +164 -0
  269. package/src/parser/languages/typescript.ts +1435 -0
  270. package/src/parser/parserContext.ts +392 -0
  271. package/src/parser/walker.ts +306 -0
  272. package/src/parser/worker.ts +181 -0
  273. package/src/parser/workerpool.ts +448 -0
  274. package/src/scip/format.ts +83 -0
  275. package/src/scip/import.ts +216 -0
  276. package/src/types.ts +457 -0
  277. package/tests/benchmark-service-links.ts +244 -0
  278. package/tests/bug-regressions.ts +626 -0
  279. package/tests/filters.ts +264 -0
  280. package/tests/fixtures/Counter.tsx +38 -0
  281. package/tests/fixtures/caller.ts +7 -0
  282. package/tests/fixtures/collisions.ts +23 -0
  283. package/tests/fixtures/local_helper.ts +5 -0
  284. package/tests/fixtures/overloads.java +17 -0
  285. package/tests/fixtures/remote_helper.ts +4 -0
  286. package/tests/fixtures/sample.c +15 -0
  287. package/tests/fixtures/sample.cpp +47 -0
  288. package/tests/fixtures/sample.cs +62 -0
  289. package/tests/fixtures/sample.go +68 -0
  290. package/tests/fixtures/sample.h +30 -0
  291. package/tests/fixtures/sample.java +85 -0
  292. package/tests/fixtures/sample.py +46 -0
  293. package/tests/fixtures/sample.rs +78 -0
  294. package/tests/fixtures/sample.ts +76 -0
  295. package/tests/fixtures-service/HttpClients.cs +30 -0
  296. package/tests/fixtures-service/HttpClients.java +24 -0
  297. package/tests/fixtures-service/billing.ts +15 -0
  298. package/tests/fixtures-service/docker-compose.yml +15 -0
  299. package/tests/fixtures-service/gateway.ts +10 -0
  300. package/tests/fixtures-service/get_user.ts +11 -0
  301. package/tests/fixtures-service/graphql_client.ts +63 -0
  302. package/tests/fixtures-service/graphql_server.ts +30 -0
  303. package/tests/fixtures-service/grpc_client.go +30 -0
  304. package/tests/fixtures-service/http_clients.go +23 -0
  305. package/tests/fixtures-service/http_clients.py +38 -0
  306. package/tests/fixtures-service/http_clients.ts +49 -0
  307. package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
  308. package/tests/fixtures-service/k8s_calls.ts +20 -0
  309. package/tests/fixtures-service/messaging.ts +87 -0
  310. package/tests/fixtures-service/trpc_client.ts +39 -0
  311. package/tests/fixtures-service/trpc_server.ts +39 -0
  312. package/tests/fixtures-service/user_service.proto +33 -0
  313. package/tests/fixtures-trackcd/Cargo.toml +11 -0
  314. package/tests/fixtures-trackcd/SpringController.java +36 -0
  315. package/tests/fixtures-trackcd/auth_service.ts +19 -0
  316. package/tests/fixtures-trackcd/complex_module.py +50 -0
  317. package/tests/fixtures-trackcd/express_app.js +30 -0
  318. package/tests/fixtures-trackcd/fastapi_app.py +49 -0
  319. package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
  320. package/tests/fixtures-trackcd/go.mod +8 -0
  321. package/tests/fixtures-trackcd/package.json +15 -0
  322. package/tests/fixtures-trackcd/requirements.txt +4 -0
  323. package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
  324. package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
  325. package/tests/fixtures-tracke/auth/crypto.ts +7 -0
  326. package/tests/fixtures-tracke/billing/Billing.ts +20 -0
  327. package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
  328. package/tests/fixtures-tracke/billing/server.ts +17 -0
  329. package/tests/fixtures-tracke/package.json +7 -0
  330. package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
  331. package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
  332. package/tests/fixtures-trackf/package.json +5 -0
  333. package/tests/fixtures-trackf/src/auth.ts +26 -0
  334. package/tests/fixtures-trackf/src/handlers.ts +35 -0
  335. package/tests/fixtures-tracki/billing/routes.ts +12 -0
  336. package/tests/fixtures-tracki/gateway/client.ts +13 -0
  337. package/tests/git-features.ts +267 -0
  338. package/tests/init.ts +141 -0
  339. package/tests/mcp-jit.ts +130 -0
  340. package/tests/mcp-smoke.ts +191 -0
  341. package/tests/mcp-trackcd.ts +169 -0
  342. package/tests/mcp-tracke.ts +229 -0
  343. package/tests/mcp-trackf.ts +330 -0
  344. package/tests/mcp-trackg.ts +219 -0
  345. package/tests/mcp-tracki.ts +174 -0
  346. package/tests/mcp-watcher.ts +126 -0
  347. package/tests/optspec.ts +194 -0
  348. package/tests/parallel-index.ts +333 -0
  349. package/tests/parallel-read.ts +125 -0
  350. package/tests/parallel-recovery.ts +241 -0
  351. package/tests/perf-callers.ts +145 -0
  352. package/tests/query-parity.ts +184 -0
  353. package/tests/query-perf.ts +55 -0
  354. package/tests/scale-parallel-parity.ts +225 -0
  355. package/tests/scale-test.ts +523 -0
  356. package/tests/smoke.ts +396 -0
  357. package/tests/trackcd.ts +325 -0
  358. package/tests/tracke-collisions.ts +255 -0
  359. package/tests/tracke.ts +314 -0
  360. package/tests/trackf-bugs.ts +406 -0
  361. package/tests/trackf.ts +390 -0
  362. package/tests/trackg.ts +1372 -0
  363. package/tests/tracki-boundaries.ts +202 -0
  364. package/tests/tracki-continuity.ts +253 -0
  365. package/tests/tracki-contract-diff.ts +249 -0
  366. package/tests/tracki-external-bundles.ts +341 -0
  367. package/tests/tracki-preflight.ts +251 -0
  368. package/tests/verify-roles.ts +51 -0
  369. package/tests/worker-parity.ts +286 -0
  370. package/tests/worker-pool.ts +262 -0
  371. package/tsconfig.json +20 -0
@@ -0,0 +1,340 @@
1
+ import { Store } from '../db/store.js';
2
+ import { rankedBehavior } from './behavior.js';
3
+ import { computeRisk, RiskResult } from './risk.js';
4
+ import type { SymbolRow, CallerRow, CalleeRow } from '../types.js';
5
+
6
+ /**
7
+ * One compact, structured pre-edit packet for a symbol.
8
+ *
9
+ * The aim is workflow compression: an agent that's about to edit a symbol
10
+ * should be able to call ONE tool and get back the deterministic evidence
11
+ * it needs — definition, callers, callees, routes, config reads, behavioral
12
+ * tests, recent history, complexity, module, blast radius, and the
13
+ * decomposed risk score. The agent then decides which slices to expand
14
+ * (`seer_history` for full chain, `seer_callers` for everyone, etc.).
15
+ *
16
+ * This is intentionally NOT an explanation layer. Seer-Core stays
17
+ * deterministic facts only; any narrative about "why this matters" belongs
18
+ * outside Core.
19
+ */
20
+
21
+ export interface ContextPacket {
22
+ symbol: {
23
+ id: number;
24
+ name: string;
25
+ qualifiedName: string | null;
26
+ kind: string;
27
+ file: string;
28
+ lineStart: number;
29
+ lineEnd: number;
30
+ signature: string | null;
31
+ pagerank: number;
32
+ symbolRole: string | null;
33
+ };
34
+ module: { id: number; label: string } | null;
35
+ /** v10 — monorepo boundary the symbol's file belongs to (null when none). */
36
+ boundary: { id: number; label: string; kind: string; rootRelPath: string } | null;
37
+ complexity: {
38
+ loc: number | null;
39
+ cyclomatic: number | null;
40
+ cognitive: number | null;
41
+ maxNesting: number | null;
42
+ };
43
+ callers: {
44
+ total: number;
45
+ preview: Array<{
46
+ name: string; qualifiedName: string | null; kind: string;
47
+ file: string; line: number;
48
+ }>;
49
+ };
50
+ callees: {
51
+ total: number;
52
+ preview: Array<{
53
+ name: string; kind: string | null;
54
+ file: string | null; line: number | null;
55
+ }>;
56
+ };
57
+ blastRadius: {
58
+ directCallers: number;
59
+ transitiveCallers: number;
60
+ /** Sample of the highest-PageRank reverse-reachable callers (capped). */
61
+ topAffected: Array<{ id: number; name: string; qualifiedName: string | null; file: string; pagerank: number }>;
62
+ maxDepth: number;
63
+ };
64
+ routes: Array<{ method: string; path: string; framework: string }>;
65
+ configKeys: Array<{ key: string; source: string; line: number }>;
66
+ /**
67
+ * v8 Track-G — outbound service calls this symbol makes (preview, capped).
68
+ * Empty array on Pre-Track-G DBs or symbols with no outgoing client calls.
69
+ */
70
+ serviceCalls: Array<{
71
+ method: string | null;
72
+ path: string | null;
73
+ framework: string;
74
+ rawTarget: string;
75
+ line: number;
76
+ envKey: string | null;
77
+ hostHint: string | null;
78
+ confidence: number;
79
+ }>;
80
+ /**
81
+ * v8 Track-G — service-link evidence pointing at this symbol as the handler
82
+ * (inbound) and as the caller (outbound). Capped previews.
83
+ */
84
+ serviceLinksInbound: Array<{
85
+ routePath: string | null;
86
+ method: string | null;
87
+ matchKind: string;
88
+ confidence: number;
89
+ callerName: string | null;
90
+ callerFile: string | null;
91
+ }>;
92
+ serviceLinksOutbound: Array<{
93
+ routePath: string | null;
94
+ method: string | null;
95
+ matchKind: string;
96
+ confidence: number;
97
+ handlerName: string | null;
98
+ handlerFile: string | null;
99
+ }>;
100
+ behavior: {
101
+ direct: number;
102
+ indirect: number;
103
+ namingMatches: number;
104
+ sameFileMatches: number;
105
+ preview: Array<{
106
+ name: string; qualifiedName: string | null; file: string; lineStart: number;
107
+ relationship: string; assertionCount: number; specificity: number;
108
+ }>;
109
+ };
110
+ recentHistory: {
111
+ total: number;
112
+ preview: Array<{
113
+ sha: string; author: string | null; email: string | null;
114
+ committedAt: number; message: string | null;
115
+ linesAdded: number; linesRemoved: number;
116
+ prNumber: number | null; prUrl: string | null;
117
+ confidence: number;
118
+ }>;
119
+ };
120
+ fileChurn: {
121
+ commitCount: number;
122
+ lastCommitAt: number | null;
123
+ topAuthor: string | null;
124
+ } | null;
125
+ risk: {
126
+ risk: 'low' | 'medium' | 'high';
127
+ score: number;
128
+ signals: RiskResult['signals'];
129
+ signalContributions: RiskResult['signalContributions'];
130
+ };
131
+ source: 'tree-sitter';
132
+ }
133
+
134
+ export interface ContextOptions {
135
+ filePath?: string;
136
+ callerLimit?: number;
137
+ calleeLimit?: number;
138
+ testLimit?: number;
139
+ historyLimit?: number;
140
+ callerDepth?: number;
141
+ affectedLimit?: number;
142
+ }
143
+
144
+ export function buildContext(
145
+ store: Store,
146
+ nameOrId: string | number,
147
+ options: ContextOptions = {},
148
+ ): ContextPacket | null {
149
+ const callerLimit = options.callerLimit ?? 10;
150
+ const calleeLimit = options.calleeLimit ?? 10;
151
+ const testLimit = options.testLimit ?? 10;
152
+ const historyLimit = options.historyLimit ?? 5;
153
+ const callerDepth = options.callerDepth ?? 3;
154
+ const affectedLimit = options.affectedLimit ?? 10;
155
+
156
+ let target: SymbolRow | null = null;
157
+ if (typeof nameOrId === 'number') {
158
+ target = store.getSymbolById(nameOrId);
159
+ } else {
160
+ const defs = store.getDefinition(nameOrId, { filePath: options.filePath });
161
+ if (defs.length === 0) return null;
162
+ target = defs[0];
163
+ }
164
+ if (!target) return null;
165
+
166
+ // Callers + callees use the id-based path so short-name siblings
167
+ // (Alpha.run vs Beta.run) don't share evidence. The legacy name-based
168
+ // APIs (findCallers / countCallers / findCallees) intentionally stay
169
+ // broad for the agent-facing CLI tools — Track E packets always have a
170
+ // resolved id and should never collapse.
171
+ const totalCallers = store.countCallersById(target.id);
172
+ const directCallers: CallerRow[] = store.findCallersById(target.id, callerLimit);
173
+
174
+ const allCallees: CalleeRow[] = store.findCalleesById(target.id);
175
+ const calleesPreview = allCallees.slice(0, calleeLimit);
176
+
177
+ // Blast radius.
178
+ const reverseHits = store.reverseReachableWithDepth(target.id, callerDepth);
179
+ const directRows = store.rawDb().prepare(
180
+ "SELECT DISTINCT from_id FROM edges WHERE to_id = ? AND kind = 'call'",
181
+ ).all(target.id) as Array<{ from_id: unknown }>;
182
+ const directSet = new Set<number>(directRows.map(r => Number(r.from_id)));
183
+ const transitive = reverseHits.filter(h => !directSet.has(h.id));
184
+ let topAffected: ContextPacket['blastRadius']['topAffected'] = [];
185
+ if (reverseHits.length > 0) {
186
+ const ids = reverseHits.map(h => h.id);
187
+ const ph = ids.map(() => '?').join(',');
188
+ const rows = store.rawDb().prepare(`
189
+ SELECT s.id, s.name, s.qualified_name AS qualifiedName, f.path AS file, s.pagerank
190
+ FROM symbols s JOIN files f ON f.id = s.file_id
191
+ WHERE s.id IN (${ph}) AND s.is_rankable = 1
192
+ ORDER BY s.pagerank DESC
193
+ LIMIT ?
194
+ `).all(...ids, affectedLimit) as Array<{
195
+ id: unknown; name: unknown; qualifiedName: unknown; file: unknown; pagerank: unknown;
196
+ }>;
197
+ topAffected = rows.map(r => ({
198
+ id: Number(r.id),
199
+ name: String(r.name),
200
+ qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
201
+ file: String(r.file),
202
+ pagerank: Number(r.pagerank),
203
+ }));
204
+ }
205
+
206
+ // Routes / config / behavior / history.
207
+ const routes = store.routesForHandler(target.id);
208
+ const configKeys = store.configKeysForSymbol(target.id);
209
+ const behavior = rankedBehavior(store, target.id, { limit: testLimit });
210
+
211
+ // v8 Track-G — service-link evidence. Capped previews so the packet stays
212
+ // compact even when the symbol is a hub.
213
+ const SERVICE_CALL_PREVIEW_CAP = 12;
214
+ const SERVICE_LINK_PREVIEW_CAP = 12;
215
+ const serviceCallsRows = store.listServiceCalls({
216
+ callerSymbolId: target.id, limit: SERVICE_CALL_PREVIEW_CAP,
217
+ });
218
+ const serviceLinksInbound = store.serviceLinksForHandler(target.id, { limit: SERVICE_LINK_PREVIEW_CAP });
219
+ const serviceLinksOutbound = store.serviceLinksForCaller(target.id, { limit: SERVICE_LINK_PREVIEW_CAP });
220
+ const history = store.getSymbolHistory(target.id, { limit: historyLimit });
221
+ const totalHistory = store.countSymbolHistory(target.id);
222
+ const fileChurn = (() => {
223
+ try {
224
+ const c = store.getFileChurn(target.filePath);
225
+ if (!c) return null;
226
+ return { commitCount: c.commitCount, lastCommitAt: c.lastCommitAt, topAuthor: c.topAuthor };
227
+ } catch { return null; }
228
+ })();
229
+
230
+ // Risk (reuses behavior + history + signals computed above; cheaper to
231
+ // recompute than to share through a back-channel.)
232
+ const risk = computeRisk(store, target.id, { callerDepth })!;
233
+
234
+ const moduleRow = store.moduleForFile(target.fileId);
235
+ const boundaryRow = store.boundaryForFile(target.fileId);
236
+
237
+ return {
238
+ symbol: {
239
+ id: target.id, name: target.name, qualifiedName: target.qualifiedName,
240
+ kind: target.kind, file: target.filePath,
241
+ lineStart: target.lineStart, lineEnd: target.lineEnd,
242
+ signature: target.signature, pagerank: target.pagerank,
243
+ symbolRole: target.symbolRole ?? null,
244
+ },
245
+ module: moduleRow,
246
+ boundary: boundaryRow,
247
+ complexity: {
248
+ loc: target.loc ?? null,
249
+ cyclomatic: target.cyclomatic ?? null,
250
+ cognitive: target.cognitive ?? null,
251
+ maxNesting: target.maxNesting ?? null,
252
+ },
253
+ callers: {
254
+ total: totalCallers,
255
+ preview: directCallers.map(c => ({
256
+ name: c.callerName, qualifiedName: c.callerQualifiedName, kind: c.callerKind,
257
+ file: c.callerFile, line: c.callerLine,
258
+ })),
259
+ },
260
+ callees: {
261
+ total: allCallees.length,
262
+ preview: calleesPreview.map(c => ({
263
+ name: c.calleeName, kind: c.calleeKind,
264
+ file: c.calleeFile, line: c.calleeLineStart,
265
+ })),
266
+ },
267
+ blastRadius: {
268
+ directCallers: directSet.size,
269
+ transitiveCallers: transitive.length,
270
+ topAffected,
271
+ maxDepth: callerDepth,
272
+ },
273
+ routes,
274
+ configKeys,
275
+ serviceCalls: serviceCallsRows.map(sc => ({
276
+ method: sc.method,
277
+ path: sc.normalizedPath,
278
+ framework: sc.framework,
279
+ rawTarget: sc.rawTarget,
280
+ line: sc.line,
281
+ envKey: sc.envKey,
282
+ hostHint: sc.hostHint,
283
+ confidence: sc.confidence,
284
+ })),
285
+ serviceLinksInbound: serviceLinksInbound.map(l => ({
286
+ routePath: l.routePath,
287
+ method: l.routeMethod ?? l.callMethod,
288
+ matchKind: l.matchKind,
289
+ confidence: l.confidence,
290
+ callerName: l.callerQualifiedName ?? l.callerName,
291
+ callerFile: l.callerFile,
292
+ })),
293
+ serviceLinksOutbound: serviceLinksOutbound.map(l => ({
294
+ routePath: l.routePath,
295
+ method: l.routeMethod ?? l.callMethod,
296
+ matchKind: l.matchKind,
297
+ confidence: l.confidence,
298
+ handlerName: l.handlerQualifiedName ?? l.handlerName,
299
+ handlerFile: l.handlerFile,
300
+ })),
301
+ behavior: {
302
+ direct: behavior?.direct ?? 0,
303
+ indirect: behavior?.indirect ?? 0,
304
+ namingMatches: behavior?.namingMatches ?? 0,
305
+ sameFileMatches: behavior?.sameFileMatches ?? 0,
306
+ preview: (behavior?.tests ?? []).map(t => ({
307
+ name: t.testSymbol.name,
308
+ qualifiedName: t.testSymbol.qualifiedName,
309
+ file: t.testSymbol.file,
310
+ lineStart: t.testSymbol.lineStart,
311
+ relationship: t.relationship,
312
+ assertionCount: t.assertionCount,
313
+ specificity: t.specificity,
314
+ })),
315
+ },
316
+ recentHistory: {
317
+ total: totalHistory,
318
+ preview: history.map(h => ({
319
+ sha: h.commitSha,
320
+ author: h.authorName,
321
+ email: h.authorEmail,
322
+ committedAt: h.committedAt,
323
+ message: h.message,
324
+ linesAdded: h.linesAdded,
325
+ linesRemoved: h.linesRemoved,
326
+ prNumber: h.prNumber,
327
+ prUrl: h.prUrl,
328
+ confidence: h.confidence,
329
+ })),
330
+ },
331
+ fileChurn,
332
+ risk: {
333
+ risk: risk.risk,
334
+ score: risk.score,
335
+ signals: risk.signals,
336
+ signalContributions: risk.signalContributions,
337
+ },
338
+ source: 'tree-sitter',
339
+ };
340
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * v10 — Symbol Rename/Move Continuity heuristics.
3
+ *
4
+ * Goal: when exact `symbol_key` history walking terminates (because the
5
+ * function was renamed or moved), surface honest, confidence-labelled
6
+ * continuity evidence so the agent can decide whether to trust the link.
7
+ *
8
+ * Heuristics (current pass — opt-in, low-confidence by default):
9
+ * - shape_hash exact match: previous-symbol candidate has the same
10
+ * structural SimHash → strong (confidence 0.85+)
11
+ * - shape_hash close match: small Hamming distance (≤ 4) + similar name
12
+ * → medium (confidence 0.65)
13
+ * - signature similarity: same arity + same containing class/module
14
+ * → weak (confidence 0.5)
15
+ * - same file rename history: file was renamed in git history, the
16
+ * historical file had a same-shape function with a different name → boost
17
+ * (confidence 0.75)
18
+ *
19
+ * Stored on `symbol_history_continuity`. Never pretends rename continuity
20
+ * is certain.
21
+ *
22
+ * This module does NOT replace existing exact-key history. It only proposes
23
+ * additional links when buildSymbolHistory's exact-key walk ran out of
24
+ * commits. The Preflight / seer_history layers can read continuity rows
25
+ * alongside symbol_history.
26
+ */
27
+
28
+ import { Store } from '../db/store.js';
29
+
30
+ export interface ContinuityCandidate {
31
+ symbolId: number;
32
+ symbolKey: string;
33
+ previousSymbolKey: string;
34
+ previousName: string;
35
+ previousFile: string;
36
+ confidence: number;
37
+ matchReasons: string[];
38
+ }
39
+
40
+ export interface ContinuityResult {
41
+ candidatesConsidered: number;
42
+ inserted: number;
43
+ skipped: number;
44
+ elapsedMs: number;
45
+ }
46
+
47
+ /**
48
+ * Run the continuity pass over every symbol whose recorded history has
49
+ * fewer than `historyThreshold` commits AND that has a shape_hash. For each
50
+ * such symbol we scan other symbols sharing a close shape_hash and propose
51
+ * the highest-confidence candidate (deduped per symbol_id).
52
+ */
53
+ export function buildContinuity(
54
+ store: Store,
55
+ options: {
56
+ historyThreshold?: number;
57
+ maxHammingDistance?: number;
58
+ log?: (msg: string) => void;
59
+ /** When true, also create candidate links for symbols that have full history
60
+ * (useful for fixture tests and debugging). Default false. */
61
+ includeAllSymbols?: boolean;
62
+ } = {},
63
+ ): ContinuityResult {
64
+ const start = Date.now();
65
+ const log = options.log ?? (() => { /* */ });
66
+ const historyThreshold = options.historyThreshold ?? 1;
67
+ const maxHamming = options.maxHammingDistance ?? 4;
68
+
69
+ // Pool of candidates: every symbol with a shape_hash.
70
+ const pool = store.listSymbolsWithShapeHash({ minLoc: 1, limit: 100000 });
71
+ if (pool.length === 0) {
72
+ log('no shape-hashed symbols; nothing to do');
73
+ return { candidatesConsidered: 0, inserted: 0, skipped: 0, elapsedMs: Date.now() - start };
74
+ }
75
+
76
+ // Bucket by qualifiedName/name → list of candidates (so we can detect
77
+ // rename: same shape, different name).
78
+ const byHash = new Map<string, typeof pool>();
79
+ for (const s of pool) {
80
+ const k = s.shapeHash.toString();
81
+ const list = byHash.get(k) ?? [];
82
+ list.push(s);
83
+ byHash.set(k, list);
84
+ }
85
+
86
+ let considered = 0;
87
+ let inserted = 0;
88
+ let skipped = 0;
89
+
90
+ const raw = store.rawDb();
91
+
92
+ for (const s of pool) {
93
+ // Find the symbol's stored history count. If it's >= historyThreshold
94
+ // AND we're not in includeAllSymbols mode, skip — exact history is fine.
95
+ if (!options.includeAllSymbols) {
96
+ const cnt = raw.prepare(
97
+ 'SELECT COUNT(*) AS c FROM symbol_history WHERE symbol_id = ?',
98
+ ).get(s.id) as { c: number } | undefined;
99
+ if (cnt && cnt.c >= historyThreshold) continue;
100
+ }
101
+
102
+ // Exact shape match candidates with a DIFFERENT (qualifiedName ?? name).
103
+ const exactMatches = (byHash.get(s.shapeHash.toString()) ?? [])
104
+ .filter(c => c.id !== s.id);
105
+ if (exactMatches.length > 0) {
106
+ const cand = pickBestCandidate(s, exactMatches);
107
+ if (cand) {
108
+ const sameClass = sharesContainingScope(s, cand);
109
+ const nameRelated = similarName(s.name, cand.name);
110
+ // How many OTHER symbols share this exact shape? A shape shared by many
111
+ // symbols (trivial getters, `return null;`, boilerplate) is NOT a
112
+ // reliable rename signal on its own. Only assert a high-confidence link
113
+ // when the shape is (near-)unique to this pair; otherwise require
114
+ // corroboration (same scope or a related name) and label the ambiguity
115
+ // honestly with a lower, capped confidence. Never pretend certainty.
116
+ const ambiguous = exactMatches.length >= 2;
117
+ if (ambiguous && !sameClass && !nameRelated) {
118
+ // Common shape, no corroborating evidence — do not invent a rename.
119
+ skipped++;
120
+ continue;
121
+ }
122
+ considered++;
123
+ const reasons = ['shape_hash_exact'];
124
+ if (sameClass) reasons.push('same_containing_scope');
125
+ if (nameRelated) reasons.push('similar_name');
126
+ let confidence: number;
127
+ if (ambiguous) {
128
+ reasons.push(`ambiguous_shape_bucket:n=${exactMatches.length + 1}`);
129
+ confidence = 0.6;
130
+ if (sameClass) confidence = Math.min(0.7, confidence + 0.05);
131
+ if (nameRelated) confidence = Math.min(0.7, confidence + 0.05);
132
+ } else {
133
+ confidence = 0.85;
134
+ if (sameClass) confidence = Math.min(0.95, confidence + 0.05);
135
+ if (nameRelated) confidence = Math.min(0.95, confidence + 0.05);
136
+ }
137
+ upsertContinuity(store, {
138
+ symbolId: s.id,
139
+ symbolKey: keyFor(s),
140
+ previousSymbolKey: keyFor(cand),
141
+ previousName: cand.name,
142
+ previousFile: cand.filePath,
143
+ confidence,
144
+ matchReasons: reasons,
145
+ });
146
+ inserted++;
147
+ continue;
148
+ }
149
+ }
150
+
151
+ // Close hash match.
152
+ let best: { peer: typeof pool[number]; distance: number } | null = null;
153
+ for (const peer of pool) {
154
+ if (peer.id === s.id) continue;
155
+ if ((peer.name === s.name) && (peer.qualifiedName === s.qualifiedName)) continue;
156
+ const d = hammingDistance(s.shapeHash, peer.shapeHash);
157
+ if (d > maxHamming) continue;
158
+ if (!best || d < best.distance) best = { peer, distance: d };
159
+ }
160
+ if (best && best.distance <= maxHamming) {
161
+ const cand = best.peer;
162
+ // Only act when names are at least loosely related (share a prefix or
163
+ // a suffix), to avoid pairing every short function in the codebase.
164
+ if (similarName(s.name, cand.name) || sharesContainingScope(s, cand)) {
165
+ considered++;
166
+ const reasons = [`shape_hash_close:d=${best.distance}`];
167
+ if (similarName(s.name, cand.name)) reasons.push('similar_name');
168
+ if (sharesContainingScope(s, cand)) reasons.push('same_containing_scope');
169
+ const confidence = best.distance === 0 ? 0.8
170
+ : best.distance <= 2 ? 0.6
171
+ : 0.4;
172
+ upsertContinuity(store, {
173
+ symbolId: s.id,
174
+ symbolKey: keyFor(s),
175
+ previousSymbolKey: keyFor(cand),
176
+ previousName: cand.name,
177
+ previousFile: cand.filePath,
178
+ confidence,
179
+ matchReasons: reasons,
180
+ });
181
+ inserted++;
182
+ continue;
183
+ }
184
+ }
185
+
186
+ skipped++;
187
+ }
188
+
189
+ return {
190
+ candidatesConsidered: considered,
191
+ inserted, skipped,
192
+ elapsedMs: Date.now() - start,
193
+ };
194
+ }
195
+
196
+ function upsertContinuity(
197
+ store: Store, c: ContinuityCandidate,
198
+ ): void {
199
+ const raw = store.rawDb();
200
+ raw.prepare(`
201
+ INSERT INTO symbol_history_continuity
202
+ (symbol_id, symbol_key, previous_symbol_key, previous_name, previous_file,
203
+ bridging_sha, confidence, match_reasons, recorded_at)
204
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)
205
+ ON CONFLICT(symbol_id, previous_symbol_key) DO UPDATE SET
206
+ confidence = excluded.confidence,
207
+ match_reasons = excluded.match_reasons,
208
+ previous_name = excluded.previous_name,
209
+ previous_file = excluded.previous_file,
210
+ recorded_at = excluded.recorded_at
211
+ `).run(
212
+ c.symbolId, c.symbolKey, c.previousSymbolKey,
213
+ c.previousName, c.previousFile,
214
+ c.confidence, JSON.stringify(c.matchReasons),
215
+ Date.now(),
216
+ );
217
+ }
218
+
219
+ function keyFor(s: { kind: string; qualifiedName: string | null; name: string }): string {
220
+ return `${s.kind}:${s.qualifiedName ?? s.name}`;
221
+ }
222
+
223
+ function sharesContainingScope(
224
+ a: { qualifiedName: string | null; name: string; filePath: string },
225
+ b: { qualifiedName: string | null; name: string; filePath: string },
226
+ ): boolean {
227
+ if (a.filePath === b.filePath) return true;
228
+ // Compare class/module prefix in the qualified name (e.g. `AuthService.foo`
229
+ // and `AuthService.bar` share `AuthService`).
230
+ const aQual = a.qualifiedName ?? '';
231
+ const bQual = b.qualifiedName ?? '';
232
+ if (!aQual.includes('.') || !bQual.includes('.')) return false;
233
+ const aPrefix = aQual.split('.').slice(0, -1).join('.');
234
+ const bPrefix = bQual.split('.').slice(0, -1).join('.');
235
+ return aPrefix.length > 0 && aPrefix === bPrefix;
236
+ }
237
+
238
+ function similarName(a: string, b: string): boolean {
239
+ if (!a || !b) return false;
240
+ if (a === b) return false; // we only flag potential RENAMES
241
+ const aL = a.toLowerCase();
242
+ const bL = b.toLowerCase();
243
+ // Same prefix of length >= 4 or same suffix of length >= 4.
244
+ const minLen = Math.min(aL.length, bL.length);
245
+ if (minLen < 4) return false;
246
+ let prefix = 0;
247
+ while (prefix < minLen && aL[prefix] === bL[prefix]) prefix++;
248
+ if (prefix >= 4) return true;
249
+ let suffix = 0;
250
+ while (suffix < minLen && aL[aL.length - 1 - suffix] === bL[bL.length - 1 - suffix]) suffix++;
251
+ if (suffix >= 4) return true;
252
+ // Names that differ by a verb prefix swap (validate → verify) — drop the
253
+ // first 4 characters and compare the rest.
254
+ if (aL.length >= 4 && bL.length >= 4 && aL.slice(4) === bL.slice(4)) return true;
255
+ return false;
256
+ }
257
+
258
+ function pickBestCandidate<T extends { id: number; name: string; qualifiedName: string | null; filePath: string; kind: string }>(
259
+ target: T,
260
+ candidates: T[],
261
+ ): T | null {
262
+ // Prefer same-file rename. Then same-class rename. Then any candidate.
263
+ const sameFile = candidates.filter(c => c.filePath === target.filePath);
264
+ if (sameFile.length > 0) return sameFile[0];
265
+ const sameClass = candidates.filter(c => sharesContainingScope(target, c));
266
+ if (sameClass.length > 0) return sameClass[0];
267
+ return candidates[0] ?? null;
268
+ }
269
+
270
+ function hammingDistance(a: bigint, b: bigint): number {
271
+ let x = a ^ b;
272
+ let n = 0;
273
+ while (x !== 0n) {
274
+ x &= x - 1n;
275
+ n++;
276
+ }
277
+ return n;
278
+ }
279
+
280
+ /**
281
+ * Fetch continuity rows for a given symbol id, ordered by confidence desc.
282
+ */
283
+ export function getContinuityForSymbol(
284
+ store: Store, symbolId: number,
285
+ ): Array<{
286
+ previousSymbolKey: string;
287
+ previousName: string;
288
+ previousFile: string;
289
+ confidence: number;
290
+ matchReasons: string[];
291
+ }> {
292
+ if (!store.hasV10()) return [];
293
+ try {
294
+ const rows = store.rawDb().prepare(`
295
+ SELECT previous_symbol_key AS previousSymbolKey,
296
+ previous_name AS previousName,
297
+ previous_file AS previousFile,
298
+ confidence, match_reasons AS matchReasons
299
+ FROM symbol_history_continuity
300
+ WHERE symbol_id = ?
301
+ ORDER BY confidence DESC, id DESC
302
+ `).all(symbolId) as Array<{
303
+ previousSymbolKey: unknown; previousName: unknown; previousFile: unknown;
304
+ confidence: unknown; matchReasons: unknown;
305
+ }>;
306
+ return rows.map(r => ({
307
+ previousSymbolKey: String(r.previousSymbolKey),
308
+ previousName: String(r.previousName ?? ''),
309
+ previousFile: String(r.previousFile ?? ''),
310
+ confidence: Number(r.confidence ?? 0),
311
+ matchReasons: parseReasons(r.matchReasons),
312
+ }));
313
+ } catch { return []; }
314
+ }
315
+
316
+ function parseReasons(v: unknown): string[] {
317
+ if (typeof v !== 'string') return [];
318
+ try {
319
+ const parsed = JSON.parse(v);
320
+ return Array.isArray(parsed) ? parsed.map(String) : [];
321
+ } catch { return []; }
322
+ }