@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
@@ -0,0 +1,145 @@
1
+ import { readdir, readFile, writeFile } from "fs/promises";
2
+ import { Element } from "./element.js";
3
+ import { CANONICAL_LAYER_NAMES } from "./layers.js";
4
+ import { StagedChangesetStorage } from "./staged-changeset-storage.js";
5
+ import { FileLock } from "../utils/file-lock.js";
6
+ import { getErrorMessage } from "../utils/errors.js";
7
+ import { RELATIONSHIP_REWRITE_TABLE } from "./relationship-migration-table.js";
8
+ function splitType(specNodeId) {
9
+ const dot = specNodeId.indexOf(".");
10
+ return { layer: specNodeId.slice(0, dot), type: specNodeId.slice(dot + 1) };
11
+ }
12
+ function mapId(id, ids, types) {
13
+ const known = ids.get(id);
14
+ if (known)
15
+ return known;
16
+ for (const [from, to] of Object.entries(types)) {
17
+ const rest = id.startsWith(`${from}.`) ? id.slice(from.length + 1) : void 0;
18
+ if (rest && !rest.includes(".") && !rest.includes(":"))
19
+ return `${to}.${rest}`;
20
+ }
21
+ return id;
22
+ }
23
+ function rewriteValue(value, ids, types) {
24
+ if (typeof value == "string") {
25
+ if (value.includes("::")) {
26
+ const parts = value.split("::"), mapped = parts.map((p) => mapId(p, ids, types));
27
+ return mapped.some((p, i) => p !== parts[i]) ? mapped.join("::") : value;
28
+ }
29
+ return mapId(value, ids, types);
30
+ }
31
+ if (Array.isArray(value))
32
+ return value.map((v) => rewriteValue(v, ids, types));
33
+ if (value && typeof value == "object") {
34
+ const obj = value, out = {};
35
+ for (const [k, v] of Object.entries(obj))
36
+ out[k] = rewriteValue(v, ids, types);
37
+ const layerId = typeof obj.layer_id == "string" ? obj.layer_id : void 0, specNodeId = typeof obj.spec_node_id == "string" ? obj.spec_node_id : layerId && typeof obj.type == "string" ? `${layerId}.${obj.type}` : void 0, renamed = specNodeId ? types[specNodeId] : void 0;
38
+ return renamed && ("spec_node_id" in obj && (out.spec_node_id = renamed), "type" in obj && (out.type = splitType(renamed).type)), out;
39
+ }
40
+ return value;
41
+ }
42
+ function renamedPath(path, from, to) {
43
+ return path.startsWith(`${from}.`) ? `${to}.${path.slice(from.length + 1)}` : path;
44
+ }
45
+ async function migrateElementTypes(model, renames = RELATIONSHIP_REWRITE_TABLE.typeRenames ?? {}, options = {}) {
46
+ const result = { filesModified: 0, changes: [], changesetsModified: 0, errors: [] };
47
+ if (Object.keys(renames).length === 0)
48
+ return result;
49
+ for (const layerName of CANONICAL_LAYER_NAMES)
50
+ model.layers.has(layerName) || await model.loadLayer(layerName);
51
+ const ids = /* @__PURE__ */ new Map();
52
+ for (const layer of model.layers.values())
53
+ for (const element of layer.listElements()) {
54
+ const oldSpec = element.spec_node_id || `${layer.name}.${element.type}`, newSpec = renames[oldSpec];
55
+ if (!newSpec)
56
+ continue;
57
+ const target = splitType(newSpec);
58
+ if (target.layer !== layer.name) {
59
+ result.errors.push(`${element.path}: renaming ${oldSpec} to ${newSpec} would move it to another layer, which the upgrade cannot do`);
60
+ continue;
61
+ }
62
+ const json = element.toJSON(), newPath = renamedPath(element.path, oldSpec, newSpec);
63
+ if (options.dryRun) {
64
+ result.changes.push({ from: element.path, to: newPath, fromType: oldSpec, toType: newSpec });
65
+ continue;
66
+ }
67
+ layer.deleteElement(element.id), layer.addElement(new Element({
68
+ ...json,
69
+ // A legacy element whose id is its semantic path keeps the two equal
70
+ id: json.id === element.path ? newPath : json.id,
71
+ path: newPath,
72
+ spec_node_id: newSpec,
73
+ type: target.type,
74
+ layer_id: layer.name
75
+ })), newPath !== element.path && ids.set(element.path, newPath), result.changes.push({ from: element.path, to: newPath, fromType: oldSpec, toType: newSpec });
76
+ }
77
+ if (result.changes.length === 0 || options.dryRun)
78
+ return result;
79
+ if (ids.size > 0)
80
+ for (const layer of model.layers.values())
81
+ for (const element of layer.listElements())
82
+ element.references?.some((r) => ids.has(r.target) || ids.has(r.source)) && (element.references = element.references.map((r) => ({
83
+ ...r,
84
+ source: ids.get(r.source) ?? r.source,
85
+ target: ids.get(r.target) ?? r.target
86
+ })), layer.updateElement(element));
87
+ if (ids.size > 0) {
88
+ const YAML = await import("yaml"), relationshipsPath = `${model.rootPath}/documentation-robotics/model/relationships.yaml`;
89
+ let content;
90
+ try {
91
+ content = await readFile(relationshipsPath, "utf-8");
92
+ } catch (error) {
93
+ if (error.code !== "ENOENT")
94
+ throw error;
95
+ }
96
+ const data = content ? YAML.parse(content) ?? [] : [];
97
+ if (Array.isArray(data)) {
98
+ let changed = !1;
99
+ const rewritten = data.map((rel) => {
100
+ const map = (v) => typeof v == "string" && mapId(v, ids, renames) !== v ? mapId(v, ids, renames) : void 0, source = map(rel.source), target = map(rel.target);
101
+ return !source && !target ? rel : (changed = !0, { ...rel, ...source && { source }, ...target && { target } });
102
+ });
103
+ if (changed) {
104
+ const header = `# Intra-layer relationships
105
+ # Format: source_id -> predicate -> target_id
106
+ # Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}
107
+
108
+ `;
109
+ await writeFile(relationshipsPath, header + YAML.stringify(rewritten), "utf-8"), result.filesModified++;
110
+ }
111
+ }
112
+ }
113
+ const storage = new StagedChangesetStorage(model.rootPath);
114
+ let entries = [];
115
+ try {
116
+ entries = (await readdir(storage.getChangesetsDir(), { withFileTypes: !0 })).filter((entry) => entry.isDirectory() && !entry.name.endsWith(".lock")).map((entry) => entry.name).sort();
117
+ } catch (error) {
118
+ if (error.code !== "ENOENT")
119
+ throw error;
120
+ }
121
+ for (const id of entries)
122
+ try {
123
+ await new FileLock(storage.getChangesetPath(id)).withLock(async () => {
124
+ const changeset = await storage.load(id);
125
+ if (!changeset || changeset.status !== "staged")
126
+ return;
127
+ const before = JSON.stringify(changeset.changes);
128
+ changeset.changes = changeset.changes.map((change) => rewriteValue(change, ids, renames)), JSON.stringify(changeset.changes) !== before && (changeset.updateModified(), await storage.save(changeset), result.changesetsModified++);
129
+ });
130
+ } catch (error) {
131
+ result.errors.push(`Failed to rename element types in staged changeset '${id}': ${getErrorMessage(error)}; fix or delete the changeset and run the upgrade again`);
132
+ }
133
+ return result;
134
+ }
135
+ function summarizeElementTypeChanges(changes) {
136
+ const byType = /* @__PURE__ */ new Map();
137
+ for (const c of changes)
138
+ byType.set(`${c.fromType} -> ${c.toType}`, (byType.get(`${c.fromType} -> ${c.toType}`) ?? 0) + 1);
139
+ return [...byType].map(([k, n]) => `${k} (${n} element${n === 1 ? "" : "s"})`).join("; ");
140
+ }
141
+ export {
142
+ migrateElementTypes,
143
+ summarizeElementTypeChanges
144
+ };
145
+ //# sourceMappingURL=element-type-migration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/element-type-migration.ts"],
4
+ "sourcesContent": ["/**\n * Element type renames for `dr upgrade`.\n *\n * When the spec renames an element type (\"product.capability\" -> \"product.softwarecapability\",\n * recorded as a rename-type action and generated into RELATIONSHIP_REWRITE_TABLE.typeRenames),\n * every model element of the old type takes the new type, and its semantic path follows\n * (\"product.capability.search\" -> \"product.softwarecapability.search\"). Anything that names the\n * old path is rewritten with it: other elements' references, relationships.yaml, and the\n * element and relationship changes of staged changesets.\n *\n * The rename runs before the relationship rewrite of the same migration, on the loaded model, so\n * the relationship rules (keyed by the new type names) resolve the renamed elements. Renamed\n * layers are left dirty for the upgrade's saveDirtyLayers(); relationships.yaml and changesets\n * are rewritten on disk.\n */\nimport { readdir, readFile, writeFile } from \"fs/promises\";\nimport type { Model } from \"./model.js\";\nimport { Element } from \"./element.js\";\nimport type { Change } from \"./changeset.js\";\nimport { CANONICAL_LAYER_NAMES } from \"./layers.js\";\nimport { StagedChangesetStorage } from \"./staged-changeset-storage.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { RELATIONSHIP_REWRITE_TABLE } from \"./relationship-migration-table.js\";\n\nexport interface ElementTypeRenameChange {\n /** Element path before and after (the UUID does not change) */\n from: string;\n to: string;\n fromType: string;\n toType: string;\n}\n\nexport interface ElementTypeMigrationResult {\n filesModified: number;\n changes: ElementTypeRenameChange[];\n /** Changesets whose staged changes named a renamed element or type */\n changesetsModified: number;\n errors: string[];\n}\n\nfunction splitType(specNodeId: string): { layer: string; type: string } {\n const dot = specNodeId.indexOf(\".\");\n return { layer: specNodeId.slice(0, dot), type: specNodeId.slice(dot + 1) };\n}\n\n/**\n * The renamed form of an element id: a renamed element's path, or any \"{old type}.{name}\" path,\n * which covers elements that exist only in a staged changeset. Other ids are returned unchanged.\n */\nfunction mapId(id: string, ids: Map<string, string>, types: Record<string, string>): string {\n const known = ids.get(id);\n if (known) return known;\n for (const [from, to] of Object.entries(types)) {\n const rest = id.startsWith(`${from}.`) ? id.slice(from.length + 1) : undefined;\n if (rest && !rest.includes(\".\") && !rest.includes(\":\")) return `${to}.${rest}`;\n }\n return id;\n}\n\n/** Replace every string in a JSON-like value that is a renamed element id (or a composite of them) */\nfunction rewriteValue(value: unknown, ids: Map<string, string>, types: Record<string, string>): unknown {\n if (typeof value === \"string\") {\n // Relationship change ids are \"source::predicate::target\"\n if (value.includes(\"::\")) {\n const parts = value.split(\"::\");\n const mapped = parts.map((p) => mapId(p, ids, types));\n return mapped.some((p, i) => p !== parts[i]) ? mapped.join(\"::\") : value;\n }\n return mapId(value, ids, types);\n }\n if (Array.isArray(value)) return value.map((v) => rewriteValue(v, ids, types));\n if (value && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) out[k] = rewriteValue(v, ids, types);\n // An element payload of a renamed type: spec_node_id and type follow the rename\n const layerId = typeof obj.layer_id === \"string\" ? obj.layer_id : undefined;\n const specNodeId =\n typeof obj.spec_node_id === \"string\"\n ? obj.spec_node_id\n : layerId && typeof obj.type === \"string\"\n ? `${layerId}.${obj.type}`\n : undefined;\n const renamed = specNodeId ? types[specNodeId] : undefined;\n if (renamed) {\n if (\"spec_node_id\" in obj) out.spec_node_id = renamed;\n if (\"type\" in obj) out.type = splitType(renamed).type;\n }\n return out;\n }\n return value;\n}\n\n/** The new semantic path for an element of a renamed type, or the path unchanged */\nfunction renamedPath(path: string, from: string, to: string): string {\n return path.startsWith(`${from}.`) ? `${to}.${path.slice(from.length + 1)}` : path;\n}\n\n/**\n * Rename the elements of every renamed type in the loaded model, and rewrite what names them.\n *\n * @param renames - old spec node id -> new spec node id; defaults to the generated table's\n * @param options.dryRun - report the elements that would be renamed, and change nothing\n */\nexport async function migrateElementTypes(\n model: Model,\n renames: Record<string, string> = RELATIONSHIP_REWRITE_TABLE.typeRenames ?? {},\n options: { dryRun?: boolean } = {}\n): Promise<ElementTypeMigrationResult> {\n const result: ElementTypeMigrationResult = { filesModified: 0, changes: [], changesetsModified: 0, errors: [] };\n if (Object.keys(renames).length === 0) return result;\n\n for (const layerName of CANONICAL_LAYER_NAMES) {\n if (!model.layers.has(layerName)) {\n await model.loadLayer(layerName);\n }\n }\n\n // 1. Elements of a renamed type: new type, new path. A move to another layer is not supported.\n const ids = new Map<string, string>();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n const oldSpec = element.spec_node_id || `${layer.name}.${element.type}`;\n const newSpec = renames[oldSpec];\n if (!newSpec) continue;\n const target = splitType(newSpec);\n if (target.layer !== layer.name) {\n result.errors.push(`${element.path}: renaming ${oldSpec} to ${newSpec} would move it to another layer, which the upgrade cannot do`);\n continue;\n }\n const json = element.toJSON();\n const newPath = renamedPath(element.path, oldSpec, newSpec);\n if (options.dryRun) {\n result.changes.push({ from: element.path, to: newPath, fromType: oldSpec, toType: newSpec });\n continue;\n }\n layer.deleteElement(element.id);\n layer.addElement(\n new Element({\n ...json,\n // A legacy element whose id is its semantic path keeps the two equal\n id: json.id === element.path ? newPath : json.id,\n path: newPath,\n spec_node_id: newSpec,\n type: target.type,\n layer_id: layer.name,\n })\n );\n if (newPath !== element.path) ids.set(element.path, newPath);\n result.changes.push({ from: element.path, to: newPath, fromType: oldSpec, toType: newSpec });\n }\n }\n if (result.changes.length === 0 || options.dryRun) return result;\n\n // 2. References from any element to a renamed path\n if (ids.size > 0) {\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n if (!element.references?.some((r) => ids.has(r.target) || ids.has(r.source))) continue;\n element.references = element.references.map((r) => ({\n ...r,\n source: ids.get(r.source) ?? r.source,\n target: ids.get(r.target) ?? r.target,\n }));\n layer.updateElement(element);\n }\n }\n }\n\n // 3. relationships.yaml names elements by path (or a legacy id)\n if (ids.size > 0) {\n const YAML = await import(\"yaml\");\n const relationshipsPath = `${model.rootPath}/documentation-robotics/model/relationships.yaml`;\n let content: string | undefined;\n try {\n content = await readFile(relationshipsPath, \"utf-8\");\n } catch (error) {\n if ((error as { code?: string }).code !== \"ENOENT\") throw error;\n }\n const data = content ? (YAML.parse(content) ?? []) : [];\n if (Array.isArray(data)) {\n let changed = false;\n const rewritten = data.map((rel: Record<string, unknown>) => {\n const map = (v: unknown) => (typeof v === \"string\" && mapId(v, ids, renames) !== v ? mapId(v, ids, renames) : undefined);\n const source = map(rel.source);\n const target = map(rel.target);\n if (!source && !target) return rel;\n changed = true;\n return { ...rel, ...(source && { source }), ...(target && { target }) };\n });\n if (changed) {\n const header = `# Intra-layer relationships\n# Format: source_id -> predicate -> target_id\n# Last updated: ${new Date().toISOString()}\n\n`;\n await writeFile(relationshipsPath, header + YAML.stringify(rewritten), \"utf-8\");\n result.filesModified++;\n }\n }\n }\n\n // 4. Staged changesets: element and relationship changes naming a renamed element or type\n const storage = new StagedChangesetStorage(model.rootPath);\n let entries: string[] = [];\n try {\n entries = (await readdir(storage.getChangesetsDir(), { withFileTypes: true }))\n .filter((entry) => entry.isDirectory() && !entry.name.endsWith(\".lock\"))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n if ((error as { code?: string }).code !== \"ENOENT\") throw error;\n }\n for (const id of entries) {\n try {\n await new FileLock(storage.getChangesetPath(id)).withLock(async () => {\n const changeset = await storage.load(id);\n if (!changeset || changeset.status !== \"staged\") return;\n const before = JSON.stringify(changeset.changes);\n changeset.changes = changeset.changes.map((change) => rewriteValue(change, ids, renames) as Change) as typeof changeset.changes;\n if (JSON.stringify(changeset.changes) === before) return;\n changeset.updateModified();\n await storage.save(changeset);\n result.changesetsModified++;\n });\n } catch (error) {\n result.errors.push(\n `Failed to rename element types in staged changeset '${id}': ${getErrorMessage(error)}; fix or delete the changeset and run the upgrade again`\n );\n }\n }\n\n // Renamed layers are dirty; the upgrade saves them\n return result;\n}\n\n/** One line per renamed type: \"product.capability -> product.softwarecapability (3 elements)\" */\nexport function summarizeElementTypeChanges(changes: ElementTypeRenameChange[]): string {\n const byType = new Map<string, number>();\n for (const c of changes) byType.set(`${c.fromType} -> ${c.toType}`, (byType.get(`${c.fromType} -> ${c.toType}`) ?? 0) + 1);\n return [...byType].map(([k, n]) => `${k} (${n} element${n === 1 ? \"\" : \"s\"})`).join(\"; \");\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,161 @@
1
+ import { MappingLoader } from "../analyzers/mapping-loader.js";
2
+ import { relationshipFor } from "../analyzers/edge-relationships.js";
3
+ import { tier0RulesFrom } from "../analyzers/tier0-rules.js";
4
+ import { getValidRelationships } from "../generated/relationship-index.js";
5
+ const NEVER_OBSERVABLE_LAYERS = /* @__PURE__ */ new Set(["motivation", "business", "product"]), NO_TEST_CARDS = /\.[cm]?[jt]sx?$/, ROUTE_SYMBOL = /^(?:__route__[A-Z]+__\/|[A-Z]+ \/)/, BARREL_FILE = /(^|\/)(index\.[cm]?[jt]sx?|__init__\.py|mod\.rs)$/, CODE_FACTS = {
6
+ "api.operation": ["http_method", "http_path"]
7
+ }, EDGE_LABELS = {
8
+ DEPLOYS: { oneEndOf: ["Resource", "Image"], why: "links infrastructure resources and images" },
9
+ INFRA_MAPS: { oneEndOf: ["Resource"], why: "links infrastructure resources" },
10
+ DEPENDS_ON: { oneEndOf: ["Package", "Image", "Resource"], why: "links packages, images and resources" },
11
+ SPAWNS: { oneEndOf: ["Process"], why: "links code to the processes it launches" }
12
+ }, EDGE_ENDPOINTS = {
13
+ DEFINES_METHOD: {
14
+ to: (element) => {
15
+ const symbols = sourceLocations(element).map((l) => l.symbol).filter((s) => !!s);
16
+ return symbols.length === 0 || symbols.some((s) => s.includes("."));
17
+ },
18
+ why: "links a class to its methods only, and this function is not a method"
19
+ }
20
+ };
21
+ function catalogHasTriple(id) {
22
+ const parts = id.split(".");
23
+ return parts.length !== 5 ? !1 : getValidRelationships(`${parts[0]}.${parts[1]}`, parts[2], `${parts[3]}.${parts[4]}`).length > 0;
24
+ }
25
+ function sourceLocations(element) {
26
+ return (element.sourceReference?.locations ?? []).filter((l) => typeof l?.file == "string" && l.file.length > 0).map((l) => ({ file: l.file.replace(/\\/g, "/").replace(/^\.\//, ""), ...l.symbol ? { symbol: l.symbol } : {} }));
27
+ }
28
+ class ExtractionCoverage {
29
+ analyzer;
30
+ producers = /* @__PURE__ */ new Map();
31
+ /** Layers a Tier-0 rule narrows to with `layer.*`. */
32
+ wildcardLayers = /* @__PURE__ */ new Map();
33
+ /** Attributes the node mapping fills, by element type. */
34
+ filled = /* @__PURE__ */ new Map();
35
+ edgeMappings;
36
+ hasTriple;
37
+ tripleCache = /* @__PURE__ */ new Map();
38
+ constructor(input) {
39
+ this.analyzer = input.analyzer, this.hasTriple = input.hasTriple ?? catalogHasTriple;
40
+ const add = (type, producer) => {
41
+ const list = this.producers.get(type) ?? [];
42
+ list.push(producer), this.producers.set(type, list);
43
+ }, fill = (type, attributes) => {
44
+ const set = this.filled.get(type) ?? /* @__PURE__ */ new Set();
45
+ for (const a of attributes)
46
+ set.add(a);
47
+ this.filled.set(type, set);
48
+ }, labels = /* @__PURE__ */ new Set();
49
+ for (const m of input.nodeMappings) {
50
+ if (!m.dr_layer || !m.dr_element_type)
51
+ continue;
52
+ labels.add(m.cbm_label);
53
+ const producer = { via: `node mapping ${m.cbm_label}`, label: m.cbm_label }, mapped = m, attributes = [
54
+ ...(mapped.attribute_mappings ?? []).map((a) => a.dr_attribute).filter((a) => !!a),
55
+ ...Object.keys(m.dr_element_fields ?? {})
56
+ ];
57
+ for (const type of [m.dr_element_type, mapped.dr_element_type_promoted].filter((t) => !!t))
58
+ add(`${m.dr_layer}.${type}`, producer), fill(`${m.dr_layer}.${type}`, attributes);
59
+ }
60
+ for (const rule of input.rules) {
61
+ if (rule.action.kind === "reject")
62
+ continue;
63
+ for (const label of rule.labels)
64
+ labels.add(label);
65
+ const producer = { via: `Tier-0 rule ${rule.name}`, ...rule.when.file ? { file: rule.when.file } : {} };
66
+ if (rule.action.kind === "decide")
67
+ add(`${rule.action.layer}.${rule.action.type}`, producer);
68
+ else
69
+ for (const c of rule.action.candidates)
70
+ c.type ? add(`${c.layer}.${c.type}`, producer) : this.wildcardLayers.set(c.layer, [...this.wildcardLayers.get(c.layer) ?? [], producer]);
71
+ }
72
+ for (const [type, facts] of Object.entries(CODE_FACTS))
73
+ fill(type, facts);
74
+ this.edgeMappings = input.edgeMappings.filter((m) => m.usage === "verification_and_mapping" && (!EDGE_LABELS[m.cbm_edge] || EDGE_LABELS[m.cbm_edge].oneEndOf.some((l) => labels.has(l))));
75
+ }
76
+ /** Element types extraction can produce at all (`layer.type`), sorted. */
77
+ observableTypes() {
78
+ return [...this.producers.keys()].sort();
79
+ }
80
+ /**
81
+ * Whether extraction could produce this element. `codeFiles` are the files the build was
82
+ * extracted from; they stand in for the element's own source file when it names none.
83
+ */
84
+ element(element, codeFiles = []) {
85
+ const type = `${element.layer}.${element.type}`;
86
+ if (NEVER_OBSERVABLE_LAYERS.has(element.layer))
87
+ return { observable: !1, reason: `extraction does not read intent (${element.layer}) from code` };
88
+ const producers = [...this.producers.get(type) ?? [], ...this.wildcardLayers.get(element.layer) ?? []];
89
+ if (producers.length === 0)
90
+ return { observable: !1, reason: `the ${this.analyzer} mapping produces no ${type} elements` };
91
+ const locations = sourceLocations(element), files = locations.length > 0 ? locations.map((l) => l.file) : codeFiles;
92
+ if (element.layer === "testing" && files.length > 0 && files.every((f) => NO_TEST_CARDS.test(f)))
93
+ return {
94
+ observable: !1,
95
+ reason: "JavaScript and TypeScript test cases are anonymous callbacks and never become extraction cards"
96
+ };
97
+ const unlocated = isUnlocatedOperation(element, locations);
98
+ if (producers.filter((p) => !(p.file && locations.length > 0 && !locations.some((l) => p.file.test(l.file)) || p.label === "Route" && (unlocated || isReExport(element, locations)))).length === 0) {
99
+ const routeOnly = producers.every((p) => p.label === "Route");
100
+ return {
101
+ observable: !1,
102
+ ...routeOnly && unlocated ? { unlocated: !0 } : {},
103
+ reason: routeOnly ? unlocated ? `extraction produces ${type} only from HTTP routes, and this one has no method, path or source reference to find it by. To make it verifiable, give it http_method and http_path (for a route), or sourceFile and sourceSymbol (the handler that serves it)` : `extraction produces ${type} only from HTTP routes, and this one is a re-export from ${locations.map((l) => l.file).join(", ")} with no method and path` : `no ${this.analyzer} rule producing ${type} applies to ${locations.map((l) => l.file).join(", ")}`
104
+ };
105
+ }
106
+ return { observable: !0 };
107
+ }
108
+ /**
109
+ * Whether extraction writes this field of an element of this type: the name and type, the
110
+ * attributes the node mapping fills, and the code facts it reads (an operation's method and
111
+ * path). Never the description, which comes from a docstring at most, or other attributes.
112
+ */
113
+ field(element, field) {
114
+ return field === "name" || field === "type" ? !0 : field.startsWith("attributes.") ? this.filled.get(`${element.layer}.${element.type}`)?.has(field.slice(11)) ?? !1 : !1;
115
+ }
116
+ /**
117
+ * Whether some mapped code edge between these two elements becomes this relationship: by their
118
+ * types, as extraction decides it, and by what the edge links (see EDGE_LABELS, EDGE_ENDPOINTS).
119
+ */
120
+ relationship(source, predicate, target) {
121
+ const sourceType = `${source.layer}.${source.type}`, targetType = `${target.layer}.${target.type}`, key = `${sourceType}|${predicate}|${targetType}`;
122
+ let edges = this.tripleCache.get(key);
123
+ if (edges === void 0) {
124
+ edges = [];
125
+ for (const m of this.edgeMappings)
126
+ for (const inverted of [!1, !0]) {
127
+ const [from, to] = inverted ? [targetType, sourceType] : [sourceType, targetType], r = relationshipFor(m, from, to, this.hasTriple);
128
+ r && r.source === sourceType && r.destination === targetType && r.predicate === predicate && edges.push({ edge: m.cbm_edge, inverted });
129
+ }
130
+ this.tripleCache.set(key, edges);
131
+ }
132
+ return edges.length === 0 ? { observable: !1, reason: `the ${this.analyzer} edge mapping turns no code edge into ${sourceType} ${predicate} ${targetType}` } : edges.filter(({ edge, inverted }) => {
133
+ const check = EDGE_ENDPOINTS[edge]?.to;
134
+ return !check || check(inverted ? source : target);
135
+ }).length === 0 ? {
136
+ observable: !1,
137
+ reason: edges.map(({ edge }) => `${edge} ${EDGE_ENDPOINTS[edge]?.why ?? "does not apply"}`).join("; ")
138
+ } : { observable: !0 };
139
+ }
140
+ }
141
+ function isReExport(element, locations) {
142
+ const attrs = element.attributes ?? {};
143
+ return attrs.http_method || attrs.http_path || attrs.method || attrs.path || locations.length === 0 ? !1 : locations.every((l) => BARREL_FILE.test(l.file) && !(l.symbol && ROUTE_SYMBOL.test(l.symbol)));
144
+ }
145
+ function isUnlocatedOperation(element, locations) {
146
+ const attrs = element.attributes ?? {};
147
+ return attrs.http_method || attrs.http_path || attrs.method || attrs.path ? !1 : locations.length === 0;
148
+ }
149
+ async function loadExtractionCoverage(analyzer) {
150
+ try {
151
+ const mapper = await MappingLoader.load(analyzer), nodeMappings = mapper.getNodeLabels().map((label) => mapper.getNodeMapping(label)).filter((m) => !!m), edgeMappings = mapper.getEdgeTypes().map((type) => mapper.getEdgeMapping(type)).filter((m) => !!m);
152
+ return new ExtractionCoverage({ analyzer, nodeMappings, edgeMappings, rules: tier0RulesFrom(mapper.getHeuristics()) });
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ export {
158
+ ExtractionCoverage,
159
+ loadExtractionCoverage
160
+ };
161
+ //# sourceMappingURL=extraction-observability.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/extraction-observability.ts"],
4
+ "sourcesContent": ["/**\n * Extraction observability \u2014 which planned elements, relationships and fields code extraction can\n * see.\n *\n * `changeset_verify` compares a plan with an as-built changeset. When the as-built side came from\n * `codebase_extract`, a planned element extraction can never produce (a requirement, a re-export,\n * a TypeScript test case) is not \"missing\": it is unverifiable, and stays out of the verdict.\n *\n * What extraction can produce is read from the analyzer's own mapping (spec/analyzers/<name>/):\n *\n * - the node mapping: each CBM label becomes one element type (and a promoted type), and fills some\n * of its attributes;\n * - the Tier-0 rules: each decides or narrows to element types, some only for files matching a\n * pattern;\n * - the edge mapping: each mapped CBM edge becomes a relationship between two element types,\n * through the relationship catalog and the edge's from_types and to_types, the way\n * `relationshipFor` decides it during extraction.\n *\n * Facts the mapping files don't state are kept here, each with its reason:\n *\n * - a Route is an HTTP route: an operation sourced to a barrel file (a package entry that\n * re-exports) with no method and path is not one, and an operation with no method, no path and\n * no source reference gives extraction nothing to find it by;\n * - some CBM edges link labels no node mapping or Tier-0 rule produces (DEPLOYS links\n * infrastructure resources), so they make no relationship at all;\n * - DEFINES_METHOD links a class to its methods, not a module to its functions;\n * - the card builder makes no cards for JavaScript and TypeScript test cases (anonymous `it()`\n * callbacks), so the testing layer is not observable in those files (docs/EXTRACTION_STRATEGY.md,\n * \"Known gap\");\n * - extraction writes an operation's method and path (`http_method`, `http_path`) besides the\n * node mapping's fields.\n *\n * Intent (motivation, business, product) is never read from code.\n */\n\nimport { MappingLoader } from \"../analyzers/mapping-loader.js\";\nimport { relationshipFor } from \"../analyzers/edge-relationships.js\";\nimport { tier0RulesFrom, type Tier0Rule } from \"../analyzers/tier0-rules.js\";\nimport type { AnalyzerEdgeMapping, AnalyzerNodeMapping } from \"../analyzers/types.js\";\nimport { getValidRelationships } from \"../generated/relationship-index.js\";\nimport type { SourceReference } from \"../types/source-reference.js\";\n\n/** Layers that state intent; no code says why the system exists. */\nconst NEVER_OBSERVABLE_LAYERS = new Set([\"motivation\", \"business\", \"product\"]);\n\n/** Files the card builder makes no test cards for: JS/TS test cases are anonymous callbacks. */\nconst NO_TEST_CARDS = /\\.[cm]?[jt]sx?$/;\n\n/** A Route's symbol: `GET /owners/{id}` or CBM's `__route__GET__/owners/{id}`. */\nconst ROUTE_SYMBOL = /^(?:__route__[A-Z]+__\\/|[A-Z]+ \\/)/;\n\n/** Barrel files: a package entry that re-exports what other files define. */\nconst BARREL_FILE = /(^|\\/)(index\\.[cm]?[jt]sx?|__init__\\.py|mod\\.rs)$/;\n\n/** Attributes extraction writes from the code beyond the node mapping (see deriveAttributes). */\nconst CODE_FACTS: Record<string, string[]> = {\n \"api.operation\": [\"http_method\", \"http_path\"],\n};\n\n/**\n * CBM edges whose ends are particular labels. An edge one of whose ends must be a label no node\n * mapping or Tier-0 rule produces makes no relationship: its ends never become elements.\n */\nconst EDGE_LABELS: Record<string, { oneEndOf: string[]; why: string }> = {\n DEPLOYS: { oneEndOf: [\"Resource\", \"Image\"], why: \"links infrastructure resources and images\" },\n INFRA_MAPS: { oneEndOf: [\"Resource\"], why: \"links infrastructure resources\" },\n DEPENDS_ON: { oneEndOf: [\"Package\", \"Image\", \"Resource\"], why: \"links packages, images and resources\" },\n SPAWNS: { oneEndOf: [\"Process\"], why: \"links code to the processes it launches\" },\n};\n\n/**\n * CBM edges that link only some symbols of their element types, by which end (in CBM's direction)\n * must be what. DEFINES_METHOD runs from a class to its methods: a module-level function, whose\n * symbol names no owner (`computeNodeDegrees`, not `OwnerController.initCreationForm`), has none.\n */\nconst EDGE_ENDPOINTS: Record<string, { to?: (element: ObservedElement) => boolean; why: string }> = {\n DEFINES_METHOD: {\n to: (element) => {\n const symbols = sourceLocations(element)\n .map((l) => l.symbol)\n .filter((s): s is string => !!s);\n return symbols.length === 0 || symbols.some((s) => s.includes(\".\"));\n },\n why: \"links a class to its methods only, and this function is not a method\",\n },\n};\n\n/** What an element looks like to the observability check. */\nexport interface ObservedElement {\n layer: string;\n type: string;\n attributes?: Record<string, unknown>;\n sourceReference?: SourceReference;\n}\n\nexport interface ObservabilityVerdict {\n observable: boolean;\n /** Why not, when it is not. */\n reason?: string;\n /**\n * Not observable only for want of a method, path or source reference to find it by (an operation\n * planned with no source fields): the build may still have it under another name.\n */\n unlocated?: true;\n}\n\n/** One way extraction produces an element type. */\ninterface Producer {\n /** `node mapping <Label>` or `Tier-0 rule <name>`. */\n via: string;\n /** For a node mapping: the CBM label. */\n label?: string;\n /** For a Tier-0 rule with a file condition: the files it applies to. */\n file?: RegExp;\n}\n\nexport interface ExtractionCoverageInput {\n analyzer: string;\n nodeMappings: AnalyzerNodeMapping[];\n edgeMappings: AnalyzerEdgeMapping[];\n rules: Tier0Rule[];\n /** Catalog check by triple id (`layer.type.predicate.layer.type`); defaults to the generated index. */\n hasTriple?: (id: string) => boolean;\n}\n\nfunction catalogHasTriple(id: string): boolean {\n const parts = id.split(\".\");\n if (parts.length !== 5) return false;\n return getValidRelationships(`${parts[0]}.${parts[1]}`, parts[2], `${parts[3]}.${parts[4]}`).length > 0;\n}\n\nfunction sourceLocations(element: ObservedElement): Array<{ file: string; symbol?: string }> {\n return (element.sourceReference?.locations ?? [])\n .filter((l) => typeof l?.file === \"string\" && l.file.length > 0)\n .map((l) => ({ file: l.file.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\"), ...(l.symbol ? { symbol: l.symbol } : {}) }));\n}\n\n/** What an analyzer's extraction can produce, by element type, relationship triple and field. */\nexport class ExtractionCoverage {\n readonly analyzer: string;\n private readonly producers = new Map<string, Producer[]>();\n /** Layers a Tier-0 rule narrows to with `layer.*`. */\n private readonly wildcardLayers = new Map<string, Producer[]>();\n /** Attributes the node mapping fills, by element type. */\n private readonly filled = new Map<string, Set<string>>();\n private readonly edgeMappings: AnalyzerEdgeMapping[];\n private readonly hasTriple: (id: string) => boolean;\n private readonly tripleCache = new Map<string, Array<{ edge: string; inverted: boolean }>>();\n\n constructor(input: ExtractionCoverageInput) {\n this.analyzer = input.analyzer;\n this.hasTriple = input.hasTriple ?? catalogHasTriple;\n const add = (type: string, producer: Producer) => {\n const list = this.producers.get(type) ?? [];\n list.push(producer);\n this.producers.set(type, list);\n };\n const fill = (type: string, attributes: string[]) => {\n const set = this.filled.get(type) ?? new Set<string>();\n for (const a of attributes) set.add(a);\n this.filled.set(type, set);\n };\n // Labels whose symbols can become elements, by the node mapping or a Tier-0 rule.\n const labels = new Set<string>();\n for (const m of input.nodeMappings) {\n if (!m.dr_layer || !m.dr_element_type) continue;\n labels.add(m.cbm_label);\n const producer = { via: `node mapping ${m.cbm_label}`, label: m.cbm_label };\n const mapped = m as AnalyzerNodeMapping & {\n dr_element_type_promoted?: string;\n attribute_mappings?: Array<{ dr_attribute?: string }>;\n };\n const attributes = [\n ...(mapped.attribute_mappings ?? []).map((a) => a.dr_attribute).filter((a): a is string => !!a),\n ...Object.keys(m.dr_element_fields ?? {}),\n ];\n for (const type of [m.dr_element_type, mapped.dr_element_type_promoted].filter((t): t is string => !!t)) {\n add(`${m.dr_layer}.${type}`, producer);\n fill(`${m.dr_layer}.${type}`, attributes);\n }\n }\n for (const rule of input.rules) {\n if (rule.action.kind === \"reject\") continue;\n for (const label of rule.labels) labels.add(label);\n const producer: Producer = { via: `Tier-0 rule ${rule.name}`, ...(rule.when.file ? { file: rule.when.file } : {}) };\n if (rule.action.kind === \"decide\") add(`${rule.action.layer}.${rule.action.type}`, producer);\n else {\n for (const c of rule.action.candidates) {\n if (c.type) add(`${c.layer}.${c.type}`, producer);\n else this.wildcardLayers.set(c.layer, [...(this.wildcardLayers.get(c.layer) ?? []), producer]);\n }\n }\n }\n for (const [type, facts] of Object.entries(CODE_FACTS)) fill(type, facts);\n this.edgeMappings = input.edgeMappings.filter(\n (m) => m.usage === \"verification_and_mapping\" && (!EDGE_LABELS[m.cbm_edge] || EDGE_LABELS[m.cbm_edge].oneEndOf.some((l) => labels.has(l)))\n );\n }\n\n /** Element types extraction can produce at all (`layer.type`), sorted. */\n observableTypes(): string[] {\n return [...this.producers.keys()].sort();\n }\n\n /**\n * Whether extraction could produce this element. `codeFiles` are the files the build was\n * extracted from; they stand in for the element's own source file when it names none.\n */\n element(element: ObservedElement, codeFiles: string[] = []): ObservabilityVerdict {\n const type = `${element.layer}.${element.type}`;\n if (NEVER_OBSERVABLE_LAYERS.has(element.layer)) {\n return { observable: false, reason: `extraction does not read intent (${element.layer}) from code` };\n }\n const producers = [...(this.producers.get(type) ?? []), ...(this.wildcardLayers.get(element.layer) ?? [])];\n if (producers.length === 0) {\n return { observable: false, reason: `the ${this.analyzer} mapping produces no ${type} elements` };\n }\n\n const locations = sourceLocations(element);\n const files = locations.length > 0 ? locations.map((l) => l.file) : codeFiles;\n if (element.layer === \"testing\" && files.length > 0 && files.every((f) => NO_TEST_CARDS.test(f))) {\n return {\n observable: false,\n reason: \"JavaScript and TypeScript test cases are anonymous callbacks and never become extraction cards\",\n };\n }\n\n const unlocated = isUnlocatedOperation(element, locations);\n const possible = producers.filter((p) => {\n if (p.file && locations.length > 0 && !locations.some((l) => p.file!.test(l.file))) return false;\n if (p.label === \"Route\" && (unlocated || isReExport(element, locations))) return false;\n return true;\n });\n if (possible.length === 0) {\n const routeOnly = producers.every((p) => p.label === \"Route\");\n return {\n observable: false,\n ...(routeOnly && unlocated ? { unlocated: true as const } : {}),\n reason: !routeOnly\n ? `no ${this.analyzer} rule producing ${type} applies to ${locations.map((l) => l.file).join(\", \")}`\n : unlocated\n ? `extraction produces ${type} only from HTTP routes, and this one has no method, path or source reference to find ` +\n \"it by. To make it verifiable, give it http_method and http_path (for a route), or sourceFile and sourceSymbol \" +\n \"(the handler that serves it)\"\n : `extraction produces ${type} only from HTTP routes, and this one is a re-export from ${locations.map((l) => l.file).join(\", \")} with no method and path`,\n };\n }\n return { observable: true };\n }\n\n /**\n * Whether extraction writes this field of an element of this type: the name and type, the\n * attributes the node mapping fills, and the code facts it reads (an operation's method and\n * path). Never the description, which comes from a docstring at most, or other attributes.\n */\n field(element: ObservedElement, field: string): boolean {\n if (field === \"name\" || field === \"type\") return true;\n if (!field.startsWith(\"attributes.\")) return false;\n return this.filled.get(`${element.layer}.${element.type}`)?.has(field.slice(\"attributes.\".length)) ?? false;\n }\n\n /**\n * Whether some mapped code edge between these two elements becomes this relationship: by their\n * types, as extraction decides it, and by what the edge links (see EDGE_LABELS, EDGE_ENDPOINTS).\n */\n relationship(source: ObservedElement, predicate: string, target: ObservedElement): ObservabilityVerdict {\n const sourceType = `${source.layer}.${source.type}`;\n const targetType = `${target.layer}.${target.type}`;\n const key = `${sourceType}|${predicate}|${targetType}`;\n let edges = this.tripleCache.get(key);\n if (edges === undefined) {\n // Each producing edge, with the CBM edge's own ends: `inverted` when it runs target \u2192 source.\n edges = [];\n for (const m of this.edgeMappings) {\n for (const inverted of [false, true]) {\n const [from, to] = inverted ? [targetType, sourceType] : [sourceType, targetType];\n const r = relationshipFor(m, from, to, this.hasTriple);\n if (r && r.source === sourceType && r.destination === targetType && r.predicate === predicate) {\n edges.push({ edge: m.cbm_edge, inverted });\n }\n }\n }\n this.tripleCache.set(key, edges);\n }\n if (edges.length === 0) {\n return { observable: false, reason: `the ${this.analyzer} edge mapping turns no code edge into ${sourceType} ${predicate} ${targetType}` };\n }\n const possible = edges.filter(({ edge, inverted }) => {\n const check = EDGE_ENDPOINTS[edge]?.to;\n return !check || check(inverted ? source : target);\n });\n if (possible.length === 0) {\n return {\n observable: false,\n reason: edges.map(({ edge }) => `${edge} ${EDGE_ENDPOINTS[edge]?.why ?? \"does not apply\"}`).join(\"; \"),\n };\n }\n return { observable: true };\n }\n}\n\n/**\n * A re-export, not a route: no method and path, sourced to a barrel file (a package entry), under a\n * name that isn't route-shaped. Without that evidence an operation may be a route (a handler symbol\n * such as `OwnerController.showVisits` names the code that serves it).\n */\nfunction isReExport(element: ObservedElement, locations: Array<{ file: string; symbol?: string }>): boolean {\n const attrs = element.attributes ?? {};\n if (attrs.http_method || attrs.http_path || attrs.method || attrs.path) return false;\n if (locations.length === 0) return false;\n return locations.every((l) => BARREL_FILE.test(l.file) && !(l.symbol && ROUTE_SYMBOL.test(l.symbol)));\n}\n\n/**\n * An operation planned with nothing extraction could find it by: no method or path, and no source\n * reference (so no handler or route symbol either). Extraction finds operations only as routes, by\n * method and path or by the handler's file and symbol, so it can't show such a one either way.\n */\nfunction isUnlocatedOperation(element: ObservedElement, locations: Array<{ file: string; symbol?: string }>): boolean {\n const attrs = element.attributes ?? {};\n if (attrs.http_method || attrs.http_path || attrs.method || attrs.path) return false;\n return locations.length === 0;\n}\n\n/** The coverage of an analyzer's bundled mapping, or null when it can't be read. */\nexport async function loadExtractionCoverage(analyzer: string): Promise<ExtractionCoverage | null> {\n try {\n const mapper = await MappingLoader.load(analyzer);\n const nodeMappings = mapper\n .getNodeLabels()\n .map((label) => mapper.getNodeMapping(label))\n .filter((m): m is AnalyzerNodeMapping => !!m);\n const edgeMappings = mapper\n .getEdgeTypes()\n .map((type) => mapper.getEdgeMapping(type))\n .filter((m): m is AnalyzerEdgeMapping => !!m);\n return new ExtractionCoverage({ analyzer, nodeMappings, edgeMappings, rules: tier0RulesFrom(mapper.getHeuristics()) });\n } catch {\n return null;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -3,15 +3,21 @@ import { readFile, writeFile } from "../utils/file-io.js";
3
3
  function validateFarmProject(project, projectKey) {
4
4
  if (!project || typeof project != "object")
5
5
  throw new Error(`Invalid project entry "${projectKey}": expected object, got ${typeof project}`);
6
- if (typeof project.name != "string" || project.name === "")
6
+ const p = project;
7
+ if (typeof p.name != "string" || p.name === "")
7
8
  throw new Error(`Project "${projectKey}" is missing required 'name' field. Each project must have name, source, and model fields.`);
8
- if (typeof project.source != "string" || project.source === "")
9
+ if (typeof p.source != "string" || p.source === "")
9
10
  throw new Error(`Project "${projectKey}" is missing required 'source' field. Each project must have name, source, and model fields.`);
10
- if (typeof project.model != "string" || project.model === "")
11
+ if (typeof p.model != "string" || p.model === "")
11
12
  throw new Error(`Project "${projectKey}" is missing required 'model' field. Each project must have name, source, and model fields.`);
12
- if (project.name !== projectKey)
13
- throw new Error(`Project name mismatch: key is "${projectKey}" but project.name is "${project.name}". They must match for consistent sync state tracking.`);
14
- return project;
13
+ if (p.name !== projectKey)
14
+ throw new Error(`Project name mismatch: key is "${projectKey}" but project.name is "${p.name}". They must match for consistent sync state tracking.`);
15
+ const validatedProject = {
16
+ name: p.name,
17
+ source: p.source,
18
+ model: p.model
19
+ };
20
+ return typeof p.remote == "string" && (validatedProject.remote = p.remote), typeof p.branch == "string" && (validatedProject.branch = p.branch), validatedProject;
15
21
  }
16
22
  class FarmManifest {
17
23
  schema;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/farm-manifest.ts"],
4
- "sourcesContent": ["/**\n * Farm Manifest - Manages farm.yaml configuration\n *\n * Represents a farm context containing multiple projects with detached model folders.\n * The farm manifest tracks project registrations and their model folder locations.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile } from \"../utils/file-io.js\";\n\n/**\n * Represents a project registered in a farm\n */\nexport interface FarmProject {\n name: string;\n source: string; // Path to the codebase folder (relative to farm root)\n model: string; // Path to the model folder (relative to farm root)\n remote?: string; // Optional git remote URL\n branch?: string; // Optional git branch\n}\n\n// Validate project object has required fields and key/name consistency\nfunction validateFarmProject(project: any, projectKey: string): FarmProject {\n if (!project || typeof project !== \"object\") {\n throw new Error(\n `Invalid project entry \"${projectKey}\": expected object, got ${typeof project}`\n );\n }\n\n if (typeof project.name !== \"string\" || project.name === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'name' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof project.source !== \"string\" || project.source === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'source' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof project.model !== \"string\" || project.model === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'model' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (project.name !== projectKey) {\n throw new Error(\n `Project name mismatch: key is \"${projectKey}\" but project.name is \"${project.name}\". ` +\n `They must match for consistent sync state tracking.`\n );\n }\n\n return project as FarmProject;\n}\n\n/**\n * Farm manifest data structure\n */\nexport interface FarmManifestData {\n schema: string; // Schema version (e.g., \"dr-farm-v1\")\n name: string; // Human-readable farm name\n created?: string; // ISO 8601 timestamp\n modified?: string; // ISO 8601 timestamp\n projects: Record<string, FarmProject>; // Projects indexed by name\n platform_view?: boolean; // Optional: Enable platform view mode\n sync?: {\n track_commits?: boolean; // Optional: Track commits during sync\n };\n}\n\n/**\n * FarmManifest class - Load, validate, and save farm.yaml\n */\nexport class FarmManifest {\n schema: string;\n name: string;\n created: string;\n modified: string;\n projects: Map<string, FarmProject>;\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n filePath?: string;\n\n constructor(data: FarmManifestData) {\n this.schema = data.schema;\n this.name = data.name;\n this.created = data.created ?? new Date().toISOString();\n this.modified = data.modified ?? new Date().toISOString();\n this.projects = new Map(Object.entries(data.projects || {}));\n this.platform_view = data.platform_view;\n this.sync = data.sync;\n }\n\n /**\n * Load farm manifest from file\n * @param filePath - Path to farm.yaml\n * @returns FarmManifest instance\n * @throws Error if manifest is malformed or projects lack required fields\n */\n static async load(filePath: string): Promise<FarmManifest> {\n const content = await readFile(filePath);\n const data = yaml.parse(content) as FarmManifestData;\n\n if (!data) {\n throw new Error(\n `Invalid farm manifest: file is empty or contains invalid YAML at ${filePath}`\n );\n }\n\n if (!data.schema) {\n throw new Error(\"Farm manifest must have a 'schema' field\");\n }\n\n if (!data.name) {\n throw new Error(\"Farm manifest must have a 'name' field\");\n }\n\n if (!data.projects) {\n data.projects = {};\n } else {\n // Validate each project has required fields before proceeding\n for (const [key, project] of Object.entries(data.projects)) {\n data.projects[key] = validateFarmProject(project, key);\n }\n }\n\n const manifest = new FarmManifest(data);\n manifest.filePath = filePath;\n return manifest;\n }\n\n /**\n * Create a new farm manifest\n * @param name - Farm name\n * @param options - Optional additional properties\n * @returns FarmManifest instance\n */\n static create(\n name: string,\n options: {\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n } = {}\n ): FarmManifest {\n return new FarmManifest({\n schema: \"dr-farm-v1\",\n name,\n created: new Date().toISOString(),\n modified: new Date().toISOString(),\n projects: {},\n platform_view: options.platform_view,\n sync: options.sync,\n });\n }\n\n /**\n * Save manifest to file\n * @param filePath - Path where to save farm.yaml\n */\n async save(filePath?: string): Promise<void> {\n const targetPath = filePath || this.filePath;\n if (!targetPath) {\n throw new Error(\"No file path specified for saving farm manifest\");\n }\n\n this.modified = new Date().toISOString();\n const data = this.toJSON();\n const yamlContent = yaml.stringify(data, { lineWidth: 0 });\n\n await writeFile(targetPath, yamlContent);\n this.filePath = targetPath;\n }\n\n // Add a project to the farm; ensures name parameter matches project.name\n addProject(name: string, project: FarmProject): void {\n if (project.name !== name) {\n throw new Error(\n `Project name mismatch: key is \"${name}\" but project.name is \"${project.name}\". ` +\n `They must match for consistent sync state tracking. ` +\n `Downstream code reads project.name for changeset names and sync state paths.`\n );\n }\n\n this.projects.set(name, project);\n this.modified = new Date().toISOString();\n }\n\n /**\n * Remove a project from the farm\n * @param name - Project name to remove\n * @returns true if project was removed, false if not found\n */\n removeProject(name: string): boolean {\n const existed = this.projects.has(name);\n this.projects.delete(name);\n if (existed) {\n this.modified = new Date().toISOString();\n }\n return existed;\n }\n\n /**\n * Get a project by name\n * @param name - Project name\n * @returns Project configuration or undefined\n */\n getProject(name: string): FarmProject | undefined {\n return this.projects.get(name);\n }\n\n /**\n * Get all projects\n * @returns Array of all projects\n */\n getAllProjects(): FarmProject[] {\n return Array.from(this.projects.values());\n }\n\n /**\n * Check if platform-view is enabled on this farm\n * @returns true if platform-view is enabled\n */\n isPlatformViewEnabled(): boolean {\n return this.platform_view === true;\n }\n\n /**\n * Serialize to JSON representation for saving\n */\n toJSON(): FarmManifestData {\n return {\n schema: this.schema,\n name: this.name,\n created: this.created,\n modified: this.modified,\n projects: Object.fromEntries(this.projects),\n ...(this.platform_view !== undefined && { platform_view: this.platform_view }),\n ...(this.sync && { sync: this.sync }),\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Farm Manifest - Manages farm.yaml configuration\n *\n * Represents a farm context containing multiple projects with detached model folders.\n * The farm manifest tracks project registrations and their model folder locations.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile } from \"../utils/file-io.js\";\n\n/**\n * Represents a project registered in a farm\n */\nexport interface FarmProject {\n name: string;\n source: string; // Path to the codebase folder (relative to farm root)\n model: string; // Path to the model folder (relative to farm root)\n remote?: string; // Optional git remote URL\n branch?: string; // Optional git branch\n}\n\n// Validate project object has required fields and key/name consistency\nfunction validateFarmProject(project: unknown, projectKey: string): FarmProject {\n if (!project || typeof project !== \"object\") {\n throw new Error(\n `Invalid project entry \"${projectKey}\": expected object, got ${typeof project}`\n );\n }\n\n const p = project as Record<string, unknown>;\n\n if (typeof p.name !== \"string\" || p.name === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'name' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof p.source !== \"string\" || p.source === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'source' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof p.model !== \"string\" || p.model === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'model' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (p.name !== projectKey) {\n throw new Error(\n `Project name mismatch: key is \"${projectKey}\" but project.name is \"${p.name}\". ` +\n `They must match for consistent sync state tracking.`\n );\n }\n\n const validatedProject: FarmProject = {\n name: p.name,\n source: p.source,\n model: p.model,\n };\n\n if (typeof p.remote === \"string\") {\n validatedProject.remote = p.remote;\n }\n\n if (typeof p.branch === \"string\") {\n validatedProject.branch = p.branch;\n }\n\n return validatedProject;\n}\n\n/**\n * Farm manifest data structure\n */\nexport interface FarmManifestData {\n schema: string; // Schema version (e.g., \"dr-farm-v1\")\n name: string; // Human-readable farm name\n created?: string; // ISO 8601 timestamp\n modified?: string; // ISO 8601 timestamp\n projects: Record<string, FarmProject>; // Projects indexed by name\n platform_view?: boolean; // Optional: Enable platform view mode\n sync?: {\n track_commits?: boolean; // Optional: Track commits during sync\n };\n}\n\n/**\n * FarmManifest class - Load, validate, and save farm.yaml\n */\nexport class FarmManifest {\n schema: string;\n name: string;\n created: string;\n modified: string;\n projects: Map<string, FarmProject>;\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n filePath?: string;\n\n constructor(data: FarmManifestData) {\n this.schema = data.schema;\n this.name = data.name;\n this.created = data.created ?? new Date().toISOString();\n this.modified = data.modified ?? new Date().toISOString();\n this.projects = new Map(Object.entries(data.projects || {}));\n this.platform_view = data.platform_view;\n this.sync = data.sync;\n }\n\n /**\n * Load farm manifest from file\n * @param filePath - Path to farm.yaml\n * @returns FarmManifest instance\n * @throws Error if manifest is malformed or projects lack required fields\n */\n static async load(filePath: string): Promise<FarmManifest> {\n const content = await readFile(filePath);\n const data = yaml.parse(content) as FarmManifestData;\n\n if (!data) {\n throw new Error(\n `Invalid farm manifest: file is empty or contains invalid YAML at ${filePath}`\n );\n }\n\n if (!data.schema) {\n throw new Error(\"Farm manifest must have a 'schema' field\");\n }\n\n if (!data.name) {\n throw new Error(\"Farm manifest must have a 'name' field\");\n }\n\n if (!data.projects) {\n data.projects = {};\n } else {\n // Validate each project has required fields before proceeding\n for (const [key, project] of Object.entries(data.projects)) {\n data.projects[key] = validateFarmProject(project, key);\n }\n }\n\n const manifest = new FarmManifest(data);\n manifest.filePath = filePath;\n return manifest;\n }\n\n /**\n * Create a new farm manifest\n * @param name - Farm name\n * @param options - Optional additional properties\n * @returns FarmManifest instance\n */\n static create(\n name: string,\n options: {\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n } = {}\n ): FarmManifest {\n return new FarmManifest({\n schema: \"dr-farm-v1\",\n name,\n created: new Date().toISOString(),\n modified: new Date().toISOString(),\n projects: {},\n platform_view: options.platform_view,\n sync: options.sync,\n });\n }\n\n /**\n * Save manifest to file\n * @param filePath - Path where to save farm.yaml\n */\n async save(filePath?: string): Promise<void> {\n const targetPath = filePath || this.filePath;\n if (!targetPath) {\n throw new Error(\"No file path specified for saving farm manifest\");\n }\n\n this.modified = new Date().toISOString();\n const data = this.toJSON();\n const yamlContent = yaml.stringify(data, { lineWidth: 0 });\n\n await writeFile(targetPath, yamlContent);\n this.filePath = targetPath;\n }\n\n // Add a project to the farm; ensures name parameter matches project.name\n addProject(name: string, project: FarmProject): void {\n if (project.name !== name) {\n throw new Error(\n `Project name mismatch: key is \"${name}\" but project.name is \"${project.name}\". ` +\n `They must match for consistent sync state tracking. ` +\n `Downstream code reads project.name for changeset names and sync state paths.`\n );\n }\n\n this.projects.set(name, project);\n this.modified = new Date().toISOString();\n }\n\n /**\n * Remove a project from the farm\n * @param name - Project name to remove\n * @returns true if project was removed, false if not found\n */\n removeProject(name: string): boolean {\n const existed = this.projects.has(name);\n this.projects.delete(name);\n if (existed) {\n this.modified = new Date().toISOString();\n }\n return existed;\n }\n\n /**\n * Get a project by name\n * @param name - Project name\n * @returns Project configuration or undefined\n */\n getProject(name: string): FarmProject | undefined {\n return this.projects.get(name);\n }\n\n /**\n * Get all projects\n * @returns Array of all projects\n */\n getAllProjects(): FarmProject[] {\n return Array.from(this.projects.values());\n }\n\n /**\n * Check if platform-view is enabled on this farm\n * @returns true if platform-view is enabled\n */\n isPlatformViewEnabled(): boolean {\n return this.platform_view === true;\n }\n\n /**\n * Serialize to JSON representation for saving\n */\n toJSON(): FarmManifestData {\n return {\n schema: this.schema,\n name: this.name,\n created: this.created,\n modified: this.modified,\n projects: Object.fromEntries(this.projects),\n ...(this.platform_view !== undefined && { platform_view: this.platform_view }),\n ...(this.sync && { sync: this.sync }),\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -157,7 +157,7 @@ class FarmSyncEngine {
157
157
  async generateChangeset(project, diff, mappings, options = {}) {
158
158
  if (!this.model)
159
159
  throw new Error("Model is required for changeset generation");
160
- const projectModelPath = path.join(this.farmRoot, project.model), stagingManager = new StagingAreaManager(projectModelPath, this.model), changesetName = `farm-sync-${project.name}-${Date.now()}`, changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);
160
+ const stagingManager = new StagingAreaManager(this.model.rootPath, this.model), changesetName = `farm-sync-${project.name}-${Date.now()}`, changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);
161
161
  if (!changeset.id)
162
162
  throw new Error("Failed to create changeset - no ID assigned");
163
163
  const warnings = [];