@documentation-robotics/cli 0.1.13 → 0.3.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 (351) hide show
  1. package/README.md +74 -2
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/call-graph-utils.js +2 -2
  5. package/dist/analyzers/call-graph-utils.js.map +2 -2
  6. package/dist/analyzers/cards/build.js +171 -0
  7. package/dist/analyzers/cards/build.js.map +7 -0
  8. package/dist/analyzers/cards/classify.js +42 -0
  9. package/dist/analyzers/cards/classify.js.map +7 -0
  10. package/dist/analyzers/cards/edge-index.js +44 -0
  11. package/dist/analyzers/cards/edge-index.js.map +7 -0
  12. package/dist/analyzers/cards/facts.js +46 -0
  13. package/dist/analyzers/cards/facts.js.map +7 -0
  14. package/dist/analyzers/cards/graph.js +42 -0
  15. package/dist/analyzers/cards/graph.js.map +7 -0
  16. package/dist/analyzers/cards/index.js +8 -0
  17. package/dist/analyzers/cards/index.js.map +7 -0
  18. package/dist/analyzers/cards/tier0.js +18 -0
  19. package/dist/analyzers/cards/tier0.js.map +7 -0
  20. package/dist/analyzers/cards/types.js +1 -0
  21. package/dist/analyzers/cards/types.js.map +7 -0
  22. package/dist/analyzers/cbm-analyzer.js +195 -171
  23. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  24. package/dist/analyzers/cbm-responses.js +336 -0
  25. package/dist/analyzers/cbm-responses.js.map +7 -0
  26. package/dist/analyzers/edge-relationships.js +45 -0
  27. package/dist/analyzers/edge-relationships.js.map +7 -0
  28. package/dist/analyzers/mapping-loader.js +9 -0
  29. package/dist/analyzers/mapping-loader.js.map +2 -2
  30. package/dist/analyzers/tier0-rules.js +153 -0
  31. package/dist/analyzers/tier0-rules.js.map +7 -0
  32. package/dist/analyzers/types.js.map +1 -1
  33. package/dist/analyzers/verify-engine.js +22 -19
  34. package/dist/analyzers/verify-engine.js.map +2 -2
  35. package/dist/audit/relationships/ai/prompts.js +10 -10
  36. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  37. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  38. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  39. package/dist/cli.js +112 -28
  40. package/dist/cli.js.map +3 -3
  41. package/dist/coding-agents/agents/claude-code.js +3 -1
  42. package/dist/coding-agents/agents/claude-code.js.map +2 -2
  43. package/dist/coding-agents/claude-code-client.js +2 -1
  44. package/dist/coding-agents/claude-code-client.js.map +2 -2
  45. package/dist/coding-agents/copilot-client.js +17 -12
  46. package/dist/coding-agents/copilot-client.js.map +2 -2
  47. package/dist/coding-agents/default-permissions.js +152 -0
  48. package/dist/coding-agents/default-permissions.js.map +7 -0
  49. package/dist/coding-agents/drbot-mcp.js +167 -0
  50. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  51. package/dist/commands/add.js +12 -7
  52. package/dist/commands/add.js.map +2 -2
  53. package/dist/commands/analyzer.js +134 -23
  54. package/dist/commands/analyzer.js.map +2 -2
  55. package/dist/commands/audit.js +6 -6
  56. package/dist/commands/audit.js.map +2 -2
  57. package/dist/commands/changeset-verify.js +55 -0
  58. package/dist/commands/changeset-verify.js.map +7 -0
  59. package/dist/commands/changeset.js +261 -147
  60. package/dist/commands/changeset.js.map +2 -2
  61. package/dist/commands/delete.js +52 -19
  62. package/dist/commands/delete.js.map +3 -3
  63. package/dist/commands/element.js +3 -2
  64. package/dist/commands/element.js.map +2 -2
  65. package/dist/commands/farm.js +773 -0
  66. package/dist/commands/farm.js.map +7 -0
  67. package/dist/commands/init.js +11 -1
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +122 -0
  70. package/dist/commands/mcp.js.map +7 -0
  71. package/dist/commands/relationship.js +251 -47
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +1 -1
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +56 -24
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -11
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +41 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +2 -2
  82. package/dist/commands/version.js.map +2 -2
  83. package/dist/commands/visualize.js +4 -3
  84. package/dist/commands/visualize.js.map +2 -2
  85. package/dist/config.js +83 -0
  86. package/dist/config.js.map +7 -0
  87. package/dist/core/annotation-store.js +138 -0
  88. package/dist/core/annotation-store.js.map +7 -0
  89. package/dist/core/catalog-migration.js +33 -0
  90. package/dist/core/catalog-migration.js.map +7 -0
  91. package/dist/core/changeset-analysis.js +421 -0
  92. package/dist/core/changeset-analysis.js.map +7 -0
  93. package/dist/core/changeset-base-index.js +153 -0
  94. package/dist/core/changeset-base-index.js.map +7 -0
  95. package/dist/core/changeset-brief-delta.js +102 -0
  96. package/dist/core/changeset-brief-delta.js.map +7 -0
  97. package/dist/core/changeset-graph.js +281 -0
  98. package/dist/core/changeset-graph.js.map +7 -0
  99. package/dist/core/changeset-only-guard.js +32 -0
  100. package/dist/core/changeset-only-guard.js.map +7 -0
  101. package/dist/core/changeset-summary.js +68 -0
  102. package/dist/core/changeset-summary.js.map +7 -0
  103. package/dist/core/changeset-validator.js +66 -20
  104. package/dist/core/changeset-validator.js.map +2 -2
  105. package/dist/core/changeset-verify.js +518 -0
  106. package/dist/core/changeset-verify.js.map +7 -0
  107. package/dist/core/changeset.js +56 -9
  108. package/dist/core/changeset.js.map +2 -2
  109. package/dist/core/element-type-migration.js +145 -0
  110. package/dist/core/element-type-migration.js.map +7 -0
  111. package/dist/core/farm-manifest.js +140 -0
  112. package/dist/core/farm-manifest.js.map +7 -0
  113. package/dist/core/farm-sync-engine.js +346 -0
  114. package/dist/core/farm-sync-engine.js.map +7 -0
  115. package/dist/core/farm-sync-state.js +167 -0
  116. package/dist/core/farm-sync-state.js.map +7 -0
  117. package/dist/core/manifest.js +6 -3
  118. package/dist/core/manifest.js.map +2 -2
  119. package/dist/core/migration-registry.js +84 -1
  120. package/dist/core/migration-registry.js.map +2 -2
  121. package/dist/core/model-upgrade.js +102 -0
  122. package/dist/core/model-upgrade.js.map +7 -0
  123. package/dist/core/model.js +257 -44
  124. package/dist/core/model.js.map +3 -3
  125. package/dist/core/mutation-handler.js +71 -6
  126. package/dist/core/mutation-handler.js.map +2 -2
  127. package/dist/core/relationship-migration-table.js +6639 -0
  128. package/dist/core/relationship-migration-table.js.map +7 -0
  129. package/dist/core/relationship-migration.js +272 -0
  130. package/dist/core/relationship-migration.js.map +7 -0
  131. package/dist/core/relationship-prune.js +49 -0
  132. package/dist/core/relationship-prune.js.map +7 -0
  133. package/dist/core/relationship-staging.js +68 -0
  134. package/dist/core/relationship-staging.js.map +7 -0
  135. package/dist/core/relationships.js +26 -4
  136. package/dist/core/relationships.js.map +2 -2
  137. package/dist/core/report-data-model.js +7 -13
  138. package/dist/core/report-data-model.js.map +2 -2
  139. package/dist/core/staged-changeset-storage.js +64 -17
  140. package/dist/core/staged-changeset-storage.js.map +2 -2
  141. package/dist/core/staged-source-reference.js +28 -0
  142. package/dist/core/staged-source-reference.js.map +7 -0
  143. package/dist/core/staging-area.js +83 -18
  144. package/dist/core/staging-area.js.map +2 -2
  145. package/dist/core/virtual-projection.js +14 -5
  146. package/dist/core/virtual-projection.js.map +2 -2
  147. package/dist/export/archimate-exporter.js +1 -0
  148. package/dist/export/archimate-exporter.js.map +2 -2
  149. package/dist/generated/compiled-validators.js +2 -2
  150. package/dist/generated/compiled-validators.js.map +2 -2
  151. package/dist/generated/layer-registry.js +16 -16
  152. package/dist/generated/layer-registry.js.map +2 -2
  153. package/dist/generated/node-types.js +186 -168
  154. package/dist/generated/node-types.js.map +2 -2
  155. package/dist/generated/relationship-index.js +2209 -7727
  156. package/dist/generated/relationship-index.js.map +2 -2
  157. package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
  158. package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
  159. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  160. package/dist/integrations/claude_code/commands/dr-map.md +13 -0
  161. package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
  162. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  163. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  164. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  165. package/dist/integrations/claude_code/docs/MCP_SERVER.md +79 -0
  166. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  167. package/dist/integrations/claude_code/docs/README.md +52 -1
  168. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  169. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  170. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  171. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  172. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  173. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  174. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  175. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  176. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  177. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  178. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  179. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  180. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
  181. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
  182. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
  183. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
  184. package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
  185. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  186. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  187. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  188. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  189. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  190. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  191. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  192. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  193. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  194. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  195. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  196. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  197. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
  198. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
  199. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  200. package/dist/integrations/hash-utils.js.map +2 -2
  201. package/dist/mcp/api-key-manager.js +111 -0
  202. package/dist/mcp/api-key-manager.js.map +7 -0
  203. package/dist/mcp/changeset-only.js +34 -0
  204. package/dist/mcp/changeset-only.js.map +7 -0
  205. package/dist/mcp/http-transport.js +176 -0
  206. package/dist/mcp/http-transport.js.map +7 -0
  207. package/dist/mcp/resource-registry.js +97 -0
  208. package/dist/mcp/resource-registry.js.map +7 -0
  209. package/dist/mcp/tool-registry.js +60 -0
  210. package/dist/mcp/tool-registry.js.map +7 -0
  211. package/dist/mcp/tools/annotation-create.js +39 -0
  212. package/dist/mcp/tools/annotation-create.js.map +7 -0
  213. package/dist/mcp/tools/annotation-delete.js +29 -0
  214. package/dist/mcp/tools/annotation-delete.js.map +7 -0
  215. package/dist/mcp/tools/annotation-get.js +29 -0
  216. package/dist/mcp/tools/annotation-get.js.map +7 -0
  217. package/dist/mcp/tools/annotation-list.js +24 -0
  218. package/dist/mcp/tools/annotation-list.js.map +7 -0
  219. package/dist/mcp/tools/annotation-reply.js +34 -0
  220. package/dist/mcp/tools/annotation-reply.js.map +7 -0
  221. package/dist/mcp/tools/annotation-shared.js +10 -0
  222. package/dist/mcp/tools/annotation-shared.js.map +7 -0
  223. package/dist/mcp/tools/annotation-update.js +39 -0
  224. package/dist/mcp/tools/annotation-update.js.map +7 -0
  225. package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
  226. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  227. package/dist/mcp/tools/changeset-editing.js +184 -0
  228. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  229. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  230. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  231. package/dist/mcp/tools/changeset-list.js +33 -0
  232. package/dist/mcp/tools/changeset-list.js.map +7 -0
  233. package/dist/mcp/tools/changeset-shared.js +31 -0
  234. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  235. package/dist/mcp/tools/changeset-show.js +55 -0
  236. package/dist/mcp/tools/changeset-show.js.map +7 -0
  237. package/dist/mcp/tools/changeset-verify.js +43 -0
  238. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  239. package/dist/mcp/tools/chat-launch.js +50 -0
  240. package/dist/mcp/tools/chat-launch.js.map +7 -0
  241. package/dist/mcp/tools/chat-status.js +26 -0
  242. package/dist/mcp/tools/chat-status.js.map +7 -0
  243. package/dist/mcp/tools/index.js +65 -0
  244. package/dist/mcp/tools/index.js.map +7 -0
  245. package/dist/mcp/tools/model-add.js +107 -0
  246. package/dist/mcp/tools/model-add.js.map +7 -0
  247. package/dist/mcp/tools/model-delete.js +99 -0
  248. package/dist/mcp/tools/model-delete.js.map +7 -0
  249. package/dist/mcp/tools/model-export.js +72 -0
  250. package/dist/mcp/tools/model-export.js.map +7 -0
  251. package/dist/mcp/tools/model-info.js +51 -0
  252. package/dist/mcp/tools/model-info.js.map +7 -0
  253. package/dist/mcp/tools/model-list.js +53 -0
  254. package/dist/mcp/tools/model-list.js.map +7 -0
  255. package/dist/mcp/tools/model-reload.js +34 -0
  256. package/dist/mcp/tools/model-reload.js.map +7 -0
  257. package/dist/mcp/tools/model-search.js +102 -0
  258. package/dist/mcp/tools/model-search.js.map +7 -0
  259. package/dist/mcp/tools/model-show.js +68 -0
  260. package/dist/mcp/tools/model-show.js.map +7 -0
  261. package/dist/mcp/tools/model-stats.js +22 -0
  262. package/dist/mcp/tools/model-stats.js.map +7 -0
  263. package/dist/mcp/tools/model-trace.js +118 -0
  264. package/dist/mcp/tools/model-trace.js.map +7 -0
  265. package/dist/mcp/tools/model-update.js +121 -0
  266. package/dist/mcp/tools/model-update.js.map +7 -0
  267. package/dist/mcp/tools/model-validate.js +78 -0
  268. package/dist/mcp/tools/model-validate.js.map +7 -0
  269. package/dist/mcp/tools/relationship-tools.js +98 -0
  270. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  271. package/dist/mcp/tools/shared.js +126 -0
  272. package/dist/mcp/tools/shared.js.map +7 -0
  273. package/dist/mcp/tools/spec-type.js +90 -0
  274. package/dist/mcp/tools/spec-type.js.map +7 -0
  275. package/dist/reports/model-layer-report-generator.js +5 -3
  276. package/dist/reports/model-layer-report-generator.js.map +2 -2
  277. package/dist/reports/model-readme-generator.js +116 -0
  278. package/dist/reports/model-readme-generator.js.map +7 -0
  279. package/dist/reports/model-report-data.js +37 -1
  280. package/dist/reports/model-report-data.js.map +2 -2
  281. package/dist/reports/model-report-orchestrator.js +29 -6
  282. package/dist/reports/model-report-orchestrator.js.map +2 -2
  283. package/dist/reports/model-report-utils.js +9 -0
  284. package/dist/reports/model-report-utils.js.map +7 -0
  285. package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
  286. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  287. package/dist/schemas/bundled/api.json +1029 -1658
  288. package/dist/schemas/bundled/apm.json +499 -734
  289. package/dist/schemas/bundled/application.json +745 -370
  290. package/dist/schemas/bundled/base.json +3 -220
  291. package/dist/schemas/bundled/business.json +452 -366
  292. package/dist/schemas/bundled/data-model.json +403 -383
  293. package/dist/schemas/bundled/data-store.json +334 -558
  294. package/dist/schemas/bundled/manifest.json +18 -18
  295. package/dist/schemas/bundled/motivation.json +349 -368
  296. package/dist/schemas/bundled/navigation.json +415 -731
  297. package/dist/schemas/bundled/product.json +325 -159
  298. package/dist/schemas/bundled/security.json +864 -1232
  299. package/dist/schemas/bundled/technology.json +574 -703
  300. package/dist/schemas/bundled/testing.json +718 -1643
  301. package/dist/schemas/bundled/ux.json +739 -1352
  302. package/dist/server/schemas.js +8 -0
  303. package/dist/server/schemas.js.map +2 -2
  304. package/dist/server/server.js +234 -31
  305. package/dist/server/server.js.map +2 -2
  306. package/dist/server-entry.js +2 -1
  307. package/dist/server-entry.js.map +2 -2
  308. package/dist/telemetry/config.js +2 -40
  309. package/dist/telemetry/config.js.map +2 -2
  310. package/dist/telemetry/index.js +1 -1
  311. package/dist/types/index.js.map +2 -2
  312. package/dist/utils/ansi-suppressor.js +23 -0
  313. package/dist/utils/ansi-suppressor.js.map +7 -0
  314. package/dist/utils/attribute-patch.js +41 -0
  315. package/dist/utils/attribute-patch.js.map +7 -0
  316. package/dist/utils/color-utils.js +34 -0
  317. package/dist/utils/color-utils.js.map +7 -0
  318. package/dist/utils/element-reference-shape.js +15 -1
  319. package/dist/utils/element-reference-shape.js.map +3 -3
  320. package/dist/utils/error-hints.js +29 -0
  321. package/dist/utils/error-hints.js.map +7 -0
  322. package/dist/utils/errors.js +75 -10
  323. package/dist/utils/errors.js.map +2 -2
  324. package/dist/utils/globals.js +17 -1
  325. package/dist/utils/globals.js.map +2 -2
  326. package/dist/utils/project-paths.js +50 -3
  327. package/dist/utils/project-paths.js.map +2 -2
  328. package/dist/utils/reference-path-parser.js +49 -0
  329. package/dist/utils/reference-path-parser.js.map +7 -0
  330. package/dist/utils/source-reference.js +17 -10
  331. package/dist/utils/source-reference.js.map +2 -2
  332. package/dist/utils/spec-version.js +10 -2
  333. package/dist/utils/spec-version.js.map +2 -2
  334. package/dist/validators/composed-reference-validator.js +240 -0
  335. package/dist/validators/composed-reference-validator.js.map +7 -0
  336. package/dist/validators/reference-validator.js +57 -13
  337. package/dist/validators/reference-validator.js.map +3 -3
  338. package/dist/validators/relationship-schema-validator.js +33 -5
  339. package/dist/validators/relationship-schema-validator.js.map +3 -3
  340. package/dist/validators/schema-validator.js +27 -9
  341. package/dist/validators/schema-validator.js.map +2 -2
  342. package/dist/validators/types.js.map +1 -1
  343. package/dist/viewer/README.md +6 -6
  344. package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
  345. package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
  346. package/dist/viewer/index.html +2 -2
  347. package/dist/viewer/manifest.json +10 -10
  348. package/dist/viewer/version.json +1 -1
  349. package/package.json +7 -3
  350. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  351. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -0,0 +1,153 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileExists } from "../utils/file-io.js";
5
+ import { getErrorMessage } from "../utils/errors.js";
6
+ import { Changeset } from "./changeset.js";
7
+ import { ChangesetGraph, relationshipKey } from "./changeset-graph.js";
8
+ const BASE_INDEX_FILE = "base-index.json", DRIFT_LIST_LIMIT = 50;
9
+ function indexPath(rootPath, changesetId) {
10
+ return path.join(rootPath, "documentation-robotics", "changesets", changesetId, BASE_INDEX_FILE);
11
+ }
12
+ function contentHash(element) {
13
+ const content = {
14
+ type: element.type,
15
+ name: element.name,
16
+ description: element.description ?? null,
17
+ attributes: element.attributes ?? {},
18
+ sourceReference: element.sourceReference ?? null
19
+ };
20
+ return createHash("sha256").update(stableStringify(content)).digest("hex").slice(0, 16);
21
+ }
22
+ async function baseGraph(model) {
23
+ const now = (/* @__PURE__ */ new Date()).toISOString(), empty = new Changeset({
24
+ id: "base",
25
+ name: "base model",
26
+ created: now,
27
+ modified: now,
28
+ status: "committed",
29
+ baseSnapshot: "",
30
+ changes: []
31
+ });
32
+ return ChangesetGraph.build(model, empty);
33
+ }
34
+ function indexFromGraph(graph) {
35
+ const elements = {};
36
+ for (const element of graph.elements.values())
37
+ element.status !== "deleted" && (elements[element.id] = contentHash(element));
38
+ return {
39
+ version: 1,
40
+ captured: (/* @__PURE__ */ new Date()).toISOString(),
41
+ elements,
42
+ relationships: graph.relationships().filter((r) => r.status !== "deleted").map((r) => relationshipKey(r)).sort()
43
+ };
44
+ }
45
+ async function captureBaseIndex(model) {
46
+ return indexFromGraph(await baseGraph(model));
47
+ }
48
+ async function writeBaseIndex(rootPath, changesetId, index) {
49
+ await writeFile(indexPath(rootPath, changesetId), JSON.stringify(index), "utf-8");
50
+ }
51
+ async function readBaseIndex(rootPath, changesetId) {
52
+ const file = indexPath(rootPath, changesetId);
53
+ if (!await fileExists(file))
54
+ return;
55
+ const parsed = JSON.parse(await readFile(file, "utf-8"));
56
+ if (parsed.version !== 1 || typeof parsed.elements != "object" || !Array.isArray(parsed.relationships))
57
+ throw new Error(`${file} is not a base index this version of dr can read`);
58
+ return parsed;
59
+ }
60
+ async function tryReadBaseIndex(rootPath, changesetId) {
61
+ try {
62
+ const index = await readBaseIndex(rootPath, changesetId);
63
+ return index ? { index, status: "recorded" } : { status: "not-recorded" };
64
+ } catch (error) {
65
+ return {
66
+ status: "unreadable",
67
+ warning: `The changeset's base index could not be read (${getErrorMessage(error)}); treating it as not recorded.`
68
+ };
69
+ }
70
+ }
71
+ function parseKey(key) {
72
+ const [source, predicate, target] = key.split("::");
73
+ return { source, predicate, target };
74
+ }
75
+ async function describeDrift(model, graph, hash) {
76
+ const changeset = graph.changeset, warnings = [...hash?.warnings ?? []], overlapping = { elements: [], relationships: [] }, inBase = graph.alreadyInBase();
77
+ for (const element of inBase.elements)
78
+ overlapping.elements.push({ id: element.id, reason: "already-in-base" });
79
+ for (const rel of inBase.relationships)
80
+ overlapping.relationships.push({ source: rel.source, predicate: rel.predicate, target: rel.target, reason: "already-in-base" });
81
+ const net = /* @__PURE__ */ new Map(), elementOps = /* @__PURE__ */ new Map(), addedHere = /* @__PURE__ */ new Set();
82
+ for (const change of changeset.changes)
83
+ if (change.type === "relationship-add" || change.type === "relationship-delete") {
84
+ const data = change.type === "relationship-delete" ? change.before ?? change.after : change.after, [source, predicate, target] = data?.source && data.predicate && data.target ? [data.source, data.predicate, data.target] : change.elementId.split("::");
85
+ if (!source || !predicate || !target)
86
+ continue;
87
+ const key = relationshipKey({ source: graph.resolve(source), predicate, target: graph.resolve(target) });
88
+ change.type === "relationship-add" && !net.has(key) && addedHere.add(key), net.set(key, change.type === "relationship-add" ? "add" : "delete");
89
+ } else {
90
+ const id = graph.resolve(change.elementId);
91
+ change.type === "add" && !elementOps.has(id) && addedHere.add(id), elementOps.set(id, change.type);
92
+ }
93
+ for (const [key, op] of net) {
94
+ if (op !== "delete" || addedHere.has(key))
95
+ continue;
96
+ const ref = parseKey(key);
97
+ graph.getRelationship(ref.source, ref.predicate, ref.target) || overlapping.relationships.push({ ...ref, reason: "already-removed" });
98
+ }
99
+ for (const [id, op] of elementOps)
100
+ op === "delete" && !addedHere.has(id) && !graph.getElement(id) && overlapping.elements.push({ id, reason: "already-removed" });
101
+ const read = await tryReadBaseIndex(model.rootPath, changeset.id);
102
+ read.warning && warnings.push(read.warning);
103
+ let baseChanges;
104
+ if (read.index) {
105
+ const then = read.index, now = indexFromGraph(await baseGraph(model)), elementsAdded = Object.keys(now.elements).filter((id) => !(id in then.elements)).sort(), elementsRemoved = Object.keys(then.elements).filter((id) => !(id in now.elements)).sort(), elementsChanged = Object.keys(now.elements).filter((id) => id in then.elements && then.elements[id] !== now.elements[id]).sort(), thenRels = new Set(then.relationships), nowRels = new Set(now.relationships), relationshipsAdded = now.relationships.filter((k) => !thenRels.has(k)), relationshipsRemoved = then.relationships.filter((k) => !nowRels.has(k));
106
+ for (const id of elementsChanged) {
107
+ const op = elementOps.get(id);
108
+ (op === "update" || op === "delete") && overlapping.elements.push({ id, reason: "changed-in-base" });
109
+ }
110
+ const cut = (list) => list.slice(0, DRIFT_LIST_LIMIT);
111
+ baseChanges = {
112
+ counts: {
113
+ elementsAdded: elementsAdded.length,
114
+ elementsRemoved: elementsRemoved.length,
115
+ elementsChanged: elementsChanged.length,
116
+ relationshipsAdded: relationshipsAdded.length,
117
+ relationshipsRemoved: relationshipsRemoved.length
118
+ },
119
+ elementsAdded: cut(elementsAdded),
120
+ elementsRemoved: cut(elementsRemoved),
121
+ elementsChanged: cut(elementsChanged),
122
+ relationshipsAdded: cut(relationshipsAdded).map(parseKey),
123
+ relationshipsRemoved: cut(relationshipsRemoved).map(parseKey),
124
+ truncated: [elementsAdded, elementsRemoved, elementsChanged, relationshipsAdded, relationshipsRemoved].some((l) => l.length > DRIFT_LIST_LIMIT)
125
+ };
126
+ } else read.status === "not-recorded" && (hash?.isDrifted || overlapping.elements.length + overlapping.relationships.length > 0) && warnings.push("No base index was recorded for this changeset (it predates them), so what changed in the base cannot be listed beyond the staged changes it overlaps.");
127
+ const baseChanged = !!baseChanges && Object.values(baseChanges.counts).some((n) => n > 0), overlapCount = overlapping.elements.length + overlapping.relationships.length, isDrifted = (hash?.isDrifted ?? !1) || baseChanged || overlapCount > 0;
128
+ return overlapCount > 0 && warnings.push(`The base now overlaps ${overlapCount} staged change(s): ` + [
129
+ ...overlapping.elements.map((e) => `${e.id} (${e.reason})`),
130
+ ...overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`)
131
+ ].slice(0, 10).join(", ") + (overlapCount > 10 ? ", ..." : "")), {
132
+ isDrifted,
133
+ warnings,
134
+ ...hash?.baseSnapshotId ? { baseSnapshotId: hash.baseSnapshotId } : {},
135
+ ...hash?.currentSnapshotId ? { currentSnapshotId: hash.currentSnapshotId } : {},
136
+ baseIndex: read.status,
137
+ overlapping,
138
+ ...baseChanges ? { baseChanges } : {}
139
+ };
140
+ }
141
+ function stableStringify(value) {
142
+ return value === void 0 ? "null" : value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
143
+ }
144
+ export {
145
+ BASE_INDEX_FILE,
146
+ DRIFT_LIST_LIMIT,
147
+ captureBaseIndex,
148
+ describeDrift,
149
+ readBaseIndex,
150
+ tryReadBaseIndex,
151
+ writeBaseIndex
152
+ };
153
+ //# sourceMappingURL=changeset-base-index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/changeset-base-index.ts"],
4
+ "sourcesContent": ["/**\n * Changeset base index \u2014 which elements and relationships the base model had when a changeset was\n * created, so later readers can tell what predates the plan and what changed in the base under it.\n *\n * The changeset's `baseSnapshot` is a single hash of the whole model: it says the base changed, not\n * what changed. The index records each base element's id with a short content hash, and each base\n * relationship's key, in `documentation-robotics/changesets/{id}/base-index.json`. It is written\n * when the changeset is created. Changesets created before it existed (or imported) have none, and\n * every reader falls back to what it can tell without one and says so.\n *\n * Readers:\n * - `changeset_verify` excludes elements that existed before the plan from the candidates for a\n * planned add (they cannot be its built form).\n * - `changeset_brief` names what changed in the base since the plan was created (`describeDrift`).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileExists } from \"../utils/file-io.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { Changeset } from \"./changeset.js\";\nimport { ChangesetGraph, relationshipKey, type GraphElement } from \"./changeset-graph.js\";\nimport type { Model } from \"./model.js\";\n\nexport const BASE_INDEX_FILE = \"base-index.json\";\n\nexport interface BaseIndex {\n version: 1;\n captured: string;\n /** Element id (dotted path) \u2192 short hash of its content. */\n elements: Record<string, string>;\n /** Relationship keys, `source::predicate::target`, endpoints as dotted paths. */\n relationships: string[];\n}\n\nexport interface RelationshipRef {\n source: string;\n predicate: string;\n target: string;\n}\n\n/** How many ids each drift list carries before it is cut short (the counts stay exact). */\nexport const DRIFT_LIST_LIMIT = 50;\n\nexport interface ChangesetDrift {\n /** Kept for compatibility: true when the base changed since the changeset was created. */\n isDrifted: boolean;\n warnings: string[];\n baseSnapshotId?: string;\n currentSnapshotId?: string;\n /** Whether a base index was recorded for the changeset; without one, `baseChanges` is absent. */\n baseIndex: \"recorded\" | \"not-recorded\" | \"unreadable\";\n /**\n * Staged changes that the base now overlaps: adds whose element or relationship the base already\n * has, deletes of what the base no longer has, and (with a base index) elements the plan updates\n * or deletes whose base content changed since the plan was created.\n */\n overlapping: {\n elements: Array<{ id: string; reason: \"already-in-base\" | \"already-removed\" | \"changed-in-base\" }>;\n relationships: Array<RelationshipRef & { reason: \"already-in-base\" | \"already-removed\" }>;\n };\n /** What changed in the base since the changeset was created; needs a base index. */\n baseChanges?: {\n counts: {\n elementsAdded: number;\n elementsRemoved: number;\n elementsChanged: number;\n relationshipsAdded: number;\n relationshipsRemoved: number;\n };\n elementsAdded: string[];\n elementsRemoved: string[];\n elementsChanged: string[];\n relationshipsAdded: RelationshipRef[];\n relationshipsRemoved: RelationshipRef[];\n /** True when a list was cut at DRIFT_LIST_LIMIT entries. */\n truncated: boolean;\n };\n}\n\nfunction indexPath(rootPath: string, changesetId: string): string {\n return path.join(rootPath, \"documentation-robotics\", \"changesets\", changesetId, BASE_INDEX_FILE);\n}\n\nfunction contentHash(element: GraphElement): string {\n const content = {\n type: element.type,\n name: element.name,\n description: element.description ?? null,\n attributes: element.attributes ?? {},\n sourceReference: element.sourceReference ?? null,\n };\n return createHash(\"sha256\").update(stableStringify(content)).digest(\"hex\").slice(0, 16);\n}\n\n/** The base model (never an active changeset's projection) as a graph with no changes. */\nasync function baseGraph(model: Model): Promise<ChangesetGraph> {\n const now = new Date().toISOString();\n const empty = new Changeset({\n id: \"base\",\n name: \"base model\",\n created: now,\n modified: now,\n status: \"committed\",\n baseSnapshot: \"\",\n changes: [],\n });\n return ChangesetGraph.build(model, empty);\n}\n\nfunction indexFromGraph(graph: ChangesetGraph): BaseIndex {\n const elements: Record<string, string> = {};\n for (const element of graph.elements.values()) {\n if (element.status !== \"deleted\") elements[element.id] = contentHash(element);\n }\n return {\n version: 1,\n captured: new Date().toISOString(),\n elements,\n relationships: graph\n .relationships()\n .filter((r) => r.status !== \"deleted\")\n .map((r) => relationshipKey(r))\n .sort(),\n };\n}\n\n/** Indexes the base model as it is now. */\nexport async function captureBaseIndex(model: Model): Promise<BaseIndex> {\n return indexFromGraph(await baseGraph(model));\n}\n\n/** Records the base index for a changeset that was just created. */\nexport async function writeBaseIndex(rootPath: string, changesetId: string, index: BaseIndex): Promise<void> {\n await writeFile(indexPath(rootPath, changesetId), JSON.stringify(index), \"utf-8\");\n}\n\n/**\n * Reads a changeset's base index. `undefined` when none was recorded; throws when the file exists\n * but cannot be read, so callers can report it rather than mistake it for \"not recorded\".\n */\nexport async function readBaseIndex(rootPath: string, changesetId: string): Promise<BaseIndex | undefined> {\n const file = indexPath(rootPath, changesetId);\n if (!(await fileExists(file))) return undefined;\n const parsed = JSON.parse(await readFile(file, \"utf-8\")) as Partial<BaseIndex>;\n if (parsed.version !== 1 || typeof parsed.elements !== \"object\" || !Array.isArray(parsed.relationships)) {\n throw new Error(`${file} is not a base index this version of dr can read`);\n }\n return parsed as BaseIndex;\n}\n\n/** Like `readBaseIndex`, but reports an unreadable index as a warning instead of throwing. */\nexport async function tryReadBaseIndex(\n rootPath: string,\n changesetId: string\n): Promise<{ index?: BaseIndex; status: ChangesetDrift[\"baseIndex\"]; warning?: string }> {\n try {\n const index = await readBaseIndex(rootPath, changesetId);\n return index ? { index, status: \"recorded\" } : { status: \"not-recorded\" };\n } catch (error) {\n return {\n status: \"unreadable\",\n warning: `The changeset's base index could not be read (${getErrorMessage(error)}); treating it as not recorded.`,\n };\n }\n}\n\nfunction parseKey(key: string): RelationshipRef {\n const [source, predicate, target] = key.split(\"::\");\n return { source, predicate, target };\n}\n\n/**\n * Names how the base model changed under a changeset: staged changes the base now overlaps and,\n * with a base index, what was added, removed or changed in the base since the changeset was created.\n *\n * `graph` is the changeset's projected graph; `hash` is the hash-based drift check\n * (`StagingAreaManager.detectDrift`), when it ran.\n */\nexport async function describeDrift(\n model: Model,\n graph: ChangesetGraph,\n hash?: { isDrifted: boolean; warnings: string[]; baseSnapshotId?: string; currentSnapshotId?: string }\n): Promise<ChangesetDrift> {\n const changeset = graph.changeset;\n const warnings = [...(hash?.warnings ?? [])];\n const overlapping: ChangesetDrift[\"overlapping\"] = { elements: [], relationships: [] };\n\n const inBase = graph.alreadyInBase();\n for (const element of inBase.elements) overlapping.elements.push({ id: element.id, reason: \"already-in-base\" });\n for (const rel of inBase.relationships) {\n overlapping.relationships.push({ source: rel.source, predicate: rel.predicate, target: rel.target, reason: \"already-in-base\" });\n }\n\n // Staged deletes of what the base no longer has.\n const net = new Map<string, \"add\" | \"delete\">();\n const elementOps = new Map<string, string>();\n const addedHere = new Set<string>();\n for (const change of changeset.changes) {\n if (change.type === \"relationship-add\" || change.type === \"relationship-delete\") {\n const data = (change.type === \"relationship-delete\" ? (change.before ?? change.after) : change.after) as\n | Partial<RelationshipRef>\n | undefined;\n const [source, predicate, target] =\n data?.source && data.predicate && data.target\n ? [data.source, data.predicate, data.target]\n : change.elementId.split(\"::\");\n if (!source || !predicate || !target) continue;\n const key = relationshipKey({ source: graph.resolve(source), predicate, target: graph.resolve(target) });\n if (change.type === \"relationship-add\" && !net.has(key)) addedHere.add(key);\n net.set(key, change.type === \"relationship-add\" ? \"add\" : \"delete\");\n } else {\n const id = graph.resolve(change.elementId);\n if (change.type === \"add\" && !elementOps.has(id)) addedHere.add(id);\n elementOps.set(id, change.type);\n }\n }\n for (const [key, op] of net) {\n if (op !== \"delete\" || addedHere.has(key)) continue;\n const ref = parseKey(key);\n if (!graph.getRelationship(ref.source, ref.predicate, ref.target)) {\n overlapping.relationships.push({ ...ref, reason: \"already-removed\" });\n }\n }\n for (const [id, op] of elementOps) {\n if (op === \"delete\" && !addedHere.has(id) && !graph.getElement(id)) overlapping.elements.push({ id, reason: \"already-removed\" });\n }\n\n const read = await tryReadBaseIndex(model.rootPath, changeset.id);\n if (read.warning) warnings.push(read.warning);\n\n let baseChanges: ChangesetDrift[\"baseChanges\"];\n if (read.index) {\n const then = read.index;\n const now = indexFromGraph(await baseGraph(model));\n const elementsAdded = Object.keys(now.elements).filter((id) => !(id in then.elements)).sort();\n const elementsRemoved = Object.keys(then.elements).filter((id) => !(id in now.elements)).sort();\n const elementsChanged = Object.keys(now.elements)\n .filter((id) => id in then.elements && then.elements[id] !== now.elements[id])\n .sort();\n const thenRels = new Set(then.relationships);\n const nowRels = new Set(now.relationships);\n const relationshipsAdded = now.relationships.filter((k) => !thenRels.has(k));\n const relationshipsRemoved = then.relationships.filter((k) => !nowRels.has(k));\n\n for (const id of elementsChanged) {\n const op = elementOps.get(id);\n if (op === \"update\" || op === \"delete\") overlapping.elements.push({ id, reason: \"changed-in-base\" });\n }\n\n const cut = <T>(list: T[]): T[] => list.slice(0, DRIFT_LIST_LIMIT);\n baseChanges = {\n counts: {\n elementsAdded: elementsAdded.length,\n elementsRemoved: elementsRemoved.length,\n elementsChanged: elementsChanged.length,\n relationshipsAdded: relationshipsAdded.length,\n relationshipsRemoved: relationshipsRemoved.length,\n },\n elementsAdded: cut(elementsAdded),\n elementsRemoved: cut(elementsRemoved),\n elementsChanged: cut(elementsChanged),\n relationshipsAdded: cut(relationshipsAdded).map(parseKey),\n relationshipsRemoved: cut(relationshipsRemoved).map(parseKey),\n truncated: [elementsAdded, elementsRemoved, elementsChanged, relationshipsAdded, relationshipsRemoved].some(\n (l) => l.length > DRIFT_LIST_LIMIT\n ),\n };\n } else if (read.status === \"not-recorded\" && (hash?.isDrifted || overlapping.elements.length + overlapping.relationships.length > 0)) {\n warnings.push(\n \"No base index was recorded for this changeset (it predates them), so what changed in the base cannot be \" +\n \"listed beyond the staged changes it overlaps.\"\n );\n }\n\n const baseChanged =\n !!baseChanges &&\n Object.values(baseChanges.counts).some((n) => n > 0);\n const overlapCount = overlapping.elements.length + overlapping.relationships.length;\n const isDrifted = (hash?.isDrifted ?? false) || baseChanged || overlapCount > 0;\n if (overlapCount > 0) {\n warnings.push(\n `The base now overlaps ${overlapCount} staged change(s): ` +\n [\n ...overlapping.elements.map((e) => `${e.id} (${e.reason})`),\n ...overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`),\n ]\n .slice(0, 10)\n .join(\", \") +\n (overlapCount > 10 ? \", ...\" : \"\")\n );\n }\n\n return {\n isDrifted,\n warnings,\n ...(hash?.baseSnapshotId ? { baseSnapshotId: hash.baseSnapshotId } : {}),\n ...(hash?.currentSnapshotId ? { currentSnapshotId: hash.currentSnapshotId } : {}),\n baseIndex: read.status,\n overlapping,\n ...(baseChanges ? { baseChanges } : {}),\n };\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === undefined) return \"null\";\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k])}`);\n return `{${entries.join(\",\")}}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,102 @@
1
+ import { buildBrief, computeUnwired, unwiredKey } from "./changeset-analysis.js";
2
+ import { ChangesetGraph, changesetRevisionToken, parseRevisionToken, relationshipKey, unstagedSince } from "./changeset-graph.js";
3
+ import { summarizeChange } from "./changeset-summary.js";
4
+ const NOTE = "The earlier state is rebuilt from the changes that made up the plan at `since` (unstaged ones from the changeset's unstage record), applied to today's base model; base drift since then is not separated out. It is approximate when a change staged by then was rewritten in place afterwards: pass the brief's revisionToken as `since` to be warned when that happened.";
5
+ async function briefDelta(model, now, since) {
6
+ let revision;
7
+ const warnings = [], changeset = now.graph.changeset;
8
+ if (typeof since == "string") {
9
+ const parsed = parseRevisionToken(since);
10
+ if (!parsed)
11
+ throw new Error(`since "${since}" is neither a revision number nor a revision token (<revision>:<hash>) from a brief`);
12
+ revision = parsed.revision;
13
+ const today = changesetRevisionToken(changeset, revision);
14
+ revision <= now.brief.revision && today !== since && warnings.push(`The plan at revision ${revision} is not the one that revision token was taken from: a change up to it was rewritten in place since then (or the changeset was rewritten, e.g. by an upgrade). The earlier state below is rebuilt from what is recorded now, so this delta is approximate.`);
15
+ } else
16
+ revision = since;
17
+ const prunedThrough = changeset.unstagedPrunedThrough;
18
+ prunedThrough !== void 0 && revision < prunedThrough && warnings.push(`Unstage tombstones up to revision ${prunedThrough} were dropped to keep the record bounded, so changes unstaged between revision ${revision} and then may be missing from the rebuilt earlier state and from \`unstaged\`; this delta is approximate.`);
19
+ const earlier = await ChangesetGraph.build(model, changeset, { throughSequence: revision }), then = {
20
+ brief: buildBrief({ graph: earlier, unwired: "none" }),
21
+ unwired: computeUnwired(earlier),
22
+ graph: earlier
23
+ }, delta = diffBriefs(then, { brief: now.brief, unwired: computeUnwired(now.graph), graph: now.graph }, revision, warnings);
24
+ return delta.unstaged = unstagedSince(changeset, revision).map((t, index) => ({
25
+ ...summarizeChange({ ...t.change, sequenceNumber: t.sequence }, index),
26
+ removedAt: t.removedAt
27
+ })), delta;
28
+ }
29
+ function diffBriefs(then, now, since, extraWarnings = []) {
30
+ const revision = now.brief.revision, warnings = [...extraWarnings];
31
+ since > revision && warnings.push(`since (${since}) is past the current revision (${revision}), so it is not a revision of this plan; the delta compares the changeset with itself.`);
32
+ const changedIds = (brief) => brief.layers.flatMap((l) => l.changes.map((c) => c.id)), thenIds = new Set(changedIds(then.brief)), nowIds = new Set(changedIds(now.brief)), gapKey = (g) => `${g.kind}|${g.element}|${g.kind === "peer-pattern" ? g.detail.replace(/^\d+ of \d+ /, "") : g.detail}`, thenGaps = new Map(then.brief.gaps.map((g) => [gapKey(g), g])), nowGaps = new Map(now.brief.gaps.map((g) => [gapKey(g), g])), flatten = (list) => {
33
+ const map = /* @__PURE__ */ new Map();
34
+ for (const { element, byLayer } of list)
35
+ for (const { layer, entries } of byLayer)
36
+ for (const entry of entries)
37
+ map.set(unwiredKey(element, entry), { element, layer, ...entry });
38
+ return map;
39
+ }, thenUnwired = flatten(then.unwired), nowUnwired = flatten(now.unwired), thenScoped = new Set(then.unwired.map((u) => u.element)), nowScoped = new Set(now.unwired.map((u) => u.element)), traced = (brief) => new Set(brief.intent.traced.map((t) => t.element)), untraced = (brief) => new Set(brief.intent.untraced), thenTraced = traced(then.brief), thenUntraced = untraced(then.brief), thenReach = new Set(then.brief.reach.touched), nowReach = new Set(now.brief.reach.touched);
40
+ return {
41
+ since,
42
+ revision,
43
+ elements: {
44
+ added: [...nowIds].filter((id) => !thenIds.has(id)),
45
+ removed: [...thenIds].filter((id) => !nowIds.has(id)),
46
+ updated: then.graph && now.graph ? elementUpdates(then.graph, now.graph) : []
47
+ },
48
+ relationships: then.graph && now.graph ? relationshipChanges(then.graph, now.graph) : { added: [], removed: [] },
49
+ unstaged: [],
50
+ gaps: {
51
+ opened: [...nowGaps].filter(([k]) => !thenGaps.has(k)).map(([, g]) => g),
52
+ closed: [...thenGaps].filter(([k]) => !nowGaps.has(k)).map(([, g]) => g)
53
+ },
54
+ unwired: {
55
+ wired: [...thenUnwired].filter(([k, e]) => !nowUnwired.has(k) && nowScoped.has(e.element)).map(([, e]) => e),
56
+ new: [...nowUnwired].filter(([k, e]) => !thenUnwired.has(k) && thenScoped.has(e.element)).map(([, e]) => e),
57
+ newElements: now.unwired.filter((u) => !thenScoped.has(u.element)).map((u) => ({ element: u.element, count: u.byLayer.reduce((sum, l) => sum + l.entries.length, 0) }))
58
+ },
59
+ reach: {
60
+ newlyReached: now.brief.reach.touched.filter((l) => !thenReach.has(l)),
61
+ noLongerReached: then.brief.reach.touched.filter((l) => !nowReach.has(l))
62
+ },
63
+ intent: {
64
+ newlyTraced: now.brief.intent.traced.map((t) => t.element).filter((id) => !thenTraced.has(id)),
65
+ newlyUntraced: now.brief.intent.untraced.filter((id) => !thenUntraced.has(id))
66
+ },
67
+ note: NOTE,
68
+ ...warnings.length > 0 ? { warnings } : {}
69
+ };
70
+ }
71
+ function elementUpdates(then, now) {
72
+ const updates = [];
73
+ for (const after of now.elements.values()) {
74
+ if (after.status === "deleted")
75
+ continue;
76
+ const before = then.getElement(after.id);
77
+ if (!before || before.status === "deleted" || before.status === "unchanged" && after.status === "unchanged")
78
+ continue;
79
+ const fields = changedFields(before, after);
80
+ fields.length > 0 && updates.push({ element: after.id, fields });
81
+ }
82
+ return updates;
83
+ }
84
+ function changedFields(before, after) {
85
+ const fields = [], same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
86
+ same(before.name, after.name) || fields.push("name"), same(before.description, after.description) || fields.push("description");
87
+ for (const key of [.../* @__PURE__ */ new Set([...Object.keys(before.attributes), ...Object.keys(after.attributes)])].sort())
88
+ same(before.attributes[key], after.attributes[key]) || fields.push(`attributes.${key}`);
89
+ return same(before.sourceReference, after.sourceReference) || fields.push("sourceReference"), fields;
90
+ }
91
+ function relationshipChanges(then, now) {
92
+ const live = (graph) => new Map(graph.relationships().filter((r) => r.status !== "deleted").map((r) => [relationshipKey(r), { source: r.source, predicate: r.predicate, target: r.target }])), before = live(then), after = live(now);
93
+ return {
94
+ added: [...after].filter(([k]) => !before.has(k)).map(([, r]) => r),
95
+ removed: [...before].filter(([k]) => !after.has(k)).map(([, r]) => r)
96
+ };
97
+ }
98
+ export {
99
+ briefDelta,
100
+ diffBriefs
101
+ };
102
+ //# sourceMappingURL=changeset-brief-delta.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/changeset-brief-delta.ts"],
4
+ "sourcesContent": ["/**\n * Brief deltas \u2014 what changed in a changeset's brief since an earlier revision.\n *\n * The earlier brief is rebuilt from the changes that made up the plan at that revision (a\n * `ChangesetGraph` over them, against today's base) and compared with the brief now. Sequence\n * numbers are never reused and every unstage leaves a tombstone, so the plan at a revision can be\n * rebuilt after later unstages too. See \"Iterating on a plan\" in docs/CHANGE_MODELING.md.\n */\n\nimport {\n buildBrief,\n computeUnwired,\n unwiredKey,\n type BriefGap,\n type ChangesetBrief,\n type ElementUnwired,\n type UnwiredEntry,\n} from \"./changeset-analysis.js\";\nimport {\n ChangesetGraph,\n changesetRevisionToken,\n parseRevisionToken,\n relationshipKey,\n unstagedSince,\n type GraphElement,\n} from \"./changeset-graph.js\";\nimport { summarizeChange, type ChangeSummaryEntry } from \"./changeset-summary.js\";\nimport type { StagedChange } from \"./changeset.js\";\nimport type { Model } from \"./model.js\";\n\nexport type UnwiredDeltaEntry = { element: string; layer: string } & UnwiredEntry;\n\nexport type RelationshipTriple = { source: string; predicate: string; target: string };\n\nexport interface BriefDelta {\n /** The revision compared against, as asked (the number, when a revision token was passed). */\n since: number;\n /** The revision now. */\n revision: number;\n /**\n * Elements that entered or left the change, and elements whose content (name, description,\n * attributes, source reference) changed since, with the fields that did.\n */\n elements: { added: string[]; removed: string[]; updated: Array<{ element: string; fields: string[] }> };\n /** Relationships live now and not then (added), or then and not now (removed), new elements' included. */\n relationships: { added: RelationshipTriple[]; removed: RelationshipTriple[] };\n /** Changes staged at `since` and unstaged after it, with the revision each unstage made. */\n unstaged: Array<ChangeSummaryEntry & { removedAt: number }>;\n gaps: { opened: BriefGap[]; closed: BriefGap[] };\n unwired: {\n /** Unwired then, wired now (the element is still in the change). */\n wired: UnwiredDeltaEntry[];\n /** Unwired now, not then, on elements that were already in the change. */\n new: UnwiredDeltaEntry[];\n /** Elements new to the change, with how many unwired connections they bring (listed under `unwired`). */\n newElements: Array<{ element: string; count: number }>;\n };\n reach: { newlyReached: string[]; noLongerReached: string[] };\n intent: { newlyTraced: string[]; newlyUntraced: string[] };\n /** How the earlier state was rebuilt, and when it is approximate. */\n note: string;\n warnings?: string[];\n}\n\nexport interface BriefSide {\n brief: ChangesetBrief;\n unwired: ElementUnwired[];\n /** The projection the brief was built from; needed for element updates and relationship changes. */\n graph?: ChangesetGraph;\n}\n\nconst NOTE =\n \"The earlier state is rebuilt from the changes that made up the plan at `since` (unstaged ones from the \" +\n \"changeset's unstage record), applied to today's base model; base drift since then is not separated out. \" +\n \"It is approximate when a change staged by then was rewritten in place afterwards: pass the brief's \" +\n \"revisionToken as `since` to be warned when that happened.\";\n\n/**\n * Rebuilds the brief side for `since` (a revision number, or a brief's `revisionToken`) and compares\n * it with `now`. A token whose hash no longer matches the plan at its revision adds a warning: a\n * change up to it was rewritten in place, so the rebuilt earlier state is not the one the token was\n * taken from.\n */\nexport async function briefDelta(\n model: Model,\n now: { graph: ChangesetGraph; brief: ChangesetBrief },\n since: number | string\n): Promise<BriefDelta> {\n let revision: number;\n const warnings: string[] = [];\n const changeset = now.graph.changeset;\n if (typeof since === \"string\") {\n const parsed = parseRevisionToken(since);\n if (!parsed) {\n throw new Error(`since \"${since}\" is neither a revision number nor a revision token (<revision>:<hash>) from a brief`);\n }\n revision = parsed.revision;\n const today = changesetRevisionToken(changeset, revision);\n if (revision <= now.brief.revision && today !== since) {\n warnings.push(\n `The plan at revision ${revision} is not the one that revision token was taken from: a change up to it ` +\n \"was rewritten in place since then (or the changeset was rewritten, e.g. by an upgrade). The earlier \" +\n \"state below is rebuilt from what is recorded now, so this delta is approximate.\"\n );\n }\n } else {\n revision = since;\n }\n const prunedThrough = changeset.unstagedPrunedThrough;\n if (prunedThrough !== undefined && revision < prunedThrough) {\n warnings.push(\n `Unstage tombstones up to revision ${prunedThrough} were dropped to keep the record bounded, so changes ` +\n `unstaged between revision ${revision} and then may be missing from the rebuilt earlier state and from ` +\n \"`unstaged`; this delta is approximate.\"\n );\n }\n const earlier = await ChangesetGraph.build(model, changeset, { throughSequence: revision });\n const then: BriefSide = {\n brief: buildBrief({ graph: earlier, unwired: \"none\" }),\n unwired: computeUnwired(earlier),\n graph: earlier,\n };\n const delta = diffBriefs(then, { brief: now.brief, unwired: computeUnwired(now.graph), graph: now.graph }, revision, warnings);\n delta.unstaged = unstagedSince(changeset, revision).map((t, index) => ({\n ...summarizeChange({ ...t.change, sequenceNumber: t.sequence } as StagedChange, index),\n removedAt: t.removedAt,\n }));\n return delta;\n}\n\nexport function diffBriefs(then: BriefSide, now: BriefSide, since: number, extraWarnings: string[] = []): BriefDelta {\n const revision = now.brief.revision;\n const warnings: string[] = [...extraWarnings];\n if (since > revision) {\n warnings.push(\n `since (${since}) is past the current revision (${revision}), so it is not a revision of this plan; the ` +\n \"delta compares the changeset with itself.\"\n );\n }\n\n const changedIds = (brief: ChangesetBrief) => brief.layers.flatMap((l) => l.changes.map((c) => c.id));\n const thenIds = new Set(changedIds(then.brief));\n const nowIds = new Set(changedIds(now.brief));\n\n const gapKey = (g: BriefGap) => `${g.kind}|${g.element}|${g.kind === \"peer-pattern\" ? g.detail.replace(/^\\d+ of \\d+ /, \"\") : g.detail}`;\n const thenGaps = new Map(then.brief.gaps.map((g) => [gapKey(g), g]));\n const nowGaps = new Map(now.brief.gaps.map((g) => [gapKey(g), g]));\n\n const flatten = (list: ElementUnwired[]) => {\n const map = new Map<string, UnwiredDeltaEntry>();\n for (const { element, byLayer } of list) {\n for (const { layer, entries } of byLayer) {\n for (const entry of entries) map.set(unwiredKey(element, entry), { element, layer, ...entry });\n }\n }\n return map;\n };\n const thenUnwired = flatten(then.unwired);\n const nowUnwired = flatten(now.unwired);\n const thenScoped = new Set(then.unwired.map((u) => u.element));\n const nowScoped = new Set(now.unwired.map((u) => u.element));\n\n const traced = (brief: ChangesetBrief) => new Set(brief.intent.traced.map((t) => t.element));\n const untraced = (brief: ChangesetBrief) => new Set(brief.intent.untraced);\n const thenTraced = traced(then.brief);\n const thenUntraced = untraced(then.brief);\n const thenReach = new Set(then.brief.reach.touched);\n const nowReach = new Set(now.brief.reach.touched);\n\n return {\n since,\n revision,\n elements: {\n added: [...nowIds].filter((id) => !thenIds.has(id)),\n removed: [...thenIds].filter((id) => !nowIds.has(id)),\n updated: then.graph && now.graph ? elementUpdates(then.graph, now.graph) : [],\n },\n relationships: then.graph && now.graph ? relationshipChanges(then.graph, now.graph) : { added: [], removed: [] },\n unstaged: [],\n gaps: {\n opened: [...nowGaps].filter(([k]) => !thenGaps.has(k)).map(([, g]) => g),\n closed: [...thenGaps].filter(([k]) => !nowGaps.has(k)).map(([, g]) => g),\n },\n unwired: {\n wired: [...thenUnwired]\n .filter(([k, e]) => !nowUnwired.has(k) && nowScoped.has(e.element))\n .map(([, e]) => e),\n new: [...nowUnwired].filter(([k, e]) => !thenUnwired.has(k) && thenScoped.has(e.element)).map(([, e]) => e),\n newElements: now.unwired\n .filter((u) => !thenScoped.has(u.element))\n .map((u) => ({ element: u.element, count: u.byLayer.reduce((sum, l) => sum + l.entries.length, 0) })),\n },\n reach: {\n newlyReached: now.brief.reach.touched.filter((l) => !thenReach.has(l)),\n noLongerReached: then.brief.reach.touched.filter((l) => !nowReach.has(l)),\n },\n intent: {\n newlyTraced: now.brief.intent.traced.map((t) => t.element).filter((id) => !thenTraced.has(id)),\n newlyUntraced: now.brief.intent.untraced.filter((id) => !thenUntraced.has(id)),\n },\n note: NOTE,\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n\n/** Elements present (not deleted) in both projections whose content differs, with the fields that do. */\nfunction elementUpdates(then: ChangesetGraph, now: ChangesetGraph): Array<{ element: string; fields: string[] }> {\n const updates: Array<{ element: string; fields: string[] }> = [];\n for (const after of now.elements.values()) {\n if (after.status === \"deleted\") continue;\n const before = then.getElement(after.id);\n if (!before || before.status === \"deleted\") continue;\n // Both from the base: same content. (One unchanged and one not still compares: an update\n // staged or unstaged since.)\n if (before.status === \"unchanged\" && after.status === \"unchanged\") continue;\n const fields = changedFields(before, after);\n if (fields.length > 0) updates.push({ element: after.id, fields });\n }\n return updates;\n}\n\nfunction changedFields(before: GraphElement, after: GraphElement): string[] {\n const fields: string[] = [];\n const same = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n if (!same(before.name, after.name)) fields.push(\"name\");\n if (!same(before.description, after.description)) fields.push(\"description\");\n for (const key of [...new Set([...Object.keys(before.attributes), ...Object.keys(after.attributes)])].sort()) {\n if (!same(before.attributes[key], after.attributes[key])) fields.push(`attributes.${key}`);\n }\n if (!same(before.sourceReference, after.sourceReference)) fields.push(\"sourceReference\");\n return fields;\n}\n\n/** Relationships live in one projection and not the other. */\nfunction relationshipChanges(\n then: ChangesetGraph,\n now: ChangesetGraph\n): { added: RelationshipTriple[]; removed: RelationshipTriple[] } {\n const live = (graph: ChangesetGraph) =>\n new Map(\n graph\n .relationships()\n .filter((r) => r.status !== \"deleted\")\n .map((r) => [relationshipKey(r), { source: r.source, predicate: r.predicate, target: r.target }])\n );\n const before = live(then);\n const after = live(now);\n return {\n added: [...after].filter(([k]) => !before.has(k)).map(([, r]) => r),\n removed: [...before].filter(([k]) => !after.has(k)).map(([, r]) => r),\n };\n}\n"],
5
+ "mappings": "AASA,SACE,YACA,gBACA,kBAKK;AACP,SACE,gBACA,wBACA,oBACA,iBACA,qBAEK;AACP,SAAS,uBAAgD;AA6CzD,MAAM,OACJ;AAWF,eAAsB,WACpB,OACA,KACA,OAAsB;AAEtB,MAAI;AACJ,QAAM,WAAqB,CAAA,GACrB,YAAY,IAAI,MAAM;AAC5B,MAAI,OAAO,SAAU,UAAU;AAC7B,UAAM,SAAS,mBAAmB,KAAK;AACvC,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,UAAU,KAAK,sFAAsF;AAEvH,eAAW,OAAO;AAClB,UAAM,QAAQ,uBAAuB,WAAW,QAAQ;AACxD,IAAI,YAAY,IAAI,MAAM,YAAY,UAAU,SAC9C,SAAS,KACP,wBAAwB,QAAQ,2PAEmD;EAGzF;AACE,eAAW;AAEb,QAAM,gBAAgB,UAAU;AAChC,EAAI,kBAAkB,UAAa,WAAW,iBAC5C,SAAS,KACP,qCAAqC,aAAa,kFACnB,QAAQ,2GACG;AAG9C,QAAM,UAAU,MAAM,eAAe,MAAM,OAAO,WAAW,EAAE,iBAAiB,SAAQ,CAAE,GACpF,OAAkB;IACtB,OAAO,WAAW,EAAE,OAAO,SAAS,SAAS,OAAM,CAAE;IACrD,SAAS,eAAe,OAAO;IAC/B,OAAO;KAEH,QAAQ,WAAW,MAAM,EAAE,OAAO,IAAI,OAAO,SAAS,eAAe,IAAI,KAAK,GAAG,OAAO,IAAI,MAAK,GAAI,UAAU,QAAQ;AAC7H,eAAM,WAAW,cAAc,WAAW,QAAQ,EAAE,IAAI,CAAC,GAAG,WAAW;IACrE,GAAG,gBAAgB,EAAE,GAAG,EAAE,QAAQ,gBAAgB,EAAE,SAAQ,GAAoB,KAAK;IACrF,WAAW,EAAE;IACb,GACK;AACT;AAEM,SAAU,WAAW,MAAiB,KAAgB,OAAe,gBAA0B,CAAA,GAAE;AACrG,QAAM,WAAW,IAAI,MAAM,UACrB,WAAqB,CAAC,GAAG,aAAa;AAC5C,EAAI,QAAQ,YACV,SAAS,KACP,UAAU,KAAK,mCAAmC,QAAQ,wFACb;AAIjD,QAAM,aAAa,CAAC,UAA0B,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,GAC9F,UAAU,IAAI,IAAI,WAAW,KAAK,KAAK,CAAC,GACxC,SAAS,IAAI,IAAI,WAAW,IAAI,KAAK,CAAC,GAEtC,SAAS,CAAC,MAAgB,GAAG,EAAE,IAAI,IAAI,EAAE,OAAO,IAAI,EAAE,SAAS,iBAAiB,EAAE,OAAO,QAAQ,gBAAgB,EAAE,IAAI,EAAE,MAAM,IAC/H,WAAW,IAAI,IAAI,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAC7D,UAAU,IAAI,IAAI,IAAI,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAE3D,UAAU,CAAC,SAA0B;AACzC,UAAM,MAAM,oBAAI,IAAG;AACnB,eAAW,EAAE,SAAS,QAAO,KAAM;AACjC,iBAAW,EAAE,OAAO,QAAO,KAAM;AAC/B,mBAAW,SAAS;AAAS,cAAI,IAAI,WAAW,SAAS,KAAK,GAAG,EAAE,SAAS,OAAO,GAAG,MAAK,CAAE;AAGjG,WAAO;EACT,GACM,cAAc,QAAQ,KAAK,OAAO,GAClC,aAAa,QAAQ,IAAI,OAAO,GAChC,aAAa,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GACvD,YAAY,IAAI,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAErD,SAAS,CAAC,UAA0B,IAAI,IAAI,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GACrF,WAAW,CAAC,UAA0B,IAAI,IAAI,MAAM,OAAO,QAAQ,GACnE,aAAa,OAAO,KAAK,KAAK,GAC9B,eAAe,SAAS,KAAK,KAAK,GAClC,YAAY,IAAI,IAAI,KAAK,MAAM,MAAM,OAAO,GAC5C,WAAW,IAAI,IAAI,IAAI,MAAM,MAAM,OAAO;AAEhD,SAAO;IACL;IACA;IACA,UAAU;MACR,OAAO,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;MAClD,SAAS,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;MACpD,SAAS,KAAK,SAAS,IAAI,QAAQ,eAAe,KAAK,OAAO,IAAI,KAAK,IAAI,CAAA;;IAE7E,eAAe,KAAK,SAAS,IAAI,QAAQ,oBAAoB,KAAK,OAAO,IAAI,KAAK,IAAI,EAAE,OAAO,CAAA,GAAI,SAAS,CAAA,EAAE;IAC9G,UAAU,CAAA;IACV,MAAM;MACJ,QAAQ,CAAC,GAAG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;MACvE,QAAQ,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;;IAEzE,SAAS;MACP,OAAO,CAAC,GAAG,WAAW,EACnB,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,KAAK,UAAU,IAAI,EAAE,OAAO,CAAC,EACjE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;MACnB,KAAK,CAAC,GAAG,UAAU,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,KAAK,WAAW,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;MAC1G,aAAa,IAAI,QACd,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,OAAO,CAAC,EACxC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,OAAO,EAAE,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,QAAQ,CAAC,EAAC,EAAG;;IAExG,OAAO;MACL,cAAc,IAAI,MAAM,MAAM,QAAQ,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;MACrE,iBAAiB,KAAK,MAAM,MAAM,QAAQ,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;;IAE1E,QAAQ;MACN,aAAa,IAAI,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;MAC7F,eAAe,IAAI,MAAM,OAAO,SAAS,OAAO,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;;IAE/E,MAAM;IACN,GAAI,SAAS,SAAS,IAAI,EAAE,SAAQ,IAAK,CAAA;;AAE7C;AAGA,SAAS,eAAe,MAAsB,KAAmB;AAC/D,QAAM,UAAwD,CAAA;AAC9D,aAAW,SAAS,IAAI,SAAS,OAAM,GAAI;AACzC,QAAI,MAAM,WAAW;AAAW;AAChC,UAAM,SAAS,KAAK,WAAW,MAAM,EAAE;AAIvC,QAHI,CAAC,UAAU,OAAO,WAAW,aAG7B,OAAO,WAAW,eAAe,MAAM,WAAW;AAAa;AACnE,UAAM,SAAS,cAAc,QAAQ,KAAK;AAC1C,IAAI,OAAO,SAAS,KAAG,QAAQ,KAAK,EAAE,SAAS,MAAM,IAAI,OAAM,CAAE;EACnE;AACA,SAAO;AACT;AAEA,SAAS,cAAc,QAAsB,OAAmB;AAC9D,QAAM,SAAmB,CAAA,GACnB,OAAO,CAAC,GAAY,MAAe,KAAK,UAAU,KAAK,IAAI,MAAM,KAAK,UAAU,KAAK,IAAI;AAC/F,EAAK,KAAK,OAAO,MAAM,MAAM,IAAI,KAAG,OAAO,KAAK,MAAM,GACjD,KAAK,OAAO,aAAa,MAAM,WAAW,KAAG,OAAO,KAAK,aAAa;AAC3E,aAAW,OAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,OAAO,UAAU,GAAG,GAAG,OAAO,KAAK,MAAM,UAAU,CAAC,CAAC,CAAC,EAAE,KAAI;AACxG,IAAK,KAAK,OAAO,WAAW,GAAG,GAAG,MAAM,WAAW,GAAG,CAAC,KAAG,OAAO,KAAK,cAAc,GAAG,EAAE;AAE3F,SAAK,KAAK,OAAO,iBAAiB,MAAM,eAAe,KAAG,OAAO,KAAK,iBAAiB,GAChF;AACT;AAGA,SAAS,oBACP,MACA,KAAmB;AAEnB,QAAM,OAAO,CAAC,UACZ,IAAI,IACF,MACG,cAAa,EACb,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,EACpC,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,QAAQ,EAAE,OAAM,CAAE,CAAC,CAAC,GAEjG,SAAS,KAAK,IAAI,GAClB,QAAQ,KAAK,GAAG;AACtB,SAAO;IACL,OAAO,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;IAClE,SAAS,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;;AAExE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,281 @@
1
+ import { createHash } from "node:crypto";
2
+ import { compactChange, nextSequenceNumber, sequenceOf } from "./changeset.js";
3
+ import { stagedSourceReference } from "./staged-source-reference.js";
4
+ function sequencedChanges(changeset) {
5
+ return changeset.changes.map((change, index) => ({ change, sequence: sequenceOf(change, index) })).sort((a, b) => a.sequence - b.sequence);
6
+ }
7
+ function changesAtRevision(changeset, revision) {
8
+ const live = sequencedChanges(changeset);
9
+ if (revision === void 0)
10
+ return live;
11
+ const bySequence = /* @__PURE__ */ new Map();
12
+ for (const entry of live)
13
+ entry.sequence <= revision && bySequence.set(entry.sequence, entry);
14
+ for (const t of tombstonesLiveAt(changeset, revision))
15
+ bySequence.set(t.sequence, {
16
+ change: { ...t.change, sequenceNumber: t.sequence },
17
+ sequence: t.sequence,
18
+ ...t.rewritten ? {} : { unstagedAt: t.removedAt }
19
+ });
20
+ return [...bySequence.values()].sort((a, b) => a.sequence - b.sequence);
21
+ }
22
+ function tombstonesLiveAt(changeset, revision) {
23
+ const earliest = /* @__PURE__ */ new Map();
24
+ for (const t of changeset.unstaged ?? []) {
25
+ if (t.sequence > revision || t.removedAt <= revision)
26
+ continue;
27
+ const kept = earliest.get(t.sequence);
28
+ (!kept || t.removedAt < kept.removedAt) && earliest.set(t.sequence, t);
29
+ }
30
+ return [...earliest.values()].sort((a, b) => a.sequence - b.sequence);
31
+ }
32
+ function unstagedSince(changeset, revision) {
33
+ const live = new Set(sequencedChanges(changeset).map(({ sequence }) => sequence)), result = [];
34
+ for (const { sequence } of tombstonesLiveAt(changeset, revision)) {
35
+ if (live.has(sequence))
36
+ continue;
37
+ const unstage = (changeset.unstaged ?? []).filter((t) => t.sequence === sequence && !t.rewritten && t.removedAt > revision).sort((x, y) => y.removedAt - x.removedAt)[0];
38
+ unstage && result.push(unstage);
39
+ }
40
+ return result;
41
+ }
42
+ function changesetRevision(changeset) {
43
+ return nextSequenceNumber(changeset) - 1;
44
+ }
45
+ function changesetRevisionToken(changeset, throughSequence) {
46
+ const revision = throughSequence ?? changesetRevision(changeset), hash = createHash("sha256");
47
+ for (const { change, sequence } of changesAtRevision(changeset, revision)) {
48
+ const compact = compactChange(change);
49
+ hash.update(JSON.stringify([sequence, compact.type, compact.elementId, compact.timestamp ?? null, compact.before ?? null, compact.after ?? null])), hash.update(`
50
+ `);
51
+ }
52
+ return `${revision}:${hash.digest("hex").slice(0, 12)}`;
53
+ }
54
+ function parseRevisionToken(token) {
55
+ const match = /^(-1|\d+):([0-9a-f]+)$/.exec(token);
56
+ return match ? { revision: Number(match[1]), hash: match[2] } : void 0;
57
+ }
58
+ function relationshipKey(rel) {
59
+ return `${rel.source}::${rel.predicate}::${rel.target}`;
60
+ }
61
+ class ChangesetGraph {
62
+ changeset;
63
+ elements = /* @__PURE__ */ new Map();
64
+ relationshipsByKey = /* @__PURE__ */ new Map();
65
+ /** UUID → dotted path, so relationship rows that name elements by UUID still resolve. */
66
+ uuidToPath = /* @__PURE__ */ new Map();
67
+ adjacency = null;
68
+ /** The staged changes this projection applied, in sequence order. */
69
+ appliedChanges = [];
70
+ _revision = -1;
71
+ constructor(changeset) {
72
+ this.changeset = changeset;
73
+ }
74
+ /** The revision this projection shows (see `changesetRevision`); -1 when nothing was staged by then. */
75
+ get revision() {
76
+ return this._revision;
77
+ }
78
+ /**
79
+ * Builds the projected graph of `changeset` over the base model. With `throughSequence`, the plan
80
+ * as it stood at that revision, over today's base: the changes numbered at most that, including
81
+ * ones unstaged since (rebuilt from their tombstones).
82
+ */
83
+ static async build(model, changeset, options = {}) {
84
+ const graph = new ChangesetGraph(changeset);
85
+ for (const layerName of model.getLayerNames()) {
86
+ const layer = await model.getBaseLayer(layerName);
87
+ if (layer)
88
+ for (const element of layer.listElements()) {
89
+ const record = element.toJSON();
90
+ graph.putElement(fromRecord(record, layerName, "unchanged"));
91
+ }
92
+ }
93
+ const baseRelationships = typeof model.getBaseRelationships == "function" ? model.getBaseRelationships() : model.relationships.getAll();
94
+ for (const rel of baseRelationships) {
95
+ const resolved = {
96
+ source: graph.resolve(rel.source),
97
+ predicate: rel.predicate,
98
+ target: graph.resolve(rel.target)
99
+ };
100
+ graph.relationshipsByKey.set(relationshipKey(resolved), {
101
+ ...resolved,
102
+ layer: rel.layer,
103
+ ...rel.targetLayer ? { targetLayer: rel.targetLayer } : {},
104
+ ...rel.properties ? { properties: rel.properties } : {},
105
+ status: "unchanged",
106
+ ...resolved.source !== rel.source || resolved.target !== rel.target ? { raw: { source: rel.source, target: rel.target } } : {}
107
+ });
108
+ }
109
+ const through = options.throughSequence, applied = changesAtRevision(changeset, through);
110
+ graph.appliedChanges = applied.map(({ change }) => change);
111
+ const current = changesetRevision(changeset);
112
+ graph._revision = through === void 0 ? current : Math.min(through, current);
113
+ for (const change of graph.appliedChanges)
114
+ graph.apply(change);
115
+ return graph;
116
+ }
117
+ /** Maps a UUID to its dotted path; returns any other identifier unchanged. */
118
+ resolve(id) {
119
+ return this.uuidToPath.get(id) ?? id;
120
+ }
121
+ getElement(id) {
122
+ return this.elements.get(this.resolve(id));
123
+ }
124
+ /** Every relationship in the projection, including the ones the changeset deletes. */
125
+ relationships() {
126
+ return [...this.relationshipsByKey.values()];
127
+ }
128
+ /** Whether the projection holds a relationship that is not deleted. */
129
+ hasLiveRelationship(source, predicate, target) {
130
+ const rel = this.relationshipsByKey.get(relationshipKey({ source: this.resolve(source), predicate, target: this.resolve(target) }));
131
+ return !!rel && rel.status !== "deleted";
132
+ }
133
+ getRelationship(source, predicate, target) {
134
+ return this.relationshipsByKey.get(relationshipKey({ source: this.resolve(source), predicate, target: this.resolve(target) }));
135
+ }
136
+ /** Elements the changeset adds, updates or deletes, in the order of their first change. */
137
+ changedElements() {
138
+ const seen = /* @__PURE__ */ new Set(), result = [];
139
+ for (const change of this.appliedChanges) {
140
+ if (change.type.startsWith("relationship-"))
141
+ continue;
142
+ const element = this.getElement(change.elementId);
143
+ !element || element.status === "unchanged" || seen.has(element.id) || (seen.add(element.id), result.push(element));
144
+ }
145
+ return result;
146
+ }
147
+ /** Relationships the changeset adds or deletes. */
148
+ changedRelationships() {
149
+ return this.relationships().filter((rel) => rel.status !== "unchanged");
150
+ }
151
+ /**
152
+ * Staged adds the base model already has: elements and relationships the changeset adds whose id
153
+ * (or source, predicate and target) now exists in the base. They are not counted as adds, since
154
+ * commit skips them; a later staged update or delete of the same element still applies.
155
+ */
156
+ alreadyInBase() {
157
+ return {
158
+ elements: [...this.elements.values()].filter((e) => e.alreadyInBase && e.status !== "deleted"),
159
+ relationships: this.relationships().filter((r) => r.alreadyInBase && r.status === "unchanged")
160
+ };
161
+ }
162
+ /** Relationships touching an element, with the element's side of each. */
163
+ neighbors(id, options = {}) {
164
+ const all = this.getAdjacency().get(this.resolve(id)) ?? [];
165
+ return options.includeDeleted ? all : all.filter((n) => n.relationship.status !== "deleted");
166
+ }
167
+ getAdjacency() {
168
+ if (this.adjacency)
169
+ return this.adjacency;
170
+ const adjacency = /* @__PURE__ */ new Map(), push = (id, neighbor) => {
171
+ const list = adjacency.get(id);
172
+ list ? list.push(neighbor) : adjacency.set(id, [neighbor]);
173
+ };
174
+ for (const rel of this.relationshipsByKey.values())
175
+ push(rel.source, { relationship: rel, direction: "out", other: rel.target }), rel.target !== rel.source && push(rel.target, { relationship: rel, direction: "in", other: rel.source });
176
+ return this.adjacency = adjacency, adjacency;
177
+ }
178
+ putElement(element) {
179
+ this.elements.set(element.id, element), element.uuid && element.uuid !== element.id && this.uuidToPath.set(element.uuid, element.id);
180
+ }
181
+ /**
182
+ * A committed changeset's changes are already in the base model, so a base element or
183
+ * relationship matching one of its adds is that add, not an earlier copy of it.
184
+ */
185
+ isCommitted() {
186
+ return this.changeset.status === "committed";
187
+ }
188
+ apply(change) {
189
+ switch (change.type) {
190
+ case "add": {
191
+ if (!change.after)
192
+ return;
193
+ const element = fromRecord(change.after, change.layerName, "added", change.elementId), existing = this.getElement(element.id);
194
+ if (existing && existing.status === "unchanged" && !this.isCommitted()) {
195
+ existing.alreadyInBase = !0;
196
+ return;
197
+ }
198
+ this.putElement(element);
199
+ return;
200
+ }
201
+ case "update": {
202
+ if (!change.after)
203
+ return;
204
+ const existing = this.getElement(change.elementId), status = existing?.status === "added" ? "added" : "updated", element = fromRecord(change.after, change.layerName, status, change.elementId);
205
+ element.before = existing?.before ?? change.before, this.putElement(element);
206
+ return;
207
+ }
208
+ case "delete": {
209
+ const existing = this.getElement(change.elementId);
210
+ if (!existing)
211
+ return;
212
+ existing.status === "added" ? this.elements.delete(existing.id) : (existing.before = existing.before ?? change.before, existing.status = "deleted");
213
+ return;
214
+ }
215
+ case "relationship-add": {
216
+ const data = change.after;
217
+ if (!data?.source || !data.target || !data.predicate)
218
+ return;
219
+ const rel = {
220
+ source: this.resolve(data.source),
221
+ predicate: data.predicate,
222
+ target: this.resolve(data.target)
223
+ }, key = relationshipKey(rel), existing = this.relationshipsByKey.get(key);
224
+ if (existing && existing.status === "unchanged" && !this.isCommitted()) {
225
+ existing.alreadyInBase = !0;
226
+ return;
227
+ }
228
+ this.relationshipsByKey.set(key, {
229
+ ...rel,
230
+ layer: data.layer ?? change.layerName,
231
+ ...data.targetLayer ? { targetLayer: data.targetLayer } : {},
232
+ ...data.properties ? { properties: data.properties } : {},
233
+ // Re-adding a relationship this changeset deleted restores it. A committed changeset's add
234
+ // is in the base because the changeset landed it, so it stays an add.
235
+ status: existing?.status === "deleted" ? "unchanged" : "added",
236
+ ...existing?.raw ? { raw: existing.raw } : {}
237
+ });
238
+ return;
239
+ }
240
+ case "relationship-delete": {
241
+ const data = change.before ?? change.after, [source, predicate, target] = data?.source ? [data.source, data.predicate, data.target] : change.elementId.split("::");
242
+ if (!source || !predicate || !target)
243
+ return;
244
+ const key = relationshipKey({
245
+ source: this.resolve(source),
246
+ predicate,
247
+ target: this.resolve(target)
248
+ }), existing = this.relationshipsByKey.get(key);
249
+ if (!existing)
250
+ return;
251
+ existing.status === "added" ? this.relationshipsByKey.delete(key) : existing.status = "deleted";
252
+ return;
253
+ }
254
+ }
255
+ }
256
+ }
257
+ function fromRecord(record, layerName, status, fallbackId) {
258
+ const str = (value) => typeof value == "string" && value.length > 0 ? value : void 0, id = str(record.path) ?? fallbackId ?? str(record.id) ?? "";
259
+ return {
260
+ id,
261
+ uuid: str(record.id),
262
+ layer: str(record.layer_id) ?? str(record.layer) ?? layerName,
263
+ type: str(record.type) ?? "",
264
+ name: str(record.name) ?? id,
265
+ description: str(record.description),
266
+ attributes: record.attributes ?? {},
267
+ sourceReference: stagedSourceReference(record),
268
+ status
269
+ };
270
+ }
271
+ export {
272
+ ChangesetGraph,
273
+ changesAtRevision,
274
+ changesetRevision,
275
+ changesetRevisionToken,
276
+ parseRevisionToken,
277
+ relationshipKey,
278
+ sequencedChanges,
279
+ unstagedSince
280
+ };
281
+ //# sourceMappingURL=changeset-graph.js.map