@documentation-robotics/cli 0.3.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 (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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\";\nimport { RelationshipCatalog } from \"./relationship-catalog.js\";\nimport {\n getValidRelationships,\n getValidPredicatesForSource,\n getValidDestinationsForSourceAndPredicate,\n} from \"../generated/relationship-index.js\";\nimport { normalizeNodeType } from \"../generated/node-types.js\";\nimport { findSimilar } from \"../utils/errors.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\n/**\n * Validate a relationship combination using schema-driven registry\n * @returns {valid, suggestions} where suggestions are provided if invalid\n */\nexport function validateRelationshipCombination(\n sourceSpecNodeId: string,\n predicate: string,\n destSpecNodeId: string\n): { valid: boolean; suggestions?: string[] } {\n const valid =\n getValidRelationships(sourceSpecNodeId, predicate, destSpecNodeId).length >\n 0;\n\n if (valid) {\n return { valid: true };\n }\n\n // Generate helpful suggestions\n const suggestions: string[] = [];\n const validPredicates = getValidPredicatesForSource(sourceSpecNodeId);\n\n if (validPredicates.length > 0) {\n suggestions.push(\n `Valid predicates for ${sourceSpecNodeId}: ${validPredicates.join(\", \")}`\n );\n } else {\n suggestions.push(`No valid relationships defined for ${sourceSpecNodeId}`);\n }\n\n // If predicate is provided, show valid destinations\n if (predicate) {\n const validDests = getValidDestinationsForSourceAndPredicate(\n sourceSpecNodeId,\n predicate\n );\n if (validDests.length > 0) {\n suggestions.push(\n `Valid destinations for --[${predicate}]-->: ${validDests.join(\", \")}`\n );\n } else {\n suggestions.push(\n `No valid destinations for ${sourceSpecNodeId} --[${predicate}]-->`\n );\n }\n }\n\n return { valid: false, suggestions };\n}\n\n// ---------------------------------------------------------------------------\n// Batches\n// ---------------------------------------------------------------------------\n\nexport interface RelationshipBatchItem {\n source: string;\n predicate: string;\n target: string;\n properties?: Record<string, unknown>;\n}\n\n/** An element the batch itself adds, so relationships in the same batch can name it. */\nexport interface PendingElement {\n id: string;\n layer: string;\n type: string;\n}\n\n/** \"added\" is \"staged\" when no changeset is active and the add is written to the model. */\nexport type RelationshipBatchStatus = \"staged\" | \"added\" | \"restored\" | \"duplicate\" | \"invalid\";\n\nexport interface RelationshipBatchResult {\n /** Position in the request, from 0. */\n index: number;\n source: string;\n predicate: string;\n target: string;\n status: RelationshipBatchStatus;\n /** Why the item was a duplicate or invalid. */\n reason?: string;\n suggestions?: string[];\n}\n\nexport interface RelationshipBatchPlan {\n /** The adds to stage (or write), in request order. */\n changes: Array<Omit<StagedChange, \"sequenceNumber\">>;\n /** Keys of staged deletions to unstage: the batch re-adds what this changeset deleted. */\n unstageKeys: string[];\n results: RelationshipBatchResult[];\n}\n\n/**\n * Checks a list of relationship adds against the projected graph and the catalog, without writing.\n * Each item is to be staged, a duplicate (in the projection, or earlier in the batch), restored\n * (the changeset deletes it), or invalid with the reason. An invalid item does not stop the rest.\n * Ends may name elements in `pending` (added by the same call); `rejected` names elements the same\n * call refused to add, with why, so the reason says so.\n */\nexport async function planRelationshipBatch(\n graph: ChangesetGraph,\n changeset: Changeset,\n items: RelationshipBatchItem[],\n options: {\n pending?: Map<string, PendingElement>;\n rejected?: Map<string, string>;\n /** The status for an add: \"staged\" into the changeset (default), or \"added\" to the model. */\n addStatus?: \"staged\" | \"added\";\n } = {}\n): Promise<RelationshipBatchPlan> {\n const addStatus = options.addStatus ?? \"staged\";\n const pending = options.pending ?? new Map<string, PendingElement>();\n const rejected = options.rejected ?? new Map<string, string>();\n const catalog = new RelationshipCatalog();\n await catalog.load();\n\n const changes: RelationshipBatchPlan[\"changes\"] = [];\n const unstageKeys: string[] = [];\n const results: RelationshipBatchResult[] = [];\n const seen = new Set<string>();\n\n const endpoint = (id: string, role: \"Source\" | \"Target\"): PendingElement | { error: string; suggestions: string[] } => {\n const added = pending.get(id);\n if (added) return added;\n const element = graph.getElement(id);\n if (element && element.status !== \"deleted\") return { id: element.id, layer: element.layer, type: element.type };\n if (element) return { error: `${role} element ${id} not found (this changeset deletes it)`, suggestions: [] };\n const refusal = rejected.get(id);\n if (refusal) return { error: `${role} element ${id} was not added: ${refusal}`, suggestions: [] };\n const layerPrefix = `${id.split(\".\")[0]}.`;\n const candidates = [...graph.elements.values()]\n .filter((e) => e.status !== \"deleted\" && e.id.startsWith(layerPrefix))\n .map((e) => e.id)\n .concat([...pending.keys()].filter((k) => k.startsWith(layerPrefix)));\n const similar = findSimilar(id, candidates, 4);\n return {\n error: `${role} element ${id} not found`,\n suggestions: similar.length\n ? [`Did you mean: ${similar.join(\" or \")}?`]\n : ['Use \"model_search\" to find elements, or \"changeset_brief\" to see what this changeset adds'],\n };\n };\n\n items.forEach((item, index) => {\n const asSent = { index, source: item.source, predicate: item.predicate, target: item.target };\n const invalid = (reason: string, suggestions?: string[]) => {\n results.push({ ...asSent, status: \"invalid\", reason, ...(suggestions?.length ? { suggestions } : {}) });\n };\n\n if (!item.source || !item.predicate || !item.target) {\n return invalid(\"Each relationship needs source, predicate and target\");\n }\n const source = endpoint(item.source, \"Source\");\n if (\"error\" in source) return invalid(source.error, source.suggestions);\n const target = endpoint(item.target, \"Target\");\n if (\"error\" in target) return invalid(target.error, target.suggestions);\n\n const sourceSpec = `${source.layer}.${normalizeNodeType(source.layer, source.type)}`;\n const targetSpec = `${target.layer}.${normalizeNodeType(target.layer, target.type)}`;\n const validation = validateRelationshipCombination(sourceSpec, item.predicate, targetSpec);\n if (!validation.valid) {\n return invalid(`The catalog has no ${sourceSpec} --[${item.predicate}]--> ${targetSpec}`, validation.suggestions);\n }\n const relType = catalog.getTypeByPredicate(item.predicate);\n if (relType && !relType.semantics.reflexivity && source.id === target.id) {\n return invalid(`Predicate \"${item.predicate}\" does not allow reflexive relationships`);\n }\n\n const triple = { source: source.id, predicate: item.predicate, target: target.id };\n const key = `${triple.source}::${triple.predicate}::${triple.target}`;\n const resolved = { ...asSent, ...triple };\n if (seen.has(key)) {\n results.push({ ...resolved, status: \"duplicate\", reason: \"Repeated earlier in this batch\" });\n return;\n }\n seen.add(key);\n\n // Ends this batch adds are checked in the projection too: one the changeset deleted and the\n // batch re-adds may have relationships whose deletion is staged, which are restored.\n const plan = planRelationshipAdd(graph, changeset, triple);\n if (plan.action === \"duplicate\") {\n results.push({ ...resolved, status: \"duplicate\", reason: \"Already in the model or this changeset\" });\n return;\n }\n if (plan.action === \"restore\") {\n unstageKeys.push(...plan.keys);\n results.push({ ...resolved, status: \"restored\" });\n return;\n }\n changes.push({\n type: \"relationship-add\",\n elementId: key,\n layerName: source.layer,\n after: {\n source: triple.source,\n target: triple.target,\n predicate: triple.predicate,\n layer: source.layer,\n ...(target.layer !== source.layer ? { targetLayer: target.layer } : {}),\n category: \"structural\",\n ...(item.properties ? { properties: item.properties } : {}),\n },\n });\n results.push({ ...resolved, status: addStatus });\n });\n\n return { changes, unstageKeys, results };\n}\n\n/** Counts of a batch's results by status. */\nexport function countByStatus(results: Array<{ status: string }>): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1;\n return counts;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -200,6 +200,59 @@ To resolve:
200
200
  4. Retry the operation`);
201
201
  }
202
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}
203
256
  Changeset path: ${changesetPath}`);
204
257
  }
205
258
  }
@@ -211,14 +264,25 @@ Changeset path: ${changesetPath}`);
211
264
  * @throws {Error} If lock acquisition, changeset load, save, or lock release fails
212
265
  */
213
266
  async removeChange(id, elementId) {
214
- 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);
215
279
  try {
216
280
  await lock.withLock(async () => {
217
281
  const changeset = await this.load(id);
218
282
  if (!changeset)
219
283
  throw new Error(`Changeset '${id}' not found`);
220
- const numbered = withSequenceNumbers(changeset.changes), removed = numbered.filter((c) => c.elementId === elementId);
221
- if (changeset.changes = numbered.filter((c) => c.elementId !== elementId), removed.length > 0) {
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) {
222
286
  const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });
223
287
  changeset.lastSequence = removedAt, addTombstones(changeset, removed.map((c, index) => ({
224
288
  sequence: sequenceOf(c, index),
@@ -301,6 +365,22 @@ Changeset path: ${changesetPath}`);
301
365
  /**
302
366
  * Get the storage path for a changeset
303
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
+ }
304
384
  getChangesetPath(id) {
305
385
  const sanitizedId = this.sanitizeId(id);
306
386
  return path.join(this.changesetsDir, sanitizedId);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/staged-changeset-storage.ts"],
4
- "sourcesContent": ["/**\n * StagedChangesetStorage - Handles YAML-based storage for staged changesets\n *\n * Manages the new storage structure:\n * documentation-robotics/changesets/{changeset-id}/\n * \u251C\u2500\u2500 metadata.yaml # Name, description, dates, status, base snapshot\n * \u2514\u2500\u2500 changes.yaml # Delta-only change records\n */\n\nimport { readFile, writeFile, readdir, rm } from \"fs/promises\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport ansis from \"ansis\";\nimport type { StagedChangesetData, StagedChange, UnstagedChange } from \"./changeset.js\";\nimport { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from \"./changeset.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * How many unstaged changes a changeset keeps as tombstones. Older ones are dropped (and\n * `unstagedPrunedThrough` records how far), so a delta reaching back past them says it is\n * approximate.\n */\nexport const MAX_UNSTAGED_TOMBSTONES = 200;\n\n/**\n * Reads the tombstone list from metadata.yaml, keeping the well-formed entries. Malformed entries\n * are reported, not silently dropped: the changeset still loads, and its changes are untouched.\n */\nfunction readTombstones(value: unknown, id: string): UnstagedChange[] | undefined {\n if (value === undefined || value === null) return undefined;\n if (!Array.isArray(value)) {\n console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));\n return undefined;\n }\n const valid = value.filter(\n (t): t is UnstagedChange =>\n !!t &&\n typeof t === \"object\" &&\n typeof t.sequence === \"number\" &&\n typeof t.removedAt === \"number\" &&\n !!t.change &&\n typeof t.change === \"object\" &&\n typeof t.change.type === \"string\" &&\n typeof t.change.elementId === \"string\"\n );\n if (valid.length < value.length) {\n console.warn(\n ansis.yellow(\n `\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`\n )\n );\n }\n return valid;\n}\n\n/** Appends tombstones, keeping the newest MAX_UNSTAGED_TOMBSTONES and recording how far it trimmed. */\nfunction addTombstones(changeset: Changeset, tombstones: UnstagedChange[]): void {\n changeset.unstaged = [...changeset.unstaged, ...tombstones];\n if (changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {\n const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);\n changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES);\n changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));\n }\n}\n\n/**\n * Storage manager for staged changesets using YAML format\n */\nexport class StagedChangesetStorage {\n private changesetsDir: string;\n\n constructor(rootPath: string) {\n // Store changesets in documentation-robotics/changesets/ directory\n this.changesetsDir = path.join(rootPath, \"documentation-robotics\", \"changesets\");\n }\n\n /**\n * Create a new staged changeset directory structure\n */\n async create(\n id: string,\n name: string,\n description: string | undefined,\n baseSnapshot: string\n ): Promise<Changeset> {\n const now = new Date().toISOString();\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n // Create directory structure\n await ensureDir(changesetPath);\n\n // Create metadata\n const metadata: Omit<StagedChangesetData, \"changes\"> = {\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n stats: {\n additions: 0,\n modifications: 0,\n deletions: 0,\n },\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write empty changes.yaml\n await writeFile(path.join(changesetPath, \"changes.yaml\"), yaml.stringify([]), \"utf-8\");\n\n // Return constructed changeset object\n return new Changeset({\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n changes: [],\n stats: metadata.stats,\n });\n }\n\n /**\n * Load a staged changeset from YAML files\n */\n async load(id: string): Promise<Changeset | null> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n const metadataPath = path.join(changesetPath, \"metadata.yaml\");\n const changesPath = path.join(changesetPath, \"changes.yaml\");\n\n // Check if changeset directory exists\n if (!(await fileExists(changesetPath))) {\n return null; // Changeset doesn't exist - this is OK\n }\n\n // Changeset directory exists - files SHOULD be present\n const metadataExists = await fileExists(metadataPath);\n const changesExists = await fileExists(changesPath);\n\n if (!metadataExists || !changesExists) {\n // Changeset directory exists but required files are missing - this is corruption\n throw new Error(\n `Changeset '${id}' is corrupted (missing ${!metadataExists ? \"metadata.yaml\" : \"changes.yaml\"}). ` +\n `Location: ${changesetPath}\\n` +\n `This may indicate disk failure or incomplete write. Check disk health and restore from backup if available.`\n );\n }\n\n try {\n // Load metadata\n const metadataContent = await readFile(metadataPath, \"utf-8\");\n const metadata = yaml.parse(metadataContent);\n\n // Load changes\n const changesContent = await readFile(changesPath, \"utf-8\");\n const changes = yaml.parse(changesContent) || [];\n\n // Construct changeset object\n const data: StagedChangesetData = {\n id: metadata.id,\n name: metadata.name,\n description: metadata.description,\n created: metadata.created,\n modified: metadata.modified,\n status: metadata.status,\n baseSnapshot: metadata.baseSnapshot,\n changes: changes as StagedChange[],\n stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },\n ...(typeof metadata.lastSequence === \"number\" ? { lastSequence: metadata.lastSequence } : {}),\n ...(typeof metadata.unstagedPrunedThrough === \"number\"\n ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough }\n : {}),\n };\n const unstaged = readTombstones(metadata.unstaged, id);\n if (unstaged) data.unstaged = unstaged;\n\n return Changeset.fromJSON(data);\n } catch (error) {\n // YAML parsing or file read error - provide detailed guidance\n throw new Error(\n `Failed to load changeset '${id}': YAML parsing failed.\\n` +\n `Error: ${getErrorMessage(error)}\\n` +\n `Location: ${metadataPath}\\n` +\n `This indicates file corruption. Try:\\n` +\n ` 1. Validate YAML syntax with: yamllint ${metadataPath}\\n` +\n ` 2. Restore from backup if available\\n` +\n ` 3. Manually edit to fix syntax errors`\n );\n }\n }\n\n /**\n * Save a staged changeset to YAML files\n */\n async save(changeset: Changeset): Promise<void> {\n if (!changeset.id) {\n throw new Error(\"Changeset must have an id to be saved in YAML format\");\n }\n\n const sanitizedId = this.sanitizeId(changeset.id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n await ensureDir(changesetPath);\n\n // A change staged in memory without a number gets the next unused one (never one an unstaged\n // change or unstage event took); lastSequence then covers it.\n const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const lastSequence =\n changeset.lastSequence !== undefined\n ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber))\n : undefined;\n\n // Prepare metadata (all fields except changes)\n const metadata = {\n id: changeset.id,\n name: changeset.name,\n description: changeset.description,\n created: changeset.created,\n modified: changeset.modified,\n status: changeset.status,\n baseSnapshot: changeset.baseSnapshot,\n stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },\n // Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.\n ...(lastSequence !== undefined ? { lastSequence } : {}),\n ...(changeset.unstagedPrunedThrough !== undefined\n ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough }\n : {}),\n ...(changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}),\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write changes.yaml\n await writeFile(\n path.join(changesetPath, \"changes.yaml\"),\n yaml.stringify(numbered),\n \"utf-8\"\n );\n }\n\n /**\n * List all staged changesets\n */\n async list(): Promise<Changeset[]> {\n if (!(await fileExists(this.changesetsDir))) {\n return [];\n }\n\n try {\n const entries = await readdir(this.changesetsDir, { withFileTypes: true });\n const changesets: Changeset[] = [];\n let skippedCount = 0;\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n try {\n const changeset = await this.load(entry.name);\n if (changeset) {\n changesets.push(changeset);\n } else {\n // load() returned null - directory exists but no valid changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Changeset directory '${entry.name}' exists but could not be loaded (may be corrupted)`\n )\n );\n skippedCount++;\n }\n } catch (loadError) {\n // load() threw an error - this is a corrupted changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Failed to load changeset '${entry.name}': ${loadError instanceof Error ? loadError.message : String(loadError)}`\n )\n );\n skippedCount++;\n }\n }\n }\n\n // Show summary if any were skipped\n if (skippedCount > 0) {\n console.warn(\n ansis.yellow(`\\n\u26A0 ${skippedCount} changeset(s) could not be loaded and were skipped.`) +\n ansis.dim(`\\nRun 'dr changeset validate' to check for corruption and repair options.`)\n );\n }\n\n return changesets;\n } catch (error) {\n throw new Error(\n `Failed to list changesets: ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Delete a staged changeset\n */\n async delete(id: string): Promise<void> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n if (!(await fileExists(changesetPath))) {\n // List available changesets to help user\n const available = await this.list();\n const availableIds = available.map((cs) => cs.id).join(\", \");\n\n throw new Error(\n `Changeset '${id}' not found.\\n` +\n `Available changesets: ${availableIds || \"(none)\"}\\n` +\n `Run 'dr changeset list' to see all changesets.`\n );\n }\n\n try {\n await rm(changesetPath, { recursive: true, force: true });\n } catch (error) {\n throw new Error(\n `Failed to delete changeset '${id}': ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Add or update a change in the changeset's changes.yaml\n *\n * Uses file locking to prevent concurrent modification race conditions\n * Assigns sequence number atomically inside the lock to prevent duplicates\n *\n * @returns The assigned sequence number for the change\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async addChange(id: string, change: Omit<StagedChange, \"sequenceNumber\">): Promise<number> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n return await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Assign sequence number atomically inside the lock\n // This prevents race conditions when multiple concurrent operations\n // try to add changes at the same time. One past every number used so far (unstaged\n // changes and unstage events included), so a number is never reused and a revision\n // always names the same prefix of the plan.\n const sequenceNumber = nextSequenceNumber(changeset);\n const stagedChange: StagedChange = {\n ...change,\n sequenceNumber,\n };\n\n changeset.changes = withSequenceNumbers(changeset.changes);\n changeset.changes.push(stagedChange);\n changeset.lastSequence = sequenceNumber;\n changeset.updateModified();\n\n await this.save(changeset);\n\n return sequenceNumber;\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to add change to changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to add change to changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Remove a change by element ID\n *\n * Uses file locking to prevent concurrent modification race conditions\n *\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async removeChange(id: string, elementId: string): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Number any change that carries none by its position first, so the ones that stay keep\n // the numbers they had. Remaining changes are not renumbered.\n const numbered = withSequenceNumbers(changeset.changes);\n const removed = numbered.filter((c) => c.elementId === elementId);\n changeset.changes = numbered.filter((c) => c.elementId !== elementId);\n\n if (removed.length > 0) {\n // The unstage is a revision of its own: it takes the next sequence number, and each\n // removed change is kept as a tombstone so the plan before it can be rebuilt.\n const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });\n changeset.lastSequence = removedAt;\n addTombstones(\n changeset,\n removed.map((c, index) => ({\n sequence: sequenceOf(c, index),\n removedAt,\n change: compactChange(c),\n }))\n );\n }\n\n changeset.updateModified();\n\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to remove change from changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to remove change from changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Update an existing change in-place within the changeset's changes.yaml\n *\n * Atomically replaces the change for the given elementId under a single file lock,\n * preventing data loss if the process is interrupted between a remove and add.\n * The original sequence number is preserved so commit-ordering is unaffected.\n *\n * @throws {Error} If changeset not found, elementId not found in changes, or I/O error\n */\n async updateChange(\n id: string,\n elementId: string,\n updated: Omit<StagedChange, \"sequenceNumber\">\n ): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n const index = changeset.changes.findIndex((c) => c.elementId === elementId);\n if (index === -1) {\n throw new Error(`No change for element '${elementId}' in changeset '${id}'`);\n }\n\n // Number every change first (an older changes.yaml may carry none), so the list is never\n // left half numbered. Spread the existing entry so runtime fields like sequenceNumber and\n // timestamp are preserved; `updated` omits sequenceNumber and cannot overwrite it.\n changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const existing = changeset.changes[index] as StagedChange;\n // The rewrite is a revision of its own, like an unstage: it takes the next sequence number,\n // and the version the change had is kept as a tombstone so the plan before it can be rebuilt.\n const rewrittenAt = nextSequenceNumber(changeset);\n changeset.lastSequence = rewrittenAt;\n addTombstones(changeset, [\n { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: true },\n ]);\n changeset.changes[index] = {\n ...existing,\n ...updated,\n sequenceNumber: existing.sequenceNumber,\n } as StagedChange;\n\n changeset.updateModified();\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n throw new Error(\n `Failed to update change in changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Change a changeset's name and/or description in place, under the changeset's file lock so a\n * concurrent stage cannot be lost. Staged changes are untouched.\n *\n * @returns The updated changeset\n * @throws {Error} If the changeset is not found, or on lock or I/O failure\n */\n async updateMetadata(\n id: string,\n patch: { name?: string; description?: string }\n ): Promise<Changeset> {\n const lock = new FileLock(this.getChangesetPath(id));\n return lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n if (patch.name !== undefined) changeset.name = patch.name;\n if (patch.description !== undefined) changeset.description = patch.description || undefined;\n changeset.updateModified();\n await this.save(changeset);\n return changeset;\n });\n }\n\n /**\n * Get the storage path for a changeset\n */\n getChangesetPath(id: string): string {\n const sanitizedId = this.sanitizeId(id);\n return path.join(this.changesetsDir, sanitizedId);\n }\n\n /**\n * Get the root changesets directory\n */\n getChangesetsDir(): string {\n return this.changesetsDir;\n }\n\n /**\n * Sanitize changeset ID for safe use in file paths\n * Prevents path traversal by removing special characters and path separators\n */\n private sanitizeId(id: string): string {\n const sanitized = id\n .replace(/\\.\\./g, \"\") // Remove .. (parent directory)\n .replace(/^\\/+/, \"\") // Remove leading slashes\n .replace(/^[A-Z]:\\\\/g, \"\") // Remove Windows drive letters\n .toLowerCase()\n .replace(/\\s+/g, \"-\") // Replace spaces with hyphens\n .replace(/[^a-z0-9-]/g, \"\"); // Keep only alphanumeric and hyphens\n\n if (!sanitized) {\n throw new Error(\n `Invalid changeset ID '${id}': results in empty string after sanitization. ` +\n `ID must contain at least one alphanumeric character.`\n );\n }\n\n return sanitized;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * StagedChangesetStorage - Handles YAML-based storage for staged changesets\n *\n * Manages the new storage structure:\n * documentation-robotics/changesets/{changeset-id}/\n * \u251C\u2500\u2500 metadata.yaml # Name, description, dates, status, base snapshot\n * \u2514\u2500\u2500 changes.yaml # Delta-only change records\n */\n\nimport { readFile, writeFile, readdir, rm } from \"fs/promises\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport ansis from \"ansis\";\nimport type { StagedChangesetData, StagedChange, UnstagedChange } from \"./changeset.js\";\nimport { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from \"./changeset.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * How many unstaged changes a changeset keeps as tombstones. Older ones are dropped (and\n * `unstagedPrunedThrough` records how far), so a delta reaching back past them says it is\n * approximate.\n */\nexport const MAX_UNSTAGED_TOMBSTONES = 200;\n\n/**\n * Reads the tombstone list from metadata.yaml, keeping the well-formed entries. Malformed entries\n * are reported, not silently dropped: the changeset still loads, and its changes are untouched.\n */\nfunction readTombstones(value: unknown, id: string): UnstagedChange[] | undefined {\n if (value === undefined || value === null) return undefined;\n if (!Array.isArray(value)) {\n console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));\n return undefined;\n }\n const valid = value.filter(\n (t): t is UnstagedChange =>\n !!t &&\n typeof t === \"object\" &&\n typeof t.sequence === \"number\" &&\n typeof t.removedAt === \"number\" &&\n !!t.change &&\n typeof t.change === \"object\" &&\n typeof t.change.type === \"string\" &&\n typeof t.change.elementId === \"string\"\n );\n if (valid.length < value.length) {\n console.warn(\n ansis.yellow(\n `\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`\n )\n );\n }\n return valid;\n}\n\n/** Appends tombstones, keeping the newest MAX_UNSTAGED_TOMBSTONES and recording how far it trimmed. */\nfunction addTombstones(changeset: Changeset, tombstones: UnstagedChange[]): void {\n changeset.unstaged = [...changeset.unstaged, ...tombstones];\n if (changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {\n const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);\n changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES);\n changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));\n }\n}\n\n/**\n * Storage manager for staged changesets using YAML format\n */\nexport class StagedChangesetStorage {\n private changesetsDir: string;\n\n constructor(rootPath: string) {\n // Store changesets in documentation-robotics/changesets/ directory\n this.changesetsDir = path.join(rootPath, \"documentation-robotics\", \"changesets\");\n }\n\n /**\n * Create a new staged changeset directory structure\n */\n async create(\n id: string,\n name: string,\n description: string | undefined,\n baseSnapshot: string\n ): Promise<Changeset> {\n const now = new Date().toISOString();\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n // Create directory structure\n await ensureDir(changesetPath);\n\n // Create metadata\n const metadata: Omit<StagedChangesetData, \"changes\"> = {\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n stats: {\n additions: 0,\n modifications: 0,\n deletions: 0,\n },\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write empty changes.yaml\n await writeFile(path.join(changesetPath, \"changes.yaml\"), yaml.stringify([]), \"utf-8\");\n\n // Return constructed changeset object\n return new Changeset({\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n changes: [],\n stats: metadata.stats,\n });\n }\n\n /**\n * Load a staged changeset from YAML files\n */\n async load(id: string): Promise<Changeset | null> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n const metadataPath = path.join(changesetPath, \"metadata.yaml\");\n const changesPath = path.join(changesetPath, \"changes.yaml\");\n\n // Check if changeset directory exists\n if (!(await fileExists(changesetPath))) {\n return null; // Changeset doesn't exist - this is OK\n }\n\n // Changeset directory exists - files SHOULD be present\n const metadataExists = await fileExists(metadataPath);\n const changesExists = await fileExists(changesPath);\n\n if (!metadataExists || !changesExists) {\n // Changeset directory exists but required files are missing - this is corruption\n throw new Error(\n `Changeset '${id}' is corrupted (missing ${!metadataExists ? \"metadata.yaml\" : \"changes.yaml\"}). ` +\n `Location: ${changesetPath}\\n` +\n `This may indicate disk failure or incomplete write. Check disk health and restore from backup if available.`\n );\n }\n\n try {\n // Load metadata\n const metadataContent = await readFile(metadataPath, \"utf-8\");\n const metadata = yaml.parse(metadataContent);\n\n // Load changes\n const changesContent = await readFile(changesPath, \"utf-8\");\n const changes = yaml.parse(changesContent) || [];\n\n // Construct changeset object\n const data: StagedChangesetData = {\n id: metadata.id,\n name: metadata.name,\n description: metadata.description,\n created: metadata.created,\n modified: metadata.modified,\n status: metadata.status,\n baseSnapshot: metadata.baseSnapshot,\n changes: changes as StagedChange[],\n stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },\n ...(typeof metadata.lastSequence === \"number\" ? { lastSequence: metadata.lastSequence } : {}),\n ...(typeof metadata.unstagedPrunedThrough === \"number\"\n ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough }\n : {}),\n };\n const unstaged = readTombstones(metadata.unstaged, id);\n if (unstaged) data.unstaged = unstaged;\n\n return Changeset.fromJSON(data);\n } catch (error) {\n // YAML parsing or file read error - provide detailed guidance\n throw new Error(\n `Failed to load changeset '${id}': YAML parsing failed.\\n` +\n `Error: ${getErrorMessage(error)}\\n` +\n `Location: ${metadataPath}\\n` +\n `This indicates file corruption. Try:\\n` +\n ` 1. Validate YAML syntax with: yamllint ${metadataPath}\\n` +\n ` 2. Restore from backup if available\\n` +\n ` 3. Manually edit to fix syntax errors`\n );\n }\n }\n\n /**\n * Save a staged changeset to YAML files\n */\n async save(changeset: Changeset): Promise<void> {\n if (!changeset.id) {\n throw new Error(\"Changeset must have an id to be saved in YAML format\");\n }\n\n const sanitizedId = this.sanitizeId(changeset.id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n await ensureDir(changesetPath);\n\n // A change staged in memory without a number gets the next unused one (never one an unstaged\n // change or unstage event took); lastSequence then covers it.\n const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const lastSequence =\n changeset.lastSequence !== undefined\n ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber))\n : undefined;\n\n // Prepare metadata (all fields except changes)\n const metadata = {\n id: changeset.id,\n name: changeset.name,\n description: changeset.description,\n created: changeset.created,\n modified: changeset.modified,\n status: changeset.status,\n baseSnapshot: changeset.baseSnapshot,\n stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },\n // Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.\n ...(lastSequence !== undefined ? { lastSequence } : {}),\n ...(changeset.unstagedPrunedThrough !== undefined\n ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough }\n : {}),\n ...(changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}),\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write changes.yaml\n await writeFile(\n path.join(changesetPath, \"changes.yaml\"),\n yaml.stringify(numbered),\n \"utf-8\"\n );\n }\n\n /**\n * List all staged changesets\n */\n async list(): Promise<Changeset[]> {\n if (!(await fileExists(this.changesetsDir))) {\n return [];\n }\n\n try {\n const entries = await readdir(this.changesetsDir, { withFileTypes: true });\n const changesets: Changeset[] = [];\n let skippedCount = 0;\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n try {\n const changeset = await this.load(entry.name);\n if (changeset) {\n changesets.push(changeset);\n } else {\n // load() returned null - directory exists but no valid changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Changeset directory '${entry.name}' exists but could not be loaded (may be corrupted)`\n )\n );\n skippedCount++;\n }\n } catch (loadError) {\n // load() threw an error - this is a corrupted changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Failed to load changeset '${entry.name}': ${loadError instanceof Error ? loadError.message : String(loadError)}`\n )\n );\n skippedCount++;\n }\n }\n }\n\n // Show summary if any were skipped\n if (skippedCount > 0) {\n console.warn(\n ansis.yellow(`\\n\u26A0 ${skippedCount} changeset(s) could not be loaded and were skipped.`) +\n ansis.dim(`\\nRun 'dr changeset validate' to check for corruption and repair options.`)\n );\n }\n\n return changesets;\n } catch (error) {\n throw new Error(\n `Failed to list changesets: ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Delete a staged changeset\n */\n async delete(id: string): Promise<void> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n if (!(await fileExists(changesetPath))) {\n // List available changesets to help user\n const available = await this.list();\n const availableIds = available.map((cs) => cs.id).join(\", \");\n\n throw new Error(\n `Changeset '${id}' not found.\\n` +\n `Available changesets: ${availableIds || \"(none)\"}\\n` +\n `Run 'dr changeset list' to see all changesets.`\n );\n }\n\n try {\n await rm(changesetPath, { recursive: true, force: true });\n } catch (error) {\n throw new Error(\n `Failed to delete changeset '${id}': ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Add or update a change in the changeset's changes.yaml\n *\n * Uses file locking to prevent concurrent modification race conditions\n * Assigns sequence number atomically inside the lock to prevent duplicates\n *\n * @returns The assigned sequence number for the change\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async addChange(id: string, change: Omit<StagedChange, \"sequenceNumber\">): Promise<number> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n return await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Assign sequence number atomically inside the lock\n // This prevents race conditions when multiple concurrent operations\n // try to add changes at the same time. One past every number used so far (unstaged\n // changes and unstage events included), so a number is never reused and a revision\n // always names the same prefix of the plan.\n const sequenceNumber = nextSequenceNumber(changeset);\n const stagedChange: StagedChange = {\n ...change,\n sequenceNumber,\n };\n\n changeset.changes = withSequenceNumbers(changeset.changes);\n changeset.changes.push(stagedChange);\n changeset.lastSequence = sequenceNumber;\n changeset.updateModified();\n\n await this.save(changeset);\n\n return sequenceNumber;\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to add change to changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to add change to changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Add several changes, and rewrite some staged ones in place, in one write: one load and one save\n * instead of one per change. Additions take the next sequence numbers in order; a rewrite keeps\n * its change's number and leaves a tombstone, as {@link updateChange} does.\n *\n * @returns The sequence numbers assigned to the additions, in order\n */\n async addChanges(\n id: string,\n changes: Array<Omit<StagedChange, \"sequenceNumber\">>,\n rewrites: Array<{ elementId: string; change: Omit<StagedChange, \"sequenceNumber\"> }> = []\n ): Promise<number[]> {\n if (changes.length === 0 && rewrites.length === 0) return [];\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n try {\n return await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n for (const { elementId, change } of rewrites) {\n const index = changeset.changes.findIndex((c) => c.elementId === elementId);\n if (index === -1) {\n throw new Error(`No change for element '${elementId}' in changeset '${id}'`);\n }\n const existing = changeset.changes[index] as StagedChange;\n const rewrittenAt = nextSequenceNumber(changeset);\n changeset.lastSequence = rewrittenAt;\n addTombstones(changeset, [\n { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: true },\n ]);\n changeset.changes[index] = { ...existing, ...change, sequenceNumber: existing.sequenceNumber } as StagedChange;\n }\n const numbers: number[] = [];\n for (const change of changes) {\n const sequenceNumber = nextSequenceNumber(changeset);\n const staged: StagedChange = { ...change, sequenceNumber };\n changeset.changes.push(staged);\n changeset.lastSequence = sequenceNumber;\n numbers.push(sequenceNumber);\n }\n changeset.updateModified();\n await this.save(changeset);\n return numbers;\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n throw new Error(\n `Failed to add changes to changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n throw new Error(`Failed to add changes to changeset '${id}': ${errorMessage}\\n` + `Changeset path: ${changesetPath}`);\n }\n }\n\n /**\n * Remove a change by element ID\n *\n * Uses file locking to prevent concurrent modification race conditions\n *\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async removeChange(id: string, elementId: string): Promise<void> {\n await this.removeChanges(id, [elementId]);\n }\n\n /**\n * Remove every change to any of `elementIds` in one write. The removal is one revision: it takes\n * one sequence number, and each removed change is kept as a tombstone.\n *\n * Uses file locking to prevent concurrent modification race conditions\n *\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async removeChanges(id: string, elementIds: string[]): Promise<void> {\n const keys = new Set(elementIds);\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Number any change that carries none by its position first, so the ones that stay keep\n // the numbers they had. Remaining changes are not renumbered.\n const numbered = withSequenceNumbers(changeset.changes);\n const removed = numbered.filter((c) => keys.has(c.elementId));\n changeset.changes = numbered.filter((c) => !keys.has(c.elementId));\n\n if (removed.length > 0) {\n // The unstage is a revision of its own: it takes the next sequence number, and each\n // removed change is kept as a tombstone so the plan before it can be rebuilt.\n const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });\n changeset.lastSequence = removedAt;\n addTombstones(\n changeset,\n removed.map((c, index) => ({\n sequence: sequenceOf(c, index),\n removedAt,\n change: compactChange(c),\n }))\n );\n }\n\n changeset.updateModified();\n\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to remove change from changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to remove change from changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Update an existing change in-place within the changeset's changes.yaml\n *\n * Atomically replaces the change for the given elementId under a single file lock,\n * preventing data loss if the process is interrupted between a remove and add.\n * The original sequence number is preserved so commit-ordering is unaffected.\n *\n * @throws {Error} If changeset not found, elementId not found in changes, or I/O error\n */\n async updateChange(\n id: string,\n elementId: string,\n updated: Omit<StagedChange, \"sequenceNumber\">\n ): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n const index = changeset.changes.findIndex((c) => c.elementId === elementId);\n if (index === -1) {\n throw new Error(`No change for element '${elementId}' in changeset '${id}'`);\n }\n\n // Number every change first (an older changes.yaml may carry none), so the list is never\n // left half numbered. Spread the existing entry so runtime fields like sequenceNumber and\n // timestamp are preserved; `updated` omits sequenceNumber and cannot overwrite it.\n changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const existing = changeset.changes[index] as StagedChange;\n // The rewrite is a revision of its own, like an unstage: it takes the next sequence number,\n // and the version the change had is kept as a tombstone so the plan before it can be rebuilt.\n const rewrittenAt = nextSequenceNumber(changeset);\n changeset.lastSequence = rewrittenAt;\n addTombstones(changeset, [\n { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: true },\n ]);\n changeset.changes[index] = {\n ...existing,\n ...updated,\n sequenceNumber: existing.sequenceNumber,\n } as StagedChange;\n\n changeset.updateModified();\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n throw new Error(\n `Failed to update change in changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Change a changeset's name and/or description in place, under the changeset's file lock so a\n * concurrent stage cannot be lost. Staged changes are untouched.\n *\n * @returns The updated changeset\n * @throws {Error} If the changeset is not found, or on lock or I/O failure\n */\n async updateMetadata(\n id: string,\n patch: { name?: string; description?: string }\n ): Promise<Changeset> {\n const lock = new FileLock(this.getChangesetPath(id));\n return lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n if (patch.name !== undefined) changeset.name = patch.name;\n if (patch.description !== undefined) changeset.description = patch.description || undefined;\n changeset.updateModified();\n await this.save(changeset);\n return changeset;\n });\n }\n\n /**\n * Get the storage path for a changeset\n */\n /**\n * Remove every file in the changeset's directory except metadata.yaml and changes.yaml (and\n * dotfiles such as a lock): e.g. the base index, once the changeset is discarded and nothing\n * reads it again.\n *\n * @returns The names of the files removed\n */\n async removeSideFiles(id: string): Promise<string[]> {\n const changesetPath = this.getChangesetPath(id);\n if (!(await fileExists(changesetPath))) return [];\n const keep = new Set([\"metadata.yaml\", \"changes.yaml\"]);\n const removed: string[] = [];\n for (const entry of await readdir(changesetPath, { withFileTypes: true })) {\n if (keep.has(entry.name) || entry.name.startsWith(\".\") || !entry.isFile()) continue;\n await rm(path.join(changesetPath, entry.name), { force: true });\n removed.push(entry.name);\n }\n return removed;\n }\n\n getChangesetPath(id: string): string {\n const sanitizedId = this.sanitizeId(id);\n return path.join(this.changesetsDir, sanitizedId);\n }\n\n /**\n * Get the root changesets directory\n */\n getChangesetsDir(): string {\n return this.changesetsDir;\n }\n\n /**\n * Sanitize changeset ID for safe use in file paths\n * Prevents path traversal by removing special characters and path separators\n */\n private sanitizeId(id: string): string {\n const sanitized = id\n .replace(/\\.\\./g, \"\") // Remove .. (parent directory)\n .replace(/^\\/+/, \"\") // Remove leading slashes\n .replace(/^[A-Z]:\\\\/g, \"\") // Remove Windows drive letters\n .toLowerCase()\n .replace(/\\s+/g, \"-\") // Replace spaces with hyphens\n .replace(/[^a-z0-9-]/g, \"\"); // Keep only alphanumeric and hyphens\n\n if (!sanitized) {\n throw new Error(\n `Invalid changeset ID '${id}': results in empty string after sanitization. ` +\n `ID must contain at least one alphanumeric character.`\n );\n }\n\n return sanitized;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,7 +9,7 @@ import { createSha256Hash } from "../types/index.js";
9
9
  import { getErrorMessage } from "../utils/errors.js";
10
10
  import { Layer } from "./layer.js";
11
11
  import { canonicalStagedSnapshot } from "./staged-source-reference.js";
12
- import { elementIdentifiers } from "./relationships.js";
12
+ import { elementIdentifiers, Relationships } from "./relationships.js";
13
13
  import { isValidLayerName } from "./layers.js";
14
14
  import { isTelemetryEnabled, startSpan, startActiveSpan, endSpan, emitLog, SeverityNumber } from "../telemetry/index.js";
15
15
  import { ModelReportOrchestrator } from "../reports/model-report-orchestrator.js";
@@ -147,6 +147,28 @@ class StagingAreaManager {
147
147
  };
148
148
  await this.storage.addChange(changesetId, stagedChange), !change.type.startsWith("relationship-") && typeof this.model?.noteStagedLayer == "function" && this.model.noteStagedLayer(changesetId, change.layerName), (await this.getProjectionEngine()).invalidateOnStage(changesetId, change.layerName);
149
149
  }
150
+ /**
151
+ * Stage several changes in one write, in order (see {@link stage}), and rewrite staged ones in
152
+ * place (see {@link restage}) in the same write. The changeset must be in 'staged' status.
153
+ */
154
+ async stageMany(changesetId, changes, rewrites = []) {
155
+ if (changes.length === 0 && rewrites.length === 0)
156
+ return;
157
+ this.validateChangesetId(changesetId);
158
+ const changeset = await this.load(changesetId);
159
+ if (!changeset)
160
+ throw new Error(`Changeset '${changesetId}' not found`);
161
+ if (changeset.status !== "staged")
162
+ throw new Error(`Cannot stage changes on changeset with status '${changeset.status}'. Changeset must have status 'staged' to accept new changes.`);
163
+ const now = (/* @__PURE__ */ new Date()).toISOString();
164
+ await this.storage.addChanges(changesetId, changes.map((change) => ({ ...change, timestamp: change.timestamp || now })), rewrites.map((r) => ({ elementId: r.elementId, change: { ...r.change, timestamp: r.change.timestamp || now } })));
165
+ const engine = await this.getProjectionEngine();
166
+ for (const layerName of new Set([...changes, ...rewrites.map((r) => r.change)].map((c) => c.layerName)))
167
+ engine.invalidateOnStage(changesetId, layerName);
168
+ if (typeof this.model?.noteStagedLayer == "function")
169
+ for (const change of changes)
170
+ change.type.startsWith("relationship-") || this.model.noteStagedLayer(changesetId, change.layerName);
171
+ }
150
172
  /**
151
173
  * Rewrite a staged change in place, keeping its sequence number: e.g. to fold an update of an
152
174
  * element into the changeset's add of it. The changeset must be in 'staged' status.
@@ -184,6 +206,27 @@ class StagingAreaManager {
184
206
  throw new Error(`Cannot unstage changes on changeset with status '${changeset.status}'. Changeset must have status 'staged'.`);
185
207
  await this.storage.removeChange(changesetId, elementId), await (await this.getProjectionEngine()).invalidateOnUnstage(changesetId, elementId);
186
208
  }
209
+ /**
210
+ * Unstage several elements' changes in one write (one revision), e.g. a batch unstage.
211
+ * The changeset must be in 'staged' status.
212
+ *
213
+ * @param changesetId - ID of the changeset to unstage from
214
+ * @param elementIds - change keys (element IDs, or relationship change keys) to remove
215
+ * @throws Error if changeset ID is invalid, changeset not found, or not in 'staged' status
216
+ */
217
+ async unstageMany(changesetId, elementIds) {
218
+ if (elementIds.length === 0)
219
+ return;
220
+ if (elementIds.length === 1)
221
+ return this.unstage(changesetId, elementIds[0]);
222
+ this.validateChangesetId(changesetId);
223
+ const changeset = await this.load(changesetId);
224
+ if (!changeset)
225
+ throw new Error(`Changeset '${changesetId}' not found`);
226
+ if (changeset.status !== "staged")
227
+ throw new Error(`Cannot unstage changes on changeset with status '${changeset.status}'. Changeset must have status 'staged'.`);
228
+ await this.storage.removeChanges(changesetId, elementIds), await (await this.getProjectionEngine()).invalidateOnUnstage(changesetId);
229
+ }
187
230
  /**
188
231
  * Discard all changes in the changeset.
189
232
  * Clears all staged changes and marks the changeset as 'discarded'.
@@ -197,7 +240,7 @@ class StagingAreaManager {
197
240
  const changeset = await this.load(changesetId);
198
241
  if (!changeset)
199
242
  throw new Error(`Changeset '${changesetId}' not found`);
200
- changeset.changes = [], changeset.unstaged = [], changeset.status = "discarded", changeset.updateModified(), await this.storage.save(changeset), (await this.getProjectionEngine()).invalidateOnDiscard(changesetId);
243
+ changeset.changes = [], changeset.unstaged = [], changeset.status = "discarded", changeset.updateModified(), await this.storage.save(changeset), await this.storage.removeSideFiles(changesetId), (await this.getProjectionEngine()).invalidateOnDiscard(changesetId);
201
244
  }
202
245
  /**
203
246
  * Capture a base snapshot of the current model state for drift detection.
@@ -340,6 +383,16 @@ Fix these validation errors before committing, or skip validation with --no-vali
340
383
  }
341
384
  const sortedChanges = withSequenceNumbers(changeset.changes).sort((a, b) => a.sequenceNumber - b.sequenceNumber);
342
385
  let relationshipsPurged = !1;
386
+ model.getActiveChangesetId() !== null && (model.relationships = new Relationships(model.getBaseRelationships()));
387
+ let preCommitReportLayers = /* @__PURE__ */ new Set();
388
+ try {
389
+ preCommitReportLayers = reportLayersForChanges(new ModelReportOrchestrator(model, model.rootPath), model, sortedChanges);
390
+ } catch (error) {
391
+ emitLog(SeverityNumber.WARN, "Could not read report layers before changeset commit", {
392
+ "changeset.id": changesetId,
393
+ "error.message": getErrorMessage(error)
394
+ });
395
+ }
343
396
  try {
344
397
  const applySpan = isTelemetryEnabled ? startSpan("staging.apply", {
345
398
  "apply.changeCount": changeset.changes.length
@@ -434,12 +487,10 @@ Fix these validation errors before committing, or skip validation with --no-vali
434
487
  throw new Error(`Failed to save manifest with changeset history: ${manifestError instanceof Error ? manifestError.message : String(manifestError)}. Model will be rolled back to maintain consistency.`);
435
488
  }
436
489
  try {
437
- const orchestrator = new ModelReportOrchestrator(model, model.rootPath), affectedLayers = /* @__PURE__ */ new Set();
438
- for (const change of sortedChanges)
439
- if (isValidLayerName(change.layerName))
440
- for (const layer of orchestrator.computeAffectedLayers(change.layerName))
441
- affectedLayers.add(layer);
442
- await orchestrator.regenerate(affectedLayers);
490
+ const orchestrator = new ModelReportOrchestrator(model, model.rootPath), reportLayers = reportLayersForChanges(orchestrator, model, sortedChanges);
491
+ for (const layer of preCommitReportLayers)
492
+ reportLayers.add(layer);
493
+ await orchestrator.regenerate(reportLayers);
443
494
  } catch (error) {
444
495
  emitLog(SeverityNumber.WARN, "Failed to regenerate layer reports after changeset commit", {
445
496
  "changeset.id": changesetId,
@@ -1061,6 +1112,32 @@ This indicates a filesystem or permissions issue. Check:
1061
1112
  return `changeset-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1062
1113
  }
1063
1114
  }
1115
+ function layerOfId(id) {
1116
+ const layer = id?.split(".")[0];
1117
+ return layer && isValidLayerName(layer) ? layer : void 0;
1118
+ }
1119
+ function layerOfElement(model, id) {
1120
+ if (!id)
1121
+ return;
1122
+ const element = model.getElementById(id);
1123
+ return element?.layer_id || element?.layer || layerOfId(id);
1124
+ }
1125
+ function reportLayersForChanges(orchestrator, model, changes) {
1126
+ const layers = /* @__PURE__ */ new Set(), add = (layer) => {
1127
+ typeof layer == "string" && isValidLayerName(layer) && layers.add(layer);
1128
+ };
1129
+ for (const change of changes) {
1130
+ if (change.type === "relationship-add" || change.type === "relationship-delete") {
1131
+ const data = change.after ?? change.before ?? {}, [source, , target] = typeof data.source == "string" ? [data.source, data.predicate, data.target] : change.elementId.split("::");
1132
+ add(change.layerName), add(data.layer), add(data.targetLayer), add(layerOfElement(model, source)), add(layerOfElement(model, target));
1133
+ continue;
1134
+ }
1135
+ const snapshot = change.after ?? change.before ?? {}, element = model.getElementById(change.elementId), ids = elementIdentifiers(element ?? snapshot, change.elementId, element?.path || void 0);
1136
+ for (const layer of orchestrator.computeElementAffectedLayers(change.layerName, ids))
1137
+ layers.add(layer);
1138
+ }
1139
+ return layers;
1140
+ }
1064
1141
  export {
1065
1142
  StagingAreaManager
1066
1143
  };