@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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/changeset-analysis.ts"],
4
+ "sourcesContent": ["/**\n * Changeset analysis \u2014 pressure-testing a planned change against the model.\n *\n * Both functions read a `ChangesetGraph` (the base model with the changeset applied, relationships\n * included):\n *\n * - `computeImpact` walks outward from everything the changeset touches and lists the existing\n * elements it reaches, with the source files they live in.\n * - `buildBrief` renders the changeset as a spec for the build: changes by layer with their\n * connections, which layers the change reaches, gaps, whether each change traces to intent, the\n * code it touches, and open review comments.\n *\n * See docs/CHANGE_MODELING.md.\n */\n\nimport { getAllLayerIds } from \"../generated/layer-registry.js\";\nimport { normalizeNodeType } from \"../generated/node-types.js\";\nimport {\n getValidRelationships,\n RELATIONSHIPS_BY_DESTINATION,\n RELATIONSHIPS_BY_SOURCE,\n} from \"../generated/relationship-index.js\";\nimport type { Annotation } from \"./annotation-store.js\";\nimport type { ChangesetDrift } from \"./changeset-base-index.js\";\nimport { changesetRevisionToken } from \"./changeset-graph.js\";\nimport type { ChangesetGraph, GraphElement, GraphRelationship, Neighbor } from \"./changeset-graph.js\";\n\n/** Layers that state why the system exists; every change should trace to one of them. */\nexport const INTENT_LAYERS = new Set([\"motivation\", \"business\", \"product\"]);\n\nconst INTENT_TRACE_DEPTH = 4;\n\n// ---------------------------------------------------------------------------\n// Impact\n// ---------------------------------------------------------------------------\n\n/** An element with more live relationships than this is a hub: reported, not walked through. */\nexport const DEFAULT_HUB_THRESHOLD = 15;\n\nexport interface ImpactOptions {\n /** How many hops to walk out from the changed elements (default 2). */\n depth?: number;\n /** Stop after this many reached elements (default 200). */\n maxElements?: number;\n /**\n * Walk from these elements (ids or paths) instead of from everything the changeset changes.\n * Ids the projected graph does not have are reported in `unknownFrom`.\n */\n from?: string[];\n /** Degree above which a reached element is a hub (default 15). */\n hubThreshold?: number;\n /** Walk through hubs as through any other element (default false). */\n expandHubs?: boolean;\n}\n\nexport interface ImpactedElement {\n id: string;\n name: string;\n type: string;\n distance: number;\n /** The relationship through which the walk first reached this element. */\n via: string;\n files: string[];\n /** Set when the element is itself changed by the changeset (only a walk `from` chosen elements reaches one). */\n status?: \"added\" | \"updated\";\n}\n\nexport interface ImpactHub {\n id: string;\n /** Live relationships the element has in the projected graph. */\n degree: number;\n /** The hop at which the walk reached it. */\n depth: number;\n}\n\nexport interface ImpactReport {\n depth: number;\n /** Elements the changeset adds, updates or deletes. Without `from`, where the walk starts. */\n changed: number;\n /** Existing elements the walk reached. */\n reached: number;\n truncated: boolean;\n byLayer: Record<string, ImpactedElement[]>;\n /** Elements reached at each hop. */\n perHop: Array<{ depth: number; reached: number }>;\n /** Reached elements with more relationships than `hubThreshold`; the walk did not go through them. */\n hubs: ImpactHub[];\n hubThreshold: number;\n /** The resolved start elements, when the walk started `from` chosen ones. */\n from?: string[];\n /** `from` ids the projected graph does not have. */\n unknownFrom?: string[];\n /** Why the walk is not what it looks like, e.g. a `from` with no start element. */\n warnings?: string[];\n}\n\nexport function computeImpact(graph: ChangesetGraph, options: ImpactOptions = {}): ImpactReport {\n const depth = options.depth ?? 2;\n const maxElements = options.maxElements ?? 200;\n const hubThreshold = options.hubThreshold ?? DEFAULT_HUB_THRESHOLD;\n const expandHubs = options.expandHubs ?? false;\n\n const changed = new Set(graph.changedElements().map((e) => e.id));\n\n let start: Set<string>;\n let unknownFrom: string[] | undefined;\n if (options.from) {\n start = new Set();\n unknownFrom = [];\n for (const id of options.from) {\n const element = graph.getElement(id);\n if (element) start.add(element.id);\n else unknownFrom.push(id);\n }\n } else {\n start = changed;\n }\n\n const reached = new Map<string, ImpactedElement & { layer: string }>();\n const visited = new Set(start);\n const hubs: ImpactHub[] = [];\n const perHop = new Map<number, number>();\n let truncated = false;\n\n /** Records a reached element; returns whether the walk should continue through it. */\n const reach = (from: string, neighbor: Neighbor, distance: number): boolean => {\n if (visited.has(neighbor.other)) return false;\n const element = graph.getElement(neighbor.other);\n if (!element || element.status === \"deleted\") return false;\n visited.add(neighbor.other);\n if (reached.size >= maxElements) {\n truncated = true;\n return false;\n }\n reached.set(element.id, {\n id: element.id,\n layer: element.layer,\n name: element.name,\n type: element.type,\n distance,\n via: describeHop(from, neighbor),\n files: sourceFiles(element),\n ...(element.status === \"added\" || element.status === \"updated\" ? { status: element.status } : {}),\n });\n perHop.set(distance, (perHop.get(distance) ?? 0) + 1);\n const degree = graph.neighbors(element.id).length;\n if (degree > hubThreshold) {\n hubs.push({ id: element.id, degree, depth: distance });\n if (!expandHubs) return false;\n }\n return true;\n };\n\n // A relationship the changeset adds or removes between two existing elements changes both of\n // them, so both ends start one hop out, reached through that relationship. A walk from chosen\n // elements starts only from those.\n let frontier: string[] = [];\n if (!options.from) {\n for (const rel of graph.changedRelationships()) {\n if (changed.has(rel.source) || changed.has(rel.target)) continue;\n const asNeighbor = (other: string, direction: \"out\" | \"in\"): Neighbor => ({ relationship: rel, direction, other });\n if (reach(rel.source, asNeighbor(rel.target, \"out\"), 1)) frontier.push(rel.target);\n if (reach(rel.target, asNeighbor(rel.source, \"in\"), 1)) frontier.push(rel.source);\n }\n }\n\n let current = [...start];\n for (let distance = 1; distance <= depth && current.length + frontier.length > 0 && !truncated; distance++) {\n const next: string[] = [];\n for (const id of current) {\n // From a changed element, follow the relationships the change deletes too: their other\n // ends are exactly what a removal affects. Past that, walk only what will still exist.\n for (const neighbor of graph.neighbors(id, { includeDeleted: changed.has(id) })) {\n if (reach(id, neighbor, distance)) next.push(neighbor.other);\n if (truncated) break;\n }\n if (truncated) break;\n }\n current = [...frontier, ...next];\n frontier = [];\n }\n\n const byLayer: Record<string, ImpactedElement[]> = {};\n for (const layer of orderedLayers([...reached.values()].map((e) => e.layer))) {\n byLayer[layer] = [...reached.values()]\n .filter((e) => e.layer === layer)\n .sort((a, b) => a.distance - b.distance || a.id.localeCompare(b.id))\n .map(({ layer: _layer, ...rest }) => rest);\n }\n\n return {\n depth,\n changed: changed.size,\n reached: reached.size,\n truncated,\n byLayer,\n perHop: [...perHop.entries()].sort(([a], [b]) => a - b).map(([d, n]) => ({ depth: d, reached: n })),\n hubs: hubs.sort((a, b) => a.depth - b.depth || b.degree - a.degree || a.id.localeCompare(b.id)),\n hubThreshold,\n ...(options.from ? { from: [...start], unknownFrom: unknownFrom ?? [] } : {}),\n ...(options.from && start.size === 0\n ? {\n warnings: [\n options.from.length === 0\n ? \"from is empty, so the walk had no start element and reached nothing; omit from to walk from every changed element.\"\n : \"None of the from ids is in the projected model, so the walk had no start element and reached nothing; see unknownFrom.\",\n ],\n }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Brief\n// ---------------------------------------------------------------------------\n\nexport interface BriefConnection {\n direction: \"out\" | \"in\";\n predicate: string;\n other: string;\n otherName?: string;\n otherLayer?: string;\n status: GraphRelationship[\"status\"];\n}\n\nexport interface BriefElementChange {\n op: \"added\" | \"updated\" | \"deleted\";\n id: string;\n type: string;\n name: string;\n description?: string;\n attributes?: Record<string, unknown>;\n changedFields?: Array<{ field: string; before: unknown; after: unknown }>;\n connections: BriefConnection[];\n}\n\nexport interface BriefGap {\n /**\n * - `unconnected`: an added element with no relationships.\n * - `missing-required`: the catalog marks a relationship from the element's type required.\n * - `peer-pattern`: most existing elements of the same type, wired to the same neighbours,\n * have a connection this one lacks. The model's own convention, not a spec rule.\n */\n kind: \"unconnected\" | \"missing-required\" | \"peer-pattern\";\n element: string;\n detail: string;\n /** For `peer-pattern`: up to three existing connections of this shape. */\n examples?: string[];\n}\n\n/** A connection the catalog defines for an element's type that the element does not have. */\nexport interface UnwiredEntry {\n /** \"out\" when the element would be the source, \"in\" when it would be the target. */\n direction: \"out\" | \"in\";\n predicate: string;\n /** Spec node id of the other end (`layer.type`). */\n otherType: string;\n /**\n * Elements of `otherType` that exist outside the changeset's own adds: base elements, updated\n * ones included, that the change does not delete (the element itself not counted).\n */\n existingCount: number;\n /** Elements of `otherType` the changeset adds (the element itself not counted). */\n addedCount: number;\n}\n\n/** One added or updated element's unwired connections, as computed (every layer). */\nexport interface ElementUnwired {\n element: string;\n /** Spec node id of the element's type. */\n type: string;\n /** By the other end's layer, in layer order. */\n byLayer: Array<{ layer: string; entries: UnwiredEntry[] }>;\n}\n\n/**\n * - `summary` (default): per element, a count per layer, with the entries for the layers the change\n * already reaches (and for any layer named in `unwiredLayers`). Only entries whose other type the\n * projected model has (existing or added) are listed; the rest are counted per layer as\n * `typesNotInModel`.\n * - `full`: every entry.\n * - `none`: no unwired section.\n */\nexport type UnwiredMode = \"summary\" | \"full\" | \"none\";\n\nexport interface BriefUnwired {\n mode: Exclude<UnwiredMode, \"none\">;\n /** The layer filter, when one was given. */\n layers?: string[];\n /** Filter layers that are not layers of the model. */\n unknownLayers?: string[];\n elements: Array<{\n element: string;\n type: string;\n /** Unwired connections across the layers shown. */\n total: number;\n byLayer: Array<{\n layer: string;\n /** Unwired connections in this layer, listed or not. */\n count: number;\n entries?: UnwiredEntry[];\n /**\n * Summary mode: unwired connections in this layer whose other type the projected model has no\n * element of (neither existing nor added), not listed in `entries`. `full` lists them.\n */\n typesNotInModel?: number;\n }>;\n }>;\n}\n\nexport interface BriefInput {\n graph: ChangesetGraph;\n annotations?: Annotation[];\n /** Hash-only drift (`{isDrifted, warnings}`) or the structured one from `describeDrift`. */\n drift?: BriefDrift;\n /** How to show unwired connections (default `summary`). */\n unwired?: UnwiredMode;\n /** Only show unwired connections whose other end is in these layers. */\n unwiredLayers?: string[];\n /** The intent trace does not walk through elements with more relationships than this (default 15, as impact). */\n hubThreshold?: number;\n}\n\n/** A changed element that reaches intent, and how. */\nexport interface IntentTrace {\n element: string;\n /** The first intent element the walk reached. */\n tracesTo: string;\n distance: number;\n /** Element ids from the changed element to `tracesTo`, both included. */\n path: string[];\n /**\n * When `tracesTo` is an intent element the changeset adds: the existing intent element it traces\n * to, and the path there. An added intent element counts only when it has one.\n */\n anchoredBy?: { id: string; path: string[] };\n}\n\n/** A changed element whose only intent is an intent element the changeset adds, which traces to nothing. */\nexport interface UnanchoredTrace {\n element: string;\n /** The added intent element it reaches. */\n tracesTo: string;\n path: string[];\n detail: string;\n}\n\n/** `isDrifted` and `warnings` always; the rest when `describeDrift` produced it. */\nexport type BriefDrift = { isDrifted: boolean; warnings: string[] } & Partial<Omit<ChangesetDrift, \"isDrifted\" | \"warnings\">>;\n\nexport interface ChangesetBrief {\n /**\n * The changeset's revision: the last sequence number used, by a stage or an unstage (-1 when\n * nothing was ever staged); pass it back as `since`.\n */\n revision: number;\n /**\n * Names the plan at `revision`, not just its length (see `changesetRevisionToken`). Pass it back\n * as `since` to have the delta warn when changes up to that revision were unstaged or rewritten.\n */\n revisionToken: string;\n changeset: {\n id: string;\n name: string;\n description?: string;\n status: string;\n created: string;\n modified: string;\n drift?: BriefDrift;\n };\n summary: {\n /** Elements the changeset adds that the base does not have yet. */\n elementsAdded: number;\n elementsUpdated: number;\n elementsDeleted: number;\n /** Relationships the changeset adds that the base does not have yet. */\n relationshipsAdded: number;\n relationshipsDeleted: number;\n /** Staged adds whose element the base already has (e.g. an as-built changeset was committed). */\n elementsAlreadyInBase: number;\n /** Staged relationship adds the base already has. */\n relationshipsAlreadyInBase: number;\n };\n /** Staged adds the base already has; commit skips them. Counted apart from the adds above. */\n alreadyInBase: {\n elements: string[];\n relationships: Array<{ source: string; predicate: string; target: string }>;\n };\n layers: Array<{ layer: string; changes: BriefElementChange[] }>;\n /** Changed relationships whose two ends the changeset does not otherwise change. */\n relationshipChanges: Array<{ status: string; source: string; predicate: string; target: string }>;\n reach: { touched: string[]; untouched: string[] };\n gaps: BriefGap[];\n /**\n * Connections the catalog defines for each added or updated element's type, in either direction,\n * that the element has no live relationship for. Absent when `unwired` is `none`.\n */\n unwired?: BriefUnwired;\n /**\n * Whether each changed element outside the intent layers reaches an intent element (motivation,\n * business, product) within a few hops, without walking through hubs. An intent element the\n * changeset adds counts only when it itself traces to an existing one.\n */\n intent: {\n traced: IntentTrace[];\n /** Elements that trace to no intent (including the ones listed under `unanchored`). */\n untraced: string[];\n /** Untraced elements that reach only added intent elements which trace to nothing. */\n unanchored?: UnanchoredTrace[];\n };\n codeTouched: Array<{ file: string; elements: Array<{ id: string; role: \"changed\" | \"neighbor\" }> }>;\n feedback: Array<{\n annotationId: string;\n elementId: string;\n author: string;\n content: string;\n tags: string[];\n replies: number;\n }>;\n}\n\nexport function buildBrief({\n graph,\n annotations = [],\n drift,\n unwired: unwiredMode = \"summary\",\n unwiredLayers,\n hubThreshold = DEFAULT_HUB_THRESHOLD,\n}: BriefInput): ChangesetBrief {\n const changeset = graph.changeset;\n const changed = graph.changedElements();\n const changedIds = new Set(changed.map((e) => e.id));\n const changedRels = graph.changedRelationships();\n const inBase = graph.alreadyInBase();\n\n // Changes by layer\n const layers: ChangesetBrief[\"layers\"] = [];\n for (const layer of orderedLayers(changed.map((e) => e.layer))) {\n layers.push({\n layer,\n changes: changed.filter((e) => e.layer === layer).map((e) => describeChange(graph, e)),\n });\n }\n\n const relationshipChanges = changedRels\n .filter((rel) => !changedIds.has(rel.source) && !changedIds.has(rel.target))\n .map((rel) => ({ status: rel.status, source: rel.source, predicate: rel.predicate, target: rel.target }));\n\n // Reach: a layer is touched when an element in it changes, or a relationship into it does.\n const touchedLayers = new Set(changed.map((e) => e.layer));\n for (const rel of changedRels) {\n for (const end of [rel.source, rel.target]) {\n const element = graph.getElement(end);\n if (element) touchedLayers.add(element.layer);\n }\n }\n const allLayers = getAllLayerIds();\n const reach = {\n touched: allLayers.filter((l) => touchedLayers.has(l)),\n untouched: allLayers.filter((l) => !touchedLayers.has(l)),\n };\n\n // Gaps\n const gaps: BriefGap[] = [];\n for (const element of changed.filter((e) => e.status === \"added\")) {\n const live = graph.neighbors(element.id);\n if (live.length === 0) {\n gaps.push({\n kind: \"unconnected\",\n element: element.id,\n detail: \"Added with no relationships: nothing in the model says what it serves, realizes or uses.\",\n });\n }\n for (const required of requiredOutgoing(element)) {\n const satisfied = live.some((n) => {\n if (n.direction !== \"out\" || n.relationship.predicate !== required.predicate) return false;\n const other = graph.getElement(n.other);\n return !!other && specNodeId(other) === required.destinationSpecNodeId;\n });\n if (!satisfied) {\n gaps.push({\n kind: \"missing-required\",\n element: element.id,\n detail: `The catalog requires ${required.sourceSpecNodeId} --${required.predicate}--> ${required.destinationSpecNodeId}.`,\n });\n }\n }\n gaps.push(...peerPatternGaps(graph, element));\n }\n\n const unwired =\n unwiredMode === \"none\"\n ? undefined\n : presentUnwired(computeUnwired(graph), unwiredMode, new Set(reach.touched), unwiredLayers);\n // Intent: changed elements outside the intent layers should reach one within a few hops.\n const traced: IntentTrace[] = [];\n const untraced: string[] = [];\n const unanchored: UnanchoredTrace[] = [];\n const intentWalk = new IntentWalk(graph, hubThreshold);\n for (const element of changed) {\n if (element.status === \"deleted\" || INTENT_LAYERS.has(element.layer)) continue;\n const result = intentWalk.trace(element.id);\n if (result.traced) {\n traced.push({ element: element.id, ...result.traced });\n } else {\n untraced.push(element.id);\n if (result.unanchored) {\n const { id, path } = result.unanchored;\n unanchored.push({\n element: element.id,\n tracesTo: id,\n path,\n detail: `traces to added intent element ${id}, which traces to nothing`,\n });\n }\n }\n }\n\n // Code touched: files of changed elements and of the existing elements next to them.\n const files = new Map<string, Map<string, \"changed\" | \"neighbor\">>();\n const addFiles = (element: GraphElement, role: \"changed\" | \"neighbor\") => {\n for (const file of sourceFiles(element)) {\n const entry = files.get(file) ?? new Map();\n if (entry.get(element.id) !== \"changed\") entry.set(element.id, role);\n files.set(file, entry);\n }\n };\n const neighborIds = new Set<string>();\n for (const element of changed) {\n addFiles(element, \"changed\");\n for (const n of graph.neighbors(element.id, { includeDeleted: true })) {\n if (!changedIds.has(n.other)) neighborIds.add(n.other);\n }\n }\n for (const rel of changedRels) {\n for (const end of [rel.source, rel.target]) if (!changedIds.has(end)) neighborIds.add(end);\n }\n for (const id of neighborIds) {\n const element = graph.getElement(id);\n if (element && element.status !== \"deleted\") addFiles(element, \"neighbor\");\n }\n const codeTouched = [...files.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([file, elements]) => ({\n file,\n elements: [...elements.entries()].map(([id, role]) => ({ id, role })),\n }));\n\n // Feedback: open annotations on what the change touches.\n const watched = new Set([...changedIds, ...neighborIds]);\n const feedback = annotations\n .filter((a) => !a.resolved && watched.has(graph.resolve(a.elementId)))\n .map((a) => ({\n annotationId: a.id,\n elementId: a.elementId,\n author: a.author,\n content: a.content,\n tags: a.tags,\n replies: a.replies.length,\n }));\n\n return {\n revision: graph.revision,\n revisionToken: changesetRevisionToken(graph.changeset, graph.revision),\n changeset: {\n id: changeset.id,\n name: changeset.name,\n ...(changeset.description ? { description: changeset.description } : {}),\n status: changeset.status,\n created: changeset.created,\n modified: changeset.modified,\n ...(drift ? { drift } : {}),\n },\n summary: {\n elementsAdded: changed.filter((e) => e.status === \"added\").length,\n elementsUpdated: changed.filter((e) => e.status === \"updated\").length,\n elementsDeleted: changed.filter((e) => e.status === \"deleted\").length,\n relationshipsAdded: changedRels.filter((r) => r.status === \"added\").length,\n relationshipsDeleted: changedRels.filter((r) => r.status === \"deleted\").length,\n elementsAlreadyInBase: inBase.elements.length,\n relationshipsAlreadyInBase: inBase.relationships.length,\n },\n alreadyInBase: {\n elements: inBase.elements.map((e) => e.id),\n relationships: inBase.relationships.map((r) => ({ source: r.source, predicate: r.predicate, target: r.target })),\n },\n layers,\n relationshipChanges,\n reach,\n gaps,\n ...(unwired ? { unwired } : {}),\n intent: { traced, untraced, ...(unanchored.length > 0 ? { unanchored } : {}) },\n codeTouched,\n feedback,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction describeChange(graph: ChangesetGraph, element: GraphElement): BriefElementChange {\n const op = element.status as BriefElementChange[\"op\"];\n const connections = graph\n .neighbors(element.id, { includeDeleted: true })\n .map((n): BriefConnection => {\n const other = graph.getElement(n.other);\n return {\n direction: n.direction,\n predicate: n.relationship.predicate,\n other: n.other,\n ...(other ? { otherName: other.name, otherLayer: other.layer } : {}),\n status: n.relationship.status,\n };\n });\n\n const change: BriefElementChange = {\n op,\n id: element.id,\n type: element.type,\n name: element.name,\n ...(element.description ? { description: element.description } : {}),\n connections,\n };\n if (op !== \"deleted\" && Object.keys(element.attributes).length > 0) {\n change.attributes = element.attributes;\n }\n if (op === \"updated\" && element.before) {\n change.changedFields = diffFields(element.before, element);\n }\n return change;\n}\n\nfunction diffFields(\n before: Record<string, unknown>,\n after: GraphElement\n): Array<{ field: string; before: unknown; after: unknown }> {\n const fields: Array<{ field: string; before: unknown; after: unknown }> = [];\n const compare = (field: string, a: unknown, b: unknown) => {\n if (JSON.stringify(a) !== JSON.stringify(b)) fields.push({ field, before: a, after: b });\n };\n compare(\"name\", before.name, after.name);\n compare(\"description\", before.description, after.description);\n const beforeAttrs = (before.attributes as Record<string, unknown> | undefined) ?? {};\n for (const key of new Set([...Object.keys(beforeAttrs), ...Object.keys(after.attributes)])) {\n compare(`attributes.${key}`, beforeAttrs[key], after.attributes[key]);\n }\n return fields;\n}\n\n/**\n * The intent trace: a breadth-first walk from a changed element to the nearest intent element,\n * at most INTENT_TRACE_DEPTH hops, that does not walk through hubs (elements with more than\n * `hubThreshold` live relationships; a hub can still be reached). An existing intent element ends\n * the walk. An intent element the changeset adds ends it only when it is anchored, i.e. it traces\n * the same way to an existing intent element; an unanchored one is remembered and walked through.\n */\nclass IntentWalk {\n private readonly anchors = new Map<string, { id: string; path: string[] } | null>();\n\n constructor(\n private readonly graph: ChangesetGraph,\n private readonly hubThreshold: number\n ) {}\n\n trace(start: string): {\n traced?: Omit<IntentTrace, \"element\">;\n unanchored?: { id: string; path: string[] };\n } {\n let unanchored: { id: string; path: string[] } | undefined;\n const hit = this.walk(start, (element, path) => {\n if (element.status !== \"added\") return true;\n if (this.anchorOf(element.id)) return true;\n unanchored ??= { id: element.id, path };\n return false;\n });\n if (!hit) return unanchored ? { unanchored } : {};\n const anchor = this.graph.getElement(hit.id)?.status === \"added\" ? this.anchorOf(hit.id) : null;\n return {\n traced: {\n tracesTo: hit.id,\n distance: hit.path.length - 1,\n path: hit.path,\n ...(anchor ? { anchoredBy: anchor } : {}),\n },\n };\n }\n\n /** The existing intent element an added intent element traces to, or null. */\n private anchorOf(id: string): { id: string; path: string[] } | null {\n if (this.anchors.has(id)) return this.anchors.get(id)!;\n this.anchors.set(id, null); // a cycle back to it finds nothing\n const hit = this.walk(id, (element) => element.status !== \"added\");\n this.anchors.set(id, hit);\n return hit;\n }\n\n /**\n * Walks from `start` and returns the first intent element `accept` takes, with the path to it.\n * Intent elements it refuses are walked through like any other.\n */\n private walk(\n start: string,\n accept: (element: GraphElement, path: string[]) => boolean\n ): { id: string; path: string[] } | null {\n const parent = new Map<string, string>();\n const visited = new Set([start]);\n const pathTo = (id: string): string[] => {\n const path = [id];\n for (let at = parent.get(id); at !== undefined; at = parent.get(at)) path.unshift(at);\n return path;\n };\n let frontier = [start];\n for (let distance = 1; distance <= INTENT_TRACE_DEPTH && frontier.length > 0; distance++) {\n const next: string[] = [];\n for (const id of frontier) {\n if (id !== start && this.graph.neighbors(id).length > this.hubThreshold) continue;\n for (const n of this.graph.neighbors(id)) {\n if (visited.has(n.other)) continue;\n visited.add(n.other);\n const element = this.graph.getElement(n.other);\n if (!element || element.status === \"deleted\") continue;\n parent.set(element.id, id);\n if (INTENT_LAYERS.has(element.layer) && accept(element, pathTo(element.id))) {\n return { id: element.id, path: pathTo(element.id) };\n }\n next.push(element.id);\n }\n }\n frontier = next;\n }\n return null;\n }\n}\n\n/** A peer pattern counts when at least this many peers, and at least half of them, have it. */\nconst PEER_MIN = 2;\nconst PEER_SHARE = 0.5;\n\n/**\n * Connections an added element's peers usually have and it lacks. Peers are existing elements of\n * the same type that relate to one of the element's neighbours the same way it does (another\n * layout function that also realizes the layout service). Only shapes the catalog still allows are\n * reported, so a convention the spec has since retired is not suggested.\n */\nfunction peerPatternGaps(graph: ChangesetGraph, element: GraphElement): BriefGap[] {\n const own = graph.neighbors(element.id);\n if (own.length === 0) return [];\n const type = specNodeId(element);\n\n const peers = new Set<string>();\n for (const mine of own) {\n for (const theirs of graph.neighbors(mine.other)) {\n // Seen from the shared neighbour, a peer's relationship points the opposite way to ours.\n if (theirs.relationship.predicate !== mine.relationship.predicate || theirs.direction === mine.direction) continue;\n const peer = graph.getElement(theirs.other);\n if (!peer || peer.id === element.id || peer.status === \"added\" || peer.status === \"deleted\") continue;\n if (specNodeId(peer) === type) peers.add(peer.id);\n }\n }\n if (peers.size < PEER_MIN) return [];\n\n const shapeOf = (n: Neighbor): string | null => {\n const other = graph.getElement(n.other);\n return other ? `${n.direction}|${n.relationship.predicate}|${specNodeId(other)}` : null;\n };\n const ownShapes = new Set(own.map(shapeOf));\n\n const shapes = new Map<string, { peers: Set<string>; examples: string[] }>();\n for (const peerId of peers) {\n for (const n of graph.neighbors(peerId)) {\n if (n.other === element.id) continue;\n const shape = shapeOf(n);\n if (!shape || ownShapes.has(shape)) continue;\n const entry = shapes.get(shape) ?? { peers: new Set<string>(), examples: [] };\n if (!entry.peers.has(peerId) && entry.examples.length < 3) entry.examples.push(describeHop(peerId, n));\n entry.peers.add(peerId);\n shapes.set(shape, entry);\n }\n }\n\n const threshold = Math.max(PEER_MIN, Math.ceil(peers.size * PEER_SHARE));\n return [...shapes.entries()]\n .filter(([, entry]) => entry.peers.size >= threshold)\n .filter(([shape]) => {\n const [direction, predicate, otherType] = shape.split(\"|\");\n return direction === \"out\"\n ? getValidRelationships(type, predicate, otherType).length > 0\n : getValidRelationships(otherType, predicate, type).length > 0;\n })\n .sort(([, a], [, b]) => b.peers.size - a.peers.size)\n .map(([shape, entry]) => {\n const [direction, predicate, otherType] = shape.split(\"|\");\n const connection =\n direction === \"out\" ? `--${predicate}--> ${otherType}` : `${otherType} --${predicate}--> them`;\n return {\n kind: \"peer-pattern\" as const,\n element: element.id,\n detail: `${entry.peers.size} of ${peers.size} existing ${type} elements wired like this one have ${connection}; this one has none.`,\n examples: entry.examples,\n };\n });\n}\n\n/**\n * Unwired connections of every added or updated element: each triple the catalog defines with the\n * element's type at one end, for which the element has no live relationship with that predicate\n * to (or from) an element of the other end's type. Only catalog triples are considered.\n */\nexport function computeUnwired(graph: ChangesetGraph): ElementUnwired[] {\n // Existing (base, updated or not) and added elements per type, counted apart.\n const existing = new Map<string, number>();\n const added = new Map<string, number>();\n for (const element of graph.elements.values()) {\n if (element.status === \"deleted\") continue;\n const counts = element.status === \"added\" ? added : existing;\n const type = specNodeId(element);\n counts.set(type, (counts.get(type) ?? 0) + 1);\n }\n\n const result: ElementUnwired[] = [];\n for (const element of graph.changedElements()) {\n if (element.status !== \"added\" && element.status !== \"updated\") continue;\n const type = specNodeId(element);\n\n const have = new Set<string>();\n for (const n of graph.neighbors(element.id)) {\n const other = graph.getElement(n.other);\n if (other && other.status !== \"deleted\") have.add(`${n.direction}|${n.relationship.predicate}|${specNodeId(other)}`);\n }\n\n const entries = new Map<string, UnwiredEntry>();\n const consider = (direction: \"out\" | \"in\", predicate: string, otherType: string) => {\n const key = `${direction}|${predicate}|${otherType}`;\n if (have.has(key) || entries.has(key)) return;\n const self = otherType === type ? 1 : 0;\n const existingCount = (existing.get(otherType) ?? 0) - (element.status === \"added\" ? 0 : self);\n const addedCount = (added.get(otherType) ?? 0) - (element.status === \"added\" ? self : 0);\n entries.set(key, { direction, predicate, otherType, existingCount, addedCount });\n };\n for (const spec of RELATIONSHIPS_BY_SOURCE.get(type) ?? []) consider(\"out\", spec.predicate, spec.destinationSpecNodeId);\n for (const spec of RELATIONSHIPS_BY_DESTINATION.get(type) ?? []) consider(\"in\", spec.predicate, spec.sourceSpecNodeId);\n\n const layerOf = (specId: string) => specId.split(\".\")[0];\n const all = [...entries.values()];\n const byLayer = orderedLayers(all.map((e) => layerOf(e.otherType))).map((layer) => ({\n layer,\n entries: all\n .filter((e) => layerOf(e.otherType) === layer)\n .sort(\n (a, b) =>\n (a.direction === b.direction ? 0 : a.direction === \"out\" ? -1 : 1) ||\n a.predicate.localeCompare(b.predicate) ||\n a.otherType.localeCompare(b.otherType)\n ),\n }));\n result.push({ element: element.id, type, byLayer });\n }\n return result;\n}\n\n/** Shapes computed unwired connections for the brief: see `UnwiredMode`. */\nfunction presentUnwired(\n computed: ElementUnwired[],\n mode: Exclude<UnwiredMode, \"none\">,\n reachedLayers: Set<string>,\n layerFilter?: string[]\n): BriefUnwired {\n const known = new Set(getAllLayerIds());\n const filter = layerFilter ? new Set(layerFilter) : undefined;\n const unknownLayers = layerFilter?.filter((l) => !known.has(l)) ?? [];\n\n const elements = computed.map(({ element, type, byLayer }) => {\n const shown = byLayer\n .filter((l) => !filter || filter.has(l.layer))\n .map((l) => {\n const count = l.entries.length;\n if (mode === \"full\") return { layer: l.layer, count, entries: l.entries };\n // Summary: list only connections to types the projected model has; count the rest.\n const inModel = l.entries.filter((e) => e.existingCount + e.addedCount > 0);\n const typesNotInModel = count - inModel.length;\n const withEntries = reachedLayers.has(l.layer) || !!filter?.has(l.layer);\n return {\n layer: l.layer,\n count,\n ...(withEntries ? { entries: inModel } : {}),\n ...(typesNotInModel > 0 ? { typesNotInModel } : {}),\n };\n });\n return { element, type, total: shown.reduce((sum, l) => sum + l.count, 0), byLayer: shown };\n });\n\n return {\n mode,\n ...(layerFilter ? { layers: layerFilter } : {}),\n ...(unknownLayers.length > 0 ? { unknownLayers } : {}),\n elements,\n };\n}\n\n/** Key of one unwired entry of one element, for comparing two briefs. */\nexport function unwiredKey(element: string, entry: Pick<UnwiredEntry, \"direction\" | \"predicate\" | \"otherType\">): string {\n return `${element}|${entry.direction}|${entry.predicate}|${entry.otherType}`;\n}\n\nfunction requiredOutgoing(element: GraphElement) {\n return getValidRelationships(specNodeId(element)).filter((r) => r.required);\n}\n\nfunction specNodeId(element: GraphElement): string {\n return `${element.layer}.${normalizeNodeType(element.layer, element.type)}`;\n}\n\nfunction describeHop(from: string, neighbor: Neighbor): string {\n const { predicate, status } = neighbor.relationship;\n const suffix = status === \"unchanged\" ? \"\" : ` (${status} by this changeset)`;\n return neighbor.direction === \"out\"\n ? `${from} --${predicate}--> ${neighbor.other}${suffix}`\n : `${neighbor.other} --${predicate}--> ${from}${suffix}`;\n}\n\nfunction sourceFiles(element: GraphElement): string[] {\n const locations = element.sourceReference?.locations ?? [];\n return [...new Set(locations.map((l) => l.file).filter((f): f is string => !!f))];\n}\n\nfunction orderedLayers(layers: Iterable<string>): string[] {\n const present = new Set(layers);\n const known = getAllLayerIds().filter((l) => present.has(l));\n const unknown = [...present].filter((l) => !known.includes(l)).sort();\n return [...known, ...unknown];\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,160 @@
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 changedSet = new Set(elementsChanged), removedSet = new Set(elementsRemoved), listed = new Set(overlapping.relationships.map((r) => relationshipKey(r)));
111
+ for (const key of net.keys()) {
112
+ if (listed.has(key))
113
+ continue;
114
+ const ref = parseKey(key), ends = [ref.source, ref.target].filter((id, i, all) => all.indexOf(id) === i), removed = ends.filter((id) => removedSet.has(id) && !addedHere.has(id)), changed = ends.filter((id) => changedSet.has(id) && !elementOps.has(id));
115
+ removed.length > 0 ? overlapping.relationships.push({ ...ref, reason: "endpoint-removed-in-base", endpoints: removed }) : changed.length > 0 && overlapping.relationships.push({ ...ref, reason: "endpoint-changed-in-base", endpoints: changed });
116
+ }
117
+ const cut = (list) => list.slice(0, DRIFT_LIST_LIMIT);
118
+ baseChanges = {
119
+ counts: {
120
+ elementsAdded: elementsAdded.length,
121
+ elementsRemoved: elementsRemoved.length,
122
+ elementsChanged: elementsChanged.length,
123
+ relationshipsAdded: relationshipsAdded.length,
124
+ relationshipsRemoved: relationshipsRemoved.length
125
+ },
126
+ elementsAdded: cut(elementsAdded),
127
+ elementsRemoved: cut(elementsRemoved),
128
+ elementsChanged: cut(elementsChanged),
129
+ relationshipsAdded: cut(relationshipsAdded).map(parseKey),
130
+ relationshipsRemoved: cut(relationshipsRemoved).map(parseKey),
131
+ truncated: [elementsAdded, elementsRemoved, elementsChanged, relationshipsAdded, relationshipsRemoved].some((l) => l.length > DRIFT_LIST_LIMIT)
132
+ };
133
+ } 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.");
134
+ 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;
135
+ return overlapCount > 0 && warnings.push(`The base now overlaps ${overlapCount} staged change(s): ` + [
136
+ ...overlapping.elements.map((e) => `${e.id} (${e.reason})`),
137
+ ...overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`)
138
+ ].slice(0, 10).join(", ") + (overlapCount > 10 ? ", ..." : "")), {
139
+ isDrifted,
140
+ warnings,
141
+ ...hash?.baseSnapshotId ? { baseSnapshotId: hash.baseSnapshotId } : {},
142
+ ...hash?.currentSnapshotId ? { currentSnapshotId: hash.currentSnapshotId } : {},
143
+ baseIndex: read.status,
144
+ overlapping,
145
+ ...baseChanges ? { baseChanges } : {}
146
+ };
147
+ }
148
+ function stableStringify(value) {
149
+ 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(",")}}`;
150
+ }
151
+ export {
152
+ BASE_INDEX_FILE,
153
+ DRIFT_LIST_LIMIT,
154
+ captureBaseIndex,
155
+ describeDrift,
156
+ readBaseIndex,
157
+ tryReadBaseIndex,
158
+ writeBaseIndex
159
+ };
160
+ //# 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, and relationships the plan\n * adds or deletes with an end on a base element that changed or was removed since (`endpoints`\n * names those ends). The plan doesn't edit such an element, but what it links to is not what it was.\n */\n overlapping: {\n elements: Array<{ id: string; reason: \"already-in-base\" | \"already-removed\" | \"changed-in-base\" }>;\n relationships: Array<\n RelationshipRef & {\n reason: \"already-in-base\" | \"already-removed\" | \"endpoint-changed-in-base\" | \"endpoint-removed-in-base\";\n /** For the endpoint reasons: the ends the base changed or removed. */\n endpoints?: string[];\n }\n >;\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 // The plan's links into base elements that changed or went away since: not an edit of theirs,\n // but what the link reaches is not what the plan saw.\n const changedSet = new Set(elementsChanged);\n const removedSet = new Set(elementsRemoved);\n const listed = new Set(overlapping.relationships.map((r) => relationshipKey(r)));\n for (const key of net.keys()) {\n if (listed.has(key)) continue;\n const ref = parseKey(key);\n const ends = [ref.source, ref.target].filter((id, i, all) => all.indexOf(id) === i);\n const removed = ends.filter((id) => removedSet.has(id) && !addedHere.has(id));\n const changed = ends.filter((id) => changedSet.has(id) && !elementOps.has(id));\n if (removed.length > 0) {\n overlapping.relationships.push({ ...ref, reason: \"endpoint-removed-in-base\", endpoints: removed });\n } else if (changed.length > 0) {\n overlapping.relationships.push({ ...ref, reason: \"endpoint-changed-in-base\", endpoints: changed });\n }\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,256 @@
1
+ import { getAllLayerIds } from "../generated/layer-registry.js";
2
+ const BRIEF_SECTIONS = [
3
+ "changes",
4
+ "changeset",
5
+ "drift",
6
+ "layers",
7
+ "relationshipChanges",
8
+ "alreadyInBase",
9
+ "gaps",
10
+ "unwired",
11
+ "intent",
12
+ "codeTouched",
13
+ "feedback",
14
+ "delta"
15
+ ], ELEMENT_DESCRIPTION_LIMIT = 160, FEEDBACK_LIMIT = 200, LARGE_PLAN = 60, IDS_PER_TYPE = 5, LIST_LIMIT = 30, rel = (r) => `${r.source} --${r.predicate}--> ${r.target}`, gapLine = (g) => `${g.element}: ${g.kind}: ${g.detail}`;
16
+ function gapLines(gaps) {
17
+ if (gaps.length <= LIST_LIMIT)
18
+ return gaps.map(gapLine);
19
+ const groups = /* @__PURE__ */ new Map();
20
+ for (const g of gaps) {
21
+ const key = `${g.kind}: ${g.detail}`;
22
+ groups.set(key, [...groups.get(key) ?? [], g.element]);
23
+ }
24
+ return [...groups].map(([key, elements]) => elements.length === 1 ? `${elements[0]}: ${key}` : `${key} (${elements.length} elements: ${elements.slice(0, IDS_PER_TYPE).join(", ")}${elements.length > IDS_PER_TYPE ? `, +${elements.length - IDS_PER_TYPE} more` : ""})`);
25
+ }
26
+ const cut = (text, limit) => text.length > limit ? `${text.slice(0, limit - 1)}\u2026` : text;
27
+ function unwiredCounts(unwired) {
28
+ const counts = /* @__PURE__ */ new Map();
29
+ let total = 0;
30
+ for (const element of unwired)
31
+ for (const { layer, entries } of element.byLayer)
32
+ counts.set(layer, (counts.get(layer) ?? 0) + entries.length), total += entries.length;
33
+ const order = getAllLayerIds(), rank = (layer) => order.includes(layer) ? order.indexOf(layer) : order.length, byLayer = Object.fromEntries([...counts].sort(([a], [b]) => rank(a) - rank(b)));
34
+ return { total, byLayer };
35
+ }
36
+ function compactDrift(drift) {
37
+ const overlapping = drift.overlapping ? [
38
+ ...drift.overlapping.elements.map((e) => `${e.id}: ${e.reason}`),
39
+ ...drift.overlapping.relationships.map((r) => `${rel(r)}: ${r.reason}${r.endpoints ? ` (${r.endpoints.join(", ")})` : ""}`)
40
+ ] : void 0;
41
+ return {
42
+ isDrifted: drift.isDrifted,
43
+ warnings: drift.warnings,
44
+ ...drift.baseIndex ? { baseIndex: drift.baseIndex } : {},
45
+ ...overlapping ? { overlapping } : {},
46
+ ...drift.baseChanges ? {
47
+ baseChanges: {
48
+ counts: drift.baseChanges.counts,
49
+ elementsChanged: drift.baseChanges.elementsChanged,
50
+ elementsRemoved: drift.baseChanges.elementsRemoved,
51
+ truncated: drift.baseChanges.truncated
52
+ }
53
+ } : {}
54
+ };
55
+ }
56
+ function capped(list, name, counts, limit = LIST_LIMIT) {
57
+ return list.length <= limit ? list : (counts[name] = list.length, list.slice(0, limit));
58
+ }
59
+ function compactDelta(delta) {
60
+ const counts = {}, out = {
61
+ since: delta.since,
62
+ revision: delta.revision,
63
+ elements: {
64
+ added: capped(delta.elements.added, "elements.added", counts),
65
+ removed: capped(delta.elements.removed, "elements.removed", counts),
66
+ updated: capped(delta.elements.updated, "elements.updated", counts).map((u) => `${u.element} (${u.fields.join(", ")})`)
67
+ },
68
+ relationships: {
69
+ added: capped(delta.relationships.added, "relationships.added", counts).map(rel),
70
+ removed: capped(delta.relationships.removed, "relationships.removed", counts).map(rel)
71
+ },
72
+ unstaged: delta.unstaged.map((u) => `#${u.sequence} ${u.operation} ${u.source ? rel({ source: u.source, predicate: u.predicate ?? "?", target: u.target ?? "?" }) : u.elementId ?? ""} (unstaged at ${u.removedAt})`),
73
+ gaps: { opened: gapLines(delta.gaps.opened), closed: gapLines(delta.gaps.closed) },
74
+ unwired: { wired: delta.unwired.wired.length, new: delta.unwired.new.length, newElements: delta.unwired.newElements.length },
75
+ reach: delta.reach,
76
+ intent: {
77
+ newlyTraced: capped(delta.intent.newlyTraced, "intent.newlyTraced", counts),
78
+ newlyUntraced: capped(delta.intent.newlyUntraced, "intent.newlyUntraced", counts)
79
+ },
80
+ ...delta.warnings ? { warnings: delta.warnings } : {}
81
+ };
82
+ return Object.keys(counts).length > 0 && (out.counts = counts), out;
83
+ }
84
+ function compactChanges(brief) {
85
+ const changedIds = new Set(brief.layers.flatMap((l) => l.changes.map((c) => c.id))), filesOf = /* @__PURE__ */ new Map(), files = /* @__PURE__ */ new Map(), neighborFiles = [], baseName = (file) => file.slice(file.lastIndexOf("/") + 1), nameCount = /* @__PURE__ */ new Map();
86
+ for (const { file } of brief.codeTouched)
87
+ nameCount.set(baseName(file), (nameCount.get(baseName(file)) ?? 0) + 1);
88
+ const shortName = (file) => nameCount.get(baseName(file)) === 1 ? baseName(file) : file;
89
+ for (const { file, elements } of brief.codeTouched) {
90
+ const changed = elements.filter((e) => e.role === "changed");
91
+ changed.length === 0 ? neighborFiles.push(file) : files.set(file, changed.length);
92
+ for (const e of changed)
93
+ filesOf.set(e.id, [...filesOf.get(e.id) ?? [], shortName(file)]);
94
+ }
95
+ const changes = [], build = [];
96
+ for (const c of brief.layers.flatMap((l) => l.changes)) {
97
+ const connections = [];
98
+ for (const n of c.connections) {
99
+ if (n.direction === "in" && changedIds.has(n.other))
100
+ continue;
101
+ const mark = n.status === "deleted" ? " (deleted)" : n.status === "unchanged" ? " (base)" : "";
102
+ connections.push(n.direction === "out" ? `${n.predicate} \u2192 ${n.other}${mark}` : `${n.predicate} \u2190 ${n.other}${mark}`);
103
+ }
104
+ const sourceFiles = filesOf.has(c.id) ? { sourceFiles: filesOf.get(c.id) } : {};
105
+ changes.push({
106
+ id: c.id,
107
+ op: c.op,
108
+ ...c.description ? { description: cut(c.description, ELEMENT_DESCRIPTION_LIMIT) } : {},
109
+ ...sourceFiles,
110
+ ...c.changedFields?.length ? { changedFields: c.changedFields.map((f) => f.field) } : {},
111
+ connections
112
+ }), build.push({
113
+ id: c.id,
114
+ op: c.op,
115
+ name: c.name,
116
+ ...c.description ? { description: c.description } : {},
117
+ ...c.attributes && Object.keys(c.attributes).length > 0 ? { attributes: c.attributes } : {},
118
+ ...c.changedFields?.length ? { changedFields: c.changedFields } : {},
119
+ ...sourceFiles,
120
+ connections
121
+ });
122
+ }
123
+ return { changes, build, files, neighborFiles };
124
+ }
125
+ function changesByType(brief, idsPerType = IDS_PER_TYPE) {
126
+ const groups = /* @__PURE__ */ new Map();
127
+ for (const layer of brief.layers)
128
+ for (const c of layer.changes) {
129
+ const parts = c.id.split("."), type = parts.length >= 3 ? `${parts[0]}.${parts[1]}` : layer.layer, group = groups.get(type) ?? { type, added: 0, updated: 0, deleted: 0, ids: [] };
130
+ group[c.op] += 1, group.ids.length < idsPerType ? group.ids.push(c.id) : group.more = (group.more ?? 0) + 1, groups.set(type, group);
131
+ }
132
+ return [...groups.values()];
133
+ }
134
+ function headerOf(brief) {
135
+ return { id: brief.changeset.id, name: brief.changeset.name, status: brief.changeset.status };
136
+ }
137
+ function compactBrief(brief, options = {}) {
138
+ const { changes, files, neighborFiles } = compactChanges(brief), changedCount = changes.length, large = changedCount > LARGE_PLAN, description = brief.changeset.description, header = { ...headerOf(brief), ...description ? { description } : {} };
139
+ brief.changeset.drift && (header.drift = compactDrift(brief.changeset.drift));
140
+ const cutLists = {}, unanchored = brief.intent.unanchored?.map((u) => u.element), intent = {
141
+ traced: capped(brief.intent.traced.map((t) => t.element), "traced", cutLists),
142
+ untraced: capped(brief.intent.untraced, "untraced", cutLists),
143
+ ...unanchored ? { unanchored: capped(unanchored, "unanchored", cutLists) } : {}
144
+ };
145
+ Object.keys(cutLists).length > 0 && (intent.counts = {
146
+ traced: brief.intent.traced.length,
147
+ untraced: brief.intent.untraced.length,
148
+ ...unanchored ? { unanchored: unanchored.length } : {}
149
+ });
150
+ const withDelta = !!options.delta, elementList = withDelta ? {} : large ? { changesByType: changesByType(brief) } : { changes }, sectionNames = `sections: [...] for only some sections, in full (${BRIEF_SECTIONS.join(", ")})`, more = (withDelta ? 'With since, the changed elements are left out: the delta says what changed (sections: ["changes"] lists them). ' : large ? `This plan changes ${changedCount} elements, so changesByType counts them per type with the first ${IDS_PER_TYPE} ids of each; sections: ["changes"] lists every one with its connections. ` : "Each changed element is listed with a short description, its files and its connections; attributes are in the full brief. ") + (brief.gaps.length > LIST_LIMIT ? 'Gaps that say the same thing share a line (sections: ["gaps"] lists each). ' : "") + (intent.counts ? `Intent lists are cut at ${LIST_LIMIT} ids (intent.counts has their lengths; sections: ["intent"] has them all). ` : "") + `Pass detail: "full" for the whole brief, or ${sectionNames}.`, out = {
151
+ detail: "compact",
152
+ revision: brief.revision,
153
+ revisionToken: brief.revisionToken,
154
+ changeset: header,
155
+ summary: brief.summary,
156
+ byLayer: brief.layers.map((l) => ({
157
+ layer: l.layer,
158
+ added: l.changes.filter((c) => c.op === "added").length,
159
+ updated: l.changes.filter((c) => c.op === "updated").length,
160
+ deleted: l.changes.filter((c) => c.op === "deleted").length
161
+ })),
162
+ reach: brief.reach,
163
+ gaps: gapLines(brief.gaps),
164
+ intent,
165
+ ...brief.unwired && !options.unwired ? { unwired: brief.unwired } : options.unwired ? { unwired: unwiredCounts(options.unwired) } : {},
166
+ ...elementList,
167
+ relationshipChanges: brief.relationshipChanges.map((r) => `${r.status} ${rel(r)}`),
168
+ codeTouched: [...files].map(([file, n]) => `${file} (${n})`),
169
+ neighborFiles,
170
+ feedback: brief.feedback.map(feedbackLine),
171
+ more
172
+ };
173
+ return brief.alreadyInBase.elements.length + brief.alreadyInBase.relationships.length > 0 && (out.alreadyInBase = brief.alreadyInBase), options.delta && (out.delta = compactDelta(options.delta)), out;
174
+ }
175
+ const feedbackLine = (f) => `${f.annotationId} on ${f.elementId} (${f.author}, ${f.replies} ${f.replies === 1 ? "reply" : "replies"}): ${cut(f.content, FEEDBACK_LIMIT)}`;
176
+ function briefSections(brief, sections, options = {}) {
177
+ const names = [...new Set(sections)], filter = options.filter && (options.filter.layers?.length || options.filter.ids?.length) ? options.filter : void 0, ids = filter?.ids ? new Set(filter.ids) : void 0, layers = filter?.layers ? new Set(filter.layers) : void 0, keep = (id) => filter ? (ids?.has(id) ?? !1) || (layers?.has(id.split(".")[0]) ?? !1) : !0, out = {
178
+ detail: "compact",
179
+ revision: brief.revision,
180
+ revisionToken: brief.revisionToken,
181
+ changeset: headerOf(brief),
182
+ summary: brief.summary,
183
+ sections: names,
184
+ ...filter ? { filter } : {},
185
+ more: ""
186
+ }, notes = [];
187
+ for (const name of names)
188
+ switch (name) {
189
+ case "changes":
190
+ out.changes = compactChanges(brief).build.filter((c) => keep(c.id));
191
+ break;
192
+ case "changeset": {
193
+ const description = brief.changeset.description;
194
+ out.changeset = {
195
+ ...out.changeset,
196
+ ...description ? { description } : {},
197
+ ...brief.changeset.drift ? { drift: brief.changeset.drift } : {}
198
+ };
199
+ break;
200
+ }
201
+ case "drift":
202
+ brief.changeset.drift ? out.drift = brief.changeset.drift : notes.push("No drift: the changeset is not staged.");
203
+ break;
204
+ case "layers":
205
+ out.layers = filter ? brief.layers.map((l) => ({ ...l, changes: l.changes.filter((c) => keep(c.id)) })).filter((l) => l.changes.length > 0) : brief.layers;
206
+ break;
207
+ case "relationshipChanges":
208
+ out.relationshipChanges = brief.relationshipChanges;
209
+ break;
210
+ case "alreadyInBase":
211
+ out.alreadyInBase = brief.alreadyInBase;
212
+ break;
213
+ case "gaps":
214
+ out.gaps = brief.gaps.filter((g) => keep(g.element));
215
+ break;
216
+ case "unwired":
217
+ brief.unwired && (out.unwired = filter ? { ...brief.unwired, elements: brief.unwired.elements.filter((e) => keep(e.element)) } : brief.unwired);
218
+ break;
219
+ case "intent":
220
+ out.intent = filter ? {
221
+ traced: brief.intent.traced.filter((t) => keep(t.element)),
222
+ untraced: brief.intent.untraced.filter(keep),
223
+ ...brief.intent.unanchored ? { unanchored: brief.intent.unanchored.filter((u) => keep(u.element)) } : {}
224
+ } : brief.intent;
225
+ break;
226
+ case "codeTouched":
227
+ out.codeTouched = brief.codeTouched;
228
+ break;
229
+ case "feedback":
230
+ out.feedback = brief.feedback.filter((f) => keep(f.elementId));
231
+ break;
232
+ case "delta":
233
+ options.delta || notes.push("delta needs since: pass the revisionToken of an earlier brief.");
234
+ break;
235
+ }
236
+ if (options.delta && (out.delta = names.includes("delta") ? options.delta : compactDelta(options.delta)), filter && ids) {
237
+ const known = new Set(brief.layers.flatMap((l) => l.changes.map((c) => c.id))), unknown = [...ids].filter((id) => !known.has(id));
238
+ unknown.length > 0 && notes.push(`Not changed by this changeset: ${unknown.join(", ")}.`);
239
+ }
240
+ return out.more = [
241
+ ...notes,
242
+ `Only the sections asked for are returned (${names.join(", ")})` + (filter ? ", for the elements the filter keeps" : "") + '. Call without sections for the compact brief, or with detail: "full" for everything.'
243
+ ].join(" "), out;
244
+ }
245
+ export {
246
+ BRIEF_SECTIONS,
247
+ LARGE_PLAN,
248
+ LIST_LIMIT,
249
+ briefSections,
250
+ changesByType,
251
+ compactBrief,
252
+ compactDelta,
253
+ gapLines,
254
+ unwiredCounts
255
+ };
256
+ //# sourceMappingURL=changeset-brief-compact.js.map