@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,8 @@
1
1
  import { z } from "zod";
2
2
  import { AnnotationStore } from "../../core/annotation-store.js";
3
- import { buildBrief, computeImpact, DEFAULT_HUB_THRESHOLD } from "../../core/changeset-analysis.js";
3
+ import { buildBrief, computeImpact, computeUnwired, DEFAULT_HUB_THRESHOLD } from "../../core/changeset-analysis.js";
4
4
  import { briefDelta } from "../../core/changeset-brief-delta.js";
5
+ import { BRIEF_SECTIONS, briefSections, compactBrief } from "../../core/changeset-brief-compact.js";
5
6
  import { describeDrift } from "../../core/changeset-base-index.js";
6
7
  import { getErrorMessage } from "../../utils/errors.js";
7
8
  import { jsonResult, loadModel, rootPathSchema, runTool } from "./shared.js";
@@ -34,8 +35,12 @@ const changesetImpactTool = {
34
35
  handler: changesetImpactHandler
35
36
  }, briefInputSchema = {
36
37
  changesetId: changesetIdSchema,
37
- unwired: z.enum(["summary", "full", "none"]).optional().describe("Unwired connections (catalog connections an added or updated element has no relationship for): summary (default) counts them per layer and lists the ones in layers the change already reaches whose other type the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them."),
38
+ unwired: z.enum(["summary", "full", "none"]).optional().describe("Unwired connections (catalog connections an added or updated element has no relationship for). The compact brief counts them per layer unless this is set. summary (the full brief's default) counts them per element and layer and lists the ones in layers the change already reaches whose other type the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them."),
38
39
  unwiredLayers: z.array(z.string()).optional().describe("Only show unwired connections whose other end is in these layers (and list them)."),
40
+ detail: z.enum(["compact", "full"]).optional().describe("compact (default): counts by layer, reach, gaps one line each, intent as ids, drift, unwired counts per layer, and each changed element with its connections as `predicate \u2192 id` lines. full: the whole brief (attributes, connection names and layers, unwired listing, code per element)."),
41
+ sections: z.array(z.enum(BRIEF_SECTIONS)).optional().describe('Return only these sections, each in full, with a small header (revision, the changeset\'s id, name and status, summary counts). ["changes"] is the build view: each changed element with its name, description, attributes, changed fields (before and after), files and connections as `predicate \u2192 id` lines. Others: ["drift", "alreadyInBase"], ["unwired"], ["layers"] (the full brief\'s layers, large). With since, the delta comes too.'),
42
+ layers: z.array(z.string()).optional().describe("With sections: only the elements in these layers (canonical layer names), in changes, layers, gaps, unwired, intent and feedback. Combines with ids (either keeps an element)."),
43
+ ids: z.array(z.string()).optional().describe("With sections: only these changed elements (ids), in changes, layers, gaps, unwired, intent and feedback."),
39
44
  since: z.union([z.number().int().min(-1), z.string().regex(/^(-1|\d+):[0-9a-f]+$/, "a revisionToken from a brief")]).optional().describe("A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was rewritten in place since."),
40
45
  rootPath: rootPathSchema
41
46
  };
@@ -59,13 +64,28 @@ async function changesetBriefHandler(args) {
59
64
  };
60
65
  }
61
66
  }
62
- const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });
63
- return args.since === void 0 ? jsonResult(brief, { compact: !0 }) : jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: !0 });
67
+ if (args.detail === "full") {
68
+ const brief2 = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });
69
+ return args.since === void 0 ? jsonResult(brief2, { compact: !0 }) : jsonResult({ ...brief2, delta: await briefDelta(model, { graph, brief: brief2 }, args.since) }, { compact: !0 });
70
+ }
71
+ const sections = args.sections ?? [], listUnwired = args.unwired !== void 0 || args.unwiredLayers !== void 0 || sections.includes("unwired"), brief = buildBrief({
72
+ graph,
73
+ annotations,
74
+ drift,
75
+ unwired: listUnwired ? args.unwired ?? "summary" : "none",
76
+ unwiredLayers: args.unwiredLayers
77
+ }), delta = args.since === void 0 ? void 0 : await briefDelta(model, { graph, brief }, args.since);
78
+ if (sections.length > 0) {
79
+ const named = listUnwired && !sections.includes("unwired") && args.unwired !== "none" ? [...sections, "unwired"] : sections;
80
+ return jsonResult(briefSections(brief, named, { delta, filter: { layers: args.layers, ids: args.ids } }), { compact: !0 });
81
+ }
82
+ const compact = compactBrief(brief, { unwired: listUnwired ? void 0 : computeUnwired(graph), delta });
83
+ return (args.layers?.length || args.ids?.length) && (compact.note = `layers and ids narrow sections output only, so the whole compact brief is returned. Pass sections too, e.g. sections: ["changes"] with them for those elements' build view.`), jsonResult(compact, { compact: !0 });
64
84
  });
65
85
  }
66
86
  const changesetBriefTool = {
67
87
  name: "changeset_brief",
68
- description: "Render a changeset as the spec for its change: every added, updated and deleted element by layer with its connections; which layers the change reaches and which it does not; gaps (unconnected additions, missing required relationships, connections its peers have); unwired connections (every catalog connection an added or updated element has no relationship for, by the other end's layer, with how many elements of the other type exist); changes that trace to no goal, requirement or feature; the source files touched; open review annotations; and how the base drifted since the changeset was created (staged adds the base already has are counted as alreadyInBase, apart from the adds). It returns a revision and a revisionToken: pass the token back as since on the next pass to get what changed. Use it to pressure-test a plan and as the input to building it.",
88
+ description: 'Render a changeset as the spec for its change. By default a compact summary: counts by layer and operation; which layers the change reaches and which it does not; gaps, one line each (unconnected additions, missing required relationships, connections its peers have); intent as ids (traced, untraced, unanchored: changes that reach no goal, requirement or feature, or only an added one that itself traces to nothing); drift since the changeset was created; unwired connections (catalog connections an added or updated element has no relationship for) counted per layer; the files of the changed elements; open review annotations; and each changed element with its type, operation and connections as `predicate \u2192 id` lines (for a plan of more than 60 elements, counts per type with the first ids of each, and long id lists cut with their counts). detail: "full" returns the whole brief (attributes, connection names, the unwired listing, code per element); sections: [...] returns only the sections named, in full, with a small header. It returns a revision and a revisionToken: pass the token back as since on the next pass to get what changed. Use it to pressure-test a plan and, at the start of a build, as its spec.',
69
89
  inputSchema: briefInputSchema,
70
90
  handler: changesetBriefHandler
71
91
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/mcp/tools/changeset-analysis-tools.ts"],
4
- "sourcesContent": ["/**\n * changeset_impact, changeset_brief \u2014 pressure-test a planned change against the model.\n * See `cli/src/core/changeset-analysis.ts` and docs/CHANGE_MODELING.md.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AnnotationStore } from \"../../core/annotation-store.js\";\nimport { buildBrief, computeImpact, DEFAULT_HUB_THRESHOLD, type BriefDrift } from \"../../core/changeset-analysis.js\";\nimport { briefDelta } from \"../../core/changeset-brief-delta.js\";\nimport { describeDrift } from \"../../core/changeset-base-index.js\";\nimport { getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { loadChangesetGraph } from \"./changeset-shared.js\";\n\nconst changesetIdSchema = z.string().optional().describe(\"Changeset ID or name. Defaults to the active changeset.\");\n\n// ---------------------------------------------------------------------------\n// changeset_impact\n// ---------------------------------------------------------------------------\n\nconst impactInputSchema = {\n changesetId: changesetIdSchema,\n depth: z.number().int().min(1).max(6).optional().describe(\"Hops to walk out from the changed elements (default 2).\"),\n maxElements: z.number().int().positive().optional().describe(\"Stop after this many reached elements (default 200).\"),\n from: z\n .array(z.string())\n .optional()\n .describe(\n \"Element ids or paths to walk from instead of every changed element. Ids the projected model does not have \" +\n \"are listed in unknownFrom.\"\n ),\n hubThreshold: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n `An element with more relationships than this is a hub: listed in hubs with its degree and not walked ` +\n `through (default ${DEFAULT_HUB_THRESHOLD}).`\n ),\n expandHubs: z.boolean().optional().describe(\"Walk through hubs too (default false).\"),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetImpactArgs = z.infer<z.ZodObject<typeof impactInputSchema>>;\n\nexport async function changesetImpactHandler(args: ChangesetImpactArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n const report = computeImpact(graph, {\n depth: args.depth,\n maxElements: args.maxElements,\n from: args.from,\n hubThreshold: args.hubThreshold,\n expandHubs: args.expandHubs,\n });\n return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });\n });\n}\n\nexport const changesetImpactTool: McpToolDefinition<typeof impactInputSchema> = {\n name: \"changeset_impact\",\n description:\n \"Walk the model outward from everything a changeset adds, changes or removes (relationships included), or \" +\n \"from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was \" +\n \"reached and its source files, plus how many each hop reached (perHop). These are what the change can break \" +\n \"or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and \" +\n \"not walked through unless expandHubs is set.\",\n inputSchema: impactInputSchema,\n handler: changesetImpactHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_brief\n// ---------------------------------------------------------------------------\n\nconst briefInputSchema = {\n changesetId: changesetIdSchema,\n unwired: z\n .enum([\"summary\", \"full\", \"none\"])\n .optional()\n .describe(\n \"Unwired connections (catalog connections an added or updated element has no relationship for): summary \" +\n \"(default) counts them per layer and lists the ones in layers the change already reaches whose other type \" +\n \"the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them.\"\n ),\n unwiredLayers: z\n .array(z.string())\n .optional()\n .describe(\"Only show unwired connections whose other end is in these layers (and list them).\"),\n since: z\n .union([z.number().int().min(-1), z.string().regex(/^(-1|\\d+):[0-9a-f]+$/, \"a revisionToken from a brief\")])\n .optional()\n .describe(\n \"A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed \" +\n \"or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, \" +\n \"unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. \" +\n \"Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was \" +\n \"rewritten in place since.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetBriefArgs = z.infer<z.ZodObject<typeof briefInputSchema>>;\n\nexport async function changesetBriefHandler(args: ChangesetBriefArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n\n const annotations = await new AnnotationStore(model.rootPath).list();\n\n let drift: BriefDrift | undefined;\n if (changeset.status === \"staged\") {\n // Each part is reported on failure and the brief goes on: the hash check, then the structured view.\n let hash: { isDrifted: boolean; warnings: string[]; baseSnapshotId?: string; currentSnapshotId?: string };\n try {\n hash = await manager.detectDrift(changeset.id);\n } catch (error) {\n hash = { isDrifted: false, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };\n }\n try {\n drift = await describeDrift(model, graph, hash);\n } catch (error) {\n drift = {\n isDrifted: hash.isDrifted,\n warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`],\n };\n }\n }\n\n const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });\n // The brief is the largest result: same JSON, short objects kept on one line.\n if (args.since === undefined) return jsonResult(brief, { compact: true });\n return jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: true });\n });\n}\n\nexport const changesetBriefTool: McpToolDefinition<typeof briefInputSchema> = {\n name: \"changeset_brief\",\n description:\n \"Render a changeset as the spec for its change: every added, updated and deleted element by layer with its \" +\n \"connections; which layers the change reaches and which it does not; gaps (unconnected additions, missing \" +\n \"required relationships, connections its peers have); unwired connections (every catalog connection an added \" +\n \"or updated element has no relationship for, by the other end's layer, with how many elements of the other \" +\n \"type exist); changes that trace to no goal, requirement or feature; the source files touched; open review \" +\n \"annotations; and how the base drifted since the changeset was created (staged adds the base already has are \" +\n \"counted as alreadyInBase, apart from the adds). It returns a revision and a revisionToken: pass the token back \" +\n \"as since on the next pass to get what changed. Use it to pressure-test a plan and as the input to building it.\",\n inputSchema: briefInputSchema,\n handler: changesetBriefHandler,\n};\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/**\n * changeset_impact, changeset_brief \u2014 pressure-test a planned change against the model.\n * See `cli/src/core/changeset-analysis.ts` and docs/CHANGE_MODELING.md.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AnnotationStore } from \"../../core/annotation-store.js\";\nimport {\n buildBrief,\n computeImpact,\n computeUnwired,\n DEFAULT_HUB_THRESHOLD,\n type BriefDrift,\n} from \"../../core/changeset-analysis.js\";\nimport { briefDelta } from \"../../core/changeset-brief-delta.js\";\nimport { BRIEF_SECTIONS, briefSections, compactBrief } from \"../../core/changeset-brief-compact.js\";\nimport { describeDrift } from \"../../core/changeset-base-index.js\";\nimport { getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { loadChangesetGraph } from \"./changeset-shared.js\";\n\nconst changesetIdSchema = z.string().optional().describe(\"Changeset ID or name. Defaults to the active changeset.\");\n\n// ---------------------------------------------------------------------------\n// changeset_impact\n// ---------------------------------------------------------------------------\n\nconst impactInputSchema = {\n changesetId: changesetIdSchema,\n depth: z.number().int().min(1).max(6).optional().describe(\"Hops to walk out from the changed elements (default 2).\"),\n maxElements: z.number().int().positive().optional().describe(\"Stop after this many reached elements (default 200).\"),\n from: z\n .array(z.string())\n .optional()\n .describe(\n \"Element ids or paths to walk from instead of every changed element. Ids the projected model does not have \" +\n \"are listed in unknownFrom.\"\n ),\n hubThreshold: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n `An element with more relationships than this is a hub: listed in hubs with its degree and not walked ` +\n `through (default ${DEFAULT_HUB_THRESHOLD}).`\n ),\n expandHubs: z.boolean().optional().describe(\"Walk through hubs too (default false).\"),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetImpactArgs = z.infer<z.ZodObject<typeof impactInputSchema>>;\n\nexport async function changesetImpactHandler(args: ChangesetImpactArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n const report = computeImpact(graph, {\n depth: args.depth,\n maxElements: args.maxElements,\n from: args.from,\n hubThreshold: args.hubThreshold,\n expandHubs: args.expandHubs,\n });\n return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });\n });\n}\n\nexport const changesetImpactTool: McpToolDefinition<typeof impactInputSchema> = {\n name: \"changeset_impact\",\n description:\n \"Walk the model outward from everything a changeset adds, changes or removes (relationships included), or \" +\n \"from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was \" +\n \"reached and its source files, plus how many each hop reached (perHop). These are what the change can break \" +\n \"or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and \" +\n \"not walked through unless expandHubs is set.\",\n inputSchema: impactInputSchema,\n handler: changesetImpactHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_brief\n// ---------------------------------------------------------------------------\n\nconst briefInputSchema = {\n changesetId: changesetIdSchema,\n unwired: z\n .enum([\"summary\", \"full\", \"none\"])\n .optional()\n .describe(\n \"Unwired connections (catalog connections an added or updated element has no relationship for). The compact \" +\n \"brief counts them per layer unless this is set. summary (the full brief's default) counts them per element \" +\n \"and layer and lists the ones in layers the change already reaches whose other type the model has (existing \" +\n \"or added), counting the rest as typesNotInModel; full lists them all; none omits them.\"\n ),\n unwiredLayers: z\n .array(z.string())\n .optional()\n .describe(\"Only show unwired connections whose other end is in these layers (and list them).\"),\n detail: z\n .enum([\"compact\", \"full\"])\n .optional()\n .describe(\n \"compact (default): counts by layer, reach, gaps one line each, intent as ids, drift, unwired counts per \" +\n \"layer, and each changed element with its connections as `predicate \u2192 id` lines. full: the whole brief \" +\n \"(attributes, connection names and layers, unwired listing, code per element).\"\n ),\n sections: z\n .array(z.enum(BRIEF_SECTIONS))\n .optional()\n .describe(\n \"Return only these sections, each in full, with a small header (revision, the changeset's id, name and status, \" +\n \"summary counts). [\\\"changes\\\"] is the build view: each changed element with its name, description, attributes, \" +\n \"changed fields (before and after), files and connections as `predicate \u2192 id` lines. Others: [\\\"drift\\\", \" +\n \"\\\"alreadyInBase\\\"], [\\\"unwired\\\"], [\\\"layers\\\"] (the full brief's layers, large). With since, the delta comes too.\"\n ),\n layers: z\n .array(z.string())\n .optional()\n .describe(\n \"With sections: only the elements in these layers (canonical layer names), in changes, layers, gaps, unwired, \" +\n \"intent and feedback. Combines with ids (either keeps an element).\"\n ),\n ids: z\n .array(z.string())\n .optional()\n .describe(\"With sections: only these changed elements (ids), in changes, layers, gaps, unwired, intent and feedback.\"),\n since: z\n .union([z.number().int().min(-1), z.string().regex(/^(-1|\\d+):[0-9a-f]+$/, \"a revisionToken from a brief\")])\n .optional()\n .describe(\n \"A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed \" +\n \"or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, \" +\n \"unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. \" +\n \"Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was \" +\n \"rewritten in place since.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetBriefArgs = z.infer<z.ZodObject<typeof briefInputSchema>>;\n\nexport async function changesetBriefHandler(args: ChangesetBriefArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n\n const annotations = await new AnnotationStore(model.rootPath).list();\n\n let drift: BriefDrift | undefined;\n if (changeset.status === \"staged\") {\n // Each part is reported on failure and the brief goes on: the hash check, then the structured view.\n let hash: { isDrifted: boolean; warnings: string[]; baseSnapshotId?: string; currentSnapshotId?: string };\n try {\n hash = await manager.detectDrift(changeset.id);\n } catch (error) {\n hash = { isDrifted: false, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };\n }\n try {\n drift = await describeDrift(model, graph, hash);\n } catch (error) {\n drift = {\n isDrifted: hash.isDrifted,\n warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`],\n };\n }\n }\n\n if (args.detail === \"full\") {\n const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });\n // The full brief is the largest result: same JSON, short objects kept on one line.\n if (args.since === undefined) return jsonResult(brief, { compact: true });\n return jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: true });\n }\n\n // Compact: the unwired listing only when asked for; otherwise counted per layer.\n const sections = args.sections ?? [];\n const listUnwired = args.unwired !== undefined || args.unwiredLayers !== undefined || sections.includes(\"unwired\");\n const brief = buildBrief({\n graph,\n annotations,\n drift,\n unwired: listUnwired ? (args.unwired ?? \"summary\") : \"none\",\n unwiredLayers: args.unwiredLayers,\n });\n const delta = args.since === undefined ? undefined : await briefDelta(model, { graph, brief }, args.since);\n if (sections.length > 0) {\n // Only what was asked for; an unwired listing asked for by its own arguments counts as asked.\n const named = listUnwired && !sections.includes(\"unwired\") && args.unwired !== \"none\" ? [...sections, \"unwired\" as const] : sections;\n return jsonResult(briefSections(brief, named, { delta, filter: { layers: args.layers, ids: args.ids } }), { compact: true });\n }\n const compact = compactBrief(brief, { unwired: listUnwired ? undefined : computeUnwired(graph), delta });\n if (args.layers?.length || args.ids?.length) {\n compact.note =\n \"layers and ids narrow sections output only, so the whole compact brief is returned. Pass sections too, \" +\n 'e.g. sections: [\"changes\"] with them for those elements\\' build view.';\n }\n return jsonResult(compact, { compact: true });\n });\n}\n\nexport const changesetBriefTool: McpToolDefinition<typeof briefInputSchema> = {\n name: \"changeset_brief\",\n description:\n \"Render a changeset as the spec for its change. By default a compact summary: counts by layer and operation; \" +\n \"which layers the change reaches and which it does not; gaps, one line each (unconnected additions, missing \" +\n \"required relationships, connections its peers have); intent as ids (traced, untraced, unanchored: changes \" +\n \"that reach no goal, requirement or feature, or only an added one that itself traces to nothing); drift since \" +\n \"the changeset was created; unwired connections (catalog connections an added or updated element has no \" +\n \"relationship for) counted per layer; the files of the changed elements; open review annotations; and each \" +\n \"changed element with its type, operation and connections as `predicate \u2192 id` lines (for a plan of more \" +\n \"than 60 elements, counts per type with the first ids of each, and long id lists cut with their counts). \" +\n \"detail: \\\"full\\\" returns the whole brief (attributes, connection names, the unwired listing, code per \" +\n \"element); sections: [...] returns only the sections named, in full, with a small header. It returns a revision and a revisionToken: pass the token back as since on the \" +\n \"next pass to get what changed. Use it to pressure-test a plan and, at the start of a build, as its spec.\",\n inputSchema: briefInputSchema,\n handler: changesetBriefHandler,\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["brief"]
7
7
  }
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ import { Model } from "../../core/model.js";
3
+ import { BaseSnapshotManager } from "../../core/base-snapshot-manager.js";
4
+ import { ChangesetGraph } from "../../core/changeset-graph.js";
5
+ import { describeDrift } from "../../core/changeset-base-index.js";
6
+ import { StagingAreaManager } from "../../core/staging-area.js";
7
+ import { isChangesetOnly, CHANGESET_ONLY_ENV } from "../../core/changeset-only-guard.js";
8
+ import { CLIError, ErrorCategory, getErrorMessage } from "../../utils/errors.js";
9
+ import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
10
+ import { resolveChangeset } from "./changeset-shared.js";
11
+ import { isExtractionChangeset } from "./codebase-extract.js";
12
+ const CONFIRMATION_TIMEOUT_MS = 10 * 6e4;
13
+ function otherWaysToLand() {
14
+ return [
15
+ "Ask the user to commit it in the viewer: call review_open, give them its url, and they press Apply on the changeset",
16
+ "Or ask them to run changeset_commit again from an interactive Claude Code session, where the confirmation dialog can be shown"
17
+ ];
18
+ }
19
+ const inputSchema = {
20
+ changeset: z.string().min(1).describe("Changeset ID or name to commit. Required: it need not be the active changeset."),
21
+ force: z.boolean().optional().describe("Commit even though the base model changed since the changeset was created (drift). The confirmation dialog shows the drift. Validation still blocks."),
22
+ rootPath: rootPathSchema
23
+ }, OPERATION_LABELS = {
24
+ add: "adds",
25
+ update: "updates",
26
+ delete: "deletes",
27
+ "relationship-add": "relationship adds",
28
+ "relationship-delete": "relationship deletes"
29
+ };
30
+ function summarizeChangeset(changeset) {
31
+ const byOperation = {}, layers = /* @__PURE__ */ new Set();
32
+ for (const change of changeset.changes)
33
+ byOperation[change.type] = (byOperation[change.type] ?? 0) + 1, change.layerName && layers.add(change.layerName);
34
+ return { byOperation, layers: [...layers].sort() };
35
+ }
36
+ function driftLines(drift, hash, forced) {
37
+ const overlapping = drift ? drift.overlapping.elements.length + drift.overlapping.relationships.length : 0;
38
+ if (!hash.isDrifted && !drift?.isDrifted)
39
+ return ["Drift: none. The base model has not changed under this changeset."];
40
+ const lines = ["Drift: the base model changed since this changeset was created."], counts = drift?.baseChanges?.counts;
41
+ if (counts) {
42
+ const parts = [
43
+ [counts.elementsAdded, "elements added"],
44
+ [counts.elementsRemoved, "elements removed"],
45
+ [counts.elementsChanged, "elements changed"],
46
+ [counts.relationshipsAdded, "relationships added"],
47
+ [counts.relationshipsRemoved, "relationships removed"]
48
+ ].filter(([n]) => n > 0).map(([n, label]) => `${n} ${label}`);
49
+ parts.length > 0 && lines.push(` In the base: ${parts.join(", ")}.`);
50
+ }
51
+ if (drift && overlapping > 0) {
52
+ const listed = [
53
+ ...drift.overlapping.elements.map((e) => `${e.id} (${e.reason})`),
54
+ ...drift.overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`)
55
+ ];
56
+ lines.push(` ${overlapping} staged change(s) overlap the base: ${listed.slice(0, 5).join(", ")}${overlapping > 5 ? ", ..." : ""}.`);
57
+ }
58
+ return hash.isDrifted ? forced && lines.push(" Committing anyway: force was passed.") : lines.push(" The drift check still passes, so this does not block the commit."), lines;
59
+ }
60
+ const EXTRACTION_MARKER = /Staged by codebase_extract/;
61
+ async function madeByExtraction(rootPath, changeset) {
62
+ return EXTRACTION_MARKER.test(changeset.description ?? "") ? !0 : !!changeset.id && await isExtractionChangeset(rootPath, changeset.id, changeset.name);
63
+ }
64
+ async function asBuiltFirstWarning(manager, rootPath, changeset) {
65
+ if (!await madeByExtraction(rootPath, changeset))
66
+ return;
67
+ const plans = [];
68
+ for (const other of await manager.list())
69
+ other.id === changeset.id || other.status !== "staged" || other.changes.length === 0 || await madeByExtraction(rootPath, other) || plans.push(other);
70
+ if (plans.length === 0)
71
+ return;
72
+ const names = plans.map((plan) => `"${plan.name}"`).join(", ");
73
+ return `Warning: "${changeset.name}" is an as-built changeset from codebase_extract, and the plan ${names} is still staged. After verify, land the plan instead, and discard this one or keep it as a record. Landing this one first changes the plan's base: the plan then needs force, and its descriptions won't apply to the elements this one adds.`;
74
+ }
75
+ function confirmationMessage(changeset, drift, hash, preExistingErrors, forced, warnings = []) {
76
+ const { byOperation, layers } = summarizeChangeset(changeset), counts = Object.entries(byOperation).map(([type, n]) => `${n} ${OPERATION_LABELS[type] ?? type}`).join(", ");
77
+ return [
78
+ `Commit changeset "${changeset.name}" to the base model?`,
79
+ "",
80
+ ...warnings.flatMap((warning) => [warning, ""]),
81
+ `Intent: ${changeset.description?.trim() || "(no description)"}`,
82
+ `Changes: ${changeset.changes.length} (${counts})`,
83
+ `Layers: ${layers.join(", ") || "none"}`,
84
+ ...driftLines(drift, hash, forced),
85
+ preExistingErrors > 0 ? `Validation: passed. ${preExistingErrors} error(s) already in the base model are not counted.` : "Validation: passed.",
86
+ "",
87
+ "Committing writes these changes to the base model and marks the changeset committed."
88
+ ].join(`
89
+ `);
90
+ }
91
+ async function confirmedState(model, changeset) {
92
+ return JSON.stringify({
93
+ changes: changeset.changes.map((c) => [c.sequenceNumber ?? null, c.type, c.elementId]),
94
+ lastSequence: changeset.lastSequence ?? null,
95
+ modified: changeset.modified,
96
+ status: changeset.status,
97
+ baseElements: await new BaseSnapshotManager().captureSnapshot(model),
98
+ baseRelationships: model.getBaseRelationships().map((r) => `${r.source}::${r.predicate}::${r.target}`).sort()
99
+ });
100
+ }
101
+ function refusal(message, suggestions) {
102
+ return new CLIError(message, ErrorCategory.USER, suggestions, { operation: "changeset_commit" });
103
+ }
104
+ function commitError(error, name) {
105
+ if (error instanceof CLIError)
106
+ return error;
107
+ const message = getErrorMessage(error);
108
+ return message.includes("drifted") ? new CLIError(`Changeset '${name}' was not committed: ${message.replace(/Use --force/, "Pass force: true")}`, ErrorCategory.USER, [
109
+ 'Read the drift with "changeset_brief" and tell the user what changed in the base',
110
+ "If they still want it landed, call changeset_commit again with force: true"
111
+ ]) : message.startsWith("Validation failed") ? new CLIError(`Changeset '${name}' was not committed. ${message.replace(/, or skip validation with --no-validate\./, ".")}`, ErrorCategory.VALIDATION, ['Fix the plan (model_update, changeset_unstage, ...) and check it with "model_validate"']) : new CLIError(`Changeset '${name}' was not committed: ${message}`, ErrorCategory.SYSTEM);
112
+ }
113
+ async function askForConfirmation(context, message) {
114
+ return context.server.elicitInput({
115
+ mode: "form",
116
+ message,
117
+ requestedSchema: {
118
+ type: "object",
119
+ properties: {
120
+ confirm: {
121
+ type: "boolean",
122
+ title: "Commit this changeset",
123
+ description: "Write the staged changes to the base model.",
124
+ default: !1
125
+ }
126
+ },
127
+ required: ["confirm"]
128
+ }
129
+ }, { relatedRequestId: context.requestId, signal: context.signal, timeout: CONFIRMATION_TIMEOUT_MS });
130
+ }
131
+ async function changesetCommitHandler(args, context) {
132
+ return runTool(async () => {
133
+ if (isChangesetOnly())
134
+ throw refusal(`This session can't commit a changeset: it is in changeset-only mode (${CHANGESET_ONLY_ENV}), where no person can confirm the commit.`, ["Tell the user the changeset is ready to land", ...otherWaysToLand()]);
135
+ const rootPath = (await loadModel(args.rootPath)).rootPath;
136
+ let model = await Model.load(rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model);
137
+ const changeset = await resolveChangeset(manager, args.changeset, "changeset");
138
+ if (changeset.status !== "staged")
139
+ throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; only a staged changeset can be committed`, ErrorCategory.USER, [
140
+ 'Use "changeset_list" to find a staged changeset'
141
+ ]);
142
+ if (changeset.changes.length === 0)
143
+ return jsonResult({ id: changeset.id, name: changeset.name, status: "nothing-to-commit", stagedChanges: 0 });
144
+ if (!context?.server?.getClientCapabilities()?.elicitation?.form)
145
+ throw refusal(`Changeset '${changeset.name}' was not committed: committing needs a person to confirm it, and this client can't show a confirmation dialog (it does not support MCP elicitation).`, otherWaysToLand());
146
+ const hash = await manager.detectDrift(changeset.id);
147
+ let drift;
148
+ try {
149
+ drift = await describeDrift(model, await ChangesetGraph.build(model, changeset), hash);
150
+ } catch (error) {
151
+ console.error(`[mcp] changeset_commit could not describe drift: ${getErrorMessage(error)}`);
152
+ }
153
+ if (hash.isDrifted && args.force !== !0)
154
+ throw new CLIError(`Changeset '${changeset.name}' was not committed: the base model changed since it was created.
155
+ ` + driftLines(drift, hash, !1).slice(1).join(`
156
+ `), ErrorCategory.USER, [
157
+ 'Read the drift with "changeset_brief" and tell the user what changed in the base',
158
+ "If they still want it landed, call changeset_commit again with force: true; the dialog will show the drift"
159
+ ], { operation: "changeset_commit" });
160
+ let preExistingErrors = 0;
161
+ try {
162
+ preExistingErrors = (await manager.commit(model, changeset.id, { dryRun: !0, validate: !0, force: args.force === !0 })).validation.preExistingErrors ?? 0;
163
+ } catch (error) {
164
+ throw commitError(error, changeset.name);
165
+ }
166
+ const warnings = [], asBuiltFirst = await asBuiltFirstWarning(manager, rootPath, changeset);
167
+ asBuiltFirst && warnings.push(asBuiltFirst);
168
+ const shown = await confirmedState(model, changeset);
169
+ let answer;
170
+ try {
171
+ answer = await askForConfirmation(context, confirmationMessage(changeset, drift, hash, preExistingErrors, args.force === !0, warnings));
172
+ } catch (error) {
173
+ throw refusal(`Changeset '${changeset.name}' was not committed: the confirmation could not be shown (${getErrorMessage(error)}).`, otherWaysToLand());
174
+ }
175
+ if (answer.action !== "accept" || answer.content?.confirm !== !0) {
176
+ const how = answer.action === "accept" ? "did not confirm it" : answer.action === "decline" ? "declined" : "cancelled";
177
+ throw refusal(`Changeset '${changeset.name}' was not committed: the user ${how}. Nothing was written.`, [
178
+ "Ask the user what should change before landing it, or leave it staged"
179
+ ]);
180
+ }
181
+ model = await Model.load(rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model);
182
+ const current = await manager.load(changeset.id);
183
+ if (!current || await confirmedState(model, current) !== shown)
184
+ throw refusal(`Changeset '${changeset.name}' was not committed: the changeset or the base model changed while the user was confirming. Nothing was written.`, ['Show the user the changeset as it is now ("changeset_brief"), then ask again with changeset_commit']);
185
+ const wasActive = await manager.getActiveId() === changeset.id;
186
+ let result;
187
+ try {
188
+ result = await manager.commit(model, changeset.id, { validate: !0, force: args.force === !0 });
189
+ } catch (error) {
190
+ throw commitError(error, changeset.name);
191
+ }
192
+ return wasActive && await manager.clearActive(), await reloadModel(args.rootPath), jsonResult({
193
+ id: changeset.id,
194
+ name: changeset.name,
195
+ status: "committed",
196
+ committed: result.committed,
197
+ skipped: result.skipped ?? 0,
198
+ ...result.skippedDetails?.length ? { skippedDetails: result.skippedDetails.slice(0, 10) } : {},
199
+ failed: result.failed,
200
+ preExistingErrors: result.validation.preExistingErrors ?? 0,
201
+ deactivated: wasActive,
202
+ // StagingAreaManager.commit reports driftWarning only on a dry run, so use the check made above.
203
+ ...result.driftWarning || hash.isDrifted ? { driftOverridden: !0 } : {},
204
+ ...warnings.length > 0 ? { warnings } : {}
205
+ });
206
+ });
207
+ }
208
+ const changesetCommitTool = {
209
+ name: "changeset_commit",
210
+ description: "Land a staged changeset: write its changes to the base model and mark it committed. Only after a person confirms it: the server asks the user in a confirmation dialog (MCP elicitation) showing the changeset's intent, counts, layers and drift, and commits only if they confirm. Refused when the client can't show the dialog, when the user declines or cancels, and in changeset-only sessions. Validation errors the changeset introduces block it; drift blocks it unless force is passed. Call it only when the user asks to land the change.",
211
+ inputSchema,
212
+ _meta: { "anthropic/requiresUserInteraction": !0 },
213
+ handler: changesetCommitHandler
214
+ };
215
+ export {
216
+ CONFIRMATION_TIMEOUT_MS,
217
+ asBuiltFirstWarning,
218
+ changesetCommitHandler,
219
+ changesetCommitTool,
220
+ confirmationMessage,
221
+ otherWaysToLand,
222
+ summarizeChangeset
223
+ };
224
+ //# sourceMappingURL=changeset-commit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/mcp/tools/changeset-commit.ts"],
4
+ "sourcesContent": ["/**\n * changeset_commit \u2014 land a staged changeset in the base model, once a person confirms it.\n *\n * Landing is the human's step (motivation.requirement.landing-a-change-needs-a-humans-confirmation),\n * so the tool is gated twice:\n * - It is listed with `_meta[\"anthropic/requiresUserInteraction\"]`. Claude Code then shows a\n * permission prompt on every call, whatever the permission mode.\n * - It always asks the client for confirmation (`elicitation/create`), showing the changeset's\n * name, intent, counts by operation, the layers it touches and any drift. It commits only on\n * `accept` with `confirm: true`. A decline, a cancel, a client without form elicitation, or a\n * changeset-only session (DrBot, where no person can answer) is refused, and nothing is written.\n *\n * The commit itself is `StagingAreaManager.commit`, the code `dr changeset commit` and the viewer's\n * Apply (`POST /api/changesets/:id/commit`) run. Like the viewer, it works on a freshly loaded model\n * and may commit any staged changeset, active or not. Like the CLI, it validates (errors the\n * changeset introduces block; the base model's own are counted) and refuses a drifted changeset\n * unless `force` is passed. Landing an as-built changeset from codebase_extract while a plan is\n * staged is allowed, but the dialog warns that the plan should land instead. Both are checked with a dry run before the person is asked, so they are\n * never asked to confirm a commit that would fail. After they accept, the changeset and the base are\n * read again and compared with what the dialog showed; any change refuses the commit.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult, ElicitResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Model } from \"../../core/model.js\";\nimport type { Changeset, StagedChange } from \"../../core/changeset.js\";\nimport { BaseSnapshotManager } from \"../../core/base-snapshot-manager.js\";\nimport { ChangesetGraph } from \"../../core/changeset-graph.js\";\nimport { describeDrift, type ChangesetDrift } from \"../../core/changeset-base-index.js\";\nimport { StagingAreaManager, type DriftReport } from \"../../core/staging-area.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../../core/changeset-only-guard.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../../utils/errors.js\";\nimport {\n jsonResult,\n loadModel,\n reloadModel,\n rootPathSchema,\n runTool,\n type McpToolContext,\n type McpToolDefinition,\n} from \"./shared.js\";\nimport { resolveChangeset } from \"./changeset-shared.js\";\nimport { isExtractionChangeset } from \"./codebase-extract.js\";\n\n/** How long the person has to answer the confirmation dialog before the commit is refused. */\nexport const CONFIRMATION_TIMEOUT_MS = 10 * 60_000;\n\n/**\n * How the user can land the changeset without this tool: the viewer, or an interactive session.\n * Never the CLI: the developer types no dr command (round 3 found agents passing it on).\n */\nexport function otherWaysToLand(): string[] {\n return [\n \"Ask the user to commit it in the viewer: call review_open, give them its url, and they press Apply on the changeset\",\n \"Or ask them to run changeset_commit again from an interactive Claude Code session, where the confirmation dialog can be shown\",\n ];\n}\n\nconst inputSchema = {\n changeset: z\n .string()\n .min(1)\n .describe(\"Changeset ID or name to commit. Required: it need not be the active changeset.\"),\n force: z\n .boolean()\n .optional()\n .describe(\n \"Commit even though the base model changed since the changeset was created (drift). The confirmation \" +\n \"dialog shows the drift. Validation still blocks.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetCommitArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\nconst OPERATION_LABELS: Record<string, string> = {\n add: \"adds\",\n update: \"updates\",\n delete: \"deletes\",\n \"relationship-add\": \"relationship adds\",\n \"relationship-delete\": \"relationship deletes\",\n};\n\n/** Counts by operation and the layers touched, in a stable order. */\nexport function summarizeChangeset(changeset: Changeset): { byOperation: Record<string, number>; layers: string[] } {\n const byOperation: Record<string, number> = {};\n const layers = new Set<string>();\n for (const change of changeset.changes) {\n byOperation[change.type] = (byOperation[change.type] ?? 0) + 1;\n if (change.layerName) layers.add(change.layerName);\n }\n return { byOperation, layers: [...layers].sort() };\n}\n\nfunction driftLines(drift: ChangesetDrift | undefined, hash: DriftReport, forced: boolean): string[] {\n const overlapping = drift ? drift.overlapping.elements.length + drift.overlapping.relationships.length : 0;\n if (!hash.isDrifted && !drift?.isDrifted) return [\"Drift: none. The base model has not changed under this changeset.\"];\n const lines = [\"Drift: the base model changed since this changeset was created.\"];\n const counts = drift?.baseChanges?.counts;\n if (counts) {\n const parts = [\n [counts.elementsAdded, \"elements added\"],\n [counts.elementsRemoved, \"elements removed\"],\n [counts.elementsChanged, \"elements changed\"],\n [counts.relationshipsAdded, \"relationships added\"],\n [counts.relationshipsRemoved, \"relationships removed\"],\n ]\n .filter(([n]) => (n as number) > 0)\n .map(([n, label]) => `${n} ${label}`);\n if (parts.length > 0) lines.push(` In the base: ${parts.join(\", \")}.`);\n }\n if (drift && overlapping > 0) {\n const listed = [\n ...drift.overlapping.elements.map((e) => `${e.id} (${e.reason})`),\n ...drift.overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`),\n ];\n lines.push(\n ` ${overlapping} staged change(s) overlap the base: ${listed.slice(0, 5).join(\", \")}${overlapping > 5 ? \", ...\" : \"\"}.`\n );\n }\n if (!hash.isDrifted) lines.push(\" The drift check still passes, so this does not block the commit.\");\n else if (forced) lines.push(\" Committing anyway: force was passed.\");\n return lines;\n}\n\n/** The description codebase_extract gives the changesets it creates. */\nconst EXTRACTION_MARKER = /Staged by codebase_extract/;\n\n/**\n * Whether codebase_extract made this changeset: its description says so, or extraction keeps a\n * record for it (its cards file, or waiting cards adopted under this name).\n */\nasync function madeByExtraction(rootPath: string, changeset: Changeset): Promise<boolean> {\n if (EXTRACTION_MARKER.test(changeset.description ?? \"\")) return true;\n return !!changeset.id && (await isExtractionChangeset(rootPath, changeset.id, changeset.name));\n}\n\n/**\n * The warning shown when an extraction (as-built) changeset is about to land while a plan is\n * staged. The verify workflow lands the plan and discards the as-built changeset (or keeps it as a\n * record); landing the as-built first drifts the plan's base, so the plan then needs force, and\n * its descriptions don't apply to the elements the as-built side already added.\n */\nexport async function asBuiltFirstWarning(\n manager: StagingAreaManager,\n rootPath: string,\n changeset: Changeset\n): Promise<string | undefined> {\n if (!(await madeByExtraction(rootPath, changeset))) return undefined;\n const plans: Changeset[] = [];\n for (const other of await manager.list()) {\n if (other.id === changeset.id || other.status !== \"staged\" || other.changes.length === 0) continue;\n if (!(await madeByExtraction(rootPath, other))) plans.push(other);\n }\n if (plans.length === 0) return undefined;\n const names = plans.map((plan) => `\"${plan.name}\"`).join(\", \");\n return (\n `Warning: \"${changeset.name}\" is an as-built changeset from codebase_extract, and the plan ${names} is still ` +\n `staged. After verify, land the plan instead, and discard this one or keep it as a record. Landing this one ` +\n `first changes the plan's base: the plan then needs force, and its descriptions won't apply to the elements ` +\n `this one adds.`\n );\n}\n\n/** The text of the confirmation dialog. */\nexport function confirmationMessage(\n changeset: Changeset,\n drift: ChangesetDrift | undefined,\n hash: DriftReport,\n preExistingErrors: number,\n forced: boolean,\n warnings: string[] = []\n): string {\n const { byOperation, layers } = summarizeChangeset(changeset);\n const counts = Object.entries(byOperation)\n .map(([type, n]) => `${n} ${OPERATION_LABELS[type] ?? type}`)\n .join(\", \");\n return [\n `Commit changeset \"${changeset.name}\" to the base model?`,\n \"\",\n ...warnings.flatMap((warning) => [warning, \"\"]),\n `Intent: ${changeset.description?.trim() || \"(no description)\"}`,\n `Changes: ${changeset.changes.length} (${counts})`,\n `Layers: ${layers.join(\", \") || \"none\"}`,\n ...driftLines(drift, hash, forced),\n preExistingErrors > 0\n ? `Validation: passed. ${preExistingErrors} error(s) already in the base model are not counted.`\n : \"Validation: passed.\",\n \"\",\n \"Committing writes these changes to the base model and marks the changeset committed.\",\n ].join(\"\\n\");\n}\n\n/**\n * A fingerprint of what the person confirms: the changeset's staged changes, its modified time and\n * status, and the base model's elements (the drift snapshot) and committed relationships.\n */\nasync function confirmedState(model: Model, changeset: Changeset): Promise<string> {\n return JSON.stringify({\n changes: (changeset.changes as StagedChange[]).map((c) => [c.sequenceNumber ?? null, c.type, c.elementId]),\n lastSequence: changeset.lastSequence ?? null,\n modified: changeset.modified,\n status: changeset.status,\n baseElements: await new BaseSnapshotManager().captureSnapshot(model),\n baseRelationships: model\n .getBaseRelationships()\n .map((r) => `${r.source}::${r.predicate}::${r.target}`)\n .sort(),\n });\n}\n\nfunction refusal(message: string, suggestions: string[]): CLIError {\n return new CLIError(message, ErrorCategory.USER, suggestions, { operation: \"changeset_commit\" });\n}\n\n/** Wraps a StagingAreaManager error (plain Error) so the agent sees what blocked the commit. */\nfunction commitError(error: unknown, name: string): CLIError {\n if (error instanceof CLIError) return error;\n const message = getErrorMessage(error);\n if (message.includes(\"drifted\")) {\n return new CLIError(`Changeset '${name}' was not committed: ${message.replace(/Use --force/, \"Pass force: true\")}`, ErrorCategory.USER, [\n 'Read the drift with \"changeset_brief\" and tell the user what changed in the base',\n \"If they still want it landed, call changeset_commit again with force: true\",\n ]);\n }\n if (message.startsWith(\"Validation failed\")) {\n return new CLIError(\n `Changeset '${name}' was not committed. ${message.replace(/, or skip validation with --no-validate\\./, \".\")}`,\n ErrorCategory.VALIDATION,\n ['Fix the plan (model_update, changeset_unstage, ...) and check it with \"model_validate\"']\n );\n }\n return new CLIError(`Changeset '${name}' was not committed: ${message}`, ErrorCategory.SYSTEM);\n}\n\nasync function askForConfirmation(context: McpToolContext, message: string): Promise<ElicitResult> {\n return context.server!.elicitInput(\n {\n mode: \"form\",\n message,\n requestedSchema: {\n type: \"object\",\n properties: {\n confirm: {\n type: \"boolean\",\n title: \"Commit this changeset\",\n description: \"Write the staged changes to the base model.\",\n default: false,\n },\n },\n required: [\"confirm\"],\n },\n },\n { relatedRequestId: context.requestId, signal: context.signal, timeout: CONFIRMATION_TIMEOUT_MS }\n );\n}\n\nexport async function changesetCommitHandler(\n args: ChangesetCommitArgs,\n context?: McpToolContext\n): Promise<CallToolResult> {\n return runTool(async () => {\n if (isChangesetOnly()) {\n throw refusal(\n `This session can't commit a changeset: it is in changeset-only mode (${CHANGESET_ONLY_ENV}), ` +\n \"where no person can confirm the commit.\",\n [\"Tell the user the changeset is ready to land\", ...otherWaysToLand()]\n );\n }\n\n // The cached model names the project; the commit runs on a fresh read, like the viewer's.\n const rootPath = (await loadModel(args.rootPath)).rootPath;\n let model = await Model.load(rootPath, { lazyLoad: false });\n let manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changeset, \"changeset\");\n if (changeset.status !== \"staged\") {\n throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; only a staged changeset can be committed`, ErrorCategory.USER, [\n 'Use \"changeset_list\" to find a staged changeset',\n ]);\n }\n if (changeset.changes.length === 0) {\n return jsonResult({ id: changeset.id, name: changeset.name, status: \"nothing-to-commit\", stagedChanges: 0 });\n }\n\n const server = context?.server;\n if (!server?.getClientCapabilities()?.elicitation?.form) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: committing needs a person to confirm it, and this client ` +\n \"can't show a confirmation dialog (it does not support MCP elicitation).\",\n otherWaysToLand()\n );\n }\n\n // Check drift and validation before asking, so the person never confirms a commit that would fail.\n const hash = await manager.detectDrift(changeset.id);\n let drift: ChangesetDrift | undefined;\n try {\n drift = await describeDrift(model, await ChangesetGraph.build(model, changeset), hash);\n } catch (error) {\n console.error(`[mcp] changeset_commit could not describe drift: ${getErrorMessage(error)}`);\n }\n if (hash.isDrifted && args.force !== true) {\n // The CLI's rule: a drifted changeset needs --force. Say what drifted so the person can decide.\n throw new CLIError(\n `Changeset '${changeset.name}' was not committed: the base model changed since it was created.\\n` +\n driftLines(drift, hash, false).slice(1).join(\"\\n\"),\n ErrorCategory.USER,\n [\n 'Read the drift with \"changeset_brief\" and tell the user what changed in the base',\n \"If they still want it landed, call changeset_commit again with force: true; the dialog will show the drift\",\n ],\n { operation: \"changeset_commit\" }\n );\n }\n let preExistingErrors = 0;\n try {\n const dryRun = await manager.commit(model, changeset.id, { dryRun: true, validate: true, force: args.force === true });\n preExistingErrors = dryRun.validation.preExistingErrors ?? 0;\n } catch (error) {\n throw commitError(error, changeset.name);\n }\n\n // An as-built changeset landing while the plan is staged: the plan should land instead.\n const warnings: string[] = [];\n const asBuiltFirst = await asBuiltFirstWarning(manager, rootPath, changeset);\n if (asBuiltFirst) warnings.push(asBuiltFirst);\n\n // What the person is shown; checked again after they answer.\n const shown = await confirmedState(model, changeset);\n let answer: ElicitResult;\n try {\n answer = await askForConfirmation(\n context!,\n confirmationMessage(changeset, drift, hash, preExistingErrors, args.force === true, warnings)\n );\n } catch (error) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: the confirmation could not be shown (${getErrorMessage(error)}).`,\n otherWaysToLand()\n );\n }\n if (answer.action !== \"accept\" || answer.content?.confirm !== true) {\n const how = answer.action === \"accept\" ? \"did not confirm it\" : answer.action === \"decline\" ? \"declined\" : \"cancelled\";\n throw refusal(`Changeset '${changeset.name}' was not committed: the user ${how}. Nothing was written.`, [\n \"Ask the user what should change before landing it, or leave it staged\",\n ]);\n }\n\n // The person may have taken a while. Commit only what they saw: re-read the changeset and the\n // base, and refuse if either changed. The commit itself checks drift and validation again.\n model = await Model.load(rootPath, { lazyLoad: false });\n manager = new StagingAreaManager(model.rootPath, model);\n const current = await manager.load(changeset.id);\n if (!current || (await confirmedState(model, current)) !== shown) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: the changeset or the base model changed while the user ` +\n \"was confirming. Nothing was written.\",\n ['Show the user the changeset as it is now (\"changeset_brief\"), then ask again with changeset_commit']\n );\n }\n const wasActive = (await manager.getActiveId()) === changeset.id;\n let result;\n try {\n result = await manager.commit(model, changeset.id, { validate: true, force: args.force === true });\n } catch (error) {\n throw commitError(error, changeset.name);\n }\n if (wasActive) await manager.clearActive();\n await reloadModel(args.rootPath);\n\n return jsonResult({\n id: changeset.id,\n name: changeset.name,\n status: \"committed\",\n committed: result.committed,\n skipped: result.skipped ?? 0,\n ...(result.skippedDetails?.length ? { skippedDetails: result.skippedDetails.slice(0, 10) } : {}),\n failed: result.failed,\n preExistingErrors: result.validation.preExistingErrors ?? 0,\n deactivated: wasActive,\n // StagingAreaManager.commit reports driftWarning only on a dry run, so use the check made above.\n ...(result.driftWarning || hash.isDrifted ? { driftOverridden: true } : {}),\n ...(warnings.length > 0 ? { warnings } : {}),\n });\n });\n}\n\nexport const changesetCommitTool: McpToolDefinition<typeof inputSchema> = {\n name: \"changeset_commit\",\n description:\n \"Land a staged changeset: write its changes to the base model and mark it committed. Only after a person \" +\n \"confirms it: the server asks the user in a confirmation dialog (MCP elicitation) showing the changeset's \" +\n \"intent, counts, layers and drift, and commits only if they confirm. Refused when the client can't show the \" +\n \"dialog, when the user declines or cancels, and in changeset-only sessions. Validation errors the changeset \" +\n \"introduces block it; drift blocks it unless force is passed. Call it only when the user asks to land the change.\",\n inputSchema,\n _meta: { \"anthropic/requiresUserInteraction\": true },\n handler: changesetCommitHandler,\n};\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }