@tyroneross/navgator 0.2.2 → 0.9.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 (692) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +21 -7
  3. package/.claude-plugin/plugin.json +16 -11
  4. package/.codex-plugin/plugin.json +31 -0
  5. package/.mcp.json +8 -0
  6. package/CLAUDE.md +197 -23
  7. package/LICENSE +202 -21
  8. package/README.md +220 -33
  9. package/agents/architecture-advisor.md +6 -2
  10. package/agents/architecture-investigator.md +163 -0
  11. package/agents/architecture-planner.md +160 -0
  12. package/agents/external-resolver.md +97 -0
  13. package/dist/__tests__/agent-output.test.d.ts +5 -0
  14. package/dist/__tests__/agent-output.test.d.ts.map +1 -0
  15. package/dist/__tests__/agent-output.test.js +233 -0
  16. package/dist/__tests__/agent-output.test.js.map +1 -0
  17. package/dist/__tests__/architecture-insights-stack.test.d.ts +21 -0
  18. package/dist/__tests__/architecture-insights-stack.test.d.ts.map +1 -0
  19. package/dist/__tests__/architecture-insights-stack.test.js +86 -0
  20. package/dist/__tests__/architecture-insights-stack.test.js.map +1 -0
  21. package/dist/__tests__/architecture-insights.test.d.ts +2 -0
  22. package/dist/__tests__/architecture-insights.test.d.ts.map +1 -0
  23. package/dist/__tests__/architecture-insights.test.js +46 -0
  24. package/dist/__tests__/architecture-insights.test.js.map +1 -0
  25. package/dist/__tests__/audit-sampler.test.d.ts +10 -0
  26. package/dist/__tests__/audit-sampler.test.d.ts.map +1 -0
  27. package/dist/__tests__/audit-sampler.test.js +172 -0
  28. package/dist/__tests__/audit-sampler.test.js.map +1 -0
  29. package/dist/__tests__/audit-spc.test.d.ts +5 -0
  30. package/dist/__tests__/audit-spc.test.d.ts.map +1 -0
  31. package/dist/__tests__/audit-spc.test.js +94 -0
  32. package/dist/__tests__/audit-spc.test.js.map +1 -0
  33. package/dist/__tests__/audit-verifiers.test.d.ts +5 -0
  34. package/dist/__tests__/audit-verifiers.test.d.ts.map +1 -0
  35. package/dist/__tests__/audit-verifiers.test.js +248 -0
  36. package/dist/__tests__/audit-verifiers.test.js.map +1 -0
  37. package/dist/__tests__/auto-refresh.test.d.ts +12 -0
  38. package/dist/__tests__/auto-refresh.test.d.ts.map +1 -0
  39. package/dist/__tests__/auto-refresh.test.js +236 -0
  40. package/dist/__tests__/auto-refresh.test.js.map +1 -0
  41. package/dist/__tests__/bare-imports.test.d.ts +8 -0
  42. package/dist/__tests__/bare-imports.test.d.ts.map +1 -0
  43. package/dist/__tests__/bare-imports.test.js +176 -0
  44. package/dist/__tests__/bare-imports.test.js.map +1 -0
  45. package/dist/__tests__/classify.test.d.ts +5 -0
  46. package/dist/__tests__/classify.test.d.ts.map +1 -0
  47. package/dist/__tests__/classify.test.js +158 -0
  48. package/dist/__tests__/classify.test.js.map +1 -0
  49. package/dist/__tests__/cli-commands.test.d.ts +8 -0
  50. package/dist/__tests__/cli-commands.test.d.ts.map +1 -0
  51. package/dist/__tests__/cli-commands.test.js +207 -0
  52. package/dist/__tests__/cli-commands.test.js.map +1 -0
  53. package/dist/__tests__/consolidated-readers.test.d.ts +23 -0
  54. package/dist/__tests__/consolidated-readers.test.d.ts.map +1 -0
  55. package/dist/__tests__/consolidated-readers.test.js +200 -0
  56. package/dist/__tests__/consolidated-readers.test.js.map +1 -0
  57. package/dist/__tests__/coverage.test.d.ts +5 -0
  58. package/dist/__tests__/coverage.test.d.ts.map +1 -0
  59. package/dist/__tests__/coverage.test.js +120 -0
  60. package/dist/__tests__/coverage.test.js.map +1 -0
  61. package/dist/__tests__/deploy-scanner-runtime.test.d.ts +6 -0
  62. package/dist/__tests__/deploy-scanner-runtime.test.d.ts.map +1 -0
  63. package/dist/__tests__/deploy-scanner-runtime.test.js +168 -0
  64. package/dist/__tests__/deploy-scanner-runtime.test.js.map +1 -0
  65. package/dist/__tests__/env-scanner.test.d.ts +2 -0
  66. package/dist/__tests__/env-scanner.test.d.ts.map +1 -0
  67. package/dist/__tests__/env-scanner.test.js +191 -0
  68. package/dist/__tests__/env-scanner.test.js.map +1 -0
  69. package/dist/__tests__/freshness/cli-freshness.test.d.ts +2 -0
  70. package/dist/__tests__/freshness/cli-freshness.test.d.ts.map +1 -0
  71. package/dist/__tests__/freshness/cli-freshness.test.js +26 -0
  72. package/dist/__tests__/freshness/cli-freshness.test.js.map +1 -0
  73. package/dist/__tests__/freshness/dirty-ledger.test.d.ts +2 -0
  74. package/dist/__tests__/freshness/dirty-ledger.test.d.ts.map +1 -0
  75. package/dist/__tests__/freshness/dirty-ledger.test.js +39 -0
  76. package/dist/__tests__/freshness/dirty-ledger.test.js.map +1 -0
  77. package/dist/__tests__/freshness/drainer.test.d.ts +2 -0
  78. package/dist/__tests__/freshness/drainer.test.d.ts.map +1 -0
  79. package/dist/__tests__/freshness/drainer.test.js +103 -0
  80. package/dist/__tests__/freshness/drainer.test.js.map +1 -0
  81. package/dist/__tests__/freshness/paths.test.d.ts +2 -0
  82. package/dist/__tests__/freshness/paths.test.d.ts.map +1 -0
  83. package/dist/__tests__/freshness/paths.test.js +19 -0
  84. package/dist/__tests__/freshness/paths.test.js.map +1 -0
  85. package/dist/__tests__/freshness/scan-lock.test.d.ts +2 -0
  86. package/dist/__tests__/freshness/scan-lock.test.d.ts.map +1 -0
  87. package/dist/__tests__/freshness/scan-lock.test.js +40 -0
  88. package/dist/__tests__/freshness/scan-lock.test.js.map +1 -0
  89. package/dist/__tests__/freshness/stamp.test.d.ts +2 -0
  90. package/dist/__tests__/freshness/stamp.test.d.ts.map +1 -0
  91. package/dist/__tests__/freshness/stamp.test.js +36 -0
  92. package/dist/__tests__/freshness/stamp.test.js.map +1 -0
  93. package/dist/__tests__/gitignore-safety.test.d.ts +2 -0
  94. package/dist/__tests__/gitignore-safety.test.d.ts.map +1 -0
  95. package/dist/__tests__/gitignore-safety.test.js +110 -0
  96. package/dist/__tests__/gitignore-safety.test.js.map +1 -0
  97. package/dist/__tests__/helpers.d.ts +37 -0
  98. package/dist/__tests__/helpers.d.ts.map +1 -0
  99. package/dist/__tests__/helpers.js +134 -0
  100. package/dist/__tests__/helpers.js.map +1 -0
  101. package/dist/__tests__/impact.test.d.ts +5 -0
  102. package/dist/__tests__/impact.test.d.ts.map +1 -0
  103. package/dist/__tests__/impact.test.js +221 -0
  104. package/dist/__tests__/impact.test.js.map +1 -0
  105. package/dist/__tests__/lessons-store.test.d.ts +8 -0
  106. package/dist/__tests__/lessons-store.test.d.ts.map +1 -0
  107. package/dist/__tests__/lessons-store.test.js +232 -0
  108. package/dist/__tests__/lessons-store.test.js.map +1 -0
  109. package/dist/__tests__/llm-dedup.test.d.ts +2 -0
  110. package/dist/__tests__/llm-dedup.test.d.ts.map +1 -0
  111. package/dist/__tests__/llm-dedup.test.js +155 -0
  112. package/dist/__tests__/llm-dedup.test.js.map +1 -0
  113. package/dist/__tests__/mjs-frontend-fetch.test.d.ts +19 -0
  114. package/dist/__tests__/mjs-frontend-fetch.test.d.ts.map +1 -0
  115. package/dist/__tests__/mjs-frontend-fetch.test.js +179 -0
  116. package/dist/__tests__/mjs-frontend-fetch.test.js.map +1 -0
  117. package/dist/__tests__/multi-stack-discovery.test.d.ts +11 -0
  118. package/dist/__tests__/multi-stack-discovery.test.d.ts.map +1 -0
  119. package/dist/__tests__/multi-stack-discovery.test.js +75 -0
  120. package/dist/__tests__/multi-stack-discovery.test.js.map +1 -0
  121. package/dist/__tests__/per-entity-files-gate.test.d.ts +22 -0
  122. package/dist/__tests__/per-entity-files-gate.test.d.ts.map +1 -0
  123. package/dist/__tests__/per-entity-files-gate.test.js +160 -0
  124. package/dist/__tests__/per-entity-files-gate.test.js.map +1 -0
  125. package/dist/__tests__/prisma-calls.test.d.ts +2 -0
  126. package/dist/__tests__/prisma-calls.test.d.ts.map +1 -0
  127. package/dist/__tests__/prisma-calls.test.js +125 -0
  128. package/dist/__tests__/prisma-calls.test.js.map +1 -0
  129. package/dist/__tests__/prisma-parser.test.d.ts +2 -0
  130. package/dist/__tests__/prisma-parser.test.d.ts.map +1 -0
  131. package/dist/__tests__/prisma-parser.test.js +252 -0
  132. package/dist/__tests__/prisma-parser.test.js.map +1 -0
  133. package/dist/__tests__/prompt-detector.test.d.ts +5 -0
  134. package/dist/__tests__/prompt-detector.test.d.ts.map +1 -0
  135. package/dist/__tests__/prompt-detector.test.js +75 -0
  136. package/dist/__tests__/prompt-detector.test.js.map +1 -0
  137. package/dist/__tests__/queue-scanner.test.d.ts +5 -0
  138. package/dist/__tests__/queue-scanner.test.d.ts.map +1 -0
  139. package/dist/__tests__/queue-scanner.test.js +85 -0
  140. package/dist/__tests__/queue-scanner.test.js.map +1 -0
  141. package/dist/__tests__/resolve.test.d.ts +5 -0
  142. package/dist/__tests__/resolve.test.d.ts.map +1 -0
  143. package/dist/__tests__/resolve.test.js +196 -0
  144. package/dist/__tests__/resolve.test.js.map +1 -0
  145. package/dist/__tests__/rules.test.d.ts +2 -0
  146. package/dist/__tests__/rules.test.d.ts.map +1 -0
  147. package/dist/__tests__/rules.test.js +343 -0
  148. package/dist/__tests__/rules.test.js.map +1 -0
  149. package/dist/__tests__/sandbox.test.d.ts +5 -0
  150. package/dist/__tests__/sandbox.test.d.ts.map +1 -0
  151. package/dist/__tests__/sandbox.test.js +189 -0
  152. package/dist/__tests__/sandbox.test.js.map +1 -0
  153. package/dist/__tests__/scanner-audit.test.d.ts +9 -0
  154. package/dist/__tests__/scanner-audit.test.d.ts.map +1 -0
  155. package/dist/__tests__/scanner-audit.test.js +64 -0
  156. package/dist/__tests__/scanner-audit.test.js.map +1 -0
  157. package/dist/__tests__/scanner-characterization.test.d.ts +16 -0
  158. package/dist/__tests__/scanner-characterization.test.d.ts.map +1 -0
  159. package/dist/__tests__/scanner-characterization.test.js +167 -0
  160. package/dist/__tests__/scanner-characterization.test.js.map +1 -0
  161. package/dist/__tests__/scanner-incremental.test.d.ts +13 -0
  162. package/dist/__tests__/scanner-incremental.test.d.ts.map +1 -0
  163. package/dist/__tests__/scanner-incremental.test.js +725 -0
  164. package/dist/__tests__/scanner-incremental.test.js.map +1 -0
  165. package/dist/__tests__/scanner-integration.test.d.ts +7 -0
  166. package/dist/__tests__/scanner-integration.test.d.ts.map +1 -0
  167. package/dist/__tests__/scanner-integration.test.js +211 -0
  168. package/dist/__tests__/scanner-integration.test.js.map +1 -0
  169. package/dist/__tests__/scip-new-catches.test.d.ts +19 -0
  170. package/dist/__tests__/scip-new-catches.test.d.ts.map +1 -0
  171. package/dist/__tests__/scip-new-catches.test.js +90 -0
  172. package/dist/__tests__/scip-new-catches.test.js.map +1 -0
  173. package/dist/__tests__/subgraph.test.d.ts +5 -0
  174. package/dist/__tests__/subgraph.test.d.ts.map +1 -0
  175. package/dist/__tests__/subgraph.test.js +145 -0
  176. package/dist/__tests__/subgraph.test.js.map +1 -0
  177. package/dist/__tests__/trace.test.d.ts +5 -0
  178. package/dist/__tests__/trace.test.d.ts.map +1 -0
  179. package/dist/__tests__/trace.test.js +221 -0
  180. package/dist/__tests__/trace.test.js.map +1 -0
  181. package/dist/agent-output.d.ts +16 -0
  182. package/dist/agent-output.d.ts.map +1 -0
  183. package/dist/agent-output.js +142 -0
  184. package/dist/agent-output.js.map +1 -0
  185. package/dist/architecture-insights.d.ts +17 -0
  186. package/dist/architecture-insights.d.ts.map +1 -0
  187. package/dist/architecture-insights.js +178 -0
  188. package/dist/architecture-insights.js.map +1 -0
  189. package/dist/audit/index.d.ts +69 -0
  190. package/dist/audit/index.d.ts.map +1 -0
  191. package/dist/audit/index.js +255 -0
  192. package/dist/audit/index.js.map +1 -0
  193. package/dist/audit/sampler.d.ts +98 -0
  194. package/dist/audit/sampler.d.ts.map +1 -0
  195. package/dist/audit/sampler.js +298 -0
  196. package/dist/audit/sampler.js.map +1 -0
  197. package/dist/audit/spc.d.ts +62 -0
  198. package/dist/audit/spc.d.ts.map +1 -0
  199. package/dist/audit/spc.js +81 -0
  200. package/dist/audit/spc.js.map +1 -0
  201. package/dist/audit/verifiers.d.ts +81 -0
  202. package/dist/audit/verifiers.d.ts.map +1 -0
  203. package/dist/audit/verifiers.js +366 -0
  204. package/dist/audit/verifiers.js.map +1 -0
  205. package/dist/classify.d.ts +19 -0
  206. package/dist/classify.d.ts.map +1 -0
  207. package/dist/classify.js +124 -0
  208. package/dist/classify.js.map +1 -0
  209. package/dist/cli/commands/connections.d.ts +3 -0
  210. package/dist/cli/commands/connections.d.ts.map +1 -0
  211. package/dist/cli/commands/connections.js +125 -0
  212. package/dist/cli/commands/connections.js.map +1 -0
  213. package/dist/cli/commands/coverage.d.ts +3 -0
  214. package/dist/cli/commands/coverage.d.ts.map +1 -0
  215. package/dist/cli/commands/coverage.js +94 -0
  216. package/dist/cli/commands/coverage.js.map +1 -0
  217. package/dist/cli/commands/dead.d.ts +3 -0
  218. package/dist/cli/commands/dead.d.ts.map +1 -0
  219. package/dist/cli/commands/dead.js +80 -0
  220. package/dist/cli/commands/dead.js.map +1 -0
  221. package/dist/cli/commands/diagram.d.ts +3 -0
  222. package/dist/cli/commands/diagram.d.ts.map +1 -0
  223. package/dist/cli/commands/diagram.js +102 -0
  224. package/dist/cli/commands/diagram.js.map +1 -0
  225. package/dist/cli/commands/find.d.ts +3 -0
  226. package/dist/cli/commands/find.d.ts.map +1 -0
  227. package/dist/cli/commands/find.js +128 -0
  228. package/dist/cli/commands/find.js.map +1 -0
  229. package/dist/cli/commands/freshness.d.ts +20 -0
  230. package/dist/cli/commands/freshness.d.ts.map +1 -0
  231. package/dist/cli/commands/freshness.js +90 -0
  232. package/dist/cli/commands/freshness.js.map +1 -0
  233. package/dist/cli/commands/helpers.d.ts +7 -0
  234. package/dist/cli/commands/helpers.d.ts.map +1 -0
  235. package/dist/cli/commands/helpers.js +30 -0
  236. package/dist/cli/commands/helpers.js.map +1 -0
  237. package/dist/cli/commands/impact.d.ts +3 -0
  238. package/dist/cli/commands/impact.d.ts.map +1 -0
  239. package/dist/cli/commands/impact.js +172 -0
  240. package/dist/cli/commands/impact.js.map +1 -0
  241. package/dist/cli/commands/lessons.d.ts +6 -0
  242. package/dist/cli/commands/lessons.d.ts.map +1 -0
  243. package/dist/cli/commands/lessons.js +279 -0
  244. package/dist/cli/commands/lessons.js.map +1 -0
  245. package/dist/cli/commands/list.d.ts +3 -0
  246. package/dist/cli/commands/list.d.ts.map +1 -0
  247. package/dist/cli/commands/list.js +91 -0
  248. package/dist/cli/commands/list.js.map +1 -0
  249. package/dist/cli/commands/llm-map.d.ts +3 -0
  250. package/dist/cli/commands/llm-map.d.ts.map +1 -0
  251. package/dist/cli/commands/llm-map.js +121 -0
  252. package/dist/cli/commands/llm-map.js.map +1 -0
  253. package/dist/cli/commands/misc.d.ts +17 -0
  254. package/dist/cli/commands/misc.d.ts.map +1 -0
  255. package/dist/cli/commands/misc.js +495 -0
  256. package/dist/cli/commands/misc.js.map +1 -0
  257. package/dist/cli/commands/prompts.d.ts +3 -0
  258. package/dist/cli/commands/prompts.d.ts.map +1 -0
  259. package/dist/cli/commands/prompts.js +74 -0
  260. package/dist/cli/commands/prompts.js.map +1 -0
  261. package/dist/cli/commands/rules.d.ts +3 -0
  262. package/dist/cli/commands/rules.d.ts.map +1 -0
  263. package/dist/cli/commands/rules.js +61 -0
  264. package/dist/cli/commands/rules.js.map +1 -0
  265. package/dist/cli/commands/scan.d.ts +3 -0
  266. package/dist/cli/commands/scan.d.ts.map +1 -0
  267. package/dist/cli/commands/scan.js +177 -0
  268. package/dist/cli/commands/scan.js.map +1 -0
  269. package/dist/cli/commands/schema.d.ts +3 -0
  270. package/dist/cli/commands/schema.d.ts.map +1 -0
  271. package/dist/cli/commands/schema.js +126 -0
  272. package/dist/cli/commands/schema.js.map +1 -0
  273. package/dist/cli/commands/status.d.ts +3 -0
  274. package/dist/cli/commands/status.d.ts.map +1 -0
  275. package/dist/cli/commands/status.js +340 -0
  276. package/dist/cli/commands/status.js.map +1 -0
  277. package/dist/cli/commands/subgraph.d.ts +3 -0
  278. package/dist/cli/commands/subgraph.d.ts.map +1 -0
  279. package/dist/cli/commands/subgraph.js +55 -0
  280. package/dist/cli/commands/subgraph.js.map +1 -0
  281. package/dist/cli/commands/temporal.d.ts +3 -0
  282. package/dist/cli/commands/temporal.d.ts.map +1 -0
  283. package/dist/cli/commands/temporal.js +112 -0
  284. package/dist/cli/commands/temporal.js.map +1 -0
  285. package/dist/cli/commands/trace.d.ts +3 -0
  286. package/dist/cli/commands/trace.d.ts.map +1 -0
  287. package/dist/cli/commands/trace.js +65 -0
  288. package/dist/cli/commands/trace.js.map +1 -0
  289. package/dist/cli/index.js +88 -825
  290. package/dist/cli/index.js.map +1 -1
  291. package/dist/config.d.ts +13 -2
  292. package/dist/config.d.ts.map +1 -1
  293. package/dist/config.js +106 -12
  294. package/dist/config.js.map +1 -1
  295. package/dist/coverage.d.ts +37 -0
  296. package/dist/coverage.d.ts.map +1 -0
  297. package/dist/coverage.js +177 -0
  298. package/dist/coverage.js.map +1 -0
  299. package/dist/diagram.d.ts.map +1 -1
  300. package/dist/diagram.js +41 -0
  301. package/dist/diagram.js.map +1 -1
  302. package/dist/diff.d.ts +57 -0
  303. package/dist/diff.d.ts.map +1 -0
  304. package/dist/diff.js +527 -0
  305. package/dist/diff.js.map +1 -0
  306. package/dist/enrich/cache.d.ts +41 -0
  307. package/dist/enrich/cache.d.ts.map +1 -0
  308. package/dist/enrich/cache.js +97 -0
  309. package/dist/enrich/cache.js.map +1 -0
  310. package/dist/enrich/external-enrichment.types.d.ts +91 -0
  311. package/dist/enrich/external-enrichment.types.d.ts.map +1 -0
  312. package/dist/enrich/external-enrichment.types.js +38 -0
  313. package/dist/enrich/external-enrichment.types.js.map +1 -0
  314. package/dist/enrich/external-resolver.d.ts +95 -0
  315. package/dist/enrich/external-resolver.d.ts.map +1 -0
  316. package/dist/enrich/external-resolver.js +222 -0
  317. package/dist/enrich/external-resolver.js.map +1 -0
  318. package/dist/enrich/fetchers.d.ts +30 -0
  319. package/dist/enrich/fetchers.d.ts.map +1 -0
  320. package/dist/enrich/fetchers.js +89 -0
  321. package/dist/enrich/fetchers.js.map +1 -0
  322. package/dist/file-resolve.d.ts +35 -0
  323. package/dist/file-resolve.d.ts.map +1 -0
  324. package/dist/file-resolve.js +159 -0
  325. package/dist/file-resolve.js.map +1 -0
  326. package/dist/freshness/dirty-ledger.d.ts +7 -0
  327. package/dist/freshness/dirty-ledger.d.ts.map +1 -0
  328. package/dist/freshness/dirty-ledger.js +56 -0
  329. package/dist/freshness/dirty-ledger.js.map +1 -0
  330. package/dist/freshness/drainer.d.ts +38 -0
  331. package/dist/freshness/drainer.d.ts.map +1 -0
  332. package/dist/freshness/drainer.js +88 -0
  333. package/dist/freshness/drainer.js.map +1 -0
  334. package/dist/freshness/paths.d.ts +9 -0
  335. package/dist/freshness/paths.d.ts.map +1 -0
  336. package/dist/freshness/paths.js +24 -0
  337. package/dist/freshness/paths.js.map +1 -0
  338. package/dist/freshness/scan-lock.d.ts +8 -0
  339. package/dist/freshness/scan-lock.d.ts.map +1 -0
  340. package/dist/freshness/scan-lock.js +68 -0
  341. package/dist/freshness/scan-lock.js.map +1 -0
  342. package/dist/freshness/stamp.d.ts +25 -0
  343. package/dist/freshness/stamp.d.ts.map +1 -0
  344. package/dist/freshness/stamp.js +50 -0
  345. package/dist/freshness/stamp.js.map +1 -0
  346. package/dist/git.d.ts +12 -0
  347. package/dist/git.d.ts.map +1 -0
  348. package/dist/git.js +42 -0
  349. package/dist/git.js.map +1 -0
  350. package/dist/gitignore-safety.d.ts +41 -0
  351. package/dist/gitignore-safety.d.ts.map +1 -0
  352. package/dist/gitignore-safety.js +107 -0
  353. package/dist/gitignore-safety.js.map +1 -0
  354. package/dist/impact.d.ts +20 -0
  355. package/dist/impact.d.ts.map +1 -0
  356. package/dist/impact.js +89 -0
  357. package/dist/impact.js.map +1 -0
  358. package/dist/index.d.ts +24 -2
  359. package/dist/index.d.ts.map +1 -1
  360. package/dist/index.js +31 -1
  361. package/dist/index.js.map +1 -1
  362. package/dist/lessons-store.d.ts +93 -0
  363. package/dist/lessons-store.d.ts.map +1 -0
  364. package/dist/lessons-store.js +265 -0
  365. package/dist/lessons-store.js.map +1 -0
  366. package/dist/llm-dedup.d.ts +40 -0
  367. package/dist/llm-dedup.d.ts.map +1 -0
  368. package/dist/llm-dedup.js +373 -0
  369. package/dist/llm-dedup.js.map +1 -0
  370. package/dist/mcp/server.d.ts +9 -0
  371. package/dist/mcp/server.d.ts.map +1 -0
  372. package/dist/mcp/server.js +87 -0
  373. package/dist/mcp/server.js.map +1 -0
  374. package/dist/mcp/tools.d.ts +198 -0
  375. package/dist/mcp/tools.d.ts.map +1 -0
  376. package/dist/mcp/tools.js +744 -0
  377. package/dist/mcp/tools.js.map +1 -0
  378. package/dist/metrics/pagerank-louvain.d.ts +44 -0
  379. package/dist/metrics/pagerank-louvain.d.ts.map +1 -0
  380. package/dist/metrics/pagerank-louvain.js +128 -0
  381. package/dist/metrics/pagerank-louvain.js.map +1 -0
  382. package/dist/parsers/scip-runner.d.ts +63 -0
  383. package/dist/parsers/scip-runner.d.ts.map +1 -0
  384. package/dist/parsers/scip-runner.js +179 -0
  385. package/dist/parsers/scip-runner.js.map +1 -0
  386. package/dist/projects.d.ts +54 -0
  387. package/dist/projects.d.ts.map +1 -0
  388. package/dist/projects.js +153 -0
  389. package/dist/projects.js.map +1 -0
  390. package/dist/resolve.d.ts +22 -0
  391. package/dist/resolve.d.ts.map +1 -0
  392. package/dist/resolve.js +128 -0
  393. package/dist/resolve.js.map +1 -0
  394. package/dist/rules.d.ts +36 -0
  395. package/dist/rules.d.ts.map +1 -0
  396. package/dist/rules.js +484 -0
  397. package/dist/rules.js.map +1 -0
  398. package/dist/sandbox.d.ts +33 -0
  399. package/dist/sandbox.d.ts.map +1 -0
  400. package/dist/sandbox.js +91 -0
  401. package/dist/sandbox.js.map +1 -0
  402. package/dist/scan-lock.d.ts +37 -0
  403. package/dist/scan-lock.d.ts.map +1 -0
  404. package/dist/scan-lock.js +145 -0
  405. package/dist/scan-lock.js.map +1 -0
  406. package/dist/scanner.d.ts +126 -1
  407. package/dist/scanner.d.ts.map +1 -1
  408. package/dist/scanner.js +1711 -235
  409. package/dist/scanner.js.map +1 -1
  410. package/dist/scanners/connections/ast-scanner.d.ts +9 -2
  411. package/dist/scanners/connections/ast-scanner.d.ts.map +1 -1
  412. package/dist/scanners/connections/ast-scanner.js +19 -4
  413. package/dist/scanners/connections/ast-scanner.js.map +1 -1
  414. package/dist/scanners/connections/import-scanner.d.ts +27 -0
  415. package/dist/scanners/connections/import-scanner.d.ts.map +1 -0
  416. package/dist/scanners/connections/import-scanner.js +537 -0
  417. package/dist/scanners/connections/import-scanner.js.map +1 -0
  418. package/dist/scanners/connections/llm-call-tracer.d.ts +1 -1
  419. package/dist/scanners/connections/llm-call-tracer.d.ts.map +1 -1
  420. package/dist/scanners/connections/llm-call-tracer.js +6 -2
  421. package/dist/scanners/connections/llm-call-tracer.js.map +1 -1
  422. package/dist/scanners/connections/prisma-calls.d.ts +11 -0
  423. package/dist/scanners/connections/prisma-calls.d.ts.map +1 -0
  424. package/dist/scanners/connections/prisma-calls.js +237 -0
  425. package/dist/scanners/connections/prisma-calls.js.map +1 -0
  426. package/dist/scanners/connections/service-calls.d.ts +1 -1
  427. package/dist/scanners/connections/service-calls.d.ts.map +1 -1
  428. package/dist/scanners/connections/service-calls.js +35 -3
  429. package/dist/scanners/connections/service-calls.js.map +1 -1
  430. package/dist/scanners/infrastructure/cron-scanner.d.ts +14 -0
  431. package/dist/scanners/infrastructure/cron-scanner.d.ts.map +1 -0
  432. package/dist/scanners/infrastructure/cron-scanner.js +383 -0
  433. package/dist/scanners/infrastructure/cron-scanner.js.map +1 -0
  434. package/dist/scanners/infrastructure/deploy-scanner.d.ts +11 -0
  435. package/dist/scanners/infrastructure/deploy-scanner.d.ts.map +1 -0
  436. package/dist/scanners/infrastructure/deploy-scanner.js +508 -0
  437. package/dist/scanners/infrastructure/deploy-scanner.js.map +1 -0
  438. package/dist/scanners/infrastructure/env-scanner.d.ts +55 -0
  439. package/dist/scanners/infrastructure/env-scanner.d.ts.map +1 -0
  440. package/dist/scanners/infrastructure/env-scanner.js +431 -0
  441. package/dist/scanners/infrastructure/env-scanner.js.map +1 -0
  442. package/dist/scanners/infrastructure/field-usage-analyzer.d.ts +52 -0
  443. package/dist/scanners/infrastructure/field-usage-analyzer.d.ts.map +1 -0
  444. package/dist/scanners/infrastructure/field-usage-analyzer.js +480 -0
  445. package/dist/scanners/infrastructure/field-usage-analyzer.js.map +1 -0
  446. package/dist/scanners/infrastructure/prisma-parser.d.ts +21 -0
  447. package/dist/scanners/infrastructure/prisma-parser.d.ts.map +1 -0
  448. package/dist/scanners/infrastructure/prisma-parser.js +58 -0
  449. package/dist/scanners/infrastructure/prisma-parser.js.map +1 -0
  450. package/dist/scanners/infrastructure/prisma-scanner.d.ts +30 -0
  451. package/dist/scanners/infrastructure/prisma-scanner.d.ts.map +1 -0
  452. package/dist/scanners/infrastructure/prisma-scanner.js +329 -0
  453. package/dist/scanners/infrastructure/prisma-scanner.js.map +1 -0
  454. package/dist/scanners/infrastructure/queue-scanner.d.ts +14 -0
  455. package/dist/scanners/infrastructure/queue-scanner.d.ts.map +1 -0
  456. package/dist/scanners/infrastructure/queue-scanner.js +455 -0
  457. package/dist/scanners/infrastructure/queue-scanner.js.map +1 -0
  458. package/dist/scanners/infrastructure/typespec-validator.d.ts +50 -0
  459. package/dist/scanners/infrastructure/typespec-validator.d.ts.map +1 -0
  460. package/dist/scanners/infrastructure/typespec-validator.js +407 -0
  461. package/dist/scanners/infrastructure/typespec-validator.js.map +1 -0
  462. package/dist/scanners/packages/swift.d.ts +5 -0
  463. package/dist/scanners/packages/swift.d.ts.map +1 -1
  464. package/dist/scanners/packages/swift.js +23 -0
  465. package/dist/scanners/packages/swift.js.map +1 -1
  466. package/dist/scanners/prompts/detector.d.ts +13 -2
  467. package/dist/scanners/prompts/detector.d.ts.map +1 -1
  468. package/dist/scanners/prompts/detector.js +97 -46
  469. package/dist/scanners/prompts/detector.js.map +1 -1
  470. package/dist/scanners/prompts/index.d.ts +1 -1
  471. package/dist/scanners/prompts/index.d.ts.map +1 -1
  472. package/dist/scanners/prompts/index.js +2 -2
  473. package/dist/scanners/prompts/index.js.map +1 -1
  474. package/dist/scanners/swift/code-scanner.d.ts +1 -1
  475. package/dist/scanners/swift/code-scanner.d.ts.map +1 -1
  476. package/dist/scanners/swift/code-scanner.js +216 -2
  477. package/dist/scanners/swift/code-scanner.js.map +1 -1
  478. package/dist/scanners/swift/swiftui-scanner.d.ts +45 -0
  479. package/dist/scanners/swift/swiftui-scanner.d.ts.map +1 -0
  480. package/dist/scanners/swift/swiftui-scanner.js +606 -0
  481. package/dist/scanners/swift/swiftui-scanner.js.map +1 -0
  482. package/dist/scanners/xcode/pbxproj-parser.d.ts +32 -0
  483. package/dist/scanners/xcode/pbxproj-parser.d.ts.map +1 -0
  484. package/dist/scanners/xcode/pbxproj-parser.js +407 -0
  485. package/dist/scanners/xcode/pbxproj-parser.js.map +1 -0
  486. package/dist/scanners/xcode/storyboard-scanner.d.ts +13 -0
  487. package/dist/scanners/xcode/storyboard-scanner.d.ts.map +1 -0
  488. package/dist/scanners/xcode/storyboard-scanner.js +236 -0
  489. package/dist/scanners/xcode/storyboard-scanner.js.map +1 -0
  490. package/dist/storage/markdown-view.d.ts +49 -0
  491. package/dist/storage/markdown-view.d.ts.map +1 -0
  492. package/dist/storage/markdown-view.js +233 -0
  493. package/dist/storage/markdown-view.js.map +1 -0
  494. package/dist/storage.d.ts +225 -9
  495. package/dist/storage.d.ts.map +1 -1
  496. package/dist/storage.js +945 -86
  497. package/dist/storage.js.map +1 -1
  498. package/dist/subgraph.d.ts +30 -0
  499. package/dist/subgraph.d.ts.map +1 -0
  500. package/dist/subgraph.js +106 -0
  501. package/dist/subgraph.js.map +1 -0
  502. package/dist/temporal/git-store.d.ts +65 -0
  503. package/dist/temporal/git-store.d.ts.map +1 -0
  504. package/dist/temporal/git-store.js +166 -0
  505. package/dist/temporal/git-store.js.map +1 -0
  506. package/dist/trace.d.ts +38 -0
  507. package/dist/trace.d.ts.map +1 -0
  508. package/dist/trace.js +292 -0
  509. package/dist/trace.js.map +1 -0
  510. package/dist/types.d.ts +322 -2
  511. package/dist/types.d.ts.map +1 -1
  512. package/dist/types.js +53 -0
  513. package/dist/types.js.map +1 -1
  514. package/hooks/hooks.json +1 -55
  515. package/hooks/mark-dirty.sh +20 -0
  516. package/hooks/post-bash-suggest.sh +30 -0
  517. package/hooks/post-edit-suggest.sh +29 -0
  518. package/hooks/pre-edit-warn.sh +28 -0
  519. package/hooks/session-start.sh +19 -0
  520. package/hooks/stop-suggest.sh +49 -0
  521. package/package.json +30 -11
  522. package/scripts/install-codex-plugin.sh +119 -0
  523. package/scripts/install-plugin.sh +29 -24
  524. package/skills/architecture-export/SKILL.md +79 -0
  525. package/skills/architecture-scan/SKILL.md +64 -0
  526. package/skills/code-review/SKILL.md +368 -0
  527. package/skills/impact-analysis/SKILL.md +80 -0
  528. package/skills/infrastructure-scanning.md +42 -0
  529. package/skills/navgator-setup/SKILL.md +108 -0
  530. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  531. package/web/.next/standalone/web/.next/app-path-routes-manifest.json +3 -0
  532. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  533. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  534. package/web/.next/standalone/web/.next/required-server-files.json +4 -4
  535. package/web/.next/standalone/web/.next/routes-manifest.json +18 -0
  536. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  537. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  538. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  539. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  540. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  541. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  542. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  543. package/web/.next/standalone/web/.next/server/app/_not-found/page/next-font-manifest.json +2 -2
  544. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  545. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  546. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +3 -3
  547. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  548. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  549. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  550. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  551. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  552. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -3
  553. package/web/.next/standalone/web/.next/server/app/api/prompts/route.js.nft.json +1 -1
  554. package/web/.next/standalone/web/.next/server/app/api/rules/route/app-paths-manifest.json +3 -0
  555. package/web/.next/standalone/web/.next/server/app/api/rules/route/build-manifest.json +11 -0
  556. package/web/.next/standalone/web/.next/server/app/api/rules/route/server-reference-manifest.json +4 -0
  557. package/web/.next/standalone/web/.next/server/app/api/rules/route.js +6 -0
  558. package/web/.next/standalone/web/.next/server/app/api/rules/route.js.map +5 -0
  559. package/web/.next/standalone/web/.next/server/app/api/rules/route.js.nft.json +1 -0
  560. package/web/.next/standalone/web/.next/server/app/api/rules/route_client-reference-manifest.js +2 -0
  561. package/web/.next/standalone/web/.next/server/app/api/subgraph/route/app-paths-manifest.json +3 -0
  562. package/web/.next/standalone/web/.next/server/app/api/subgraph/route/build-manifest.json +11 -0
  563. package/web/.next/standalone/web/.next/server/app/api/subgraph/route/server-reference-manifest.json +4 -0
  564. package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js +6 -0
  565. package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js.map +5 -0
  566. package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js.nft.json +1 -0
  567. package/web/.next/standalone/web/.next/server/app/api/subgraph/route_client-reference-manifest.js +2 -0
  568. package/web/.next/standalone/web/.next/server/app/api/trace/route/app-paths-manifest.json +3 -0
  569. package/web/.next/standalone/web/.next/server/app/api/trace/route/build-manifest.json +11 -0
  570. package/web/.next/standalone/web/.next/server/app/api/trace/route/server-reference-manifest.json +4 -0
  571. package/web/.next/standalone/web/.next/server/app/api/trace/route.js +6 -0
  572. package/web/.next/standalone/web/.next/server/app/api/trace/route.js.map +5 -0
  573. package/web/.next/standalone/web/.next/server/app/api/trace/route.js.nft.json +1 -0
  574. package/web/.next/standalone/web/.next/server/app/api/trace/route_client-reference-manifest.js +2 -0
  575. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  576. package/web/.next/standalone/web/.next/server/app/index.rsc +6 -6
  577. package/web/.next/standalone/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  578. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +6 -6
  579. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  580. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  581. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +5 -5
  582. package/web/.next/standalone/web/.next/server/app/page/next-font-manifest.json +2 -2
  583. package/web/.next/standalone/web/.next/server/app/page_client-reference-manifest.js +1 -1
  584. package/web/.next/standalone/web/.next/server/app-paths-manifest.json +3 -0
  585. package/web/.next/standalone/web/.next/server/chunks/2374f_next_dist_esm_build_templates_app-route_0bb4e66a.js +1 -1
  586. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__006b837d._.js +1 -1
  587. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__0426efe8._.js +3 -0
  588. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__2e09fec9._.js +1 -1
  589. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__38d0390f._.js +1 -1
  590. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__594bcf20._.js +1 -1
  591. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__b888fadf._.js +1 -1
  592. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__cd5f36ce._.js +3 -0
  593. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__ee6fc95f._.js +3 -0
  594. package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__fa2ec862._.js +1 -1
  595. package/web/.next/standalone/web/.next/server/chunks/ssr/web_171de0df._.js +9 -4
  596. package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_rules_route_actions_3de01bd5.js +3 -0
  597. package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_subgraph_route_actions_d8b5a63f.js +3 -0
  598. package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_trace_route_actions_b0703ae2.js +3 -0
  599. package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
  600. package/web/.next/standalone/web/.next/server/next-font-manifest.json +4 -4
  601. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  602. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  603. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  604. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  605. package/web/.next/standalone/web/.next/static/chunks/22a09ecf6ba35cfd.js +17 -0
  606. package/web/.next/standalone/web/.next/static/chunks/9857ba86ce4e82d8.css +2 -0
  607. package/web/.next/standalone/web/.next/static/chunks/f899547f99ef4b76.css +1 -0
  608. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  609. package/web/.next/standalone/web/.next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
  610. package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  611. package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  612. package/web/.next/standalone/web/.next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
  613. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  614. package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  615. package/web/.next/standalone/web/.next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
  616. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  617. package/web/.next/standalone/web/.next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
  618. package/web/.next/standalone/web/.next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
  619. package/web/.next/standalone/web/app/api/components/route.ts +1 -1
  620. package/web/.next/standalone/web/app/api/connections/route.ts +3 -1
  621. package/web/.next/standalone/web/app/api/graph/route.ts +3 -3
  622. package/web/.next/standalone/web/app/api/projects/route.ts +1 -1
  623. package/web/.next/standalone/web/app/api/prompts/route.ts +2 -2
  624. package/web/.next/standalone/web/app/api/rules/route.ts +213 -0
  625. package/web/.next/standalone/web/app/api/settings/route.ts +2 -2
  626. package/web/.next/standalone/web/app/api/status/route.ts +1 -1
  627. package/web/.next/standalone/web/app/api/subgraph/route.ts +267 -0
  628. package/web/.next/standalone/web/app/api/trace/route.ts +321 -0
  629. package/web/.next/standalone/web/app/page.tsx +9 -1
  630. package/web/.next/standalone/web/components/connections-panel.tsx +23 -6
  631. package/web/.next/standalone/web/components/coverage-panel.tsx +309 -0
  632. package/web/.next/standalone/web/components/rules-panel.tsx +156 -0
  633. package/web/.next/standalone/web/components/sidebar.tsx +8 -0
  634. package/web/.next/standalone/web/components/status-overview.tsx +24 -1
  635. package/web/.next/standalone/web/components/subgraph-panel.tsx +382 -0
  636. package/web/.next/standalone/web/components/trace-panel.tsx +325 -0
  637. package/web/.next/standalone/web/lib/hooks/index.ts +3 -0
  638. package/web/.next/standalone/web/lib/hooks/use-coverage.ts +68 -0
  639. package/web/.next/standalone/web/lib/hooks/use-subgraph.ts +85 -0
  640. package/web/.next/standalone/web/lib/hooks/use-trace.ts +84 -0
  641. package/web/.next/standalone/web/lib/types.ts +108 -0
  642. package/web/.next/standalone/web/package-lock.json +218 -0
  643. package/web/.next/standalone/web/package.json +4 -2
  644. package/web/.next/standalone/web/server.js +1 -1
  645. package/web/.next/static/chunks/22a09ecf6ba35cfd.js +17 -0
  646. package/web/.next/static/chunks/9857ba86ce4e82d8.css +2 -0
  647. package/web/.next/static/chunks/f899547f99ef4b76.css +1 -0
  648. package/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
  649. package/web/.next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
  650. package/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
  651. package/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
  652. package/web/.next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
  653. package/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
  654. package/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
  655. package/web/.next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
  656. package/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
  657. package/web/.next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
  658. package/web/.next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
  659. package/skills/check/SKILL.md +0 -64
  660. package/skills/connections/SKILL.md +0 -54
  661. package/skills/diagram/SKILL.md +0 -64
  662. package/skills/export/SKILL.md +0 -49
  663. package/skills/impact/SKILL.md +0 -58
  664. package/skills/install/SKILL.md +0 -94
  665. package/skills/scan/SKILL.md +0 -75
  666. package/skills/status/SKILL.md +0 -37
  667. package/skills/ui/SKILL.md +0 -43
  668. package/skills/update/SKILL.md +0 -43
  669. package/web/.next/standalone/web/.next/static/chunks/8a80e7184ad3a13f.css +0 -2
  670. package/web/.next/standalone/web/.next/static/chunks/c056475f5f4424b6.css +0 -1
  671. package/web/.next/standalone/web/.next/static/chunks/cb3513192b63e480.js +0 -12
  672. package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  673. package/web/.next/standalone/web/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  674. package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  675. package/web/.next/standalone/web/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  676. package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  677. package/web/.next/standalone/web/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  678. package/web/.next/static/chunks/8a80e7184ad3a13f.css +0 -2
  679. package/web/.next/static/chunks/c056475f5f4424b6.css +0 -1
  680. package/web/.next/static/chunks/cb3513192b63e480.js +0 -12
  681. package/web/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  682. package/web/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  683. package/web/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  684. package/web/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  685. package/web/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  686. package/web/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  687. /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_buildManifest.js +0 -0
  688. /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_clientMiddlewareManifest.json +0 -0
  689. /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_ssgManifest.js +0 -0
  690. /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_buildManifest.js +0 -0
  691. /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_clientMiddlewareManifest.json +0 -0
  692. /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_ssgManifest.js +0 -0
package/dist/scanner.js CHANGED
@@ -2,18 +2,291 @@
2
2
  * NavGator Main Scanner
3
3
  * Orchestrates all component and connection scanners
4
4
  */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
5
7
  import { glob } from 'glob';
8
+ const DEFAULT_IGNORE_PATTERNS = [
9
+ '**/node_modules/**',
10
+ '**/dist/**',
11
+ '**/build/**',
12
+ '**/.next/**',
13
+ '**/__pycache__/**',
14
+ '**/venv/**',
15
+ '**/.venv/**',
16
+ '**/.git/**',
17
+ '**/.build/**',
18
+ '**/DerivedData/**',
19
+ '**/.swiftpm/**',
20
+ '**/Pods/**',
21
+ '**/coverage/**',
22
+ // Saved-webpage asset directories (Mediasite/Confluence/MHTML exports etc.)
23
+ // contain inert JS that has no runtime role in the project.
24
+ '**/*_files/**',
25
+ ];
26
+ function getIgnorePatterns(root) {
27
+ const userFile = path.join(root, '.navgatorignore');
28
+ if (!fs.existsSync(userFile))
29
+ return DEFAULT_IGNORE_PATTERNS;
30
+ try {
31
+ const userPatterns = fs.readFileSync(userFile, 'utf-8')
32
+ .split('\n')
33
+ .map(line => line.trim())
34
+ .filter(line => line && !line.startsWith('#'));
35
+ return [...DEFAULT_IGNORE_PATTERNS, ...userPatterns];
36
+ }
37
+ catch {
38
+ return DEFAULT_IGNORE_PATTERNS;
39
+ }
40
+ }
41
+ import { getGitInfo } from './git.js';
42
+ import { enrichFromCache } from './enrich/external-resolver.js';
43
+ import { loadCache, makeLookup } from './enrich/cache.js';
6
44
  import { scanNpmPackages, detectNpm } from './scanners/packages/npm.js';
7
45
  import { scanPipPackages, detectPip } from './scanners/packages/pip.js';
8
46
  import { scanSpmPackages, detectSpm } from './scanners/packages/swift.js';
9
47
  import { scanInfrastructure } from './scanners/infrastructure/index.js';
48
+ import { scanPrismaSchema, detectPrisma } from './scanners/infrastructure/prisma-scanner.js';
49
+ import { scanEnvVars, detectEnvFiles } from './scanners/infrastructure/env-scanner.js';
50
+ import { scanQueues, detectQueues } from './scanners/infrastructure/queue-scanner.js';
51
+ import { scanCronJobs, detectCrons } from './scanners/infrastructure/cron-scanner.js';
52
+ import { scanDeployConfig } from './scanners/infrastructure/deploy-scanner.js';
53
+ import { scanPrismaCalls } from './scanners/connections/prisma-calls.js';
54
+ import { scanFieldUsage, canAnalyzeFieldUsage } from './scanners/infrastructure/field-usage-analyzer.js';
55
+ import { scanTypeSpecValidation, canValidateTypeSpec } from './scanners/infrastructure/typespec-validator.js';
10
56
  import { scanServiceCalls } from './scanners/connections/service-calls.js';
11
57
  import { scanWithAST, scanDatabaseOperations } from './scanners/connections/ast-scanner.js';
12
58
  import { scanPrompts, convertToArchitecture, formatPromptsOutput } from './scanners/prompts/index.js';
13
59
  import { traceLLMCalls } from './scanners/connections/llm-call-tracer.js';
14
60
  import { scanSwiftCode } from './scanners/swift/code-scanner.js';
15
- import { storeComponents, storeConnections, buildIndex, buildGraph, buildFileMap, buildSummary, savePromptScan, clearStorage, createSnapshot, computeFileHashes, saveHashes, detectFileChanges, formatFileChangeSummary, } from './storage.js';
16
- import { getConfig, ensureStorageDirectories } from './config.js';
61
+ import { scanImports } from './scanners/connections/import-scanner.js';
62
+ import { storeComponents, storeConnections, migratePerEntityFiles, buildIndex, buildGraph, buildFileMap, buildSummary, savePromptScan, clearStorage, clearForFiles, loadIndex, loadAllComponents, loadAllConnections, loadReverseDeps, runIntegrityCheck, mergeByStableId, atomicWriteJSON, ensureStableIdPublic, buildReverseDepsIndex, buildDerivedManifest, createSnapshot, computeFileHashes, saveHashes, detectFileChanges, formatFileChangeSummary, } from './storage.js';
63
+ import { getConfig, ensureStorageDirectories, getIndexPath, getStoragePath, SCHEMA_VERSION, getComponentsPath, getConnectionsPath } from './config.js';
64
+ import { acquireLock } from './scan-lock.js';
65
+ import { computeArchitectureDiff, classifySignificance, loadLatestSnapshot, buildCurrentSnapshot, saveTimelineEntry, generateTimelineId, } from './diff.js';
66
+ import { registerProject } from './projects.js';
67
+ import { runAudit, updateEwmaForAudit } from './audit/index.js';
68
+ /**
69
+ * Strip internal scratch fields (prefixed with `__`) before persisting
70
+ * an AuditReport on a TimelineEntry.
71
+ */
72
+ function stripInternals(report) {
73
+ const { ...clean } = report;
74
+ for (const k of Object.keys(clean)) {
75
+ if (k.startsWith('__')) {
76
+ delete clean[k];
77
+ }
78
+ }
79
+ return clean;
80
+ }
81
+ import { classifyAllConnections } from './classify.js';
82
+ import { isSandboxMode } from './sandbox.js';
83
+ import { ensureSafeGitignore } from './gitignore-safety.js';
84
+ // =============================================================================
85
+ // MULTI-STACK ROOT DISCOVERY
86
+ // =============================================================================
87
+ /**
88
+ * Manifest filenames that mark a directory as the root of a discrete stack.
89
+ * Order matters: when we walk one level deep we stop at the first match per
90
+ * subdir, so place the language-canonical manifests first.
91
+ */
92
+ const STACK_MANIFESTS = [
93
+ 'package.json',
94
+ 'pyproject.toml',
95
+ 'Cargo.toml',
96
+ 'go.mod',
97
+ 'pom.xml',
98
+ 'Gemfile',
99
+ // Catch-all for .NET — we glob the subdir for `*.csproj` separately
100
+ // because there's no fixed filename. Handled via discoverStackRoots.
101
+ ];
102
+ /**
103
+ * Walk one level under `root`, return roots to scan. Behavior:
104
+ *
105
+ * - If `root` has any stack manifest, return `[{ path: root, origin: '.' }]`.
106
+ * No further walking — single-stack repos behave exactly as before.
107
+ * - Else, look at every direct child directory (depth 1). Any child that
108
+ * carries a stack manifest is included.
109
+ * - When more than one child stack is found, all of them are scanned and
110
+ * components get an `origin_root` metadata tag so consumers can group.
111
+ *
112
+ * Skips dotfiles, `node_modules`, `dist`, `build`, `__pycache__`, `.venv`,
113
+ * and anything starting with `.` to avoid scanning vendored or generated dirs.
114
+ */
115
+ export function discoverStackRoots(root, verbose) {
116
+ // Uses the module-level `fs`/`path` namespace imports at the top of
117
+ // the file. ESM-only — no `require()` here.
118
+ const hasManifest = (dir) => {
119
+ for (const m of STACK_MANIFESTS) {
120
+ if (fs.existsSync(path.join(dir, m)))
121
+ return true;
122
+ }
123
+ // .NET — any *.csproj
124
+ try {
125
+ const entries = fs.readdirSync(dir);
126
+ if (entries.some(e => e.endsWith('.csproj')))
127
+ return true;
128
+ }
129
+ catch {
130
+ // unreadable dir → not a stack root
131
+ }
132
+ return false;
133
+ };
134
+ if (hasManifest(root)) {
135
+ return [{ path: root, origin: '.' }];
136
+ }
137
+ const skipDirs = new Set([
138
+ 'node_modules', 'dist', 'build', '.git', '.next', '.cache',
139
+ '__pycache__', '.venv', 'venv', '.tox', 'target', 'vendor',
140
+ 'coverage', '.pytest_cache', '.navgator', '.ibr', '.bookmark',
141
+ '.claude',
142
+ ]);
143
+ let entries;
144
+ try {
145
+ entries = fs.readdirSync(root);
146
+ }
147
+ catch {
148
+ return [{ path: root, origin: '.' }];
149
+ }
150
+ const found = [];
151
+ for (const name of entries) {
152
+ if (name.startsWith('.'))
153
+ continue;
154
+ if (skipDirs.has(name))
155
+ continue;
156
+ const child = path.join(root, name);
157
+ let isDir = false;
158
+ try {
159
+ isDir = fs.statSync(child).isDirectory();
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ if (!isDir)
165
+ continue;
166
+ if (hasManifest(child)) {
167
+ found.push({ path: child, origin: name });
168
+ }
169
+ }
170
+ if (found.length === 0) {
171
+ // Nothing one level down either — keep legacy behavior so older
172
+ // projects don't silently turn into no-ops.
173
+ if (verbose) {
174
+ console.log(' - No stack manifest at root or any direct child; scanning root anyway');
175
+ }
176
+ return [{ path: root, origin: '.' }];
177
+ }
178
+ if (verbose) {
179
+ console.log(` - Multi-stack project: ${found.length} subroot(s): ` +
180
+ found.map(f => f.origin).join(', '));
181
+ }
182
+ return found;
183
+ }
184
+ // =============================================================================
185
+ // MODE SELECTION (Run 1 — D2)
186
+ // =============================================================================
187
+ /**
188
+ * Files whose presence in fileChanges forces a full scan because they alter
189
+ * the package/dependency graph in ways that ripple through every component.
190
+ */
191
+ const FULL_SCAN_TRIGGER_FILES = new Set([
192
+ // Lockfiles / manifests — change the package graph
193
+ 'package.json',
194
+ 'package-lock.json',
195
+ 'pnpm-lock.yaml',
196
+ 'yarn.lock',
197
+ 'pyproject.toml',
198
+ 'requirements.txt',
199
+ 'requirements-dev.txt',
200
+ 'requirements-test.txt',
201
+ 'prisma/schema.prisma',
202
+ 'Package.swift',
203
+ 'Package.resolved',
204
+ // Build / runtime config — change resolution, deploy targets, ignore rules
205
+ 'tsconfig.json',
206
+ 'vercel.json',
207
+ 'fly.toml',
208
+ 'railway.json',
209
+ '.gitignore',
210
+ ]);
211
+ /** Days × ms in one day — used by stale-full check. */
212
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
213
+ /** Cap on consecutive incremental scans before forcing a full scan. */
214
+ const INCREMENTAL_CAP = 20;
215
+ /**
216
+ * Decide whether to run a full or incremental scan based on the requested
217
+ * mode, the prior index state, and the file changes since last scan.
218
+ *
219
+ * Pure function — no I/O. All inputs precomputed by the caller.
220
+ *
221
+ * Policy (for mode='auto'):
222
+ * 1. No prior index → full / no-prior-state
223
+ * 2. schema_version mismatch (and not 1.0.0 → 1.1.0 soft-upgrade) → full / schema-mismatch
224
+ * 3. Any FULL_SCAN_TRIGGER_FILES in changedFiles → full / manifest-changed
225
+ * 4. now − last_full_scan > 7 days → full / stale-full
226
+ * 5. incrementals_since_full ≥ 20 → full / incremental-cap
227
+ * 6. No file changes at all → noop case (caller handles); we still return
228
+ * 'incremental' here for the no-op flow.
229
+ * 7. Else → incremental / fast-path
230
+ */
231
+ export function selectScanMode(fileChanges, index, options, now = Date.now()) {
232
+ const mode = options.mode ?? (options.clearFirst ? 'full' : options.incremental ? 'incremental' : 'auto');
233
+ if (mode === 'full') {
234
+ return { mode: 'full', reason: 'flag-full' };
235
+ }
236
+ if (mode === 'incremental') {
237
+ if (!index) {
238
+ return { mode: 'full', reason: 'no-prior-state' };
239
+ }
240
+ return { mode: 'incremental', reason: 'flag-incremental' };
241
+ }
242
+ // mode === 'auto'
243
+ if (!index) {
244
+ return { mode: 'full', reason: 'no-prior-state' };
245
+ }
246
+ // 1.0.0 → 1.1.0 is a soft upgrade (loadIndex injected defaults).
247
+ // Any other mismatch demands a full rebuild.
248
+ const sv = index.schema_version ?? '1.0.0';
249
+ if (sv !== '1.0.0' && sv !== SCHEMA_VERSION) {
250
+ return { mode: 'full', reason: 'schema-mismatch' };
251
+ }
252
+ // Run 2 — D5: prior scan's audit detected EWMA drift breach.
253
+ // Force a full + Cochran audit pass on this run.
254
+ if (index.pending_drift_breach) {
255
+ return { mode: 'full', reason: 'audit-drift-breach' };
256
+ }
257
+ const changed = new Set();
258
+ if (fileChanges) {
259
+ for (const f of fileChanges.added)
260
+ changed.add(f);
261
+ for (const f of fileChanges.modified)
262
+ changed.add(f);
263
+ for (const f of fileChanges.removed)
264
+ changed.add(f);
265
+ }
266
+ for (const trigger of FULL_SCAN_TRIGGER_FILES) {
267
+ if (changed.has(trigger)) {
268
+ return { mode: 'full', reason: 'manifest-changed' };
269
+ }
270
+ }
271
+ // New files have no recorded reverse-dep edges yet, so an incremental walk-set
272
+ // can't find their importers. Cleaner to force a full scan than gymnastics
273
+ // (Run 1.6 — item #5).
274
+ if (fileChanges && fileChanges.added.length > 0) {
275
+ return { mode: 'full', reason: 'new-files' };
276
+ }
277
+ const lastFull = index.last_full_scan ?? 0;
278
+ if (lastFull > 0 && now - lastFull > SEVEN_DAYS_MS) {
279
+ return { mode: 'full', reason: 'stale-full' };
280
+ }
281
+ const incCount = index.incrementals_since_full ?? 0;
282
+ if (incCount >= INCREMENTAL_CAP) {
283
+ return { mode: 'full', reason: 'incremental-cap' };
284
+ }
285
+ if (changed.size === 0) {
286
+ return { mode: 'incremental', reason: 'no-changes' };
287
+ }
288
+ return { mode: 'incremental', reason: 'fast-path' };
289
+ }
17
290
  // =============================================================================
18
291
  // MAIN SCANNER
19
292
  // =============================================================================
@@ -24,25 +297,105 @@ export async function scan(projectRoot, options = {}) {
24
297
  const startTime = Date.now();
25
298
  const root = projectRoot || process.cwd();
26
299
  const config = getConfig();
300
+ // R6 footprint fix: CLI/programmatic option overrides config flag.
301
+ if (options.perEntityFiles !== undefined) {
302
+ config.perEntityFiles = options.perEntityFiles;
303
+ }
304
+ // Sandbox mode: restrict scan behavior
305
+ if (isSandboxMode()) {
306
+ options.quick = true;
307
+ options.prompts = false;
308
+ options.useAST = false;
309
+ }
310
+ // Opt-in branch tracking
311
+ let gitInfo;
312
+ if (options.trackBranch) {
313
+ const info = await getGitInfo(root);
314
+ if (info) {
315
+ gitInfo = info;
316
+ if (options.verbose) {
317
+ console.log(`Branch tracking: ${info.branch} @ ${info.commit}`);
318
+ }
319
+ }
320
+ }
27
321
  if (options.verbose) {
28
322
  console.log(`Scanning project: ${root}`);
29
323
  }
30
- // Clear existing data if requested
31
- if (options.clearFirst) {
32
- await clearStorage(config, root);
33
- }
34
- // Ensure storage directories exist
324
+ // Ensure storage directories exist BEFORE we look at any prior state.
35
325
  ensureStorageDirectories(config, root);
36
326
  // ==========================================================================
37
- // Phase 0: File Discovery & Change Detection
327
+ // Phase 0.0: Concurrency lock (Run 1.6 — item #4)
38
328
  // ==========================================================================
39
- const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,py,swift,h,m}', {
40
- cwd: root,
41
- ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', '__pycache__/**', 'venv/**', '.git/**', '.build/**', 'DerivedData/**', '.swiftpm/**', 'Pods/**'],
42
- });
43
- let fileChanges;
44
- if (!options.clearFirst) {
45
- fileChanges = await detectFileChanges(sourceFiles, root, config);
329
+ // Prevent two `navgator scan` processes corrupting each other's
330
+ // .navgator/architecture/ output. Stale locks (>10 min OR pid gone)
331
+ // auto-clear. Live contention exits cleanly with code 0.
332
+ const storeDir = getStoragePath(config, root);
333
+ const requestedScanType = options.mode ?? (options.clearFirst ? 'full' : options.incremental ? 'incremental' : 'auto');
334
+ const lock = acquireLock(storeDir, requestedScanType);
335
+ if (!lock.ok) {
336
+ console.log(lock.message);
337
+ const duration = Date.now() - startTime;
338
+ return {
339
+ components: [],
340
+ connections: [],
341
+ warnings: [],
342
+ stats: {
343
+ scan_duration_ms: duration,
344
+ components_found: 0,
345
+ connections_found: 0,
346
+ warnings_count: 0,
347
+ files_scanned: 0,
348
+ files_changed: 0,
349
+ },
350
+ };
351
+ }
352
+ try {
353
+ // ==========================================================================
354
+ // Phase 0: File Discovery & Change Detection
355
+ // ==========================================================================
356
+ const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,mjs,cjs,py,swift,h,m}', {
357
+ cwd: root,
358
+ ignore: getIgnorePatterns(root),
359
+ });
360
+ // For change detection, also include manifest files at the project root
361
+ // (and a few well-known nested ones). selectScanMode consults these to
362
+ // decide whether to force a full scan. Manifests are NOT scanned by the
363
+ // per-language scanners — they're tracked here only for change detection.
364
+ const manifestPatterns = [
365
+ 'package.json',
366
+ 'package-lock.json',
367
+ 'pnpm-lock.yaml',
368
+ 'yarn.lock',
369
+ 'pyproject.toml',
370
+ 'requirements.txt',
371
+ 'requirements-dev.txt',
372
+ 'requirements-test.txt',
373
+ 'prisma/schema.prisma',
374
+ 'Package.swift',
375
+ 'Package.resolved',
376
+ // Build / runtime config — track so changes trigger full scan
377
+ 'tsconfig.json',
378
+ 'vercel.json',
379
+ 'fly.toml',
380
+ 'railway.json',
381
+ '.gitignore',
382
+ ];
383
+ const manifestFiles = [];
384
+ for (const m of manifestPatterns) {
385
+ try {
386
+ const fs = await import('node:fs');
387
+ if (fs.existsSync(path.join(root, m)))
388
+ manifestFiles.push(m);
389
+ }
390
+ catch {
391
+ // ignore
392
+ }
393
+ }
394
+ const filesForChangeDetection = [...sourceFiles, ...manifestFiles];
395
+ // Detect file changes using prior hashes BEFORE any clearing.
396
+ // (Used by mode selection AND timeline summary even on full scans.)
397
+ let fileChanges;
398
+ fileChanges = await detectFileChanges(filesForChangeDetection, root, config);
46
399
  if (options.verbose) {
47
400
  console.log(`File changes: ${formatFileChangeSummary(fileChanges)}`);
48
401
  if (fileChanges.added.length > 0 && fileChanges.added.length <= 5) {
@@ -52,272 +405,1314 @@ export async function scan(projectRoot, options = {}) {
52
405
  console.log(` Modified: ${fileChanges.modified.join(', ')}`);
53
406
  }
54
407
  }
55
- }
56
- const allComponents = [];
57
- const allConnections = [];
58
- const allWarnings = [];
59
- let promptScanResultHolder;
60
- let projectMetadata;
61
- // ==========================================================================
62
- // Phase 1: Package Detection
63
- // ==========================================================================
64
- if (options.verbose) {
65
- console.log('Phase 1: Scanning packages...');
66
- }
67
- // NPM packages
68
- if (detectNpm(root)) {
69
- if (options.verbose)
70
- console.log(' - Detected npm/yarn/pnpm project');
71
- const result = await scanNpmPackages(root);
72
- allComponents.push(...result.components);
73
- allWarnings.push(...result.warnings);
74
- }
75
- // Python packages
76
- if (detectPip(root)) {
77
- if (options.verbose)
78
- console.log(' - Detected Python project');
79
- const result = await scanPipPackages(root);
80
- allComponents.push(...result.components);
81
- allWarnings.push(...result.warnings);
82
- }
83
- // Swift/iOS/Mac packages (SPM, CocoaPods)
84
- if (detectSpm(root)) {
85
- if (options.verbose)
86
- console.log(' - Detected Swift/Xcode project');
87
- const result = await scanSpmPackages(root);
88
- allComponents.push(...result.components);
89
- allWarnings.push(...result.warnings);
90
- }
91
- // ==========================================================================
92
- // Phase 2: Infrastructure Detection
93
- // ==========================================================================
94
- if (options.verbose) {
95
- console.log('Phase 2: Scanning infrastructure...');
96
- }
97
- const infraResult = await scanInfrastructure(root);
98
- allComponents.push(...infraResult.components);
99
- allWarnings.push(...infraResult.warnings);
100
- // ==========================================================================
101
- // Phase 3: Connection Detection (unless quick mode)
102
- // ==========================================================================
103
- if (!options.quick || options.connections) {
408
+ // ==========================================================================
409
+ // Phase 0.5: Scan-mode selection (Run 1 — D2)
410
+ // ==========================================================================
411
+ const priorIndex = await loadIndex(config, root);
412
+ const decision = selectScanMode(fileChanges, priorIndex, options);
413
+ // scanType captures the mode the scan ACTUALLY ran in (after potential
414
+ // integrity-check promotion). Initialized to the decision; may be promoted
415
+ // to 'incremental→full' below.
416
+ //
417
+ // Run 1.7 — Problem A: when this scan is the recursive re-entry from a
418
+ // failed integrity check (`_promotedFromIncremental === true`), `decision.mode`
419
+ // is 'full' (clearFirst forces it), but the user-visible scan_type should
420
+ // remain 'incremental→full' so timeline + stats consumers see the promotion
421
+ // evidence (Run 1.6 #3 contract). The actual scan body still runs as full.
422
+ let scanType = options._promotedFromIncremental ? 'incremental→full' : decision.mode;
423
+ if (options.verbose) {
424
+ console.log(`Scan mode: ${decision.mode} (${decision.reason})`);
425
+ }
426
+ // Compute walk-set for incremental: changedFiles ∪ reverseDeps
427
+ const changedSet = new Set();
428
+ if (fileChanges) {
429
+ for (const f of fileChanges.added)
430
+ changedSet.add(f);
431
+ for (const f of fileChanges.modified)
432
+ changedSet.add(f);
433
+ for (const f of fileChanges.removed)
434
+ changedSet.add(f);
435
+ }
436
+ let walkSet = new Set(changedSet);
437
+ if (decision.mode === 'incremental' && changedSet.size > 0) {
438
+ const reverseDeps = await loadReverseDeps(changedSet, config, root);
439
+ for (const f of reverseDeps)
440
+ walkSet.add(f);
441
+ if (options.verbose) {
442
+ console.log(` Walk-set: ${changedSet.size} changed + ${reverseDeps.size} reverse-deps = ${walkSet.size} files`);
443
+ }
444
+ }
445
+ // Pass walkSet to scanners only on incremental. Full scans pass undefined to
446
+ // preserve bit-identical output (regression-locked by characterization snapshot).
447
+ const incWalkSet = decision.mode === 'incremental' ? walkSet : undefined;
448
+ // ==========================================================================
449
+ // Phase 0.6: Noop short-circuit (incremental + zero changes)
450
+ // ==========================================================================
451
+ if (decision.mode === 'incremental' && decision.reason === 'no-changes') {
452
+ // Nothing changed since last scan. Bump last_scan, update incrementals_since_full
453
+ // to 0 (no incremental work was done — but keep it as-is to honor the cap).
454
+ // Save fresh hashes (idempotent), update index timestamp, record noop timeline entry.
455
+ if (priorIndex) {
456
+ priorIndex.last_scan = Date.now();
457
+ // Note: incrementals_since_full and last_full_scan unchanged on noop.
458
+ await atomicWriteJSON(getIndexPath(config, root), priorIndex);
459
+ }
460
+ const fileHashes = await computeFileHashes(filesForChangeDetection, root);
461
+ await saveHashes(fileHashes, config, root);
462
+ const noopTimelineEntry = {
463
+ id: generateTimelineId(),
464
+ timestamp: Date.now(),
465
+ significance: 'patch',
466
+ triggers: [],
467
+ diff: {
468
+ components: { added: [], removed: [], modified: [] },
469
+ connections: { added: [], removed: [] },
470
+ stats: {
471
+ total_changes: 0,
472
+ components_before: priorIndex?.stats.total_components ?? 0,
473
+ components_after: priorIndex?.stats.total_components ?? 0,
474
+ connections_before: priorIndex?.stats.total_connections ?? 0,
475
+ connections_after: priorIndex?.stats.total_connections ?? 0,
476
+ },
477
+ },
478
+ git: gitInfo,
479
+ scan_type: 'noop',
480
+ files_scanned: 0,
481
+ };
482
+ await saveTimelineEntry(noopTimelineEntry, config, root);
483
+ // Load existing components/connections so callers see the unchanged graph.
484
+ const existingComponents = await loadAllComponents(config, root);
485
+ const existingConnections = await loadAllConnections(config, root);
486
+ const duration = Date.now() - startTime;
487
+ if (options.verbose) {
488
+ console.log(`Scan complete (noop) in ${duration}ms`);
489
+ }
490
+ return {
491
+ components: existingComponents,
492
+ connections: existingConnections,
493
+ warnings: [],
494
+ fileChanges,
495
+ timelineEntry: noopTimelineEntry,
496
+ gitInfo,
497
+ stats: {
498
+ scan_duration_ms: duration,
499
+ components_found: existingComponents.length,
500
+ connections_found: existingConnections.length,
501
+ warnings_count: 0,
502
+ files_scanned: 0,
503
+ files_changed: 0,
504
+ },
505
+ };
506
+ }
507
+ // For full scans: clear ALL prior data up front (legacy clearFirst semantics).
508
+ // For incremental: defer to Phase 4 (clearForFiles + merge).
509
+ if (decision.mode === 'full' || options.clearFirst) {
510
+ await clearStorage(config, root);
511
+ ensureStorageDirectories(config, root);
512
+ }
513
+ const allComponents = [];
514
+ const allConnections = [];
515
+ const allWarnings = [];
516
+ let promptScanResultHolder;
517
+ let projectMetadata;
518
+ // ==========================================================================
519
+ // Phase 1: Package Detection
520
+ // ==========================================================================
104
521
  if (options.verbose) {
105
- console.log('Phase 3: Scanning connections...');
522
+ console.log('Phase 1: Scanning packages...');
106
523
  }
107
- if (options.useAST) {
108
- // AST-based scanning (more accurate)
524
+ // Package scanners run in parallel (independent of each other).
525
+ //
526
+ // Multi-stack auto-discovery: if the project root has no stack manifest
527
+ // of its own, walk one level deep and scan each subdir that does. This
528
+ // catches the common monorepo-lite shape — a top-level `frontend/` with
529
+ // package.json + a top-level `backend/` with pyproject.toml — that the
530
+ // legacy single-root behavior silently missed (it would only scan
531
+ // whichever side was at the root).
532
+ //
533
+ // Pass `singleStack: true` (CLI: --single-stack) to force the legacy
534
+ // behavior. Each component scanned from a subroot gets its origin tagged
535
+ // via `metadata.origin_root` so downstream layers can group by stack.
536
+ {
537
+ const stackRoots = options.singleStack
538
+ ? [{ path: root, origin: '.' }]
539
+ : discoverStackRoots(root, options.verbose === true);
540
+ const packageTasks = [];
541
+ for (const sr of stackRoots) {
542
+ const tagOrigin = (result) => {
543
+ // Skip when origin is the root — keeps single-stack output identical.
544
+ if (sr.origin === '.')
545
+ return;
546
+ for (const c of result.components) {
547
+ const md = c.metadata ?? {};
548
+ c.metadata = { ...md, origin_root: sr.origin };
549
+ }
550
+ };
551
+ if (detectNpm(sr.path)) {
552
+ if (options.verbose) {
553
+ console.log(` - Detected npm/yarn/pnpm project (${sr.origin})`);
554
+ }
555
+ packageTasks.push(scanNpmPackages(sr.path).then(result => {
556
+ tagOrigin(result);
557
+ allComponents.push(...result.components);
558
+ allWarnings.push(...result.warnings);
559
+ }));
560
+ }
561
+ if (detectPip(sr.path)) {
562
+ if (options.verbose) {
563
+ console.log(` - Detected Python project (${sr.origin})`);
564
+ }
565
+ packageTasks.push(scanPipPackages(sr.path).then(result => {
566
+ tagOrigin(result);
567
+ allComponents.push(...result.components);
568
+ allWarnings.push(...result.warnings);
569
+ }));
570
+ }
571
+ if (detectSpm(sr.path)) {
572
+ if (options.verbose) {
573
+ console.log(` - Detected Swift/Xcode project (${sr.origin})`);
574
+ }
575
+ packageTasks.push(scanSpmPackages(sr.path).then(result => {
576
+ tagOrigin(result);
577
+ allComponents.push(...result.components);
578
+ allWarnings.push(...result.warnings);
579
+ }));
580
+ }
581
+ }
582
+ await Promise.all(packageTasks);
583
+ }
584
+ // ==========================================================================
585
+ // Phase 2: Infrastructure Detection
586
+ // ==========================================================================
587
+ if (options.verbose) {
588
+ console.log('Phase 2: Scanning infrastructure...');
589
+ }
590
+ const infraResult = await scanInfrastructure(root);
591
+ allComponents.push(...infraResult.components);
592
+ allWarnings.push(...infraResult.warnings);
593
+ // Prisma schema → database models + relations
594
+ if (detectPrisma(root)) {
109
595
  if (options.verbose)
110
- console.log(' - Running AST analysis (ts-morph)...');
596
+ console.log(' - Detected Prisma schema');
111
597
  try {
112
- const astResult = await scanWithAST(root);
113
- allComponents.push(...astResult.components);
114
- allConnections.push(...astResult.connections);
115
- allWarnings.push(...astResult.warnings);
116
- // Also scan for database operations
117
- if (options.verbose)
118
- console.log(' - Scanning database operations...');
119
- const dbResult = await scanDatabaseOperations(root);
120
- allComponents.push(...dbResult.components);
121
- allConnections.push(...dbResult.connections);
122
- allWarnings.push(...dbResult.warnings);
598
+ const prismaResult = await scanPrismaSchema(root);
599
+ allComponents.push(...prismaResult.components);
600
+ allConnections.push(...prismaResult.connections);
601
+ allWarnings.push(...prismaResult.warnings);
602
+ if (options.verbose) {
603
+ console.log(` Models: ${prismaResult.components.length}, Relations: ${prismaResult.connections.length}`);
604
+ }
123
605
  }
124
606
  catch (error) {
125
607
  allWarnings.push({
126
608
  type: 'parse_error',
127
- message: `AST scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
609
+ message: `Prisma scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
128
610
  });
129
- // Fall back to regex scanning
130
- if (options.verbose)
131
- console.log(' - Falling back to regex scanning...');
132
- const serviceResult = await scanServiceCalls(root);
133
- allComponents.push(...serviceResult.components);
134
- allConnections.push(...serviceResult.connections);
135
- allWarnings.push(...serviceResult.warnings);
136
611
  }
137
612
  }
138
- else {
139
- // Regex-based scanning (faster but less accurate)
613
+ // DB field usage analyzer (opt-in via FEATURE FLAG: fieldUsage)
614
+ let fieldUsageReportResult;
615
+ if (options.fieldUsage && canAnalyzeFieldUsage(root)) {
140
616
  if (options.verbose)
141
- console.log(' - Scanning service calls (regex)...');
142
- const serviceResult = await scanServiceCalls(root);
143
- allComponents.push(...serviceResult.components);
144
- allConnections.push(...serviceResult.connections);
145
- allWarnings.push(...serviceResult.warnings);
146
- }
147
- // Swift code analysis (runtime deps, protocols, state, LLM calls)
148
- if (detectSpm(root)) {
617
+ console.log(' - Analyzing DB field usage...');
618
+ try {
619
+ const fieldResult = await scanFieldUsage(root, incWalkSet);
620
+ allComponents.push(...fieldResult.components);
621
+ allConnections.push(...fieldResult.connections);
622
+ allWarnings.push(...fieldResult.warnings);
623
+ fieldUsageReportResult = fieldResult.report;
624
+ if (options.verbose && fieldResult.report) {
625
+ const r = fieldResult.report;
626
+ console.log(` Fields: ${r.totalFields} total, ${r.unusedFields} unused, ${r.writeOnlyFields} write-only`);
627
+ }
628
+ }
629
+ catch (error) {
630
+ allWarnings.push({
631
+ type: 'parse_error',
632
+ message: `Field usage analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
633
+ });
634
+ }
635
+ }
636
+ // TypeSpec validator (opt-in via FEATURE FLAG: typeSpec)
637
+ let typeSpecReportResult;
638
+ if (options.typeSpec && canValidateTypeSpec(root)) {
149
639
  if (options.verbose)
150
- console.log(' - Scanning Swift code connections...');
640
+ console.log(' - Validating TypeSpec (Prisma vs TS interfaces)...');
151
641
  try {
152
- const swiftResult = await scanSwiftCode(root);
153
- allComponents.push(...swiftResult.components);
154
- allConnections.push(...swiftResult.connections);
155
- allWarnings.push(...swiftResult.warnings);
156
- projectMetadata = swiftResult.projectMeta;
157
- if (options.verbose) {
158
- console.log(` Swift: ${swiftResult.components.length} components, ${swiftResult.connections.length} connections`);
159
- if (swiftResult.projectMeta.platforms) {
160
- console.log(` Platforms: ${swiftResult.projectMeta.platforms.join(', ')}`);
642
+ const tsResult = await scanTypeSpecValidation(root);
643
+ allWarnings.push(...tsResult.warnings);
644
+ typeSpecReportResult = tsResult.report;
645
+ if (options.verbose && tsResult.report) {
646
+ const r = tsResult.report;
647
+ console.log(` Interfaces: ${r.modelsWithInterfaces}/${r.modelsChecked} matched, ${r.totalMismatches} mismatches`);
648
+ }
649
+ }
650
+ catch (error) {
651
+ allWarnings.push({
652
+ type: 'parse_error',
653
+ message: `TypeSpec validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
654
+ });
655
+ }
656
+ }
657
+ // Env, queues, and crons are independent — run in parallel
658
+ {
659
+ const infraTasks = [];
660
+ if (detectEnvFiles(root)) {
661
+ if (options.verbose)
662
+ console.log(' - Detected environment files');
663
+ infraTasks.push(scanEnvVars(root, incWalkSet).then(envResult => {
664
+ allComponents.push(...envResult.components);
665
+ allConnections.push(...envResult.connections);
666
+ allWarnings.push(...envResult.warnings);
667
+ if (options.verbose) {
668
+ console.log(` Env vars: ${envResult.components.length}, References: ${envResult.connections.length}`);
161
669
  }
162
- if (swiftResult.projectMeta.architecture_pattern) {
163
- console.log(` Architecture: ${swiftResult.projectMeta.architecture_pattern}`);
670
+ }).catch(error => {
671
+ allWarnings.push({
672
+ type: 'parse_error',
673
+ message: `Env scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
674
+ });
675
+ }));
676
+ }
677
+ if (detectQueues(root)) {
678
+ if (options.verbose)
679
+ console.log(' - Detected queue system');
680
+ infraTasks.push(scanQueues(root, incWalkSet).then(queueResult => {
681
+ allComponents.push(...queueResult.components);
682
+ allConnections.push(...queueResult.connections);
683
+ allWarnings.push(...queueResult.warnings);
684
+ if (options.verbose) {
685
+ console.log(` Queues: ${queueResult.components.length}, Connections: ${queueResult.connections.length}`);
164
686
  }
687
+ }).catch(error => {
688
+ allWarnings.push({
689
+ type: 'parse_error',
690
+ message: `Queue scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
691
+ });
692
+ }));
693
+ }
694
+ if (detectCrons(root)) {
695
+ if (options.verbose)
696
+ console.log(' - Detected cron jobs');
697
+ infraTasks.push(scanCronJobs(root, incWalkSet).then(cronResult => {
698
+ allComponents.push(...cronResult.components);
699
+ allConnections.push(...cronResult.connections);
700
+ allWarnings.push(...cronResult.warnings);
701
+ if (options.verbose) {
702
+ console.log(` Cron jobs: ${cronResult.components.length}, Route connections: ${cronResult.connections.length}`);
703
+ }
704
+ }).catch(error => {
705
+ allWarnings.push({
706
+ type: 'parse_error',
707
+ message: `Cron scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
708
+ });
709
+ }));
710
+ }
711
+ await Promise.all(infraTasks);
712
+ }
713
+ // Deployment config → detailed infra metadata
714
+ if (options.verbose)
715
+ console.log(' - Scanning deployment config...');
716
+ try {
717
+ const deployResult = await scanDeployConfig(root);
718
+ allComponents.push(...deployResult.components);
719
+ allConnections.push(...deployResult.connections);
720
+ allWarnings.push(...deployResult.warnings);
721
+ if (options.verbose && deployResult.components.length > 0) {
722
+ console.log(` Deploy configs: ${deployResult.components.length}, Entry points: ${deployResult.connections.length}`);
723
+ }
724
+ }
725
+ catch (error) {
726
+ allWarnings.push({
727
+ type: 'parse_error',
728
+ message: `Deploy config scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
729
+ });
730
+ }
731
+ // Prisma call detection: map source files to database models they query
732
+ const prismaModelComps = allComponents.filter(c => c.type === 'database' && c.tags?.includes('prisma'));
733
+ if (prismaModelComps.length > 0) {
734
+ if (options.verbose)
735
+ console.log(' - Scanning Prisma client calls...');
736
+ try {
737
+ const prismaCallResult = await scanPrismaCalls(root, prismaModelComps, incWalkSet);
738
+ allConnections.push(...prismaCallResult.connections);
739
+ if (options.verbose && prismaCallResult.connections.length > 0) {
740
+ const uniqueModels = new Set(prismaCallResult.connections.map(c => c.description?.split(' queries ')[1]?.split(' ')[0]));
741
+ console.log(` DB queries: ${prismaCallResult.connections.length} file→model connections across ${uniqueModels.size} models`);
165
742
  }
166
743
  }
167
744
  catch (error) {
168
745
  allWarnings.push({
169
746
  type: 'parse_error',
170
- message: `Swift code scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
747
+ message: `Prisma call scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
171
748
  });
172
749
  }
173
750
  }
174
- // AI prompts & LLM call tracing
175
- if (options.prompts) {
176
- // Step 1: Run anchor-based LLM call tracer (primary detection)
751
+ // ==========================================================================
752
+ // Phase 3: Connection Detection (unless quick mode)
753
+ // ==========================================================================
754
+ if (!options.quick || options.connections) {
755
+ if (options.verbose) {
756
+ console.log('Phase 3: Scanning connections...');
757
+ }
758
+ if (options.useAST) {
759
+ // AST-based scanning (more accurate)
760
+ if (options.verbose)
761
+ console.log(' - Running AST analysis (ts-morph)...');
762
+ try {
763
+ const astResult = await scanWithAST(root, incWalkSet);
764
+ allComponents.push(...astResult.components);
765
+ allConnections.push(...astResult.connections);
766
+ allWarnings.push(...astResult.warnings);
767
+ // Also scan for database operations
768
+ if (options.verbose)
769
+ console.log(' - Scanning database operations...');
770
+ const dbResult = await scanDatabaseOperations(root, incWalkSet);
771
+ allComponents.push(...dbResult.components);
772
+ allConnections.push(...dbResult.connections);
773
+ allWarnings.push(...dbResult.warnings);
774
+ }
775
+ catch (error) {
776
+ allWarnings.push({
777
+ type: 'parse_error',
778
+ message: `AST scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
779
+ });
780
+ // Fall back to regex scanning
781
+ if (options.verbose)
782
+ console.log(' - Falling back to regex scanning...');
783
+ const serviceResult = await scanServiceCalls(root, incWalkSet);
784
+ allComponents.push(...serviceResult.components);
785
+ allConnections.push(...serviceResult.connections);
786
+ allWarnings.push(...serviceResult.warnings);
787
+ }
788
+ }
789
+ else {
790
+ // Regex-based scanning (faster but less accurate)
791
+ if (options.verbose)
792
+ console.log(' - Scanning service calls (regex)...');
793
+ const serviceResult = await scanServiceCalls(root, incWalkSet);
794
+ allComponents.push(...serviceResult.components);
795
+ allConnections.push(...serviceResult.connections);
796
+ allWarnings.push(...serviceResult.warnings);
797
+ }
798
+ // File-level import graph (TS/JS local imports)
177
799
  if (options.verbose)
178
- console.log(' - Running LLM call tracer (anchor-based)...');
179
- let traceResult;
800
+ console.log(' - Scanning file imports...');
180
801
  try {
181
- traceResult = await traceLLMCalls(root);
182
- allComponents.push(...traceResult.scanResult.components);
183
- allConnections.push(...traceResult.scanResult.connections);
802
+ // Collect npm package components so bare imports (`import X from "react"`)
803
+ // can be resolved to the package component and emitted as `uses-package`
804
+ // edges. Use config_files filter instead of type filter: packages can be
805
+ // classified as 'npm' | 'framework' | 'database' | 'service' depending
806
+ // on FRAMEWORK_SIGNATURES, but all originate from a package.json.
807
+ const knownPackages = allComponents
808
+ .filter(c => c.source.config_files?.some(f => f === 'package.json' || f.endsWith('/package.json')))
809
+ .map(c => ({ name: c.name, component_id: c.component_id }));
810
+ // In incremental mode, restrict the import scan to walk-set files. Falls
811
+ // back to the full sourceFiles list (bit-identical) on full scans.
812
+ const importSourceFiles = incWalkSet
813
+ ? sourceFiles.filter(f => incWalkSet.has(f))
814
+ : sourceFiles;
815
+ const importResult = await scanImports(root, importSourceFiles, knownPackages);
816
+ allComponents.push(...importResult.components);
817
+ allConnections.push(...importResult.connections);
184
818
  if (options.verbose) {
185
- console.log(` Traced ${traceResult.calls.length} LLM call sites`);
186
- console.log(` Wrappers: ${traceResult.wrappers.length}`);
187
- const providers = new Map();
188
- for (const call of traceResult.calls) {
189
- const p = call.provider.name;
190
- providers.set(p, (providers.get(p) || 0) + 1);
819
+ const usesPkgCount = importResult.connections.filter(c => c.connection_type === 'uses-package').length;
820
+ console.log(` Found ${importResult.components.length} internal modules, ${importResult.connections.length} file-level imports (${usesPkgCount} uses-package)`);
821
+ }
822
+ // SCIP overlay (T11): when --scip / NAVGATOR_SCIP=1, run the
823
+ // compiler-accurate indexer and ADD any cross-file edges the regex
824
+ // import-scanner missed (re-exports, dynamic imports, type-only refs,
825
+ // etc.). Existing edges from the regex pass are preserved as-is so
826
+ // the characterization snapshots stay stable for non-SCIP runs.
827
+ const scipEnabled = process.env['NAVGATOR_SCIP'] === '1' || options.scip === true;
828
+ if (scipEnabled) {
829
+ try {
830
+ const { runScip, crossFileEdges, hasTsConfig } = await import('./parsers/scip-runner.js');
831
+ if (!hasTsConfig(root)) {
832
+ if (options.verbose)
833
+ console.log(' SCIP requested but no tsconfig.json — skipping');
834
+ }
835
+ else {
836
+ if (options.verbose)
837
+ console.log(' - Running SCIP indexer (compiler-accurate)...');
838
+ const scipResult = await runScip(root, { timeoutMs: 60_000 });
839
+ if (!scipResult.ok) {
840
+ allWarnings.push({
841
+ type: 'parse_error',
842
+ message: `SCIP indexer failed: ${scipResult.error}`,
843
+ });
844
+ }
845
+ else {
846
+ const cross = crossFileEdges(scipResult.edges);
847
+ const fileToComponentId = new Map();
848
+ for (const c of importResult.components) {
849
+ const f = c.source?.config_files?.[0];
850
+ if (f)
851
+ fileToComponentId.set(f, c.component_id);
852
+ }
853
+ const existing = new Set(importResult.connections
854
+ .filter((c) => c.connection_type === 'imports')
855
+ .map((c) => `${c.from?.location?.file ?? ''}→${c.code_reference?.file ?? ''}`));
856
+ let added = 0;
857
+ const now = Date.now();
858
+ for (const e of cross) {
859
+ const fromId = fileToComponentId.get(e.from_file);
860
+ const toId = fileToComponentId.get(e.to_file ?? '');
861
+ if (!fromId || !toId)
862
+ continue;
863
+ const key = `${e.from_file}→${e.to_file}`;
864
+ if (existing.has(key))
865
+ continue;
866
+ existing.add(key);
867
+ allConnections.push({
868
+ connection_id: `CONN_imports_scip_${Math.random().toString(36).slice(2, 10)}`,
869
+ from: {
870
+ component_id: fromId,
871
+ location: { file: e.from_file, line: e.from_line + 1 },
872
+ },
873
+ to: { component_id: toId },
874
+ connection_type: 'imports',
875
+ code_reference: {
876
+ file: e.from_file,
877
+ symbol: e.display_name || e.symbol.split('/').pop()?.slice(0, 40) || 'scip-ref',
878
+ symbol_type: e.is_definition ? 'export' : 'import',
879
+ line_start: e.from_line + 1,
880
+ },
881
+ description: 'SCIP-resolved cross-file reference',
882
+ detected_from: 'scip-typescript',
883
+ confidence: 0.99,
884
+ timestamp: now,
885
+ last_verified: now,
886
+ });
887
+ added++;
888
+ }
889
+ if (options.verbose) {
890
+ console.log(` SCIP added ${added} cross-file edges (${scipResult.duration_ms}ms, ${scipResult.documents_indexed} docs)`);
891
+ }
892
+ }
893
+ }
191
894
  }
192
- for (const [provider, count] of providers) {
193
- console.log(` ${provider}: ${count} call sites`);
895
+ catch (err) {
896
+ allWarnings.push({
897
+ type: 'parse_error',
898
+ message: `SCIP overlay failed: ${err.message}`,
899
+ });
194
900
  }
195
901
  }
196
902
  }
197
903
  catch (error) {
198
904
  allWarnings.push({
199
905
  type: 'parse_error',
200
- message: `LLM call tracer failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
906
+ message: `Import scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
201
907
  });
908
+ }
909
+ // Swift code analysis (runtime deps, protocols, state, LLM calls)
910
+ if (detectSpm(root)) {
202
911
  if (options.verbose)
203
- console.log(` LLM tracer error: ${error instanceof Error ? error.message : 'Unknown'}`);
912
+ console.log(' - Scanning Swift code connections...');
913
+ try {
914
+ const swiftResult = await scanSwiftCode(root, incWalkSet);
915
+ allComponents.push(...swiftResult.components);
916
+ allConnections.push(...swiftResult.connections);
917
+ allWarnings.push(...swiftResult.warnings);
918
+ projectMetadata = swiftResult.projectMeta;
919
+ if (options.verbose) {
920
+ console.log(` Swift: ${swiftResult.components.length} components, ${swiftResult.connections.length} connections`);
921
+ if (swiftResult.projectMeta.platforms) {
922
+ console.log(` Platforms: ${swiftResult.projectMeta.platforms.join(', ')}`);
923
+ }
924
+ if (swiftResult.projectMeta.architecture_pattern) {
925
+ console.log(` Architecture: ${swiftResult.projectMeta.architecture_pattern}`);
926
+ }
927
+ }
928
+ }
929
+ catch (error) {
930
+ allWarnings.push({
931
+ type: 'parse_error',
932
+ message: `Swift code scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
933
+ });
934
+ }
935
+ // Xcode project analysis (.pbxproj + storyboards)
936
+ try {
937
+ const { findXcodeProject } = await import('./scanners/packages/swift.js');
938
+ const pbxprojPath = findXcodeProject(root);
939
+ if (pbxprojPath) {
940
+ if (options.verbose)
941
+ console.log(' - Scanning Xcode project...');
942
+ const { parseXcodeProject, mapTargetToComponent, mapSourceMembership } = await import('./scanners/xcode/pbxproj-parser.js');
943
+ const xcodeData = parseXcodeProject(pbxprojPath);
944
+ const timestamp = Date.now();
945
+ for (const target of xcodeData.targets) {
946
+ const comp = mapTargetToComponent(target, timestamp);
947
+ allComponents.push(comp);
948
+ const memberConns = mapSourceMembership(target, comp.component_id, timestamp);
949
+ allConnections.push(...memberConns);
950
+ }
951
+ // Enrich project metadata with Xcode target info
952
+ if (projectMetadata) {
953
+ projectMetadata.targets = xcodeData.targets.map(t => ({
954
+ name: t.name,
955
+ type: t.type,
956
+ dependencies: t.frameworks,
957
+ }));
958
+ projectMetadata.xcodeProject = {
959
+ path: pbxprojPath,
960
+ targets: xcodeData.targets.map(t => ({
961
+ name: t.name,
962
+ type: t.type,
963
+ bundleId: t.bundleId,
964
+ })),
965
+ };
966
+ }
967
+ if (options.verbose) {
968
+ console.log(` Xcode: ${xcodeData.targets.length} targets`);
969
+ }
970
+ }
971
+ // Storyboard/XIB scanning
972
+ const { scanStoryboards } = await import('./scanners/xcode/storyboard-scanner.js');
973
+ const storyboardResult = await scanStoryboards(root);
974
+ allComponents.push(...storyboardResult.components);
975
+ allConnections.push(...storyboardResult.connections);
976
+ if (options.verbose && storyboardResult.components.length > 0) {
977
+ console.log(` Storyboards: ${storyboardResult.components.length} VCs, ${storyboardResult.connections.length} segues`);
978
+ }
979
+ }
980
+ catch (error) {
981
+ allWarnings.push({
982
+ type: 'parse_error',
983
+ message: `Xcode project scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
984
+ });
985
+ }
204
986
  }
205
- // Step 2: Run regex prompt detector with corroboration (secondary — catches prompt definitions)
206
- if (options.verbose)
207
- console.log(' - Running prompt detector (corroboration-filtered)...');
208
- promptScanResultHolder = await scanPrompts(root, {
209
- includeRawContent: true,
210
- detectVariables: true,
211
- requireAPICallAnchor: true,
212
- minCorroborationSignals: 2,
213
- });
214
- // Attach tracer results to prompt scan data (for web UI)
215
- if (traceResult) {
216
- promptScanResultHolder.tracedCalls = traceResult.calls;
217
- promptScanResultHolder.summary.tracedCallSites = traceResult.calls.length;
218
- }
219
- // Convert prompt definitions to architecture format
220
- const promptArchitecture = convertToArchitecture(promptScanResultHolder.prompts);
221
- allComponents.push(...promptArchitecture.components);
222
- allConnections.push(...promptArchitecture.connections);
223
- allWarnings.push(...promptArchitecture.warnings);
987
+ // AI prompts & LLM call tracing
988
+ if (options.prompts) {
989
+ // Step 1: Run anchor-based LLM call tracer (primary detection)
990
+ if (options.verbose)
991
+ console.log(' - Running LLM call tracer (anchor-based)...');
992
+ let traceResult;
993
+ try {
994
+ traceResult = await traceLLMCalls(root, incWalkSet);
995
+ allComponents.push(...traceResult.scanResult.components);
996
+ allConnections.push(...traceResult.scanResult.connections);
997
+ if (options.verbose) {
998
+ console.log(` Traced ${traceResult.calls.length} LLM call sites`);
999
+ console.log(` Wrappers: ${traceResult.wrappers.length}`);
1000
+ const providers = new Map();
1001
+ for (const call of traceResult.calls) {
1002
+ const p = call.provider.name;
1003
+ providers.set(p, (providers.get(p) || 0) + 1);
1004
+ }
1005
+ for (const [provider, count] of providers) {
1006
+ console.log(` ${provider}: ${count} call sites`);
1007
+ }
1008
+ }
1009
+ }
1010
+ catch (error) {
1011
+ allWarnings.push({
1012
+ type: 'parse_error',
1013
+ message: `LLM call tracer failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
1014
+ });
1015
+ if (options.verbose)
1016
+ console.log(` LLM tracer error: ${error instanceof Error ? error.message : 'Unknown'}`);
1017
+ }
1018
+ // Step 2: Run regex prompt detector with corroboration (secondary — catches prompt definitions)
1019
+ if (options.verbose)
1020
+ console.log(' - Running prompt detector (corroboration-filtered)...');
1021
+ promptScanResultHolder = await scanPrompts(root, {
1022
+ includeRawContent: true,
1023
+ detectVariables: true,
1024
+ aggressive: true,
1025
+ }, incWalkSet);
1026
+ // Attach tracer results to prompt scan data (for web UI)
1027
+ if (traceResult) {
1028
+ promptScanResultHolder.tracedCalls = traceResult.calls;
1029
+ promptScanResultHolder.summary.tracedCallSites = traceResult.calls.length;
1030
+ }
1031
+ // Convert prompt definitions to architecture format
1032
+ const promptArchitecture = convertToArchitecture(promptScanResultHolder.prompts);
1033
+ allComponents.push(...promptArchitecture.components);
1034
+ allConnections.push(...promptArchitecture.connections);
1035
+ allWarnings.push(...promptArchitecture.warnings);
1036
+ if (options.verbose) {
1037
+ console.log(` Found ${promptScanResultHolder.prompts.length} prompt definitions`);
1038
+ if (promptScanResultHolder.summary.byProvider) {
1039
+ for (const [provider, count] of Object.entries(promptScanResultHolder.summary.byProvider)) {
1040
+ console.log(` ${provider}: ${count}`);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ // ==========================================================================
1047
+ // Phase 3.5: Semantic Classification
1048
+ // ==========================================================================
1049
+ if (allConnections.length > 0 && allComponents.length > 0) {
224
1050
  if (options.verbose) {
225
- console.log(` Found ${promptScanResultHolder.prompts.length} prompt definitions`);
226
- if (promptScanResultHolder.summary.byProvider) {
227
- for (const [provider, count] of Object.entries(promptScanResultHolder.summary.byProvider)) {
228
- console.log(` ${provider}: ${count}`);
1051
+ console.log('Phase 3.5: Classifying connections...');
1052
+ }
1053
+ const semantics = classifyAllConnections(allConnections, allComponents);
1054
+ for (const conn of allConnections) {
1055
+ const info = semantics.get(conn.connection_id);
1056
+ if (info) {
1057
+ conn.semantic = info;
1058
+ }
1059
+ }
1060
+ if (options.verbose) {
1061
+ const byClass = new Map();
1062
+ for (const [, info] of semantics) {
1063
+ byClass.set(info.classification, (byClass.get(info.classification) || 0) + 1);
1064
+ }
1065
+ for (const [cls, count] of byClass) {
1066
+ console.log(` ${cls}: ${count}`);
1067
+ }
1068
+ }
1069
+ }
1070
+ // ==========================================================================
1071
+ // Phase 4: Deduplicate & Store
1072
+ // ==========================================================================
1073
+ if (options.verbose) {
1074
+ console.log('Phase 4: Storing results...');
1075
+ }
1076
+ // Deduplicate components by (type, name, primary-source-file) within current scan.
1077
+ //
1078
+ // Run 1.7 — Problem B: the prior key was `component.name` alone. That
1079
+ // collided cross-type — e.g., the file-level component for `lib/prisma.ts`
1080
+ // (type='component', name='prisma') vs the Prisma DB component
1081
+ // (type='database', name='prisma'). The DB component won on confidence;
1082
+ // the file component was silently dropped. But the import-scanner had
1083
+ // already emitted edges referencing the dropped file component_id —
1084
+ // 410 orphan edges on atomize-ai, which fired the integrity-promote and
1085
+ // truncated the graph (Problem A's loud symptom).
1086
+ //
1087
+ // The fix keys by `${type}|${name}|${first-config-file}`:
1088
+ // • Different types coexist (was: collided).
1089
+ // • Same-type same-name from different paths coexist (was: collided —
1090
+ // `app/proxy.ts` and `proxy.ts` both produce a file-level component
1091
+ // named `proxy`; both are real, both must be kept). Path
1092
+ // disambiguation matches Run 1.6 verify #6's stable_id contract for
1093
+ // the 6 component types where `name` alone isn't unique.
1094
+ // • Same-type same-name same-file STILL dedupes (the genuine duplicate
1095
+ // case — AST + regex both detecting the same service call), with
1096
+ // highest confidence winning. For components with no config_files
1097
+ // (rare) the key falls back to `${type}|${name}|` and behaves like
1098
+ // the legacy by-name dedup within that type.
1099
+ const componentMap = new Map();
1100
+ for (const component of allComponents) {
1101
+ const primaryFile = component.source?.config_files?.[0] ?? '';
1102
+ const key = `${component.type}|${component.name}|${primaryFile}`;
1103
+ const existing = componentMap.get(key);
1104
+ if (!existing || component.source.confidence > existing.source.confidence) {
1105
+ componentMap.set(key, component);
1106
+ }
1107
+ }
1108
+ const uniqueComponents = Array.from(componentMap.values());
1109
+ // Deduplicate connections by composite key (within current scan)
1110
+ // Keeps highest confidence when duplicates found (e.g., regex + AST detect same call)
1111
+ const connectionMap = new Map();
1112
+ for (const conn of allConnections) {
1113
+ const key = `${conn.from.component_id}|${conn.to.component_id}|${conn.connection_type}|${conn.code_reference?.file || ''}:${conn.code_reference?.line_start || ''}`;
1114
+ const existing = connectionMap.get(key);
1115
+ if (!existing || conn.confidence > existing.confidence) {
1116
+ connectionMap.set(key, conn);
1117
+ }
1118
+ }
1119
+ const uniqueConnections = Array.from(connectionMap.values());
1120
+ // (C) Resolve FILE: prefixed connection targets to real component IDs
1121
+ // This enables trace to follow imports from route files instead of dead-ending
1122
+ const compByFile = new Map(); // file path → component_id
1123
+ for (const comp of uniqueComponents) {
1124
+ for (const f of comp.source.config_files || []) {
1125
+ compByFile.set(f, comp.component_id);
1126
+ }
1127
+ }
1128
+ for (const conn of uniqueConnections) {
1129
+ if (conn.to.component_id?.startsWith('FILE:')) {
1130
+ const filePath = conn.to.component_id.slice(5);
1131
+ const realId = compByFile.get(filePath);
1132
+ if (realId) {
1133
+ conn.to.component_id = realId;
1134
+ }
1135
+ }
1136
+ if (conn.from.component_id?.startsWith('FILE:')) {
1137
+ const filePath = conn.from.component_id.slice(5);
1138
+ const realId = compByFile.get(filePath);
1139
+ if (realId) {
1140
+ conn.from.component_id = realId;
1141
+ }
1142
+ }
1143
+ }
1144
+ // Snapshot previous state before overwriting (for change tracking)
1145
+ // Also load the pre-scan snapshot for diff computation
1146
+ let preScanSnapshot = null;
1147
+ if (!options.clearFirst && decision.mode !== 'full') {
1148
+ try {
1149
+ await createSnapshot('pre-scan', config, root);
1150
+ preScanSnapshot = await loadLatestSnapshot(config, root);
1151
+ }
1152
+ catch {
1153
+ // No previous data to snapshot — first scan
1154
+ }
1155
+ }
1156
+ else if (decision.mode === 'full') {
1157
+ // For full scans, still create a snapshot for diff (if prior data exists)
1158
+ try {
1159
+ preScanSnapshot = await loadLatestSnapshot(config, root);
1160
+ }
1161
+ catch {
1162
+ // First-ever scan, no prior snapshot.
1163
+ }
1164
+ }
1165
+ // ==========================================================================
1166
+ // Phase 4 storage decision (Run 1 — D1 + D2):
1167
+ // - 'full': clearStorage was already done up front; now store everything fresh.
1168
+ // - 'incremental': clear ONLY components/connections that originate in the
1169
+ // walk-set, then merge the freshly-scanned uniqueComponents/Connections
1170
+ // with the survivors. Run integrity check; on failure, promote to full.
1171
+ // ==========================================================================
1172
+ let finalComponents = uniqueComponents;
1173
+ let finalConnections = uniqueConnections;
1174
+ if (decision.mode === 'incremental' && !options.clearFirst) {
1175
+ // Snapshot the FULL prior on-disk component set BEFORE clearForFiles —
1176
+ // we need it to remap surviving connections from old random component_ids
1177
+ // to the new ones (since stable_id is the join key but connections
1178
+ // reference component_id, which gets a fresh random suffix per scan).
1179
+ const preClearComponents = await loadAllComponents(config, root);
1180
+ // Clear only the touched subset.
1181
+ await clearForFiles(config, root, walkSet);
1182
+ // Load survivors (everything NOT in walk-set, still on disk).
1183
+ const survivingComponents = await loadAllComponents(config, root);
1184
+ const survivingConnections = await loadAllConnections(config, root);
1185
+ // Populate stable_ids on the in-memory uniqueComponents BEFORE merging.
1186
+ // Disk-loaded survivors get stable_ids from loadAllComponents, but
1187
+ // freshly-scanned components don't have them set until storeComponents
1188
+ // runs — and we merge BEFORE store. Without this, every fresh component
1189
+ // looks like a new entry to mergeByStableId (because its key falls back
1190
+ // to its random component_id), breaking dedup.
1191
+ for (const c of uniqueComponents)
1192
+ ensureStableIdPublic(c);
1193
+ // Merge: incoming wins on stable_id collision (component) or composite key (connection).
1194
+ // Components keyed by stable_id (or component_id fallback).
1195
+ finalComponents = mergeByStableId(survivingComponents, uniqueComponents, (c) => c.stable_id ?? c.component_id);
1196
+ // Build a remap: prior_component_id → new_component_id (via stable_id).
1197
+ // Connections from disk reference OLD random component_ids; the freshly
1198
+ // scanned components have NEW random component_ids. Same stable_id ties
1199
+ // them together. Rewrite surviving connection from/to ids so the merged
1200
+ // graph stays consistent.
1201
+ const stableToNewId = new Map();
1202
+ for (const c of finalComponents) {
1203
+ if (c.stable_id)
1204
+ stableToNewId.set(c.stable_id, c.component_id);
1205
+ }
1206
+ // oldIdToStable: maps every PRIOR-scan component_id (including ones we
1207
+ // just deleted via clearForFiles) to its stable_id. Built from the
1208
+ // pre-clear snapshot so we can resolve connection refs to their new IDs.
1209
+ const oldIdToStable = new Map();
1210
+ for (const c of preClearComponents) {
1211
+ if (c.stable_id)
1212
+ oldIdToStable.set(c.component_id, c.stable_id);
1213
+ }
1214
+ // Also map fresh components' ids → stable (no remap needed but keeps the
1215
+ // rewrite loop a no-op for these instead of leaving them undefined).
1216
+ for (const c of uniqueComponents) {
1217
+ if (c.stable_id)
1218
+ oldIdToStable.set(c.component_id, c.stable_id);
1219
+ }
1220
+ function remapId(id) {
1221
+ if (!id)
1222
+ return id;
1223
+ if (id.startsWith('FILE:'))
1224
+ return id;
1225
+ const stable = oldIdToStable.get(id);
1226
+ if (stable) {
1227
+ const newId = stableToNewId.get(stable);
1228
+ if (newId)
1229
+ return newId;
1230
+ }
1231
+ return id; // No remap available — leave alone, integrity check may catch
1232
+ }
1233
+ // Rewrite surviving connections to use the latest component_ids.
1234
+ for (const conn of survivingConnections) {
1235
+ if (conn.from?.component_id) {
1236
+ conn.from.component_id = remapId(conn.from.component_id) ?? conn.from.component_id;
1237
+ }
1238
+ if (conn.to?.component_id) {
1239
+ conn.to.component_id = remapId(conn.to.component_id) ?? conn.to.component_id;
1240
+ }
1241
+ }
1242
+ // Connections keyed by from|to|type|file:line composite (matches dedup key).
1243
+ const connKey = (c) => `${c.from?.component_id ?? ''}|${c.to?.component_id ?? ''}|${c.connection_type}|${c.code_reference?.file ?? ''}:${c.code_reference?.line_start ?? ''}`;
1244
+ finalConnections = mergeByStableId(survivingConnections, uniqueConnections, connKey);
1245
+ // Integrity check: every connection endpoint must exist; every walk-set
1246
+ // component must reference real source files. On failure → promote to full.
1247
+ const integrity = await runIntegrityCheck(finalComponents, finalConnections, root, walkSet);
1248
+ if (!integrity.ok) {
1249
+ if (options.verbose) {
1250
+ console.log(` Integrity check failed (${integrity.issues.length} issues) — promoting to full scan`);
1251
+ for (const issue of integrity.issues.slice(0, 3)) {
1252
+ console.log(` ${issue}`);
229
1253
  }
230
1254
  }
1255
+ // ============================================================
1256
+ // Run 1.7 — Problem A (recursive re-entry promote)
1257
+ // ============================================================
1258
+ // The pre-Run-1.7 promote reused the in-memory uniqueComponents/
1259
+ // uniqueConnections that were just computed under the walk-set
1260
+ // restriction. After Run 1.5's walk-set plumbing, those are NOT
1261
+ // the full source tree — only the walk-set's slice of it. Reusing
1262
+ // them on promote truncated the graph (atomize-ai: 6,445 → 58
1263
+ // connections, 2,452 → 58 components).
1264
+ //
1265
+ // Fix: release the scan lock and recursively re-enter scan() with
1266
+ // `mode: 'full', clearFirst: true`. The inner scan walks the full
1267
+ // source tree, the lock re-acquires cleanly inside, and its result
1268
+ // is returned verbatim. The internal `_promotedFromIncremental`
1269
+ // flag tells the inner scan to label its timeline entry and stats
1270
+ // `scan_type: 'incremental→full'` — preserving the Run 1.6 #3
1271
+ // evidence-preservation contract.
1272
+ //
1273
+ // We `return` early so the rest of the outer scan's phases
1274
+ // (storage, timeline, manifest, hashes) don't double-run.
1275
+ lock.release();
1276
+ return await scan(root, {
1277
+ ...options,
1278
+ mode: 'full',
1279
+ clearFirst: true,
1280
+ incremental: false,
1281
+ _promotedFromIncremental: true,
1282
+ });
1283
+ }
1284
+ // ============================================================
1285
+ // Run 1.7 — orphan-disk-file cleanup on successful incremental merge.
1286
+ // ============================================================
1287
+ // `clearForFiles` only deletes disk files whose `source.config_files`
1288
+ // overlap the walk-set. Components produced by always-full scanners
1289
+ // (npm/pip/swift packages, infra, prisma) don't list user source
1290
+ // files in `config_files` (they list manifests / abs paths), so
1291
+ // their disk files survive `clearForFiles`. After the merge, the
1292
+ // freshly-scanned versions get NEW random `component_id`s and are
1293
+ // written to NEW filenames. The OLD survivor files are now orphans:
1294
+ // unreachable from `finalComponents` but still on disk.
1295
+ //
1296
+ // Pre-Run-1.7 this was masked by the always-failing integrity check
1297
+ // on real projects (`clearStorage` on promote wiped the orphans).
1298
+ // Now that integrity passes (Problem B fix), and the promote is
1299
+ // recursive (Problem A fix), this latent bug surfaces as a doubling
1300
+ // of `npm`/`database`/`config`/`infra` components per incremental.
1301
+ //
1302
+ // Fix: after the merge, walk the components/connections directories
1303
+ // and unlink any file whose ID isn't in `finalComponents` /
1304
+ // `finalConnections`. Idempotent and atomic per-file (unlink errors
1305
+ // are silently swallowed — best-effort, matches the integrity-promote
1306
+ // pattern). Connections also need this since their random IDs aren't
1307
+ // stable across scans either.
1308
+ {
1309
+ const finalComponentIds = new Set(finalComponents.map((c) => c.component_id));
1310
+ const finalConnectionIds = new Set(finalConnections.map((c) => c.connection_id));
1311
+ const fsPromises = (await import('node:fs')).promises;
1312
+ const purgeOrphans = async (dir, keepIds) => {
1313
+ try {
1314
+ const files = await fsPromises.readdir(dir);
1315
+ await Promise.all(files
1316
+ .filter((f) => f.endsWith('.json'))
1317
+ .map(async (f) => {
1318
+ const id = f.slice(0, -'.json'.length);
1319
+ if (!keepIds.has(id)) {
1320
+ await fsPromises.unlink(path.join(dir, f)).catch(() => { });
1321
+ }
1322
+ }));
1323
+ }
1324
+ catch {
1325
+ // Dir missing or unreadable — non-fatal.
1326
+ }
1327
+ };
1328
+ await purgeOrphans(getComponentsPath(config, root), finalComponentIds);
1329
+ await purgeOrphans(getConnectionsPath(config, root), finalConnectionIds);
231
1330
  }
232
1331
  }
233
- }
234
- // ==========================================================================
235
- // Phase 4: Deduplicate & Store
236
- // ==========================================================================
237
- if (options.verbose) {
238
- console.log('Phase 4: Storing results...');
239
- }
240
- // Deduplicate components by name (within current scan)
241
- const componentMap = new Map();
242
- for (const component of allComponents) {
243
- const existing = componentMap.get(component.name);
244
- if (!existing || component.source.confidence > existing.source.confidence) {
245
- componentMap.set(component.name, component);
246
- }
247
- }
248
- const uniqueComponents = Array.from(componentMap.values());
249
- // Deduplicate connections by composite key (within current scan)
250
- // Keeps highest confidence when duplicates found (e.g., regex + AST detect same call)
251
- const connectionMap = new Map();
252
- for (const conn of allConnections) {
253
- const key = `${conn.from.component_id}|${conn.to.component_id}|${conn.connection_type}|${conn.code_reference?.file || ''}:${conn.code_reference?.line_start || ''}`;
254
- const existing = connectionMap.get(key);
255
- if (!existing || conn.confidence > existing.confidence) {
256
- connectionMap.set(key, conn);
257
- }
258
- }
259
- const uniqueConnections = Array.from(connectionMap.values());
260
- // Snapshot previous state before overwriting (for change tracking)
261
- if (!options.clearFirst) {
1332
+ // External enrichment (structural axis): stamp boundary nodes (npm/service/
1333
+ // llm/infra/spm/...) with cached canonical identity, latest version, docs, and
1334
+ // a freshness verdict. Offline + sync — reads NavGator's own JSON cache, never
1335
+ // the network, so it cannot slow or fail the scan. The freshness axis (network
1336
+ // re-checks) runs separately via refreshExternal / the external-resolver agent.
262
1337
  try {
263
- await createSnapshot('pre-scan', config, root);
1338
+ const cache = loadCache();
1339
+ enrichFromCache(finalComponents, makeLookup(cache), Date.now());
264
1340
  }
265
1341
  catch {
266
- // No previous data to snapshot first scan
1342
+ /* enrichment is best-effort never block persistence on it */
267
1343
  }
1344
+ // Store final state (atomic per-file writes — see storage.ts).
1345
+ await storeComponents(finalComponents, config, root);
1346
+ await storeConnections(finalConnections, config, root);
1347
+ // R6 footprint fix: clean up legacy per-entity files when the feature is
1348
+ // disabled (default). Idempotent and best-effort — never blocks the scan.
1349
+ // Surfaces a one-line notice when something actually got cleaned.
1350
+ try {
1351
+ const migrated = await migratePerEntityFiles(config, root);
1352
+ if (options.verbose &&
1353
+ (migrated.componentsRemoved > 0 ||
1354
+ migrated.connectionsRemoved > 0 ||
1355
+ migrated.dirsRemoved > 0)) {
1356
+ console.log(` R6 migration: removed ${migrated.componentsRemoved} legacy component file(s), ${migrated.connectionsRemoved} legacy connection file(s), ${migrated.dirsRemoved} now-empty dir(s)`);
1357
+ }
1358
+ }
1359
+ catch {
1360
+ // Best-effort.
1361
+ }
1362
+ // ==========================================================================
1363
+ // Phase 4.5: SQC Audit (Run 2 — D4)
1364
+ //
1365
+ // Sample the just-stored output, run deterministic verifiers (+ structured
1366
+ // LLM-judge payload in MCP mode), and update per-stratum EWMA state.
1367
+ // Audit failure NEVER fails the scan — only updates the index's EWMA so the
1368
+ // NEXT scan can auto-promote on detected drift.
1369
+ // ==========================================================================
1370
+ let auditReport;
1371
+ if (!options.noAudit) {
1372
+ try {
1373
+ const planOpt = options.auditPlan
1374
+ ? (options.auditPlan.toUpperCase() === 'AQL'
1375
+ ? 'AQL'
1376
+ : options.auditPlan.toUpperCase() === 'SPRT'
1377
+ ? 'SPRT'
1378
+ : 'Cochran')
1379
+ : undefined;
1380
+ const auditOpts = {
1381
+ plan: planOpt,
1382
+ isMcpMode: !!options.isMcpMode,
1383
+ priorEwma: priorIndex?.ewma,
1384
+ priorAuditCount: priorIndex?.audit_history_count ?? 0,
1385
+ forceCochran: !!priorIndex?.pending_drift_breach,
1386
+ };
1387
+ const r = await runAudit({ components: finalComponents, connections: finalConnections }, config, root, auditOpts);
1388
+ if (r) {
1389
+ // Update EWMA snapshot from prior index. We do not write the index here —
1390
+ // the post-Phase-5 index annotation already runs (line ~1430) and we
1391
+ // hijack `priorIndex` via the loadIndex/atomicWriteJSON cycle there.
1392
+ const { ewma, anyBreach } = updateEwmaForAudit(priorIndex?.ewma ?? undefined, r);
1393
+ if (anyBreach)
1394
+ r.drift_breach = true;
1395
+ auditReport = r;
1396
+ // Stash on a local for the index-annotation block below to consume.
1397
+ // (We re-read priorIndex after the freshIndex write to merge ewma.)
1398
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1399
+ auditReport.__ewma = ewma;
1400
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1401
+ auditReport.__anyBreach = anyBreach;
1402
+ }
1403
+ }
1404
+ catch (err) {
1405
+ if (process.env['NAVGATOR_DEBUG']) {
1406
+ console.error('[audit] skipped due to error:', err.message);
1407
+ }
1408
+ }
1409
+ }
1410
+ // ==========================================================================
1411
+ // Phase 5: Architecture Diff
1412
+ // ==========================================================================
1413
+ let timelineEntry;
1414
+ if (options.verbose) {
1415
+ console.log('Phase 5: Computing architecture diff...');
1416
+ }
1417
+ try {
1418
+ const currentSnapshot = await buildCurrentSnapshot(config, root);
1419
+ const diff = computeArchitectureDiff(preScanSnapshot, currentSnapshot);
1420
+ const { significance, triggers } = classifySignificance(diff);
1421
+ timelineEntry = {
1422
+ id: generateTimelineId(),
1423
+ timestamp: Date.now(),
1424
+ significance,
1425
+ triggers,
1426
+ diff,
1427
+ snapshot_id: currentSnapshot.snapshot_id,
1428
+ git: gitInfo,
1429
+ scan_type: scanType,
1430
+ // Run 1.6 — item #3: report walk-set size for 'incremental' AND for the
1431
+ // legacy in-place 'incremental→full' promote (which kept walkSet
1432
+ // populated), so a silent integrity-promote didn't erase evidence
1433
+ // that an incremental walk-set was attempted.
1434
+ //
1435
+ // Run 1.7 — Problem A: the recursive-re-entry promote runs as a true
1436
+ // full scan (walkSet empty). Reporting `walkSet.size = 0` would be
1437
+ // dishonest — the inner scan really did walk every source file.
1438
+ // Report `sourceFiles.length` in that case. Run 1.6 #3 still holds:
1439
+ // any future in-place promote path (walkSet populated under
1440
+ // 'incremental→full') reports walk-set size.
1441
+ // Use decision.mode (the EFFECTIVE scan mode) instead of scanType
1442
+ // (the user-visible label). On the recursive-re-entry promote (Run 1.7
1443
+ // Problem A), decision.mode='full' even though scanType='incremental→full',
1444
+ // and walkSet may be populated by the still-modified file — but the inner
1445
+ // scan walked the full source tree, so files_scanned must be sourceFiles.length.
1446
+ files_scanned: decision.mode === 'incremental' && walkSet.size > 0
1447
+ ? walkSet.size
1448
+ : sourceFiles.length,
1449
+ // Run 2 — D4: audit report (when produced).
1450
+ ...(auditReport ? { audit: stripInternals(auditReport) } : {}),
1451
+ };
1452
+ // Only save timeline entry if there are changes (or first scan)
1453
+ if (diff.stats.total_changes > 0 || !preScanSnapshot) {
1454
+ await saveTimelineEntry(timelineEntry, config, root);
1455
+ }
1456
+ if (options.verbose) {
1457
+ console.log(` Significance: ${significance}`);
1458
+ console.log(` Changes: ${diff.stats.total_changes}`);
1459
+ if (triggers.length > 0) {
1460
+ console.log(` Triggers: ${triggers.join(', ')}`);
1461
+ }
1462
+ }
1463
+ }
1464
+ catch {
1465
+ // Diff computation is non-critical
1466
+ if (options.verbose) {
1467
+ console.log(' Diff computation skipped (non-critical error)');
1468
+ }
1469
+ }
1470
+ // Build index, graph, file map, and summary.
1471
+ // R6: pass the in-memory final state so derived files don't depend on
1472
+ // per-entity disk reads (which are now opt-in and empty by default).
1473
+ const derivedData = { components: finalComponents, connections: finalConnections };
1474
+ await buildIndex(config, root, projectMetadata, derivedData);
1475
+ await buildGraph(config, root, derivedData);
1476
+ await buildFileMap(config, root, derivedData);
1477
+ // ==========================================================================
1478
+ // Phase 5.4: Derived reverse-deps index + manifest (Run 1.6 — items #8 + #9)
1479
+ // ==========================================================================
1480
+ // The reverse-deps index lets the next incremental scan compute walk-set
1481
+ // expansion from a single file open instead of walking every per-edge JSON.
1482
+ // The manifest lists all derived artifacts with their generated_at stamps.
1483
+ let reverseDepsEdgeCount;
1484
+ try {
1485
+ const result = await buildReverseDepsIndex(finalComponents, finalConnections, config, root);
1486
+ reverseDepsEdgeCount = result.edge_count;
1487
+ }
1488
+ catch (err) {
1489
+ if (process.env['NAVGATOR_DEBUG']) {
1490
+ console.error('[reverse-deps] index build skipped:', err.message);
1491
+ }
1492
+ }
1493
+ try {
1494
+ await buildDerivedManifest(config, root, { reverseDepsEdgeCount });
1495
+ }
1496
+ catch (err) {
1497
+ if (process.env['NAVGATOR_DEBUG']) {
1498
+ console.error('[manifest] write skipped:', err.message);
1499
+ }
1500
+ }
1501
+ // ==========================================================================
1502
+ // Phase 5.5: Annotate index with mode-tracking fields (Run 1 — D2)
1503
+ // Run AFTER buildIndex so we can annotate the freshly-written index
1504
+ // without buildIndex needing to know about modes.
1505
+ // ==========================================================================
1506
+ try {
1507
+ const freshIndex = await loadIndex(config, root);
1508
+ if (freshIndex) {
1509
+ if (scanType === 'full' || scanType === 'incremental→full') {
1510
+ freshIndex.last_full_scan = Date.now();
1511
+ freshIndex.incrementals_since_full = 0;
1512
+ }
1513
+ else if (scanType === 'incremental') {
1514
+ // Preserve last_full_scan from prior index; bump counter.
1515
+ if (priorIndex) {
1516
+ freshIndex.last_full_scan = priorIndex.last_full_scan ?? priorIndex.last_scan ?? 0;
1517
+ }
1518
+ freshIndex.incrementals_since_full = (priorIndex?.incrementals_since_full ?? 0) + 1;
1519
+ }
1520
+ // Always set schema_version to current build's version.
1521
+ freshIndex.schema_version = SCHEMA_VERSION;
1522
+ // Run 2 — D5: persist EWMA + audit history bookkeeping.
1523
+ if (auditReport) {
1524
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1525
+ const stash = auditReport;
1526
+ if (stash.__ewma) {
1527
+ freshIndex.ewma = stash.__ewma;
1528
+ }
1529
+ freshIndex.audit_history_count = (priorIndex?.audit_history_count ?? 0) + 1;
1530
+ freshIndex.pending_drift_breach = !!stash.__anyBreach;
1531
+ }
1532
+ else if (priorIndex) {
1533
+ // No audit this run — preserve prior values.
1534
+ if (priorIndex.ewma)
1535
+ freshIndex.ewma = priorIndex.ewma;
1536
+ if (priorIndex.audit_history_count !== undefined) {
1537
+ freshIndex.audit_history_count = priorIndex.audit_history_count;
1538
+ }
1539
+ // Clear pending breach if we just promoted on it.
1540
+ if (priorIndex.pending_drift_breach && scanType !== 'incremental') {
1541
+ freshIndex.pending_drift_breach = false;
1542
+ }
1543
+ else if (priorIndex.pending_drift_breach !== undefined) {
1544
+ freshIndex.pending_drift_breach = priorIndex.pending_drift_breach;
1545
+ }
1546
+ }
1547
+ await atomicWriteJSON(getIndexPath(config, root), freshIndex);
1548
+ }
1549
+ }
1550
+ catch {
1551
+ // Non-fatal: mode-tracking annotation is best-effort.
1552
+ }
1553
+ // Compute graph-wide metrics (PageRank + Louvain communities) → metrics.json,
1554
+ // and back-write per-component scores into component metadata so any consumer
1555
+ // that loads a component sees them. Suppressed for graphs <20 nodes.
1556
+ try {
1557
+ const { computeAndStoreMetrics } = await import('./metrics/pagerank-louvain.js');
1558
+ await computeAndStoreMetrics(config, root, {
1559
+ components: finalComponents,
1560
+ connections: finalConnections,
1561
+ });
1562
+ }
1563
+ catch (err) {
1564
+ // Non-fatal — scan still produces all other artifacts.
1565
+ if (process.env['NAVGATOR_DEBUG']) {
1566
+ console.error('[metrics] PageRank/Louvain skipped:', err.message);
1567
+ }
1568
+ }
1569
+ await buildSummary(config, root, promptScanResultHolder, projectMetadata, timelineEntry, gitInfo, derivedData);
1570
+ // R6: full-shape JSONL writers — the consolidated source of truth when
1571
+ // per-entity files are disabled (the default). These carry the complete
1572
+ // ArchitectureComponent / ArchitectureConnection objects so downstream
1573
+ // loaders never need per-entity files. Always written; cheap (~2MB for
1574
+ // atomize-ai-scale projects vs the 70MB per-entity sprawl).
1575
+ try {
1576
+ const { writeFullComponentsJsonl, writeFullConnectionsJsonl, } = await import('./storage/markdown-view.js');
1577
+ const { getStoragePath: getStoragePathFn } = await import('./config.js');
1578
+ const storeDir = getStoragePathFn(config, root);
1579
+ await writeFullComponentsJsonl(storeDir, finalComponents);
1580
+ await writeFullConnectionsJsonl(storeDir, finalConnections);
1581
+ }
1582
+ catch (err) {
1583
+ if (process.env['NAVGATOR_DEBUG']) {
1584
+ console.error('[full-jsonl] skipped:', err.message);
1585
+ }
1586
+ }
1587
+ // Markdown views + connections.jsonl (T3, trimmed scope).
1588
+ // Derived from in-memory components/connections — JSON remains canonical.
1589
+ // Emits .navgator/architecture/components-md/<type>/<slug>.md (Obsidian-readable,
1590
+ // git-diff-friendly, ripgrep-targetable) and connections.jsonl.
1591
+ // Disable via NAVGATOR_NO_MARKDOWN=1 if downstream tooling chokes.
1592
+ if (process.env['NAVGATOR_NO_MARKDOWN'] !== '1') {
1593
+ try {
1594
+ const { writeComponentMarkdownViews, writeConnectionsJsonl } = await import('./storage/markdown-view.js');
1595
+ const { getStoragePath: getStoragePathFn } = await import('./config.js');
1596
+ const storeDir = getStoragePathFn(config, root);
1597
+ await writeComponentMarkdownViews(storeDir, finalComponents, finalConnections);
1598
+ await writeConnectionsJsonl(storeDir, finalComponents, finalConnections);
1599
+ }
1600
+ catch (err) {
1601
+ if (process.env['NAVGATOR_DEBUG']) {
1602
+ console.error('[markdown-view] skipped:', err.message);
1603
+ }
1604
+ }
1605
+ }
1606
+ // Git-backed temporal snapshot (T5). Commits the .navgator/ directory to a
1607
+ // NESTED git store at .navgator/.git — invisible to the parent repo
1608
+ // (gitignored). OPT-IN: enable via NAVGATOR_COMMIT=1 or `--commit` scan
1609
+ // flag. Per-scan git subprocess overhead is ~180ms; default is OFF to
1610
+ // preserve the speed criterion.
1611
+ if (process.env['NAVGATOR_COMMIT'] === '1' || options.commit === true) {
1612
+ try {
1613
+ const { commitScan } = await import('./temporal/git-store.js');
1614
+ const { getStoragePath } = await import('./config.js');
1615
+ const storeDir = getStoragePath(config, root);
1616
+ const sha7 = (gitInfo?.commit ?? '').slice(0, 7);
1617
+ const msg = `scan ${new Date().toISOString()}${sha7 ? ` @ ${sha7}` : ''}`;
1618
+ const result = commitScan(storeDir, msg);
1619
+ if (!result.ok && process.env['NAVGATOR_DEBUG']) {
1620
+ console.error('[temporal] commit failed:', result.error);
1621
+ }
1622
+ }
1623
+ catch (err) {
1624
+ if (process.env['NAVGATOR_DEBUG']) {
1625
+ console.error('[temporal] skipped:', err.message);
1626
+ }
1627
+ }
1628
+ }
1629
+ // Persist prompt scan results if available
1630
+ if (promptScanResultHolder) {
1631
+ await savePromptScan(promptScanResultHolder, config, root);
1632
+ }
1633
+ // Register project in global registry
1634
+ try {
1635
+ await registerProject(root, {
1636
+ components: finalComponents.length,
1637
+ connections: finalConnections.length,
1638
+ prompts: promptScanResultHolder?.prompts.length ?? 0,
1639
+ }, timelineEntry?.significance, gitInfo);
1640
+ }
1641
+ catch {
1642
+ // Non-critical
1643
+ }
1644
+ // ==========================================================================
1645
+ // Phase 6: Save File Hashes
1646
+ // ==========================================================================
1647
+ if (options.verbose) {
1648
+ console.log('Phase 6: Saving file hashes...');
1649
+ }
1650
+ // Phase 6: hash both source files AND manifests so manifest edits are
1651
+ // detectable on the next scan (selectScanMode uses this to fire 'manifest-changed').
1652
+ const fileHashes = await computeFileHashes(filesForChangeDetection, root);
1653
+ await saveHashes(fileHashes, config, root);
1654
+ const duration = Date.now() - startTime;
1655
+ const filesChanged = fileChanges
1656
+ ? fileChanges.added.length + fileChanges.modified.length + fileChanges.removed.length
1657
+ : sourceFiles.length;
1658
+ if (options.verbose) {
1659
+ console.log(`\nScan complete in ${duration}ms`);
1660
+ console.log(` Components: ${finalComponents.length}`);
1661
+ console.log(` Connections: ${finalConnections.length}`);
1662
+ console.log(` Files scanned: ${sourceFiles.length}`);
1663
+ console.log(` Files changed: ${filesChanged}`);
1664
+ console.log(` Warnings: ${allWarnings.length}`);
1665
+ }
1666
+ // Gitignore safety guard: NavGator's per-config-var component files and
1667
+ // NAVSUMMARY docs include parsed hostnames from .env files. They're
1668
+ // regenerated on every scan, so there's no loss from keeping them local.
1669
+ // Auto-add gitignore entries on first scan so hostnames don't drift into
1670
+ // git history. Silent unless it makes a change.
1671
+ try {
1672
+ const guardResult = await ensureSafeGitignore(root);
1673
+ if (guardResult.action === 'added' && options.verbose) {
1674
+ console.log(` NavGator safety guard: added gitignore block for architecture/components/COMP_config_*.json + NAVSUMMARY*.md`);
1675
+ }
1676
+ }
1677
+ catch {
1678
+ // Non-fatal: scan already completed, gitignore guard is best-effort
1679
+ }
1680
+ return {
1681
+ components: finalComponents,
1682
+ connections: finalConnections,
1683
+ warnings: allWarnings,
1684
+ fileChanges,
1685
+ promptScan: promptScanResultHolder,
1686
+ fieldUsageReport: fieldUsageReportResult,
1687
+ typeSpecReport: typeSpecReportResult,
1688
+ timelineEntry,
1689
+ gitInfo,
1690
+ stats: {
1691
+ scan_duration_ms: duration,
1692
+ components_found: finalComponents.length,
1693
+ connections_found: finalConnections.length,
1694
+ warnings_count: allWarnings.length,
1695
+ // Run 1.6 — item #3 / Run 1.7 — Problem A: walk-set size for incremental
1696
+ // and for an in-place promote (walkSet populated). Recursive-re-entry
1697
+ // promote (walkSet empty) reports actual source-file count.
1698
+ // Use decision.mode (the EFFECTIVE scan mode) instead of scanType
1699
+ // (the user-visible label). On the recursive-re-entry promote (Run 1.7
1700
+ // Problem A), decision.mode='full' even though scanType='incremental→full',
1701
+ // and walkSet may be populated by the still-modified file — but the inner
1702
+ // scan walked the full source tree, so files_scanned must be sourceFiles.length.
1703
+ files_scanned: decision.mode === 'incremental' && walkSet.size > 0
1704
+ ? walkSet.size
1705
+ : sourceFiles.length,
1706
+ files_changed: filesChanged,
1707
+ prompts_found: promptScanResultHolder?.prompts.length,
1708
+ },
1709
+ };
268
1710
  }
269
- // Clear old components/connections before storing new ones
270
- // This ensures no duplicate accumulation across scans
271
- await clearStorage(config, root);
272
- ensureStorageDirectories(config, root);
273
- // Store components and connections
274
- await storeComponents(uniqueComponents, config, root);
275
- await storeConnections(uniqueConnections, config, root);
276
- // Build index, graph, file map, and summary
277
- await buildIndex(config, root, projectMetadata);
278
- await buildGraph(config, root);
279
- await buildFileMap(config, root);
280
- await buildSummary(config, root, promptScanResultHolder, projectMetadata);
281
- // Persist prompt scan results if available
282
- if (promptScanResultHolder) {
283
- await savePromptScan(promptScanResultHolder, config, root);
284
- }
285
- // ==========================================================================
286
- // Phase 5: Save File Hashes
287
- // ==========================================================================
288
- if (options.verbose) {
289
- console.log('Phase 5: Saving file hashes...');
290
- }
291
- const fileHashes = await computeFileHashes(sourceFiles, root);
292
- await saveHashes(fileHashes, config, root);
293
- const duration = Date.now() - startTime;
294
- const filesChanged = fileChanges
295
- ? fileChanges.added.length + fileChanges.modified.length + fileChanges.removed.length
296
- : sourceFiles.length;
297
- if (options.verbose) {
298
- console.log(`\nScan complete in ${duration}ms`);
299
- console.log(` Components: ${uniqueComponents.length}`);
300
- console.log(` Connections: ${uniqueConnections.length}`);
301
- console.log(` Files scanned: ${sourceFiles.length}`);
302
- console.log(` Files changed: ${filesChanged}`);
303
- console.log(` Warnings: ${allWarnings.length}`);
1711
+ finally {
1712
+ // Run 1.6 item #4: release the scan lock on every exit path
1713
+ // (success, early-return, throw). Idempotent.
1714
+ lock.release();
304
1715
  }
305
- return {
306
- components: uniqueComponents,
307
- connections: uniqueConnections,
308
- warnings: allWarnings,
309
- fileChanges,
310
- promptScan: promptScanResultHolder,
311
- stats: {
312
- scan_duration_ms: duration,
313
- components_found: uniqueComponents.length,
314
- connections_found: allConnections.length,
315
- warnings_count: allWarnings.length,
316
- files_scanned: sourceFiles.length,
317
- files_changed: filesChanged,
318
- prompts_found: promptScanResultHolder?.prompts.length,
319
- },
320
- };
321
1716
  }
322
1717
  /**
323
1718
  * Quick scan - only packages, no code analysis
@@ -355,8 +1750,7 @@ export async function scanPromptsOnly(projectRoot, options = {}) {
355
1750
  const result = await scanPrompts(root, {
356
1751
  includeRawContent: true,
357
1752
  detectVariables: true,
358
- requireAPICallAnchor: true,
359
- minCorroborationSignals: 2,
1753
+ aggressive: true,
360
1754
  });
361
1755
  // Attach tracer data
362
1756
  if (traceResult) {
@@ -372,6 +1766,88 @@ export async function scanPromptsOnly(projectRoot, options = {}) {
372
1766
  export { formatPromptsOutput, formatPromptDetail } from './scanners/prompts/index.js';
373
1767
  // Re-export tracer types
374
1768
  export { traceLLMCalls } from './scanners/connections/llm-call-tracer.js';
1769
+ /**
1770
+ * In-process debounce: tracks the timestamp of the last refresh ATTEMPT
1771
+ * per resolved project root. If a second call arrives while a first is still
1772
+ * in-flight (or was attempted within staleAfterMs), the second call returns
1773
+ * {refreshed:false, reason:'fresh'} without dispatching a second scan.
1774
+ *
1775
+ * This prevents a polling loop on MCP `status` from fanning out N concurrent
1776
+ * incremental scans. The map is module-scoped so it persists across calls
1777
+ * within the same Node.js process lifetime.
1778
+ */
1779
+ const _lastRefreshAttemptMs = new Map();
1780
+ export async function autoRefreshIfStale(projectRoot, options = {}) {
1781
+ // Resolve opt-in / opt-out. Programmatic > env > default(true).
1782
+ const envOptOut = process.env['NAVGATOR_AUTO_REFRESH'] === 'false';
1783
+ const enabled = options.enabled ?? !envOptOut;
1784
+ if (!enabled) {
1785
+ return { refreshed: false, reason: 'disabled', message: 'auto-refresh disabled' };
1786
+ }
1787
+ const staleAfterMs = (options.staleAfterMinutes ?? 5) * 60 * 1000;
1788
+ const root = projectRoot || process.cwd();
1789
+ // Debounce: if a refresh was attempted for this root within staleAfterMs,
1790
+ // return 'fresh' immediately rather than piling on a second scan.
1791
+ // Stamp BEFORE any async work to close the race window — concurrent callers
1792
+ // see the guard the moment the first call passes it.
1793
+ const lastAttempt = _lastRefreshAttemptMs.get(root) ?? 0;
1794
+ if (Date.now() - lastAttempt < staleAfterMs) {
1795
+ return { refreshed: false, reason: 'fresh', message: 'refresh already in-flight or recently attempted' };
1796
+ }
1797
+ _lastRefreshAttemptMs.set(root, Date.now());
1798
+ try {
1799
+ const { loadIndex } = await import('./storage.js');
1800
+ const config = getConfig();
1801
+ const index = await loadIndex(config, root);
1802
+ if (!index || !index.last_scan) {
1803
+ return {
1804
+ refreshed: false,
1805
+ reason: 'no-index',
1806
+ message: 'no prior scan — run `navgator scan` first',
1807
+ };
1808
+ }
1809
+ const ageMs = Date.now() - index.last_scan;
1810
+ if (ageMs < staleAfterMs) {
1811
+ return {
1812
+ refreshed: false,
1813
+ reason: 'fresh',
1814
+ message: `graph fresh (${Math.round(ageMs / 1000)}s old)`,
1815
+ };
1816
+ }
1817
+ // Stale → run incremental. selectScanMode will pick the right mode
1818
+ // internally; on a "no changes" hit it returns almost immediately.
1819
+ const scanFn = options.scanImpl ?? scan;
1820
+ const result = await scanFn(root, { mode: 'incremental' });
1821
+ const changed = (result.fileChanges?.added.length ?? 0) +
1822
+ (result.fileChanges?.modified.length ?? 0) +
1823
+ (result.fileChanges?.removed.length ?? 0);
1824
+ // Stamp coherence: this incremental scan covered every changed file via
1825
+ // hashes, so the freshness ledger/stamp must be reconciled or the stamp
1826
+ // would lie. Best-effort — never fail the refresh on freshness bookkeeping.
1827
+ try {
1828
+ const { reconcileClean } = await import('./freshness/drainer.js');
1829
+ await reconcileClean(root);
1830
+ }
1831
+ catch {
1832
+ /* freshness subsystem is optional; ignore */
1833
+ }
1834
+ return {
1835
+ refreshed: true,
1836
+ reason: 'stale',
1837
+ filesChanged: changed,
1838
+ message: changed > 0
1839
+ ? `↻ refreshed ${changed} changed file(s)`
1840
+ : '↻ refreshed (no file changes)',
1841
+ };
1842
+ }
1843
+ catch (err) {
1844
+ return {
1845
+ refreshed: false,
1846
+ reason: 'error',
1847
+ message: `auto-refresh failed: ${err instanceof Error ? err.message : 'unknown'}`,
1848
+ };
1849
+ }
1850
+ }
375
1851
  /**
376
1852
  * Get scan status/summary without running a full scan
377
1853
  */