@documentation-robotics/cli 0.1.0 → 0.1.5

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 (510) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +466 -693
  3. package/dist/analysis/data-model-analyzer.js +189 -0
  4. package/dist/analysis/data-model-analyzer.js.map +7 -0
  5. package/dist/analysis/index.js +3 -0
  6. package/dist/analysis/index.js.map +7 -0
  7. package/dist/analysis/relationship-classifier.js +181 -0
  8. package/dist/analysis/relationship-classifier.js.map +7 -0
  9. package/dist/analyzers/base-analyzer.js +1 -0
  10. package/dist/analyzers/base-analyzer.js.map +7 -0
  11. package/dist/analyzers/call-graph-utils.js +39 -0
  12. package/dist/analyzers/call-graph-utils.js.map +7 -0
  13. package/dist/analyzers/cbm-analyzer.js +1205 -0
  14. package/dist/analyzers/cbm-analyzer.js.map +7 -0
  15. package/dist/analyzers/heuristic-utils.js +55 -0
  16. package/dist/analyzers/heuristic-utils.js.map +7 -0
  17. package/dist/analyzers/mapping-loader.js +146 -0
  18. package/dist/analyzers/mapping-loader.js.map +7 -0
  19. package/dist/analyzers/registry.js +124 -0
  20. package/dist/analyzers/registry.js.map +7 -0
  21. package/dist/analyzers/session-state.js +176 -0
  22. package/dist/analyzers/session-state.js.map +7 -0
  23. package/dist/analyzers/stdio-client.js +206 -0
  24. package/dist/analyzers/stdio-client.js.map +7 -0
  25. package/dist/analyzers/types.js +15 -0
  26. package/dist/analyzers/types.js.map +7 -0
  27. package/dist/analyzers/verify-engine.js +209 -0
  28. package/dist/analyzers/verify-engine.js.map +7 -0
  29. package/dist/analyzers/verify-ignore.js +130 -0
  30. package/dist/analyzers/verify-ignore.js.map +7 -0
  31. package/dist/analyzers/version.js +8 -0
  32. package/dist/analyzers/version.js.map +7 -0
  33. package/dist/audit/ai/runner.js +41 -0
  34. package/dist/audit/ai/runner.js.map +7 -0
  35. package/dist/audit/differential-analyzer.js +183 -0
  36. package/dist/audit/differential-analyzer.js.map +7 -0
  37. package/dist/audit/index.js +32 -0
  38. package/dist/audit/index.js.map +7 -0
  39. package/dist/audit/nodes/ai/evaluator.js +49 -0
  40. package/dist/audit/nodes/ai/evaluator.js.map +7 -0
  41. package/dist/audit/nodes/ai/parser.js +55 -0
  42. package/dist/audit/nodes/ai/parser.js.map +7 -0
  43. package/dist/audit/nodes/ai/prompts.js +103 -0
  44. package/dist/audit/nodes/ai/prompts.js.map +7 -0
  45. package/dist/audit/nodes/analysis/node-completeness-checker.js +32 -0
  46. package/dist/audit/nodes/analysis/node-completeness-checker.js.map +7 -0
  47. package/dist/audit/nodes/analysis/node-definition-analyzer.js +47 -0
  48. package/dist/audit/nodes/analysis/node-definition-analyzer.js.map +7 -0
  49. package/dist/audit/nodes/analysis/node-overlap-detector.js +79 -0
  50. package/dist/audit/nodes/analysis/node-overlap-detector.js.map +7 -0
  51. package/dist/audit/nodes/model/orchestrator.js +82 -0
  52. package/dist/audit/nodes/model/orchestrator.js.map +7 -0
  53. package/dist/audit/nodes/spec/loader.js +116 -0
  54. package/dist/audit/nodes/spec/loader.js.map +7 -0
  55. package/dist/audit/nodes/spec/orchestrator.js +68 -0
  56. package/dist/audit/nodes/spec/orchestrator.js.map +7 -0
  57. package/dist/audit/nodes/types.js +1 -0
  58. package/dist/audit/nodes/types.js.map +7 -0
  59. package/dist/audit/pipeline/index.js +5 -0
  60. package/dist/audit/pipeline/index.js.map +7 -0
  61. package/dist/audit/pipeline/pipeline-orchestrator.js +218 -0
  62. package/dist/audit/pipeline/pipeline-orchestrator.js.map +7 -0
  63. package/dist/audit/relationships/ai/evaluator.js +235 -0
  64. package/dist/audit/relationships/ai/evaluator.js.map +7 -0
  65. package/dist/audit/relationships/ai/parser.js +197 -0
  66. package/dist/audit/relationships/ai/parser.js.map +7 -0
  67. package/dist/audit/relationships/ai/prompts.js +274 -0
  68. package/dist/audit/relationships/ai/prompts.js.map +7 -0
  69. package/dist/audit/relationships/analysis/balance-assessor.js +115 -0
  70. package/dist/audit/relationships/analysis/balance-assessor.js.map +7 -0
  71. package/dist/audit/relationships/analysis/coverage-analyzer.js +99 -0
  72. package/dist/audit/relationships/analysis/coverage-analyzer.js.map +7 -0
  73. package/dist/audit/relationships/analysis/duplicate-detector.js +95 -0
  74. package/dist/audit/relationships/analysis/duplicate-detector.js.map +7 -0
  75. package/dist/audit/relationships/analysis/gap-analyzer.js +99 -0
  76. package/dist/audit/relationships/analysis/gap-analyzer.js.map +7 -0
  77. package/dist/audit/relationships/analysis/layer-templates.js +294 -0
  78. package/dist/audit/relationships/analysis/layer-templates.js.map +7 -0
  79. package/dist/audit/relationships/graph/connectivity.js +129 -0
  80. package/dist/audit/relationships/graph/connectivity.js.map +7 -0
  81. package/dist/audit/relationships/graph/relationship-graph.js +179 -0
  82. package/dist/audit/relationships/graph/relationship-graph.js.map +7 -0
  83. package/dist/audit/relationships/model/balance-assessor.js +49 -0
  84. package/dist/audit/relationships/model/balance-assessor.js.map +7 -0
  85. package/dist/audit/relationships/model/coverage-analyzer.js +34 -0
  86. package/dist/audit/relationships/model/coverage-analyzer.js.map +7 -0
  87. package/dist/audit/relationships/model/duplicate-detector.js +68 -0
  88. package/dist/audit/relationships/model/duplicate-detector.js.map +7 -0
  89. package/dist/audit/relationships/model/gap-analyzer.js +41 -0
  90. package/dist/audit/relationships/model/gap-analyzer.js.map +7 -0
  91. package/dist/audit/relationships/model/orchestrator.js +126 -0
  92. package/dist/audit/relationships/model/orchestrator.js.map +7 -0
  93. package/dist/audit/relationships/spec/orchestrator.js +157 -0
  94. package/dist/audit/relationships/spec/orchestrator.js.map +7 -0
  95. package/dist/audit/reports/index.js +5 -0
  96. package/dist/audit/reports/index.js.map +7 -0
  97. package/dist/audit/reports/report-generator.js +118 -0
  98. package/dist/audit/reports/report-generator.js.map +7 -0
  99. package/dist/audit/snapshot-storage.js +163 -0
  100. package/dist/audit/snapshot-storage.js.map +7 -0
  101. package/dist/audit/types.js +21 -0
  102. package/dist/audit/types.js.map +7 -0
  103. package/dist/cli.js +223 -92
  104. package/dist/cli.js.map +3 -3
  105. package/dist/coding-agents/agents/claude-code.js +147 -0
  106. package/dist/coding-agents/agents/claude-code.js.map +7 -0
  107. package/dist/coding-agents/agents/index.js +5 -0
  108. package/dist/coding-agents/agents/index.js.map +7 -0
  109. package/dist/coding-agents/agents/types.js +1 -0
  110. package/dist/coding-agents/agents/types.js.map +7 -0
  111. package/dist/coding-agents/base-chat-client.js +45 -0
  112. package/dist/coding-agents/base-chat-client.js.map +7 -0
  113. package/dist/coding-agents/chat-utils.js +23 -0
  114. package/dist/coding-agents/chat-utils.js.map +7 -0
  115. package/dist/coding-agents/claude-code-client.js +246 -0
  116. package/dist/coding-agents/claude-code-client.js.map +7 -0
  117. package/dist/coding-agents/copilot-client.js +209 -0
  118. package/dist/coding-agents/copilot-client.js.map +7 -0
  119. package/dist/commands/add.js +95 -33
  120. package/dist/commands/add.js.map +2 -2
  121. package/dist/commands/analyzer.js +511 -0
  122. package/dist/commands/analyzer.js.map +7 -0
  123. package/dist/commands/audit-diff.js +49 -0
  124. package/dist/commands/audit-diff.js.map +7 -0
  125. package/dist/commands/audit-snapshots.js +62 -0
  126. package/dist/commands/audit-snapshots.js.map +7 -0
  127. package/dist/commands/audit.js +81 -0
  128. package/dist/commands/audit.js.map +7 -0
  129. package/dist/commands/catalog.js +248 -0
  130. package/dist/commands/catalog.js.map +7 -0
  131. package/dist/commands/changeset.js +577 -66
  132. package/dist/commands/changeset.js.map +2 -2
  133. package/dist/commands/chat.js +106 -83
  134. package/dist/commands/chat.js.map +2 -2
  135. package/dist/commands/claude.js +74 -0
  136. package/dist/commands/claude.js.map +7 -0
  137. package/dist/commands/conformance.js +71 -68
  138. package/dist/commands/conformance.js.map +2 -2
  139. package/dist/commands/copilot.js +75 -0
  140. package/dist/commands/copilot.js.map +7 -0
  141. package/dist/commands/delete.js +125 -11
  142. package/dist/commands/delete.js.map +3 -3
  143. package/dist/commands/discover-logic.js +44 -0
  144. package/dist/commands/discover-logic.js.map +7 -0
  145. package/dist/commands/docs.js +87 -0
  146. package/dist/commands/docs.js.map +7 -0
  147. package/dist/commands/element.js +14 -20
  148. package/dist/commands/element.js.map +2 -2
  149. package/dist/commands/export.js +13 -3
  150. package/dist/commands/export.js.map +2 -2
  151. package/dist/commands/import.js +51 -0
  152. package/dist/commands/import.js.map +7 -0
  153. package/dist/commands/info.js +30 -24
  154. package/dist/commands/info.js.map +2 -2
  155. package/dist/commands/init.js +27 -11
  156. package/dist/commands/init.js.map +2 -2
  157. package/dist/commands/list.js +63 -13
  158. package/dist/commands/list.js.map +3 -3
  159. package/dist/commands/relationship.js +225 -37
  160. package/dist/commands/relationship.js.map +2 -2
  161. package/dist/commands/report.js +54 -0
  162. package/dist/commands/report.js.map +7 -0
  163. package/dist/commands/reports.js +45 -0
  164. package/dist/commands/reports.js.map +7 -0
  165. package/dist/commands/schema.js +135 -0
  166. package/dist/commands/schema.js.map +7 -0
  167. package/dist/commands/search.js +40 -12
  168. package/dist/commands/search.js.map +2 -2
  169. package/dist/commands/show.js +58 -27
  170. package/dist/commands/show.js.map +2 -2
  171. package/dist/commands/stats.js +35 -0
  172. package/dist/commands/stats.js.map +7 -0
  173. package/dist/commands/trace.js +39 -16
  174. package/dist/commands/trace.js.map +2 -2
  175. package/dist/commands/update.js +141 -14
  176. package/dist/commands/update.js.map +2 -2
  177. package/dist/commands/upgrade.js +210 -46
  178. package/dist/commands/upgrade.js.map +3 -3
  179. package/dist/commands/validate.js +142 -23
  180. package/dist/commands/validate.js.map +3 -3
  181. package/dist/commands/version.js +8 -12
  182. package/dist/commands/version.js.map +2 -2
  183. package/dist/commands/visualize.js +155 -25
  184. package/dist/commands/visualize.js.map +2 -2
  185. package/dist/core/base-snapshot-manager.js +84 -0
  186. package/dist/core/base-snapshot-manager.js.map +7 -0
  187. package/dist/core/changeset-exporter.js +305 -0
  188. package/dist/core/changeset-exporter.js.map +7 -0
  189. package/dist/core/changeset-validator.js +41 -0
  190. package/dist/core/changeset-validator.js.map +7 -0
  191. package/dist/core/changeset.js +88 -173
  192. package/dist/core/changeset.js.map +2 -2
  193. package/dist/core/dependency-tracker.js +4 -2
  194. package/dist/core/dependency-tracker.js.map +2 -2
  195. package/dist/core/element.js +98 -27
  196. package/dist/core/element.js.map +2 -2
  197. package/dist/core/golden-copy-cache.js +290 -0
  198. package/dist/core/golden-copy-cache.js.map +7 -0
  199. package/dist/core/graph-model.js +266 -0
  200. package/dist/core/graph-model.js.map +7 -0
  201. package/dist/core/layer.js +159 -16
  202. package/dist/core/layer.js.map +2 -2
  203. package/dist/core/layers.js +32 -0
  204. package/dist/core/layers.js.map +7 -0
  205. package/dist/core/manifest.js +32 -11
  206. package/dist/core/manifest.js.map +2 -2
  207. package/dist/core/migration-registry.js +56 -1
  208. package/dist/core/migration-registry.js.map +2 -2
  209. package/dist/core/model.js +424 -217
  210. package/dist/core/model.js.map +3 -3
  211. package/dist/core/mutation-handler.js +179 -0
  212. package/dist/core/mutation-handler.js.map +7 -0
  213. package/dist/core/reference-registry.js +29 -21
  214. package/dist/core/reference-registry.js.map +2 -2
  215. package/dist/core/relationship-catalog.js +189 -0
  216. package/dist/core/relationship-catalog.js.map +7 -0
  217. package/dist/core/relationship-registry.js.map +2 -2
  218. package/dist/core/relationships.js.map +1 -1
  219. package/dist/core/report-data-model.js +285 -0
  220. package/dist/core/report-data-model.js.map +7 -0
  221. package/dist/core/spec-data-service.js +185 -0
  222. package/dist/core/spec-data-service.js.map +7 -0
  223. package/dist/core/spec-loader-types.js +1 -0
  224. package/dist/core/spec-loader-types.js.map +7 -0
  225. package/dist/core/spec-loader.js +234 -0
  226. package/dist/core/spec-loader.js.map +7 -0
  227. package/dist/core/staged-changeset-storage.js +282 -0
  228. package/dist/core/staged-changeset-storage.js.map +7 -0
  229. package/dist/core/staging-area.js +1002 -0
  230. package/dist/core/staging-area.js.map +7 -0
  231. package/dist/core/stats-collector.js +169 -0
  232. package/dist/core/stats-collector.js.map +7 -0
  233. package/dist/core/virtual-projection.js +443 -0
  234. package/dist/core/virtual-projection.js.map +7 -0
  235. package/dist/export/archimate-exporter.js +99 -60
  236. package/dist/export/archimate-exporter.js.map +2 -2
  237. package/dist/export/audit-formatters.js +479 -0
  238. package/dist/export/audit-formatters.js.map +7 -0
  239. package/dist/export/diff-formatters.js +99 -0
  240. package/dist/export/diff-formatters.js.map +7 -0
  241. package/dist/export/export-manager.js.map +2 -2
  242. package/dist/export/graphml-exporter.js +44 -27
  243. package/dist/export/graphml-exporter.js.map +2 -2
  244. package/dist/export/index.js +4 -0
  245. package/dist/export/index.js.map +2 -2
  246. package/dist/export/json-schema-exporter.js +55 -42
  247. package/dist/export/json-schema-exporter.js.map +2 -2
  248. package/dist/export/markdown-exporter.js +92 -70
  249. package/dist/export/markdown-exporter.js.map +2 -2
  250. package/dist/export/markdown-generator.js +248 -0
  251. package/dist/export/markdown-generator.js.map +7 -0
  252. package/dist/export/markdown-utils.js +30 -0
  253. package/dist/export/markdown-utils.js.map +7 -0
  254. package/dist/export/mermaid-markdown-exporter.js +37 -0
  255. package/dist/export/mermaid-markdown-exporter.js.map +7 -0
  256. package/dist/export/openapi-exporter.js +119 -71
  257. package/dist/export/openapi-exporter.js.map +2 -2
  258. package/dist/export/plantuml-exporter.js +104 -39
  259. package/dist/export/plantuml-exporter.js.map +2 -2
  260. package/dist/export/report-formatters.js +111 -0
  261. package/dist/export/report-formatters.js.map +7 -0
  262. package/dist/export/stats-formatters.js +98 -0
  263. package/dist/export/stats-formatters.js.map +7 -0
  264. package/dist/export/types.js +2 -14
  265. package/dist/export/types.js.map +2 -2
  266. package/dist/export/verify-formatters.js +153 -0
  267. package/dist/export/verify-formatters.js.map +7 -0
  268. package/dist/generated/compiled-validators.js +17 -0
  269. package/dist/generated/compiled-validators.js.map +7 -0
  270. package/dist/generated/index.js +31 -0
  271. package/dist/generated/index.js.map +7 -0
  272. package/dist/generated/layer-registry.js +190 -0
  273. package/dist/generated/layer-registry.js.map +7 -0
  274. package/dist/generated/layer-types.js +7 -0
  275. package/dist/generated/layer-types.js.map +7 -0
  276. package/dist/generated/node-types.js +1719 -0
  277. package/dist/generated/node-types.js.map +7 -0
  278. package/dist/generated/relationship-index.js +14552 -0
  279. package/dist/generated/relationship-index.js.map +7 -0
  280. package/dist/import/archimate-importer.js +166 -0
  281. package/dist/import/archimate-importer.js.map +7 -0
  282. package/dist/import/index.js +47 -0
  283. package/dist/import/index.js.map +7 -0
  284. package/dist/import/openapi-importer.js +143 -0
  285. package/dist/import/openapi-importer.js.map +7 -0
  286. package/dist/import/types.js +1 -0
  287. package/dist/import/types.js.map +7 -0
  288. package/dist/integrations/base-manager.js +311 -0
  289. package/dist/integrations/base-manager.js.map +7 -0
  290. package/dist/integrations/claude-manager.js +360 -0
  291. package/dist/integrations/claude-manager.js.map +7 -0
  292. package/dist/integrations/claude_code/agents/dr-advisor.md +253 -0
  293. package/dist/integrations/claude_code/agents/dr-architect.md +278 -0
  294. package/dist/integrations/claude_code/agents/dr-extractor.md +286 -0
  295. package/dist/integrations/claude_code/commands/dr-changeset.md +657 -0
  296. package/dist/integrations/claude_code/commands/dr-design.md +373 -0
  297. package/dist/integrations/claude_code/commands/dr-info.md +132 -0
  298. package/dist/integrations/claude_code/commands/dr-init.md +251 -0
  299. package/dist/integrations/claude_code/commands/dr-map.md +936 -0
  300. package/dist/integrations/claude_code/commands/dr-model.md +493 -0
  301. package/dist/integrations/claude_code/commands/dr-relate.md +814 -0
  302. package/dist/integrations/claude_code/commands/dr-sync.md +279 -0
  303. package/dist/integrations/claude_code/commands/dr-validate.md +654 -0
  304. package/dist/integrations/claude_code/commands/dr-verify.md +559 -0
  305. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +903 -0
  306. package/dist/integrations/claude_code/docs/QUICK_START.md +271 -0
  307. package/dist/integrations/claude_code/docs/README.md +347 -0
  308. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +550 -0
  309. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +620 -0
  310. package/dist/integrations/claude_code/skills/dr_03_security_layer/SKILL.md +642 -0
  311. package/dist/integrations/claude_code/skills/dr_04_application_layer/SKILL.md +911 -0
  312. package/dist/integrations/claude_code/skills/dr_05_technology_layer/SKILL.md +836 -0
  313. package/dist/integrations/claude_code/skills/dr_06_api_layer/SKILL.md +933 -0
  314. package/dist/integrations/claude_code/skills/dr_07_data_model_layer/SKILL.md +210 -0
  315. package/dist/integrations/claude_code/skills/dr_08_dataset_layer/SKILL.md +319 -0
  316. package/dist/integrations/claude_code/skills/dr_09_ux_layer/SKILL.md +319 -0
  317. package/dist/integrations/claude_code/skills/dr_10_navigation_layer/SKILL.md +318 -0
  318. package/dist/integrations/claude_code/skills/dr_11_apm_layer/SKILL.md +273 -0
  319. package/dist/integrations/claude_code/skills/dr_12_testing_layer/SKILL.md +273 -0
  320. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +187 -0
  321. package/dist/integrations/claude_code/skills/dr_codebase_memory/SKILL.md +590 -0
  322. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +295 -0
  323. package/dist/integrations/copilot-manager.js +328 -0
  324. package/dist/integrations/copilot-manager.js.map +7 -0
  325. package/dist/integrations/github_copilot/agents/dr-advisor.md +243 -0
  326. package/dist/integrations/github_copilot/agents/dr-architect.md +1567 -0
  327. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +491 -0
  328. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +539 -0
  329. package/dist/integrations/github_copilot/skills/dr_03_security_layer/SKILL.md +667 -0
  330. package/dist/integrations/github_copilot/skills/dr_04_application_layer/SKILL.md +670 -0
  331. package/dist/integrations/github_copilot/skills/dr_05_technology_layer/SKILL.md +660 -0
  332. package/dist/integrations/github_copilot/skills/dr_06_api_layer/SKILL.md +855 -0
  333. package/dist/integrations/github_copilot/skills/dr_07_data_model_layer/SKILL.md +171 -0
  334. package/dist/integrations/github_copilot/skills/dr_08_dataset_layer/SKILL.md +216 -0
  335. package/dist/integrations/github_copilot/skills/dr_09_ux_layer/SKILL.md +230 -0
  336. package/dist/integrations/github_copilot/skills/dr_10_navigation_layer/SKILL.md +228 -0
  337. package/dist/integrations/github_copilot/skills/dr_11_apm_layer/SKILL.md +234 -0
  338. package/dist/integrations/github_copilot/skills/dr_12_testing_layer/SKILL.md +230 -0
  339. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +187 -0
  340. package/dist/integrations/hash-utils.js +51 -0
  341. package/dist/integrations/hash-utils.js.map +7 -0
  342. package/dist/integrations/types.js +16 -0
  343. package/dist/integrations/types.js.map +7 -0
  344. package/dist/reports/model-layer-report-generator.js +278 -0
  345. package/dist/reports/model-layer-report-generator.js.map +7 -0
  346. package/dist/reports/model-report-data.js +57 -0
  347. package/dist/reports/model-report-data.js.map +7 -0
  348. package/dist/reports/model-report-orchestrator.js +103 -0
  349. package/dist/reports/model-report-orchestrator.js.map +7 -0
  350. package/dist/schemas/bundled/analyzers/cbm.json +595 -0
  351. package/dist/schemas/bundled/analyzers/manifest.json +9 -0
  352. package/dist/schemas/bundled/api.json +3411 -0
  353. package/dist/schemas/bundled/apm.json +2371 -0
  354. package/dist/schemas/bundled/application.json +1253 -0
  355. package/dist/schemas/bundled/base.json +1622 -0
  356. package/dist/schemas/bundled/business.json +1279 -0
  357. package/dist/schemas/bundled/data-model.json +1298 -0
  358. package/dist/schemas/bundled/data-store.json +2025 -0
  359. package/dist/schemas/bundled/manifest.json +93 -0
  360. package/dist/schemas/bundled/motivation.json +1360 -0
  361. package/dist/schemas/bundled/navigation.json +1874 -0
  362. package/dist/schemas/bundled/security.json +3488 -0
  363. package/dist/schemas/bundled/technology.json +2067 -0
  364. package/dist/schemas/bundled/testing.json +3120 -0
  365. package/dist/schemas/bundled/ux.json +3592 -0
  366. package/dist/server/schemas.js +202 -0
  367. package/dist/server/schemas.js.map +7 -0
  368. package/dist/server/server.js +1336 -252
  369. package/dist/server/server.js.map +3 -3
  370. package/dist/server/telemetry-middleware.js +22 -15
  371. package/dist/server/telemetry-middleware.js.map +2 -2
  372. package/dist/server-entry.js +65 -0
  373. package/dist/server-entry.js.map +7 -0
  374. package/dist/telemetry/config.js +89 -0
  375. package/dist/telemetry/config.js.map +7 -0
  376. package/dist/telemetry/console-interceptor.js.map +1 -1
  377. package/dist/telemetry/detect.js +24 -0
  378. package/dist/telemetry/detect.js.map +7 -0
  379. package/dist/telemetry/fetch-otlp-exporter.js +112 -0
  380. package/dist/telemetry/fetch-otlp-exporter.js.map +7 -0
  381. package/dist/telemetry/index.js +153 -3
  382. package/dist/telemetry/index.js.map +2 -2
  383. package/dist/telemetry/resilient-exporter.js +22 -8
  384. package/dist/telemetry/resilient-exporter.js.map +2 -2
  385. package/dist/telemetry/resilient-log-exporter.js +17 -8
  386. package/dist/telemetry/resilient-log-exporter.js.map +2 -2
  387. package/dist/telemetry/test-instrumentation.js.map +2 -2
  388. package/dist/types/index.js +84 -0
  389. package/dist/types/index.js.map +3 -3
  390. package/dist/types/source-reference.js +1 -0
  391. package/dist/types/source-reference.js.map +7 -0
  392. package/dist/utils/chat-logger.js +292 -0
  393. package/dist/utils/chat-logger.js.map +7 -0
  394. package/dist/utils/claude-stream.js +8 -0
  395. package/dist/utils/claude-stream.js.map +7 -0
  396. package/dist/utils/date-utils.js +7 -0
  397. package/dist/utils/date-utils.js.map +7 -0
  398. package/dist/utils/element-utils.js.map +1 -1
  399. package/dist/utils/errors.js +88 -15
  400. package/dist/utils/errors.js.map +3 -3
  401. package/dist/utils/export-python-annotations.js +82 -0
  402. package/dist/utils/export-python-annotations.js.map +7 -0
  403. package/dist/utils/file-io.js +7 -2
  404. package/dist/utils/file-io.js.map +2 -2
  405. package/dist/utils/file-lock.js +156 -0
  406. package/dist/utils/file-lock.js.map +7 -0
  407. package/dist/utils/id-generator.js +18 -0
  408. package/dist/utils/id-generator.js.map +7 -0
  409. package/dist/utils/layer-name-formatter.js +16 -0
  410. package/dist/utils/layer-name-formatter.js.map +7 -0
  411. package/dist/utils/markdown-anchor.js +7 -0
  412. package/dist/utils/markdown-anchor.js.map +7 -0
  413. package/dist/utils/markdown-link.js +12 -0
  414. package/dist/utils/markdown-link.js.map +7 -0
  415. package/dist/utils/markdown-table.js +17 -0
  416. package/dist/utils/markdown-table.js.map +7 -0
  417. package/dist/utils/mermaid-utils.js +11 -0
  418. package/dist/utils/mermaid-utils.js.map +7 -0
  419. package/dist/utils/project-paths.js.map +2 -2
  420. package/dist/utils/source-reference.js +52 -0
  421. package/dist/utils/source-reference.js.map +7 -0
  422. package/dist/utils/spec-installer.js +40 -53
  423. package/dist/utils/spec-installer.js.map +2 -2
  424. package/dist/utils/spec-version.js +5 -1
  425. package/dist/utils/spec-version.js.map +2 -2
  426. package/dist/utils/string-utils.js +12 -0
  427. package/dist/utils/string-utils.js.map +7 -0
  428. package/dist/utils/table-formatting.js +19 -0
  429. package/dist/utils/table-formatting.js.map +7 -0
  430. package/dist/validators/index.js +4 -0
  431. package/dist/validators/index.js.map +2 -2
  432. package/dist/validators/naming-validator.js +20 -23
  433. package/dist/validators/naming-validator.js.map +2 -2
  434. package/dist/validators/reference-validator.js +23 -38
  435. package/dist/validators/reference-validator.js.map +2 -2
  436. package/dist/validators/relationship-schema-validator.js +243 -0
  437. package/dist/validators/relationship-schema-validator.js.map +7 -0
  438. package/dist/validators/schema-validator.js +168 -68
  439. package/dist/validators/schema-validator.js.map +2 -2
  440. package/dist/validators/semantic-validator.js +36 -60
  441. package/dist/validators/semantic-validator.js.map +2 -2
  442. package/dist/validators/types.js.map +2 -2
  443. package/dist/validators/validation-formatter.js +159 -0
  444. package/dist/validators/validation-formatter.js.map +7 -0
  445. package/dist/validators/validator.js +24 -38
  446. package/dist/validators/validator.js.map +2 -2
  447. package/dist/viewer/README.md +139 -0
  448. package/dist/viewer/assets/main-CY0Xufuj.css +1 -0
  449. package/dist/viewer/assets/main-DtgErnwF.js +81 -0
  450. package/dist/viewer/assets/vendor-CxKvDp2x.js +17 -0
  451. package/dist/viewer/favicon.svg +11 -0
  452. package/dist/viewer/fonts/inter/Inter-Black.woff2 +0 -0
  453. package/dist/viewer/fonts/inter/Inter-Bold.woff2 +0 -0
  454. package/dist/viewer/fonts/inter/Inter-ExtraBold.woff2 +0 -0
  455. package/dist/viewer/fonts/inter/Inter-Light.woff2 +0 -0
  456. package/dist/viewer/fonts/inter/Inter-Medium.woff2 +0 -0
  457. package/dist/viewer/fonts/inter/Inter-Regular.woff2 +0 -0
  458. package/dist/viewer/fonts/inter/Inter-SemiBold.woff2 +0 -0
  459. package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
  460. package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
  461. package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
  462. package/dist/viewer/index.html +15 -0
  463. package/dist/viewer/manifest.json +99 -0
  464. package/dist/viewer/version.json +1 -0
  465. package/package.json +37 -13
  466. package/PYTHON_CLI_PARITY.md +0 -265
  467. package/PYTHON_CLI_STRUCTURE_UPDATE.md +0 -246
  468. package/biome.json +0 -42
  469. package/bunfig.toml +0 -3
  470. package/dist/ai/claude-client.js +0 -71
  471. package/dist/ai/claude-client.js.map +0 -7
  472. package/dist/ai/context-provider.js +0 -129
  473. package/dist/ai/context-provider.js.map +0 -7
  474. package/dist/ai/tools.js +0 -231
  475. package/dist/ai/tools.js.map +0 -7
  476. package/dist/commands/migrate.js +0 -36
  477. package/dist/commands/migrate.js.map +0 -7
  478. package/dist/commands/project.js +0 -74
  479. package/dist/commands/project.js.map +0 -7
  480. package/dist/core/active-changeset.js +0 -52
  481. package/dist/core/active-changeset.js.map +0 -7
  482. package/dist/core/projection-engine.js +0 -288
  483. package/dist/core/projection-engine.js.map +0 -7
  484. package/dist/schemas/bundled/01-motivation-layer.schema.json +0 -942
  485. package/dist/schemas/bundled/02-business-layer.schema.json +0 -1293
  486. package/dist/schemas/bundled/03-security-layer.schema.json +0 -2177
  487. package/dist/schemas/bundled/04-application-layer.schema.json +0 -1328
  488. package/dist/schemas/bundled/05-technology-layer.schema.json +0 -1465
  489. package/dist/schemas/bundled/06-api-layer.schema.json +0 -1743
  490. package/dist/schemas/bundled/07-data-model-layer.schema.json +0 -1253
  491. package/dist/schemas/bundled/08-datastore-layer.schema.json +0 -892
  492. package/dist/schemas/bundled/09-ux-layer.schema.json +0 -1506
  493. package/dist/schemas/bundled/10-navigation-layer.schema.json +0 -843
  494. package/dist/schemas/bundled/11-apm-observability-layer.schema.json +0 -1430
  495. package/dist/schemas/bundled/12-testing-layer.schema.json +0 -1091
  496. package/dist/schemas/bundled/common/layer-extensions.schema.json +0 -266
  497. package/dist/schemas/bundled/common/predicates.schema.json +0 -493
  498. package/dist/schemas/bundled/common/relationships.schema.json +0 -791
  499. package/dist/schemas/bundled/common/source-references.schema.json +0 -71
  500. package/dist/schemas/bundled/link-registry.json +0 -1839
  501. package/dist/schemas/bundled/link-registry.schema.json +0 -339
  502. package/dist/schemas/bundled/relationship-catalog.json +0 -1302
  503. package/dist/schemas/bundled/relationship-type.schema.json +0 -221
  504. package/dist/utils/model-path.js +0 -22
  505. package/dist/utils/model-path.js.map +0 -7
  506. package/docs/TEST-INSTRUMENTATION.md +0 -405
  507. package/docs/telemetry.md +0 -427
  508. package/test-debug.js +0 -33
  509. package/test-special.mjs +0 -46
  510. package/test_runner.ts +0 -7
@@ -1,60 +1,298 @@
1
- import { Hono } from "hono";
2
- import { upgradeWebSocket, websocket } from "hono/bun";
1
+ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
3
2
  import { cors } from "hono/cors";
4
- import { serve } from "bun";
3
+ import { swaggerUI } from "@hono/swagger-ui";
4
+ import { randomBytes } from "crypto";
5
+ import { createRequire } from "module";
6
+ import { fileURLToPath } from "url";
7
+ import path from "path";
8
+ let upgradeWebSocket, websocket;
9
+ const require2 = createRequire(import.meta.url);
5
10
  import { Model } from "../core/model.js";
11
+ import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
6
12
  import { telemetryMiddleware } from "./telemetry-middleware.js";
13
+ import { ClaudeCodeClient } from "../coding-agents/claude-code-client.js";
14
+ import { CopilotClient } from "../coding-agents/copilot-client.js";
15
+ import { detectAvailableClients, selectChatClient } from "../coding-agents/chat-utils.js";
16
+ import { getErrorMessage } from "../utils/errors.js";
17
+ import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
18
+ const JSONRPC_ERRORS = {
19
+ // Standard JSON-RPC errors
20
+ INVALID_PARAMS: -32602,
21
+ // Invalid method parameters
22
+ METHOD_NOT_FOUND: -32601,
23
+ // Method does not exist
24
+ INTERNAL_ERROR: -32603,
25
+ // Internal server error
26
+ // Custom application errors
27
+ NO_CLIENT_AVAILABLE: -32001
28
+ // No chat client available
29
+ };
7
30
  class VisualizationServer {
8
31
  app;
9
32
  model;
10
33
  _server;
34
+ // Bun.serve return type, loaded dynamically
11
35
  clients = /* @__PURE__ */ new Set();
12
36
  watcher;
13
37
  annotations = /* @__PURE__ */ new Map();
14
38
  // annotationId -> annotation
15
39
  annotationsByElement = /* @__PURE__ */ new Map();
16
40
  // elementId -> Set<annotationId>
41
+ replies = /* @__PURE__ */ new Map();
42
+ // annotationId -> replies[]
17
43
  changesets = /* @__PURE__ */ new Map();
18
44
  // changesetId -> changeset
45
+ changesetStorage;
19
46
  authToken;
20
47
  authEnabled = !0;
21
- // Enabled by default for security
48
+ withDanger = !1;
49
+ // Danger mode disabled by default
50
+ viewerPath;
51
+ // Optional custom viewer path
22
52
  activeChatProcesses = /* @__PURE__ */ new Map();
23
53
  // conversationId -> Bun.spawn process
24
54
  chatConversationCounter = 0;
55
+ selectedChatClient;
56
+ // Selected chat client for server
57
+ chatInitializationError;
58
+ // Store initialization error for status endpoint
59
+ /**
60
+ * Valid spec node IDs per layer
61
+ * Dynamically loaded from compiled spec (spec/dist/{layer}.json at module load time)
62
+ * Used to validate and resolve viewer spec node IDs
63
+ * Prevents silent drift when node types are added to spec/schemas/nodes/
64
+ */
65
+ static VALID_SPEC_NODE_IDS = VisualizationServer.loadValidSpecNodeIds();
66
+ /**
67
+ * Load valid spec node IDs dynamically from bundled spec files
68
+ * Extracts node_types arrays from each layer's compiled schema file
69
+ * This ensures the list is always in sync with the spec build output
70
+ * If any file cannot be loaded, falls back to a minimal safe set
71
+ */
72
+ static loadValidSpecNodeIds() {
73
+ const result = {}, requiredLayers = [
74
+ "motivation",
75
+ "business",
76
+ "security",
77
+ "application",
78
+ "technology",
79
+ "api",
80
+ "data-model",
81
+ "data-store",
82
+ "ux",
83
+ "navigation",
84
+ "apm",
85
+ "testing"
86
+ ], __dirname = path.dirname(fileURLToPath(import.meta.url)), bundledDir = path.join(__dirname, "..", "schemas", "bundled");
87
+ for (const layer of requiredLayers)
88
+ try {
89
+ const specFilePath = path.join(bundledDir, `${layer}.json`), nodeTypes = require2(specFilePath).layer?.node_types;
90
+ Array.isArray(nodeTypes) && nodeTypes.length > 0 ? result[layer] = nodeTypes : (console.warn(`[SPEC-LOAD-001] Layer ${layer} has no node_types in spec file`), result[layer] = []);
91
+ } catch (error) {
92
+ console.warn(`[SPEC-LOAD-002] Failed to load spec node IDs for layer ${layer}: ${getErrorMessage(error)}`), result[layer] = [];
93
+ }
94
+ return result;
95
+ }
96
+ /**
97
+ * Fallback spec node IDs used when type extraction from elementId fails
98
+ * These are used when no valid spec node ID can be extracted from the elementId
99
+ * or found through other resolution strategies
100
+ */
101
+ static LAYER_FALLBACK = {
102
+ security: "business.businessprocess",
103
+ technology: "application.applicationservice",
104
+ api: "business.businessprocess",
105
+ "data-model": "data-store.database",
106
+ "data-store": "data-store.database",
107
+ ux: "application.applicationcomponent",
108
+ navigation: "application.applicationcomponent",
109
+ apm: "application.applicationservice",
110
+ testing: "business.businessprocess"
111
+ };
112
+ /**
113
+ * All valid spec node IDs across all layers (cached Set for O(1) lookup)
114
+ */
115
+ static ALL_VALID_IDS = (() => {
116
+ const ids = /* @__PURE__ */ new Set();
117
+ return Object.values(VisualizationServer.VALID_SPEC_NODE_IDS).forEach((layerIds) => {
118
+ layerIds.forEach((id) => ids.add(id));
119
+ }), ids;
120
+ })();
25
121
  constructor(model, options) {
26
- this.app = new Hono(), this.model = model, this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
122
+ this.app = new OpenAPIHono(), this.model = model, this.loadBunAdapters(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
27
123
  if (!this.authEnabled)
28
124
  return next();
29
125
  const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
30
126
  let providedToken = null;
31
127
  return authHeader && authHeader.startsWith("Bearer ") ? providedToken = authHeader.substring(7) : queryToken && (providedToken = queryToken), providedToken ? providedToken !== this.authToken ? c.json({ error: "Invalid authentication token" }, 403) : next() : c.json({ error: "Authentication required. Please provide a valid token." }, 401);
32
- }), this.setupRoutes(), this.authEnabled && process.env.VERBOSE && console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`);
128
+ }), this.setupRoutes(), this.authEnabled && process.env.VERBOSE && console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`), this.initializeChatClients().catch((error) => {
129
+ this.chatInitializationError = error instanceof Error ? error : new Error(String(error)), console.error("[Chat] Failed to initialize chat clients:", error);
130
+ });
131
+ }
132
+ /**
133
+ * Load Bun-specific WebSocket adapters if available
134
+ * This is called in the constructor to support WebSocket functionality in Bun runtime
135
+ * In Node.js/tsx environments (e.g., generating OpenAPI spec), these imports are skipped
136
+ */
137
+ loadBunAdapters() {
138
+ if (typeof global.Bun < "u")
139
+ try {
140
+ const honoModule = require2("hono/bun");
141
+ upgradeWebSocket = honoModule.upgradeWebSocket, websocket = honoModule.websocket;
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ console.warn(`[Server] WARNING: Could not load WebSocket adapters from hono/bun: ${message}. WebSocket endpoints (/ws) will not be available. Please ensure 'hono' is installed and the Bun runtime is properly configured.`), process.env.VERBOSE && error instanceof Error && error.stack && console.debug("[Server] Stack trace:", error.stack);
145
+ }
146
+ }
147
+ /**
148
+ * Detect and initialize available chat clients
149
+ */
150
+ async initializeChatClients() {
151
+ const clients = await detectAvailableClients();
152
+ this.selectedChatClient = selectChatClient(clients, null), this.selectedChatClient && process.env.VERBOSE ? console.log(`[Chat] Using chat client: ${this.selectedChatClient.getClientName()}`) : clients.length === 0 && process.env.VERBOSE && console.log("[Chat] No chat clients available");
33
153
  }
34
154
  /**
35
155
  * Generate a random auth token
36
156
  */
37
157
  generateAuthToken() {
38
- return `dr-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
158
+ return `dr-${randomBytes(24).toString("base64url")}`;
159
+ }
160
+ /**
161
+ * Shared annotation update logic for PUT/PATCH handlers
162
+ * Performs partial updates with atomic operations
163
+ */
164
+ async updateAnnotation(annotationId, body) {
165
+ const annotation = this.annotations.get(annotationId);
166
+ if (!annotation)
167
+ return null;
168
+ const updatedAnnotation = {
169
+ ...annotation,
170
+ content: body.content ?? annotation.content,
171
+ tags: body.tags ?? annotation.tags,
172
+ resolved: body.resolved ?? annotation.resolved,
173
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
174
+ };
175
+ return this.annotations.set(annotationId, updatedAnnotation), await this.broadcastMessage({
176
+ type: "annotation.updated",
177
+ annotationId: updatedAnnotation.id,
178
+ timestamp: updatedAnnotation.updatedAt
179
+ }), updatedAnnotation;
39
180
  }
40
181
  /**
41
182
  * Setup HTTP routes and WebSocket handler
42
183
  */
43
184
  setupRoutes() {
44
- this.app.get("/", (c) => c.html(this.getViewerHTML())), this.app.get("/health", (c) => c.json({
45
- status: "ok",
46
- version: "0.1.0"
47
- })), this.app.get("/api/model", async (c) => {
185
+ this.app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
186
+ type: "http",
187
+ scheme: "bearer",
188
+ description: "API token (shown at server startup, pass as Authorization: Bearer <token>)"
189
+ }), this.app.get("/", (c) => this.viewerPath ? this.serveCustomViewer("index.html") : c.html(this.getViewerHTML())), this.viewerPath && this.app.get("/*", async (c, next) => {
190
+ const requestPath = c.req.path;
191
+ return process.env.VERBOSE && console.log(`[ROUTE] Catch-all matched: ${requestPath}`), requestPath.startsWith("/api/") || requestPath === "/ws" || requestPath === "/health" || requestPath === "/api-spec.yaml" || requestPath === "/api-docs" ? (process.env.VERBOSE && console.log(`[ROUTE] Catch-all delegating to next handler for: ${requestPath}`), next()) : (process.env.VERBOSE && console.log(`[ROUTE] Catch-all serving custom viewer file: ${requestPath}`), this.serveCustomViewer(requestPath.substring(1)));
192
+ });
193
+ const healthRoute = createRoute({
194
+ method: "get",
195
+ path: "/health",
196
+ tags: ["Health"],
197
+ summary: "Health check",
198
+ description: "Check if the server is running and healthy",
199
+ responses: {
200
+ 200: {
201
+ description: "Server is healthy",
202
+ content: {
203
+ "application/json": {
204
+ schema: HealthResponseSchema
205
+ }
206
+ }
207
+ }
208
+ }
209
+ });
210
+ this.app.openapi(healthRoute, (c) => {
211
+ const response = {
212
+ status: this.chatInitializationError ? "degraded" : "ok",
213
+ version: "0.1.0"
214
+ };
215
+ return this.chatInitializationError && (response.warnings = [
216
+ `Chat service unavailable: ${getErrorMessage(this.chatInitializationError)}`
217
+ ]), c.json(response);
218
+ });
219
+ const getModelRoute = createRoute({
220
+ method: "get",
221
+ path: "/api/model",
222
+ tags: ["Model"],
223
+ summary: "Get full model",
224
+ description: "Retrieve the complete architecture model across all layers",
225
+ security: [{ bearerAuth: [] }],
226
+ responses: {
227
+ 200: {
228
+ description: "Model data retrieved successfully",
229
+ content: {
230
+ "application/json": {
231
+ schema: ModelResponseSchema
232
+ }
233
+ }
234
+ },
235
+ 500: {
236
+ description: "Server error",
237
+ content: {
238
+ "application/json": {
239
+ schema: ErrorResponseSchema
240
+ }
241
+ }
242
+ }
243
+ }
244
+ });
245
+ this.app.openapi(getModelRoute, async (c) => {
246
+ process.env.VERBOSE && console.log("[ROUTE] /api/model handler called");
48
247
  try {
248
+ process.env.VERBOSE && console.log("[ROUTE] /api/model serializing model...");
49
249
  const modelData = await this.serializeModel();
50
- return c.json(modelData);
250
+ return process.env.VERBOSE && console.log(`[ROUTE] /api/model returning ${Object.keys(modelData.layers || {}).length} layers`), c.json(modelData);
51
251
  } catch (error) {
52
- const message = error instanceof Error ? error.message : String(error);
53
- return c.json({ error: message }, 500);
252
+ const message = getErrorMessage(error);
253
+ return console.error(`[ROUTE] /api/model error: ${message}`), c.json({ error: message }, 500);
54
254
  }
55
- }), this.app.get("/api/layers/:name", async (c) => {
255
+ });
256
+ const getLayerRoute = createRoute({
257
+ method: "get",
258
+ path: "/api/layers/:layerName",
259
+ tags: ["Model"],
260
+ summary: "Get layer",
261
+ description: "Retrieve a specific layer with all its elements",
262
+ security: [{ bearerAuth: [] }],
263
+ request: {
264
+ params: z.object({ layerName: LayerNameSchema })
265
+ },
266
+ responses: {
267
+ 200: {
268
+ description: "Layer retrieved successfully",
269
+ content: {
270
+ "application/json": {
271
+ schema: LayerResponseSchema
272
+ }
273
+ }
274
+ },
275
+ 404: {
276
+ description: "Layer not found",
277
+ content: {
278
+ "application/json": {
279
+ schema: ErrorResponseSchema
280
+ }
281
+ }
282
+ },
283
+ 500: {
284
+ description: "Server error",
285
+ content: {
286
+ "application/json": {
287
+ schema: ErrorResponseSchema
288
+ }
289
+ }
290
+ }
291
+ }
292
+ });
293
+ this.app.openapi(getLayerRoute, async (c) => {
56
294
  try {
57
- const layerName = c.req.param("name"), layer = await this.model.getLayer(layerName);
295
+ const { layerName } = c.req.valid("param"), layer = await this.model.getLayer(layerName);
58
296
  if (!layer)
59
297
  return c.json({ error: "Layer not found" }, 404);
60
298
  const elements = layer.listElements().map((e) => e.toJSON());
@@ -64,21 +302,37 @@ class VisualizationServer {
64
302
  elementCount: elements.length
65
303
  });
66
304
  } catch (error) {
67
- const message = error instanceof Error ? error.message : String(error);
305
+ const message = getErrorMessage(error);
68
306
  return c.json({ error: message }, 500);
69
307
  }
70
- }), this.app.get("/api/elements/:id", async (c) => {
71
- try {
72
- const elementId = c.req.param("id"), element = await this.findElement(elementId);
73
- return element ? c.json({
74
- ...element.toJSON(),
75
- annotations: this.annotations.get(elementId) || []
76
- }) : c.json({ error: "Element not found" }, 404);
77
- } catch (error) {
78
- const message = error instanceof Error ? error.message : String(error);
79
- return c.json({ error: message }, 500);
308
+ });
309
+ const getSpecRoute = createRoute({
310
+ method: "get",
311
+ path: "/api/spec",
312
+ tags: ["Schema"],
313
+ summary: "Get JSON schemas",
314
+ description: "Retrieve all JSON Schema definitions used by the system",
315
+ security: [{ bearerAuth: [] }],
316
+ responses: {
317
+ 200: {
318
+ description: "Schemas retrieved successfully",
319
+ content: {
320
+ "application/json": {
321
+ schema: SpecResponseSchema
322
+ }
323
+ }
324
+ },
325
+ 500: {
326
+ description: "Server error",
327
+ content: {
328
+ "application/json": {
329
+ schema: ErrorResponseSchema
330
+ }
331
+ }
332
+ }
80
333
  }
81
- }), this.app.get("/api/spec", async (c) => {
334
+ });
335
+ this.app.openapi(getSpecRoute, async (c) => {
82
336
  try {
83
337
  const schemas = await this.loadSchemas();
84
338
  return c.json({
@@ -90,19 +344,33 @@ class VisualizationServer {
90
344
  schemaCount: Object.keys(schemas).length
91
345
  });
92
346
  } catch (error) {
93
- const message = error instanceof Error ? error.message : String(error);
347
+ const message = getErrorMessage(error);
94
348
  return c.json({ error: message }, 500);
95
349
  }
96
- }), this.app.get("/api/link-registry", async (c) => {
97
- try {
98
- const linkRegistry = await this.loadLinkRegistry();
99
- return c.json(linkRegistry);
100
- } catch (error) {
101
- const message = error instanceof Error ? error.message : String(error);
102
- return c.json({ error: message }, 404);
350
+ });
351
+ const getAnnotationsRoute = createRoute({
352
+ method: "get",
353
+ path: "/api/annotations",
354
+ tags: ["Annotations"],
355
+ summary: "Get annotations",
356
+ description: "Retrieve all annotations, optionally filtered by element",
357
+ security: [{ bearerAuth: [] }],
358
+ request: {
359
+ query: AnnotationFilterSchema
360
+ },
361
+ responses: {
362
+ 200: {
363
+ description: "Annotations retrieved successfully",
364
+ content: {
365
+ "application/json": {
366
+ schema: AnnotationsListSchema
367
+ }
368
+ }
369
+ }
103
370
  }
104
- }), this.app.get("/api/annotations", (c) => {
105
- const elementId = c.req.query("elementId");
371
+ });
372
+ this.app.openapi(getAnnotationsRoute, (c) => {
373
+ const elementId = c.req.valid("query").elementId;
106
374
  if (elementId) {
107
375
  const annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
108
376
  return c.json({ annotations });
@@ -110,22 +378,72 @@ class VisualizationServer {
110
378
  const annotations = Array.from(this.annotations.values());
111
379
  return c.json({ annotations });
112
380
  }
113
- }), this.app.post("/api/annotations", async (c) => {
381
+ });
382
+ const createAnnotationRoute = createRoute({
383
+ method: "post",
384
+ path: "/api/annotations",
385
+ tags: ["Annotations"],
386
+ summary: "Create annotation",
387
+ description: "Create a new annotation on a model element",
388
+ security: [{ bearerAuth: [] }],
389
+ request: {
390
+ body: {
391
+ content: {
392
+ "application/json": {
393
+ schema: AnnotationCreateSchema
394
+ }
395
+ }
396
+ }
397
+ },
398
+ responses: {
399
+ 201: {
400
+ description: "Annotation created successfully",
401
+ content: {
402
+ "application/json": {
403
+ schema: AnnotationSchema
404
+ }
405
+ }
406
+ },
407
+ 400: {
408
+ description: "Invalid request body",
409
+ content: {
410
+ "application/json": {
411
+ schema: ErrorResponseSchema
412
+ }
413
+ }
414
+ },
415
+ 404: {
416
+ description: "Element not found",
417
+ content: {
418
+ "application/json": {
419
+ schema: ErrorResponseSchema
420
+ }
421
+ }
422
+ },
423
+ 500: {
424
+ description: "Server error",
425
+ content: {
426
+ "application/json": {
427
+ schema: ErrorResponseSchema
428
+ }
429
+ }
430
+ }
431
+ }
432
+ });
433
+ this.app.openapi(createAnnotationRoute, async (c) => {
114
434
  try {
115
- const body = await c.req.json();
116
- if (!body.elementId || !body.content || !body.author)
117
- return c.json({
118
- error: "Missing required fields: elementId, content, author"
119
- }, 400);
435
+ const body = c.req.valid("json");
120
436
  if (!await this.findElement(body.elementId))
121
437
  return c.json({ error: "Element not found" }, 404);
122
438
  const annotation = {
123
439
  id: this.generateAnnotationId(),
124
- elementId: String(body.elementId),
125
- author: String(body.author),
126
- content: String(body.content),
440
+ elementId: body.elementId,
441
+ author: body.author,
442
+ content: body.content,
127
443
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
128
- tags: body.tags || []
444
+ tags: body.tags,
445
+ resolved: !1
446
+ // Default to unresolved
129
447
  };
130
448
  return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(annotation.elementId) || this.annotationsByElement.set(annotation.elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(annotation.elementId).add(annotation.id), await this.broadcastMessage({
131
449
  type: "annotation.added",
@@ -134,30 +452,200 @@ class VisualizationServer {
134
452
  timestamp: annotation.createdAt
135
453
  }), c.json(annotation, 201);
136
454
  } catch (error) {
137
- const message = error instanceof Error ? error.message : String(error);
455
+ const message = getErrorMessage(error);
138
456
  return c.json({ error: message }, 500);
139
457
  }
140
- }), this.app.put("/api/annotations/:annotationId", async (c) => {
458
+ });
459
+ const getAnnotationRoute = createRoute({
460
+ method: "get",
461
+ path: "/api/annotations/:annotationId",
462
+ tags: ["Annotations"],
463
+ summary: "Get annotation by ID",
464
+ description: "Retrieve a specific annotation by its ID",
465
+ security: [{ bearerAuth: [] }],
466
+ request: {
467
+ params: z.object({ annotationId: IdSchema })
468
+ },
469
+ responses: {
470
+ 200: {
471
+ description: "Annotation retrieved successfully",
472
+ content: {
473
+ "application/json": {
474
+ schema: AnnotationSchema
475
+ }
476
+ }
477
+ },
478
+ 404: {
479
+ description: "Annotation not found",
480
+ content: {
481
+ "application/json": {
482
+ schema: ErrorResponseSchema
483
+ }
484
+ }
485
+ },
486
+ 500: {
487
+ description: "Server error",
488
+ content: {
489
+ "application/json": {
490
+ schema: ErrorResponseSchema
491
+ }
492
+ }
493
+ }
494
+ }
495
+ });
496
+ this.app.openapi(getAnnotationRoute, async (c) => {
141
497
  try {
142
- const annotationId = c.req.param("annotationId"), annotation = this.annotations.get(annotationId);
143
- if (!annotation)
144
- return c.json({ error: "Annotation not found" }, 404);
145
- const body = await c.req.json();
146
- return body.content !== void 0 && (annotation.content = String(body.content)), body.tags !== void 0 && (annotation.tags = body.tags), annotation.updatedAt = (/* @__PURE__ */ new Date()).toISOString(), await this.broadcastMessage({
147
- type: "annotation.updated",
148
- annotationId: annotation.id,
149
- timestamp: annotation.updatedAt
150
- }), c.json(annotation);
498
+ const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
499
+ return annotation ? c.json(annotation) : c.json({ error: "Annotation not found" }, 404);
151
500
  } catch (error) {
152
- const message = error instanceof Error ? error.message : String(error);
501
+ const message = getErrorMessage(error);
502
+ return c.json({ error: message }, 500);
503
+ }
504
+ });
505
+ const putAnnotationRoute = createRoute({
506
+ method: "put",
507
+ path: "/api/annotations/:annotationId",
508
+ tags: ["Annotations"],
509
+ summary: "Update annotation (partial)",
510
+ description: "Update an existing annotation with partial fields (NOTE: partial updates, use PATCH for recommended behavior)",
511
+ security: [{ bearerAuth: [] }],
512
+ request: {
513
+ params: z.object({ annotationId: IdSchema }),
514
+ body: {
515
+ content: {
516
+ "application/json": {
517
+ schema: AnnotationUpdateSchema
518
+ }
519
+ }
520
+ }
521
+ },
522
+ responses: {
523
+ 200: {
524
+ description: "Annotation updated successfully",
525
+ content: {
526
+ "application/json": {
527
+ schema: AnnotationSchema
528
+ }
529
+ }
530
+ },
531
+ 404: {
532
+ description: "Annotation not found",
533
+ content: {
534
+ "application/json": {
535
+ schema: ErrorResponseSchema
536
+ }
537
+ }
538
+ },
539
+ 500: {
540
+ description: "Server error",
541
+ content: {
542
+ "application/json": {
543
+ schema: ErrorResponseSchema
544
+ }
545
+ }
546
+ }
547
+ }
548
+ });
549
+ this.app.openapi(putAnnotationRoute, async (c) => {
550
+ try {
551
+ const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), updatedAnnotation = await this.updateAnnotation(annotationId, body);
552
+ return updatedAnnotation ? c.json(updatedAnnotation) : c.json({ error: "Annotation not found" }, 404);
553
+ } catch (error) {
554
+ const message = getErrorMessage(error);
555
+ return c.json({ error: message }, 500);
556
+ }
557
+ });
558
+ const patchAnnotationRoute = createRoute({
559
+ method: "patch",
560
+ path: "/api/annotations/:annotationId",
561
+ tags: ["Annotations"],
562
+ summary: "Partially update annotation",
563
+ description: "Partially update an existing annotation (recommended over PUT)",
564
+ security: [{ bearerAuth: [] }],
565
+ request: {
566
+ params: z.object({ annotationId: IdSchema }),
567
+ body: {
568
+ content: {
569
+ "application/json": {
570
+ schema: AnnotationUpdateSchema
571
+ }
572
+ }
573
+ }
574
+ },
575
+ responses: {
576
+ 200: {
577
+ description: "Annotation updated successfully",
578
+ content: {
579
+ "application/json": {
580
+ schema: AnnotationSchema
581
+ }
582
+ }
583
+ },
584
+ 404: {
585
+ description: "Annotation not found",
586
+ content: {
587
+ "application/json": {
588
+ schema: ErrorResponseSchema
589
+ }
590
+ }
591
+ },
592
+ 500: {
593
+ description: "Server error",
594
+ content: {
595
+ "application/json": {
596
+ schema: ErrorResponseSchema
597
+ }
598
+ }
599
+ }
600
+ }
601
+ });
602
+ this.app.openapi(patchAnnotationRoute, async (c) => {
603
+ try {
604
+ const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), updatedAnnotation = await this.updateAnnotation(annotationId, body);
605
+ return updatedAnnotation ? c.json(updatedAnnotation) : c.json({ error: "Annotation not found" }, 404);
606
+ } catch (error) {
607
+ const message = getErrorMessage(error);
153
608
  return c.json({ error: message }, 500);
154
609
  }
155
- }), this.app.delete("/api/annotations/:annotationId", async (c) => {
610
+ });
611
+ const deleteAnnotationRoute = createRoute({
612
+ method: "delete",
613
+ path: "/api/annotations/:annotationId",
614
+ tags: ["Annotations"],
615
+ summary: "Delete annotation",
616
+ description: "Delete an existing annotation",
617
+ security: [{ bearerAuth: [] }],
618
+ request: {
619
+ params: z.object({ annotationId: IdSchema })
620
+ },
621
+ responses: {
622
+ 204: {
623
+ description: "Annotation deleted successfully"
624
+ },
625
+ 404: {
626
+ description: "Annotation not found",
627
+ content: {
628
+ "application/json": {
629
+ schema: ErrorResponseSchema
630
+ }
631
+ }
632
+ },
633
+ 500: {
634
+ description: "Server error",
635
+ content: {
636
+ "application/json": {
637
+ schema: ErrorResponseSchema
638
+ }
639
+ }
640
+ }
641
+ }
642
+ });
643
+ this.app.openapi(deleteAnnotationRoute, async (c) => {
156
644
  try {
157
- const annotationId = c.req.param("annotationId"), annotation = this.annotations.get(annotationId);
645
+ const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
158
646
  if (!annotation)
159
647
  return c.json({ error: "Annotation not found" }, 404);
160
- this.annotations.delete(annotationId);
648
+ this.annotations.delete(annotationId), this.replies.delete(annotationId);
161
649
  const elementAnnotations = this.annotationsByElement.get(annotation.elementId);
162
650
  return elementAnnotations && elementAnnotations.delete(annotationId), await this.broadcastMessage({
163
651
  type: "annotation.deleted",
@@ -165,142 +653,478 @@ class VisualizationServer {
165
653
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
166
654
  }), c.body(null, 204);
167
655
  } catch (error) {
168
- const message = error instanceof Error ? error.message : String(error);
656
+ const message = getErrorMessage(error);
657
+ return c.json({ error: message }, 500);
658
+ }
659
+ });
660
+ const getAnnotationRepliesRoute = createRoute({
661
+ method: "get",
662
+ path: "/api/annotations/:annotationId/replies",
663
+ tags: ["Annotations"],
664
+ summary: "Get annotation replies",
665
+ description: "Retrieve all replies to an annotation",
666
+ security: [{ bearerAuth: [] }],
667
+ request: {
668
+ params: z.object({ annotationId: IdSchema })
669
+ },
670
+ responses: {
671
+ 200: {
672
+ description: "Replies retrieved successfully",
673
+ content: {
674
+ "application/json": {
675
+ schema: AnnotationRepliesSchema
676
+ }
677
+ }
678
+ },
679
+ 404: {
680
+ description: "Annotation not found",
681
+ content: {
682
+ "application/json": {
683
+ schema: ErrorResponseSchema
684
+ }
685
+ }
686
+ }
687
+ }
688
+ });
689
+ this.app.openapi(getAnnotationRepliesRoute, (c) => {
690
+ const { annotationId } = c.req.valid("param");
691
+ if (!this.annotations.get(annotationId))
692
+ return c.json({ error: "Annotation not found" }, 404);
693
+ const replies = this.replies.get(annotationId) || [];
694
+ return c.json({ replies });
695
+ });
696
+ const createAnnotationReplyRoute = createRoute({
697
+ method: "post",
698
+ path: "/api/annotations/:annotationId/replies",
699
+ tags: ["Annotations"],
700
+ summary: "Create annotation reply",
701
+ description: "Add a reply to an annotation",
702
+ security: [{ bearerAuth: [] }],
703
+ request: {
704
+ params: z.object({ annotationId: IdSchema }),
705
+ body: {
706
+ content: {
707
+ "application/json": {
708
+ schema: AnnotationReplyCreateSchema
709
+ }
710
+ }
711
+ }
712
+ },
713
+ responses: {
714
+ 201: {
715
+ description: "Reply created successfully",
716
+ content: {
717
+ "application/json": {
718
+ schema: AnnotationReplySchema
719
+ }
720
+ }
721
+ },
722
+ 404: {
723
+ description: "Annotation not found",
724
+ content: {
725
+ "application/json": {
726
+ schema: ErrorResponseSchema
727
+ }
728
+ }
729
+ },
730
+ 500: {
731
+ description: "Server error",
732
+ content: {
733
+ "application/json": {
734
+ schema: ErrorResponseSchema
735
+ }
736
+ }
737
+ }
738
+ }
739
+ });
740
+ this.app.openapi(createAnnotationReplyRoute, async (c) => {
741
+ try {
742
+ const { annotationId } = c.req.valid("param");
743
+ if (!this.annotations.get(annotationId))
744
+ return c.json({ error: "Annotation not found" }, 404);
745
+ const body = c.req.valid("json"), reply = {
746
+ id: this.generateReplyId(),
747
+ author: body.author,
748
+ content: body.content,
749
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
750
+ };
751
+ return this.replies.has(annotationId) || this.replies.set(annotationId, []), this.replies.get(annotationId).push(reply), await this.broadcastMessage({
752
+ type: "annotation.reply.added",
753
+ annotationId,
754
+ replyId: reply.id,
755
+ timestamp: reply.createdAt
756
+ }), c.json(reply, 201);
757
+ } catch (error) {
758
+ const message = getErrorMessage(error);
169
759
  return c.json({ error: message }, 500);
170
760
  }
171
- }), this.app.get("/api/changesets", (c) => {
761
+ });
762
+ const getChangesetsRoute = createRoute({
763
+ method: "get",
764
+ path: "/api/changesets",
765
+ tags: ["Changesets"],
766
+ summary: "Get changesets",
767
+ description: "Retrieve a list of all changesets",
768
+ security: [{ bearerAuth: [] }],
769
+ responses: {
770
+ 200: {
771
+ description: "Changesets retrieved successfully",
772
+ content: {
773
+ "application/json": {
774
+ schema: ChangesetsListSchema
775
+ }
776
+ }
777
+ }
778
+ }
779
+ });
780
+ this.app.openapi(getChangesetsRoute, (c) => {
172
781
  const changesets = {};
173
782
  for (const [id, changeset] of this.changesets)
174
783
  changesets[id] = {
175
- name: changeset.metadata.name,
176
- status: changeset.metadata.status,
177
- type: changeset.metadata.type,
178
- created_at: changeset.metadata.created_at,
179
- elements_count: changeset.changes.changes.length
784
+ name: changeset.name,
785
+ status: changeset.status,
786
+ created: changeset.created,
787
+ changes_count: changeset.changes.length
180
788
  };
181
789
  return c.json({
182
790
  version: "1.0.0",
183
791
  changesets
184
792
  });
185
- }), this.app.get("/api/changesets/:changesetId", (c) => {
186
- const changesetId = c.req.param("changesetId"), changeset = this.changesets.get(changesetId);
187
- return changeset ? c.json(changeset) : c.json({ error: "Changeset not found" }, 404);
188
- }), this.app.get("/api/elements/:id/annotations", (c) => {
189
- const elementId = c.req.param("id"), annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
190
- return c.json({ elementId, annotations });
191
- }), this.app.post("/api/elements/:id/annotations", async (c) => {
192
- try {
193
- const elementId = c.req.param("id");
194
- if (!await this.findElement(elementId))
195
- return c.json({ error: "Element not found" }, 404);
196
- const body = await c.req.json(), content = (body.content || body.text) && String(body.content || body.text).trim();
197
- if (!content)
198
- return c.json({ error: "Annotation content cannot be empty" }, 400);
199
- const author = body.author ? String(body.author).trim() : "Anonymous", annotation = {
200
- id: this.generateAnnotationId(),
201
- elementId,
202
- author,
203
- content,
204
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
205
- tags: body.tags || []
206
- };
207
- return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(elementId) || this.annotationsByElement.set(elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(elementId).add(annotation.id), await this.broadcastMessage({
208
- type: "annotation.added",
209
- annotationId: annotation.id,
210
- elementId: annotation.elementId,
211
- timestamp: annotation.createdAt
212
- }), c.json({ success: !0, annotation });
213
- } catch (error) {
214
- const message = error instanceof Error ? error.message : String(error);
215
- return c.json({ error: message }, 500);
793
+ });
794
+ const getChangesetRoute = createRoute({
795
+ method: "get",
796
+ path: "/api/changesets/:changesetId",
797
+ tags: ["Changesets"],
798
+ summary: "Get changeset",
799
+ description: "Retrieve a specific changeset with all its changes",
800
+ security: [{ bearerAuth: [] }],
801
+ request: {
802
+ params: z.object({ changesetId: IdSchema })
803
+ },
804
+ responses: {
805
+ 200: {
806
+ description: "Changeset retrieved successfully",
807
+ content: {
808
+ "application/json": {
809
+ schema: ChangesetDetailSchema
810
+ }
811
+ }
812
+ },
813
+ 404: {
814
+ description: "Changeset not found",
815
+ content: {
816
+ "application/json": {
817
+ schema: ErrorResponseSchema
818
+ }
819
+ }
820
+ }
216
821
  }
217
- }), this.app.get("/ws", async (c, next) => {
822
+ });
823
+ this.app.openapi(getChangesetRoute, (c) => {
824
+ const { changesetId } = c.req.valid("param"), changeset = this.changesets.get(changesetId);
825
+ return changeset ? c.json({
826
+ id: changeset.id,
827
+ name: changeset.name,
828
+ description: changeset.description,
829
+ status: changeset.status,
830
+ created: changeset.created,
831
+ modified: changeset.modified,
832
+ baseSnapshot: changeset.baseSnapshot,
833
+ stats: changeset.stats,
834
+ changes: changeset.changes
835
+ }) : c.json({ error: "Changeset not found" }, 404);
836
+ }), this.app.doc("/api-spec.yaml", {
837
+ openapi: "3.1.0",
838
+ info: {
839
+ title: "Documentation Robotics Visualization Server API",
840
+ version: "0.1.0",
841
+ description: "API specification for the DR CLI visualization server",
842
+ contact: {
843
+ name: "Documentation Robotics",
844
+ url: "https://github.com/tinkermonkey/documentation_robotics"
845
+ },
846
+ license: {
847
+ name: "ISC"
848
+ }
849
+ },
850
+ servers: [
851
+ { url: "http://localhost:8080", description: "Local development server" }
852
+ ],
853
+ tags: [
854
+ { name: "Health", description: "Server health and status" },
855
+ { name: "Schema", description: "JSON Schema specifications" },
856
+ { name: "Model", description: "Architecture model data" },
857
+ { name: "Changesets", description: "Model changesets and history" },
858
+ { name: "Annotations", description: "User annotations on model elements" },
859
+ { name: "WebSocket", description: "Real-time updates via WebSocket" }
860
+ ]
861
+ }), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), upgradeWebSocket && this.app.get("/ws", async (c, next) => {
218
862
  if (this.authEnabled) {
219
- const queryToken = c.req.query("token");
220
- if (!queryToken)
221
- return c.json({ error: "Authentication required. Provide token as query parameter: ?token=YOUR_TOKEN" }, 401);
222
- if (queryToken !== this.authToken)
863
+ const queryToken = c.req.query("token"), wsProtocol = c.req.header("Sec-WebSocket-Protocol");
864
+ let protocolToken = null;
865
+ if (wsProtocol) {
866
+ const protocols = wsProtocol.split(",").map((p) => p.trim()), tokenIndex = protocols.indexOf("token");
867
+ tokenIndex !== -1 && protocols.length > tokenIndex + 1 && (protocolToken = protocols[tokenIndex + 1]);
868
+ }
869
+ const authHeader = c.req.header("Authorization"), bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null, token = queryToken || protocolToken || bearerToken;
870
+ if (!token)
871
+ return c.json({
872
+ error: 'Authentication required. Provide token via: 1) Query param (?token=YOUR_TOKEN), 2) Sec-WebSocket-Protocol header (browser: new WebSocket(url, ["token", "YOUR_TOKEN"])), or 3) Authorization header (Bearer YOUR_TOKEN)'
873
+ }, 401);
874
+ if (token !== this.authToken)
223
875
  return c.json({ error: "Invalid authentication token" }, 403);
876
+ protocolToken && c.header("Sec-WebSocket-Protocol", "token");
224
877
  }
225
878
  return next();
226
879
  }, upgradeWebSocket(() => ({
227
- onOpen: (_evt, ws) => {
228
- this.clients.add(ws), ws.send(JSON.stringify({
880
+ onOpen: async (_evt, ws) => {
881
+ await this.recordWebSocketEvent("ws.connection.open", {
882
+ "ws.client_count": this.clients.size + 1
883
+ }), this.clients.add(ws), ws.send(JSON.stringify({
229
884
  type: "connected",
230
885
  version: "0.1.0",
231
886
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
232
887
  })), process.env.VERBOSE && console.log(`[WebSocket] Client connected (total: ${this.clients.size})`);
233
888
  },
234
- onClose: (_evt, ws) => {
235
- this.clients.delete(ws), process.env.VERBOSE && console.log(`[WebSocket] Client disconnected (total: ${this.clients.size})`);
889
+ onClose: async (_evt, ws) => {
890
+ await this.recordWebSocketEvent("ws.connection.close", {
891
+ "ws.client_count": this.clients.size - 1
892
+ }), this.clients.delete(ws), process.env.VERBOSE && console.log(`[WebSocket] Client disconnected (total: ${this.clients.size})`);
236
893
  },
237
894
  onMessage: async (message, ws) => {
895
+ const messageStartTime = Date.now();
896
+ let messageType = "unknown";
238
897
  try {
239
898
  const rawData = message.data ?? message;
240
899
  let msgStr;
241
900
  typeof rawData == "string" ? msgStr = rawData : rawData instanceof Buffer || rawData instanceof Uint8Array ? msgStr = new TextDecoder().decode(rawData) : msgStr = String(rawData);
242
- const data = JSON.parse(msgStr);
243
- data.jsonrpc === "2.0" ? await this.handleJSONRPCMessage(ws, data) : await this.handleWSMessage(ws, data);
901
+ const rawMessage = JSON.parse(msgStr), validationResult = WSMessageSchema.safeParse(rawMessage);
902
+ if (!validationResult.success) {
903
+ const errorMsg = validationResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
904
+ console.error("Invalid WebSocket message format:", errorMsg), ws.send(JSON.stringify({ error: "Invalid message format", details: errorMsg }));
905
+ return;
906
+ }
907
+ const data = validationResult.data;
908
+ "jsonrpc" in data && data.jsonrpc === "2.0" ? "method" in data && typeof data.method == "string" ? messageType = data.method : messageType = data.id?.toString() || "jsonrpc-response" : "type" in data ? messageType = data.type : messageType = "unknown", await this.recordWebSocketEvent("ws.message.received", {
909
+ "ws.message.type": messageType,
910
+ "ws.message.size_bytes": msgStr.length
911
+ }), "jsonrpc" in data && data.jsonrpc === "2.0" ? await this.handleJSONRPCMessage(ws, data) : await this.handleWSMessage(ws, data);
912
+ const durationMs = Date.now() - messageStartTime;
913
+ await this.recordWebSocketEvent("ws.message.processed", {
914
+ "ws.message.type": messageType,
915
+ "ws.message.duration_ms": durationMs,
916
+ "ws.message.status": "success"
917
+ });
244
918
  } catch (error) {
245
- const errorMsg = error instanceof Error ? error.message : String(error);
246
- process.env.DEBUG && console.error(`[WebSocket] Error handling message: ${errorMsg}`), ws.send(JSON.stringify({
919
+ const errorMsg = getErrorMessage(error), durationMs = Date.now() - messageStartTime;
920
+ await this.recordWebSocketEvent("ws.message.error", {
921
+ "ws.message.type": messageType,
922
+ "ws.message.duration_ms": durationMs,
923
+ "ws.message.status": "error",
924
+ "error.message": errorMsg
925
+ }), console.warn(`[WebSocket] Error handling ${messageType} message (${durationMs}ms): ${errorMsg}`), ws.send(JSON.stringify({
247
926
  type: "error",
248
927
  message: errorMsg
249
928
  }));
250
929
  }
251
930
  },
252
- onError: (error) => {
253
- const message = error instanceof Error ? error.message : String(error);
254
- console.error(`[WebSocket] Error: ${message}`);
931
+ onError: async (error) => {
932
+ const message = getErrorMessage(error);
933
+ await this.recordWebSocketEvent("ws.error", {
934
+ "error.type": error instanceof Error ? error.constructor.name : "unknown",
935
+ "error.message": message
936
+ }), console.error("[WebSocket] Error:", error);
255
937
  }
256
938
  })));
257
939
  }
258
940
  /**
259
- * Serialize model to JSON for client consumption
941
+ * Get OpenAPI specification document
942
+ * Provides a stable public API for OpenAPI spec generation.
943
+ *
944
+ * BEHAVIOR: Returns an OpenAPI spec with the version specified in `config.openapi`.
945
+ * The returned document will have whatever OpenAPI version was passed in the config.
946
+ *
947
+ * NOTE: Hono's getOpenAPI31Document() passes the config directly through and
948
+ * uses the specified version in the output. There is no forced version conversion.
949
+ */
950
+ getOpenAPIDocument(config) {
951
+ return this.app.getOpenAPI31Document(config);
952
+ }
953
+ /**
954
+ * Serialize model to graph format for client consumption.
955
+ *
956
+ * Returns { nodes, links } where:
957
+ * - nodes: flat array of all elements across all layers
958
+ * - links: intra-layer relationships (carry layer_id) and cross-layer
959
+ * references (carry source_layer_id + target_layer_id)
960
+ *
961
+ * Links are deduplicated: if the same relationship appears on multiple
962
+ * elements (e.g. stored on both source and target), only one entry is kept.
260
963
  */
261
964
  async serializeModel() {
262
- const layers = {};
263
- for (const [name, layer] of this.model.layers) {
264
- const elements = layer.listElements().map((e) => {
265
- const annotationIds = this.annotationsByElement.get(e.id) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
266
- return {
267
- ...e.toJSON(),
268
- annotations
965
+ const nodes = [], linksMap = /* @__PURE__ */ new Map(), elementLayerMap = /* @__PURE__ */ new Map();
966
+ for (const [layerName, layer] of this.model.layers)
967
+ for (const e of layer.listElements()) {
968
+ const layerId = e.layer_id || layerName;
969
+ e.id && elementLayerMap.set(e.id, layerId), e.path && e.path !== e.id && elementLayerMap.set(e.path, layerId);
970
+ }
971
+ for (const [layerName, layer] of this.model.layers)
972
+ for (const e of layer.listElements()) {
973
+ const layerId = e.layer_id || layerName, viewerSpecNodeId = this.resolveViewerSpecNodeId(e.id, layerId, e.type, e.spec_node_id), node = {
974
+ id: e.id,
975
+ spec_node_id: viewerSpecNodeId,
976
+ type: e.type,
977
+ layer_id: layerId,
978
+ name: e.name
269
979
  };
270
- });
271
- layers[name] = {
272
- name,
273
- elements,
274
- elementCount: elements.length
275
- };
980
+ e.description && (node.description = e.description), e.attributes && Object.keys(e.attributes).length > 0 && (node.attributes = e.attributes), e.source_reference && (node.source_reference = e.source_reference), e.metadata && (node.metadata = e.metadata), nodes.push(node);
981
+ for (const rel of e.relationships) {
982
+ const linkId = `rel:${rel.source}:${rel.target}:${rel.predicate}`;
983
+ linksMap.has(linkId) || linksMap.set(linkId, {
984
+ id: linkId,
985
+ source: rel.source,
986
+ target: rel.target,
987
+ type: rel.predicate,
988
+ layer_id: layerId
989
+ });
990
+ }
991
+ for (const ref of e.references) {
992
+ const linkId = `ref:${ref.source}:${ref.target}:${ref.type}`;
993
+ linksMap.has(linkId) || linksMap.set(linkId, {
994
+ id: linkId,
995
+ source: ref.source,
996
+ target: ref.target,
997
+ type: ref.type,
998
+ source_layer_id: elementLayerMap.get(ref.source) || layerId,
999
+ target_layer_id: elementLayerMap.get(ref.target) || "unknown"
1000
+ });
1001
+ }
1002
+ }
1003
+ for (const rel of this.model.relationships.getAll()) {
1004
+ const linkId = `rel:${rel.source}:${rel.target}:${rel.predicate}`;
1005
+ if (!linksMap.has(linkId)) {
1006
+ const layerId = rel.layer || elementLayerMap.get(rel.source);
1007
+ if (!layerId)
1008
+ continue;
1009
+ const link = {
1010
+ id: linkId,
1011
+ source: rel.source,
1012
+ target: rel.target,
1013
+ type: rel.predicate
1014
+ };
1015
+ rel.targetLayer ? (link.source_layer_id = layerId, link.target_layer_id = rel.targetLayer) : link.layer_id = layerId, linksMap.set(linkId, link);
1016
+ }
276
1017
  }
277
1018
  return {
278
- manifest: this.model.manifest.toJSON(),
279
- layers,
280
- totalElements: Array.from(this.model.layers.values()).reduce((sum, layer) => sum + layer.listElements().length, 0)
1019
+ nodes,
1020
+ links: Array.from(linksMap.values())
281
1021
  };
282
1022
  }
1023
+ /**
1024
+ * Resolve viewer spec node ID with fallback logic
1025
+ * Handles multiple resolution strategies:
1026
+ * 1. Valid pass-through: Return specNodeId if it's already a valid spec node ID
1027
+ * 2. Dot-format extraction: Extract type from elementId in "layer.type.name" format and construct candidate
1028
+ * 3. Hyphen-format extraction: Extract type from elementId in "layer-type-name" format and construct candidate
1029
+ * 4. Type-parameter fallback: If extracted type differs from type parameter, retry with type parameter
1030
+ * 5. Layer-specific fallback: Use LAYER_FALLBACK map for layers with limited viewer support
1031
+ *
1032
+ * @param elementId - The element ID (can be in dot-format or hyphen-format)
1033
+ * @param layer - The layer name (canonical form)
1034
+ * @param type - The element type field
1035
+ * @param specNodeId - The spec node ID if already known
1036
+ * @returns The resolved spec node ID string
1037
+ */
1038
+ resolveViewerSpecNodeId(elementId, layer, type, specNodeId) {
1039
+ if (specNodeId && VisualizationServer.ALL_VALID_IDS.has(specNodeId))
1040
+ return specNodeId;
1041
+ let extractedType = null;
1042
+ if (elementId) {
1043
+ if (elementId.includes(".")) {
1044
+ const parts = elementId.split(".");
1045
+ parts.length >= 2 && (extractedType = parts[1]);
1046
+ }
1047
+ if (!extractedType && elementId.includes("-"))
1048
+ if (layer === "data-model" || layer === "data-store") {
1049
+ const expectedPrefix = layer + "-";
1050
+ if (elementId.startsWith(expectedPrefix)) {
1051
+ const parts = elementId.substring(expectedPrefix.length).split("-");
1052
+ parts.length >= 1 && (extractedType = parts[0]);
1053
+ }
1054
+ } else {
1055
+ const parts = elementId.split("-");
1056
+ parts.length >= 2 && (extractedType = parts[1]);
1057
+ }
1058
+ }
1059
+ if (extractedType) {
1060
+ const candidate = `${layer}.${extractedType}`;
1061
+ if ((VisualizationServer.VALID_SPEC_NODE_IDS[layer] || []).includes(candidate))
1062
+ return candidate;
1063
+ }
1064
+ if (type && (!extractedType || extractedType !== type)) {
1065
+ const candidate = `${layer}.${type}`;
1066
+ if ((VisualizationServer.VALID_SPEC_NODE_IDS[layer] || []).includes(candidate))
1067
+ return candidate;
1068
+ }
1069
+ return layer in VisualizationServer.LAYER_FALLBACK ? VisualizationServer.LAYER_FALLBACK[layer] : "data-store.database";
1070
+ }
283
1071
  /**
284
1072
  * Generate unique annotation ID
1073
+ * Uses cryptographically secure random bytes for collision-resistant IDs
285
1074
  */
286
1075
  generateAnnotationId() {
287
- return `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1076
+ return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
1077
+ }
1078
+ /**
1079
+ * Generate unique reply ID
1080
+ * Uses cryptographically secure random bytes for collision-resistant IDs
1081
+ */
1082
+ generateReplyId() {
1083
+ return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
288
1084
  }
289
1085
  /**
290
1086
  * Broadcast message to all connected WebSocket clients
291
1087
  */
292
1088
  async broadcastMessage(message) {
293
- const messageStr = JSON.stringify(message);
1089
+ const messageStr = JSON.stringify(message), messageType = message.type || "unknown";
1090
+ await this.recordWebSocketEvent("ws.broadcast.start", {
1091
+ "ws.broadcast.type": messageType,
1092
+ "ws.broadcast.client_count": this.clients.size,
1093
+ "ws.broadcast.message_size_bytes": messageStr.length
1094
+ });
294
1095
  let failureCount = 0;
1096
+ const failedClients = [];
295
1097
  for (const client of this.clients)
296
1098
  try {
297
1099
  client.send(messageStr);
298
1100
  } catch (error) {
299
- failureCount++;
300
- const msg = error instanceof Error ? error.message : String(error);
301
- process.env.VERBOSE && console.warn(`[WebSocket] Failed to send message to client: ${msg}`);
1101
+ failureCount++, failedClients.push(client);
1102
+ const msg = getErrorMessage(error);
1103
+ console.warn(`[WebSocket] Failed to send message to client: ${msg}`);
302
1104
  }
303
- failureCount > 0 && process.env.VERBOSE && console.warn(`[WebSocket] Failed to send message to ${failureCount}/${this.clients.size} clients`);
1105
+ for (const client of failedClients)
1106
+ this.clients.delete(client);
1107
+ if (await this.recordWebSocketEvent("ws.broadcast.complete", {
1108
+ "ws.broadcast.type": messageType,
1109
+ "ws.broadcast.success_count": this.clients.size - failureCount,
1110
+ "ws.broadcast.failure_count": failureCount
1111
+ }), failureCount > 0) {
1112
+ const failureRate = this.clients.size > 0 ? failureCount / this.clients.size * 100 : 0;
1113
+ (failureRate >= 10 || failureCount > 5) && console.warn(`[WebSocket] Broadcast completed with ${failureCount}/${this.clients.size} client failures (${failureRate.toFixed(1)}% failure rate)`);
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Record WebSocket telemetry event
1118
+ * Creates spans for WebSocket operations when telemetry is enabled
1119
+ */
1120
+ async recordWebSocketEvent(eventName, attributes) {
1121
+ return;
1122
+ try {
1123
+ const { startSpan, endSpan } = await import("../telemetry/index.js"), span = startSpan(eventName, attributes);
1124
+ endSpan(span);
1125
+ } catch (error) {
1126
+ console.warn(`[Telemetry] Failed to record WebSocket event: ${getErrorMessage(error)}`);
1127
+ }
304
1128
  }
305
1129
  /**
306
1130
  * Find an element across all layers
@@ -317,9 +1141,10 @@ class VisualizationServer {
317
1141
  * Handle WebSocket messages
318
1142
  */
319
1143
  async handleWSMessage(ws, data) {
320
- switch (data.type) {
1144
+ const simpleMsg = data;
1145
+ switch (simpleMsg.type) {
321
1146
  case "subscribe":
322
- const topics = data.topics || ["model", "annotations"];
1147
+ const topics = simpleMsg.topics || ["model", "annotations"];
323
1148
  ws.send(JSON.stringify({
324
1149
  type: "subscribed",
325
1150
  topics,
@@ -339,15 +1164,15 @@ class VisualizationServer {
339
1164
  }));
340
1165
  break;
341
1166
  case "annotate":
342
- data.annotation && await this.broadcastMessage({
1167
+ simpleMsg.annotation && await this.broadcastMessage({
343
1168
  type: "annotation",
344
- data: data.annotation
1169
+ data: simpleMsg.annotation
345
1170
  });
346
1171
  break;
347
1172
  default:
348
1173
  ws.send(JSON.stringify({
349
1174
  type: "error",
350
- message: `Unknown message type: ${data.type}`
1175
+ message: `Unknown message type: ${simpleMsg.type}`
351
1176
  }));
352
1177
  }
353
1178
  }
@@ -355,7 +1180,12 @@ class VisualizationServer {
355
1180
  * Handle JSON-RPC 2.0 messages for chat functionality
356
1181
  */
357
1182
  async handleJSONRPCMessage(ws, data) {
358
- const { method, params, id } = data, sendResponse = (result) => {
1183
+ if (!("method" in data)) {
1184
+ const responseType = "result" in data ? "success" : "error" in data ? "error" : "unknown", msgId = "id" in data ? data.id : "unknown";
1185
+ console.warn(`[WebSocket] Received JSON-RPC ${responseType} response from client (ID: ${msgId}), ignoring`);
1186
+ return;
1187
+ }
1188
+ const rpcMsg = data, { method, params, id } = rpcMsg, sendResponse = (result) => {
359
1189
  ws.send(JSON.stringify({
360
1190
  jsonrpc: "2.0",
361
1191
  result,
@@ -371,24 +1201,25 @@ class VisualizationServer {
371
1201
  try {
372
1202
  switch (method) {
373
1203
  case "chat.status":
374
- const claudeAvailable = await this.checkClaudeCodeCLI();
375
- sendResponse({
376
- sdk_available: claudeAvailable,
377
- sdk_version: claudeAvailable ? "claude-code-cli" : null,
378
- error_message: claudeAvailable ? null : "Claude Code CLI not found in PATH"
1204
+ const hasClient = this.selectedChatClient !== void 0;
1205
+ let statusError = null;
1206
+ this.chatInitializationError ? statusError = `Chat initialization failed: ${this.chatInitializationError.message}` : hasClient || (statusError = "No chat client available. Install Claude Code or GitHub Copilot."), sendResponse({
1207
+ sdk_available: hasClient,
1208
+ sdk_version: hasClient ? this.selectedChatClient.getClientName() : null,
1209
+ error_message: statusError
379
1210
  });
380
1211
  break;
381
1212
  case "chat.send":
382
- if (!params || !params.message) {
383
- sendError(-32602, "Message cannot be empty");
1213
+ if (!params || typeof params != "object" || !("message" in params) || typeof params.message != "string" || params.message.trim() === "") {
1214
+ sendError(JSONRPC_ERRORS.INVALID_PARAMS, "Message cannot be empty");
384
1215
  return;
385
1216
  }
386
- if (!await this.checkClaudeCodeCLI()) {
387
- sendError(-32001, "Claude Code CLI not available. Install Claude Code to enable chat.");
1217
+ if (!this.selectedChatClient) {
1218
+ sendError(JSONRPC_ERRORS.NO_CLIENT_AVAILABLE, "No chat client available. Install Claude Code or GitHub Copilot to enable chat.");
388
1219
  return;
389
1220
  }
390
1221
  const conversationId = `conv-${++this.chatConversationCounter}-${Date.now()}`;
391
- await this.launchClaudeCodeChat(ws, conversationId, params.message, id);
1222
+ await this.launchChat(ws, conversationId, params.message, id);
392
1223
  break;
393
1224
  case "chat.cancel":
394
1225
  let cancelled = !1, cancelledConvId = null;
@@ -396,7 +1227,8 @@ class VisualizationServer {
396
1227
  try {
397
1228
  process2.kill(), this.activeChatProcesses.delete(convId), cancelled = !0, cancelledConvId = convId;
398
1229
  break;
399
- } catch {
1230
+ } catch (error) {
1231
+ console.warn(`[Chat] Failed to cancel process for conversation ${convId}: ${getErrorMessage(error)}`);
400
1232
  }
401
1233
  sendResponse({
402
1234
  cancelled,
@@ -404,43 +1236,46 @@ class VisualizationServer {
404
1236
  });
405
1237
  break;
406
1238
  default:
407
- sendError(-32601, `Method not found: ${method}`);
1239
+ sendError(JSONRPC_ERRORS.METHOD_NOT_FOUND, `Method not found: ${method}`);
408
1240
  }
409
1241
  } catch (error) {
410
- const errorMsg = error instanceof Error ? error.message : String(error);
411
- sendError(-32603, "Internal error", errorMsg);
1242
+ const errorMsg = getErrorMessage(error);
1243
+ sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Internal error", errorMsg);
412
1244
  }
413
1245
  }
414
1246
  /**
415
- * Check if Claude Code CLI is available
1247
+ * Launch chat with selected client and stream responses via WebSocket
416
1248
  */
417
- async checkClaudeCodeCLI() {
418
- try {
419
- return Bun.spawnSync({
420
- cmd: ["which", "claude"],
421
- stdout: "pipe",
422
- stderr: "pipe"
423
- }).exitCode === 0;
424
- } catch {
425
- return !1;
1249
+ async launchChat(ws, conversationId, message, requestId) {
1250
+ if (!this.selectedChatClient) {
1251
+ ws.send(JSON.stringify({
1252
+ jsonrpc: "2.0",
1253
+ error: {
1254
+ code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
1255
+ message: "No chat client available"
1256
+ },
1257
+ id: requestId
1258
+ }));
1259
+ return;
426
1260
  }
1261
+ this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId) : ws.send(JSON.stringify({
1262
+ jsonrpc: "2.0",
1263
+ error: {
1264
+ code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
1265
+ message: "Unknown chat client type"
1266
+ },
1267
+ id: requestId
1268
+ }));
427
1269
  }
428
1270
  /**
429
1271
  * Launch Claude Code CLI with dr-architect agent and stream responses
430
1272
  */
431
1273
  async launchClaudeCodeChat(ws, conversationId, message, requestId) {
432
1274
  try {
1275
+ const cmd = ["claude", "--agent", "dr-architect", "--print"];
1276
+ this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json");
433
1277
  const proc = Bun.spawn({
434
- cmd: [
435
- "claude",
436
- "--agent",
437
- "dr-architect",
438
- "--print",
439
- "--dangerously-skip-permissions",
440
- "--verbose",
441
- "--output-format",
442
- "stream-json"
443
- ],
1278
+ cmd,
444
1279
  cwd: this.model.rootPath,
445
1280
  stdin: "pipe",
446
1281
  stdout: "pipe",
@@ -495,8 +1330,8 @@ class VisualizationServer {
495
1330
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
496
1331
  }
497
1332
  }));
498
- } catch {
499
- ws.send(JSON.stringify({
1333
+ } catch (parseError) {
1334
+ process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), ws.send(JSON.stringify({
500
1335
  jsonrpc: "2.0",
501
1336
  method: "chat.response.chunk",
502
1337
  params: {
@@ -532,96 +1367,197 @@ class VisualizationServer {
532
1367
  id: requestId
533
1368
  }));
534
1369
  } catch (error) {
535
- const errorMsg = error instanceof Error ? error.message : String(error);
1370
+ const errorMsg = getErrorMessage(error);
536
1371
  ws.send(JSON.stringify({
537
1372
  jsonrpc: "2.0",
538
1373
  error: {
539
- code: -32603,
1374
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
540
1375
  message: `Chat failed: ${errorMsg}`
541
1376
  },
542
1377
  id: requestId
543
1378
  })), this.activeChatProcesses.delete(conversationId);
544
1379
  try {
545
- proc.kill();
546
- } catch {
1380
+ await this.killProcessWithRetry(proc, conversationId, "Claude Code");
1381
+ } catch (error2) {
1382
+ console.error(`[Claude Code] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
547
1383
  }
548
1384
  }
549
- })();
1385
+ })().catch((err) => {
1386
+ const errorMsg = getErrorMessage(err);
1387
+ console.error(`[Claude Code] Stream error: ${errorMsg}`);
1388
+ try {
1389
+ ws.send(JSON.stringify({
1390
+ jsonrpc: "2.0",
1391
+ error: {
1392
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1393
+ message: `Stream error: ${errorMsg}`
1394
+ },
1395
+ id: requestId
1396
+ }));
1397
+ } catch (sendError) {
1398
+ console.error(`[Claude Code] Failed to send stream error notification: ${getErrorMessage(sendError)}`);
1399
+ }
1400
+ });
550
1401
  } catch (error) {
551
- const errorMsg = error instanceof Error ? error.message : String(error);
1402
+ const errorMsg = getErrorMessage(error);
552
1403
  ws.send(JSON.stringify({
553
1404
  jsonrpc: "2.0",
554
1405
  error: {
555
- code: -32603,
1406
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
556
1407
  message: `Failed to launch Claude Code: ${errorMsg}`
557
1408
  },
558
1409
  id: requestId
559
1410
  }));
560
1411
  }
561
1412
  }
1413
+ /**
1414
+ * Launch GitHub Copilot CLI and stream responses via WebSocket
1415
+ */
1416
+ async launchCopilotChat(ws, conversationId, message, requestId) {
1417
+ try {
1418
+ let cmd;
1419
+ Bun.spawnSync({
1420
+ cmd: ["gh", "copilot", "--version"],
1421
+ stdout: "pipe",
1422
+ stderr: "pipe"
1423
+ }).exitCode === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
1424
+ const proc = Bun.spawn({
1425
+ cmd,
1426
+ cwd: this.model.rootPath,
1427
+ stdin: "pipe",
1428
+ stdout: "pipe",
1429
+ stderr: "pipe"
1430
+ });
1431
+ this.activeChatProcesses.set(conversationId, proc);
1432
+ const stdoutReader = proc.stdout.getReader(), decoder = new TextDecoder();
1433
+ (async () => {
1434
+ let accumulatedText = "";
1435
+ try {
1436
+ for (; ; ) {
1437
+ const { done, value } = await stdoutReader.read();
1438
+ if (done)
1439
+ break;
1440
+ const chunk = decoder.decode(value, { stream: !0 });
1441
+ accumulatedText += chunk, ws.send(JSON.stringify({
1442
+ jsonrpc: "2.0",
1443
+ method: "chat.response.chunk",
1444
+ params: {
1445
+ conversation_id: conversationId,
1446
+ content: chunk,
1447
+ is_final: !1,
1448
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1449
+ }
1450
+ }));
1451
+ }
1452
+ const exitCode = await proc.exited;
1453
+ this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
1454
+ jsonrpc: "2.0",
1455
+ result: {
1456
+ conversation_id: conversationId,
1457
+ status: exitCode === 0 ? "complete" : "error",
1458
+ exit_code: exitCode,
1459
+ full_response: accumulatedText,
1460
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1461
+ },
1462
+ id: requestId
1463
+ }));
1464
+ } catch (error) {
1465
+ const errorMsg = getErrorMessage(error);
1466
+ ws.send(JSON.stringify({
1467
+ jsonrpc: "2.0",
1468
+ error: {
1469
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1470
+ message: `Chat failed: ${errorMsg}`
1471
+ },
1472
+ id: requestId
1473
+ })), this.activeChatProcesses.delete(conversationId);
1474
+ try {
1475
+ await this.killProcessWithRetry(proc, conversationId, "Copilot");
1476
+ } catch (error2) {
1477
+ console.error(`[Copilot] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
1478
+ }
1479
+ }
1480
+ })().catch((err) => {
1481
+ console.error(`[Copilot] Stream error: ${getErrorMessage(err)}`);
1482
+ });
1483
+ } catch (error) {
1484
+ const errorMsg = getErrorMessage(error);
1485
+ ws.send(JSON.stringify({
1486
+ jsonrpc: "2.0",
1487
+ error: {
1488
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1489
+ message: `Failed to launch GitHub Copilot: ${errorMsg}`
1490
+ },
1491
+ id: requestId
1492
+ }));
1493
+ }
1494
+ }
562
1495
  /**
563
1496
  * Setup file watcher for model changes
564
1497
  */
565
1498
  setupFileWatcher() {
566
- const drPath = `${this.model.rootPath}/.dr`, bunWatch = globalThis.Bun?.watch;
1499
+ const modelPath = `${this.model.rootPath}/documentation-robotics/model`, bunWatch = globalThis.Bun?.watch;
567
1500
  if (!bunWatch) {
568
1501
  console.warn("[Watcher] Bun.watch not available, file watching disabled");
569
1502
  return;
570
1503
  }
571
- this.watcher = bunWatch(drPath, {
572
- recursive: !0,
573
- onChange: async (_event, path) => {
574
- process.env.VERBOSE && console.log(`[Watcher] File changed: ${path}`);
575
- try {
576
- this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 });
577
- const modelData = await this.serializeModel(), message = JSON.stringify({
578
- type: "model.updated",
579
- data: modelData,
580
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
581
- });
582
- for (const client of this.clients)
583
- try {
584
- client.send(message);
585
- } catch (error) {
586
- if (process.env.DEBUG) {
587
- const msg = error instanceof Error ? error.message : String(error);
588
- console.debug(`[Watcher] Failed to send update: ${msg}`);
1504
+ try {
1505
+ this.watcher = bunWatch(modelPath, {
1506
+ recursive: !0,
1507
+ onChange: async (_event, path2) => {
1508
+ process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
1509
+ try {
1510
+ this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
1511
+ const modelData = await this.serializeModel(), message = JSON.stringify({
1512
+ type: "model.updated",
1513
+ data: modelData,
1514
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1515
+ });
1516
+ for (const client of this.clients)
1517
+ try {
1518
+ client.send(message);
1519
+ } catch (error) {
1520
+ const msg = getErrorMessage(error);
1521
+ console.warn(`[Watcher] Failed to send update: ${msg}`);
589
1522
  }
590
- }
591
- } catch (error) {
592
- const message = error instanceof Error ? error.message : String(error);
593
- console.error(`[Watcher] Failed to reload model: ${message}`);
1523
+ } catch (error) {
1524
+ const message = getErrorMessage(error);
1525
+ console.error(`[Watcher] Failed to reload model: ${message}`);
1526
+ }
594
1527
  }
595
- }
596
- });
1528
+ });
1529
+ } catch (err) {
1530
+ console.warn(`[Watcher] Could not watch ${modelPath}: ${err}`);
1531
+ }
597
1532
  }
598
1533
  /**
599
- * Load all JSON schemas from bundled directory
1534
+ * Load all JSON schemas from bundled directory (recursive)
1535
+ * Loads schemas from all subdirectories: base/, nodes/, relationships/, etc.
600
1536
  */
601
1537
  async loadSchemas() {
602
1538
  const schemasPath = new URL("../schemas/bundled/", import.meta.url).pathname, schemas = {};
603
1539
  try {
604
- const fs = await import("fs/promises"), path = await import("path"), fileList = await fs.readdir(schemasPath);
605
- for (const file of fileList)
606
- if (file.endsWith(".schema.json") || file.endsWith(".json")) {
607
- const filePath = path.join(schemasPath, file), content = await fs.readFile(filePath, "utf-8");
608
- schemas[file] = JSON.parse(content);
1540
+ const fs = await import("fs/promises"), path2 = await import("path"), walkDirectory = async (dir, prefix = "") => {
1541
+ const entries = await fs.readdir(dir, { withFileTypes: !0 });
1542
+ for (const entry of entries) {
1543
+ const fullPath = path2.join(dir, entry.name), schemaKey = prefix ? `${prefix}/${entry.name}` : entry.name;
1544
+ if (entry.isDirectory())
1545
+ await walkDirectory(fullPath, schemaKey);
1546
+ else if (entry.isFile() && (entry.name.endsWith(".schema.json") || entry.name.endsWith(".json"))) {
1547
+ const content = await fs.readFile(fullPath, "utf-8");
1548
+ try {
1549
+ schemas[schemaKey] = JSON.parse(content);
1550
+ } catch (parseError) {
1551
+ const parseErrorMsg = parseError instanceof Error ? parseError.message : String(parseError);
1552
+ throw new Error(`Invalid JSON in schema file ${fullPath}: ${parseErrorMsg}`);
1553
+ }
1554
+ }
609
1555
  }
610
- return schemas;
611
- } catch (error) {
612
- throw console.error("Failed to load schemas:", error), new Error("Failed to load schema files");
613
- }
614
- }
615
- /**
616
- * Load link registry from bundled directory
617
- */
618
- async loadLinkRegistry() {
619
- const linkRegistryPath = new URL("../schemas/bundled/link-registry.json", import.meta.url).pathname;
620
- try {
621
- const content = await (await import("fs/promises")).readFile(linkRegistryPath, "utf-8");
622
- return JSON.parse(content);
1556
+ };
1557
+ return await walkDirectory(schemasPath), schemas;
623
1558
  } catch (error) {
624
- throw console.error("Failed to load link registry:", error), new Error("Link registry not found");
1559
+ const errorMsg = getErrorMessage(error);
1560
+ throw console.error("Failed to load schemas:", error), new Error(`Failed to load schema files: ${errorMsg}`);
625
1561
  }
626
1562
  }
627
1563
  /**
@@ -967,7 +1903,27 @@ class VisualizationServer {
967
1903
  return div.innerHTML;
968
1904
  }
969
1905
 
970
- const ws = new WebSocket(\`ws://\${window.location.host}/ws\`);
1906
+ // Extract token from URL query parameters if present
1907
+ const urlParams = new URLSearchParams(window.location.search);
1908
+ const token = urlParams.get('token');
1909
+
1910
+ // Log token status for debugging
1911
+ if (!token) {
1912
+ console.warn('[WebSocket] No authentication token found in URL. If authentication is enabled, connection will fail.');
1913
+ console.warn('[WebSocket] Expected URL format: http://localhost:PORT?token=YOUR_TOKEN');
1914
+ } else {
1915
+ console.log('[WebSocket] Found authentication token in URL');
1916
+ }
1917
+
1918
+ // Build WebSocket URL with token if available
1919
+ let wsUrl = \`ws://\${window.location.host}/ws\`;
1920
+ if (token) {
1921
+ wsUrl += \`?token=\${encodeURIComponent(token)}\`;
1922
+ }
1923
+
1924
+ console.log('[WebSocket] Connecting to:', wsUrl.replace(/token=[^&]+/, 'token=***'));
1925
+
1926
+ const ws = new WebSocket(wsUrl);
971
1927
 
972
1928
  ws.addEventListener('open', () => {
973
1929
  console.log('WebSocket connected');
@@ -993,16 +1949,45 @@ class VisualizationServer {
993
1949
  }
994
1950
  });
995
1951
 
996
- ws.addEventListener('close', () => {
997
- console.log('WebSocket disconnected');
1952
+ ws.addEventListener('close', (event) => {
1953
+ console.log('WebSocket disconnected', event.code, event.reason);
998
1954
  updateStatus(false);
1955
+
1956
+ // If closed immediately with auth error codes, show helpful message
1957
+ if (!token && (event.code === 1002 || event.code === 1008 || event.code === 1006)) {
1958
+ showAuthError();
1959
+ }
999
1960
  });
1000
1961
 
1001
1962
  ws.addEventListener('error', (error) => {
1002
1963
  console.error('WebSocket error:', error);
1003
1964
  updateStatus(false);
1965
+
1966
+ // If we don't have a token, likely an auth issue
1967
+ if (!token) {
1968
+ showAuthError();
1969
+ }
1004
1970
  });
1005
1971
 
1972
+ function showAuthError() {
1973
+ const errorDiv = document.createElement('div');
1974
+ errorDiv.style.cssText = 'position: fixed; top: 80px; left: 50%; transform: translateX(-50%); background: #f44336; color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; max-width: 500px; text-align: center;';
1975
+ errorDiv.innerHTML = \`
1976
+ <strong>\u26A0\uFE0F Authentication Required</strong><br>
1977
+ <span style="font-size: 14px; margin-top: 8px; display: inline-block;">
1978
+ Please use the full URL with authentication token from the terminal.
1979
+ </span>
1980
+ \`;
1981
+ document.body.appendChild(errorDiv);
1982
+
1983
+ // Auto-dismiss after 10 seconds
1984
+ setTimeout(() => {
1985
+ errorDiv.style.transition = 'opacity 0.5s';
1986
+ errorDiv.style.opacity = '0';
1987
+ setTimeout(() => errorDiv.remove(), 500);
1988
+ }, 10000);
1989
+ }
1990
+
1006
1991
  function updateStatus(connected) {
1007
1992
  if (connected) {
1008
1993
  statusEl.className = 'status-badge connected';
@@ -1044,12 +2029,13 @@ class VisualizationServer {
1044
2029
  for (const element of layerData.elements) {
1045
2030
  const elemDiv = document.createElement('div');
1046
2031
  elemDiv.className = 'element-item';
1047
- if (element.id === selectedElementId) {
2032
+ const elementSlug = element.path || element.id;
2033
+ if (elementSlug === selectedElementId) {
1048
2034
  elemDiv.classList.add('selected');
1049
2035
  }
1050
- elemDiv.innerHTML = \`\${escapeHtml(element.id)}<br><span style="font-size: 11px; color: #999;">\${escapeHtml(element.name)}</span>\`;
2036
+ elemDiv.innerHTML = \`\${escapeHtml(elementSlug)}<br><span style="font-size: 11px; color: #999;">\${escapeHtml(element.name)}</span>\`;
1051
2037
  elemDiv.addEventListener('click', () => {
1052
- selectedElementId = element.id;
2038
+ selectedElementId = elementSlug;
1053
2039
  document.querySelectorAll('.element-item').forEach(el => el.classList.remove('selected'));
1054
2040
  elemDiv.classList.add('selected');
1055
2041
  renderSelectedElement();
@@ -1073,7 +2059,7 @@ class VisualizationServer {
1073
2059
  // Find element in model
1074
2060
  let element = null;
1075
2061
  for (const layer of Object.values(currentModel.layers)) {
1076
- element = layer.elements.find(e => e.id === selectedElementId);
2062
+ element = layer.elements.find(e => (e.path || e.id) === selectedElementId);
1077
2063
  if (element) break;
1078
2064
  }
1079
2065
 
@@ -1086,7 +2072,7 @@ class VisualizationServer {
1086
2072
  <div class="detail-header">
1087
2073
  <div class="element-type">\${escapeHtml(element.type)}</div>
1088
2074
  <h2>\${escapeHtml(element.name)}</h2>
1089
- <div style="font-size: 12px; color: #666; margin-top: 4px;"><code>\${escapeHtml(element.id)}</code></div>
2075
+ <div style="font-size: 12px; color: #666; margin-top: 4px;"><code>\${escapeHtml(element.path || element.id)}</code></div>
1090
2076
  </div>
1091
2077
  \`;
1092
2078
 
@@ -1128,11 +2114,12 @@ class VisualizationServer {
1128
2114
  html += \`
1129
2115
  <div class="detail-section">
1130
2116
  <h3>Add Annotation</h3>
1131
- <form class="annotation-form" onsubmit="addAnnotation(event, '\${escapeHtml(element.id)}')">
2117
+ <form class="annotation-form" onsubmit="addAnnotation(event, '\${escapeHtml(element.path || element.id)}')">
1132
2118
  <input type="text" placeholder="Author name" id="ann-author" required>
1133
2119
  <textarea placeholder="Annotation text" id="ann-text" required></textarea>
1134
2120
  <button type="submit">Add Annotation</button>
1135
2121
  </form>
2122
+ <div id="ann-error" style="margin-top: 8px; min-height: 1.2em;"></div>
1136
2123
  </div>
1137
2124
  \`;
1138
2125
 
@@ -1147,30 +2134,101 @@ class VisualizationServer {
1147
2134
 
1148
2135
  if (!author || !text) return;
1149
2136
 
1150
- // Send to server
1151
- fetch(\`/api/elements/\${elementId}/annotations\`, {
2137
+ // Send to server using new annotation API
2138
+ fetch('/api/annotations', {
1152
2139
  method: 'POST',
1153
2140
  headers: { 'Content-Type': 'application/json' },
1154
- body: JSON.stringify({ author, text })
2141
+ body: JSON.stringify({ elementId, author, content: text })
1155
2142
  })
1156
- .then(r => r.json())
1157
- .then(data => {
1158
- if (data.success) {
2143
+ .then(r => r.json().then(data => ({ ok: r.ok, data })))
2144
+ .then(({ ok, data }) => {
2145
+ if (ok && data && data.id) {
1159
2146
  document.getElementById('ann-author').value = '';
1160
2147
  document.getElementById('ann-text').value = '';
2148
+ const errorDiv = document.getElementById('ann-error');
2149
+ if (errorDiv) errorDiv.textContent = '';
2150
+ } else if (!ok) {
2151
+ const errorMsg = (data && data.error) ? data.error : 'Failed to create annotation';
2152
+ const errorDiv = document.getElementById('ann-error');
2153
+ if (errorDiv) {
2154
+ errorDiv.textContent = 'Error: ' + errorMsg;
2155
+ errorDiv.style.color = '#d32f2f';
2156
+ errorDiv.style.marginTop = '8px';
2157
+ }
1161
2158
  }
1162
2159
  })
1163
- .catch(err => console.error('Failed to add annotation:', err));
2160
+ .catch(err => {
2161
+ const errorDiv = document.getElementById('ann-error');
2162
+ if (errorDiv) {
2163
+ errorDiv.textContent = 'Error: Failed to add annotation: ' + err.message;
2164
+ errorDiv.style.color = '#d32f2f';
2165
+ errorDiv.style.marginTop = '8px';
2166
+ }
2167
+ });
1164
2168
  }
1165
2169
  </script>
1166
2170
  </body>
1167
2171
  </html>`;
1168
2172
  }
2173
+ /**
2174
+ * Serve a file from the custom viewer path
2175
+ */
2176
+ async serveCustomViewer(filePath) {
2177
+ if (process.env.VERBOSE && console.log(`[VIEWER] serveCustomViewer called for: ${filePath}`), !this.viewerPath)
2178
+ return console.error("[VIEWER] Custom viewer path not configured"), new Response("Custom viewer path not configured", { status: 500 });
2179
+ try {
2180
+ const path2 = await import("path"), fs = await import("fs/promises"), fullPath = path2.resolve(this.viewerPath, filePath);
2181
+ if (process.env.VERBOSE && console.log(`[VIEWER] Resolved path: ${fullPath}`), !fullPath.startsWith(path2.resolve(this.viewerPath)))
2182
+ return console.error("[VIEWER] Security check failed - path outside viewer directory"), new Response("Forbidden", { status: 403 });
2183
+ process.env.VERBOSE && console.log(`[VIEWER] Reading file: ${fullPath}`);
2184
+ const content = await fs.readFile(fullPath), ext = path2.extname(fullPath).toLowerCase(), contentType = {
2185
+ ".html": "text/html",
2186
+ ".css": "text/css",
2187
+ ".js": "application/javascript",
2188
+ ".json": "application/json",
2189
+ ".png": "image/png",
2190
+ ".jpg": "image/jpeg",
2191
+ ".jpeg": "image/jpeg",
2192
+ ".gif": "image/gif",
2193
+ ".svg": "image/svg+xml",
2194
+ ".ico": "image/x-icon",
2195
+ ".woff": "font/woff",
2196
+ ".woff2": "font/woff2",
2197
+ ".ttf": "font/ttf",
2198
+ ".eot": "application/vnd.ms-fontobject"
2199
+ }[ext] || "application/octet-stream";
2200
+ return process.env.VERBOSE && console.log(`[VIEWER] Successfully read file, returning with Content-Type: ${contentType}`), new Response(content, {
2201
+ status: 200,
2202
+ headers: {
2203
+ "Content-Type": contentType
2204
+ }
2205
+ });
2206
+ } catch (error) {
2207
+ return error.code === "ENOENT" ? (console.error(`[VIEWER] File not found: ${filePath}`), new Response("File not found", { status: 404 })) : (console.error(`[VIEWER] Error serving custom viewer file ${filePath}:`, error), new Response("Internal server error", { status: 500 }));
2208
+ }
2209
+ }
2210
+ /**
2211
+ * Load all changesets from disk into the in-memory map
2212
+ */
2213
+ async loadChangesetsFromDisk() {
2214
+ if (this.changesetStorage)
2215
+ try {
2216
+ const changesets = await this.changesetStorage.list();
2217
+ this.changesets.clear();
2218
+ for (const cs of changesets)
2219
+ this.changesets.set(cs.id, cs);
2220
+ process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
2221
+ } catch (err) {
2222
+ console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
2223
+ }
2224
+ }
1169
2225
  /**
1170
2226
  * Start the server
1171
2227
  */
1172
2228
  async start(port = 8080) {
1173
- this.setupFileWatcher(), this._server = serve({
2229
+ this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher();
2230
+ const { serve } = await import("bun");
2231
+ this._server = serve({
1174
2232
  port,
1175
2233
  fetch: this.app.fetch,
1176
2234
  websocket
@@ -1189,6 +2247,30 @@ class VisualizationServer {
1189
2247
  isAuthEnabled() {
1190
2248
  return this.authEnabled;
1191
2249
  }
2250
+ /**
2251
+ * Kill a process with retry logic and exponential backoff
2252
+ * Attempts graceful SIGTERM first, then force SIGKILL if needed
2253
+ */
2254
+ async killProcessWithRetry(proc, conversationId, label) {
2255
+ let attempt = 0;
2256
+ const baseDelay = 100;
2257
+ for (; attempt < 3; ) {
2258
+ try {
2259
+ if (proc.killed || (proc.kill("SIGTERM"), await new Promise((resolve) => setTimeout(resolve, 500)), proc.killed))
2260
+ return;
2261
+ } catch {
2262
+ }
2263
+ if (attempt++, attempt < 3) {
2264
+ const delay = baseDelay * Math.pow(2, attempt - 1);
2265
+ await new Promise((resolve) => setTimeout(resolve, delay));
2266
+ }
2267
+ }
2268
+ try {
2269
+ proc.killed || (proc.kill("SIGKILL"), console.warn(`[${label}] Process for conversation ${conversationId} did not respond to SIGTERM, force killed with SIGKILL`));
2270
+ } catch (error) {
2271
+ throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
2272
+ }
2273
+ }
1192
2274
  /**
1193
2275
  * Stop the server
1194
2276
  */
@@ -1196,7 +2278,9 @@ class VisualizationServer {
1196
2278
  for (const client of this.clients)
1197
2279
  try {
1198
2280
  client.close();
1199
- } catch {
2281
+ } catch (error) {
2282
+ const errorMessage = error instanceof Error ? error.message : String(error);
2283
+ errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
1200
2284
  }
1201
2285
  this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.stop();
1202
2286
  }