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,249 @@
1
+ /**
2
+ * Track I — Feature 2: Contract Diff between two .seerbundle artifacts.
3
+ *
4
+ * Verifies:
5
+ * - HTTP: a removed route + a method-changed route + an added route are
6
+ * all reported in the diff.
7
+ * - gRPC: an added method is reported under added.
8
+ * - Kafka: a removed topic is reported under removed.
9
+ * - The diff is computed without importing either bundle into a workspace.
10
+ * - JSON output is deterministic between runs.
11
+ * - Exit code is 0 even when breaking changes are found.
12
+ *
13
+ * Run: npx tsx tests/tracki-contract-diff.ts
14
+ */
15
+
16
+ import path from 'path';
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import { Indexer } from '../src/indexer/index';
20
+ import { Store } from '../src/db/store';
21
+ import { exportBundle } from '../src/bundle/export';
22
+ import { contractDiff, formatContractDiffTable } from '../src/bundle/contract';
23
+
24
+ const TMP = path.join(os.tmpdir(), `seer-tracki-contract-${Date.now()}`);
25
+
26
+ let passed = 0;
27
+ let failed = 0;
28
+ function assert(cond: boolean, msg: string): void {
29
+ if (cond) { console.log(` ✓ ${msg}`); passed++; }
30
+ else { console.error(` ✗ ${msg}`); failed++; }
31
+ }
32
+ function assertEq<T>(actual: T, expected: T, msg: string): void {
33
+ assert(actual === expected,
34
+ `${msg} (got ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)})`);
35
+ }
36
+ function cleanup(): void {
37
+ try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* */ }
38
+ }
39
+
40
+ interface FixtureFile { path: string; content: string }
41
+
42
+ async function buildBundle(name: string, files: FixtureFile[]): Promise<string> {
43
+ const repo = path.join(TMP, name);
44
+ fs.mkdirSync(repo, { recursive: true });
45
+ for (const f of files) {
46
+ const abs = path.join(repo, f.path);
47
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
48
+ fs.writeFileSync(abs, f.content);
49
+ }
50
+ const dbPath = path.join(TMP, `${name}.db`);
51
+ const store = new Store(dbPath);
52
+ try {
53
+ const idx = new Indexer(store);
54
+ await idx.indexDirectory(repo, { quiet: true });
55
+ } finally { store.close(); }
56
+ const bundlePath = path.join(TMP, `${name}.seerbundle`);
57
+ await exportBundle(dbPath, repo, { out: bundlePath, builtAt: 0 });
58
+ return bundlePath;
59
+ }
60
+
61
+ async function main(): Promise<void> {
62
+ console.log('\nSeer Track I — Feature 2: Contract Diff');
63
+ console.log('========================================\n');
64
+ cleanup();
65
+ fs.mkdirSync(TMP, { recursive: true });
66
+
67
+ // ── Build "old" and "new" HTTP bundles ──────────────────────────────────
68
+ console.log('── HTTP route diff ──');
69
+ const oldHttpBundle = await buildBundle('old-http', [
70
+ { path: 'routes.ts', content: `
71
+ declare const app: any;
72
+ export function listUsers(_req: any, res: any) { return res.send([]); }
73
+ export function getUser(_req: any, res: any) { return res.send({}); }
74
+ export function legacyHandler(_req: any, res: any) { return res.send('legacy'); }
75
+ app.get('/api/v1/users', listUsers);
76
+ app.get('/api/v1/users/:id', getUser);
77
+ app.post('/api/v1/legacy', legacyHandler);
78
+ ` },
79
+ ]);
80
+ const newHttpBundle = await buildBundle('new-http', [
81
+ { path: 'routes.ts', content: `
82
+ declare const app: any;
83
+ export function listUsers(_req: any, res: any) { return res.send([]); }
84
+ export function getUser(_req: any, res: any) { return res.send({}); }
85
+ export function deleteUser(_req: any, res: any) { return res.send({}); }
86
+ // legacy removed
87
+ app.get('/api/v1/users', listUsers);
88
+ // /api/v1/users/:id is now POST instead of GET
89
+ app.post('/api/v1/users/:id', getUser);
90
+ // /api/v1/users/:id/delete is new
91
+ app.delete('/api/v1/users/:id/delete', deleteUser);
92
+ ` },
93
+ ]);
94
+
95
+ const diff = await contractDiff(oldHttpBundle, newHttpBundle, { includeAffectedCallers: false });
96
+ console.log(formatContractDiffTable(diff));
97
+
98
+ // Expectations: legacy removed, delete added, /api/v1/users/:id method changed.
99
+ const legacyRemoved = diff.removed.find(r =>
100
+ r.protocol === 'http' && r.method === 'POST' && r.path === '/api/v1/legacy');
101
+ assert(legacyRemoved != null, 'removed: POST /api/v1/legacy');
102
+
103
+ const deleteAdded = diff.added.find(r =>
104
+ r.protocol === 'http' && r.method === 'DELETE' && r.path === '/api/v1/users/:id/delete');
105
+ assert(deleteAdded != null, 'added: DELETE /api/v1/users/:id/delete');
106
+
107
+ // GET /api/v1/users/:id was removed and POST /api/v1/users/:id was added —
108
+ // because our key is method|path, that's two events, not a "changed" event.
109
+ // That's by design — HTTP method change IS effectively a different endpoint.
110
+ const oldUserGet = diff.removed.find(r =>
111
+ r.protocol === 'http' && r.method === 'GET' && r.path === '/api/v1/users/:id');
112
+ const newUserPost = diff.added.find(r =>
113
+ r.protocol === 'http' && r.method === 'POST' && r.path === '/api/v1/users/:id');
114
+ assert(oldUserGet != null, 'removed: GET /api/v1/users/:id (method changed → POST)');
115
+ assert(newUserPost != null, 'added: POST /api/v1/users/:id (method changed from GET)');
116
+
117
+ // listUsers stays the same on both sides — must not appear in any of the
118
+ // three sets.
119
+ const listUsersPresent =
120
+ [...diff.added, ...diff.removed].some(r =>
121
+ r.path === '/api/v1/users' && r.method === 'GET') ||
122
+ diff.changed.some(r =>
123
+ r.before.path === '/api/v1/users' && r.before.method === 'GET');
124
+ assertEq(listUsersPresent, false, 'unchanged route does not appear in any diff bucket');
125
+
126
+ // Determinism check: run the diff a second time, JSON output identical.
127
+ const diff2 = await contractDiff(oldHttpBundle, newHttpBundle);
128
+ assertEq(JSON.stringify(diff.added), JSON.stringify(diff2.added),
129
+ 'added list is deterministic across runs');
130
+ assertEq(JSON.stringify(diff.removed), JSON.stringify(diff2.removed),
131
+ 'removed list is deterministic across runs');
132
+
133
+ // ── gRPC + Kafka — synthesize routes via SQL since proto fixtures are big.
134
+ console.log('\n── gRPC + Kafka diff via direct route injection ──');
135
+ const grpcKafkaOld = path.join(TMP, 'grpc-old');
136
+ fs.mkdirSync(grpcKafkaOld, { recursive: true });
137
+ fs.writeFileSync(path.join(grpcKafkaOld, 'placeholder.ts'),
138
+ 'export const x = 1;\n');
139
+ const grpcKafkaOldDb = path.join(TMP, 'grpc-old.db');
140
+ {
141
+ const s = new Store(grpcKafkaOldDb);
142
+ try {
143
+ const idx = new Indexer(s);
144
+ await idx.indexDirectory(grpcKafkaOld, { quiet: true });
145
+ // Inject a tRPC, gRPC, and Kafka route directly into the DB so we can
146
+ // verify cross-protocol diff without spinning up a full proto/extractor
147
+ // pipeline. file_id 1 is the placeholder file.
148
+ const fileId = (s.rawDb().prepare('SELECT id FROM files LIMIT 1').get() as { id: number }).id;
149
+ s.insertRoute(fileId, 'ANY', '', 'grpc', null, 0, {
150
+ protocol: 'grpc', service: 'UserService', operation: 'GetUser',
151
+ });
152
+ s.insertRoute(fileId, 'ANY', '', 'kafka', null, 0, {
153
+ protocol: 'kafka', topic: 'user.created', broker: 'kafka:9092',
154
+ });
155
+ s.insertRoute(fileId, 'ANY', '', 'kafka', null, 0, {
156
+ protocol: 'kafka', topic: 'user.deleted', broker: 'kafka:9092',
157
+ });
158
+ } finally { s.close(); }
159
+ }
160
+ const grpcKafkaOldBundle = path.join(TMP, 'grpc-old.seerbundle');
161
+ await exportBundle(grpcKafkaOldDb, grpcKafkaOld, { out: grpcKafkaOldBundle, builtAt: 0 });
162
+
163
+ const grpcKafkaNew = path.join(TMP, 'grpc-new');
164
+ fs.mkdirSync(grpcKafkaNew, { recursive: true });
165
+ fs.writeFileSync(path.join(grpcKafkaNew, 'placeholder.ts'),
166
+ 'export const x = 1;\n');
167
+ const grpcKafkaNewDb = path.join(TMP, 'grpc-new.db');
168
+ {
169
+ const s = new Store(grpcKafkaNewDb);
170
+ try {
171
+ const idx = new Indexer(s);
172
+ await idx.indexDirectory(grpcKafkaNew, { quiet: true });
173
+ const fileId = (s.rawDb().prepare('SELECT id FROM files LIMIT 1').get() as { id: number }).id;
174
+ s.insertRoute(fileId, 'ANY', '', 'grpc', null, 0, {
175
+ protocol: 'grpc', service: 'UserService', operation: 'GetUser',
176
+ });
177
+ // New gRPC method added.
178
+ s.insertRoute(fileId, 'ANY', '', 'grpc', null, 0, {
179
+ protocol: 'grpc', service: 'UserService', operation: 'ListUsers',
180
+ });
181
+ // user.created stays; user.deleted is REMOVED.
182
+ s.insertRoute(fileId, 'ANY', '', 'kafka', null, 0, {
183
+ protocol: 'kafka', topic: 'user.created', broker: 'kafka:9092',
184
+ });
185
+ } finally { s.close(); }
186
+ }
187
+ const grpcKafkaNewBundle = path.join(TMP, 'grpc-new.seerbundle');
188
+ await exportBundle(grpcKafkaNewDb, grpcKafkaNew, { out: grpcKafkaNewBundle, builtAt: 0 });
189
+
190
+ const gd = await contractDiff(grpcKafkaOldBundle, grpcKafkaNewBundle);
191
+ console.log(formatContractDiffTable(gd));
192
+
193
+ const addedGrpc = gd.added.find(e => e.protocol === 'grpc' && e.operation === 'ListUsers');
194
+ assert(addedGrpc != null, 'added: grpc UserService.ListUsers');
195
+
196
+ const removedTopic = gd.removed.find(e => e.protocol === 'kafka' && e.topic === 'user.deleted');
197
+ assert(removedTopic != null, 'removed: kafka user.deleted');
198
+
199
+ // Unchanged grpc method does NOT appear.
200
+ const getUserUnchanged =
201
+ [...gd.added, ...gd.removed].some(e =>
202
+ e.protocol === 'grpc' && e.operation === 'GetUser');
203
+ assertEq(getUserUnchanged, false, 'unchanged gRPC method stays out of added/removed');
204
+
205
+ // Exit-code contract: function returns successfully; CLI/MCP layer never
206
+ // throws on breaking changes (tested by call path here returning without
207
+ // exception).
208
+ assertEq(gd.totals.added >= 1, true, 'gd reports >=1 added endpoint');
209
+ assertEq(gd.totals.removed >= 1, true, 'gd reports >=1 removed endpoint');
210
+
211
+ // ── Malformed bundle is rejected with a clear message (regression) ──────
212
+ console.log('\n── Malformed / truncated bundle handling ──');
213
+ {
214
+ // Not a bundle at all.
215
+ const junk = path.join(TMP, 'junk.seerbundle');
216
+ fs.writeFileSync(junk, Buffer.from('this is not a seer bundle at all!!'));
217
+ let threw = false; let msg = '';
218
+ try { await contractDiff(junk, newHttpBundle); }
219
+ catch (err) { threw = true; msg = (err as Error).message; }
220
+ assert(threw, 'contractDiff throws on a non-bundle file');
221
+ assert(/not a seer bundle|bad magic/i.test(msg),
222
+ `non-bundle rejection is clear (got "${msg}")`);
223
+
224
+ // Valid header + magic but a manifest length that overruns the file.
225
+ const truncated = path.join(TMP, 'truncated.seerbundle');
226
+ const header = Buffer.alloc(12);
227
+ Buffer.from(oldHttpBundle && fs.readFileSync(oldHttpBundle).subarray(0, 4)).copy(header, 0);
228
+ header.writeUInt32BE(1, 4); // format version
229
+ header.writeUInt32BE(0xffffff, 8); // absurd manifest length
230
+ fs.writeFileSync(truncated, header);
231
+ let threw2 = false; let msg2 = '';
232
+ try { await contractDiff(truncated, newHttpBundle); }
233
+ catch (err) { threw2 = true; msg2 = (err as Error).message; }
234
+ assert(threw2, 'contractDiff throws on a truncated bundle');
235
+ assert(/truncat|magic|bundle/i.test(msg2),
236
+ `truncated rejection is clear, not a raw JSON error (got "${msg2}")`);
237
+ }
238
+
239
+ console.log('\n────────────────────────────');
240
+ console.log(`Passed: ${passed} Failed: ${failed}`);
241
+ cleanup();
242
+ if (failed > 0) process.exit(1);
243
+ }
244
+
245
+ main().catch(err => {
246
+ console.error(err);
247
+ cleanup();
248
+ process.exit(1);
249
+ });
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Track I — Feature 1: External Bundle Layers.
3
+ *
4
+ * Verifies:
5
+ * - Exporting a bundle from service A and importing it as an EXTERNAL
6
+ * layer into workspace B leaves B's local symbols/files unchanged.
7
+ * - External routes are queryable and clearly marked
8
+ * (source_kind='external-bundle' + external_bundle_id).
9
+ * - The service-link resolver can match a local service_call to an
10
+ * external route, producing a service_link with handler_symbol_id=NULL
11
+ * (the handler lives in the external bundle's phantom file).
12
+ * - Re-importing the same bundle is idempotent (no row duplication).
13
+ * - Re-running `seer index` does not prune external rows.
14
+ *
15
+ * Run: npx tsx tests/tracki-external-bundles.ts
16
+ */
17
+
18
+ import path from 'path';
19
+ import fs from 'fs';
20
+ import os from 'os';
21
+ import { Indexer } from '../src/indexer/index';
22
+ import { Store } from '../src/db/store';
23
+ import { exportBundle } from '../src/bundle/export';
24
+ import { importExternalBundle } from '../src/bundle/external';
25
+
26
+ const FIXTURES = path.join(__dirname, 'fixtures-tracki');
27
+ const TMP = path.join(os.tmpdir(), `seer-tracki-ext-${Date.now()}`);
28
+
29
+ let passed = 0;
30
+ let failed = 0;
31
+ function assert(cond: boolean, msg: string): void {
32
+ if (cond) { console.log(` ✓ ${msg}`); passed++; }
33
+ else { console.error(` ✗ ${msg}`); failed++; }
34
+ }
35
+ function assertEq<T>(actual: T, expected: T, msg: string): void {
36
+ assert(actual === expected,
37
+ `${msg} (got ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)})`);
38
+ }
39
+ function cleanup(): void {
40
+ try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* */ }
41
+ }
42
+
43
+ async function main(): Promise<void> {
44
+ console.log('\nSeer Track I — External Bundle Layers');
45
+ console.log('======================================\n');
46
+ cleanup();
47
+ fs.mkdirSync(TMP, { recursive: true });
48
+
49
+ // ── Step 1: Index the "billing" repo and export its bundle ──────────────
50
+ console.log('── Step 1: index + export the billing service\'s bundle ──');
51
+ const billingDb = path.join(TMP, 'billing.db');
52
+ const billingStore = new Store(billingDb);
53
+ const billingRepo = path.join(FIXTURES, 'billing');
54
+ try {
55
+ const idx = new Indexer(billingStore);
56
+ const r = await idx.indexDirectory(billingRepo, { quiet: true });
57
+ assert(r.filesIndexed >= 1, 'billing: indexed at least one file');
58
+ assert((r.routesResolved ?? 0) >= 0, 'billing: route resolution ran');
59
+ } finally { billingStore.close(); }
60
+
61
+ const bundleOut = path.join(TMP, 'billing.seerbundle');
62
+ await exportBundle(billingDb, billingRepo, {
63
+ out: bundleOut, builtAt: 0,
64
+ });
65
+ assert(fs.existsSync(bundleOut), 'billing.seerbundle exists on disk');
66
+
67
+ // ── Step 2: Index the "gateway" repo into its own DB ────────────────────
68
+ console.log('\n── Step 2: index the gateway service into its own DB ──');
69
+ const gatewayDb = path.join(TMP, 'gateway.db');
70
+ const gatewayRepo = path.join(FIXTURES, 'gateway');
71
+ let initialSymbols = 0;
72
+ let initialFiles = 0;
73
+ let initialRoutes = 0;
74
+ let initialServiceCalls = 0;
75
+ {
76
+ const store = new Store(gatewayDb);
77
+ try {
78
+ const idx = new Indexer(store);
79
+ const r = await idx.indexDirectory(gatewayRepo, { quiet: true });
80
+ assert(r.filesIndexed >= 1, 'gateway: indexed at least one file');
81
+ const stats = store.getStats();
82
+ initialSymbols = stats.symbols;
83
+ initialFiles = stats.files;
84
+ initialRoutes = stats.routes ?? 0;
85
+ initialServiceCalls = stats.serviceCalls ?? 0;
86
+ assert(initialServiceCalls >= 1, `gateway: extracted ≥1 service call (got ${initialServiceCalls})`);
87
+ assertEq(initialRoutes, 0, 'gateway: has 0 local routes (it is a client only)');
88
+ } finally { store.close(); }
89
+ }
90
+
91
+ // ── Step 3: Import the billing bundle into gateway as EXTERNAL ─────────
92
+ console.log('\n── Step 3: external import of billing.seerbundle into gateway ──');
93
+ let firstImportBundleId = 0;
94
+ {
95
+ const store = new Store(gatewayDb);
96
+ try {
97
+ const r = await importExternalBundle(bundleOut, store, { alias: 'billing' });
98
+ assertEq(r.alreadyImported, false, 'first import is not a no-op');
99
+ assert(r.routesImported >= 2, `imported ≥2 external routes (got ${r.routesImported})`);
100
+ assertEq(r.externalProject, 'billing', 'externalProject alias persisted');
101
+ firstImportBundleId = r.bundleId;
102
+
103
+ // Full marks: local symbols + local files are UNCHANGED by the import
104
+ // (apart from the phantom file added for the external layer).
105
+ const stats = store.getStats();
106
+ assertEq(stats.symbols, initialSymbols, 'local symbols count is unchanged after import');
107
+ assertEq(stats.files, initialFiles + 1, 'files count increased by exactly 1 (phantom external file)');
108
+
109
+ // External routes are queryable with clear provenance.
110
+ const layers = store.listExternalBundles();
111
+ assertEq(layers.length, 1, 'one external bundle layer recorded');
112
+ assertEq(layers[0].sourceKind, 'external-bundle', 'sourceKind is "external-bundle"');
113
+ assertEq(layers[0].externalProject, 'billing', 'externalProject matches alias');
114
+
115
+ const externals = store.listExternalRoutes({ bundleId: firstImportBundleId });
116
+ assert(externals.length >= 2, `external routes are queryable (got ${externals.length})`);
117
+ const chargeRoute = externals.find(r => r.path === '/api/charge');
118
+ assert(chargeRoute != null, 'external POST /api/charge is queryable');
119
+ assertEq(chargeRoute?.externalProject ?? null, 'billing',
120
+ 'external route carries externalProject');
121
+
122
+ // External routes also appear in the normal listRoutes() (they are
123
+ // still routes, just with external_bundle_id set).
124
+ const allRoutes = store.listRoutes({});
125
+ const externalCount = allRoutes.filter(r => r.path === '/api/charge').length;
126
+ assert(externalCount >= 1, 'listRoutes() includes external route paths');
127
+ } finally { store.close(); }
128
+ }
129
+
130
+ // ── Step 4: Re-import is idempotent (alreadyImported=true) ──────────────
131
+ console.log('\n── Step 4: idempotent re-import ──');
132
+ {
133
+ const store = new Store(gatewayDb);
134
+ try {
135
+ const r2 = await importExternalBundle(bundleOut, store, { alias: 'billing' });
136
+ assertEq(r2.alreadyImported, true, 'second import with unchanged hash is a no-op');
137
+ assertEq(r2.bundleId, firstImportBundleId, 'bundleId is stable across re-imports');
138
+ const layers = store.listExternalBundles();
139
+ assertEq(layers.length, 1, 'still exactly one external bundle layer (no duplication)');
140
+ const stats = store.getStats();
141
+ assertEq(stats.files, initialFiles + 1, 'files count still unchanged after idempotent re-import');
142
+ } finally { store.close(); }
143
+ }
144
+
145
+ // ── Step 5: Re-running the indexer does NOT prune the external layer ────
146
+ console.log('\n── Step 5: local re-index keeps external rows ──');
147
+ {
148
+ const store = new Store(gatewayDb);
149
+ try {
150
+ const idx = new Indexer(store);
151
+ await idx.indexDirectory(gatewayRepo, { quiet: true });
152
+ const layers = store.listExternalBundles();
153
+ assertEq(layers.length, 1, 'external bundle layer survived a fresh index pass');
154
+ const externals = store.listExternalRoutes({ bundleId: firstImportBundleId });
155
+ assert(externals.length >= 2, `external routes survived re-index (got ${externals.length})`);
156
+ } finally { store.close(); }
157
+ }
158
+
159
+ // ── Step 6: Service-link resolver matches local calls to external routes ─
160
+ console.log('\n── Step 6: service-link resolver matches against external routes ──');
161
+ {
162
+ const store = new Store(gatewayDb);
163
+ try {
164
+ const links = store.listServiceLinks({});
165
+ assert(links.length >= 1, `at least one service_link produced (got ${links.length})`);
166
+ // The /api/charge call from gateway/client.ts should link to the external
167
+ // /api/charge route. handlerSymbolId may be NULL (it's external code).
168
+ const chargeLink = links.find(l =>
169
+ (l.routePath === '/api/charge') || (l.callNormalizedPath === '/api/charge'));
170
+ assert(chargeLink != null, 'gateway → /api/charge service link is present');
171
+ if (chargeLink) {
172
+ assert(chargeLink.confidence > 0.5, `chargeLink confidence > 0.5 (got ${chargeLink.confidence})`);
173
+ }
174
+ } finally { store.close(); }
175
+ }
176
+
177
+ // ── Step 7: Forced re-import after rebuilding a different hash ──────────
178
+ console.log('\n── Step 7: force re-import replaces the layer atomically ──');
179
+ // Build a SECOND bundle with a different hash (write a stub file into billing)
180
+ // then export again so dbSha256 differs.
181
+ const extraFile = path.join(billingRepo, '__stub.ts');
182
+ fs.writeFileSync(extraFile, 'export const newStub = 1;\n');
183
+ try {
184
+ const re = new Store(billingDb);
185
+ try {
186
+ const idx = new Indexer(re);
187
+ await idx.indexDirectory(billingRepo, { quiet: true });
188
+ } finally { re.close(); }
189
+ const bundleOut2 = path.join(TMP, 'billing-v2.seerbundle');
190
+ await exportBundle(billingDb, billingRepo, { out: bundleOut2, builtAt: 0 });
191
+
192
+ // Reuse the SAME bundle path slot by overwriting. Since importExternalBundle
193
+ // keys on bundle_path, we'll import the new file at a NEW path so the
194
+ // re-import path triggers properly. The hash differs so it must NOT skip.
195
+ const newBundlePath = path.join(TMP, 'billing.seerbundle');
196
+ fs.copyFileSync(bundleOut2, newBundlePath);
197
+
198
+ const store = new Store(gatewayDb);
199
+ try {
200
+ const r3 = await importExternalBundle(newBundlePath, store, {
201
+ alias: 'billing', force: true,
202
+ });
203
+ assertEq(r3.alreadyImported, false, 'forced re-import with new hash is not a no-op');
204
+ // Layer count stays at 1 — same path replaces the old layer.
205
+ const layers = store.listExternalBundles();
206
+ assertEq(layers.length, 1, 'still exactly one external bundle layer after replace');
207
+ } finally { store.close(); }
208
+ } finally {
209
+ try { fs.unlinkSync(extraFile); } catch { /* */ }
210
+ }
211
+
212
+ // ── Step 8: integrity — a tampered dbSha256 is REJECTED (bug regression) ──
213
+ console.log('\n── Step 8: tampered bundle integrity is rejected ──');
214
+ {
215
+ const tampered = path.join(TMP, 'tampered.seerbundle');
216
+ rewriteManifest(bundleOut, tampered, (m) => { m.dbSha256 = '0'.repeat(64); });
217
+ const store = new Store(gatewayDb);
218
+ let threw = false;
219
+ let msg = '';
220
+ try {
221
+ await importExternalBundle(tampered, store, { alias: 'tampered', force: true });
222
+ } catch (err) { threw = true; msg = (err as Error).message; }
223
+ finally { store.close(); }
224
+ assert(threw, 'import of a hash-tampered bundle throws');
225
+ assert(/integrity/i.test(msg), `rejection mentions integrity (got "${msg}")`);
226
+ // The failed import must NOT have left a partial layer behind.
227
+ const verify = new Store(gatewayDb);
228
+ try {
229
+ const stray = verify.listExternalBundles().find(l => l.externalProject === 'tampered');
230
+ assert(stray == null, 'no partial "tampered" layer left after rejected import');
231
+ } finally { verify.close(); }
232
+ }
233
+
234
+ // ── Step 9: a newer schemaVersion is rejected (bug regression) ───────────
235
+ console.log('\n── Step 9: bundle with newer schemaVersion is rejected ──');
236
+ {
237
+ const futureBundle = path.join(TMP, 'future.seerbundle');
238
+ rewriteManifest(bundleOut, futureBundle, (m) => { m.schemaVersion = 9999; });
239
+ const store = new Store(gatewayDb);
240
+ let threw = false; let msg = '';
241
+ try {
242
+ await importExternalBundle(futureBundle, store, { alias: 'future', force: true });
243
+ } catch (err) { threw = true; msg = (err as Error).message; }
244
+ finally { store.close(); }
245
+ assert(threw, 'import of a future-schema bundle throws');
246
+ assert(/schemaVersion|newer/i.test(msg), `rejection mentions schema (got "${msg}")`);
247
+ }
248
+
249
+ // ── Step 10: forced re-import does NOT leak sibling phantom files ─────────
250
+ console.log('\n── Step 10: force re-import keeps phantom files == layers ──');
251
+ {
252
+ const siblingDb = path.join(TMP, 'sibling.db');
253
+ {
254
+ const store = new Store(siblingDb);
255
+ try {
256
+ const idx = new Indexer(store);
257
+ await idx.indexDirectory(gatewayRepo, { quiet: true });
258
+ } finally { store.close(); }
259
+ }
260
+ // Two distinct external layers (same payload, two paths → two layers).
261
+ const layerApath = path.join(TMP, 'layerA.seerbundle');
262
+ const layerBpath = path.join(TMP, 'layerB.seerbundle');
263
+ fs.copyFileSync(bundleOut, layerApath);
264
+ fs.copyFileSync(bundleOut, layerBpath);
265
+ {
266
+ const store = new Store(siblingDb);
267
+ try {
268
+ await importExternalBundle(layerApath, store, { alias: 'A' });
269
+ await importExternalBundle(layerBpath, store, { alias: 'B' });
270
+ assertEq(store.listExternalBundles().length, 2, 'two sibling layers imported');
271
+ assertEq(store.listExternalPhantomFileIds().length, 2,
272
+ 'two phantom files for two layers');
273
+ // Force re-import of layer A — must replace in place, not orphan a phantom.
274
+ await importExternalBundle(layerApath, store, { alias: 'A', force: true });
275
+ assertEq(store.listExternalBundles().length, 2,
276
+ 'still two layers after forced re-import of A');
277
+ assertEq(store.listExternalPhantomFileIds().length, 2,
278
+ 'still exactly two phantom files (no leak)');
279
+ } finally { store.close(); }
280
+ }
281
+ }
282
+
283
+ // ── Step 11: service links are visible immediately after import ──────────
284
+ // (no re-index needed — bug regression for stale service_links)
285
+ console.log('\n── Step 11: service links resolved at import time (no re-index) ──');
286
+ {
287
+ const freshDb = path.join(TMP, 'fresh-gateway.db');
288
+ {
289
+ const store = new Store(freshDb);
290
+ try {
291
+ const idx = new Indexer(store);
292
+ await idx.indexDirectory(gatewayRepo, { quiet: true });
293
+ } finally { store.close(); }
294
+ }
295
+ const store = new Store(freshDb);
296
+ try {
297
+ const before = store.listServiceLinks({}).length;
298
+ const r = await importExternalBundle(bundleOut, store, { alias: 'billing' });
299
+ assertEq(r.alreadyImported, false, 'fresh import is not a no-op');
300
+ // Without any re-index, the link should already exist.
301
+ const links = store.listServiceLinks({});
302
+ const chargeLink = links.find(l =>
303
+ (l.routePath === '/api/charge') || (l.callNormalizedPath === '/api/charge'));
304
+ assert(chargeLink != null,
305
+ `/api/charge link present immediately after import (before=${before}, after=${links.length})`);
306
+ } finally { store.close(); }
307
+ }
308
+
309
+ console.log('\n────────────────────────────');
310
+ console.log(`Passed: ${passed} Failed: ${failed}`);
311
+ cleanup();
312
+ if (failed > 0) process.exit(1);
313
+ }
314
+
315
+ /**
316
+ * Read a .seerbundle, mutate its manifest JSON via `mutate`, and write a new
317
+ * bundle with a corrected manifest length header. The gzip DB payload is
318
+ * copied verbatim so its bytes (and thus the *real* sha256) are unchanged —
319
+ * letting us test that a manifest claiming the wrong dbSha256 is rejected.
320
+ */
321
+ function rewriteManifest(
322
+ src: string, dst: string, mutate: (m: Record<string, unknown>) => void,
323
+ ): void {
324
+ const buf = fs.readFileSync(src);
325
+ const manifestLen = buf.readUInt32BE(8);
326
+ const manifestEnd = 12 + manifestLen;
327
+ const manifest = JSON.parse(buf.slice(12, manifestEnd).toString('utf8'));
328
+ mutate(manifest);
329
+ const newManifest = Buffer.from(JSON.stringify(manifest), 'utf8');
330
+ const header = Buffer.alloc(12);
331
+ buf.copy(header, 0, 0, 8); // magic (4) + format version (4)
332
+ header.writeUInt32BE(newManifest.length, 8);
333
+ const payload = buf.slice(manifestEnd); // verbatim gzip DB
334
+ fs.writeFileSync(dst, Buffer.concat([header, newManifest, payload]));
335
+ }
336
+
337
+ main().catch(err => {
338
+ console.error(err);
339
+ cleanup();
340
+ process.exit(1);
341
+ });