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,548 @@
1
+ /**
2
+ * v10 — Preflight Context.
3
+ *
4
+ * One compact "should I edit this?" evidence packet for an agent. Combines
5
+ * existing deterministic facts (context, risk, behavior, history, service
6
+ * links) plus a "likely tests" recommendation derived from the behavior
7
+ * ranker. Optionally compares a git ref range, mapping touched lines to
8
+ * symbols and aggregating evidence across the change set.
9
+ *
10
+ * No AI prose. Structured facts only. Output is bounded and stable.
11
+ *
12
+ * Two entry points:
13
+ * 1. `preflightForSymbol(store, symbolNameOrId)` — packet for a single
14
+ * symbol; built on top of buildContext + computeRisk.
15
+ * 2. `preflightForRange(store, workspace, fromRef, toRef)` — packet for a
16
+ * diff range; uses detectChanges and aggregates per-symbol evidence.
17
+ */
18
+
19
+ import { Store } from '../db/store.js';
20
+ import { buildContext, ContextPacket } from './context.js';
21
+ import { computeRisk, RiskResult } from './risk.js';
22
+ import { rankedBehavior, BehaviorResult } from './behavior.js';
23
+ import { detectChanges } from './detectchanges.js';
24
+ import { contractDiff, ContractDiff } from '../bundle/contract.js';
25
+ import type { SymbolRow } from '../types.js';
26
+
27
+ export interface PreflightTouchedSymbol {
28
+ id: number;
29
+ name: string;
30
+ qualifiedName: string | null;
31
+ kind: string;
32
+ file: string;
33
+ lineStart: number;
34
+ lineEnd: number;
35
+ }
36
+
37
+ export interface PreflightLikelyTest {
38
+ testSymbol: {
39
+ name: string;
40
+ qualifiedName: string | null;
41
+ file: string;
42
+ lineStart: number;
43
+ };
44
+ relationship: string;
45
+ specificity: number;
46
+ assertionCount: number;
47
+ graphDistance: number | null;
48
+ }
49
+
50
+ export interface PreflightServiceImpact {
51
+ inbound: Array<{
52
+ routePath: string | null;
53
+ routeMethod: string | null;
54
+ protocol: string;
55
+ matchKind: string;
56
+ callerName: string | null;
57
+ callerFile: string | null;
58
+ }>;
59
+ outbound: Array<{
60
+ routePath: string | null;
61
+ routeMethod: string | null;
62
+ protocol: string;
63
+ matchKind: string;
64
+ handlerName: string | null;
65
+ handlerFile: string | null;
66
+ }>;
67
+ }
68
+
69
+ export interface PreflightHistoryRow {
70
+ sha: string;
71
+ author: string | null;
72
+ email: string | null;
73
+ committedAt: number;
74
+ message: string | null;
75
+ linesAdded: number;
76
+ linesRemoved: number;
77
+ prNumber: number | null;
78
+ prUrl: string | null;
79
+ }
80
+
81
+ export interface PreflightResult {
82
+ ok: boolean;
83
+ reason?: string;
84
+ /** Mode: 'symbol' for --symbol or 'range' for --from/--to. */
85
+ mode: 'symbol' | 'range';
86
+ /** Symbol mode: the focal symbol. Range mode: empty. */
87
+ symbol?: PreflightTouchedSymbol;
88
+ /** Range mode only. */
89
+ range?: {
90
+ fromRef: string | null;
91
+ toRef: string | null;
92
+ changedFiles: number;
93
+ directHunkCount: number;
94
+ };
95
+ /** All symbols touched by this preflight. */
96
+ touchedSymbols: PreflightTouchedSymbol[];
97
+ /** Risk verdict, aggregated when range mode. */
98
+ risk: {
99
+ overall: 'low' | 'medium' | 'high';
100
+ perSymbol: Array<{
101
+ symbol: PreflightTouchedSymbol;
102
+ risk: 'low' | 'medium' | 'high';
103
+ score: number;
104
+ topContributors: Array<{ signal: string; value: number; contribution: number }>;
105
+ }>;
106
+ };
107
+ likelyTests: PreflightLikelyTest[];
108
+ serviceImpact: PreflightServiceImpact;
109
+ contractChanges?: ContractDiff;
110
+ history: PreflightHistoryRow[];
111
+ warnings: string[];
112
+ module: { id: number; label: string } | null;
113
+ /** v10 — boundaries the touched symbol(s) live in or cross into. */
114
+ boundaries: {
115
+ primary: { id: number; label: string; kind: string; rootRelPath: string } | null;
116
+ crossed: Array<{ id: number; label: string; kind: string; rootRelPath: string }>;
117
+ };
118
+ source: 'tree-sitter';
119
+ }
120
+
121
+ export interface PreflightOptions {
122
+ symbol?: string | number;
123
+ /** Disambiguate via file. */
124
+ filePath?: string;
125
+ fromRef?: string;
126
+ toRef?: string;
127
+ /**
128
+ * When true (and no `symbol` was supplied), run range-mode preflight even
129
+ * if `fromRef`/`toRef` are both omitted — uses git's working tree diff to
130
+ * find touched symbols. Lets agents call `seer_preflight` with just a
131
+ * workspace and get a "what changed locally" packet.
132
+ */
133
+ range?: boolean;
134
+ /** Workspace (required when fromRef/toRef supplied). */
135
+ workspace?: string;
136
+ /** Optional bundle paths for contract diff comparison. */
137
+ oldBundle?: string;
138
+ newBundle?: string;
139
+ /** Bound: max symbols returned in touchedSymbols. */
140
+ maxSymbols?: number;
141
+ /** Bound: max tests in likelyTests. */
142
+ maxTests?: number;
143
+ /** Bound: max history rows. */
144
+ maxHistory?: number;
145
+ /** Bound: caller BFS depth. */
146
+ callerDepth?: number;
147
+ }
148
+
149
+ const DEFAULT_MAX_SYMBOLS = 12;
150
+ const DEFAULT_MAX_TESTS = 8;
151
+ const DEFAULT_MAX_HISTORY = 8;
152
+
153
+ export async function preflight(
154
+ store: Store, options: PreflightOptions = {},
155
+ ): Promise<PreflightResult> {
156
+ if (options.symbol !== undefined) {
157
+ return preflightForSymbol(store, options);
158
+ }
159
+ if (options.fromRef !== undefined || options.toRef !== undefined || options.range === true) {
160
+ return preflightForRange(store, options);
161
+ }
162
+ return {
163
+ ok: false,
164
+ reason: 'preflight requires either --symbol or --from/--to',
165
+ mode: 'symbol',
166
+ touchedSymbols: [],
167
+ risk: { overall: 'low', perSymbol: [] },
168
+ likelyTests: [],
169
+ serviceImpact: { inbound: [], outbound: [] },
170
+ history: [],
171
+ warnings: ['no input provided'],
172
+ module: null,
173
+ boundaries: { primary: null, crossed: [] },
174
+ source: 'tree-sitter',
175
+ };
176
+ }
177
+
178
+ function preflightForSymbol(
179
+ store: Store, options: PreflightOptions,
180
+ ): PreflightResult {
181
+ const maxSymbols = options.maxSymbols ?? DEFAULT_MAX_SYMBOLS;
182
+ const maxTests = options.maxTests ?? DEFAULT_MAX_TESTS;
183
+ const maxHistory = options.maxHistory ?? DEFAULT_MAX_HISTORY;
184
+ const callerDepth = options.callerDepth ?? 3;
185
+
186
+ const ctx = buildContext(store, options.symbol!, {
187
+ filePath: options.filePath,
188
+ callerLimit: 10, calleeLimit: 10,
189
+ testLimit: maxTests, historyLimit: maxHistory,
190
+ callerDepth,
191
+ });
192
+ if (!ctx) {
193
+ return {
194
+ ok: false,
195
+ reason: `no symbol "${options.symbol}"`,
196
+ mode: 'symbol',
197
+ touchedSymbols: [],
198
+ risk: { overall: 'low', perSymbol: [] },
199
+ likelyTests: [],
200
+ serviceImpact: { inbound: [], outbound: [] },
201
+ history: [],
202
+ warnings: [`symbol "${options.symbol}" not found in index`],
203
+ module: null,
204
+ boundaries: { primary: null, crossed: [] },
205
+ source: 'tree-sitter',
206
+ };
207
+ }
208
+
209
+ const focal: PreflightTouchedSymbol = {
210
+ id: ctx.symbol.id, name: ctx.symbol.name,
211
+ qualifiedName: ctx.symbol.qualifiedName, kind: ctx.symbol.kind,
212
+ file: ctx.symbol.file, lineStart: ctx.symbol.lineStart,
213
+ lineEnd: ctx.symbol.lineEnd,
214
+ };
215
+
216
+ const warnings: string[] = collectWarnings(ctx);
217
+
218
+ const likelyTests = pickLikelyTestsFromContext(ctx, maxTests);
219
+ // Read service links straight from the store so the real protocol
220
+ // (http/trpc/grpc/kafka/...) is preserved — the ContextPacket preview drops
221
+ // it, and hardcoding 'http' here mislabels non-HTTP links.
222
+ const serviceImpact: PreflightServiceImpact = {
223
+ inbound: store.serviceLinksForHandler(focal.id, { limit: 10 }).map(l => ({
224
+ routePath: l.routePath, routeMethod: l.routeMethod ?? l.callMethod,
225
+ protocol: l.protocol, matchKind: l.matchKind,
226
+ callerName: l.callerQualifiedName ?? l.callerName, callerFile: l.callerFile,
227
+ })),
228
+ outbound: store.serviceLinksForCaller(focal.id, { limit: 10 }).map(l => ({
229
+ routePath: l.routePath, routeMethod: l.routeMethod ?? l.callMethod,
230
+ protocol: l.protocol, matchKind: l.matchKind,
231
+ handlerName: l.handlerQualifiedName ?? l.handlerName, handlerFile: l.handlerFile,
232
+ })),
233
+ };
234
+
235
+ const history: PreflightHistoryRow[] = ctx.recentHistory.preview.slice(0, maxHistory).map(h => ({
236
+ sha: h.sha, author: h.author, email: h.email,
237
+ committedAt: h.committedAt, message: h.message,
238
+ linesAdded: h.linesAdded, linesRemoved: h.linesRemoved,
239
+ prNumber: h.prNumber, prUrl: h.prUrl,
240
+ }));
241
+
242
+ return {
243
+ ok: true,
244
+ mode: 'symbol',
245
+ symbol: focal,
246
+ touchedSymbols: [focal].slice(0, maxSymbols),
247
+ risk: {
248
+ overall: ctx.risk.risk,
249
+ perSymbol: [{
250
+ symbol: focal,
251
+ risk: ctx.risk.risk,
252
+ score: ctx.risk.score,
253
+ topContributors: pickTopContributions(ctx.risk.signalContributions, 5),
254
+ }],
255
+ },
256
+ likelyTests,
257
+ serviceImpact,
258
+ history,
259
+ warnings,
260
+ module: ctx.module,
261
+ boundaries: {
262
+ primary: ctx.boundary,
263
+ crossed: extractCrossedBoundaries(store, focal.id, ctx.boundary?.id ?? null),
264
+ },
265
+ source: 'tree-sitter',
266
+ };
267
+ }
268
+
269
+ async function preflightForRange(
270
+ store: Store, options: PreflightOptions,
271
+ ): Promise<PreflightResult> {
272
+ const workspace = options.workspace;
273
+ if (!workspace) {
274
+ return {
275
+ ok: false,
276
+ reason: 'preflight --from/--to requires a workspace (pass workspace option)',
277
+ mode: 'range',
278
+ touchedSymbols: [],
279
+ risk: { overall: 'low', perSymbol: [] },
280
+ likelyTests: [],
281
+ serviceImpact: { inbound: [], outbound: [] },
282
+ history: [],
283
+ warnings: ['missing workspace path for range preflight'],
284
+ module: null,
285
+ boundaries: { primary: null, crossed: [] },
286
+ source: 'tree-sitter',
287
+ };
288
+ }
289
+ const maxSymbols = options.maxSymbols ?? DEFAULT_MAX_SYMBOLS;
290
+ const maxTests = options.maxTests ?? DEFAULT_MAX_TESTS;
291
+ const maxHistory = options.maxHistory ?? DEFAULT_MAX_HISTORY;
292
+ const callerDepth = options.callerDepth ?? 2;
293
+
294
+ const dc = detectChanges(workspace, store, {
295
+ fromRef: options.fromRef, toRef: options.toRef, callerDepth,
296
+ });
297
+ if (dc.directlyChanged.length === 0) {
298
+ return {
299
+ ok: true,
300
+ mode: 'range',
301
+ range: {
302
+ fromRef: dc.fromRef, toRef: dc.toRef,
303
+ changedFiles: dc.changedFiles.length,
304
+ directHunkCount: dc.changedFiles.reduce((acc, f) => acc + f.hunks, 0),
305
+ },
306
+ touchedSymbols: [],
307
+ risk: { overall: 'low', perSymbol: [] },
308
+ likelyTests: [],
309
+ serviceImpact: { inbound: [], outbound: [] },
310
+ history: [],
311
+ warnings: ['no symbol-bearing changes detected in range'],
312
+ module: null,
313
+ boundaries: { primary: null, crossed: [] },
314
+ source: 'tree-sitter',
315
+ };
316
+ }
317
+
318
+ const touched: PreflightTouchedSymbol[] = dc.directlyChanged.slice(0, maxSymbols).map(s => ({
319
+ id: s.id, name: s.name, qualifiedName: s.qualifiedName, kind: s.kind,
320
+ file: s.filePath, lineStart: s.lineStart, lineEnd: s.lineEnd,
321
+ }));
322
+
323
+ const risks: Array<{
324
+ symbol: PreflightTouchedSymbol;
325
+ risk: 'low' | 'medium' | 'high';
326
+ score: number;
327
+ topContributors: Array<{ signal: string; value: number; contribution: number }>;
328
+ }> = [];
329
+ const likelyTests: PreflightLikelyTest[] = [];
330
+ const seenTestKey = new Set<string>();
331
+
332
+ const serviceImpact: PreflightServiceImpact = { inbound: [], outbound: [] };
333
+ const allHistory: PreflightHistoryRow[] = [];
334
+
335
+ for (const t of touched) {
336
+ const r = computeRisk(store, t.id, { callerDepth });
337
+ if (r) {
338
+ risks.push({
339
+ symbol: t,
340
+ risk: r.risk, score: r.score,
341
+ topContributors: pickTopContributions(r.signalContributions, 4),
342
+ });
343
+ }
344
+ const b = rankedBehavior(store, t.id, { limit: maxTests });
345
+ if (b) {
346
+ for (const test of b.tests) {
347
+ const key = `${test.testSymbol.file}:${test.testSymbol.lineStart}:${test.testSymbol.name}`;
348
+ if (seenTestKey.has(key)) continue;
349
+ seenTestKey.add(key);
350
+ likelyTests.push({
351
+ testSymbol: {
352
+ name: test.testSymbol.name,
353
+ qualifiedName: test.testSymbol.qualifiedName,
354
+ file: test.testSymbol.file,
355
+ lineStart: test.testSymbol.lineStart,
356
+ },
357
+ relationship: test.relationship,
358
+ specificity: test.specificity,
359
+ assertionCount: test.assertionCount,
360
+ graphDistance: test.graphDistance,
361
+ });
362
+ if (likelyTests.length >= maxTests) break;
363
+ }
364
+ }
365
+ const inbound = store.serviceLinksForHandler(t.id, { limit: 5 });
366
+ for (const l of inbound) {
367
+ serviceImpact.inbound.push({
368
+ routePath: l.routePath, routeMethod: l.routeMethod ?? l.callMethod,
369
+ protocol: l.protocol, matchKind: l.matchKind,
370
+ callerName: l.callerQualifiedName ?? l.callerName,
371
+ callerFile: l.callerFile,
372
+ });
373
+ }
374
+ const outbound = store.serviceLinksForCaller(t.id, { limit: 5 });
375
+ for (const l of outbound) {
376
+ serviceImpact.outbound.push({
377
+ routePath: l.routePath, routeMethod: l.routeMethod ?? l.callMethod,
378
+ protocol: l.protocol, matchKind: l.matchKind,
379
+ handlerName: l.handlerQualifiedName ?? l.handlerName,
380
+ handlerFile: l.handlerFile,
381
+ });
382
+ }
383
+ const history = store.getSymbolHistory(t.id, { limit: 3 });
384
+ for (const h of history) {
385
+ allHistory.push({
386
+ sha: h.commitSha, author: h.authorName, email: h.authorEmail,
387
+ committedAt: h.committedAt, message: h.message,
388
+ linesAdded: h.linesAdded, linesRemoved: h.linesRemoved,
389
+ prNumber: h.prNumber, prUrl: h.prUrl,
390
+ });
391
+ }
392
+ }
393
+ // Deduplicate history by sha and sort by committedAt DESC.
394
+ const seenSha = new Set<string>();
395
+ const dedupedHistory: PreflightHistoryRow[] = [];
396
+ allHistory.sort((a, b) => b.committedAt - a.committedAt);
397
+ for (const h of allHistory) {
398
+ if (seenSha.has(h.sha)) continue;
399
+ seenSha.add(h.sha);
400
+ dedupedHistory.push(h);
401
+ if (dedupedHistory.length >= maxHistory) break;
402
+ }
403
+
404
+ const overall: 'low' | 'medium' | 'high' = pickOverallRisk(risks.map(r => r.risk));
405
+
406
+ const warnings: string[] = [];
407
+ if (risks.some(r => r.risk === 'high')) {
408
+ warnings.push('at least one touched symbol classified as high risk');
409
+ }
410
+ if (dc.changedFiles.some(f => f.symbols.length === 0)) {
411
+ warnings.push('some changed files contain hunks outside known symbol ranges (graph may be stale)');
412
+ }
413
+
414
+ // Module label of the first touched symbol — gives the agent a single
415
+ // "you're working in X" pointer.
416
+ const moduleRow = touched.length > 0
417
+ ? store.moduleForFile((store.getSymbolById(touched[0].id)?.fileId ?? -1))
418
+ : null;
419
+
420
+ let contractChanges: ContractDiff | undefined;
421
+ if (options.oldBundle && options.newBundle) {
422
+ try {
423
+ contractChanges = await contractDiffSync(options.oldBundle, options.newBundle);
424
+ } catch (err) {
425
+ warnings.push(`contract diff failed: ${(err as Error).message}`);
426
+ }
427
+ }
428
+
429
+ return {
430
+ ok: true,
431
+ mode: 'range',
432
+ range: {
433
+ fromRef: dc.fromRef, toRef: dc.toRef,
434
+ changedFiles: dc.changedFiles.length,
435
+ directHunkCount: dc.changedFiles.reduce((acc, f) => acc + f.hunks, 0),
436
+ },
437
+ touchedSymbols: touched,
438
+ risk: { overall, perSymbol: risks },
439
+ likelyTests: likelyTests.slice(0, maxTests),
440
+ serviceImpact,
441
+ contractChanges,
442
+ history: dedupedHistory,
443
+ warnings,
444
+ module: moduleRow,
445
+ boundaries: {
446
+ primary: touched.length > 0
447
+ ? store.boundaryForFile((store.getSymbolById(touched[0].id)?.fileId ?? -1))
448
+ : null,
449
+ crossed: extractCrossedBoundariesMany(store, touched),
450
+ },
451
+ source: 'tree-sitter',
452
+ };
453
+ }
454
+
455
+ function extractCrossedBoundaries(
456
+ store: Store, symbolId: number, ownBoundaryId: number | null,
457
+ ): Array<{ id: number; label: string; kind: string; rootRelPath: string }> {
458
+ if (ownBoundaryId == null) return [];
459
+ const seen = new Map<number, { id: number; label: string; kind: string; rootRelPath: string }>();
460
+ for (const r of store.calleeBoundariesOf(symbolId)) {
461
+ if (r.boundaryId === ownBoundaryId) continue;
462
+ if (seen.has(r.boundaryId)) continue;
463
+ const meta = store.listBoundaries(10000).find(b => b.id === r.boundaryId);
464
+ if (!meta) continue;
465
+ seen.set(r.boundaryId, {
466
+ id: meta.id, label: meta.label, kind: meta.kind, rootRelPath: meta.rootRelPath,
467
+ });
468
+ }
469
+ return Array.from(seen.values());
470
+ }
471
+
472
+ function extractCrossedBoundariesMany(
473
+ store: Store, touched: PreflightTouchedSymbol[],
474
+ ): Array<{ id: number; label: string; kind: string; rootRelPath: string }> {
475
+ if (touched.length === 0) return [];
476
+ const owns = new Map<number, number | null>();
477
+ for (const s of touched) {
478
+ const meta = store.getSymbolById(s.id);
479
+ if (!meta) continue;
480
+ const b = store.boundaryForFile(meta.fileId);
481
+ owns.set(s.id, b?.id ?? null);
482
+ }
483
+ const seen = new Map<number, { id: number; label: string; kind: string; rootRelPath: string }>();
484
+ const allBoundaries = store.listBoundaries(10000);
485
+ const byId = new Map(allBoundaries.map(b => [b.id, b]));
486
+ for (const s of touched) {
487
+ const ownId = owns.get(s.id);
488
+ if (ownId == null) continue;
489
+ for (const r of store.calleeBoundariesOf(s.id)) {
490
+ if (r.boundaryId === ownId) continue;
491
+ if (seen.has(r.boundaryId)) continue;
492
+ const meta = byId.get(r.boundaryId);
493
+ if (!meta) continue;
494
+ seen.set(r.boundaryId, {
495
+ id: meta.id, label: meta.label, kind: meta.kind, rootRelPath: meta.rootRelPath,
496
+ });
497
+ }
498
+ }
499
+ return Array.from(seen.values());
500
+ }
501
+
502
+ function pickOverallRisk(verdicts: Array<'low' | 'medium' | 'high'>): 'low' | 'medium' | 'high' {
503
+ if (verdicts.includes('high')) return 'high';
504
+ if (verdicts.includes('medium')) return 'medium';
505
+ return 'low';
506
+ }
507
+
508
+ function pickTopContributions(
509
+ contributions: Array<{ signal: string; value: number; contribution: number }>,
510
+ n: number,
511
+ ): Array<{ signal: string; value: number; contribution: number }> {
512
+ return [...contributions]
513
+ .sort((a, b) => Math.abs(b.contribution) - Math.abs(a.contribution))
514
+ .slice(0, n);
515
+ }
516
+
517
+ function pickLikelyTestsFromContext(ctx: ContextPacket, maxN: number): PreflightLikelyTest[] {
518
+ return ctx.behavior.preview.slice(0, maxN).map(t => ({
519
+ testSymbol: {
520
+ name: t.name, qualifiedName: t.qualifiedName,
521
+ file: t.file, lineStart: t.lineStart,
522
+ },
523
+ relationship: t.relationship,
524
+ specificity: t.specificity,
525
+ assertionCount: t.assertionCount,
526
+ graphDistance: null,
527
+ }));
528
+ }
529
+
530
+ function collectWarnings(ctx: ContextPacket): string[] {
531
+ const out: string[] = [];
532
+ if (ctx.behavior.direct === 0 && ctx.behavior.indirect === 0) {
533
+ out.push('no direct or indirect test coverage detected');
534
+ }
535
+ if (ctx.risk.risk === 'high') {
536
+ out.push('risk classified as high; inspect signal contributions before editing');
537
+ }
538
+ if (ctx.routes.length > 0 && ctx.behavior.direct === 0) {
539
+ out.push('symbol is route-exposed but lacks direct tests');
540
+ }
541
+ return out;
542
+ }
543
+
544
+ // Wrap the async contractDiff so the sync-shaped range helper above doesn't
545
+ // need to be split into multiple async branches; we await it once.
546
+ async function contractDiffSync(oldBundle: string, newBundle: string): Promise<ContractDiff> {
547
+ return await contractDiff(oldBundle, newBundle);
548
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * v9 Track-H — minimal .proto file scanner.
3
+ *
4
+ * Parses `service X { rpc Foo(Req) returns (Resp); }` blocks with a small
5
+ * regex pipeline (no tree-sitter dependency) and emits one route per rpc
6
+ * with protocol='grpc', service=X, operation='X/Foo', method=ANY.
7
+ *
8
+ * The .proto file itself is upserted into `files` with language='proto' so
9
+ * routes have a valid file_id to reference. We do not extract symbols or
10
+ * edges from .proto — only routes — keeping the scanner deterministic and
11
+ * limited in scope.
12
+ *
13
+ * Recognized syntax (proto3-friendly):
14
+ * service UserService {
15
+ * rpc GetUser (GetUserRequest) returns (GetUserResponse);
16
+ * rpc ListUsers (ListUsersRequest) returns (stream User) {
17
+ * option (google.api.http) = { get: "/v1/users" };
18
+ * }
19
+ * }
20
+ *
21
+ * Comments (`//` and `/* * /`) are stripped before parsing so they cannot
22
+ * accidentally satisfy a rpc header pattern.
23
+ */
24
+
25
+ import fs from 'fs';
26
+ import path from 'path';
27
+ import crypto from 'crypto';
28
+ import glob from 'fast-glob';
29
+ import { Store, FileClassification } from '../db/store.js';
30
+ import { classifyFile } from './classify.js';
31
+
32
+ export interface ProtoScanResult {
33
+ filesScanned: number;
34
+ filesIndexed: number;
35
+ filesReusedFromCache: number;
36
+ fileIds: number[];
37
+ servicesFound: number;
38
+ rpcsFound: number;
39
+ }
40
+
41
+ const PROTO_SERVICE_RE = /service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/g;
42
+ const PROTO_RPC_RE = /rpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(?:stream\s+)?([A-Za-z_][A-Za-z0-9_.]*)\s*\)\s*returns\s*\(\s*(?:stream\s+)?([A-Za-z_][A-Za-z0-9_.]*)\s*\)/g;
43
+
44
+ const PROTO_CLASSIFICATION: FileClassification = {
45
+ role: 'project', isVendor: 0, isGenerated: 0,
46
+ };
47
+
48
+ export async function scanProtoFiles(
49
+ absRoot: string,
50
+ store: Store,
51
+ ): Promise<ProtoScanResult> {
52
+ const entries = await glob(['**/*.proto'], {
53
+ cwd: absRoot,
54
+ ignore: [
55
+ 'node_modules/**', '.git/**', 'dist/**', 'build/**', 'out/**',
56
+ 'vendor/**', '**/vendor/**', '**/__pycache__/**', '.next/**',
57
+ ],
58
+ onlyFiles: true, followSymbolicLinks: false, dot: false,
59
+ });
60
+ entries.sort();
61
+ const fileIds: number[] = [];
62
+ let indexed = 0;
63
+ let reused = 0;
64
+ let services = 0;
65
+ let rpcs = 0;
66
+ for (const rel of entries) {
67
+ const abs = path.join(absRoot, rel);
68
+ let src: string;
69
+ try { src = fs.readFileSync(abs, 'utf8'); }
70
+ catch { continue; }
71
+ const sha = crypto.createHash('sha1').update(src).digest('hex');
72
+ const stripped = stripProtoComments(src);
73
+ const lines = src.split('\n').length;
74
+ const classification = classifyFile(rel) ?? PROTO_CLASSIFICATION;
75
+ const { fileId, unchanged } = store.upsertFileWithCache(
76
+ abs, rel, 'proto', sha, lines, classification,
77
+ );
78
+ fileIds.push(fileId);
79
+ if (unchanged) {
80
+ reused++;
81
+ continue;
82
+ }
83
+ indexed++;
84
+ PROTO_SERVICE_RE.lastIndex = 0;
85
+ let svcMatch: RegExpExecArray | null;
86
+ while ((svcMatch = PROTO_SERVICE_RE.exec(stripped)) !== null) {
87
+ const serviceName = svcMatch[1];
88
+ const body = svcMatch[2];
89
+ const bodyOffset = svcMatch.index + svcMatch[0].indexOf('{') + 1;
90
+ services++;
91
+ PROTO_RPC_RE.lastIndex = 0;
92
+ let rpcMatch: RegExpExecArray | null;
93
+ while ((rpcMatch = PROTO_RPC_RE.exec(body)) !== null) {
94
+ const rpcName = rpcMatch[1];
95
+ const inputType = rpcMatch[2];
96
+ const outputType = rpcMatch[3];
97
+ const lineNo = byteOffsetToLine(stripped, bodyOffset + rpcMatch.index);
98
+ const operation = `${serviceName}/${rpcName}`;
99
+ store.insertRoute(
100
+ fileId,
101
+ 'ANY',
102
+ operation, // path = canonical service/method
103
+ 'grpc', // framework
104
+ null, // handler_name — handlers live in code, not .proto
105
+ lineNo,
106
+ {
107
+ protocol: 'grpc',
108
+ operation,
109
+ service: serviceName,
110
+ metadataJson: JSON.stringify({
111
+ service: serviceName,
112
+ method: rpcName,
113
+ inputType, outputType,
114
+ }),
115
+ },
116
+ );
117
+ rpcs++;
118
+ }
119
+ }
120
+ }
121
+ return {
122
+ filesScanned: entries.length,
123
+ filesIndexed: indexed,
124
+ filesReusedFromCache: reused,
125
+ fileIds,
126
+ servicesFound: services,
127
+ rpcsFound: rpcs,
128
+ };
129
+ }
130
+
131
+ /** Strip // and /* * / comments while preserving line offsets. */
132
+ function stripProtoComments(src: string): string {
133
+ // Block comments: replace with same-length runs of spaces (preserving newlines)
134
+ let out = src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
135
+ // Line comments: replace from // to end of line with spaces
136
+ out = out.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
137
+ return out;
138
+ }
139
+
140
+ /** 0-indexed line number for a given byte offset in `src`. */
141
+ function byteOffsetToLine(src: string, offset: number): number {
142
+ let line = 0;
143
+ for (let i = 0; i < offset && i < src.length; i++) {
144
+ if (src[i] === '\n') line++;
145
+ }
146
+ return line;
147
+ }