@documentation-robotics/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +46 -16
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/active-analyzer.js +62 -0
  5. package/dist/analyzers/active-analyzer.js.map +7 -0
  6. package/dist/analyzers/call-graph-utils.js +2 -2
  7. package/dist/analyzers/call-graph-utils.js.map +2 -2
  8. package/dist/analyzers/cards/build.js +191 -0
  9. package/dist/analyzers/cards/build.js.map +7 -0
  10. package/dist/analyzers/cards/classify.js +42 -0
  11. package/dist/analyzers/cards/classify.js.map +7 -0
  12. package/dist/analyzers/cards/edge-index.js +44 -0
  13. package/dist/analyzers/cards/edge-index.js.map +7 -0
  14. package/dist/analyzers/cards/extract.js +138 -0
  15. package/dist/analyzers/cards/extract.js.map +7 -0
  16. package/dist/analyzers/cards/facts.js +46 -0
  17. package/dist/analyzers/cards/facts.js.map +7 -0
  18. package/dist/analyzers/cards/graph.js +42 -0
  19. package/dist/analyzers/cards/graph.js.map +7 -0
  20. package/dist/analyzers/cards/index.js +8 -0
  21. package/dist/analyzers/cards/index.js.map +7 -0
  22. package/dist/analyzers/cards/routes.js +96 -0
  23. package/dist/analyzers/cards/routes.js.map +7 -0
  24. package/dist/analyzers/cards/tier0.js +18 -0
  25. package/dist/analyzers/cards/tier0.js.map +7 -0
  26. package/dist/analyzers/cards/types.js +1 -0
  27. package/dist/analyzers/cards/types.js.map +7 -0
  28. package/dist/analyzers/cbm-analyzer.js +270 -174
  29. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  30. package/dist/analyzers/cbm-responses.js +342 -0
  31. package/dist/analyzers/cbm-responses.js.map +7 -0
  32. package/dist/analyzers/code-changes.js +159 -0
  33. package/dist/analyzers/code-changes.js.map +7 -0
  34. package/dist/analyzers/edge-relationships.js +49 -0
  35. package/dist/analyzers/edge-relationships.js.map +7 -0
  36. package/dist/analyzers/extraction-plan.js +541 -0
  37. package/dist/analyzers/extraction-plan.js.map +7 -0
  38. package/dist/analyzers/index-job.js +229 -0
  39. package/dist/analyzers/index-job.js.map +7 -0
  40. package/dist/analyzers/mapping-loader.js +9 -0
  41. package/dist/analyzers/mapping-loader.js.map +2 -2
  42. package/dist/analyzers/tier0-rules.js +153 -0
  43. package/dist/analyzers/tier0-rules.js.map +7 -0
  44. package/dist/analyzers/types.js.map +1 -1
  45. package/dist/audit/relationships/ai/prompts.js +10 -10
  46. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  47. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  48. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  49. package/dist/cli.js +23 -11
  50. package/dist/cli.js.map +2 -2
  51. package/dist/coding-agents/default-permissions.js +31 -1
  52. package/dist/coding-agents/default-permissions.js.map +2 -2
  53. package/dist/coding-agents/drbot-mcp.js +269 -0
  54. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  55. package/dist/commands/add.js +7 -4
  56. package/dist/commands/add.js.map +2 -2
  57. package/dist/commands/analyzer.js +143 -40
  58. package/dist/commands/analyzer.js.map +2 -2
  59. package/dist/commands/changeset-verify.js +63 -0
  60. package/dist/commands/changeset-verify.js.map +7 -0
  61. package/dist/commands/changeset.js +80 -29
  62. package/dist/commands/changeset.js.map +2 -2
  63. package/dist/commands/delete.js +10 -4
  64. package/dist/commands/delete.js.map +2 -2
  65. package/dist/commands/farm.js +176 -60
  66. package/dist/commands/farm.js.map +2 -2
  67. package/dist/commands/init.js +9 -26
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +40 -21
  70. package/dist/commands/mcp.js.map +3 -3
  71. package/dist/commands/relationship.js +208 -37
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +3 -3
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +52 -32
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -103
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +19 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +1 -1
  82. package/dist/commands/visualize.js +34 -15
  83. package/dist/commands/visualize.js.map +2 -2
  84. package/dist/core/annotation-store.js +15 -5
  85. package/dist/core/annotation-store.js.map +2 -2
  86. package/dist/core/catalog-migration.js +34 -0
  87. package/dist/core/catalog-migration.js.map +7 -0
  88. package/dist/core/changeset-analysis.js +421 -0
  89. package/dist/core/changeset-analysis.js.map +7 -0
  90. package/dist/core/changeset-base-index.js +160 -0
  91. package/dist/core/changeset-base-index.js.map +7 -0
  92. package/dist/core/changeset-brief-compact.js +256 -0
  93. package/dist/core/changeset-brief-compact.js.map +7 -0
  94. package/dist/core/changeset-brief-delta.js +102 -0
  95. package/dist/core/changeset-brief-delta.js.map +7 -0
  96. package/dist/core/changeset-graph.js +281 -0
  97. package/dist/core/changeset-graph.js.map +7 -0
  98. package/dist/core/changeset-only-guard.js +32 -0
  99. package/dist/core/changeset-only-guard.js.map +7 -0
  100. package/dist/core/changeset-summary.js +68 -0
  101. package/dist/core/changeset-summary.js.map +7 -0
  102. package/dist/core/changeset-validator.js +66 -20
  103. package/dist/core/changeset-validator.js.map +2 -2
  104. package/dist/core/changeset-verify.js +848 -0
  105. package/dist/core/changeset-verify.js.map +7 -0
  106. package/dist/core/changeset.js +56 -9
  107. package/dist/core/changeset.js.map +2 -2
  108. package/dist/core/element-type-migration.js +145 -0
  109. package/dist/core/element-type-migration.js.map +7 -0
  110. package/dist/core/extraction-observability.js +161 -0
  111. package/dist/core/extraction-observability.js.map +7 -0
  112. package/dist/core/farm-manifest.js +12 -6
  113. package/dist/core/farm-manifest.js.map +2 -2
  114. package/dist/core/farm-sync-engine.js +1 -1
  115. package/dist/core/farm-sync-engine.js.map +2 -2
  116. package/dist/core/farm-sync-state.js +10 -2
  117. package/dist/core/farm-sync-state.js.map +2 -2
  118. package/dist/core/migration-registry.js +88 -47
  119. package/dist/core/migration-registry.js.map +2 -2
  120. package/dist/core/model-upgrade.js +102 -0
  121. package/dist/core/model-upgrade.js.map +7 -0
  122. package/dist/core/model.js +82 -30
  123. package/dist/core/model.js.map +2 -2
  124. package/dist/core/mutation-handler.js +71 -6
  125. package/dist/core/mutation-handler.js.map +2 -2
  126. package/dist/core/project-init.js +53 -0
  127. package/dist/core/project-init.js.map +7 -0
  128. package/dist/core/relationship-migration-table.js +6639 -0
  129. package/dist/core/relationship-migration-table.js.map +7 -0
  130. package/dist/core/relationship-migration.js +272 -0
  131. package/dist/core/relationship-migration.js.map +7 -0
  132. package/dist/core/relationship-prune.js +49 -0
  133. package/dist/core/relationship-prune.js.map +7 -0
  134. package/dist/core/relationship-staging.js +161 -0
  135. package/dist/core/relationship-staging.js.map +7 -0
  136. package/dist/core/relationships.js +26 -4
  137. package/dist/core/relationships.js.map +2 -2
  138. package/dist/core/report-data-model.js +7 -13
  139. package/dist/core/report-data-model.js.map +2 -2
  140. package/dist/core/staged-changeset-storage.js +145 -18
  141. package/dist/core/staged-changeset-storage.js.map +2 -2
  142. package/dist/core/staged-source-reference.js +28 -0
  143. package/dist/core/staged-source-reference.js.map +7 -0
  144. package/dist/core/staging-area.js +166 -24
  145. package/dist/core/staging-area.js.map +2 -2
  146. package/dist/core/upgrade-plan.js +117 -0
  147. package/dist/core/upgrade-plan.js.map +7 -0
  148. package/dist/core/virtual-projection.js +13 -5
  149. package/dist/core/virtual-projection.js.map +2 -2
  150. package/dist/export/archimate-exporter.js +1 -0
  151. package/dist/export/archimate-exporter.js.map +2 -2
  152. package/dist/generated/compiled-validators.js +2 -2
  153. package/dist/generated/compiled-validators.js.map +2 -2
  154. package/dist/generated/layer-registry.js +16 -16
  155. package/dist/generated/layer-registry.js.map +2 -2
  156. package/dist/generated/node-types.js +186 -168
  157. package/dist/generated/node-types.js.map +2 -2
  158. package/dist/generated/relationship-index.js +2468 -7913
  159. package/dist/generated/relationship-index.js.map +2 -2
  160. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  161. package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
  162. package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
  163. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  164. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  165. package/dist/integrations/claude_code/commands/dr-map.md +20 -1
  166. package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
  167. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  168. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  169. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  170. package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
  171. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  172. package/dist/integrations/claude_code/docs/README.md +68 -2
  173. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  174. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  175. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  176. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  177. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  178. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  179. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  180. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  181. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  182. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  183. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  184. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  185. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  186. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  187. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
  188. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
  189. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
  190. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
  191. package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
  192. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  193. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  194. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  195. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  196. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  197. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  198. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  199. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  200. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  201. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  202. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  203. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  204. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
  205. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
  206. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  207. package/dist/integrations/hash-utils.js.map +2 -2
  208. package/dist/mcp/api-key-manager.js +14 -1
  209. package/dist/mcp/api-key-manager.js.map +2 -2
  210. package/dist/mcp/changeset-only.js +45 -0
  211. package/dist/mcp/changeset-only.js.map +7 -0
  212. package/dist/mcp/project-root.js +85 -0
  213. package/dist/mcp/project-root.js.map +7 -0
  214. package/dist/mcp/server-instructions.js +27 -0
  215. package/dist/mcp/server-instructions.js.map +7 -0
  216. package/dist/mcp/strict-schema.js +52 -0
  217. package/dist/mcp/strict-schema.js.map +7 -0
  218. package/dist/mcp/tool-registry.js +55 -5
  219. package/dist/mcp/tool-registry.js.map +2 -2
  220. package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
  221. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  222. package/dist/mcp/tools/changeset-commit.js +224 -0
  223. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  224. package/dist/mcp/tools/changeset-editing.js +260 -0
  225. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  226. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  227. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  228. package/dist/mcp/tools/changeset-shared.js +31 -0
  229. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  230. package/dist/mcp/tools/changeset-show.js +17 -1
  231. package/dist/mcp/tools/changeset-show.js.map +2 -2
  232. package/dist/mcp/tools/changeset-verify.js +49 -0
  233. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  234. package/dist/mcp/tools/codebase-extract.js +836 -0
  235. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  236. package/dist/mcp/tools/codebase-index.js +125 -0
  237. package/dist/mcp/tools/codebase-index.js.map +7 -0
  238. package/dist/mcp/tools/codebase-shared.js +265 -0
  239. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  240. package/dist/mcp/tools/codebase-verify.js +48 -0
  241. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  242. package/dist/mcp/tools/index.js +34 -1
  243. package/dist/mcp/tools/index.js.map +2 -2
  244. package/dist/mcp/tools/model-add.js +191 -74
  245. package/dist/mcp/tools/model-add.js.map +2 -2
  246. package/dist/mcp/tools/model-delete.js +2 -2
  247. package/dist/mcp/tools/model-delete.js.map +2 -2
  248. package/dist/mcp/tools/model-search.js +14 -0
  249. package/dist/mcp/tools/model-search.js.map +2 -2
  250. package/dist/mcp/tools/model-show.js +29 -2
  251. package/dist/mcp/tools/model-show.js.map +2 -2
  252. package/dist/mcp/tools/model-trace.js +93 -30
  253. package/dist/mcp/tools/model-trace.js.map +2 -2
  254. package/dist/mcp/tools/model-update.js +46 -26
  255. package/dist/mcp/tools/model-update.js.map +2 -2
  256. package/dist/mcp/tools/model-validate.js +46 -4
  257. package/dist/mcp/tools/model-validate.js.map +2 -2
  258. package/dist/mcp/tools/project-status-collectors.js +381 -0
  259. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  260. package/dist/mcp/tools/project-status.js +237 -0
  261. package/dist/mcp/tools/project-status.js.map +7 -0
  262. package/dist/mcp/tools/project-tools.js +505 -0
  263. package/dist/mcp/tools/project-tools.js.map +7 -0
  264. package/dist/mcp/tools/relationship-tools.js +167 -0
  265. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  266. package/dist/mcp/tools/review-open.js +210 -0
  267. package/dist/mcp/tools/review-open.js.map +7 -0
  268. package/dist/mcp/tools/shared.js +35 -4
  269. package/dist/mcp/tools/shared.js.map +2 -2
  270. package/dist/mcp/tools/spec-type.js +90 -0
  271. package/dist/mcp/tools/spec-type.js.map +7 -0
  272. package/dist/reports/model-report-orchestrator.js +31 -2
  273. package/dist/reports/model-report-orchestrator.js.map +2 -2
  274. package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
  275. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  276. package/dist/schemas/bundled/api.json +1029 -1658
  277. package/dist/schemas/bundled/apm.json +499 -734
  278. package/dist/schemas/bundled/application.json +735 -371
  279. package/dist/schemas/bundled/base.json +3 -220
  280. package/dist/schemas/bundled/business.json +452 -366
  281. package/dist/schemas/bundled/data-model.json +395 -411
  282. package/dist/schemas/bundled/data-store.json +334 -558
  283. package/dist/schemas/bundled/manifest.json +18 -18
  284. package/dist/schemas/bundled/motivation.json +349 -368
  285. package/dist/schemas/bundled/navigation.json +415 -731
  286. package/dist/schemas/bundled/product.json +325 -159
  287. package/dist/schemas/bundled/security.json +864 -1232
  288. package/dist/schemas/bundled/technology.json +574 -703
  289. package/dist/schemas/bundled/testing.json +865 -1646
  290. package/dist/schemas/bundled/ux.json +739 -1352
  291. package/dist/server/schemas.js +8 -0
  292. package/dist/server/schemas.js.map +2 -2
  293. package/dist/server/server.js +686 -231
  294. package/dist/server/server.js.map +3 -3
  295. package/dist/server/viewer-registry.js +264 -0
  296. package/dist/server/viewer-registry.js.map +7 -0
  297. package/dist/server-entry.js +2 -1
  298. package/dist/server-entry.js.map +2 -2
  299. package/dist/telemetry/index.js +1 -1
  300. package/dist/utils/ansi-suppressor.js +6 -8
  301. package/dist/utils/ansi-suppressor.js.map +2 -2
  302. package/dist/utils/attribute-patch.js +41 -0
  303. package/dist/utils/attribute-patch.js.map +7 -0
  304. package/dist/utils/error-hints.js +29 -0
  305. package/dist/utils/error-hints.js.map +7 -0
  306. package/dist/utils/errors.js +9 -3
  307. package/dist/utils/errors.js.map +2 -2
  308. package/dist/utils/id-generator.js +5 -2
  309. package/dist/utils/id-generator.js.map +2 -2
  310. package/dist/utils/project-paths.js +26 -1
  311. package/dist/utils/project-paths.js.map +2 -2
  312. package/dist/utils/source-reference.js +64 -31
  313. package/dist/utils/source-reference.js.map +2 -2
  314. package/dist/utils/spec-version.js +2 -2
  315. package/dist/utils/spec-version.js.map +1 -1
  316. package/dist/validators/composed-reference-validator.js +5 -27
  317. package/dist/validators/composed-reference-validator.js.map +2 -2
  318. package/dist/validators/model-warnings.js +28 -0
  319. package/dist/validators/model-warnings.js.map +7 -0
  320. package/dist/validators/reference-validator.js +0 -7
  321. package/dist/validators/reference-validator.js.map +2 -2
  322. package/dist/validators/relationship-schema-validator.js +3 -3
  323. package/dist/validators/relationship-schema-validator.js.map +2 -2
  324. package/dist/validators/schema-validator.js +27 -9
  325. package/dist/validators/schema-validator.js.map +2 -2
  326. package/dist/validators/types.js.map +1 -1
  327. package/dist/viewer/README.md +6 -6
  328. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  329. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  330. package/dist/viewer/index.html +2 -2
  331. package/dist/viewer/manifest.json +10 -10
  332. package/dist/viewer/version.json +1 -1
  333. package/package.json +2 -1
  334. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  335. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -1,3 +1,9 @@
1
+ function elementIdentifiers(element, ...extra) {
2
+ const ids = /* @__PURE__ */ new Set();
3
+ for (const candidate of [element.id, element.path, ...extra])
4
+ typeof candidate == "string" && candidate.length > 0 && ids.add(candidate);
5
+ return [...ids];
6
+ }
1
7
  class Relationships {
2
8
  relationships = [];
3
9
  dirty = !1;
@@ -20,14 +26,29 @@ class Relationships {
20
26
  return deletedCount > 0 && (this.dirty = !0), deletedCount;
21
27
  }
22
28
  /**
23
- * Delete all relationships involving an element (as source or target)
29
+ * Delete all relationships involving an element (as source or target).
30
+ *
31
+ * A row may name an element by its UUID `id` or by its dotted `path`, so callers
32
+ * should pass every identifier the element is known by (see
33
+ * `elementIdentifiers()`); any row whose source or target matches one of them
34
+ * is removed.
24
35
  */
25
36
  deleteForElement(elementId) {
26
- const initialLength = this.relationships.length;
27
- this.relationships = this.relationships.filter((rel) => rel.source !== elementId && rel.target !== elementId);
37
+ const ids = new Set((Array.isArray(elementId) ? elementId : [elementId]).filter((id) => !!id)), initialLength = this.relationships.length;
38
+ this.relationships = this.relationships.filter((rel) => !ids.has(rel.source) && !ids.has(rel.target));
28
39
  const deletedCount = initialLength - this.relationships.length;
29
40
  return this.dirty = !0, deletedCount;
30
41
  }
42
+ /**
43
+ * Keep only the relationships for which `keep` returns true, preserving order.
44
+ * Returns the number of relationships removed.
45
+ */
46
+ retain(keep) {
47
+ const initialLength = this.relationships.length;
48
+ this.relationships = this.relationships.filter(keep);
49
+ const removedCount = initialLength - this.relationships.length;
50
+ return removedCount > 0 && (this.dirty = !0), removedCount;
51
+ }
31
52
  /**
32
53
  * Get all relationships for an element
33
54
  */
@@ -73,6 +94,7 @@ class Relationships {
73
94
  }
74
95
  }
75
96
  export {
76
- Relationships
97
+ Relationships,
98
+ elementIdentifiers
77
99
  };
78
100
  //# sourceMappingURL=relationships.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/relationships.ts"],
4
- "sourcesContent": ["/**\n * Relationships management for intra-layer and cross-layer relationships\n */\n\nexport interface Relationship {\n source: string;\n predicate: string;\n target: string;\n category?: \"structural\" | \"behavioral\";\n layer: string;\n targetLayer?: string;\n properties?: Record<string, unknown>;\n}\n\n/**\n * Relationships class for managing the central relationships.yaml file\n */\nexport class Relationships {\n private relationships: Relationship[] = [];\n private dirty: boolean = false;\n\n constructor(relationships: Relationship[] = []) {\n this.relationships = relationships;\n }\n\n /**\n * Add a relationship\n */\n add(relationship: Relationship): void {\n this.relationships.push(relationship);\n this.dirty = true;\n }\n\n /**\n * Delete relationships matching criteria\n */\n delete(source: string, target: string, predicate?: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) =>\n !(\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n )\n );\n const deletedCount = initialLength - this.relationships.length;\n if (deletedCount > 0) {\n this.dirty = true;\n }\n return deletedCount;\n }\n\n /**\n * Delete all relationships involving an element (as source or target)\n */\n deleteForElement(elementId: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) => rel.source !== elementId && rel.target !== elementId\n );\n const deletedCount = initialLength - this.relationships.length;\n // Always mark as dirty to ensure file is rewritten (Python CLI behavior)\n this.dirty = true;\n return deletedCount;\n }\n\n /**\n * Get all relationships for an element\n */\n getForElement(elementId: string): {\n outgoing: Relationship[];\n incoming: Relationship[];\n } {\n const outgoing = this.relationships.filter((rel) => rel.source === elementId);\n const incoming = this.relationships.filter((rel) => rel.target === elementId);\n return { outgoing, incoming };\n }\n\n /**\n * Get all relationships\n */\n getAll(): Relationship[] {\n return [...this.relationships];\n }\n\n /**\n * Find relationships between two elements\n */\n find(source: string, target: string, predicate?: string): Relationship[] {\n return this.relationships.filter(\n (rel) =>\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n );\n }\n\n /**\n * Check if relationships have unsaved changes\n */\n isDirty(): boolean {\n return this.dirty;\n }\n\n /**\n * Mark relationships as clean (no unsaved changes)\n */\n markClean(): void {\n this.dirty = false;\n }\n\n /**\n * Serialize to array for YAML\n */\n toArray(): Relationship[] {\n return this.relationships;\n }\n\n /**\n * Load from array (from YAML)\n */\n static fromArray(data: Relationship[]): Relationships {\n return new Relationships(data);\n }\n}\n"],
5
- "mappings": "AAiBM,MAAO,cAAa;EAChB,gBAAgC,CAAA;EAChC,QAAiB;EAEzB,YAAY,gBAAgC,CAAA,GAAE;AAC5C,SAAK,gBAAgB;EACvB;;;;EAKA,IAAI,cAA0B;AAC5B,SAAK,cAAc,KAAK,YAAY,GACpC,KAAK,QAAQ;EACf;;;;EAKA,OAAO,QAAgB,QAAgB,WAAkB;AACvD,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QACC,EACE,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,WAClC;AAEL,UAAM,eAAe,gBAAgB,KAAK,cAAc;AACxD,WAAI,eAAe,MACjB,KAAK,QAAQ,KAER;EACT;;;;EAKA,iBAAiB,WAAiB;AAChC,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QAAQ,IAAI,WAAW,aAAa,IAAI,WAAW,SAAS;AAE/D,UAAM,eAAe,gBAAgB,KAAK,cAAc;AAExD,gBAAK,QAAQ,IACN;EACT;;;;EAKA,cAAc,WAAiB;AAI7B,UAAM,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS,GACtE,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS;AAC5E,WAAO,EAAE,UAAU,SAAQ;EAC7B;;;;EAKA,SAAM;AACJ,WAAO,CAAC,GAAG,KAAK,aAAa;EAC/B;;;;EAKA,KAAK,QAAgB,QAAgB,WAAkB;AACrD,WAAO,KAAK,cAAc,OACxB,CAAC,QACC,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,UAAU;EAEjD;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,YAAS;AACP,SAAK,QAAQ;EACf;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,OAAO,UAAU,MAAoB;AACnC,WAAO,IAAI,cAAc,IAAI;EAC/B;;",
4
+ "sourcesContent": ["/**\n * Relationships management for intra-layer and cross-layer relationships\n */\n\nexport interface Relationship {\n source: string;\n predicate: string;\n target: string;\n category?: \"structural\" | \"behavioral\";\n layer: string;\n targetLayer?: string;\n properties?: Record<string, unknown>;\n}\n\n/**\n * Every identifier a relationship row may use to name an element: its UUID `id`\n * and its dotted `path`. Both forms are legal in relationships.yaml.\n */\nexport function elementIdentifiers(\n element: { id?: unknown; path?: unknown },\n ...extra: Array<string | undefined>\n): string[] {\n const ids = new Set<string>();\n for (const candidate of [element.id, element.path, ...extra]) {\n if (typeof candidate === \"string\" && candidate.length > 0) {\n ids.add(candidate);\n }\n }\n return [...ids];\n}\n\n/**\n * Relationships class for managing the central relationships.yaml file\n */\nexport class Relationships {\n private relationships: Relationship[] = [];\n private dirty: boolean = false;\n\n constructor(relationships: Relationship[] = []) {\n this.relationships = relationships;\n }\n\n /**\n * Add a relationship\n */\n add(relationship: Relationship): void {\n this.relationships.push(relationship);\n this.dirty = true;\n }\n\n /**\n * Delete relationships matching criteria\n */\n delete(source: string, target: string, predicate?: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) =>\n !(\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n )\n );\n const deletedCount = initialLength - this.relationships.length;\n if (deletedCount > 0) {\n this.dirty = true;\n }\n return deletedCount;\n }\n\n /**\n * Delete all relationships involving an element (as source or target).\n *\n * A row may name an element by its UUID `id` or by its dotted `path`, so callers\n * should pass every identifier the element is known by (see\n * `elementIdentifiers()`); any row whose source or target matches one of them\n * is removed.\n */\n deleteForElement(elementId: string | string[]): number {\n const ids = new Set(\n (Array.isArray(elementId) ? elementId : [elementId]).filter((id) => !!id)\n );\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) => !ids.has(rel.source) && !ids.has(rel.target)\n );\n const deletedCount = initialLength - this.relationships.length;\n // Always mark as dirty to ensure file is rewritten (Python CLI behavior)\n this.dirty = true;\n return deletedCount;\n }\n\n /**\n * Keep only the relationships for which `keep` returns true, preserving order.\n * Returns the number of relationships removed.\n */\n retain(keep: (rel: Relationship) => boolean): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(keep);\n const removedCount = initialLength - this.relationships.length;\n if (removedCount > 0) {\n this.dirty = true;\n }\n return removedCount;\n }\n\n /**\n * Get all relationships for an element\n */\n getForElement(elementId: string): {\n outgoing: Relationship[];\n incoming: Relationship[];\n } {\n const outgoing = this.relationships.filter((rel) => rel.source === elementId);\n const incoming = this.relationships.filter((rel) => rel.target === elementId);\n return { outgoing, incoming };\n }\n\n /**\n * Get all relationships\n */\n getAll(): Relationship[] {\n return [...this.relationships];\n }\n\n /**\n * Find relationships between two elements\n */\n find(source: string, target: string, predicate?: string): Relationship[] {\n return this.relationships.filter(\n (rel) =>\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n );\n }\n\n /**\n * Check if relationships have unsaved changes\n */\n isDirty(): boolean {\n return this.dirty;\n }\n\n /**\n * Mark relationships as clean (no unsaved changes)\n */\n markClean(): void {\n this.dirty = false;\n }\n\n /**\n * Serialize to array for YAML\n */\n toArray(): Relationship[] {\n return this.relationships;\n }\n\n /**\n * Load from array (from YAML)\n */\n static fromArray(data: Relationship[]): Relationships {\n return new Relationships(data);\n }\n}\n"],
5
+ "mappings": "AAkBM,SAAU,mBACd,YACG,OAAgC;AAEnC,QAAM,MAAM,oBAAI,IAAG;AACnB,aAAW,aAAa,CAAC,QAAQ,IAAI,QAAQ,MAAM,GAAG,KAAK;AACzD,IAAI,OAAO,aAAc,YAAY,UAAU,SAAS,KACtD,IAAI,IAAI,SAAS;AAGrB,SAAO,CAAC,GAAG,GAAG;AAChB;AAKM,MAAO,cAAa;EAChB,gBAAgC,CAAA;EAChC,QAAiB;EAEzB,YAAY,gBAAgC,CAAA,GAAE;AAC5C,SAAK,gBAAgB;EACvB;;;;EAKA,IAAI,cAA0B;AAC5B,SAAK,cAAc,KAAK,YAAY,GACpC,KAAK,QAAQ;EACf;;;;EAKA,OAAO,QAAgB,QAAgB,WAAkB;AACvD,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QACC,EACE,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,WAClC;AAEL,UAAM,eAAe,gBAAgB,KAAK,cAAc;AACxD,WAAI,eAAe,MACjB,KAAK,QAAQ,KAER;EACT;;;;;;;;;EAUA,iBAAiB,WAA4B;AAC3C,UAAM,MAAM,IAAI,KACb,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,GAErE,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC;AAEvD,UAAM,eAAe,gBAAgB,KAAK,cAAc;AAExD,gBAAK,QAAQ,IACN;EACT;;;;;EAMA,OAAO,MAAoC;AACzC,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OAAO,IAAI;AACnD,UAAM,eAAe,gBAAgB,KAAK,cAAc;AACxD,WAAI,eAAe,MACjB,KAAK,QAAQ,KAER;EACT;;;;EAKA,cAAc,WAAiB;AAI7B,UAAM,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS,GACtE,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS;AAC5E,WAAO,EAAE,UAAU,SAAQ;EAC7B;;;;EAKA,SAAM;AACJ,WAAO,CAAC,GAAG,KAAK,aAAa;EAC/B;;;;EAKA,KAAK,QAAgB,QAAgB,WAAkB;AACrD,WAAO,KAAK,cAAc,OACxB,CAAC,QACC,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,UAAU;EAEjD;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,YAAS;AACP,SAAK,QAAQ;EACf;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,OAAO,UAAU,MAAoB;AACnC,WAAO,IAAI,cAAc,IAAI;EAC/B;;",
6
6
  "names": []
7
7
  }
@@ -248,22 +248,16 @@ class ReportDataModel {
248
248
  return Math.round(specCompliant / analysis.totalRelationships * 100);
249
249
  }
250
250
  /**
251
- * Calculate layer compliance score (directional compliance)
252
- * Measures adherence to the higher -> lower layer reference rule
251
+ * Calculate layer compliance score: the share of cross-layer relationships between known
252
+ * layers whose predicate is in the catalog. There is no layer-direction rule; direction is a
253
+ * property of the predicate (spec/DESIGN_CHARTER.md).
253
254
  */
254
255
  calculateLayerComplianceScore(analysis) {
255
- const validRelationships = analysis.classified.filter((rel) => {
256
- const sourceOrder = getLayerOrder(rel.sourceLayer), targetOrder = getLayerOrder(rel.targetLayer);
257
- return sourceOrder > 0 && targetOrder > 0;
258
- });
259
- if (validRelationships.length === 0)
256
+ const crossLayer = analysis.classified.filter((rel) => rel.isCrossLayer && getLayerOrder(rel.sourceLayer) > 0 && getLayerOrder(rel.targetLayer) > 0);
257
+ if (crossLayer.length === 0)
260
258
  return 100;
261
- let compliantCount = 0;
262
- for (const rel of validRelationships) {
263
- const sourceOrder = getLayerOrder(rel.sourceLayer), targetOrder = getLayerOrder(rel.targetLayer);
264
- sourceOrder >= targetOrder && compliantCount++;
265
- }
266
- return Math.round(compliantCount / validRelationships.length * 100);
259
+ const compliantCount = crossLayer.filter((rel) => rel.isSpecCompliant).length;
260
+ return Math.round(compliantCount / crossLayer.length * 100);
267
261
  }
268
262
  /**
269
263
  * Clear all cached data
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/report-data-model.ts"],
4
- "sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of relationships following higher\u2192lower layer rule\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: proportion of relationships going higher -> lower\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score (directional compliance)\n * Measures adherence to the higher -> lower layer reference rule\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n // Only count relationships with known (valid) layer names\n const validRelationships = analysis.classified.filter((rel) => {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n return sourceOrder > 0 && targetOrder > 0;\n });\n\n if (validRelationships.length === 0) return 100;\n\n let compliantCount = 0;\n\n for (const rel of validRelationships) {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n\n // Compliant if source >= target (higher layer -> lower/same layer)\n if (sourceOrder >= targetOrder) {\n compliantCount++;\n }\n }\n\n return Math.round((compliantCount / validRelationships.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of cross-layer relationships whose predicate is in the catalog (direction is\n * defined by the predicate, not by layer order; see spec/DESIGN_CHARTER.md)\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: cross-layer relationships whose predicate is in the catalog\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score: the share of cross-layer relationships between known\n * layers whose predicate is in the catalog. There is no layer-direction rule; direction is a\n * property of the predicate (spec/DESIGN_CHARTER.md).\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n const crossLayer = analysis.classified.filter(\n (rel) =>\n rel.isCrossLayer && getLayerOrder(rel.sourceLayer) > 0 && getLayerOrder(rel.targetLayer) > 0\n );\n\n if (crossLayer.length === 0) return 100;\n\n const compliantCount = crossLayer.filter((rel) => rel.isSpecCompliant).length;\n return Math.round((compliantCount / crossLayer.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,8 +4,25 @@ import { FileLock } from "../utils/file-lock.js";
4
4
  import path from "path";
5
5
  import yaml from "yaml";
6
6
  import ansis from "ansis";
7
- import { Changeset } from "./changeset.js";
7
+ import { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from "./changeset.js";
8
8
  import { getErrorMessage } from "../utils/errors.js";
9
+ const MAX_UNSTAGED_TOMBSTONES = 200;
10
+ function readTombstones(value, id) {
11
+ if (value == null)
12
+ return;
13
+ if (!Array.isArray(value)) {
14
+ console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));
15
+ return;
16
+ }
17
+ const valid = value.filter((t) => !!t && typeof t == "object" && typeof t.sequence == "number" && typeof t.removedAt == "number" && !!t.change && typeof t.change == "object" && typeof t.change.type == "string" && typeof t.change.elementId == "string");
18
+ return valid.length < value.length && console.warn(ansis.yellow(`\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`)), valid;
19
+ }
20
+ function addTombstones(changeset, tombstones) {
21
+ if (changeset.unstaged = [...changeset.unstaged, ...tombstones], changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {
22
+ const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);
23
+ changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES), changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));
24
+ }
25
+ }
9
26
  class StagedChangesetStorage {
10
27
  changesetsDir;
11
28
  constructor(rootPath) {
@@ -64,9 +81,11 @@ This may indicate disk failure or incomplete write. Check disk health and restor
64
81
  status: metadata.status,
65
82
  baseSnapshot: metadata.baseSnapshot,
66
83
  changes,
67
- stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 }
68
- };
69
- return Changeset.fromJSON(data);
84
+ stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },
85
+ ...typeof metadata.lastSequence == "number" ? { lastSequence: metadata.lastSequence } : {},
86
+ ...typeof metadata.unstagedPrunedThrough == "number" ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough } : {}
87
+ }, unstaged = readTombstones(metadata.unstaged, id);
88
+ return unstaged && (data.unstaged = unstaged), Changeset.fromJSON(data);
70
89
  } catch (error) {
71
90
  throw new Error(`Failed to load changeset '${id}': YAML parsing failed.
72
91
  Error: ${getErrorMessage(error)}
@@ -85,7 +104,7 @@ This indicates file corruption. Try:
85
104
  throw new Error("Changeset must have an id to be saved in YAML format");
86
105
  const sanitizedId = this.sanitizeId(changeset.id), changesetPath = path.join(this.changesetsDir, sanitizedId);
87
106
  await ensureDir(changesetPath);
88
- const metadata = {
107
+ const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset)), lastSequence = changeset.lastSequence !== void 0 ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber)) : void 0, metadata = {
89
108
  id: changeset.id,
90
109
  name: changeset.name,
91
110
  description: changeset.description,
@@ -93,9 +112,13 @@ This indicates file corruption. Try:
93
112
  modified: changeset.modified,
94
113
  status: changeset.status,
95
114
  baseSnapshot: changeset.baseSnapshot,
96
- stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 }
115
+ stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },
116
+ // Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.
117
+ ...lastSequence !== void 0 ? { lastSequence } : {},
118
+ ...changeset.unstagedPrunedThrough !== void 0 ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough } : {},
119
+ ...changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}
97
120
  };
98
- await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(changeset.changes), "utf-8");
121
+ await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(numbered), "utf-8");
99
122
  }
100
123
  /**
101
124
  * List all staged changesets
@@ -154,11 +177,11 @@ Run 'dr changeset list' to see all changesets.`);
154
177
  const changeset = await this.load(id);
155
178
  if (!changeset)
156
179
  throw new Error(`Changeset '${id}' not found`);
157
- const sequenceNumber = changeset.changes.length, stagedChange = {
180
+ const sequenceNumber = nextSequenceNumber(changeset), stagedChange = {
158
181
  ...change,
159
182
  sequenceNumber
160
183
  };
161
- return changeset.changes.push(stagedChange), changeset.updateModified(), await this.save(changeset), sequenceNumber;
184
+ return changeset.changes = withSequenceNumbers(changeset.changes), changeset.changes.push(stagedChange), changeset.lastSequence = sequenceNumber, changeset.updateModified(), await this.save(changeset), sequenceNumber;
162
185
  });
163
186
  } catch (error) {
164
187
  const errorMessage = getErrorMessage(error);
@@ -177,6 +200,59 @@ To resolve:
177
200
  4. Retry the operation`);
178
201
  }
179
202
  throw new Error(`Failed to add change to changeset '${id}': ${errorMessage}
203
+ Changeset path: ${changesetPath}`);
204
+ }
205
+ }
206
+ /**
207
+ * Add several changes, and rewrite some staged ones in place, in one write: one load and one save
208
+ * instead of one per change. Additions take the next sequence numbers in order; a rewrite keeps
209
+ * its change's number and leaves a tombstone, as {@link updateChange} does.
210
+ *
211
+ * @returns The sequence numbers assigned to the additions, in order
212
+ */
213
+ async addChanges(id, changes, rewrites = []) {
214
+ if (changes.length === 0 && rewrites.length === 0)
215
+ return [];
216
+ const changesetPath = this.getChangesetPath(id), lock = new FileLock(changesetPath);
217
+ try {
218
+ return await lock.withLock(async () => {
219
+ const changeset = await this.load(id);
220
+ if (!changeset)
221
+ throw new Error(`Changeset '${id}' not found`);
222
+ changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));
223
+ for (const { elementId, change } of rewrites) {
224
+ const index = changeset.changes.findIndex((c) => c.elementId === elementId);
225
+ if (index === -1)
226
+ throw new Error(`No change for element '${elementId}' in changeset '${id}'`);
227
+ const existing = changeset.changes[index], rewrittenAt = nextSequenceNumber(changeset);
228
+ changeset.lastSequence = rewrittenAt, addTombstones(changeset, [
229
+ { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: !0 }
230
+ ]), changeset.changes[index] = { ...existing, ...change, sequenceNumber: existing.sequenceNumber };
231
+ }
232
+ const numbers = [];
233
+ for (const change of changes) {
234
+ const sequenceNumber = nextSequenceNumber(changeset), staged = { ...change, sequenceNumber };
235
+ changeset.changes.push(staged), changeset.lastSequence = sequenceNumber, numbers.push(sequenceNumber);
236
+ }
237
+ return changeset.updateModified(), await this.save(changeset), numbers;
238
+ });
239
+ } catch (error) {
240
+ const errorMessage = getErrorMessage(error);
241
+ if (errorMessage.includes("lock") || errorMessage.includes("timeout")) {
242
+ const lockFilePath = path.join(changesetPath, ".lock");
243
+ throw new Error(`Failed to add changes to changeset '${id}': Lock acquisition failed.
244
+ Error: ${errorMessage}
245
+
246
+ This usually means another dr process is modifying this changeset.
247
+ If no other processes are running, a stale lock file may exist.
248
+
249
+ To resolve:
250
+ 1. Wait for other operations to complete
251
+ 2. Check for running dr processes: ps aux | grep dr
252
+ 3. If no processes found, remove stale lock: rm "${lockFilePath}"
253
+ 4. Retry the operation`);
254
+ }
255
+ throw new Error(`Failed to add changes to changeset '${id}': ${errorMessage}
180
256
  Changeset path: ${changesetPath}`);
181
257
  }
182
258
  }
@@ -188,17 +264,33 @@ Changeset path: ${changesetPath}`);
188
264
  * @throws {Error} If lock acquisition, changeset load, save, or lock release fails
189
265
  */
190
266
  async removeChange(id, elementId) {
191
- const changesetPath = this.getChangesetPath(id), lock = new FileLock(changesetPath);
267
+ await this.removeChanges(id, [elementId]);
268
+ }
269
+ /**
270
+ * Remove every change to any of `elementIds` in one write. The removal is one revision: it takes
271
+ * one sequence number, and each removed change is kept as a tombstone.
272
+ *
273
+ * Uses file locking to prevent concurrent modification race conditions
274
+ *
275
+ * @throws {Error} If lock acquisition, changeset load, save, or lock release fails
276
+ */
277
+ async removeChanges(id, elementIds) {
278
+ const keys = new Set(elementIds), changesetPath = this.getChangesetPath(id), lock = new FileLock(changesetPath);
192
279
  try {
193
280
  await lock.withLock(async () => {
194
281
  const changeset = await this.load(id);
195
282
  if (!changeset)
196
283
  throw new Error(`Changeset '${id}' not found`);
197
- const filtered = changeset.changes.filter((c) => c.elementId !== elementId);
198
- changeset.changes = filtered.map((c, idx) => ({
199
- ...c,
200
- sequenceNumber: idx
201
- })), changeset.updateModified(), await this.save(changeset);
284
+ const numbered = withSequenceNumbers(changeset.changes), removed = numbered.filter((c) => keys.has(c.elementId));
285
+ if (changeset.changes = numbered.filter((c) => !keys.has(c.elementId)), removed.length > 0) {
286
+ const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });
287
+ changeset.lastSequence = removedAt, addTombstones(changeset, removed.map((c, index) => ({
288
+ sequence: sequenceOf(c, index),
289
+ removedAt,
290
+ change: compactChange(c)
291
+ })));
292
+ }
293
+ changeset.updateModified(), await this.save(changeset);
202
294
  });
203
295
  } catch (error) {
204
296
  const errorMessage = getErrorMessage(error);
@@ -239,11 +331,14 @@ Changeset path: ${changesetPath}`);
239
331
  const index = changeset.changes.findIndex((c) => c.elementId === elementId);
240
332
  if (index === -1)
241
333
  throw new Error(`No change for element '${elementId}' in changeset '${id}'`);
242
- const existing = changeset.changes[index];
243
- changeset.changes[index] = {
334
+ changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));
335
+ const existing = changeset.changes[index], rewrittenAt = nextSequenceNumber(changeset);
336
+ changeset.lastSequence = rewrittenAt, addTombstones(changeset, [
337
+ { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: !0 }
338
+ ]), changeset.changes[index] = {
244
339
  ...existing,
245
340
  ...updated,
246
- sequenceNumber: existing.sequenceNumber ?? index
341
+ sequenceNumber: existing.sequenceNumber
247
342
  }, changeset.updateModified(), await this.save(changeset);
248
343
  });
249
344
  } catch (error) {
@@ -252,9 +347,40 @@ Changeset path: ${changesetPath}`);
252
347
  Changeset path: ${changesetPath}`);
253
348
  }
254
349
  }
350
+ /**
351
+ * Change a changeset's name and/or description in place, under the changeset's file lock so a
352
+ * concurrent stage cannot be lost. Staged changes are untouched.
353
+ *
354
+ * @returns The updated changeset
355
+ * @throws {Error} If the changeset is not found, or on lock or I/O failure
356
+ */
357
+ async updateMetadata(id, patch) {
358
+ return new FileLock(this.getChangesetPath(id)).withLock(async () => {
359
+ const changeset = await this.load(id);
360
+ if (!changeset)
361
+ throw new Error(`Changeset '${id}' not found`);
362
+ return patch.name !== void 0 && (changeset.name = patch.name), patch.description !== void 0 && (changeset.description = patch.description || void 0), changeset.updateModified(), await this.save(changeset), changeset;
363
+ });
364
+ }
255
365
  /**
256
366
  * Get the storage path for a changeset
257
367
  */
368
+ /**
369
+ * Remove every file in the changeset's directory except metadata.yaml and changes.yaml (and
370
+ * dotfiles such as a lock): e.g. the base index, once the changeset is discarded and nothing
371
+ * reads it again.
372
+ *
373
+ * @returns The names of the files removed
374
+ */
375
+ async removeSideFiles(id) {
376
+ const changesetPath = this.getChangesetPath(id);
377
+ if (!await fileExists(changesetPath))
378
+ return [];
379
+ const keep = /* @__PURE__ */ new Set(["metadata.yaml", "changes.yaml"]), removed = [];
380
+ for (const entry of await readdir(changesetPath, { withFileTypes: !0 }))
381
+ keep.has(entry.name) || entry.name.startsWith(".") || !entry.isFile() || (await rm(path.join(changesetPath, entry.name), { force: !0 }), removed.push(entry.name));
382
+ return removed;
383
+ }
258
384
  getChangesetPath(id) {
259
385
  const sanitizedId = this.sanitizeId(id);
260
386
  return path.join(this.changesetsDir, sanitizedId);
@@ -277,6 +403,7 @@ Changeset path: ${changesetPath}`);
277
403
  }
278
404
  }
279
405
  export {
406
+ MAX_UNSTAGED_TOMBSTONES,
280
407
  StagedChangesetStorage
281
408
  };
282
409
  //# sourceMappingURL=staged-changeset-storage.js.map