@documentation-robotics/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +46 -16
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/active-analyzer.js +62 -0
  5. package/dist/analyzers/active-analyzer.js.map +7 -0
  6. package/dist/analyzers/call-graph-utils.js +2 -2
  7. package/dist/analyzers/call-graph-utils.js.map +2 -2
  8. package/dist/analyzers/cards/build.js +191 -0
  9. package/dist/analyzers/cards/build.js.map +7 -0
  10. package/dist/analyzers/cards/classify.js +42 -0
  11. package/dist/analyzers/cards/classify.js.map +7 -0
  12. package/dist/analyzers/cards/edge-index.js +44 -0
  13. package/dist/analyzers/cards/edge-index.js.map +7 -0
  14. package/dist/analyzers/cards/extract.js +138 -0
  15. package/dist/analyzers/cards/extract.js.map +7 -0
  16. package/dist/analyzers/cards/facts.js +46 -0
  17. package/dist/analyzers/cards/facts.js.map +7 -0
  18. package/dist/analyzers/cards/graph.js +42 -0
  19. package/dist/analyzers/cards/graph.js.map +7 -0
  20. package/dist/analyzers/cards/index.js +8 -0
  21. package/dist/analyzers/cards/index.js.map +7 -0
  22. package/dist/analyzers/cards/routes.js +96 -0
  23. package/dist/analyzers/cards/routes.js.map +7 -0
  24. package/dist/analyzers/cards/tier0.js +18 -0
  25. package/dist/analyzers/cards/tier0.js.map +7 -0
  26. package/dist/analyzers/cards/types.js +1 -0
  27. package/dist/analyzers/cards/types.js.map +7 -0
  28. package/dist/analyzers/cbm-analyzer.js +270 -174
  29. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  30. package/dist/analyzers/cbm-responses.js +342 -0
  31. package/dist/analyzers/cbm-responses.js.map +7 -0
  32. package/dist/analyzers/code-changes.js +159 -0
  33. package/dist/analyzers/code-changes.js.map +7 -0
  34. package/dist/analyzers/edge-relationships.js +49 -0
  35. package/dist/analyzers/edge-relationships.js.map +7 -0
  36. package/dist/analyzers/extraction-plan.js +541 -0
  37. package/dist/analyzers/extraction-plan.js.map +7 -0
  38. package/dist/analyzers/index-job.js +229 -0
  39. package/dist/analyzers/index-job.js.map +7 -0
  40. package/dist/analyzers/mapping-loader.js +9 -0
  41. package/dist/analyzers/mapping-loader.js.map +2 -2
  42. package/dist/analyzers/tier0-rules.js +153 -0
  43. package/dist/analyzers/tier0-rules.js.map +7 -0
  44. package/dist/analyzers/types.js.map +1 -1
  45. package/dist/audit/relationships/ai/prompts.js +10 -10
  46. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  47. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  48. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  49. package/dist/cli.js +23 -11
  50. package/dist/cli.js.map +2 -2
  51. package/dist/coding-agents/default-permissions.js +31 -1
  52. package/dist/coding-agents/default-permissions.js.map +2 -2
  53. package/dist/coding-agents/drbot-mcp.js +269 -0
  54. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  55. package/dist/commands/add.js +7 -4
  56. package/dist/commands/add.js.map +2 -2
  57. package/dist/commands/analyzer.js +143 -40
  58. package/dist/commands/analyzer.js.map +2 -2
  59. package/dist/commands/changeset-verify.js +63 -0
  60. package/dist/commands/changeset-verify.js.map +7 -0
  61. package/dist/commands/changeset.js +80 -29
  62. package/dist/commands/changeset.js.map +2 -2
  63. package/dist/commands/delete.js +10 -4
  64. package/dist/commands/delete.js.map +2 -2
  65. package/dist/commands/farm.js +176 -60
  66. package/dist/commands/farm.js.map +2 -2
  67. package/dist/commands/init.js +9 -26
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +40 -21
  70. package/dist/commands/mcp.js.map +3 -3
  71. package/dist/commands/relationship.js +208 -37
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +3 -3
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +52 -32
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -103
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +19 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +1 -1
  82. package/dist/commands/visualize.js +34 -15
  83. package/dist/commands/visualize.js.map +2 -2
  84. package/dist/core/annotation-store.js +15 -5
  85. package/dist/core/annotation-store.js.map +2 -2
  86. package/dist/core/catalog-migration.js +34 -0
  87. package/dist/core/catalog-migration.js.map +7 -0
  88. package/dist/core/changeset-analysis.js +421 -0
  89. package/dist/core/changeset-analysis.js.map +7 -0
  90. package/dist/core/changeset-base-index.js +160 -0
  91. package/dist/core/changeset-base-index.js.map +7 -0
  92. package/dist/core/changeset-brief-compact.js +256 -0
  93. package/dist/core/changeset-brief-compact.js.map +7 -0
  94. package/dist/core/changeset-brief-delta.js +102 -0
  95. package/dist/core/changeset-brief-delta.js.map +7 -0
  96. package/dist/core/changeset-graph.js +281 -0
  97. package/dist/core/changeset-graph.js.map +7 -0
  98. package/dist/core/changeset-only-guard.js +32 -0
  99. package/dist/core/changeset-only-guard.js.map +7 -0
  100. package/dist/core/changeset-summary.js +68 -0
  101. package/dist/core/changeset-summary.js.map +7 -0
  102. package/dist/core/changeset-validator.js +66 -20
  103. package/dist/core/changeset-validator.js.map +2 -2
  104. package/dist/core/changeset-verify.js +848 -0
  105. package/dist/core/changeset-verify.js.map +7 -0
  106. package/dist/core/changeset.js +56 -9
  107. package/dist/core/changeset.js.map +2 -2
  108. package/dist/core/element-type-migration.js +145 -0
  109. package/dist/core/element-type-migration.js.map +7 -0
  110. package/dist/core/extraction-observability.js +161 -0
  111. package/dist/core/extraction-observability.js.map +7 -0
  112. package/dist/core/farm-manifest.js +12 -6
  113. package/dist/core/farm-manifest.js.map +2 -2
  114. package/dist/core/farm-sync-engine.js +1 -1
  115. package/dist/core/farm-sync-engine.js.map +2 -2
  116. package/dist/core/farm-sync-state.js +10 -2
  117. package/dist/core/farm-sync-state.js.map +2 -2
  118. package/dist/core/migration-registry.js +88 -47
  119. package/dist/core/migration-registry.js.map +2 -2
  120. package/dist/core/model-upgrade.js +102 -0
  121. package/dist/core/model-upgrade.js.map +7 -0
  122. package/dist/core/model.js +82 -30
  123. package/dist/core/model.js.map +2 -2
  124. package/dist/core/mutation-handler.js +71 -6
  125. package/dist/core/mutation-handler.js.map +2 -2
  126. package/dist/core/project-init.js +53 -0
  127. package/dist/core/project-init.js.map +7 -0
  128. package/dist/core/relationship-migration-table.js +6639 -0
  129. package/dist/core/relationship-migration-table.js.map +7 -0
  130. package/dist/core/relationship-migration.js +272 -0
  131. package/dist/core/relationship-migration.js.map +7 -0
  132. package/dist/core/relationship-prune.js +49 -0
  133. package/dist/core/relationship-prune.js.map +7 -0
  134. package/dist/core/relationship-staging.js +161 -0
  135. package/dist/core/relationship-staging.js.map +7 -0
  136. package/dist/core/relationships.js +26 -4
  137. package/dist/core/relationships.js.map +2 -2
  138. package/dist/core/report-data-model.js +7 -13
  139. package/dist/core/report-data-model.js.map +2 -2
  140. package/dist/core/staged-changeset-storage.js +145 -18
  141. package/dist/core/staged-changeset-storage.js.map +2 -2
  142. package/dist/core/staged-source-reference.js +28 -0
  143. package/dist/core/staged-source-reference.js.map +7 -0
  144. package/dist/core/staging-area.js +166 -24
  145. package/dist/core/staging-area.js.map +2 -2
  146. package/dist/core/upgrade-plan.js +117 -0
  147. package/dist/core/upgrade-plan.js.map +7 -0
  148. package/dist/core/virtual-projection.js +13 -5
  149. package/dist/core/virtual-projection.js.map +2 -2
  150. package/dist/export/archimate-exporter.js +1 -0
  151. package/dist/export/archimate-exporter.js.map +2 -2
  152. package/dist/generated/compiled-validators.js +2 -2
  153. package/dist/generated/compiled-validators.js.map +2 -2
  154. package/dist/generated/layer-registry.js +16 -16
  155. package/dist/generated/layer-registry.js.map +2 -2
  156. package/dist/generated/node-types.js +186 -168
  157. package/dist/generated/node-types.js.map +2 -2
  158. package/dist/generated/relationship-index.js +2468 -7913
  159. package/dist/generated/relationship-index.js.map +2 -2
  160. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  161. package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
  162. package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
  163. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  164. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  165. package/dist/integrations/claude_code/commands/dr-map.md +20 -1
  166. package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
  167. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  168. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  169. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  170. package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
  171. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  172. package/dist/integrations/claude_code/docs/README.md +68 -2
  173. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  174. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  175. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  176. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  177. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  178. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  179. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  180. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  181. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  182. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  183. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  184. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  185. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  186. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  187. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
  188. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
  189. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
  190. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
  191. package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
  192. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  193. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  194. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  195. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  196. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  197. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  198. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  199. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  200. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  201. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  202. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  203. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  204. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
  205. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
  206. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  207. package/dist/integrations/hash-utils.js.map +2 -2
  208. package/dist/mcp/api-key-manager.js +14 -1
  209. package/dist/mcp/api-key-manager.js.map +2 -2
  210. package/dist/mcp/changeset-only.js +45 -0
  211. package/dist/mcp/changeset-only.js.map +7 -0
  212. package/dist/mcp/project-root.js +85 -0
  213. package/dist/mcp/project-root.js.map +7 -0
  214. package/dist/mcp/server-instructions.js +27 -0
  215. package/dist/mcp/server-instructions.js.map +7 -0
  216. package/dist/mcp/strict-schema.js +52 -0
  217. package/dist/mcp/strict-schema.js.map +7 -0
  218. package/dist/mcp/tool-registry.js +55 -5
  219. package/dist/mcp/tool-registry.js.map +2 -2
  220. package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
  221. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  222. package/dist/mcp/tools/changeset-commit.js +224 -0
  223. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  224. package/dist/mcp/tools/changeset-editing.js +260 -0
  225. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  226. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  227. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  228. package/dist/mcp/tools/changeset-shared.js +31 -0
  229. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  230. package/dist/mcp/tools/changeset-show.js +17 -1
  231. package/dist/mcp/tools/changeset-show.js.map +2 -2
  232. package/dist/mcp/tools/changeset-verify.js +49 -0
  233. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  234. package/dist/mcp/tools/codebase-extract.js +836 -0
  235. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  236. package/dist/mcp/tools/codebase-index.js +125 -0
  237. package/dist/mcp/tools/codebase-index.js.map +7 -0
  238. package/dist/mcp/tools/codebase-shared.js +265 -0
  239. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  240. package/dist/mcp/tools/codebase-verify.js +48 -0
  241. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  242. package/dist/mcp/tools/index.js +34 -1
  243. package/dist/mcp/tools/index.js.map +2 -2
  244. package/dist/mcp/tools/model-add.js +191 -74
  245. package/dist/mcp/tools/model-add.js.map +2 -2
  246. package/dist/mcp/tools/model-delete.js +2 -2
  247. package/dist/mcp/tools/model-delete.js.map +2 -2
  248. package/dist/mcp/tools/model-search.js +14 -0
  249. package/dist/mcp/tools/model-search.js.map +2 -2
  250. package/dist/mcp/tools/model-show.js +29 -2
  251. package/dist/mcp/tools/model-show.js.map +2 -2
  252. package/dist/mcp/tools/model-trace.js +93 -30
  253. package/dist/mcp/tools/model-trace.js.map +2 -2
  254. package/dist/mcp/tools/model-update.js +46 -26
  255. package/dist/mcp/tools/model-update.js.map +2 -2
  256. package/dist/mcp/tools/model-validate.js +46 -4
  257. package/dist/mcp/tools/model-validate.js.map +2 -2
  258. package/dist/mcp/tools/project-status-collectors.js +381 -0
  259. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  260. package/dist/mcp/tools/project-status.js +237 -0
  261. package/dist/mcp/tools/project-status.js.map +7 -0
  262. package/dist/mcp/tools/project-tools.js +505 -0
  263. package/dist/mcp/tools/project-tools.js.map +7 -0
  264. package/dist/mcp/tools/relationship-tools.js +167 -0
  265. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  266. package/dist/mcp/tools/review-open.js +210 -0
  267. package/dist/mcp/tools/review-open.js.map +7 -0
  268. package/dist/mcp/tools/shared.js +35 -4
  269. package/dist/mcp/tools/shared.js.map +2 -2
  270. package/dist/mcp/tools/spec-type.js +90 -0
  271. package/dist/mcp/tools/spec-type.js.map +7 -0
  272. package/dist/reports/model-report-orchestrator.js +31 -2
  273. package/dist/reports/model-report-orchestrator.js.map +2 -2
  274. package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
  275. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  276. package/dist/schemas/bundled/api.json +1029 -1658
  277. package/dist/schemas/bundled/apm.json +499 -734
  278. package/dist/schemas/bundled/application.json +735 -371
  279. package/dist/schemas/bundled/base.json +3 -220
  280. package/dist/schemas/bundled/business.json +452 -366
  281. package/dist/schemas/bundled/data-model.json +395 -411
  282. package/dist/schemas/bundled/data-store.json +334 -558
  283. package/dist/schemas/bundled/manifest.json +18 -18
  284. package/dist/schemas/bundled/motivation.json +349 -368
  285. package/dist/schemas/bundled/navigation.json +415 -731
  286. package/dist/schemas/bundled/product.json +325 -159
  287. package/dist/schemas/bundled/security.json +864 -1232
  288. package/dist/schemas/bundled/technology.json +574 -703
  289. package/dist/schemas/bundled/testing.json +865 -1646
  290. package/dist/schemas/bundled/ux.json +739 -1352
  291. package/dist/server/schemas.js +8 -0
  292. package/dist/server/schemas.js.map +2 -2
  293. package/dist/server/server.js +686 -231
  294. package/dist/server/server.js.map +3 -3
  295. package/dist/server/viewer-registry.js +264 -0
  296. package/dist/server/viewer-registry.js.map +7 -0
  297. package/dist/server-entry.js +2 -1
  298. package/dist/server-entry.js.map +2 -2
  299. package/dist/telemetry/index.js +1 -1
  300. package/dist/utils/ansi-suppressor.js +6 -8
  301. package/dist/utils/ansi-suppressor.js.map +2 -2
  302. package/dist/utils/attribute-patch.js +41 -0
  303. package/dist/utils/attribute-patch.js.map +7 -0
  304. package/dist/utils/error-hints.js +29 -0
  305. package/dist/utils/error-hints.js.map +7 -0
  306. package/dist/utils/errors.js +9 -3
  307. package/dist/utils/errors.js.map +2 -2
  308. package/dist/utils/id-generator.js +5 -2
  309. package/dist/utils/id-generator.js.map +2 -2
  310. package/dist/utils/project-paths.js +26 -1
  311. package/dist/utils/project-paths.js.map +2 -2
  312. package/dist/utils/source-reference.js +64 -31
  313. package/dist/utils/source-reference.js.map +2 -2
  314. package/dist/utils/spec-version.js +2 -2
  315. package/dist/utils/spec-version.js.map +1 -1
  316. package/dist/validators/composed-reference-validator.js +5 -27
  317. package/dist/validators/composed-reference-validator.js.map +2 -2
  318. package/dist/validators/model-warnings.js +28 -0
  319. package/dist/validators/model-warnings.js.map +7 -0
  320. package/dist/validators/reference-validator.js +0 -7
  321. package/dist/validators/reference-validator.js.map +2 -2
  322. package/dist/validators/relationship-schema-validator.js +3 -3
  323. package/dist/validators/relationship-schema-validator.js.map +2 -2
  324. package/dist/validators/schema-validator.js +27 -9
  325. package/dist/validators/schema-validator.js.map +2 -2
  326. package/dist/validators/types.js.map +1 -1
  327. package/dist/viewer/README.md +6 -6
  328. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  329. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  330. package/dist/viewer/index.html +2 -2
  331. package/dist/viewer/manifest.json +10 -10
  332. package/dist/viewer/version.json +1 -1
  333. package/package.json +2 -1
  334. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  335. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/errors.ts"],
4
- "sourcesContent": ["/**\n * Error handling utilities with formatted output\n * Matches Python CLI error message formatting\n *\n * Error Categories:\n * - User Error (exit code 1): Invalid input, wrong format, element exists\n * - Not Found (exit code 2): File/element/model not found\n * - System Error (exit code 3): Permission denied, I/O failure\n * - Validation Error (exit code 4): Schema/reference/semantic validation failed\n * - Breaking Change (exit code 5): Version migration required\n */\n\nimport ansis from \"ansis\";\nimport { isJson } from \"./globals.js\";\n\nconst MAX_SUGGESTIONS = 5;\n\nexport enum ErrorCategory {\n USER = 1,\n NOT_FOUND = 2,\n SYSTEM = 3,\n VALIDATION = 4,\n BREAKING_CHANGE = 5,\n}\n\nexport interface ErrorContext {\n operation?: string;\n context?: string;\n relatedElements?: string[];\n partialProgress?: { completed: number; total: number };\n}\n\nexport class CLIError extends Error {\n constructor(\n message: string,\n public exitCode: number = ErrorCategory.USER,\n public suggestions?: string[],\n public context?: ErrorContext\n ) {\n super(message);\n this.name = \"CLIError\";\n }\n\n format(): string {\n const lines: string[] = [];\n\n lines.push(ansis.red(`Error: ${this.message}`));\n\n if (this.context?.operation) {\n lines.push(ansis.dim(`During: ${this.context.operation}`));\n }\n\n if (this.context?.partialProgress) {\n const { completed, total } = this.context.partialProgress;\n lines.push(\n ansis.yellow(`\u26A0 Partial progress: ${completed}/${total} completed (operation rolled back)`)\n );\n }\n\n if (this.context?.relatedElements && this.context.relatedElements.length > 0) {\n lines.push(ansis.dim(\"Related elements:\"));\n for (const elem of this.context.relatedElements.slice(0, MAX_SUGGESTIONS)) {\n lines.push(ansis.dim(` \u2022 ${elem}`));\n }\n if (this.context.relatedElements.length > MAX_SUGGESTIONS) {\n lines.push(\n ansis.dim(` ... and ${this.context.relatedElements.length - MAX_SUGGESTIONS} more`)\n );\n }\n }\n\n if (this.suggestions && this.suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of this.suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n\n return lines.join(\"\\n\");\n }\n}\n\nexport class ValidationError extends CLIError {\n constructor(\n message: string,\n public errors: Array<{ layer?: string; elementId?: string; message: string }> = [],\n suggestions?: string[],\n context?: ErrorContext\n ) {\n super(message, ErrorCategory.VALIDATION, suggestions, context);\n this.name = \"ValidationError\";\n }\n\n format(): string {\n const lines: string[] = [];\n\n lines.push(ansis.red(`Error: ${this.message}`));\n\n if (this.errors.length > 0) {\n // Show error count and grouping\n const errorsByLayer: Record<string, Array<{ elementId?: string; message: string }>> = {};\n for (const error of this.errors) {\n const layer = error.layer || \"general\";\n if (!errorsByLayer[layer]) {\n errorsByLayer[layer] = [];\n }\n errorsByLayer[layer].push({ elementId: error.elementId, message: error.message });\n }\n\n lines.push(\"\");\n lines.push(ansis.dim(`Validation errors: ${this.errors.length} found`));\n lines.push(\"\");\n\n const layerNames = Object.keys(errorsByLayer).sort();\n\n for (const layer of layerNames) {\n const layerErrors = errorsByLayer[layer];\n lines.push(ansis.dim(` [${layer}] ${layerErrors.length} error(s):`));\n\n for (let i = 0; i < Math.min(layerErrors.length, MAX_SUGGESTIONS); i++) {\n const error = layerErrors[i];\n let detail = \"\";\n if (error.elementId) {\n detail = ` ${error.elementId}: ${error.message}`;\n } else {\n detail = ` ${error.message}`;\n }\n lines.push(ansis.dim(detail));\n }\n\n if (layerErrors.length > MAX_SUGGESTIONS) {\n lines.push(\n ansis.dim(` ... and ${layerErrors.length - MAX_SUGGESTIONS} more in this layer`)\n );\n }\n }\n }\n\n if (this.suggestions && this.suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of this.suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n\n return lines.join(\"\\n\");\n }\n}\n\nexport class FileNotFoundError extends CLIError {\n constructor(path: string, context?: string) {\n const message = context ? `${context}: ${path}` : `File not found: ${path}`;\n super(message, ErrorCategory.NOT_FOUND, [\n `Check that the path is correct: ${path}`,\n 'Use \"dr init\" to create a new model',\n 'Use \"dr list <layer>\" to see available elements',\n ]);\n this.name = \"FileNotFoundError\";\n }\n}\n\nexport class ElementNotFoundError extends CLIError {\n constructor(elementId: string) {\n super(`Element not found: ${elementId}`, ErrorCategory.NOT_FOUND, [\n `Use \"dr search ${elementId}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n \"Check the element ID format: {layer}.{type}.{kebab-case-name}\",\n ]);\n this.name = \"ElementNotFoundError\";\n }\n}\n\nexport class ModelNotFoundError extends CLIError {\n constructor(rootPath: string = \".\") {\n super(`No model found at ${rootPath}`, ErrorCategory.NOT_FOUND, [\n `Run \"dr init\" to initialize a new model`,\n \"Check that you are in the correct directory\",\n ]);\n this.name = \"ModelNotFoundError\";\n }\n}\n\nexport class InvalidJSONError extends CLIError {\n constructor(input: string, context?: string) {\n const message = context ? `Invalid JSON in ${context}` : \"Invalid JSON format\";\n super(message, ErrorCategory.USER, [\n `Check your JSON syntax: ${input}`,\n 'Use single quotes to wrap JSON: --attributes \\'{\"key\":\"value\"}\\'',\n \"Escape special characters properly\",\n ]);\n this.name = \"InvalidJSONError\";\n }\n}\n\nexport function handleError(error: unknown): never {\n if (error instanceof CLIError) {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: error.exitCode,\n message: error.message,\n };\n if (error.suggestions && error.suggestions.length > 0) {\n output.suggestions = error.suggestions;\n }\n if (error.context) {\n if (error.context.operation) output.operation = error.context.operation;\n if (error.context.relatedElements && error.context.relatedElements.length > 0) {\n output.relatedElements = error.context.relatedElements;\n }\n if (error.context.partialProgress) {\n output.partialProgress = error.context.partialProgress;\n }\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(error.format());\n }\n // Don't call process.exit() - rethrow so CLI wrapper can handle telemetry shutdown\n throw error;\n } else if (error instanceof Error) {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: ErrorCategory.USER,\n message: error.message,\n };\n if (process.env.DEBUG && error.stack) {\n output.stack = error.stack;\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(ansis.red(`Error: ${error.message}`));\n if (process.env.DEBUG) {\n console.error(ansis.dim(error.stack));\n }\n }\n // Rethrow to allow CLI wrapper to handle shutdown\n throw error;\n } else {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: ErrorCategory.USER,\n message: \"An unexpected error occurred\",\n };\n if (process.env.DEBUG) {\n output.details = String(error);\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(ansis.red(\"An unexpected error occurred\"));\n if (process.env.DEBUG) {\n console.error(ansis.dim(String(error)));\n }\n }\n // Create proper error to throw\n throw new Error(String(error));\n }\n}\n\nexport function handleWarning(message: string, suggestions?: string[]): void {\n if (isJson()) {\n // In JSON mode, emit warnings to stderr as structured JSON\n const output: Record<string, unknown> = {\n level: \"warning\",\n message,\n };\n if (suggestions && suggestions.length > 0) {\n output.suggestions = suggestions;\n }\n console.error(JSON.stringify(output));\n } else {\n const lines: string[] = [];\n lines.push(ansis.yellow(`Warning: ${message}`));\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n console.warn(lines.join(\"\\n\"));\n }\n}\n\nexport function handleSuccess(message: string, details?: Record<string, unknown>, options?: { verbose?: boolean }): void {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"ok\",\n ...details,\n };\n console.log(JSON.stringify(output));\n } else {\n console.log(ansis.green(`\u2713 ${message}`));\n // Only show details in non-JSON mode if verbose flag is set or details include changesetStatus/status\n if (details && (options?.verbose || details.changesetStatus)) {\n // Convert camelCase keys to PascalCase for display\n for (const [key, value] of Object.entries(details)) {\n const displayKey = keyToDisplayName(key);\n console.log(ansis.dim(` ${displayKey}: ${value}`));\n }\n }\n }\n}\n\nfunction keyToDisplayName(key: string): string {\n // Convert camelCase and snake_case to PascalCase/Title Case\n return key\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase to words\n .replace(/_/g, ' ') // snake_case to words\n .split(' ')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\nexport function handleInfo(message: string, details?: Record<string, string>): void {\n if (!isJson()) {\n console.log(message);\n if (details) {\n for (const [key, value] of Object.entries(details)) {\n console.log(ansis.dim(` ${key}: ${value}`));\n }\n }\n }\n}\n\n/**\n * Utility to extract common valid options for better error suggestions\n */\nexport function formatValidOptions(options: string[], heading: string = \"Valid options\"): string {\n if (options.length === 0) return \"\";\n if (options.length === 1) return `${heading}: ${options[0]}`;\n return `${heading}: ${options.slice(0, -1).join(\", \")} or ${options[options.length - 1]}`;\n}\n\n/**\n * Categorize errors based on type, using stable error.code for system errors\n * Returns the appropriate ErrorCategory for a given error\n */\nexport function categorizeError(error: unknown): ErrorCategory {\n if (error instanceof CLIError) {\n return error.exitCode;\n }\n\n // Check for system errors using stable error.code\n if (error instanceof Error && \"code\" in error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (typeof code === \"string\") {\n // File not found errors should map to NOT_FOUND category\n if (code === \"ENOENT\") {\n return ErrorCategory.NOT_FOUND;\n }\n // Other system-level errors\n const SYSTEM_ERROR_CODES = new Set([\"EACCES\", \"ENOSPC\", \"EIO\", \"EROFS\", \"EPERM\"]);\n if (SYSTEM_ERROR_CODES.has(code)) {\n return ErrorCategory.SYSTEM;\n }\n }\n }\n\n // Default to USER for other errors\n return ErrorCategory.USER;\n}\n\n/**\n * Extract error message from any error type\n * Handles Error instances, strings, and unknown types consistently\n */\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n}\n\n/**\n * Simple fuzzy match to find similar strings (for typo suggestions)\n */\nexport function findSimilar(input: string, options: string[], maxDistance: number = 2): string[] {\n return options\n .map((option) => ({\n option,\n distance: levenshteinDistance(input.toLowerCase(), option.toLowerCase()),\n }))\n .filter(({ distance }) => distance <= maxDistance)\n .sort((a, b) => a.distance - b.distance)\n .slice(0, 3)\n .map(({ option }) => option);\n}\n\n/**\n * Calculate Levenshtein distance for typo detection\n */\nfunction levenshteinDistance(a: string, b: string): number {\n const matrix: number[][] = [];\n\n for (let i = 0; i <= b.length; i++) {\n matrix[i] = [i];\n }\n\n for (let j = 0; j <= a.length; j++) {\n matrix[0][j] = j;\n }\n\n for (let i = 1; i <= b.length; i++) {\n for (let j = 1; j <= a.length; j++) {\n if (b.charAt(i - 1) === a.charAt(j - 1)) {\n matrix[i][j] = matrix[i - 1][j - 1];\n } else {\n matrix[i][j] = Math.min(\n matrix[i - 1][j - 1] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j] + 1\n );\n }\n }\n }\n\n return matrix[b.length][a.length];\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Error handling utilities with formatted output\n * Matches Python CLI error message formatting\n *\n * Error Categories:\n * - User Error (exit code 1): Invalid input, wrong format, element exists\n * - Not Found (exit code 2): File/element/model not found\n * - System Error (exit code 3): Permission denied, I/O failure\n * - Validation Error (exit code 4): Schema/reference/semantic validation failed\n * - Breaking Change (exit code 5): Version migration required\n */\n\nimport ansis from \"ansis\";\nimport { isJson } from \"./globals.js\";\n\nconst MAX_SUGGESTIONS = 5;\n\nexport enum ErrorCategory {\n USER = 1,\n NOT_FOUND = 2,\n SYSTEM = 3,\n VALIDATION = 4,\n BREAKING_CHANGE = 5,\n}\n\nexport interface ErrorContext {\n operation?: string;\n context?: string;\n relatedElements?: string[];\n partialProgress?: { completed: number; total: number };\n}\n\nexport class CLIError extends Error {\n constructor(\n message: string,\n public exitCode: number = ErrorCategory.USER,\n public suggestions?: string[],\n public context?: ErrorContext\n ) {\n super(message);\n this.name = \"CLIError\";\n }\n\n format(): string {\n const lines: string[] = [];\n\n lines.push(ansis.red(`Error: ${this.message}`));\n\n if (this.context?.operation) {\n lines.push(ansis.dim(`During: ${this.context.operation}`));\n }\n\n if (this.context?.partialProgress) {\n const { completed, total } = this.context.partialProgress;\n lines.push(\n ansis.yellow(`\u26A0 Partial progress: ${completed}/${total} completed (operation rolled back)`)\n );\n }\n\n if (this.context?.relatedElements && this.context.relatedElements.length > 0) {\n lines.push(ansis.dim(\"Related elements:\"));\n for (const elem of this.context.relatedElements.slice(0, MAX_SUGGESTIONS)) {\n lines.push(ansis.dim(` \u2022 ${elem}`));\n }\n if (this.context.relatedElements.length > MAX_SUGGESTIONS) {\n lines.push(\n ansis.dim(` ... and ${this.context.relatedElements.length - MAX_SUGGESTIONS} more`)\n );\n }\n }\n\n if (this.suggestions && this.suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of this.suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n\n return lines.join(\"\\n\");\n }\n}\n\nexport class ValidationError extends CLIError {\n constructor(\n message: string,\n public errors: Array<{ layer?: string; elementId?: string; message: string }> = [],\n suggestions?: string[],\n context?: ErrorContext\n ) {\n super(message, ErrorCategory.VALIDATION, suggestions, context);\n this.name = \"ValidationError\";\n }\n\n format(): string {\n const lines: string[] = [];\n\n lines.push(ansis.red(`Error: ${this.message}`));\n\n if (this.errors.length > 0) {\n // Show error count and grouping\n const errorsByLayer: Record<string, Array<{ elementId?: string; message: string }>> = {};\n for (const error of this.errors) {\n const layer = error.layer || \"general\";\n if (!errorsByLayer[layer]) {\n errorsByLayer[layer] = [];\n }\n errorsByLayer[layer].push({ elementId: error.elementId, message: error.message });\n }\n\n lines.push(\"\");\n lines.push(ansis.dim(`Validation errors: ${this.errors.length} found`));\n lines.push(\"\");\n\n const layerNames = Object.keys(errorsByLayer).sort();\n\n for (const layer of layerNames) {\n const layerErrors = errorsByLayer[layer];\n lines.push(ansis.dim(` [${layer}] ${layerErrors.length} error(s):`));\n\n for (let i = 0; i < Math.min(layerErrors.length, MAX_SUGGESTIONS); i++) {\n const error = layerErrors[i];\n let detail = \"\";\n if (error.elementId) {\n detail = ` ${error.elementId}: ${error.message}`;\n } else {\n detail = ` ${error.message}`;\n }\n lines.push(ansis.dim(detail));\n }\n\n if (layerErrors.length > MAX_SUGGESTIONS) {\n lines.push(\n ansis.dim(` ... and ${layerErrors.length - MAX_SUGGESTIONS} more in this layer`)\n );\n }\n }\n }\n\n if (this.suggestions && this.suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of this.suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n\n return lines.join(\"\\n\");\n }\n}\n\nexport class FileNotFoundError extends CLIError {\n constructor(path: string, context?: string) {\n const message = context ? `${context}: ${path}` : `File not found: ${path}`;\n super(message, ErrorCategory.NOT_FOUND, [\n `Check that the path is correct: ${path}`,\n 'Use \"dr init\" to create a new model',\n 'Use \"dr list <layer>\" to see available elements',\n ]);\n this.name = \"FileNotFoundError\";\n }\n}\n\nexport class ElementNotFoundError extends CLIError {\n constructor(elementId: string) {\n super(`Element not found: ${elementId}`, ErrorCategory.NOT_FOUND, [\n `Use \"dr search ${elementId}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n \"Check the element ID format: {layer}.{type}.{kebab-case-name}\",\n ]);\n this.name = \"ElementNotFoundError\";\n }\n}\n\nexport class ModelNotFoundError extends CLIError {\n constructor(rootPath: string = \".\") {\n super(`No model found at ${rootPath}`, ErrorCategory.NOT_FOUND, [\n `Run \"dr init\" to initialize a new model`,\n \"Check that you are in the correct directory\",\n ]);\n this.name = \"ModelNotFoundError\";\n }\n}\n\nexport class InvalidJSONError extends CLIError {\n constructor(input: string, context?: string) {\n const message = context ? `Invalid JSON in ${context}` : \"Invalid JSON format\";\n super(message, ErrorCategory.USER, [\n `Check your JSON syntax: ${input}`,\n 'Use single quotes to wrap JSON: --attributes \\'{\"key\":\"value\"}\\'',\n \"Escape special characters properly\",\n ]);\n this.name = \"InvalidJSONError\";\n }\n}\n\nexport function handleError(error: unknown): never {\n if (error instanceof CLIError) {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: error.exitCode,\n message: error.message,\n };\n if (error.suggestions && error.suggestions.length > 0) {\n output.suggestions = error.suggestions;\n }\n if (error.context) {\n if (error.context.operation) output.operation = error.context.operation;\n if (error.context.relatedElements && error.context.relatedElements.length > 0) {\n output.relatedElements = error.context.relatedElements;\n }\n if (error.context.partialProgress) {\n output.partialProgress = error.context.partialProgress;\n }\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(error.format());\n }\n // Don't call process.exit() - rethrow so CLI wrapper can handle telemetry shutdown\n throw error;\n } else if (error instanceof Error) {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: ErrorCategory.USER,\n message: error.message,\n };\n if (process.env.DEBUG && error.stack) {\n output.stack = error.stack;\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(ansis.red(`Error: ${error.message}`));\n if (process.env.DEBUG) {\n console.error(ansis.dim(error.stack));\n }\n }\n // Rethrow to allow CLI wrapper to handle shutdown\n throw error;\n } else {\n if (isJson()) {\n const output: Record<string, unknown> = {\n status: \"error\",\n code: ErrorCategory.USER,\n message: \"An unexpected error occurred\",\n };\n if (process.env.DEBUG) {\n output.details = String(error);\n }\n console.log(JSON.stringify(output));\n } else {\n console.error(ansis.red(\"An unexpected error occurred\"));\n if (process.env.DEBUG) {\n console.error(ansis.dim(String(error)));\n }\n }\n // Create proper error to throw\n throw new Error(String(error));\n }\n}\n\nexport function handleWarning(message: string, suggestions?: string[]): void {\n if (isJson()) {\n // In JSON mode, emit warnings to stderr as structured JSON\n const output: Record<string, unknown> = {\n level: \"warning\",\n message,\n };\n if (suggestions && suggestions.length > 0) {\n output.suggestions = suggestions;\n }\n console.error(JSON.stringify(output));\n } else {\n const lines: string[] = [];\n lines.push(ansis.yellow(`Warning: ${message}`));\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(ansis.dim(\"Suggestions:\"));\n for (const suggestion of suggestions) {\n lines.push(ansis.dim(` \u2022 ${suggestion}`));\n }\n }\n console.warn(lines.join(\"\\n\"));\n }\n}\n\nexport function handleSuccess(message: string, details?: Record<string, unknown>, options?: { verbose?: boolean }): void {\n if (isJson()) {\n const output: Record<string, unknown> = {\n ...details,\n status: \"ok\",\n };\n console.log(JSON.stringify(output));\n } else {\n console.log(ansis.green(`\u2713 ${message}`));\n // Only show details in non-JSON mode if verbose flag is set or details include changesetStatus/status\n if (details && (options?.verbose || details.changesetStatus)) {\n // Convert camelCase keys to PascalCase for display\n for (const [key, value] of Object.entries(details)) {\n const displayKey = keyToDisplayName(key);\n console.log(ansis.dim(` ${displayKey}: ${value}`));\n }\n }\n }\n}\n\nfunction keyToDisplayName(key: string): string {\n // Convert camelCase and snake_case to PascalCase/Title Case\n return key\n .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase to words\n .replace(/_/g, ' ') // snake_case to words\n .split(' ')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\nexport function handleInfo(message: string, details?: Record<string, string>): void {\n if (isJson()) {\n const output: Record<string, unknown> = {\n level: \"info\",\n message,\n };\n if (details) {\n output.details = details;\n }\n console.error(JSON.stringify(output));\n } else {\n console.log(message);\n if (details) {\n for (const [key, value] of Object.entries(details)) {\n console.log(ansis.dim(` ${key}: ${value}`));\n }\n }\n }\n}\n\n/**\n * Utility to extract common valid options for better error suggestions\n */\nexport function formatValidOptions(options: string[], heading: string = \"Valid options\"): string {\n if (options.length === 0) return \"\";\n if (options.length === 1) return `${heading}: ${options[0]}`;\n return `${heading}: ${options.slice(0, -1).join(\", \")} or ${options[options.length - 1]}`;\n}\n\n/**\n * Categorize errors based on type, using stable error.code for system errors\n * Returns the appropriate ErrorCategory for a given error\n */\nexport function categorizeError(error: unknown): ErrorCategory {\n if (error instanceof CLIError) {\n return error.exitCode;\n }\n\n // Check for system errors using stable error.code\n if (error instanceof Error && \"code\" in error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (typeof code === \"string\") {\n // File not found errors should map to NOT_FOUND category\n if (code === \"ENOENT\") {\n return ErrorCategory.NOT_FOUND;\n }\n // Other system-level errors\n const SYSTEM_ERROR_CODES = new Set([\"EACCES\", \"ENOSPC\", \"EIO\", \"EROFS\", \"EPERM\"]);\n if (SYSTEM_ERROR_CODES.has(code)) {\n return ErrorCategory.SYSTEM;\n }\n }\n }\n\n // Default to USER for other errors\n return ErrorCategory.USER;\n}\n\n/**\n * Extract error message from any error type\n * Handles Error instances, strings, and unknown types consistently\n */\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n}\n\n/**\n * Simple fuzzy match to find similar strings (for typo suggestions)\n */\nexport function findSimilar(input: string, options: string[], maxDistance: number = 2): string[] {\n return options\n .map((option) => ({\n option,\n distance: levenshteinDistance(input.toLowerCase(), option.toLowerCase()),\n }))\n .filter(({ distance }) => distance <= maxDistance)\n .sort((a, b) => a.distance - b.distance)\n .slice(0, 3)\n .map(({ option }) => option);\n}\n\n/**\n * Calculate Levenshtein distance for typo detection\n */\nfunction levenshteinDistance(a: string, b: string): number {\n const matrix: number[][] = [];\n\n for (let i = 0; i <= b.length; i++) {\n matrix[i] = [i];\n }\n\n for (let j = 0; j <= a.length; j++) {\n matrix[0][j] = j;\n }\n\n for (let i = 1; i <= b.length; i++) {\n for (let j = 1; j <= a.length; j++) {\n if (b.charAt(i - 1) === a.charAt(j - 1)) {\n matrix[i][j] = matrix[i - 1][j - 1];\n } else {\n matrix[i][j] = Math.min(\n matrix[i - 1][j - 1] + 1,\n matrix[i][j - 1] + 1,\n matrix[i - 1][j] + 1\n );\n }\n }\n }\n\n return matrix[b.length][a.length];\n}\n"],
5
+ "mappings": "AAYA,OAAO,WAAW;AAClB,SAAS,cAAc;AAEvB,MAAM,kBAAkB;AAExB,IAAY;CAAZ,SAAYA,gBAAa;AACvB,EAAAA,eAAAA,eAAA,OAAA,CAAA,IAAA,QACAA,eAAAA,eAAA,YAAA,CAAA,IAAA,aACAA,eAAAA,eAAA,SAAA,CAAA,IAAA,UACAA,eAAAA,eAAA,aAAA,CAAA,IAAA,cACAA,eAAAA,eAAA,kBAAA,CAAA,IAAA;AACF,GANY,kBAAA,gBAAa,CAAA,EAAA;AAenB,MAAO,iBAAiB,MAAK;EAGxB;EACA;EACA;EAJT,YACE,SACO,WAAmB,cAAc,MACjC,aACA,SAAsB;AAE7B,UAAM,OAAO,GAJN,KAAA,WAAA,UACA,KAAA,cAAA,aACA,KAAA,UAAA,SAGP,KAAK,OAAO;EACd;EAEA,SAAM;AACJ,UAAM,QAAkB,CAAA;AAQxB,QANA,MAAM,KAAK,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC,GAE1C,KAAK,SAAS,aAChB,MAAM,KAAK,MAAM,IAAI,WAAW,KAAK,QAAQ,SAAS,EAAE,CAAC,GAGvD,KAAK,SAAS,iBAAiB;AACjC,YAAM,EAAE,WAAW,MAAK,IAAK,KAAK,QAAQ;AAC1C,YAAM,KACJ,MAAM,OAAO,4BAAuB,SAAS,IAAI,KAAK,oCAAoC,CAAC;IAE/F;AAEA,QAAI,KAAK,SAAS,mBAAmB,KAAK,QAAQ,gBAAgB,SAAS,GAAG;AAC5E,YAAM,KAAK,MAAM,IAAI,mBAAmB,CAAC;AACzC,iBAAW,QAAQ,KAAK,QAAQ,gBAAgB,MAAM,GAAG,eAAe;AACtE,cAAM,KAAK,MAAM,IAAI,YAAO,IAAI,EAAE,CAAC;AAErC,MAAI,KAAK,QAAQ,gBAAgB,SAAS,mBACxC,MAAM,KACJ,MAAM,IAAI,aAAa,KAAK,QAAQ,gBAAgB,SAAS,eAAe,OAAO,CAAC;IAG1F;AAEA,QAAI,KAAK,eAAe,KAAK,YAAY,SAAS,GAAG;AACnD,YAAM,KAAK,EAAE,GACb,MAAM,KAAK,MAAM,IAAI,cAAc,CAAC;AACpC,iBAAW,cAAc,KAAK;AAC5B,cAAM,KAAK,MAAM,IAAI,YAAO,UAAU,EAAE,CAAC;IAE7C;AAEA,WAAO,MAAM,KAAK;CAAI;EACxB;;AAGI,MAAO,wBAAwB,SAAQ;EAGlC;EAFT,YACE,SACO,SAAyE,CAAA,GAChF,aACA,SAAsB;AAEtB,UAAM,SAAS,cAAc,YAAY,aAAa,OAAO,GAJtD,KAAA,SAAA,QAKP,KAAK,OAAO;EACd;EAEA,SAAM;AACJ,UAAM,QAAkB,CAAA;AAIxB,QAFA,MAAM,KAAK,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC,GAE1C,KAAK,OAAO,SAAS,GAAG;AAE1B,YAAM,gBAAgF,CAAA;AACtF,iBAAW,SAAS,KAAK,QAAQ;AAC/B,cAAM,QAAQ,MAAM,SAAS;AAC7B,QAAK,cAAc,KAAK,MACtB,cAAc,KAAK,IAAI,CAAA,IAEzB,cAAc,KAAK,EAAE,KAAK,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,QAAO,CAAE;MAClF;AAEA,YAAM,KAAK,EAAE,GACb,MAAM,KAAK,MAAM,IAAI,sBAAsB,KAAK,OAAO,MAAM,QAAQ,CAAC,GACtE,MAAM,KAAK,EAAE;AAEb,YAAM,aAAa,OAAO,KAAK,aAAa,EAAE,KAAI;AAElD,iBAAW,SAAS,YAAY;AAC9B,cAAM,cAAc,cAAc,KAAK;AACvC,cAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK,YAAY,MAAM,YAAY,CAAC;AAEpE,iBAAS,IAAI,GAAG,IAAI,KAAK,IAAI,YAAY,QAAQ,eAAe,GAAG,KAAK;AACtE,gBAAM,QAAQ,YAAY,CAAC;AAC3B,cAAI,SAAS;AACb,UAAI,MAAM,YACR,SAAS,OAAO,MAAM,SAAS,KAAK,MAAM,OAAO,KAEjD,SAAS,OAAO,MAAM,OAAO,IAE/B,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC;QAC9B;AAEA,QAAI,YAAY,SAAS,mBACvB,MAAM,KACJ,MAAM,IAAI,eAAe,YAAY,SAAS,eAAe,qBAAqB,CAAC;MAGzF;IACF;AAEA,QAAI,KAAK,eAAe,KAAK,YAAY,SAAS,GAAG;AACnD,YAAM,KAAK,EAAE,GACb,MAAM,KAAK,MAAM,IAAI,cAAc,CAAC;AACpC,iBAAW,cAAc,KAAK;AAC5B,cAAM,KAAK,MAAM,IAAI,YAAO,UAAU,EAAE,CAAC;IAE7C;AAEA,WAAO,MAAM,KAAK;CAAI;EACxB;;AAGI,MAAO,0BAA0B,SAAQ;EAC7C,YAAY,MAAc,SAAgB;AACxC,UAAM,UAAU,UAAU,GAAG,OAAO,KAAK,IAAI,KAAK,mBAAmB,IAAI;AACzE,UAAM,SAAS,cAAc,WAAW;MACtC,mCAAmC,IAAI;MACvC;MACA;KACD,GACD,KAAK,OAAO;EACd;;AAGI,MAAO,6BAA6B,SAAQ;EAChD,YAAY,WAAiB;AAC3B,UAAM,sBAAsB,SAAS,IAAI,cAAc,WAAW;MAChE,kBAAkB,SAAS;MAC3B;MACA;KACD,GACD,KAAK,OAAO;EACd;;AAGI,MAAO,2BAA2B,SAAQ;EAC9C,YAAY,WAAmB,KAAG;AAChC,UAAM,qBAAqB,QAAQ,IAAI,cAAc,WAAW;MAC9D;MACA;KACD,GACD,KAAK,OAAO;EACd;;AAGI,MAAO,yBAAyB,SAAQ;EAC5C,YAAY,OAAe,SAAgB;AACzC,UAAM,UAAU,UAAU,mBAAmB,OAAO,KAAK;AACzD,UAAM,SAAS,cAAc,MAAM;MACjC,2BAA2B,KAAK;MAChC;MACA;KACD,GACD,KAAK,OAAO;EACd;;AAGI,SAAU,YAAY,OAAc;AACxC,MAAI,iBAAiB,UAAU;AAC7B,QAAI,OAAM,GAAI;AACZ,YAAM,SAAkC;QACtC,QAAQ;QACR,MAAM,MAAM;QACZ,SAAS,MAAM;;AAEjB,MAAI,MAAM,eAAe,MAAM,YAAY,SAAS,MAClD,OAAO,cAAc,MAAM,cAEzB,MAAM,YACJ,MAAM,QAAQ,cAAW,OAAO,YAAY,MAAM,QAAQ,YAC1D,MAAM,QAAQ,mBAAmB,MAAM,QAAQ,gBAAgB,SAAS,MAC1E,OAAO,kBAAkB,MAAM,QAAQ,kBAErC,MAAM,QAAQ,oBAChB,OAAO,kBAAkB,MAAM,QAAQ,mBAG3C,QAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;IACpC;AACE,cAAQ,MAAM,MAAM,OAAM,CAAE;AAG9B,UAAM;EACR,WAAW,iBAAiB,OAAO;AACjC,QAAI,OAAM,GAAI;AACZ,YAAM,SAAkC;QACtC,QAAQ;QACR,MAAM,cAAc;QACpB,SAAS,MAAM;;AAEjB,MAAI,QAAQ,IAAI,SAAS,MAAM,UAC7B,OAAO,QAAQ,MAAM,QAEvB,QAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;IACpC;AACE,cAAQ,MAAM,MAAM,IAAI,UAAU,MAAM,OAAO,EAAE,CAAC,GAC9C,QAAQ,IAAI,SACd,QAAQ,MAAM,MAAM,IAAI,MAAM,KAAK,CAAC;AAIxC,UAAM;EACR,OAAO;AACL,QAAI,OAAM,GAAI;AACZ,YAAM,SAAkC;QACtC,QAAQ;QACR,MAAM,cAAc;QACpB,SAAS;;AAEX,MAAI,QAAQ,IAAI,UACd,OAAO,UAAU,OAAO,KAAK,IAE/B,QAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;IACpC;AACE,cAAQ,MAAM,MAAM,IAAI,8BAA8B,CAAC,GACnD,QAAQ,IAAI,SACd,QAAQ,MAAM,MAAM,IAAI,OAAO,KAAK,CAAC,CAAC;AAI1C,UAAM,IAAI,MAAM,OAAO,KAAK,CAAC;EAC/B;AACF;AAEM,SAAU,cAAc,SAAiB,aAAsB;AACnE,MAAI,OAAM,GAAI;AAEZ,UAAM,SAAkC;MACtC,OAAO;MACP;;AAEF,IAAI,eAAe,YAAY,SAAS,MACtC,OAAO,cAAc,cAEvB,QAAQ,MAAM,KAAK,UAAU,MAAM,CAAC;EACtC,OAAO;AACL,UAAM,QAAkB,CAAA;AAExB,QADA,MAAM,KAAK,MAAM,OAAO,YAAY,OAAO,EAAE,CAAC,GAC1C,eAAe,YAAY,SAAS,GAAG;AACzC,YAAM,KAAK,EAAE,GACb,MAAM,KAAK,MAAM,IAAI,cAAc,CAAC;AACpC,iBAAW,cAAc;AACvB,cAAM,KAAK,MAAM,IAAI,YAAO,UAAU,EAAE,CAAC;IAE7C;AACA,YAAQ,KAAK,MAAM,KAAK;CAAI,CAAC;EAC/B;AACF;AAEM,SAAU,cAAc,SAAiB,SAAmC,SAA+B;AAC/G,MAAI,OAAM,GAAI;AACZ,UAAM,SAAkC;MACtC,GAAG;MACH,QAAQ;;AAEV,YAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;EACpC,WACE,QAAQ,IAAI,MAAM,MAAM,UAAK,OAAO,EAAE,CAAC,GAEnC,YAAY,SAAS,WAAW,QAAQ;AAE1C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,YAAM,aAAa,iBAAiB,GAAG;AACvC,cAAQ,IAAI,MAAM,IAAI,KAAK,UAAU,KAAK,KAAK,EAAE,CAAC;IACpD;AAGN;AAEA,SAAS,iBAAiB,KAAW;AAEnC,SAAO,IACJ,QAAQ,mBAAmB,OAAO,EAClC,QAAQ,MAAM,GAAG,EACjB,MAAM,GAAG,EACT,IAAI,UAAQ,KAAK,OAAO,CAAC,EAAE,YAAW,IAAK,KAAK,MAAM,CAAC,CAAC,EACxD,KAAK,GAAG;AACb;AAEM,SAAU,WAAW,SAAiB,SAAgC;AAC1E,MAAI,OAAM,GAAI;AACZ,UAAM,SAAkC;MACtC,OAAO;MACP;;AAEF,IAAI,YACF,OAAO,UAAU,UAEnB,QAAQ,MAAM,KAAK,UAAU,MAAM,CAAC;EACtC,WACE,QAAQ,IAAI,OAAO,GACf;AACF,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO;AAC/C,cAAQ,IAAI,MAAM,IAAI,KAAK,GAAG,KAAK,KAAK,EAAE,CAAC;AAInD;AAKM,SAAU,mBAAmB,SAAmB,UAAkB,iBAAe;AACrF,SAAI,QAAQ,WAAW,IAAU,KAC7B,QAAQ,WAAW,IAAU,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,KACnD,GAAG,OAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC;AACzF;AAMM,SAAU,gBAAgB,OAAc;AAC5C,MAAI,iBAAiB;AACnB,WAAO,MAAM;AAIf,MAAI,iBAAiB,SAAS,UAAU,OAAO;AAC7C,UAAM,OAAQ,MAAgC;AAC9C,QAAI,OAAO,QAAS,UAAU;AAE5B,UAAI,SAAS;AACX,eAAO,cAAc;AAIvB,WAD2B,oBAAI,IAAI,CAAC,UAAU,UAAU,OAAO,SAAS,OAAO,CAAC,GACzD,IAAI,IAAI;AAC7B,eAAO,cAAc;IAEzB;EACF;AAGA,SAAO,cAAc;AACvB;AAMM,SAAU,gBAAgB,OAAc;AAC5C,SAAI,iBAAiB,QACZ,MAAM,UAER,OAAO,KAAK;AACrB;AAKM,SAAU,YAAY,OAAe,SAAmB,cAAsB,GAAC;AACnF,SAAO,QACJ,IAAI,CAAC,YAAY;IAChB;IACA,UAAU,oBAAoB,MAAM,YAAW,GAAI,OAAO,YAAW,CAAE;IACvE,EACD,OAAO,CAAC,EAAE,SAAQ,MAAO,YAAY,WAAW,EAChD,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,EACtC,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,EAAE,OAAM,MAAO,MAAM;AAC/B;AAKA,SAAS,oBAAoB,GAAW,GAAS;AAC/C,QAAM,SAAqB,CAAA;AAE3B,WAAS,IAAI,GAAG,KAAK,EAAE,QAAQ;AAC7B,WAAO,CAAC,IAAI,CAAC,CAAC;AAGhB,WAAS,IAAI,GAAG,KAAK,EAAE,QAAQ;AAC7B,WAAO,CAAC,EAAE,CAAC,IAAI;AAGjB,WAAS,IAAI,GAAG,KAAK,EAAE,QAAQ;AAC7B,aAAS,IAAI,GAAG,KAAK,EAAE,QAAQ;AAC7B,MAAI,EAAE,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,IACpC,OAAO,CAAC,EAAE,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,IAAI,CAAC,IAElC,OAAO,CAAC,EAAE,CAAC,IAAI,KAAK,IAClB,OAAO,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,GACvB,OAAO,CAAC,EAAE,IAAI,CAAC,IAAI,GACnB,OAAO,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;AAM5B,SAAO,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM;AAClC;",
6
6
  "names": ["ErrorCategory"]
7
7
  }
@@ -1,7 +1,10 @@
1
1
  import crypto from "node:crypto";
2
2
  function toKebabCase(text) {
3
- let result = text.replace(/[\s_]+/g, "-");
4
- return result = result.replace(/([a-z])([A-Z])/g, "$1-$2"), result = result.toLowerCase(), result = result.replace(/\./g, ""), result = result.replace(/[^a-z0-9-]/g, ""), result = result.replace(/-+/g, "-"), result = result.replace(/^-+|-+$/g, ""), result;
3
+ let result = text.replace(/[A-Za-z0-9$]+(?:\.[A-Za-z0-9$]+)+/g, (chain) => {
4
+ const parts = chain.split("."), camel = (part) => /[a-z][A-Z]/.test(part);
5
+ return parts.reduce((joined, part, i) => i === 0 ? part : joined + (camel(parts[i - 1]) || camel(part) ? "-" : ".") + part, "");
6
+ });
7
+ return result = result.replace(/[\s_]+/g, "-"), result = result.replace(/([a-z])([A-Z])/g, "$1-$2"), result = result.toLowerCase(), result = result.replace(/\./g, ""), result = result.replace(/[^a-z0-9-]/g, ""), result = result.replace(/-+/g, "-"), result = result.replace(/^-+|-+$/g, ""), result;
5
8
  }
6
9
  function generateElementId(layer, elementType, name) {
7
10
  const kebabName = toKebabCase(name);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/id-generator.ts"],
4
- "sourcesContent": ["/**\n * Element ID generation utilities matching Python CLI implementation.\n */\n\nimport crypto from \"node:crypto\";\n\n/**\n * Convert text to kebab-case.\n * Matches Python implementation in utils/id_generator.py\n *\n * @example\n * toKebabCase(\"Customer Management\") // \"customer-management\"\n * toKebabCase(\"CustomerManagement\") // \"customer-management\"\n * toKebabCase(\"customer_management\") // \"customer-management\"\n */\nexport function toKebabCase(text: string): string {\n // Replace spaces and underscores with hyphens\n let result = text.replace(/[\\s_]+/g, \"-\");\n\n // Insert hyphen before capital letters (for camelCase/PascalCase)\n result = result.replace(/([a-z])([A-Z])/g, \"$1-$2\");\n\n // Convert to lowercase\n result = result.toLowerCase();\n\n // Remove dots (e.g. \"Node.js\" \u2192 \"nodejs\") to prevent extra ID segments\n result = result.replace(/\\./g, \"\");\n\n // Remove any characters not valid in a slug (keeps only a-z, 0-9, hyphens)\n result = result.replace(/[^a-z0-9-]/g, \"\");\n\n // Remove multiple consecutive hyphens\n result = result.replace(/-+/g, \"-\");\n\n // Remove leading/trailing hyphens\n result = result.replace(/^-+|-+$/g, \"\");\n\n return result;\n}\n\n/**\n * Generate element ID following Python CLI convention: {layer}.{type}.{kebab-name}\n * Matches Python implementation in utils/id_generator.py\n *\n * @example\n * generateElementId(\"business\", \"service\", \"Customer Management\")\n * // \"business.service.customer-management\"\n */\nexport function generateElementId(layer: string, elementType: string, name: string): string {\n const kebabName = toKebabCase(name);\n return `${layer}.${elementType}.${kebabName}`;\n}\n\n/**\n * Generate a new UUIDv4 using the Node.js built-in crypto module.\n *\n * @example\n * generateUUID() // \"550e8400-e29b-41d4-a716-446655440000\"\n */\nexport function generateUUID(): string {\n return crypto.randomUUID();\n}\n"],
5
- "mappings": "AAIA,OAAO,YAAY;AAWb,SAAU,YAAY,MAAY;AAEtC,MAAI,SAAS,KAAK,QAAQ,WAAW,GAAG;AAGxC,kBAAS,OAAO,QAAQ,mBAAmB,OAAO,GAGlD,SAAS,OAAO,YAAW,GAG3B,SAAS,OAAO,QAAQ,OAAO,EAAE,GAGjC,SAAS,OAAO,QAAQ,eAAe,EAAE,GAGzC,SAAS,OAAO,QAAQ,OAAO,GAAG,GAGlC,SAAS,OAAO,QAAQ,YAAY,EAAE,GAE/B;AACT;AAUM,SAAU,kBAAkB,OAAe,aAAqB,MAAY;AAChF,QAAM,YAAY,YAAY,IAAI;AAClC,SAAO,GAAG,KAAK,IAAI,WAAW,IAAI,SAAS;AAC7C;AAQM,SAAU,eAAY;AAC1B,SAAO,OAAO,WAAU;AAC1B;",
4
+ "sourcesContent": ["/**\n * Element ID generation utilities matching Python CLI implementation.\n */\n\nimport crypto from \"node:crypto\";\n\n/**\n * Convert text to kebab-case.\n * Matches Python implementation in utils/id_generator.py\n *\n * @example\n * toKebabCase(\"Customer Management\") // \"customer-management\"\n * toKebabCase(\"CustomerManagement\") // \"customer-management\"\n * toKebabCase(\"customer_management\") // \"customer-management\"\n * toKebabCase(\"PetTypeRepository.findPetTypes\") // \"pet-type-repository-find-pet-types\"\n * toKebabCase(\"Node.js\") // \"nodejs\"\n */\nexport function toKebabCase(text: string): string {\n // A dot between code-symbol parts is a word break: \"Repo.findAll\" \u2192 \"Repo-findAll\". A part is a\n // code symbol when it is camelCase (a lowercase letter followed by an uppercase one). Other dots\n // are dropped below, so \"Node.js\", \"ASP.NET\" and \"1.2.3\" join as before.\n let result = text.replace(/[A-Za-z0-9$]+(?:\\.[A-Za-z0-9$]+)+/g, (chain) => {\n const parts = chain.split(\".\");\n const camel = (part: string) => /[a-z][A-Z]/.test(part);\n return parts.reduce((joined, part, i) => {\n if (i === 0) return part;\n return joined + (camel(parts[i - 1]) || camel(part) ? \"-\" : \".\") + part;\n }, \"\");\n });\n\n // Replace spaces and underscores with hyphens\n result = result.replace(/[\\s_]+/g, \"-\");\n\n // Insert hyphen before capital letters (for camelCase/PascalCase)\n result = result.replace(/([a-z])([A-Z])/g, \"$1-$2\");\n\n // Convert to lowercase\n result = result.toLowerCase();\n\n // Remove dots (e.g. \"Node.js\" \u2192 \"nodejs\") to prevent extra ID segments\n result = result.replace(/\\./g, \"\");\n\n // Remove any characters not valid in a slug (keeps only a-z, 0-9, hyphens)\n result = result.replace(/[^a-z0-9-]/g, \"\");\n\n // Remove multiple consecutive hyphens\n result = result.replace(/-+/g, \"-\");\n\n // Remove leading/trailing hyphens\n result = result.replace(/^-+|-+$/g, \"\");\n\n return result;\n}\n\n/**\n * Generate element ID following Python CLI convention: {layer}.{type}.{kebab-name}\n * Matches Python implementation in utils/id_generator.py\n *\n * @example\n * generateElementId(\"business\", \"service\", \"Customer Management\")\n * // \"business.service.customer-management\"\n */\nexport function generateElementId(layer: string, elementType: string, name: string): string {\n const kebabName = toKebabCase(name);\n return `${layer}.${elementType}.${kebabName}`;\n}\n\n/**\n * Generate a new UUIDv4 using the Node.js built-in crypto module.\n *\n * @example\n * generateUUID() // \"550e8400-e29b-41d4-a716-446655440000\"\n */\nexport function generateUUID(): string {\n return crypto.randomUUID();\n}\n"],
5
+ "mappings": "AAIA,OAAO,YAAY;AAab,SAAU,YAAY,MAAY;AAItC,MAAI,SAAS,KAAK,QAAQ,sCAAsC,CAAC,UAAS;AACxE,UAAM,QAAQ,MAAM,MAAM,GAAG,GACvB,QAAQ,CAAC,SAAiB,aAAa,KAAK,IAAI;AACtD,WAAO,MAAM,OAAO,CAAC,QAAQ,MAAM,MAC7B,MAAM,IAAU,OACb,UAAU,MAAM,MAAM,IAAI,CAAC,CAAC,KAAK,MAAM,IAAI,IAAI,MAAM,OAAO,MAClE,EAAE;EACP,CAAC;AAGD,kBAAS,OAAO,QAAQ,WAAW,GAAG,GAGtC,SAAS,OAAO,QAAQ,mBAAmB,OAAO,GAGlD,SAAS,OAAO,YAAW,GAG3B,SAAS,OAAO,QAAQ,OAAO,EAAE,GAGjC,SAAS,OAAO,QAAQ,eAAe,EAAE,GAGzC,SAAS,OAAO,QAAQ,OAAO,GAAG,GAGlC,SAAS,OAAO,QAAQ,YAAY,EAAE,GAE/B;AACT;AAUM,SAAU,kBAAkB,OAAe,aAAqB,MAAY;AAChF,QAAM,YAAY,YAAY,IAAI;AAClC,SAAO,GAAG,KAAK,IAAI,WAAW,IAAI,SAAS;AAC7C;AAQM,SAAU,eAAY;AAC1B,SAAO,OAAO,WAAU;AAC1B;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import { fileExists } from "./file-io.js";
2
2
  import { join, dirname, resolve } from "path";
3
+ import { stat } from "fs/promises";
3
4
  const MAX_SEARCH_DEPTH = 5, PROJECT_FOLDER_NAME = "documentation-robotics", SPEC_FOLDER_NAME = ".dr", FARM_FILE_NAME = "farm.yaml";
4
5
  async function findProjectRoot(startPath) {
5
6
  let currentPath = startPath || process.cwd();
@@ -56,12 +57,36 @@ Ensure DR_FARM_PATH points to a directory containing a farm.yaml file.`);
56
57
  }
57
58
  return null;
58
59
  }
60
+ const MODEL_LAYOUT_CANDIDATES = [
61
+ { layout: "standard", segments: [PROJECT_FOLDER_NAME, "model"] },
62
+ { layout: "legacy", segments: ["model"] },
63
+ { layout: "detached", segments: [] }
64
+ ];
65
+ async function resolveModelDirectory(basePath, options = {}) {
66
+ const resolvedBase = resolve(basePath);
67
+ for (const candidate of MODEL_LAYOUT_CANDIDATES) {
68
+ const modelDir = join(resolvedBase, ...candidate.segments), stats = await statIfExists(modelDir);
69
+ if (stats && !(options.requireManifest && (!stats.isDirectory() || !await statIfExists(join(modelDir, "manifest.yaml")))))
70
+ return { modelDir, layout: candidate.layout, isDirectory: stats.isDirectory() };
71
+ }
72
+ return null;
73
+ }
74
+ async function statIfExists(target) {
75
+ try {
76
+ return await stat(target);
77
+ } catch (err) {
78
+ if (err?.code === "ENOENT")
79
+ return;
80
+ throw err;
81
+ }
82
+ }
59
83
  export {
60
84
  findFarmRoot,
61
85
  findProjectRoot,
62
86
  getDocumentationRobotsPath,
63
87
  getModelPath,
64
88
  getSpecReferencePath,
65
- isInDRProject
89
+ isInDRProject,
90
+ resolveModelDirectory
66
91
  };
67
92
  //# sourceMappingURL=project-paths.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/project-paths.ts"],
4
- "sourcesContent": ["/**\n * Project Path Utilities\n *\n * Handles finding project root and related paths in the DR project structure:\n *\n * my-project/\n * \u251C\u2500\u2500 .dr/ # Reference spec (ephemeral, git-ignored)\n * \u2502 \u251C\u2500\u2500 manifest.json # Installed spec version\n * \u2502 \u2514\u2500\u2500 schemas/\n * \u2514\u2500\u2500 documentation-robotics/ # Project model (permanent, git-committed)\n * \u251C\u2500\u2500 model/\n * \u2502 \u251C\u2500\u2500 manifest.yaml # Model spec version\n * \u2502 \u2514\u2500\u2500 layers/\n * \u251C\u2500\u2500 annotations/\n * \u2514\u2500\u2500 changesets/\n */\n\nimport { fileExists } from \"./file-io.js\";\nimport { join, dirname, resolve } from \"path\";\n\nconst MAX_SEARCH_DEPTH = 5;\nconst PROJECT_FOLDER_NAME = \"documentation-robotics\";\nconst SPEC_FOLDER_NAME = \".dr\";\nconst FARM_FILE_NAME = \"farm.yaml\";\n\n/**\n * Find the project root by searching for the documentation-robotics/ folder\n *\n * Searches upward from the current working directory up to MAX_SEARCH_DEPTH levels.\n * Returns the directory containing the documentation_robotics/ folder.\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to project root, or null if not found\n *\n * @example\n * // From /home/user/my-project/documentation-robotics/model/01_motivation\n * // Returns: /home/user/my-project\n * const root = await findProjectRoot();\n */\nexport async function findProjectRoot(startPath?: string): Promise<string | null> {\n let currentPath = startPath || process.cwd();\n\n for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth++) {\n const projectPath = join(currentPath, PROJECT_FOLDER_NAME);\n\n if (await fileExists(projectPath)) {\n return currentPath;\n }\n\n // Move up one directory\n const parentPath = dirname(currentPath);\n\n // Check if we've reached the root\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return null;\n}\n\n/**\n * Get the path to the documentation-robotics/ folder\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to documentation-robotics/ folder, or null if not found\n */\nexport async function getDocumentationRobotsPath(startPath?: string): Promise<string | null> {\n const root = await findProjectRoot(startPath);\n return root ? join(root, PROJECT_FOLDER_NAME) : null;\n}\n\n/**\n * Get the path to the model directory (documentation-robotics/model/)\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to model directory, or null if not found\n */\nexport async function getModelPath(startPath?: string): Promise<string | null> {\n const drPath = await getDocumentationRobotsPath(startPath);\n if (!drPath) return null;\n\n const modelPath = join(drPath, \"model\");\n return (await fileExists(modelPath)) ? modelPath : null;\n}\n\n/**\n * Get the path to the .dr/ folder (spec reference directory)\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to .dr/ folder, or null if not found\n */\nexport async function getSpecReferencePath(startPath?: string): Promise<string | null> {\n const root = await findProjectRoot(startPath);\n if (!root) return null;\n\n const specPath = join(root, SPEC_FOLDER_NAME);\n return (await fileExists(specPath)) ? specPath : null;\n}\n\n/**\n * Check if currently in a DR project\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns True if documentation-robotics/ folder exists in hierarchy\n */\nexport async function isInDRProject(startPath?: string): Promise<boolean> {\n return (await findProjectRoot(startPath)) !== null;\n}\n\n/**\n * Find the farm root by searching for the farm.yaml file\n *\n * Resolution order:\n * 1. DR_FARM_PATH environment variable (if set, must point to a directory with farm.yaml)\n * 2. Directory walk upward from startPath (default: process.cwd()) up to MAX_SEARCH_DEPTH levels\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to farm root, or null if not found\n *\n * @example\n * // From /home/user/architecture-farm/service-a-model/documentation-robotics/model\n * // Returns: /home/user/architecture-farm (if farm.yaml exists there)\n * const farmRoot = await findFarmRoot();\n *\n * @example\n * // With DR_FARM_PATH environment variable set\n * // Returns: /path/specified/in/env (if farm.yaml exists there)\n * const farmRoot = await findFarmRoot();\n */\nexport async function findFarmRoot(startPath?: string): Promise<string | null> {\n // Check DR_FARM_PATH environment variable first\n if (process.env.DR_FARM_PATH) {\n const envPath = resolve(process.env.DR_FARM_PATH);\n const farmFilePath = join(envPath, FARM_FILE_NAME);\n if (await fileExists(farmFilePath)) {\n return envPath;\n }\n // If DR_FARM_PATH is set but invalid, throw a clear error rather than silently returning null\n throw new Error(\n `Invalid DR_FARM_PATH: ${process.env.DR_FARM_PATH}\\n` +\n `Expected farm.yaml at: ${farmFilePath}\\n` +\n `Ensure DR_FARM_PATH points to a directory containing a farm.yaml file.`\n );\n }\n\n let currentPath = startPath || process.cwd();\n\n for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth++) {\n const farmFilePath = join(currentPath, FARM_FILE_NAME);\n\n if (await fileExists(farmFilePath)) {\n return currentPath;\n }\n\n // Move up one directory\n const parentPath = dirname(currentPath);\n\n // Check if we've reached the root\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return null;\n}\n"],
5
- "mappings": "AAiBA,SAAS,kBAAkB;AAC3B,SAAS,MAAM,SAAS,eAAe;AAEvC,MAAM,mBAAmB,GACnB,sBAAsB,0BACtB,mBAAmB,OACnB,iBAAiB;AAgBvB,eAAsB,gBAAgB,WAAkB;AACtD,MAAI,cAAc,aAAa,QAAQ,IAAG;AAE1C,WAAS,QAAQ,GAAG,QAAQ,kBAAkB,SAAS;AACrD,UAAM,cAAc,KAAK,aAAa,mBAAmB;AAEzD,QAAI,MAAM,WAAW,WAAW;AAC9B,aAAO;AAIT,UAAM,aAAa,QAAQ,WAAW;AAGtC,QAAI,eAAe;AACjB;AAGF,kBAAc;EAChB;AAEA,SAAO;AACT;AAQA,eAAsB,2BAA2B,WAAkB;AACjE,QAAM,OAAO,MAAM,gBAAgB,SAAS;AAC5C,SAAO,OAAO,KAAK,MAAM,mBAAmB,IAAI;AAClD;AAQA,eAAsB,aAAa,WAAkB;AACnD,QAAM,SAAS,MAAM,2BAA2B,SAAS;AACzD,MAAI,CAAC;AAAQ,WAAO;AAEpB,QAAM,YAAY,KAAK,QAAQ,OAAO;AACtC,SAAQ,MAAM,WAAW,SAAS,IAAK,YAAY;AACrD;AAQA,eAAsB,qBAAqB,WAAkB;AAC3D,QAAM,OAAO,MAAM,gBAAgB,SAAS;AAC5C,MAAI,CAAC;AAAM,WAAO;AAElB,QAAM,WAAW,KAAK,MAAM,gBAAgB;AAC5C,SAAQ,MAAM,WAAW,QAAQ,IAAK,WAAW;AACnD;AAQA,eAAsB,cAAc,WAAkB;AACpD,SAAQ,MAAM,gBAAgB,SAAS,MAAO;AAChD;AAsBA,eAAsB,aAAa,WAAkB;AAEnD,MAAI,QAAQ,IAAI,cAAc;AAC5B,UAAM,UAAU,QAAQ,QAAQ,IAAI,YAAY,GAC1C,eAAe,KAAK,SAAS,cAAc;AACjD,QAAI,MAAM,WAAW,YAAY;AAC/B,aAAO;AAGT,UAAM,IAAI,MACR,yBAAyB,QAAQ,IAAI,YAAY;yBACvB,YAAY;uEACkC;EAE5E;AAEA,MAAI,cAAc,aAAa,QAAQ,IAAG;AAE1C,WAAS,QAAQ,GAAG,QAAQ,kBAAkB,SAAS;AACrD,UAAM,eAAe,KAAK,aAAa,cAAc;AAErD,QAAI,MAAM,WAAW,YAAY;AAC/B,aAAO;AAIT,UAAM,aAAa,QAAQ,WAAW;AAGtC,QAAI,eAAe;AACjB;AAGF,kBAAc;EAChB;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["/**\n * Project Path Utilities\n *\n * Handles finding project root and related paths in the DR project structure:\n *\n * my-project/\n * \u251C\u2500\u2500 .dr/ # Reference spec (ephemeral, git-ignored)\n * \u2502 \u251C\u2500\u2500 manifest.json # Installed spec version\n * \u2502 \u2514\u2500\u2500 schemas/\n * \u2514\u2500\u2500 documentation-robotics/ # Project model (permanent, git-committed)\n * \u251C\u2500\u2500 model/\n * \u2502 \u251C\u2500\u2500 manifest.yaml # Model spec version\n * \u2502 \u2514\u2500\u2500 layers/\n * \u251C\u2500\u2500 annotations/\n * \u2514\u2500\u2500 changesets/\n */\n\nimport { fileExists } from \"./file-io.js\";\nimport { join, dirname, resolve } from \"path\";\nimport { stat } from \"fs/promises\";\nimport type { Stats } from \"fs\";\n\nconst MAX_SEARCH_DEPTH = 5;\nconst PROJECT_FOLDER_NAME = \"documentation-robotics\";\nconst SPEC_FOLDER_NAME = \".dr\";\nconst FARM_FILE_NAME = \"farm.yaml\";\n\n/**\n * Find the project root by searching for the documentation-robotics/ folder\n *\n * Searches upward from the current working directory up to MAX_SEARCH_DEPTH levels.\n * Returns the directory containing the documentation_robotics/ folder.\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to project root, or null if not found\n *\n * @example\n * // From /home/user/my-project/documentation-robotics/model/01_motivation\n * // Returns: /home/user/my-project\n * const root = await findProjectRoot();\n */\nexport async function findProjectRoot(startPath?: string): Promise<string | null> {\n let currentPath = startPath || process.cwd();\n\n for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth++) {\n const projectPath = join(currentPath, PROJECT_FOLDER_NAME);\n\n if (await fileExists(projectPath)) {\n return currentPath;\n }\n\n // Move up one directory\n const parentPath = dirname(currentPath);\n\n // Check if we've reached the root\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return null;\n}\n\n/**\n * Get the path to the documentation-robotics/ folder\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to documentation-robotics/ folder, or null if not found\n */\nexport async function getDocumentationRobotsPath(startPath?: string): Promise<string | null> {\n const root = await findProjectRoot(startPath);\n return root ? join(root, PROJECT_FOLDER_NAME) : null;\n}\n\n/**\n * Get the path to the model directory (documentation-robotics/model/)\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to model directory, or null if not found\n */\nexport async function getModelPath(startPath?: string): Promise<string | null> {\n const drPath = await getDocumentationRobotsPath(startPath);\n if (!drPath) return null;\n\n const modelPath = join(drPath, \"model\");\n return (await fileExists(modelPath)) ? modelPath : null;\n}\n\n/**\n * Get the path to the .dr/ folder (spec reference directory)\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to .dr/ folder, or null if not found\n */\nexport async function getSpecReferencePath(startPath?: string): Promise<string | null> {\n const root = await findProjectRoot(startPath);\n if (!root) return null;\n\n const specPath = join(root, SPEC_FOLDER_NAME);\n return (await fileExists(specPath)) ? specPath : null;\n}\n\n/**\n * Check if currently in a DR project\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns True if documentation-robotics/ folder exists in hierarchy\n */\nexport async function isInDRProject(startPath?: string): Promise<boolean> {\n return (await findProjectRoot(startPath)) !== null;\n}\n\n/**\n * Find the farm root by searching for the farm.yaml file\n *\n * Resolution order:\n * 1. DR_FARM_PATH environment variable (if set, must point to a directory with farm.yaml)\n * 2. Directory walk upward from startPath (default: process.cwd()) up to MAX_SEARCH_DEPTH levels\n *\n * @param startPath - Optional starting path (defaults to process.cwd())\n * @returns Path to farm root, or null if not found\n *\n * @example\n * // From /home/user/architecture-farm/service-a-model/documentation-robotics/model\n * // Returns: /home/user/architecture-farm (if farm.yaml exists there)\n * const farmRoot = await findFarmRoot();\n *\n * @example\n * // With DR_FARM_PATH environment variable set\n * // Returns: /path/specified/in/env (if farm.yaml exists there)\n * const farmRoot = await findFarmRoot();\n */\nexport async function findFarmRoot(startPath?: string): Promise<string | null> {\n // Check DR_FARM_PATH environment variable first\n if (process.env.DR_FARM_PATH) {\n const envPath = resolve(process.env.DR_FARM_PATH);\n const farmFilePath = join(envPath, FARM_FILE_NAME);\n if (await fileExists(farmFilePath)) {\n return envPath;\n }\n // If DR_FARM_PATH is set but invalid, throw a clear error rather than silently returning null\n throw new Error(\n `Invalid DR_FARM_PATH: ${process.env.DR_FARM_PATH}\\n` +\n `Expected farm.yaml at: ${farmFilePath}\\n` +\n `Ensure DR_FARM_PATH points to a directory containing a farm.yaml file.`\n );\n }\n\n let currentPath = startPath || process.cwd();\n\n for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth++) {\n const farmFilePath = join(currentPath, FARM_FILE_NAME);\n\n if (await fileExists(farmFilePath)) {\n return currentPath;\n }\n\n // Move up one directory\n const parentPath = dirname(currentPath);\n\n // Check if we've reached the root\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return null;\n}\n\n/**\n * Layouts under which a model directory can be found below a base path.\n * - `standard`: `{base}/documentation-robotics/model/` (a project that self-hosts its model)\n * - `legacy`: `{base}/model/`\n * - `detached`: `{base}` itself holds `manifest.yaml` and the layer folders\n */\nexport type ModelLayout = \"standard\" | \"legacy\" | \"detached\";\n\nexport interface ResolvedModelDirectory {\n /** Absolute path of the directory that holds the model (manifest.yaml and layer folders) */\n modelDir: string;\n layout: ModelLayout;\n /** False when something that is not a directory sits at the resolved path */\n isDirectory: boolean;\n}\n\nconst MODEL_LAYOUT_CANDIDATES: ReadonlyArray<{ layout: ModelLayout; segments: string[] }> = [\n { layout: \"standard\", segments: [PROJECT_FOLDER_NAME, \"model\"] },\n { layout: \"legacy\", segments: [\"model\"] },\n { layout: \"detached\", segments: [] },\n];\n\n/**\n * Locate a model directory under `basePath`, trying the standard, legacy and detached\n * layouts in that order. Shared by the composed reference validator (resolving external\n * models) and `dr farm add` (adopting a codebase's self-hosted model).\n *\n * Only a missing path moves the search on to the next candidate; permission and I/O\n * errors propagate so callers never mistake them for \"no model here\".\n *\n * @param basePath - Project root, model folder or any path to probe\n * @param options.requireManifest - When true, a candidate counts only if it is a directory\n * that contains `manifest.yaml`; otherwise the first existing path wins (even a file, so\n * the caller can report it)\n * @returns The resolved directory, or null when none of the candidates exists\n */\nexport async function resolveModelDirectory(\n basePath: string,\n options: { requireManifest?: boolean } = {}\n): Promise<ResolvedModelDirectory | null> {\n const resolvedBase = resolve(basePath);\n\n for (const candidate of MODEL_LAYOUT_CANDIDATES) {\n const modelDir = join(resolvedBase, ...candidate.segments);\n const stats = await statIfExists(modelDir);\n if (!stats) {\n continue;\n }\n\n if (options.requireManifest) {\n if (!stats.isDirectory() || !(await statIfExists(join(modelDir, \"manifest.yaml\")))) {\n continue;\n }\n }\n\n return { modelDir, layout: candidate.layout, isDirectory: stats.isDirectory() };\n }\n\n return null;\n}\n\nasync function statIfExists(target: string): Promise<Stats | undefined> {\n try {\n return await stat(target);\n } catch (err) {\n if ((err as NodeJS.ErrnoException)?.code === \"ENOENT\") {\n return undefined;\n }\n throw err;\n }\n}\n"],
5
+ "mappings": "AAiBA,SAAS,kBAAkB;AAC3B,SAAS,MAAM,SAAS,eAAe;AACvC,SAAS,YAAY;AAGrB,MAAM,mBAAmB,GACnB,sBAAsB,0BACtB,mBAAmB,OACnB,iBAAiB;AAgBvB,eAAsB,gBAAgB,WAAkB;AACtD,MAAI,cAAc,aAAa,QAAQ,IAAG;AAE1C,WAAS,QAAQ,GAAG,QAAQ,kBAAkB,SAAS;AACrD,UAAM,cAAc,KAAK,aAAa,mBAAmB;AAEzD,QAAI,MAAM,WAAW,WAAW;AAC9B,aAAO;AAIT,UAAM,aAAa,QAAQ,WAAW;AAGtC,QAAI,eAAe;AACjB;AAGF,kBAAc;EAChB;AAEA,SAAO;AACT;AAQA,eAAsB,2BAA2B,WAAkB;AACjE,QAAM,OAAO,MAAM,gBAAgB,SAAS;AAC5C,SAAO,OAAO,KAAK,MAAM,mBAAmB,IAAI;AAClD;AAQA,eAAsB,aAAa,WAAkB;AACnD,QAAM,SAAS,MAAM,2BAA2B,SAAS;AACzD,MAAI,CAAC;AAAQ,WAAO;AAEpB,QAAM,YAAY,KAAK,QAAQ,OAAO;AACtC,SAAQ,MAAM,WAAW,SAAS,IAAK,YAAY;AACrD;AAQA,eAAsB,qBAAqB,WAAkB;AAC3D,QAAM,OAAO,MAAM,gBAAgB,SAAS;AAC5C,MAAI,CAAC;AAAM,WAAO;AAElB,QAAM,WAAW,KAAK,MAAM,gBAAgB;AAC5C,SAAQ,MAAM,WAAW,QAAQ,IAAK,WAAW;AACnD;AAQA,eAAsB,cAAc,WAAkB;AACpD,SAAQ,MAAM,gBAAgB,SAAS,MAAO;AAChD;AAsBA,eAAsB,aAAa,WAAkB;AAEnD,MAAI,QAAQ,IAAI,cAAc;AAC5B,UAAM,UAAU,QAAQ,QAAQ,IAAI,YAAY,GAC1C,eAAe,KAAK,SAAS,cAAc;AACjD,QAAI,MAAM,WAAW,YAAY;AAC/B,aAAO;AAGT,UAAM,IAAI,MACR,yBAAyB,QAAQ,IAAI,YAAY;yBACvB,YAAY;uEACkC;EAE5E;AAEA,MAAI,cAAc,aAAa,QAAQ,IAAG;AAE1C,WAAS,QAAQ,GAAG,QAAQ,kBAAkB,SAAS;AACrD,UAAM,eAAe,KAAK,aAAa,cAAc;AAErD,QAAI,MAAM,WAAW,YAAY;AAC/B,aAAO;AAIT,UAAM,aAAa,QAAQ,WAAW;AAGtC,QAAI,eAAe;AACjB;AAGF,kBAAc;EAChB;AAEA,SAAO;AACT;AAkBA,MAAM,0BAAsF;EAC1F,EAAE,QAAQ,YAAY,UAAU,CAAC,qBAAqB,OAAO,EAAC;EAC9D,EAAE,QAAQ,UAAU,UAAU,CAAC,OAAO,EAAC;EACvC,EAAE,QAAQ,YAAY,UAAU,CAAA,EAAE;;AAiBpC,eAAsB,sBACpB,UACA,UAAyC,CAAA,GAAE;AAE3C,QAAM,eAAe,QAAQ,QAAQ;AAErC,aAAW,aAAa,yBAAyB;AAC/C,UAAM,WAAW,KAAK,cAAc,GAAG,UAAU,QAAQ,GACnD,QAAQ,MAAM,aAAa,QAAQ;AACzC,QAAK,SAID,UAAQ,oBACN,CAAC,MAAM,YAAW,KAAM,CAAE,MAAM,aAAa,KAAK,UAAU,eAAe,CAAC;AAKlF,aAAO,EAAE,UAAU,QAAQ,UAAU,QAAQ,aAAa,MAAM,YAAW,EAAE;EAC/E;AAEA,SAAO;AACT;AAEA,eAAe,aAAa,QAAc;AACxC,MAAI;AACF,WAAO,MAAM,KAAK,MAAM;EAC1B,SAAS,KAAK;AACZ,QAAK,KAA+B,SAAS;AAC3C;AAEF,UAAM;EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,46 +1,76 @@
1
1
  import { CLIError } from "./errors.js";
2
- function validateSourceReferenceOptions(options) {
3
- if (!(options.sourceFile || options.sourceSymbol || options.sourceProvenance || options.sourceRepoRemote || options.sourceRepoCommit)) {
4
- process.stderr.isTTY && console.warn("Warning: no source provenance specified. Add --source-file and --source-provenance to maintain traceability for drift detection and /dr-sync workflows.");
5
- return;
6
- }
7
- if (options.sourceSymbol && !options.sourceFile)
8
- throw new CLIError("--source-symbol requires --source-file", 1, [
9
- "Provide the source file that contains the symbol",
10
- 'Example: --source-file "src/auth.ts" --source-symbol "validateToken"'
11
- ]);
12
- if (!options.sourceProvenance)
13
- throw new CLIError("--source-provenance is required when specifying source reference", 1, [
14
- "Specify provenance type: extracted, manual, inferred, or generated"
15
- ]);
16
- const fileOptionalProvenances = ["inferred", "generated"];
17
- if (!options.sourceFile && !fileOptionalProvenances.includes(options.sourceProvenance))
18
- throw new CLIError("--source-file is required when specifying source reference", 1, [
19
- "Provide source file path relative to repository root",
20
- 'Example: --source-file "src/services/auth.ts"',
21
- "Note: --source-file is optional when --source-provenance is 'inferred' or 'generated'"
2
+ import { optionExample, optionName } from "./error-hints.js";
3
+ const VALID_PROVENANCE = ["extracted", "manual", "inferred", "generated"], FILE_REQUIRED_PROVENANCE = ["extracted", "manual"], DEFAULT_PROVENANCE = "manual";
4
+ function hasSourceOptions(options) {
5
+ return !!(options.sourceFile || options.sourceSymbol || options.sourceProvenance || options.sourceRepoRemote || options.sourceRepoCommit);
6
+ }
7
+ function referenceFiles(reference) {
8
+ return [...new Set((reference?.locations ?? []).map((location) => location.file).filter(Boolean))];
9
+ }
10
+ function symbolOnExistingFile(options, surface, existing) {
11
+ const name = (option) => optionName(surface, option), example = (option, value) => optionExample(surface, option, value), files = referenceFiles(existing);
12
+ if (files.length === 0)
13
+ throw new CLIError(`${name("sourceSymbol")} needs ${name("sourceFile")}: a symbol names something inside a file, and the element has no source file to put it on`, 1, [
14
+ "Send the source file that contains the symbol with it",
15
+ `Example: ${example("sourceFile", "src/auth.ts")} ${example("sourceSymbol", "validateToken")}`
22
16
  ]);
23
- const validProvenance = ["extracted", "manual", "inferred", "generated"];
24
- if (!validProvenance.includes(options.sourceProvenance))
25
- throw new CLIError(`Invalid --source-provenance value: "${options.sourceProvenance}"`, 1, [
26
- `Must be one of: ${validProvenance.join(", ")}`
17
+ if (files.length > 1)
18
+ throw new CLIError(`${name("sourceSymbol")} alone is ambiguous: this element has ${files.length} source files (${files.join(", ")})`, 1, [`Send ${name("sourceFile")} with it to say which file holds the symbol`]);
19
+ const file = files[0], symbol = options.sourceSymbol, current = existing?.locations ?? [], settledLocations = current.length === 1 && !current[0].symbol ? [{ ...current[0], symbol }] : current.some((location) => location.symbol === symbol) ? current.map((location) => ({ ...location })) : [...current.map((location) => ({ ...location })), { file, symbol }], repository = existing?.repository, keepRepository = !options.sourceRepoRemote && !options.sourceRepoCommit && !!repository?.url && !!repository?.commit;
20
+ return {
21
+ ...options,
22
+ sourceFile: file,
23
+ settledLocations,
24
+ sourceProvenance: options.sourceProvenance ?? existing?.provenance ?? DEFAULT_PROVENANCE,
25
+ ...keepRepository ? { sourceRepoRemote: repository.url, sourceRepoCommit: repository.commit } : {}
26
+ };
27
+ }
28
+ function precheckUpdateSourceReferenceOptions(options, surface = "cli") {
29
+ resolveSourceReferenceOptions(options.sourceFile ? options : { ...options, sourceSymbol: void 0 }, surface);
30
+ }
31
+ function resolveSourceReferenceOptions(options, surface = "cli", existing) {
32
+ const name = (option) => optionName(surface, option), example = (option, value) => optionExample(surface, option, value), warnings = [];
33
+ if (!hasSourceOptions(options))
34
+ return { options, warnings };
35
+ if (options.sourceProvenance && !VALID_PROVENANCE.includes(options.sourceProvenance))
36
+ throw new CLIError(`Invalid ${name("sourceProvenance")} value: "${options.sourceProvenance}"`, 1, [
37
+ `Must be one of: ${VALID_PROVENANCE.join(", ")}`
27
38
  ]);
28
- if (options.sourceRepoCommit && !/^[0-9a-f]{40}$/.test(options.sourceRepoCommit))
29
- throw new CLIError(`Invalid --source-repo-commit value: "${options.sourceRepoCommit}"`, 1, [
39
+ if (options.sourceSymbol && !options.sourceFile && (options = symbolOnExistingFile(options, surface, existing)), options.sourceRepoCommit && !/^[0-9a-f]{40}$/.test(options.sourceRepoCommit))
40
+ throw new CLIError(`Invalid ${name("sourceRepoCommit")} value: "${options.sourceRepoCommit}"`, 1, [
30
41
  "Must be exactly 40 hexadecimal characters (full Git commit SHA)"
31
42
  ]);
32
43
  if (options.sourceRepoRemote && !options.sourceRepoCommit)
33
- throw new CLIError("--source-repo-commit is required when --source-repo-remote is provided", 1, ["Provide both remote URL and commit SHA for repository context"]);
44
+ throw new CLIError(`${name("sourceRepoCommit")} is required when ${name("sourceRepoRemote")} is provided`, 1, ["Provide both remote URL and commit SHA for repository context"]);
34
45
  if (options.sourceRepoCommit && !options.sourceRepoRemote)
35
- throw new CLIError("--source-repo-remote is required when --source-repo-commit is provided", 1, ["Provide both remote URL and commit SHA for repository context"]);
46
+ throw new CLIError(`${name("sourceRepoRemote")} is required when ${name("sourceRepoCommit")} is provided`, 1, ["Provide both remote URL and commit SHA for repository context"]);
47
+ if (!options.sourceProvenance) {
48
+ if (!options.sourceFile)
49
+ throw new CLIError(`${name("sourceRepoRemote")} and ${name("sourceRepoCommit")} need a source file`, 1, [
50
+ `Send ${name("sourceFile")} with them, e.g. ${example("sourceFile", "src/services/auth.ts")}`
51
+ ]);
52
+ const provenance = existing?.provenance ?? DEFAULT_PROVENANCE;
53
+ return warnings.push(`${name("sourceProvenance")} was not given; used '${provenance}'` + (existing?.provenance ? " (the element's current provenance)." : ".")), { options: { ...options, sourceProvenance: provenance }, warnings };
54
+ }
55
+ return !(!!options.sourceFile || (existing?.locations?.length ?? 0) > 0) && FILE_REQUIRED_PROVENANCE.includes(options.sourceProvenance) ? (warnings.push(`${name("sourceProvenance")} '${options.sourceProvenance}' was ignored: it needs a ${name("sourceFile")}, so no source reference was recorded. For an element with no code yet, send no source fields (or ${example("sourceProvenance", "inferred")}); otherwise send ${name("sourceFile")} with it.`), { options: {}, warnings }) : { options, warnings };
56
+ }
57
+ function validateSourceReferenceOptions(options, surface = "cli") {
58
+ if (!hasSourceOptions(options)) {
59
+ if (process.stderr.isTTY) {
60
+ const name = (option) => optionName(surface, option);
61
+ console.warn(`Warning: no source provenance specified. Add ${name("sourceFile")} and ${name("sourceProvenance")} to maintain traceability for drift detection and /dr-sync workflows.`);
62
+ }
63
+ return;
64
+ }
65
+ resolveSourceReferenceOptions(options, surface);
36
66
  }
37
- function buildSourceReference(options) {
67
+ function buildSourceReference(options, existing) {
38
68
  if (!options.sourceProvenance)
39
69
  return;
40
70
  const reference = {
41
71
  provenance: options.sourceProvenance
42
72
  };
43
- return options.sourceFile && (reference.locations = [
73
+ return options.settledLocations ? reference.locations = options.settledLocations : options.sourceFile && (reference.locations = [
44
74
  {
45
75
  file: options.sourceFile,
46
76
  ...options.sourceSymbol && { symbol: options.sourceSymbol }
@@ -48,10 +78,13 @@ function buildSourceReference(options) {
48
78
  ]), options.sourceRepoRemote && options.sourceRepoCommit && (reference.repository = {
49
79
  url: options.sourceRepoRemote,
50
80
  commit: options.sourceRepoCommit
51
- }), reference;
81
+ }), !reference.locations && existing?.locations?.length && (reference.locations = existing.locations, !reference.repository && existing.repository && (reference.repository = existing.repository)), reference;
52
82
  }
53
83
  export {
84
+ DEFAULT_PROVENANCE,
54
85
  buildSourceReference,
86
+ precheckUpdateSourceReferenceOptions,
87
+ resolveSourceReferenceOptions,
55
88
  validateSourceReferenceOptions
56
89
  };
57
90
  //# sourceMappingURL=source-reference.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/source-reference.ts"],
4
- "sourcesContent": ["/**\n * Source Reference Utilities\n * Shared validation and builder functions for source reference options\n */\n\nimport type { SourceReference, ProvenanceType } from \"../types/index.js\";\nimport { CLIError } from \"./errors.js\";\n\nexport interface SourceReferenceOptions {\n sourceFile?: string;\n sourceSymbol?: string;\n sourceProvenance?: string;\n sourceRepoRemote?: string;\n sourceRepoCommit?: string;\n}\n\n/**\n * Validate source reference options\n */\nexport function validateSourceReferenceOptions(options: SourceReferenceOptions): void {\n const hasSourceOptions =\n options.sourceFile ||\n options.sourceSymbol ||\n options.sourceProvenance ||\n options.sourceRepoRemote ||\n options.sourceRepoCommit;\n\n if (!hasSourceOptions) {\n if (process.stderr.isTTY) {\n console.warn(\n \"Warning: no source provenance specified. Add --source-file and --source-provenance to maintain traceability for drift detection and /dr-sync workflows.\"\n );\n }\n return;\n }\n\n // Require source-symbol to have source-file (both or neither is not acceptable here)\n if (options.sourceSymbol && !options.sourceFile) {\n throw new CLIError(\"--source-symbol requires --source-file\", 1, [\n \"Provide the source file that contains the symbol\",\n 'Example: --source-file \"src/auth.ts\" --source-symbol \"validateToken\"',\n ]);\n }\n\n // Require provenance first so we can use it to determine file requirements\n if (!options.sourceProvenance) {\n throw new CLIError(\"--source-provenance is required when specifying source reference\", 1, [\n \"Specify provenance type: extracted, manual, inferred, or generated\",\n ]);\n }\n\n // For inferred and generated provenance, source file is optional (no code exists yet)\n const fileOptionalProvenances = [\"inferred\", \"generated\"];\n if (!options.sourceFile && !fileOptionalProvenances.includes(options.sourceProvenance)) {\n throw new CLIError(\"--source-file is required when specifying source reference\", 1, [\n \"Provide source file path relative to repository root\",\n 'Example: --source-file \"src/services/auth.ts\"',\n \"Note: --source-file is optional when --source-provenance is 'inferred' or 'generated'\",\n ]);\n }\n\n // Validate provenance enum\n const validProvenance = [\"extracted\", \"manual\", \"inferred\", \"generated\"];\n if (!validProvenance.includes(options.sourceProvenance)) {\n throw new CLIError(`Invalid --source-provenance value: \"${options.sourceProvenance}\"`, 1, [\n `Must be one of: ${validProvenance.join(\", \")}`,\n ]);\n }\n\n // Validate commit SHA format if provided\n if (options.sourceRepoCommit && !/^[0-9a-f]{40}$/.test(options.sourceRepoCommit)) {\n throw new CLIError(`Invalid --source-repo-commit value: \"${options.sourceRepoCommit}\"`, 1, [\n \"Must be exactly 40 hexadecimal characters (full Git commit SHA)\",\n ]);\n }\n\n // Require both or neither repo context fields\n if (options.sourceRepoRemote && !options.sourceRepoCommit) {\n throw new CLIError(\n \"--source-repo-commit is required when --source-repo-remote is provided\",\n 1,\n [\"Provide both remote URL and commit SHA for repository context\"]\n );\n }\n\n if (options.sourceRepoCommit && !options.sourceRepoRemote) {\n throw new CLIError(\n \"--source-repo-remote is required when --source-repo-commit is provided\",\n 1,\n [\"Provide both remote URL and commit SHA for repository context\"]\n );\n }\n}\n\n/**\n * Build SourceReference object from options\n */\nexport function buildSourceReference(options: SourceReferenceOptions): SourceReference | undefined {\n if (!options.sourceProvenance) {\n return undefined;\n }\n\n const reference: SourceReference = {\n provenance: options.sourceProvenance as ProvenanceType,\n };\n\n if (options.sourceFile) {\n reference.locations = [\n {\n file: options.sourceFile,\n ...(options.sourceSymbol && { symbol: options.sourceSymbol }),\n },\n ];\n }\n\n if (options.sourceRepoRemote && options.sourceRepoCommit) {\n reference.repository = {\n url: options.sourceRepoRemote,\n commit: options.sourceRepoCommit,\n };\n }\n\n return reference;\n}\n"],
5
- "mappings": "AAMA,SAAS,gBAAgB;AAanB,SAAU,+BAA+B,SAA+B;AAQ5E,MAAI,EANF,QAAQ,cACR,QAAQ,gBACR,QAAQ,oBACR,QAAQ,oBACR,QAAQ,mBAEa;AACrB,IAAI,QAAQ,OAAO,SACjB,QAAQ,KACN,yJAAyJ;AAG7J;EACF;AAGA,MAAI,QAAQ,gBAAgB,CAAC,QAAQ;AACnC,UAAM,IAAI,SAAS,0CAA0C,GAAG;MAC9D;MACA;KACD;AAIH,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,SAAS,oEAAoE,GAAG;MACxF;KACD;AAIH,QAAM,0BAA0B,CAAC,YAAY,WAAW;AACxD,MAAI,CAAC,QAAQ,cAAc,CAAC,wBAAwB,SAAS,QAAQ,gBAAgB;AACnF,UAAM,IAAI,SAAS,8DAA8D,GAAG;MAClF;MACA;MACA;KACD;AAIH,QAAM,kBAAkB,CAAC,aAAa,UAAU,YAAY,WAAW;AACvE,MAAI,CAAC,gBAAgB,SAAS,QAAQ,gBAAgB;AACpD,UAAM,IAAI,SAAS,uCAAuC,QAAQ,gBAAgB,KAAK,GAAG;MACxF,mBAAmB,gBAAgB,KAAK,IAAI,CAAC;KAC9C;AAIH,MAAI,QAAQ,oBAAoB,CAAC,iBAAiB,KAAK,QAAQ,gBAAgB;AAC7E,UAAM,IAAI,SAAS,wCAAwC,QAAQ,gBAAgB,KAAK,GAAG;MACzF;KACD;AAIH,MAAI,QAAQ,oBAAoB,CAAC,QAAQ;AACvC,UAAM,IAAI,SACR,0EACA,GACA,CAAC,+DAA+D,CAAC;AAIrE,MAAI,QAAQ,oBAAoB,CAAC,QAAQ;AACvC,UAAM,IAAI,SACR,0EACA,GACA,CAAC,+DAA+D,CAAC;AAGvE;AAKM,SAAU,qBAAqB,SAA+B;AAClE,MAAI,CAAC,QAAQ;AACX;AAGF,QAAM,YAA6B;IACjC,YAAY,QAAQ;;AAGtB,SAAI,QAAQ,eACV,UAAU,YAAY;IACpB;MACE,MAAM,QAAQ;MACd,GAAI,QAAQ,gBAAgB,EAAE,QAAQ,QAAQ,aAAY;;MAK5D,QAAQ,oBAAoB,QAAQ,qBACtC,UAAU,aAAa;IACrB,KAAK,QAAQ;IACb,QAAQ,QAAQ;MAIb;AACT;",
4
+ "sourcesContent": ["/**\n * Source Reference Utilities\n * Shared validation and builder functions for source reference options\n */\n\nimport type { SourceReference, SourceLocation, ProvenanceType } from \"../types/index.js\";\nimport { CLIError } from \"./errors.js\";\nimport { optionExample, optionName, type HintOption, type HintSurface } from \"./error-hints.js\";\n\nexport interface SourceReferenceOptions {\n sourceFile?: string;\n sourceSymbol?: string;\n sourceProvenance?: string;\n sourceRepoRemote?: string;\n sourceRepoCommit?: string;\n /**\n * Set only by {@link resolveSourceReferenceOptions} for a symbol sent without a file: the\n * element's locations with the symbol added. {@link buildSourceReference} uses them as they are.\n */\n settledLocations?: SourceLocation[];\n}\n\nconst VALID_PROVENANCE = [\"extracted\", \"manual\", \"inferred\", \"generated\"];\n\n/** Provenances the source-references schema documents as needing a location (a file). */\nconst FILE_REQUIRED_PROVENANCE = [\"extracted\", \"manual\"];\n\n/** The provenance given to a source file sent without one. The schema has no `planned` value. */\nexport const DEFAULT_PROVENANCE = \"manual\";\n\nexport interface ResolvedSourceReferenceOptions {\n /** The options to build the reference from: provenance filled in, or the source fields dropped. */\n options: SourceReferenceOptions;\n /** What was defaulted or ignored, to show the caller. */\n warnings: string[];\n}\n\nfunction hasSourceOptions(options: SourceReferenceOptions): boolean {\n return !!(\n options.sourceFile ||\n options.sourceSymbol ||\n options.sourceProvenance ||\n options.sourceRepoRemote ||\n options.sourceRepoCommit\n );\n}\n\n/** The distinct files an element's source reference names, in order. */\nfunction referenceFiles(reference?: SourceReference): string[] {\n return [...new Set((reference?.locations ?? []).map((location) => location.file).filter(Boolean))];\n}\n\n/**\n * Settle a symbol sent without a file, on the element's one source file, keeping its other\n * locations, its provenance and its repository unless the call names them:\n *\n * - one location with no symbol: the symbol goes on it;\n * - otherwise the file holds other symbols: `{file, symbol}` is appended, unless it is there already.\n *\n * Throws when there is no file to keep (always, on add) or more than one to choose from.\n */\nfunction symbolOnExistingFile(\n options: SourceReferenceOptions,\n surface: HintSurface,\n existing?: SourceReference\n): SourceReferenceOptions {\n const name = (option: HintOption) => optionName(surface, option);\n const example = (option: HintOption, value: string) => optionExample(surface, option, value);\n const files = referenceFiles(existing);\n\n if (files.length === 0) {\n throw new CLIError(\n `${name(\"sourceSymbol\")} needs ${name(\"sourceFile\")}: a symbol names something inside a file, and the ` +\n `element has no source file to put it on`,\n 1,\n [\n \"Send the source file that contains the symbol with it\",\n `Example: ${example(\"sourceFile\", \"src/auth.ts\")} ${example(\"sourceSymbol\", \"validateToken\")}`,\n ]\n );\n }\n if (files.length > 1) {\n throw new CLIError(\n `${name(\"sourceSymbol\")} alone is ambiguous: this element has ${files.length} source files (${files.join(\", \")})`,\n 1,\n [`Send ${name(\"sourceFile\")} with it to say which file holds the symbol`]\n );\n }\n\n const file = files[0];\n const symbol = options.sourceSymbol!;\n const current = existing?.locations ?? [];\n const settledLocations: SourceLocation[] =\n current.length === 1 && !current[0].symbol\n ? [{ ...current[0], symbol }]\n : current.some((location) => location.symbol === symbol)\n ? current.map((location) => ({ ...location }))\n : [...current.map((location) => ({ ...location })), { file, symbol }];\n\n const repository = existing?.repository;\n const keepRepository =\n !options.sourceRepoRemote && !options.sourceRepoCommit && !!repository?.url && !!repository?.commit;\n return {\n ...options,\n sourceFile: file,\n settledLocations,\n sourceProvenance: options.sourceProvenance ?? existing?.provenance ?? DEFAULT_PROVENANCE,\n ...(keepRepository ? { sourceRepoRemote: repository!.url, sourceRepoCommit: repository!.commit } : {}),\n };\n}\n\n/**\n * Refuse, before the element is loaded, the source options no element could settle. A symbol sent\n * without a file is left to {@link resolveSourceReferenceOptions} with the element's reference: on\n * update it can go on the element's own file.\n */\nexport function precheckUpdateSourceReferenceOptions(\n options: SourceReferenceOptions,\n surface: HintSurface = \"cli\"\n): void {\n resolveSourceReferenceOptions(options.sourceFile ? options : { ...options, sourceSymbol: undefined }, surface);\n}\n\n/**\n * Check source reference options and settle the two partial forms instead of refusing them:\n *\n * - A file without a provenance gets the element's current provenance (`existing`, on update),\n * else `manual`.\n * - A symbol without a file goes on the element's one source file (`existing`, on update), keeping\n * its provenance and repository.\n * - An `extracted` or `manual` provenance without a file (and no file on the element already) is\n * ignored with a warning: the schema documents a location as required for those two. `inferred`\n * and `generated` need no file.\n *\n * Throws for what cannot be settled: an unknown provenance, a symbol with no file sent or kept, a bad commit\n * SHA, or repository fields without a file or with one of the pair missing.\n */\nexport function resolveSourceReferenceOptions(\n options: SourceReferenceOptions,\n surface: HintSurface = \"cli\",\n existing?: SourceReference\n): ResolvedSourceReferenceOptions {\n const name = (option: HintOption) => optionName(surface, option);\n const example = (option: HintOption, value: string) => optionExample(surface, option, value);\n const warnings: string[] = [];\n\n if (!hasSourceOptions(options)) {\n return { options, warnings };\n }\n\n if (options.sourceProvenance && !VALID_PROVENANCE.includes(options.sourceProvenance)) {\n throw new CLIError(`Invalid ${name(\"sourceProvenance\")} value: \"${options.sourceProvenance}\"`, 1, [\n `Must be one of: ${VALID_PROVENANCE.join(\", \")}`,\n ]);\n }\n\n // A symbol names something inside a file. Sent alone on update, it goes on the element's own\n // file, keeping the provenance and repository; with no file to keep, the file must be sent.\n if (options.sourceSymbol && !options.sourceFile) {\n options = symbolOnExistingFile(options, surface, existing);\n }\n\n // Validate commit SHA format if provided\n if (options.sourceRepoCommit && !/^[0-9a-f]{40}$/.test(options.sourceRepoCommit)) {\n throw new CLIError(`Invalid ${name(\"sourceRepoCommit\")} value: \"${options.sourceRepoCommit}\"`, 1, [\n \"Must be exactly 40 hexadecimal characters (full Git commit SHA)\",\n ]);\n }\n\n // Require both or neither repo context fields\n if (options.sourceRepoRemote && !options.sourceRepoCommit) {\n throw new CLIError(\n `${name(\"sourceRepoCommit\")} is required when ${name(\"sourceRepoRemote\")} is provided`,\n 1,\n [\"Provide both remote URL and commit SHA for repository context\"]\n );\n }\n\n if (options.sourceRepoCommit && !options.sourceRepoRemote) {\n throw new CLIError(\n `${name(\"sourceRepoRemote\")} is required when ${name(\"sourceRepoCommit\")} is provided`,\n 1,\n [\"Provide both remote URL and commit SHA for repository context\"]\n );\n }\n\n if (!options.sourceProvenance) {\n if (!options.sourceFile) {\n // Only repository fields: there is no file to attach them to.\n throw new CLIError(`${name(\"sourceRepoRemote\")} and ${name(\"sourceRepoCommit\")} need a source file`, 1, [\n `Send ${name(\"sourceFile\")} with them, e.g. ${example(\"sourceFile\", \"src/services/auth.ts\")}`,\n ]);\n }\n const provenance = existing?.provenance ?? DEFAULT_PROVENANCE;\n warnings.push(\n `${name(\"sourceProvenance\")} was not given; used '${provenance}'` +\n (existing?.provenance ? \" (the element's current provenance).\" : \".\")\n );\n return { options: { ...options, sourceProvenance: provenance }, warnings };\n }\n\n const hasFile = !!options.sourceFile || (existing?.locations?.length ?? 0) > 0;\n if (!hasFile && FILE_REQUIRED_PROVENANCE.includes(options.sourceProvenance)) {\n warnings.push(\n `${name(\"sourceProvenance\")} '${options.sourceProvenance}' was ignored: it needs a ${name(\"sourceFile\")}, so no ` +\n `source reference was recorded. For an element with no code yet, send no source fields (or ` +\n `${example(\"sourceProvenance\", \"inferred\")}); otherwise send ${name(\"sourceFile\")} with it.`\n );\n return { options: {}, warnings };\n }\n\n return { options, warnings };\n}\n\n/**\n * Validate source reference options. Throws where {@link resolveSourceReferenceOptions} throws;\n * the partial forms it settles pass.\n */\nexport function validateSourceReferenceOptions(\n options: SourceReferenceOptions,\n surface: HintSurface = \"cli\"\n): void {\n if (!hasSourceOptions(options)) {\n if (process.stderr.isTTY) {\n const name = (option: HintOption) => optionName(surface, option);\n console.warn(\n `Warning: no source provenance specified. Add ${name(\"sourceFile\")} and ${name(\"sourceProvenance\")} to maintain traceability for drift detection and /dr-sync workflows.`\n );\n }\n return;\n }\n resolveSourceReferenceOptions(options, surface);\n}\n\n/**\n * Build SourceReference object from options. On update, pass the element's current reference as\n * `existing`: a change that names no file keeps its locations (and its repository with them).\n */\nexport function buildSourceReference(\n options: SourceReferenceOptions,\n existing?: SourceReference\n): SourceReference | undefined {\n if (!options.sourceProvenance) {\n return undefined;\n }\n\n const reference: SourceReference = {\n provenance: options.sourceProvenance as ProvenanceType,\n };\n\n if (options.settledLocations) {\n reference.locations = options.settledLocations;\n } else if (options.sourceFile) {\n reference.locations = [\n {\n file: options.sourceFile,\n ...(options.sourceSymbol && { symbol: options.sourceSymbol }),\n },\n ];\n }\n\n if (options.sourceRepoRemote && options.sourceRepoCommit) {\n reference.repository = {\n url: options.sourceRepoRemote,\n commit: options.sourceRepoCommit,\n };\n }\n\n if (!reference.locations && existing?.locations?.length) {\n reference.locations = existing.locations;\n if (!reference.repository && existing.repository) reference.repository = existing.repository;\n }\n\n return reference;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { readJSON, fileExists } from "./file-io.js";
2
2
  import { join } from "path";
3
3
  import { createRequire } from "node:module";
4
- const BUNDLED_SPEC_VERSION = "0.10.0";
4
+ const BUNDLED_SPEC_VERSION = "0.12.0";
5
5
  function resolveDevVersion() {
6
6
  try {
7
7
  return createRequire(import.meta.url)("../../package.json").version ?? "0.0.0";
@@ -10,7 +10,7 @@ function resolveDevVersion() {
10
10
  }
11
11
  }
12
12
  function getCliVersion() {
13
- return "0.2.0";
13
+ return "0.4.0";
14
14
  }
15
15
  function getCliBundledSpecVersion() {
16
16
  return BUNDLED_SPEC_VERSION;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/spec-version.ts"],
4
- "sourcesContent": ["/**\n * Spec Version Utilities\n *\n * Manages spec version information for the CLI\n */\n\nimport { readJSON, fileExists } from \"./file-io.js\";\nimport { join } from \"path\";\nimport { createRequire } from \"node:module\";\n\n// Declare build-time constant (substituted by esbuild)\ndeclare const CLI_VERSION: string;\n\n/**\n * Bundled spec version\n * This should match the version in ../../../spec/VERSION\n * Updated during build process\n */\nconst BUNDLED_SPEC_VERSION = \"0.10.0\";\n\n// Dev-mode fallback (e.g. `tsx src/cli.ts`) that reads package.json directly, so it can't drift.\nfunction resolveDevVersion(): string {\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../../package.json\") as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch (error) {\n console.error(\n `Failed to resolve dev-mode CLI version from package.json, falling back to \"0.0.0\": ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n return \"0.0.0\";\n }\n}\n\n/**\n * Get the CLI version\n *\n * @returns CLI version string\n */\nexport function getCliVersion(): string {\n return typeof CLI_VERSION !== \"undefined\" ? CLI_VERSION : resolveDevVersion();\n}\n\n/**\n * Get the CLI's bundled spec version\n *\n * This is the version of the DR specification that is bundled with this CLI.\n * It represents what version of the spec this CLI supports.\n *\n * @returns Bundled spec version string\n */\nexport function getCliBundledSpecVersion(): string {\n return BUNDLED_SPEC_VERSION;\n}\n\n/**\n * Read spec version from .dr/manifest.json (installed spec reference)\n *\n * @param drPath - Path to .dr/ directory\n * @returns Spec version from .dr/manifest.json, or null if not found\n */\nexport async function getInstalledSpecVersion(drPath: string): Promise<string | null> {\n const manifestPath = join(drPath, \"manifest.json\");\n\n if (!(await fileExists(manifestPath))) {\n return null;\n }\n\n try {\n const manifest = await readJSON<{ specVersion?: string }>(manifestPath);\n return manifest.specVersion || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Read spec version from documentation_robotics/model/manifest.yaml\n *\n * @param modelPath - Path to model directory\n * @returns Spec version from model manifest, or null if not found\n */\nexport async function getModelSpecVersion(modelPath: string): Promise<string | null> {\n const manifestPath = join(modelPath, \"manifest.yaml\");\n\n if (!(await fileExists(manifestPath))) {\n return null;\n }\n\n try {\n // Use YAML parser\n const yaml = await import(\"yaml\");\n const fs = await import(\"fs/promises\");\n const content = await fs.readFile(manifestPath, \"utf-8\");\n const manifest = yaml.parse(content) as { spec_version?: string; specVersion?: string };\n // Check snake_case first (Python CLI format), then camelCase (legacy)\n return manifest.spec_version || manifest.specVersion || null;\n } catch {\n return null;\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * Spec Version Utilities\n *\n * Manages spec version information for the CLI\n */\n\nimport { readJSON, fileExists } from \"./file-io.js\";\nimport { join } from \"path\";\nimport { createRequire } from \"node:module\";\n\n// Declare build-time constant (substituted by esbuild)\ndeclare const CLI_VERSION: string;\n\n/**\n * Bundled spec version\n * This should match the version in ../../../spec/VERSION\n * Updated during build process\n */\nconst BUNDLED_SPEC_VERSION = \"0.12.0\";\n\n// Dev-mode fallback (e.g. `tsx src/cli.ts`) that reads package.json directly, so it can't drift.\nfunction resolveDevVersion(): string {\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../../package.json\") as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch (error) {\n console.error(\n `Failed to resolve dev-mode CLI version from package.json, falling back to \"0.0.0\": ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n return \"0.0.0\";\n }\n}\n\n/**\n * Get the CLI version\n *\n * @returns CLI version string\n */\nexport function getCliVersion(): string {\n return typeof CLI_VERSION !== \"undefined\" ? CLI_VERSION : resolveDevVersion();\n}\n\n/**\n * Get the CLI's bundled spec version\n *\n * This is the version of the DR specification that is bundled with this CLI.\n * It represents what version of the spec this CLI supports.\n *\n * @returns Bundled spec version string\n */\nexport function getCliBundledSpecVersion(): string {\n return BUNDLED_SPEC_VERSION;\n}\n\n/**\n * Read spec version from .dr/manifest.json (installed spec reference)\n *\n * @param drPath - Path to .dr/ directory\n * @returns Spec version from .dr/manifest.json, or null if not found\n */\nexport async function getInstalledSpecVersion(drPath: string): Promise<string | null> {\n const manifestPath = join(drPath, \"manifest.json\");\n\n if (!(await fileExists(manifestPath))) {\n return null;\n }\n\n try {\n const manifest = await readJSON<{ specVersion?: string }>(manifestPath);\n return manifest.specVersion || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Read spec version from documentation_robotics/model/manifest.yaml\n *\n * @param modelPath - Path to model directory\n * @returns Spec version from model manifest, or null if not found\n */\nexport async function getModelSpecVersion(modelPath: string): Promise<string | null> {\n const manifestPath = join(modelPath, \"manifest.yaml\");\n\n if (!(await fileExists(manifestPath))) {\n return null;\n }\n\n try {\n // Use YAML parser\n const yaml = await import(\"yaml\");\n const fs = await import(\"fs/promises\");\n const content = await fs.readFile(manifestPath, \"utf-8\");\n const manifest = yaml.parse(content) as { spec_version?: string; specVersion?: string };\n // Check snake_case first (Python CLI format), then camelCase (legacy)\n return manifest.spec_version || manifest.specVersion || null;\n } catch {\n return null;\n }\n}\n"],
5
5
  "mappings": "AAMA,SAAS,UAAU,kBAAkB;AACrC,SAAS,YAAY;AACrB,SAAS,qBAAqB;AAU9B,MAAM,uBAAuB;AAG7B,SAAS,oBAAiB;AACxB,MAAI;AAGF,WAFgB,cAAc,YAAY,GAAG,EACzB,oBAAoB,EAC7B,WAAW;EACxB,SAAS,OAAO;AACd,mBAAQ,MACN,sFACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD,EAAE,GAEG;EACT;AACF;AAOM,SAAU,gBAAa;AAC3B,SAA4C;AAC9C;AAUM,SAAU,2BAAwB;AACtC,SAAO;AACT;AAQA,eAAsB,wBAAwB,QAAc;AAC1D,QAAM,eAAe,KAAK,QAAQ,eAAe;AAEjD,MAAI,CAAE,MAAM,WAAW,YAAY;AACjC,WAAO;AAGT,MAAI;AAEF,YADiB,MAAM,SAAmC,YAAY,GACtD,eAAe;EACjC,QAAQ;AACN,WAAO;EACT;AACF;AAQA,eAAsB,oBAAoB,WAAiB;AACzD,QAAM,eAAe,KAAK,WAAW,eAAe;AAEpD,MAAI,CAAE,MAAM,WAAW,YAAY;AACjC,WAAO;AAGT,MAAI;AAEF,UAAM,OAAO,MAAM,OAAO,MAAM,GAE1B,UAAU,OADL,MAAM,OAAO,aAAa,GACZ,SAAS,cAAc,OAAO,GACjD,WAAW,KAAK,MAAM,OAAO;AAEnC,WAAO,SAAS,gBAAgB,SAAS,eAAe;EAC1D,QAAQ;AACN,WAAO;EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import { promises as fs } from "fs";
6
6
  import path from "path";
7
7
  import { parse as parseYAML } from "yaml";
8
8
  import { FarmManifest } from "../core/farm-manifest.js";
9
+ import { resolveModelDirectory } from "../utils/project-paths.js";
9
10
  class ComposedReferenceValidator {
10
11
  modelPathOverrides;
11
12
  resolvedModels = /* @__PURE__ */ new Map();
@@ -99,36 +100,13 @@ class ComposedReferenceValidator {
99
100
  });
100
101
  return;
101
102
  }
102
- let modelDir = path.join(modelPath, "documentation-robotics", "model"), stats;
103
- try {
104
- stats = await fs.stat(modelDir);
105
- } catch (err) {
106
- if (err?.code !== "ENOENT")
107
- throw err;
108
- }
109
- if (!stats) {
110
- modelDir = path.join(modelPath, "model");
111
- try {
112
- stats = await fs.stat(modelDir);
113
- } catch (err) {
114
- if (err?.code !== "ENOENT")
115
- throw err;
116
- }
117
- }
118
- if (!stats) {
119
- modelDir = modelPath;
120
- try {
121
- stats = await fs.stat(modelDir);
122
- } catch (err) {
123
- if (err?.code !== "ENOENT")
124
- throw err;
125
- }
126
- }
127
- if (!stats) {
103
+ const resolved = await resolveModelDirectory(modelPath);
104
+ if (!resolved) {
128
105
  this.addResolutionDiagnostic(modelName, new Error("Model directory not found"), modelPath, !0);
129
106
  return;
130
107
  }
131
- if (!stats.isDirectory()) {
108
+ const modelDir = resolved.modelDir;
109
+ if (!resolved.isDirectory) {
132
110
  this.resolutionDiagnostics.addWarning({
133
111
  layer: "manifest",
134
112
  message: `External model '${modelName}' path is not a directory: ${modelDir}`,