@documentation-robotics/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +36 -2
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/call-graph-utils.js +2 -2
  5. package/dist/analyzers/call-graph-utils.js.map +2 -2
  6. package/dist/analyzers/cards/build.js +171 -0
  7. package/dist/analyzers/cards/build.js.map +7 -0
  8. package/dist/analyzers/cards/classify.js +42 -0
  9. package/dist/analyzers/cards/classify.js.map +7 -0
  10. package/dist/analyzers/cards/edge-index.js +44 -0
  11. package/dist/analyzers/cards/edge-index.js.map +7 -0
  12. package/dist/analyzers/cards/facts.js +46 -0
  13. package/dist/analyzers/cards/facts.js.map +7 -0
  14. package/dist/analyzers/cards/graph.js +42 -0
  15. package/dist/analyzers/cards/graph.js.map +7 -0
  16. package/dist/analyzers/cards/index.js +8 -0
  17. package/dist/analyzers/cards/index.js.map +7 -0
  18. package/dist/analyzers/cards/tier0.js +18 -0
  19. package/dist/analyzers/cards/tier0.js.map +7 -0
  20. package/dist/analyzers/cards/types.js +1 -0
  21. package/dist/analyzers/cards/types.js.map +7 -0
  22. package/dist/analyzers/cbm-analyzer.js +195 -171
  23. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  24. package/dist/analyzers/cbm-responses.js +336 -0
  25. package/dist/analyzers/cbm-responses.js.map +7 -0
  26. package/dist/analyzers/edge-relationships.js +45 -0
  27. package/dist/analyzers/edge-relationships.js.map +7 -0
  28. package/dist/analyzers/mapping-loader.js +9 -0
  29. package/dist/analyzers/mapping-loader.js.map +2 -2
  30. package/dist/analyzers/tier0-rules.js +153 -0
  31. package/dist/analyzers/tier0-rules.js.map +7 -0
  32. package/dist/analyzers/types.js.map +1 -1
  33. package/dist/audit/relationships/ai/prompts.js +10 -10
  34. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  35. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  36. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  37. package/dist/cli.js +12 -2
  38. package/dist/cli.js.map +2 -2
  39. package/dist/coding-agents/default-permissions.js +31 -1
  40. package/dist/coding-agents/default-permissions.js.map +2 -2
  41. package/dist/coding-agents/drbot-mcp.js +167 -0
  42. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  43. package/dist/commands/add.js +3 -2
  44. package/dist/commands/add.js.map +2 -2
  45. package/dist/commands/analyzer.js +117 -14
  46. package/dist/commands/analyzer.js.map +2 -2
  47. package/dist/commands/changeset-verify.js +55 -0
  48. package/dist/commands/changeset-verify.js.map +7 -0
  49. package/dist/commands/changeset.js +43 -22
  50. package/dist/commands/changeset.js.map +2 -2
  51. package/dist/commands/delete.js +10 -4
  52. package/dist/commands/delete.js.map +2 -2
  53. package/dist/commands/farm.js +176 -60
  54. package/dist/commands/farm.js.map +2 -2
  55. package/dist/commands/mcp.js +19 -4
  56. package/dist/commands/mcp.js.map +2 -2
  57. package/dist/commands/relationship.js +207 -26
  58. package/dist/commands/relationship.js.map +2 -2
  59. package/dist/commands/update.js +42 -23
  60. package/dist/commands/update.js.map +2 -2
  61. package/dist/commands/upgrade.js +99 -11
  62. package/dist/commands/upgrade.js.map +3 -3
  63. package/dist/commands/version.js +1 -1
  64. package/dist/commands/visualize.js +4 -3
  65. package/dist/commands/visualize.js.map +2 -2
  66. package/dist/core/catalog-migration.js +33 -0
  67. package/dist/core/catalog-migration.js.map +7 -0
  68. package/dist/core/changeset-analysis.js +421 -0
  69. package/dist/core/changeset-analysis.js.map +7 -0
  70. package/dist/core/changeset-base-index.js +153 -0
  71. package/dist/core/changeset-base-index.js.map +7 -0
  72. package/dist/core/changeset-brief-delta.js +102 -0
  73. package/dist/core/changeset-brief-delta.js.map +7 -0
  74. package/dist/core/changeset-graph.js +281 -0
  75. package/dist/core/changeset-graph.js.map +7 -0
  76. package/dist/core/changeset-only-guard.js +32 -0
  77. package/dist/core/changeset-only-guard.js.map +7 -0
  78. package/dist/core/changeset-summary.js +68 -0
  79. package/dist/core/changeset-summary.js.map +7 -0
  80. package/dist/core/changeset-validator.js +66 -20
  81. package/dist/core/changeset-validator.js.map +2 -2
  82. package/dist/core/changeset-verify.js +518 -0
  83. package/dist/core/changeset-verify.js.map +7 -0
  84. package/dist/core/changeset.js +56 -9
  85. package/dist/core/changeset.js.map +2 -2
  86. package/dist/core/element-type-migration.js +145 -0
  87. package/dist/core/element-type-migration.js.map +7 -0
  88. package/dist/core/farm-manifest.js +12 -6
  89. package/dist/core/farm-manifest.js.map +2 -2
  90. package/dist/core/farm-sync-engine.js +1 -1
  91. package/dist/core/farm-sync-engine.js.map +2 -2
  92. package/dist/core/farm-sync-state.js +10 -2
  93. package/dist/core/farm-sync-state.js.map +2 -2
  94. package/dist/core/migration-registry.js +75 -46
  95. package/dist/core/migration-registry.js.map +2 -2
  96. package/dist/core/model-upgrade.js +102 -0
  97. package/dist/core/model-upgrade.js.map +7 -0
  98. package/dist/core/model.js +82 -30
  99. package/dist/core/model.js.map +2 -2
  100. package/dist/core/mutation-handler.js +71 -6
  101. package/dist/core/mutation-handler.js.map +2 -2
  102. package/dist/core/relationship-migration-table.js +6639 -0
  103. package/dist/core/relationship-migration-table.js.map +7 -0
  104. package/dist/core/relationship-migration.js +272 -0
  105. package/dist/core/relationship-migration.js.map +7 -0
  106. package/dist/core/relationship-prune.js +49 -0
  107. package/dist/core/relationship-prune.js.map +7 -0
  108. package/dist/core/relationship-staging.js +68 -0
  109. package/dist/core/relationship-staging.js.map +7 -0
  110. package/dist/core/relationships.js +26 -4
  111. package/dist/core/relationships.js.map +2 -2
  112. package/dist/core/report-data-model.js +7 -13
  113. package/dist/core/report-data-model.js.map +2 -2
  114. package/dist/core/staged-changeset-storage.js +64 -17
  115. package/dist/core/staged-changeset-storage.js.map +2 -2
  116. package/dist/core/staged-source-reference.js +28 -0
  117. package/dist/core/staged-source-reference.js.map +7 -0
  118. package/dist/core/staging-area.js +83 -18
  119. package/dist/core/staging-area.js.map +2 -2
  120. package/dist/core/virtual-projection.js +13 -5
  121. package/dist/core/virtual-projection.js.map +2 -2
  122. package/dist/export/archimate-exporter.js +1 -0
  123. package/dist/export/archimate-exporter.js.map +2 -2
  124. package/dist/generated/compiled-validators.js +2 -2
  125. package/dist/generated/compiled-validators.js.map +2 -2
  126. package/dist/generated/layer-registry.js +16 -16
  127. package/dist/generated/layer-registry.js.map +2 -2
  128. package/dist/generated/node-types.js +186 -168
  129. package/dist/generated/node-types.js.map +2 -2
  130. package/dist/generated/relationship-index.js +2236 -7790
  131. package/dist/generated/relationship-index.js.map +2 -2
  132. package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
  133. package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
  134. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  135. package/dist/integrations/claude_code/commands/dr-map.md +13 -0
  136. package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
  137. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  138. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  139. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  140. package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
  141. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  142. package/dist/integrations/claude_code/docs/README.md +48 -1
  143. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  144. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  145. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  146. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  147. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  148. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  149. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  150. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  151. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  152. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  153. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  154. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  155. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
  156. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
  157. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
  158. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
  159. package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
  160. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  161. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  162. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  163. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  164. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  165. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  166. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  167. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  168. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  169. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  170. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  171. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  172. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
  173. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
  174. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  175. package/dist/integrations/hash-utils.js.map +2 -2
  176. package/dist/mcp/api-key-manager.js +14 -1
  177. package/dist/mcp/api-key-manager.js.map +2 -2
  178. package/dist/mcp/changeset-only.js +34 -0
  179. package/dist/mcp/changeset-only.js.map +7 -0
  180. package/dist/mcp/tool-registry.js +19 -3
  181. package/dist/mcp/tool-registry.js.map +2 -2
  182. package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
  183. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  184. package/dist/mcp/tools/changeset-editing.js +184 -0
  185. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  186. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  187. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  188. package/dist/mcp/tools/changeset-shared.js +31 -0
  189. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  190. package/dist/mcp/tools/changeset-show.js +17 -1
  191. package/dist/mcp/tools/changeset-show.js.map +2 -2
  192. package/dist/mcp/tools/changeset-verify.js +43 -0
  193. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  194. package/dist/mcp/tools/index.js +19 -1
  195. package/dist/mcp/tools/index.js.map +2 -2
  196. package/dist/mcp/tools/model-add.js +6 -5
  197. package/dist/mcp/tools/model-add.js.map +2 -2
  198. package/dist/mcp/tools/model-delete.js +2 -2
  199. package/dist/mcp/tools/model-delete.js.map +2 -2
  200. package/dist/mcp/tools/model-search.js +14 -0
  201. package/dist/mcp/tools/model-search.js.map +2 -2
  202. package/dist/mcp/tools/model-show.js +29 -2
  203. package/dist/mcp/tools/model-show.js.map +2 -2
  204. package/dist/mcp/tools/model-trace.js +93 -30
  205. package/dist/mcp/tools/model-trace.js.map +2 -2
  206. package/dist/mcp/tools/model-update.js +35 -19
  207. package/dist/mcp/tools/model-update.js.map +2 -2
  208. package/dist/mcp/tools/relationship-tools.js +98 -0
  209. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  210. package/dist/mcp/tools/shared.js +25 -2
  211. package/dist/mcp/tools/shared.js.map +2 -2
  212. package/dist/mcp/tools/spec-type.js +90 -0
  213. package/dist/mcp/tools/spec-type.js.map +7 -0
  214. package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
  215. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  216. package/dist/schemas/bundled/api.json +1029 -1658
  217. package/dist/schemas/bundled/apm.json +499 -734
  218. package/dist/schemas/bundled/application.json +735 -371
  219. package/dist/schemas/bundled/base.json +3 -220
  220. package/dist/schemas/bundled/business.json +452 -366
  221. package/dist/schemas/bundled/data-model.json +395 -411
  222. package/dist/schemas/bundled/data-store.json +334 -558
  223. package/dist/schemas/bundled/manifest.json +18 -18
  224. package/dist/schemas/bundled/motivation.json +349 -368
  225. package/dist/schemas/bundled/navigation.json +415 -731
  226. package/dist/schemas/bundled/product.json +325 -159
  227. package/dist/schemas/bundled/security.json +864 -1232
  228. package/dist/schemas/bundled/technology.json +574 -703
  229. package/dist/schemas/bundled/testing.json +718 -1643
  230. package/dist/schemas/bundled/ux.json +739 -1352
  231. package/dist/server/schemas.js +8 -0
  232. package/dist/server/schemas.js.map +2 -2
  233. package/dist/server/server.js +218 -34
  234. package/dist/server/server.js.map +2 -2
  235. package/dist/server-entry.js +2 -1
  236. package/dist/server-entry.js.map +2 -2
  237. package/dist/telemetry/index.js +1 -1
  238. package/dist/utils/ansi-suppressor.js +6 -8
  239. package/dist/utils/ansi-suppressor.js.map +2 -2
  240. package/dist/utils/attribute-patch.js +41 -0
  241. package/dist/utils/attribute-patch.js.map +7 -0
  242. package/dist/utils/error-hints.js +29 -0
  243. package/dist/utils/error-hints.js.map +7 -0
  244. package/dist/utils/errors.js +9 -3
  245. package/dist/utils/errors.js.map +2 -2
  246. package/dist/utils/project-paths.js +26 -1
  247. package/dist/utils/project-paths.js.map +2 -2
  248. package/dist/utils/source-reference.js +14 -12
  249. package/dist/utils/source-reference.js.map +2 -2
  250. package/dist/utils/spec-version.js +2 -2
  251. package/dist/utils/spec-version.js.map +1 -1
  252. package/dist/validators/composed-reference-validator.js +5 -27
  253. package/dist/validators/composed-reference-validator.js.map +2 -2
  254. package/dist/validators/reference-validator.js +0 -7
  255. package/dist/validators/reference-validator.js.map +2 -2
  256. package/dist/validators/relationship-schema-validator.js +3 -3
  257. package/dist/validators/relationship-schema-validator.js.map +2 -2
  258. package/dist/validators/schema-validator.js +27 -9
  259. package/dist/validators/schema-validator.js.map +2 -2
  260. package/dist/validators/types.js.map +1 -1
  261. package/dist/viewer/README.md +6 -6
  262. package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
  263. package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
  264. package/dist/viewer/index.html +2 -2
  265. package/dist/viewer/manifest.json +10 -10
  266. package/dist/viewer/version.json +1 -1
  267. package/package.json +2 -1
  268. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  269. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -0,0 +1,272 @@
1
+ import { readdir } from "fs/promises";
2
+ import { nextSequenceNumber } from "./changeset.js";
3
+ import { CANONICAL_LAYER_NAMES } from "./layers.js";
4
+ import { StagedChangesetStorage } from "./staged-changeset-storage.js";
5
+ import { FileLock } from "../utils/file-lock.js";
6
+ import { getErrorMessage } from "../utils/errors.js";
7
+ import { normalizeNodeType } from "../generated/node-types.js";
8
+ import { RELATIONSHIP_REWRITE_TABLE } from "./relationship-migration-table.js";
9
+ const MAX_RULES_PER_RELATIONSHIP = 4;
10
+ function predicateOf(triple) {
11
+ const parts = triple.split(".");
12
+ if (parts.length !== 5)
13
+ throw new Error(`Malformed relationship triple '${triple}' in the rewrite table`);
14
+ return parts[2];
15
+ }
16
+ function swapEnds(rel, predicate) {
17
+ const sourceLayer = rel.targetLayer ?? rel.layer, targetLayer = rel.layer, swapped = {
18
+ ...rel,
19
+ source: rel.target,
20
+ target: rel.source,
21
+ predicate,
22
+ layer: sourceLayer
23
+ };
24
+ return targetLayer && targetLayer !== sourceLayer ? swapped.targetLayer = targetLayer : delete swapped.targetLayer, swapped;
25
+ }
26
+ function rewriteOne(rel, resolve, table) {
27
+ let current = rel;
28
+ const rules = [];
29
+ for (let i = 0; i < MAX_RULES_PER_RELATIONSHIP; i++) {
30
+ const source = resolve(current.source), target = resolve(current.target), key = source && target ? `${source.specNodeId}.${current.predicate}.${target.specNodeId}` : void 0, rule = key ? table.triples[key] : void 0;
31
+ if (key && rule) {
32
+ if (rule.to === null)
33
+ return rules.push(`${key} -> (removed)`), { rel: null, rules };
34
+ rules.push(`${key} -> ${rule.to}`);
35
+ const predicate = predicateOf(rule.to);
36
+ current = rule.swap ? swapEnds(current, predicate) : { ...current, predicate };
37
+ continue;
38
+ }
39
+ const renamed = table.predicateRenames[current.predicate];
40
+ if (renamed) {
41
+ rules.push(`${current.predicate} -> ${renamed}`), current = { ...current, predicate: renamed };
42
+ continue;
43
+ }
44
+ break;
45
+ }
46
+ return rules.length > 0 ? { rel: current, rules } : null;
47
+ }
48
+ function identityOf(resolve) {
49
+ return (rel) => [
50
+ resolve(rel.source)?.canonicalId ?? rel.source,
51
+ rel.predicate,
52
+ resolve(rel.target)?.canonicalId ?? rel.target
53
+ ].join("\0");
54
+ }
55
+ function tripleOf(rel) {
56
+ return { source: rel.source, predicate: rel.predicate, target: rel.target };
57
+ }
58
+ function rewriteRelationships(relationships, resolve, table = RELATIONSHIP_REWRITE_TABLE) {
59
+ const identity = identityOf(resolve), triple = tripleOf, outcomes = relationships.map((rel) => ({ rel, outcome: rewriteOne(rel, resolve, table) })), present = new Set(outcomes.filter((o) => o.outcome === null).map((o) => identity(o.rel))), result = [], changes = [];
60
+ for (const { rel, outcome } of outcomes) {
61
+ if (outcome === null) {
62
+ result.push(rel);
63
+ continue;
64
+ }
65
+ if (outcome.rel === null) {
66
+ changes.push({ action: "dropped", before: triple(rel), rules: outcome.rules });
67
+ continue;
68
+ }
69
+ const key = identity(outcome.rel);
70
+ if (present.has(key)) {
71
+ changes.push({
72
+ action: "dropped-duplicate",
73
+ before: triple(rel),
74
+ after: triple(outcome.rel),
75
+ rules: outcome.rules
76
+ });
77
+ continue;
78
+ }
79
+ present.add(key), result.push(outcome.rel), changes.push({
80
+ action: "rewritten",
81
+ before: triple(rel),
82
+ after: triple(outcome.rel),
83
+ rules: outcome.rules
84
+ });
85
+ }
86
+ return { relationships: result, changes };
87
+ }
88
+ function toResolved(element, layerName) {
89
+ const text = (value) => typeof value == "string" && value ? value : void 0, id = text(element.id), layerId = text(element.layer_id) ?? layerName, type = text(element.type), stated = text(element.spec_node_id) ?? (type ? `${layerId}.${normalizeNodeType(layerId, type)}` : void 0), specNodeId = stated ? RELATIONSHIP_REWRITE_TABLE.typeRenames?.[stated] ?? stated : void 0;
90
+ return id && specNodeId ? { specNodeId, canonicalId: id } : void 0;
91
+ }
92
+ async function buildResolver(model) {
93
+ for (const layerName of CANONICAL_LAYER_NAMES)
94
+ model.layers.has(layerName) || await model.loadLayer(layerName);
95
+ const index = /* @__PURE__ */ new Map();
96
+ for (const layer of model.layers.values())
97
+ for (const element of layer.listElements()) {
98
+ const resolved = toResolved(element, layer.name);
99
+ index.set(element.path, resolved), index.set(element.id, resolved);
100
+ }
101
+ return (elementId) => {
102
+ if (index.has(elementId))
103
+ return index.get(elementId);
104
+ let resolved;
105
+ for (const layer of model.layers.values()) {
106
+ const element = layer.getElement(elementId);
107
+ if (element) {
108
+ resolved = toResolved(element, layer.name);
109
+ break;
110
+ }
111
+ }
112
+ return index.set(elementId, resolved), resolved;
113
+ };
114
+ }
115
+ async function migrateRelationshipsFile(model, table = RELATIONSHIP_REWRITE_TABLE, options = {}) {
116
+ const fs = await import("fs/promises"), YAML = await import("yaml"), relationshipsPath = `${model.rootPath}/documentation-robotics/model/relationships.yaml`;
117
+ let content;
118
+ try {
119
+ content = await fs.readFile(relationshipsPath, "utf-8");
120
+ } catch (error) {
121
+ if (error.code === "ENOENT")
122
+ return { filesModified: 0, changes: [], committed: { before: [], after: [] } };
123
+ throw error;
124
+ }
125
+ const data = YAML.parse(content) ?? [];
126
+ if (!Array.isArray(data))
127
+ return {
128
+ filesModified: 0,
129
+ changes: [],
130
+ committed: { before: [], after: [] },
131
+ error: `relationships.yaml is not a list of relationships (found ${typeof data}); fix the file and run the upgrade again`
132
+ };
133
+ const resolve = await buildResolver(model), { relationships, changes } = rewriteRelationships(data, resolve, table), committed = { before: data, after: relationships };
134
+ if (changes.length === 0 || options.dryRun)
135
+ return { filesModified: 0, changes, committed };
136
+ const header = `# Intra-layer relationships
137
+ # Format: source_id -> predicate -> target_id
138
+ # Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}
139
+
140
+ `;
141
+ return await fs.writeFile(relationshipsPath, header + YAML.stringify(relationships), "utf-8"), { filesModified: 1, changes, committed };
142
+ }
143
+ function isRelationship(value) {
144
+ const rel = value;
145
+ return typeof rel == "object" && rel !== null && typeof rel.source == "string" && typeof rel.predicate == "string" && typeof rel.target == "string";
146
+ }
147
+ function withRelationship(change, field, rel) {
148
+ return {
149
+ ...change,
150
+ elementId: `${rel.source}::${rel.predicate}::${rel.target}`,
151
+ layerName: rel.layer || change.layerName,
152
+ [field]: rel
153
+ };
154
+ }
155
+ function withStagedElements(base, changes) {
156
+ const staged = /* @__PURE__ */ new Map();
157
+ for (const change of changes) {
158
+ if (change.type !== "add" || !change.after)
159
+ continue;
160
+ const resolved = toResolved(change.after, change.layerName);
161
+ if (resolved)
162
+ for (const key of [change.after.path, change.after.id, change.elementId])
163
+ typeof key == "string" && key && staged.set(key, resolved);
164
+ }
165
+ return (elementId) => base(elementId) ?? staged.get(elementId);
166
+ }
167
+ function rewriteStagedChangeset(changeset, committed, resolve, table = RELATIONSHIP_REWRITE_TABLE) {
168
+ const identity = identityOf(resolve), ordered = changeset.changes.map((change, index) => ({ change, order: change.sequenceNumber ?? index })).sort((a, b) => a.order - b.order).map(({ change, order }) => {
169
+ const value = change.type === "relationship-add" ? change.after : change.type === "relationship-delete" ? change.before : void 0, rel = isRelationship(value) ? value : void 0;
170
+ return {
171
+ change: { ...change, sequenceNumber: order },
172
+ rel,
173
+ outcome: rel ? rewriteOne(rel, resolve, table) : null
174
+ };
175
+ }), imageOf = (rel, outcome) => outcome === null ? rel : outcome.rel, endState = new Map(committed.before.map((rel) => [identity(rel), rel]));
176
+ for (const { change, rel } of ordered)
177
+ rel && change.type === "relationship-add" ? endState.set(identity(rel), rel) : rel && endState.delete(identity(rel));
178
+ const kept = /* @__PURE__ */ new Set();
179
+ for (const rel of endState.values()) {
180
+ const image = imageOf(rel, rewriteOne(rel, resolve, table));
181
+ image && kept.add(identity(image));
182
+ }
183
+ const lastAdd = /* @__PURE__ */ new Map();
184
+ ordered.forEach(({ change, rel, outcome }, position) => {
185
+ const image = rel && change.type === "relationship-add" ? imageOf(rel, outcome) : null;
186
+ image && lastAdd.set(identity(image), position);
187
+ });
188
+ const inModel = new Set(committed.after.map(identity)), added = new Set(ordered.filter(({ change, rel, outcome }) => rel && change.type === "relationship-add" && outcome === null).map(({ rel }) => identity(rel))), deleted = /* @__PURE__ */ new Set(), changes = [], report = [], record = (stagedAs, entry, duplicateOf) => report.push({ ...entry, changeset: changeset.id, stagedAs, ...duplicateOf && { duplicateOf } });
189
+ return ordered.forEach(({ change, rel, outcome }, position) => {
190
+ if (!rel) {
191
+ changes.push(change);
192
+ return;
193
+ }
194
+ const stagedAs = change.type;
195
+ if (outcome !== null && outcome.rel === null) {
196
+ record(stagedAs, { action: "dropped", before: tripleOf(rel), rules: outcome.rules });
197
+ return;
198
+ }
199
+ const image = outcome?.rel ?? rel, key = identity(image), entry = {
200
+ action: "dropped-duplicate",
201
+ before: tripleOf(rel),
202
+ after: tripleOf(image),
203
+ rules: outcome?.rules ?? []
204
+ };
205
+ if (stagedAs === "relationship-add") {
206
+ if (outcome !== null && inModel.has(key)) {
207
+ record(stagedAs, entry, "model");
208
+ return;
209
+ }
210
+ if (outcome !== null && added.has(key)) {
211
+ record(stagedAs, entry, "changeset");
212
+ return;
213
+ }
214
+ added.add(key), deleted.delete(key);
215
+ } else {
216
+ if (kept.has(key) && (lastAdd.get(key) ?? -1) < position) {
217
+ record(stagedAs, entry, "kept");
218
+ return;
219
+ }
220
+ if (outcome !== null && deleted.has(key)) {
221
+ record(stagedAs, entry, "changeset");
222
+ return;
223
+ }
224
+ deleted.add(key), inModel.delete(key), added.delete(key);
225
+ }
226
+ outcome === null ? changes.push(change) : (changes.push(withRelationship(change, stagedAs === "relationship-add" ? "after" : "before", image)), record(stagedAs, { ...entry, action: "rewritten" }));
227
+ }), { changes, report };
228
+ }
229
+ async function migrateStagedChangesets(model, committed, table = RELATIONSHIP_REWRITE_TABLE, options = {}) {
230
+ const storage = new StagedChangesetStorage(model.rootPath), result = { changesetsModified: 0, changes: [], errors: [] };
231
+ let entries;
232
+ try {
233
+ entries = (await readdir(storage.getChangesetsDir(), { withFileTypes: !0 })).filter((entry) => entry.isDirectory() && !entry.name.endsWith(".lock")).map((entry) => entry.name).sort();
234
+ } catch (error) {
235
+ if (error.code === "ENOENT")
236
+ return result;
237
+ throw error;
238
+ }
239
+ const base = await buildResolver(model);
240
+ for (const id of entries)
241
+ try {
242
+ await new FileLock(storage.getChangesetPath(id)).withLock(async () => {
243
+ const changeset = await storage.load(id);
244
+ if (!changeset || changeset.status !== "staged")
245
+ return;
246
+ const resolve = withStagedElements(base, changeset.changes), { changes, report } = rewriteStagedChangeset(changeset, committed, resolve, table);
247
+ if (report.length === 0)
248
+ return;
249
+ if (options.dryRun) {
250
+ result.changes.push(...report);
251
+ return;
252
+ }
253
+ const lastSequence = nextSequenceNumber(changeset) - 1;
254
+ changeset.changes = changes, lastSequence >= 0 && (changeset.lastSequence = lastSequence), changeset.updateModified(), await storage.save(changeset), result.changesetsModified++, result.changes.push(...report);
255
+ });
256
+ } catch (error) {
257
+ result.errors.push(`Failed to migrate staged changeset '${id}': ${getErrorMessage(error)}; fix or delete the changeset and run the upgrade again`);
258
+ }
259
+ return result;
260
+ }
261
+ function summarizeRelationshipChanges(changes, label = "relationships") {
262
+ const count = (action) => changes.filter((c) => c.action === action).length, parts = [`${count("rewritten")} rewritten`, `${count("dropped-duplicate")} dropped as duplicates`], dropped = count("dropped");
263
+ return dropped > 0 && parts.push(`${dropped} dropped (no replacement)`), `${label}: ${parts.join(", ")}`;
264
+ }
265
+ export {
266
+ migrateRelationshipsFile,
267
+ migrateStagedChangesets,
268
+ rewriteRelationships,
269
+ rewriteStagedChangeset,
270
+ summarizeRelationshipChanges
271
+ };
272
+ //# sourceMappingURL=relationship-migration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/relationship-migration.ts"],
4
+ "sourcesContent": ["/**\n * Relationship catalog migration\n *\n * When the spec removes, renames or reverses relationship schemas, relationships that a model\n * recorded against the old catalog stop validating. RELATIONSHIP_REWRITE_TABLE maps every retired\n * triple ({sourceLayer}.{sourceType}.{predicate}.{destLayer}.{destType}) to the triple that now\n * expresses the same meaning, and every retired predicate to its replacement. The table is\n * generated from the applied-*.json change records by cli/scripts/generate-relationship-migration.ts;\n * this module applies it to a model's relationships.yaml and to the relationship changes held in\n * its staged changesets.\n */\n\nimport { readdir } from \"fs/promises\";\nimport type { Model } from \"./model.js\";\nimport type { Relationship } from \"./relationships.js\";\nimport type { Change, StagedChange } from \"./changeset.js\";\nimport { nextSequenceNumber } from \"./changeset.js\";\nimport { CANONICAL_LAYER_NAMES } from \"./layers.js\";\nimport { StagedChangesetStorage } from \"./staged-changeset-storage.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { normalizeNodeType } from \"../generated/node-types.js\";\nimport { RELATIONSHIP_REWRITE_TABLE } from \"./relationship-migration-table.js\";\n\nexport interface RelationshipRewriteRule {\n /** Replacement triple, or null when the relationship has no replacement and is dropped */\n to: string | null;\n /** True when the replacement runs the other way, so the relationship's source and target swap */\n swap: boolean;\n /** Change records the rule was composed from, oldest first */\n records: string[];\n}\n\nexport interface RelationshipRewriteTable {\n /** Change record files the table was generated from */\n sources: string[];\n /** Retired predicate -> replacement predicate, applied to any pair */\n predicateRenames: Record<string, string>;\n /** Retired triple -> replacement */\n triples: Record<string, RelationshipRewriteRule>;\n /**\n * Retired element type -> replacement type (\"product.capability\" -> \"product.softwarecapability\").\n * The upgrade renames elements of the old type before it rewrites relationships, so the triple\n * rules above are keyed by the new type names.\n */\n typeRenames?: Record<string, string>;\n}\n\n/** What a model element id resolves to, for rule lookup and duplicate detection */\nexport interface ResolvedElement {\n /** Spec node type, e.g. \"application.applicationservice\" */\n specNodeId: string;\n /** One id per element, so a path and a UUID naming the same element compare equal */\n canonicalId: string;\n}\n\nexport type ElementResolver = (elementId: string) => ResolvedElement | undefined;\n\nexport interface RelationshipRewriteChange {\n action: \"rewritten\" | \"dropped-duplicate\" | \"dropped\";\n before: { source: string; predicate: string; target: string };\n after?: { source: string; predicate: string; target: string };\n /** The rules applied, e.g. \"a.x.implements.b.y -> a.x.realizes.b.y\" */\n rules: string[];\n}\n\nexport interface RelationshipRewriteResult {\n relationships: Relationship[];\n changes: RelationshipRewriteChange[];\n}\n\n/** Rules are composed when the table is generated, so a chain is at most a predicate rename plus one triple rule */\nconst MAX_RULES_PER_RELATIONSHIP = 4;\n\nfunction predicateOf(triple: string): string {\n const parts = triple.split(\".\");\n if (parts.length !== 5) {\n throw new Error(`Malformed relationship triple '${triple}' in the rewrite table`);\n }\n return parts[2];\n}\n\nfunction swapEnds(rel: Relationship, predicate: string): Relationship {\n const sourceLayer = rel.targetLayer ?? rel.layer;\n const targetLayer = rel.layer;\n const swapped: Relationship = {\n ...rel,\n source: rel.target,\n target: rel.source,\n predicate,\n layer: sourceLayer,\n };\n if (targetLayer && targetLayer !== sourceLayer) {\n swapped.targetLayer = targetLayer;\n } else {\n delete swapped.targetLayer;\n }\n return swapped;\n}\n\nfunction rewriteOne(\n rel: Relationship,\n resolve: ElementResolver,\n table: RelationshipRewriteTable\n): { rel: Relationship | null; rules: string[] } | null {\n let current = rel;\n const rules: string[] = [];\n\n for (let i = 0; i < MAX_RULES_PER_RELATIONSHIP; i++) {\n const source = resolve(current.source);\n const target = resolve(current.target);\n const key =\n source && target ? `${source.specNodeId}.${current.predicate}.${target.specNodeId}` : undefined;\n const rule = key ? table.triples[key] : undefined;\n\n if (key && rule) {\n if (rule.to === null) {\n rules.push(`${key} -> (removed)`);\n return { rel: null, rules };\n }\n rules.push(`${key} -> ${rule.to}`);\n const predicate = predicateOf(rule.to);\n current = rule.swap ? swapEnds(current, predicate) : { ...current, predicate };\n continue;\n }\n\n const renamed = table.predicateRenames[current.predicate];\n if (renamed) {\n rules.push(`${current.predicate} -> ${renamed}`);\n current = { ...current, predicate: renamed };\n continue;\n }\n\n break;\n }\n\n return rules.length > 0 ? { rel: current, rules } : null;\n}\n\n/** Key under which two relationships naming the same element pair and predicate compare equal */\nfunction identityOf(resolve: ElementResolver): (rel: Relationship) => string {\n return (rel) =>\n [\n resolve(rel.source)?.canonicalId ?? rel.source,\n rel.predicate,\n resolve(rel.target)?.canonicalId ?? rel.target,\n ].join(\"\\u0000\");\n}\n\nfunction tripleOf(rel: Relationship): RelationshipRewriteChange[\"before\"] {\n return { source: rel.source, predicate: rel.predicate, target: rel.target };\n}\n\n/**\n * Rewrite relationships recorded against retired catalog entries.\n *\n * A relationship whose triple has a rule takes the replacement predicate, with source and target\n * swapped when the replacement runs the other way. A relationship whose endpoints cannot be\n * resolved only gets predicate renames, which need no types. A rewritten relationship that would\n * duplicate one already in the list (same source, predicate and target) is dropped; relationships\n * the table does not touch are kept as they are, in their original order.\n */\nexport function rewriteRelationships(\n relationships: Relationship[],\n resolve: ElementResolver,\n table: RelationshipRewriteTable = RELATIONSHIP_REWRITE_TABLE\n): RelationshipRewriteResult {\n const identity = identityOf(resolve);\n const triple = tripleOf;\n\n const outcomes = relationships.map((rel) => ({ rel, outcome: rewriteOne(rel, resolve, table) }));\n\n // Untouched relationships win over rewritten ones, wherever they appear in the file\n const present = new Set(outcomes.filter((o) => o.outcome === null).map((o) => identity(o.rel)));\n\n const result: Relationship[] = [];\n const changes: RelationshipRewriteChange[] = [];\n\n for (const { rel, outcome } of outcomes) {\n if (outcome === null) {\n result.push(rel);\n continue;\n }\n if (outcome.rel === null) {\n changes.push({ action: \"dropped\", before: triple(rel), rules: outcome.rules });\n continue;\n }\n const key = identity(outcome.rel);\n if (present.has(key)) {\n changes.push({\n action: \"dropped-duplicate\",\n before: triple(rel),\n after: triple(outcome.rel),\n rules: outcome.rules,\n });\n continue;\n }\n present.add(key);\n result.push(outcome.rel);\n changes.push({\n action: \"rewritten\",\n before: triple(rel),\n after: triple(outcome.rel),\n rules: outcome.rules,\n });\n }\n\n return { relationships: result, changes };\n}\n\n/** The element fields a resolver reads; a staged add's `after` carries the same ones */\ninterface ElementData {\n id?: unknown;\n spec_node_id?: unknown;\n type?: unknown;\n layer_id?: unknown;\n}\n\n/**\n * An element's id and spec node type for rule lookup. A type the table renames resolves to its\n * new name: the rules are keyed by new type names, and an element not yet renamed (a dry run, or\n * an element staged in a changeset) is still of that type.\n */\nfunction toResolved(element: ElementData, layerName: string): ResolvedElement | undefined {\n const text = (value: unknown) => (typeof value === \"string\" && value ? value : undefined);\n const id = text(element.id);\n const layerId = text(element.layer_id) ?? layerName;\n const type = text(element.type);\n const stated =\n text(element.spec_node_id) ?? (type ? `${layerId}.${normalizeNodeType(layerId, type)}` : undefined);\n const specNodeId = stated ? (RELATIONSHIP_REWRITE_TABLE.typeRenames?.[stated] ?? stated) : undefined;\n return id && specNodeId ? { specNodeId, canonicalId: id } : undefined;\n}\n\n/**\n * Build a resolver over the model's base layers (never a changeset projection), loading any\n * layer that is not in memory yet.\n */\nasync function buildResolver(model: Model): Promise<ElementResolver> {\n for (const layerName of CANONICAL_LAYER_NAMES) {\n if (!model.layers.has(layerName)) {\n await model.loadLayer(layerName);\n }\n }\n\n // Index paths and UUIDs once; Layer.getElement scans every node for a UUID\n const index = new Map<string, ResolvedElement | undefined>();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n const resolved = toResolved(element, layer.name);\n index.set(element.path, resolved);\n index.set(element.id, resolved);\n }\n }\n\n return (elementId: string) => {\n if (index.has(elementId)) {\n return index.get(elementId);\n }\n // Legacy semantic ids are only reachable through Layer.getElement\n let resolved: ResolvedElement | undefined;\n for (const layer of model.layers.values()) {\n const element = layer.getElement(elementId);\n if (element) {\n resolved = toResolved(element, layer.name);\n break;\n }\n }\n index.set(elementId, resolved);\n return resolved;\n };\n}\n\n/** The committed relationships as recorded before the migration and as rewritten by it */\nexport interface CommittedRelationships {\n before: Relationship[];\n after: Relationship[];\n}\n\nexport interface RelationshipMigrationResult {\n filesModified: number;\n changes: RelationshipRewriteChange[];\n committed: CommittedRelationships;\n error?: string;\n}\n\n/**\n * Apply the rewrite table to documentation-robotics/model/relationships.yaml on disk.\n * The file is rewritten only when at least one relationship changes.\n */\nexport async function migrateRelationshipsFile(\n model: Model,\n table: RelationshipRewriteTable = RELATIONSHIP_REWRITE_TABLE,\n options: { dryRun?: boolean } = {}\n): Promise<RelationshipMigrationResult> {\n const fs = await import(\"fs/promises\");\n const YAML = await import(\"yaml\");\n const relationshipsPath = `${model.rootPath}/documentation-robotics/model/relationships.yaml`;\n\n let content: string;\n try {\n content = await fs.readFile(relationshipsPath, \"utf-8\");\n } catch (error) {\n if ((error as { code?: string }).code === \"ENOENT\") {\n return { filesModified: 0, changes: [], committed: { before: [], after: [] } };\n }\n throw error;\n }\n\n const data = YAML.parse(content) ?? [];\n if (!Array.isArray(data)) {\n return {\n filesModified: 0,\n changes: [],\n committed: { before: [], after: [] },\n error: `relationships.yaml is not a list of relationships (found ${typeof data}); fix the file and run the upgrade again`,\n };\n }\n\n const resolve = await buildResolver(model);\n const { relationships, changes } = rewriteRelationships(data as Relationship[], resolve, table);\n const committed = { before: data as Relationship[], after: relationships };\n\n if (changes.length === 0 || options.dryRun) {\n return { filesModified: 0, changes, committed };\n }\n\n // Same layout as Model.saveRelationships()\n const header = `# Intra-layer relationships\n# Format: source_id -> predicate -> target_id\n# Last updated: ${new Date().toISOString()}\n\n`;\n await fs.writeFile(relationshipsPath, header + YAML.stringify(relationships), \"utf-8\");\n return { filesModified: 1, changes, committed };\n}\n\n/** A staged relationship change the migration rewrote or removed from its changeset */\nexport interface StagedRelationshipRewriteChange extends RelationshipRewriteChange {\n /** Id of the changeset that holds the change */\n changeset: string;\n /** What the staged change does to the relationship */\n stagedAs: \"relationship-add\" | \"relationship-delete\";\n /**\n * For a dropped duplicate, what already covers the rewritten relationship:\n * \"model\" - the migrated model has it (staged add);\n * \"changeset\" - an earlier staged change already adds or deletes it;\n * \"kept\" - the changeset deletes it, but another relationship the model or the changeset keeps\n * was rewritten to it, so deleting it would also remove that one (staged delete)\n */\n duplicateOf?: \"model\" | \"changeset\" | \"kept\";\n}\n\nfunction isRelationship(value: unknown): value is Relationship {\n const rel = value as Partial<Relationship> | undefined;\n return (\n typeof rel === \"object\" &&\n rel !== null &&\n typeof rel.source === \"string\" &&\n typeof rel.predicate === \"string\" &&\n typeof rel.target === \"string\"\n );\n}\n\n/** A staged relationship change carrying a rewritten relationship, re-keyed to match it */\nfunction withRelationship(change: Change, field: \"before\" | \"after\", rel: Relationship): Change {\n return {\n ...change,\n elementId: `${rel.source}::${rel.predicate}::${rel.target}`,\n layerName: rel.layer || change.layerName,\n [field]: rel as unknown as Record<string, unknown>,\n };\n}\n\n/** Resolve the elements a changeset adds as well as the model's own */\nfunction withStagedElements(base: ElementResolver, changes: Change[]): ElementResolver {\n const staged = new Map<string, ResolvedElement>();\n for (const change of changes) {\n if (change.type !== \"add\" || !change.after) {\n continue;\n }\n const resolved = toResolved(change.after, change.layerName);\n if (!resolved) {\n continue;\n }\n for (const key of [change.after.path, change.after.id, change.elementId]) {\n if (typeof key === \"string\" && key) {\n staged.set(key, resolved);\n }\n }\n }\n // The model wins: committing an add whose element already exists is a no-op\n return (elementId) => base(elementId) ?? staged.get(elementId);\n}\n\n/**\n * Rewrite the relationship changes of one staged changeset recorded against retired catalog\n * entries, so that committing it after the migration has the effect it would have had before.\n *\n * - A relationship-add takes its rewritten relationship and key. It is dropped when the rule has\n * no replacement, or when the rewritten relationship is already in the migrated model or\n * added by an earlier change in the changeset (an add the table does not touch wins, as in\n * rewriteRelationships).\n * - A relationship-delete takes the rewritten form of the relationship it deletes, which is what\n * the committed relationship became. It is dropped when the rule has no replacement (the\n * migration already removed that relationship), when an earlier change already deletes the same\n * rewritten relationship, or when the changeset's end state keeps another relationship that was\n * rewritten to the same one and nothing later in the changeset adds it back: in the old catalog\n * they were two relationships and the changeset deleted only one of them.\n *\n * Other changes are kept as they are. Changes come back in commit order, each keeping the sequence\n * number it was staged with (a change that carried none takes its position): numbers are never\n * reused, so unstage tombstones and revisions taken before the upgrade still name the same changes.\n * A dropped change leaves a gap, as an unstaged one does.\n */\nexport function rewriteStagedChangeset(\n changeset: { id: string; changes: Change[] },\n committed: CommittedRelationships,\n resolve: ElementResolver,\n table: RelationshipRewriteTable = RELATIONSHIP_REWRITE_TABLE\n): { changes: Change[]; report: StagedRelationshipRewriteChange[] } {\n const identity = identityOf(resolve);\n const ordered = changeset.changes\n .map((change, index) => ({ change, order: (change as StagedChange).sequenceNumber ?? index }))\n .sort((a, b) => a.order - b.order)\n .map(({ change, order }) => {\n const value =\n change.type === \"relationship-add\"\n ? change.after\n : change.type === \"relationship-delete\"\n ? change.before\n : undefined;\n const rel = isRelationship(value) ? value : undefined;\n // rewriteOne: null when the table leaves the relationship as it is\n return {\n change: { ...change, sequenceNumber: order } as Change,\n rel,\n outcome: rel ? rewriteOne(rel, resolve, table) : null,\n };\n });\n const imageOf = (rel: Relationship, outcome: ReturnType<typeof rewriteOne>) =>\n outcome === null ? rel : outcome.rel;\n\n // Relationships the changeset's end state holds, replayed over the committed model as recorded,\n // and what they become\n const endState = new Map(committed.before.map((rel) => [identity(rel), rel]));\n for (const { change, rel } of ordered) {\n if (rel && change.type === \"relationship-add\") {\n endState.set(identity(rel), rel);\n } else if (rel) {\n endState.delete(identity(rel));\n }\n }\n const kept = new Set<string>();\n for (const rel of endState.values()) {\n const image = imageOf(rel, rewriteOne(rel, resolve, table));\n if (image) {\n kept.add(identity(image));\n }\n }\n\n // Position of the last staged add of each rewritten relationship\n const lastAdd = new Map<string, number>();\n ordered.forEach(({ change, rel, outcome }, position) => {\n const image = rel && change.type === \"relationship-add\" ? imageOf(rel, outcome) : null;\n if (image) {\n lastAdd.set(identity(image), position);\n }\n });\n\n // What the model holds, and what the changeset adds and deletes, as the rewritten changes replay\n const inModel = new Set(committed.after.map(identity));\n const added = new Set(\n ordered\n .filter(({ change, rel, outcome }) => rel && change.type === \"relationship-add\" && outcome === null)\n .map(({ rel }) => identity(rel!))\n );\n const deleted = new Set<string>();\n\n const changes: Change[] = [];\n const report: StagedRelationshipRewriteChange[] = [];\n const record = (\n stagedAs: StagedRelationshipRewriteChange[\"stagedAs\"],\n entry: RelationshipRewriteChange,\n duplicateOf?: StagedRelationshipRewriteChange[\"duplicateOf\"]\n ) => report.push({ ...entry, changeset: changeset.id, stagedAs, ...(duplicateOf && { duplicateOf }) });\n\n ordered.forEach(({ change, rel, outcome }, position) => {\n if (!rel) {\n changes.push(change);\n return;\n }\n const stagedAs = change.type as StagedRelationshipRewriteChange[\"stagedAs\"];\n if (outcome !== null && outcome.rel === null) {\n // A delete has nothing left to delete: the file migration dropped the relationship too\n record(stagedAs, { action: \"dropped\", before: tripleOf(rel), rules: outcome.rules });\n return;\n }\n const image = outcome?.rel ?? rel;\n const key = identity(image);\n const entry = {\n action: \"dropped-duplicate\" as const,\n before: tripleOf(rel),\n after: tripleOf(image),\n rules: outcome?.rules ?? [],\n };\n\n if (stagedAs === \"relationship-add\") {\n if (outcome !== null && inModel.has(key)) {\n record(stagedAs, entry, \"model\");\n return;\n }\n if (outcome !== null && added.has(key)) {\n record(stagedAs, entry, \"changeset\");\n return;\n }\n added.add(key);\n deleted.delete(key);\n } else {\n if (kept.has(key) && (lastAdd.get(key) ?? -1) < position) {\n record(stagedAs, entry, \"kept\");\n return;\n }\n if (outcome !== null && deleted.has(key)) {\n record(stagedAs, entry, \"changeset\");\n return;\n }\n deleted.add(key);\n inModel.delete(key);\n added.delete(key);\n }\n\n if (outcome === null) {\n changes.push(change);\n } else {\n changes.push(withRelationship(change, stagedAs === \"relationship-add\" ? \"after\" : \"before\", image));\n record(stagedAs, { ...entry, action: \"rewritten\" });\n }\n });\n\n return { changes, report };\n}\n\nexport interface StagedRelationshipMigrationResult {\n changesetsModified: number;\n changes: StagedRelationshipRewriteChange[];\n errors: string[];\n}\n\n/**\n * Apply the rewrite table to the relationship changes of every staged changeset under\n * documentation-robotics/changesets. Committed and discarded changesets are history and stay as\n * they are. A changeset is rewritten only when at least one of its changes is affected.\n *\n * @param committed - relationships.yaml before and after migrateRelationshipsFile rewrote it\n */\nexport async function migrateStagedChangesets(\n model: Model,\n committed: CommittedRelationships,\n table: RelationshipRewriteTable = RELATIONSHIP_REWRITE_TABLE,\n options: { dryRun?: boolean } = {}\n): Promise<StagedRelationshipMigrationResult> {\n const storage = new StagedChangesetStorage(model.rootPath);\n const result: StagedRelationshipMigrationResult = { changesetsModified: 0, changes: [], errors: [] };\n\n let entries: string[];\n try {\n entries = (await readdir(storage.getChangesetsDir(), { withFileTypes: true }))\n // FileLock keeps \"<changeset>.lock\" directories beside the changesets\n .filter((entry) => entry.isDirectory() && !entry.name.endsWith(\".lock\"))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n if ((error as { code?: string }).code === \"ENOENT\") {\n return result;\n }\n throw error;\n }\n\n const base = await buildResolver(model);\n\n for (const id of entries) {\n try {\n await new FileLock(storage.getChangesetPath(id)).withLock(async () => {\n const changeset = await storage.load(id);\n if (!changeset || changeset.status !== \"staged\") {\n return;\n }\n const resolve = withStagedElements(base, changeset.changes);\n const { changes, report } = rewriteStagedChangeset(changeset, committed, resolve, table);\n if (report.length === 0) {\n return;\n }\n if (options.dryRun) {\n result.changes.push(...report);\n return;\n }\n // Dropped changes leave gaps; record the highest number used so none is reused\n const lastSequence = nextSequenceNumber(changeset) - 1;\n changeset.changes = changes;\n if (lastSequence >= 0) {\n changeset.lastSequence = lastSequence;\n }\n changeset.updateModified();\n await storage.save(changeset);\n result.changesetsModified++;\n result.changes.push(...report);\n });\n } catch (error) {\n result.errors.push(\n `Failed to migrate staged changeset '${id}': ${getErrorMessage(error)}; fix or delete the changeset and run the upgrade again`\n );\n }\n }\n\n return result;\n}\n\n/** One-line summary of a relationship migration, for the upgrade output */\nexport function summarizeRelationshipChanges(\n changes: RelationshipRewriteChange[],\n label = \"relationships\"\n): string {\n const count = (action: RelationshipRewriteChange[\"action\"]) =>\n changes.filter((c) => c.action === action).length;\n const parts = [`${count(\"rewritten\")} rewritten`, `${count(\"dropped-duplicate\")} dropped as duplicates`];\n const dropped = count(\"dropped\");\n if (dropped > 0) {\n parts.push(`${dropped} dropped (no replacement)`);\n }\n return `${label}: ${parts.join(\", \")}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ import { elementIdentifiers } from "./relationships.js";
2
+ function collectKnownIdentifiers(elements) {
3
+ const known = /* @__PURE__ */ new Set();
4
+ for (const element of elements)
5
+ for (const id of elementIdentifiers(element))
6
+ known.add(id);
7
+ return known;
8
+ }
9
+ function findDanglingRelationships(relationships, knownIdentifiers, options = {}) {
10
+ const layerFilter = options.layers && options.layers.length > 0 ? new Set(options.layers) : null, dangling = [], kept = [];
11
+ let scanned = 0;
12
+ for (const rel of relationships) {
13
+ if (!(!layerFilter || layerFilter.has(rel.layer) || rel.targetLayer !== void 0 && layerFilter.has(rel.targetLayer))) {
14
+ kept.push(rel);
15
+ continue;
16
+ }
17
+ scanned++;
18
+ const sourceMissing = !knownIdentifiers.has(rel.source), targetMissing = !knownIdentifiers.has(rel.target);
19
+ if (!sourceMissing && !targetMissing) {
20
+ kept.push(rel);
21
+ continue;
22
+ }
23
+ const unresolved = sourceMissing && targetMissing ? "both" : sourceMissing ? "source" : "target";
24
+ dangling.push({ relationship: rel, unresolved });
25
+ }
26
+ return { scanned, dangling, kept };
27
+ }
28
+ function extractRelationshipsHeader(yamlContent) {
29
+ const lines = yamlContent.split(`
30
+ `), headerLines = [];
31
+ for (const line of lines) {
32
+ const trimmed = line.trim();
33
+ if (trimmed === "" || trimmed.startsWith("#"))
34
+ headerLines.push(line);
35
+ else
36
+ break;
37
+ }
38
+ for (; headerLines.length > 0 && headerLines[headerLines.length - 1].trim() === ""; )
39
+ headerLines.pop();
40
+ return headerLines.length > 0 ? headerLines.join(`
41
+ `) + `
42
+ ` : "";
43
+ }
44
+ export {
45
+ collectKnownIdentifiers,
46
+ extractRelationshipsHeader,
47
+ findDanglingRelationships
48
+ };
49
+ //# sourceMappingURL=relationship-prune.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/relationship-prune.ts"],
4
+ "sourcesContent": ["/**\n * Dangling relationship detection for `dr relationship prune`.\n *\n * A relationship row is dangling when its `source` or `target` matches neither the\n * UUID `id` nor the dotted `path` of any current element. Both forms are legal in\n * relationships.yaml, so matching on only one of them produces false positives.\n */\n\nimport type { Relationship } from \"./relationships.js\";\nimport { elementIdentifiers } from \"./relationships.js\";\n\nexport type UnresolvedSide = \"source\" | \"target\" | \"both\";\n\nexport interface DanglingRelationship {\n relationship: Relationship;\n /** Which endpoint(s) failed to resolve to a current element */\n unresolved: UnresolvedSide;\n}\n\nexport interface PruneAnalysis {\n /** Rows examined (after the optional layer filter) */\n scanned: number;\n /** Rows to remove, in file order */\n dangling: DanglingRelationship[];\n /** Rows to keep (including rows outside the layer filter), in file order */\n kept: Relationship[];\n}\n\nexport interface PruneOptions {\n /**\n * Restrict the scan to rows whose source layer (`layer`) or target layer\n * (`targetLayer`) is one of these canonical layer names. Rows outside the\n * filter are always kept.\n */\n layers?: string[];\n}\n\n/**\n * Collect every identifier (UUID id and dotted path) of the given elements.\n */\nexport function collectKnownIdentifiers(\n elements: Iterable<{ id?: unknown; path?: unknown }>\n): Set<string> {\n const known = new Set<string>();\n for (const element of elements) {\n for (const id of elementIdentifiers(element)) {\n known.add(id);\n }\n }\n return known;\n}\n\n/**\n * Partition relationships into dangling rows and rows to keep.\n *\n * Order is preserved in both lists so a rewrite of relationships.yaml keeps the\n * surviving rows in their original sequence.\n */\nexport function findDanglingRelationships(\n relationships: readonly Relationship[],\n knownIdentifiers: ReadonlySet<string>,\n options: PruneOptions = {}\n): PruneAnalysis {\n const layerFilter = options.layers && options.layers.length > 0 ? new Set(options.layers) : null;\n\n const dangling: DanglingRelationship[] = [];\n const kept: Relationship[] = [];\n let scanned = 0;\n\n for (const rel of relationships) {\n const inScope =\n !layerFilter ||\n layerFilter.has(rel.layer) ||\n (rel.targetLayer !== undefined && layerFilter.has(rel.targetLayer));\n\n if (!inScope) {\n kept.push(rel);\n continue;\n }\n\n scanned++;\n const sourceMissing = !knownIdentifiers.has(rel.source);\n const targetMissing = !knownIdentifiers.has(rel.target);\n\n if (!sourceMissing && !targetMissing) {\n kept.push(rel);\n continue;\n }\n\n const unresolved: UnresolvedSide =\n sourceMissing && targetMissing ? \"both\" : sourceMissing ? \"source\" : \"target\";\n dangling.push({ relationship: rel, unresolved });\n }\n\n return { scanned, dangling, kept };\n}\n\n/**\n * Split a relationships.yaml document into its leading comment block and the rest.\n *\n * The header is every line up to (but excluding) the first line that is neither a\n * `#` comment nor blank. An empty string is returned when the file has no header.\n */\nexport function extractRelationshipsHeader(yamlContent: string): string {\n const lines = yamlContent.split(\"\\n\");\n const headerLines: string[] = [];\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === \"\" || trimmed.startsWith(\"#\")) {\n headerLines.push(line);\n } else {\n break;\n }\n }\n // Drop trailing blank lines; the writer re-adds a single separator\n while (headerLines.length > 0 && headerLines[headerLines.length - 1].trim() === \"\") {\n headerLines.pop();\n }\n return headerLines.length > 0 ? headerLines.join(\"\\n\") + \"\\n\" : \"\";\n}\n"],
5
+ "mappings": "AASA,SAAS,0BAA0B;AA+B7B,SAAU,wBACd,UAAoD;AAEpD,QAAM,QAAQ,oBAAI,IAAG;AACrB,aAAW,WAAW;AACpB,eAAW,MAAM,mBAAmB,OAAO;AACzC,YAAM,IAAI,EAAE;AAGhB,SAAO;AACT;AAQM,SAAU,0BACd,eACA,kBACA,UAAwB,CAAA,GAAE;AAE1B,QAAM,cAAc,QAAQ,UAAU,QAAQ,OAAO,SAAS,IAAI,IAAI,IAAI,QAAQ,MAAM,IAAI,MAEtF,WAAmC,CAAA,GACnC,OAAuB,CAAA;AAC7B,MAAI,UAAU;AAEd,aAAW,OAAO,eAAe;AAM/B,QAAI,EAJF,CAAC,eACD,YAAY,IAAI,IAAI,KAAK,KACxB,IAAI,gBAAgB,UAAa,YAAY,IAAI,IAAI,WAAW,IAErD;AACZ,WAAK,KAAK,GAAG;AACb;IACF;AAEA;AACA,UAAM,gBAAgB,CAAC,iBAAiB,IAAI,IAAI,MAAM,GAChD,gBAAgB,CAAC,iBAAiB,IAAI,IAAI,MAAM;AAEtD,QAAI,CAAC,iBAAiB,CAAC,eAAe;AACpC,WAAK,KAAK,GAAG;AACb;IACF;AAEA,UAAM,aACJ,iBAAiB,gBAAgB,SAAS,gBAAgB,WAAW;AACvE,aAAS,KAAK,EAAE,cAAc,KAAK,WAAU,CAAE;EACjD;AAEA,SAAO,EAAE,SAAS,UAAU,KAAI;AAClC;AAQM,SAAU,2BAA2B,aAAmB;AAC5D,QAAM,QAAQ,YAAY,MAAM;CAAI,GAC9B,cAAwB,CAAA;AAC9B,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAI;AACzB,QAAI,YAAY,MAAM,QAAQ,WAAW,GAAG;AAC1C,kBAAY,KAAK,IAAI;;AAErB;EAEJ;AAEA,SAAO,YAAY,SAAS,KAAK,YAAY,YAAY,SAAS,CAAC,EAAE,KAAI,MAAO;AAC9E,gBAAY,IAAG;AAEjB,SAAO,YAAY,SAAS,IAAI,YAAY,KAAK;CAAI,IAAI;IAAO;AAClE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,68 @@
1
+ function stagedRelationshipTriple(change) {
2
+ const data = change.after ?? change.before, [source, predicate, target] = data?.source ? [data.source, data.predicate, data.target] : change.elementId.split("::");
3
+ return source && predicate && target ? { source, predicate, target } : null;
4
+ }
5
+ function stagedRelationshipKeys(graph, changeset, type, rel) {
6
+ const source = graph.resolve(rel.source), target = graph.resolve(rel.target), keys = /* @__PURE__ */ new Set();
7
+ for (const change of changeset.changes) {
8
+ if (change.type !== type)
9
+ continue;
10
+ const triple = stagedRelationshipTriple(change);
11
+ triple && (!rel.predicate || triple.predicate === rel.predicate) && graph.resolve(triple.source) === source && graph.resolve(triple.target) === target && keys.add(change.elementId);
12
+ }
13
+ return [...keys];
14
+ }
15
+ function stagedRelationshipChangesAtEnds(graph, changeset, ends) {
16
+ if (!ends.source && !ends.target)
17
+ throw new Error("stagedRelationshipChangesAtEnds needs a source or a target");
18
+ const source = ends.source ? graph.resolve(ends.source) : void 0, target = ends.target ? graph.resolve(ends.target) : void 0;
19
+ return changeset.changes.filter((change) => {
20
+ if (change.type !== "relationship-add" && change.type !== "relationship-delete")
21
+ return !1;
22
+ const triple = stagedRelationshipTriple(change);
23
+ return !!triple && (!ends.predicate || triple.predicate === ends.predicate) && (!source || graph.resolve(triple.source) === source) && (!target || graph.resolve(triple.target) === target);
24
+ });
25
+ }
26
+ function planRelationshipAdd(graph, changeset, triple) {
27
+ const existing = graph.getRelationship(triple.source, triple.predicate, triple.target);
28
+ return existing ? existing.status !== "deleted" ? { action: "duplicate" } : { action: "restore", keys: stagedRelationshipKeys(graph, changeset, "relationship-delete", existing) } : { action: "stage" };
29
+ }
30
+ function liveRelationshipsBetween(graph, source, target, predicate) {
31
+ const from = graph.resolve(source), to = graph.resolve(target);
32
+ return graph.relationships().filter((rel) => rel.status !== "deleted" && rel.source === from && rel.target === to && (!predicate || rel.predicate === predicate));
33
+ }
34
+ async function stageRelationshipDeletes(manager, graph, changeset, relationships) {
35
+ const results = [];
36
+ for (const rel of relationships) {
37
+ if (rel.status === "added") {
38
+ for (const key of stagedRelationshipKeys(graph, changeset, "relationship-add", rel))
39
+ await manager.unstage(changeset.id, key);
40
+ results.push({ predicate: rel.predicate, status: "unstaged" });
41
+ continue;
42
+ }
43
+ const source = rel.raw?.source ?? rel.source, target = rel.raw?.target ?? rel.target;
44
+ await manager.stage(changeset.id, {
45
+ type: "relationship-delete",
46
+ elementId: `${source}::${rel.predicate}::${target}`,
47
+ layerName: rel.layer,
48
+ before: {
49
+ source,
50
+ target,
51
+ predicate: rel.predicate,
52
+ layer: rel.layer,
53
+ ...rel.targetLayer ? { targetLayer: rel.targetLayer } : {},
54
+ ...rel.properties ? { properties: rel.properties } : {}
55
+ }
56
+ }), results.push({ predicate: rel.predicate, status: "staged" });
57
+ }
58
+ return results;
59
+ }
60
+ export {
61
+ liveRelationshipsBetween,
62
+ planRelationshipAdd,
63
+ stageRelationshipDeletes,
64
+ stagedRelationshipChangesAtEnds,
65
+ stagedRelationshipKeys,
66
+ stagedRelationshipTriple
67
+ };
68
+ //# sourceMappingURL=relationship-staging.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/relationship-staging.ts"],
4
+ "sourcesContent": ["/**\n * Relationship staging against a changeset's projected graph.\n *\n * Shared by `dr relationship add|delete` and the MCP `relationship_add|delete` /\n * `changeset_unstage` tools, so the CLI and an agent stage relationships the same way:\n *\n * - Adding a relationship the projected graph already holds (in the base model or staged by this\n * changeset) is reported as a duplicate instead of staged twice.\n * - Re-adding a relationship this changeset staged for deletion unstages the deletion.\n * - Deleting a relationship this changeset added unstages the add instead of stacking a delete on it.\n *\n * Every function here works on staged keys (`source::predicate::target`) however a relationship's\n * ends are spelled (UUID or dotted path), by resolving them through the graph.\n */\n\nimport type { ChangesetGraph, GraphRelationship } from \"./changeset-graph.js\";\nimport type { Changeset, StagedChange } from \"./changeset.js\";\nimport type { StagingAreaManager } from \"./staging-area.js\";\n\nexport interface RelationshipTriple {\n source: string;\n predicate: string;\n target: string;\n}\n\nexport type RelationshipChangeType = \"relationship-add\" | \"relationship-delete\";\n\n/** The triple a staged relationship change names, read from its data or its composite key. */\nexport function stagedRelationshipTriple(change: StagedChange): RelationshipTriple | null {\n const data = (change.after ?? change.before) as Partial<GraphRelationship> | undefined;\n const [source, predicate, target] = data?.source\n ? [data.source, data.predicate, data.target]\n : change.elementId.split(\"::\");\n return source && predicate && target ? { source, predicate, target } : null;\n}\n\n/**\n * Keys of the staged relationship changes of `type` that name `rel` (any predicate when\n * `rel.predicate` is omitted), however their ends are spelled.\n */\nexport function stagedRelationshipKeys(\n graph: ChangesetGraph,\n changeset: Changeset,\n type: RelationshipChangeType,\n rel: { source: string; predicate?: string; target: string }\n): string[] {\n const source = graph.resolve(rel.source);\n const target = graph.resolve(rel.target);\n const keys = new Set<string>();\n for (const change of changeset.changes as StagedChange[]) {\n if (change.type !== type) continue;\n const triple = stagedRelationshipTriple(change);\n if (\n triple &&\n (!rel.predicate || triple.predicate === rel.predicate) &&\n graph.resolve(triple.source) === source &&\n graph.resolve(triple.target) === target\n ) {\n keys.add(change.elementId);\n }\n }\n return [...keys];\n}\n\n/**\n * The staged relationship changes (adds and deletes) with the given end: every one from `source`,\n * or every one to `target`, or between them when both are given; optionally one predicate.\n * Ends are compared through the graph, however they are spelled.\n */\nexport function stagedRelationshipChangesAtEnds(\n graph: ChangesetGraph,\n changeset: Changeset,\n ends: { source?: string; target?: string; predicate?: string }\n): StagedChange[] {\n if (!ends.source && !ends.target) {\n throw new Error(\"stagedRelationshipChangesAtEnds needs a source or a target\");\n }\n const source = ends.source ? graph.resolve(ends.source) : undefined;\n const target = ends.target ? graph.resolve(ends.target) : undefined;\n return (changeset.changes as StagedChange[]).filter((change) => {\n if (change.type !== \"relationship-add\" && change.type !== \"relationship-delete\") return false;\n const triple = stagedRelationshipTriple(change);\n return (\n !!triple &&\n (!ends.predicate || triple.predicate === ends.predicate) &&\n (!source || graph.resolve(triple.source) === source) &&\n (!target || graph.resolve(triple.target) === target)\n );\n });\n}\n\n/** What staging a relationship add would do, given the changeset's projected graph. */\nexport type RelationshipAddPlan =\n /** The projection already holds it, from the base model or this changeset. Stage nothing. */\n | { action: \"duplicate\" }\n /** This changeset deletes it. Unstage the deletion (these keys) instead of staging an add. */\n | { action: \"restore\"; keys: string[] }\n /** Not in the projection. Stage the add. */\n | { action: \"stage\" };\n\nexport function planRelationshipAdd(\n graph: ChangesetGraph,\n changeset: Changeset,\n triple: RelationshipTriple\n): RelationshipAddPlan {\n const existing = graph.getRelationship(triple.source, triple.predicate, triple.target);\n if (!existing) return { action: \"stage\" };\n if (existing.status !== \"deleted\") return { action: \"duplicate\" };\n return { action: \"restore\", keys: stagedRelationshipKeys(graph, changeset, \"relationship-delete\", existing) };\n}\n\n/** Relationships in the projection from `source` to `target` (one predicate, or all) that are not deleted. */\nexport function liveRelationshipsBetween(\n graph: ChangesetGraph,\n source: string,\n target: string,\n predicate?: string\n): GraphRelationship[] {\n const from = graph.resolve(source);\n const to = graph.resolve(target);\n return graph\n .relationships()\n .filter(\n (rel) =>\n rel.status !== \"deleted\" && rel.source === from && rel.target === to && (!predicate || rel.predicate === predicate)\n );\n}\n\nexport interface StagedRelationshipDeleteResult {\n predicate: string;\n /** \"unstaged\": this changeset added it, so the add was dropped. \"staged\": a removal was staged. */\n status: \"staged\" | \"unstaged\";\n}\n\n/**\n * Removes `relationships` (live ones from the projection) within a changeset: unstages the ones\n * this changeset added and stages a deletion for the rest.\n */\nexport async function stageRelationshipDeletes(\n manager: StagingAreaManager,\n graph: ChangesetGraph,\n changeset: Changeset,\n relationships: GraphRelationship[]\n): Promise<StagedRelationshipDeleteResult[]> {\n const results: StagedRelationshipDeleteResult[] = [];\n for (const rel of relationships) {\n if (rel.status === \"added\") {\n for (const key of stagedRelationshipKeys(graph, changeset, \"relationship-add\", rel)) {\n await manager.unstage(changeset.id!, key);\n }\n results.push({ predicate: rel.predicate, status: \"unstaged\" });\n continue;\n }\n // Name the row the way relationships.yaml does, so the commit finds it.\n const source = rel.raw?.source ?? rel.source;\n const target = rel.raw?.target ?? rel.target;\n await manager.stage(changeset.id!, {\n type: \"relationship-delete\",\n elementId: `${source}::${rel.predicate}::${target}`,\n layerName: rel.layer,\n before: {\n source,\n target,\n predicate: rel.predicate,\n layer: rel.layer,\n ...(rel.targetLayer ? { targetLayer: rel.targetLayer } : {}),\n ...(rel.properties ? { properties: rel.properties } : {}),\n },\n });\n results.push({ predicate: rel.predicate, status: \"staged\" });\n }\n return results;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -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