@documentation-robotics/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +36 -2
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/call-graph-utils.js +2 -2
  5. package/dist/analyzers/call-graph-utils.js.map +2 -2
  6. package/dist/analyzers/cards/build.js +171 -0
  7. package/dist/analyzers/cards/build.js.map +7 -0
  8. package/dist/analyzers/cards/classify.js +42 -0
  9. package/dist/analyzers/cards/classify.js.map +7 -0
  10. package/dist/analyzers/cards/edge-index.js +44 -0
  11. package/dist/analyzers/cards/edge-index.js.map +7 -0
  12. package/dist/analyzers/cards/facts.js +46 -0
  13. package/dist/analyzers/cards/facts.js.map +7 -0
  14. package/dist/analyzers/cards/graph.js +42 -0
  15. package/dist/analyzers/cards/graph.js.map +7 -0
  16. package/dist/analyzers/cards/index.js +8 -0
  17. package/dist/analyzers/cards/index.js.map +7 -0
  18. package/dist/analyzers/cards/tier0.js +18 -0
  19. package/dist/analyzers/cards/tier0.js.map +7 -0
  20. package/dist/analyzers/cards/types.js +1 -0
  21. package/dist/analyzers/cards/types.js.map +7 -0
  22. package/dist/analyzers/cbm-analyzer.js +195 -171
  23. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  24. package/dist/analyzers/cbm-responses.js +336 -0
  25. package/dist/analyzers/cbm-responses.js.map +7 -0
  26. package/dist/analyzers/edge-relationships.js +45 -0
  27. package/dist/analyzers/edge-relationships.js.map +7 -0
  28. package/dist/analyzers/mapping-loader.js +9 -0
  29. package/dist/analyzers/mapping-loader.js.map +2 -2
  30. package/dist/analyzers/tier0-rules.js +153 -0
  31. package/dist/analyzers/tier0-rules.js.map +7 -0
  32. package/dist/analyzers/types.js.map +1 -1
  33. package/dist/audit/relationships/ai/prompts.js +10 -10
  34. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  35. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  36. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  37. package/dist/cli.js +12 -2
  38. package/dist/cli.js.map +2 -2
  39. package/dist/coding-agents/default-permissions.js +31 -1
  40. package/dist/coding-agents/default-permissions.js.map +2 -2
  41. package/dist/coding-agents/drbot-mcp.js +167 -0
  42. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  43. package/dist/commands/add.js +3 -2
  44. package/dist/commands/add.js.map +2 -2
  45. package/dist/commands/analyzer.js +117 -14
  46. package/dist/commands/analyzer.js.map +2 -2
  47. package/dist/commands/changeset-verify.js +55 -0
  48. package/dist/commands/changeset-verify.js.map +7 -0
  49. package/dist/commands/changeset.js +43 -22
  50. package/dist/commands/changeset.js.map +2 -2
  51. package/dist/commands/delete.js +10 -4
  52. package/dist/commands/delete.js.map +2 -2
  53. package/dist/commands/farm.js +176 -60
  54. package/dist/commands/farm.js.map +2 -2
  55. package/dist/commands/mcp.js +19 -4
  56. package/dist/commands/mcp.js.map +2 -2
  57. package/dist/commands/relationship.js +207 -26
  58. package/dist/commands/relationship.js.map +2 -2
  59. package/dist/commands/update.js +42 -23
  60. package/dist/commands/update.js.map +2 -2
  61. package/dist/commands/upgrade.js +99 -11
  62. package/dist/commands/upgrade.js.map +3 -3
  63. package/dist/commands/version.js +1 -1
  64. package/dist/commands/visualize.js +4 -3
  65. package/dist/commands/visualize.js.map +2 -2
  66. package/dist/core/catalog-migration.js +33 -0
  67. package/dist/core/catalog-migration.js.map +7 -0
  68. package/dist/core/changeset-analysis.js +421 -0
  69. package/dist/core/changeset-analysis.js.map +7 -0
  70. package/dist/core/changeset-base-index.js +153 -0
  71. package/dist/core/changeset-base-index.js.map +7 -0
  72. package/dist/core/changeset-brief-delta.js +102 -0
  73. package/dist/core/changeset-brief-delta.js.map +7 -0
  74. package/dist/core/changeset-graph.js +281 -0
  75. package/dist/core/changeset-graph.js.map +7 -0
  76. package/dist/core/changeset-only-guard.js +32 -0
  77. package/dist/core/changeset-only-guard.js.map +7 -0
  78. package/dist/core/changeset-summary.js +68 -0
  79. package/dist/core/changeset-summary.js.map +7 -0
  80. package/dist/core/changeset-validator.js +66 -20
  81. package/dist/core/changeset-validator.js.map +2 -2
  82. package/dist/core/changeset-verify.js +518 -0
  83. package/dist/core/changeset-verify.js.map +7 -0
  84. package/dist/core/changeset.js +56 -9
  85. package/dist/core/changeset.js.map +2 -2
  86. package/dist/core/element-type-migration.js +145 -0
  87. package/dist/core/element-type-migration.js.map +7 -0
  88. package/dist/core/farm-manifest.js +12 -6
  89. package/dist/core/farm-manifest.js.map +2 -2
  90. package/dist/core/farm-sync-engine.js +1 -1
  91. package/dist/core/farm-sync-engine.js.map +2 -2
  92. package/dist/core/farm-sync-state.js +10 -2
  93. package/dist/core/farm-sync-state.js.map +2 -2
  94. package/dist/core/migration-registry.js +75 -46
  95. package/dist/core/migration-registry.js.map +2 -2
  96. package/dist/core/model-upgrade.js +102 -0
  97. package/dist/core/model-upgrade.js.map +7 -0
  98. package/dist/core/model.js +82 -30
  99. package/dist/core/model.js.map +2 -2
  100. package/dist/core/mutation-handler.js +71 -6
  101. package/dist/core/mutation-handler.js.map +2 -2
  102. package/dist/core/relationship-migration-table.js +6639 -0
  103. package/dist/core/relationship-migration-table.js.map +7 -0
  104. package/dist/core/relationship-migration.js +272 -0
  105. package/dist/core/relationship-migration.js.map +7 -0
  106. package/dist/core/relationship-prune.js +49 -0
  107. package/dist/core/relationship-prune.js.map +7 -0
  108. package/dist/core/relationship-staging.js +68 -0
  109. package/dist/core/relationship-staging.js.map +7 -0
  110. package/dist/core/relationships.js +26 -4
  111. package/dist/core/relationships.js.map +2 -2
  112. package/dist/core/report-data-model.js +7 -13
  113. package/dist/core/report-data-model.js.map +2 -2
  114. package/dist/core/staged-changeset-storage.js +64 -17
  115. package/dist/core/staged-changeset-storage.js.map +2 -2
  116. package/dist/core/staged-source-reference.js +28 -0
  117. package/dist/core/staged-source-reference.js.map +7 -0
  118. package/dist/core/staging-area.js +83 -18
  119. package/dist/core/staging-area.js.map +2 -2
  120. package/dist/core/virtual-projection.js +13 -5
  121. package/dist/core/virtual-projection.js.map +2 -2
  122. package/dist/export/archimate-exporter.js +1 -0
  123. package/dist/export/archimate-exporter.js.map +2 -2
  124. package/dist/generated/compiled-validators.js +2 -2
  125. package/dist/generated/compiled-validators.js.map +2 -2
  126. package/dist/generated/layer-registry.js +16 -16
  127. package/dist/generated/layer-registry.js.map +2 -2
  128. package/dist/generated/node-types.js +186 -168
  129. package/dist/generated/node-types.js.map +2 -2
  130. package/dist/generated/relationship-index.js +2236 -7790
  131. package/dist/generated/relationship-index.js.map +2 -2
  132. package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
  133. package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
  134. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  135. package/dist/integrations/claude_code/commands/dr-map.md +13 -0
  136. package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
  137. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  138. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  139. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  140. package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
  141. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  142. package/dist/integrations/claude_code/docs/README.md +48 -1
  143. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  144. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  145. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  146. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  147. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  148. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  149. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  150. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  151. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  152. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  153. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  154. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  155. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
  156. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
  157. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
  158. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
  159. package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
  160. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  161. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  162. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  163. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  164. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  165. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  166. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  167. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  168. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  169. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  170. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  171. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  172. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
  173. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
  174. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  175. package/dist/integrations/hash-utils.js.map +2 -2
  176. package/dist/mcp/api-key-manager.js +14 -1
  177. package/dist/mcp/api-key-manager.js.map +2 -2
  178. package/dist/mcp/changeset-only.js +34 -0
  179. package/dist/mcp/changeset-only.js.map +7 -0
  180. package/dist/mcp/tool-registry.js +19 -3
  181. package/dist/mcp/tool-registry.js.map +2 -2
  182. package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
  183. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  184. package/dist/mcp/tools/changeset-editing.js +184 -0
  185. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  186. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  187. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  188. package/dist/mcp/tools/changeset-shared.js +31 -0
  189. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  190. package/dist/mcp/tools/changeset-show.js +17 -1
  191. package/dist/mcp/tools/changeset-show.js.map +2 -2
  192. package/dist/mcp/tools/changeset-verify.js +43 -0
  193. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  194. package/dist/mcp/tools/index.js +19 -1
  195. package/dist/mcp/tools/index.js.map +2 -2
  196. package/dist/mcp/tools/model-add.js +6 -5
  197. package/dist/mcp/tools/model-add.js.map +2 -2
  198. package/dist/mcp/tools/model-delete.js +2 -2
  199. package/dist/mcp/tools/model-delete.js.map +2 -2
  200. package/dist/mcp/tools/model-search.js +14 -0
  201. package/dist/mcp/tools/model-search.js.map +2 -2
  202. package/dist/mcp/tools/model-show.js +29 -2
  203. package/dist/mcp/tools/model-show.js.map +2 -2
  204. package/dist/mcp/tools/model-trace.js +93 -30
  205. package/dist/mcp/tools/model-trace.js.map +2 -2
  206. package/dist/mcp/tools/model-update.js +35 -19
  207. package/dist/mcp/tools/model-update.js.map +2 -2
  208. package/dist/mcp/tools/relationship-tools.js +98 -0
  209. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  210. package/dist/mcp/tools/shared.js +25 -2
  211. package/dist/mcp/tools/shared.js.map +2 -2
  212. package/dist/mcp/tools/spec-type.js +90 -0
  213. package/dist/mcp/tools/spec-type.js.map +7 -0
  214. package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
  215. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  216. package/dist/schemas/bundled/api.json +1029 -1658
  217. package/dist/schemas/bundled/apm.json +499 -734
  218. package/dist/schemas/bundled/application.json +735 -371
  219. package/dist/schemas/bundled/base.json +3 -220
  220. package/dist/schemas/bundled/business.json +452 -366
  221. package/dist/schemas/bundled/data-model.json +395 -411
  222. package/dist/schemas/bundled/data-store.json +334 -558
  223. package/dist/schemas/bundled/manifest.json +18 -18
  224. package/dist/schemas/bundled/motivation.json +349 -368
  225. package/dist/schemas/bundled/navigation.json +415 -731
  226. package/dist/schemas/bundled/product.json +325 -159
  227. package/dist/schemas/bundled/security.json +864 -1232
  228. package/dist/schemas/bundled/technology.json +574 -703
  229. package/dist/schemas/bundled/testing.json +718 -1643
  230. package/dist/schemas/bundled/ux.json +739 -1352
  231. package/dist/server/schemas.js +8 -0
  232. package/dist/server/schemas.js.map +2 -2
  233. package/dist/server/server.js +218 -34
  234. package/dist/server/server.js.map +2 -2
  235. package/dist/server-entry.js +2 -1
  236. package/dist/server-entry.js.map +2 -2
  237. package/dist/telemetry/index.js +1 -1
  238. package/dist/utils/ansi-suppressor.js +6 -8
  239. package/dist/utils/ansi-suppressor.js.map +2 -2
  240. package/dist/utils/attribute-patch.js +41 -0
  241. package/dist/utils/attribute-patch.js.map +7 -0
  242. package/dist/utils/error-hints.js +29 -0
  243. package/dist/utils/error-hints.js.map +7 -0
  244. package/dist/utils/errors.js +9 -3
  245. package/dist/utils/errors.js.map +2 -2
  246. package/dist/utils/project-paths.js +26 -1
  247. package/dist/utils/project-paths.js.map +2 -2
  248. package/dist/utils/source-reference.js +14 -12
  249. package/dist/utils/source-reference.js.map +2 -2
  250. package/dist/utils/spec-version.js +2 -2
  251. package/dist/utils/spec-version.js.map +1 -1
  252. package/dist/validators/composed-reference-validator.js +5 -27
  253. package/dist/validators/composed-reference-validator.js.map +2 -2
  254. package/dist/validators/reference-validator.js +0 -7
  255. package/dist/validators/reference-validator.js.map +2 -2
  256. package/dist/validators/relationship-schema-validator.js +3 -3
  257. package/dist/validators/relationship-schema-validator.js.map +2 -2
  258. package/dist/validators/schema-validator.js +27 -9
  259. package/dist/validators/schema-validator.js.map +2 -2
  260. package/dist/validators/types.js.map +1 -1
  261. package/dist/viewer/README.md +6 -6
  262. package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
  263. package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
  264. package/dist/viewer/index.html +2 -2
  265. package/dist/viewer/manifest.json +10 -10
  266. package/dist/viewer/version.json +1 -1
  267. package/package.json +2 -1
  268. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  269. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -0,0 +1,167 @@
1
+ import { existsSync } from "node:fs";
2
+ import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ApiKeyManager } from "../mcp/api-key-manager.js";
7
+ import { CHANGESET_ONLY_ENV } from "../core/changeset-only-guard.js";
8
+ const DR_MCP_SERVER_NAME = "dr", DR_MCP_ROOT_PATH_ENV = "DR_MCP_ROOT_PATH", DR_CLI_ENTRY_ENV = "DR_CLI_ENTRY", DR_MCP_READ_TOOLS = Object.freeze([
9
+ "model_list",
10
+ "model_show",
11
+ "model_search",
12
+ "model_stats",
13
+ "model_info",
14
+ "model_trace",
15
+ "model_validate",
16
+ "model_export",
17
+ "model_reload",
18
+ "changeset_list",
19
+ "changeset_show",
20
+ "changeset_impact",
21
+ "changeset_brief",
22
+ "changeset_verify",
23
+ "spec_type",
24
+ "annotation_list",
25
+ "annotation_get",
26
+ "chat_status"
27
+ ]), DR_MCP_CHANGESET_TOOLS = Object.freeze([
28
+ "changeset_create",
29
+ "changeset_activate",
30
+ "changeset_deactivate",
31
+ "model_add",
32
+ "model_update",
33
+ "model_delete",
34
+ "relationship_add",
35
+ "relationship_delete",
36
+ "changeset_update",
37
+ "changeset_unstage"
38
+ ]);
39
+ function drMcpToolRule(tool) {
40
+ return `mcp__${DR_MCP_SERVER_NAME}__${tool}`;
41
+ }
42
+ function drMcpAllowedTools(mode) {
43
+ return mode === "danger" ? [] : (mode === "changesets" ? [...DR_MCP_READ_TOOLS, ...DR_MCP_CHANGESET_TOOLS] : [...DR_MCP_READ_TOOLS]).map(drMcpToolRule);
44
+ }
45
+ function resolveDrCommand(env = process.env, exists = existsSync) {
46
+ const override = env[DR_CLI_ENTRY_ENV];
47
+ if (override && exists(override))
48
+ return { command: process.execPath, args: [override] };
49
+ const bundled = fileURLToPath(new URL("../cli.js", import.meta.url));
50
+ return exists(bundled) ? { command: process.execPath, args: [bundled] } : { command: "dr", args: [] };
51
+ }
52
+ function buildDrMcpConfig(options) {
53
+ const env = {
54
+ DR_MCP_API_KEY: options.apiKey,
55
+ [DR_MCP_ROOT_PATH_ENV]: options.rootPath,
56
+ // The key is already in this private config; don't print it into the client's MCP logs.
57
+ DR_MCP_QUIET_KEY: "1"
58
+ };
59
+ return options.changesetOnly && (env[CHANGESET_ONLY_ENV] = "true"), {
60
+ mcpServers: {
61
+ [DR_MCP_SERVER_NAME]: {
62
+ type: "stdio",
63
+ command: options.drCommand.command,
64
+ args: [...options.drCommand.args, "mcp"],
65
+ env
66
+ }
67
+ }
68
+ };
69
+ }
70
+ async function prepareDrMcpLaunch(options) {
71
+ const keyManager = options.keyManager ?? new ApiKeyManager(), { key } = await keyManager.ensureKey(), config = buildDrMcpConfig({
72
+ drCommand: options.drCommand ?? resolveDrCommand(),
73
+ apiKey: key,
74
+ rootPath: options.rootPath,
75
+ changesetOnly: options.changesetOnly
76
+ }), dir = await mkdtemp(join(options.tempRoot ?? tmpdir(), "dr-drbot-mcp-"));
77
+ let removed = !1;
78
+ const cleanup = async () => {
79
+ if (!removed) {
80
+ removed = !0;
81
+ try {
82
+ await rm(dir, { recursive: !0, force: !0 });
83
+ } catch (error) {
84
+ console.warn(`[DrBot] Could not remove MCP config directory ${dir}: ${error.message}`);
85
+ }
86
+ }
87
+ };
88
+ try {
89
+ await chmod(dir, 448);
90
+ const configPath = join(dir, "mcp.json");
91
+ return await writeFile(configPath, JSON.stringify(config, null, 2), {
92
+ mode: 384
93
+ }), { configPath, cleanup };
94
+ } catch (error) {
95
+ throw await cleanup(), error;
96
+ }
97
+ }
98
+ function drBotMcpPrompt(mode) {
99
+ const lines = [
100
+ `The Documentation Robotics MCP server "${DR_MCP_SERVER_NAME}" is connected for the model this session serves (tools named mcp__${DR_MCP_SERVER_NAME}__*). Prefer these tools to running dr through Bash: model_search/model_show/model_list to look things up, spec_type to check what a type takes and which relationships the catalog allows, changeset_impact and changeset_brief to pressure-test a change.`
101
+ ];
102
+ return mode === "changesets" ? lines.push("This session may change the model only through a changeset. Call changeset_create first (it activates the new changeset), then stage changes with model_add, model_update, model_delete, relationship_add and relationship_delete, and review them with changeset_brief. Writes with no active changeset are refused. Never commit or apply a changeset: the user reviews and commits it.") : mode === "read-safe" && lines.push("This session is read-only: answer from the model, and do not try to change it."), lines.join(`
103
+
104
+ `);
105
+ }
106
+ const DR_ARCHITECT_AGENT = "dr-architect";
107
+ function parseAgentFrontmatterTools(markdown) {
108
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
109
+ if (!match)
110
+ return null;
111
+ const lines = match[1].split(/\r?\n/), index = lines.findIndex((line) => /^tools\s*:/.test(line));
112
+ if (index === -1)
113
+ return null;
114
+ const unquote = (value) => value.trim().replace(/^["']|["']$/g, "").trim(), inline = lines[index].replace(/^tools\s*:/, "").trim();
115
+ if (inline)
116
+ return inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter(Boolean);
117
+ const tools = [];
118
+ for (const line of lines.slice(index + 1)) {
119
+ const item = /^\s+-\s*(.+)$/.exec(line);
120
+ if (!item)
121
+ break;
122
+ tools.push(unquote(item[1]));
123
+ }
124
+ return tools;
125
+ }
126
+ function findInstalledAgentFile(rootPath, homeDir = homedir(), exists = existsSync) {
127
+ for (const base of [rootPath, homeDir]) {
128
+ const candidate = join(base, ".claude", "agents", `${DR_ARCHITECT_AGENT}.md`);
129
+ if (exists(candidate))
130
+ return candidate;
131
+ }
132
+ return null;
133
+ }
134
+ function drMcpToolsForMode(mode) {
135
+ return mode === "danger" ? [...DR_MCP_READ_TOOLS, ...DR_MCP_CHANGESET_TOOLS].map(drMcpToolRule) : drMcpAllowedTools(mode);
136
+ }
137
+ async function checkInstalledAgentMcpTools(rootPath, mode, homeDir = homedir()) {
138
+ const path = findInstalledAgentFile(rootPath, homeDir);
139
+ if (!path)
140
+ return null;
141
+ const tools = parseAgentFrontmatterTools(await readFile(path, "utf8"));
142
+ if (tools === null)
143
+ return null;
144
+ const listed = new Set(tools);
145
+ if (listed.has(`mcp__${DR_MCP_SERVER_NAME}`) || listed.has(`mcp__${DR_MCP_SERVER_NAME}__*`))
146
+ return null;
147
+ const required = drMcpToolsForMode(mode), missing = required.filter((rule) => !listed.has(rule));
148
+ return missing.length === 0 ? null : { path, missing, noneVisible: missing.length === required.length };
149
+ }
150
+ export {
151
+ DR_ARCHITECT_AGENT,
152
+ DR_CLI_ENTRY_ENV,
153
+ DR_MCP_CHANGESET_TOOLS,
154
+ DR_MCP_READ_TOOLS,
155
+ DR_MCP_ROOT_PATH_ENV,
156
+ DR_MCP_SERVER_NAME,
157
+ buildDrMcpConfig,
158
+ checkInstalledAgentMcpTools,
159
+ drBotMcpPrompt,
160
+ drMcpAllowedTools,
161
+ drMcpToolRule,
162
+ findInstalledAgentFile,
163
+ parseAgentFrontmatterTools,
164
+ prepareDrMcpLaunch,
165
+ resolveDrCommand
166
+ };
167
+ //# sourceMappingURL=drbot-mcp.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/coding-agents/drbot-mcp.ts"],
4
+ "sourcesContent": ["/**\n * DrBot on MCP: connect the viewer's chat agent to the dr MCP server.\n *\n * `dr visualize` launches Claude Code as DrBot (server/server.ts). Rather than having the\n * agent shell out to `dr` through Bash, the launch hands Claude Code an MCP config that runs\n * this same dr installation's `dr mcp` over stdio, pointed at the served model, so DrBot plans\n * changes with the change-modeling tools (changeset_create, spec_type, model_add,\n * relationship_add, changeset_brief) the way any other agent does.\n *\n * Authentication: `dr mcp` over stdio requires DR_MCP_API_KEY to match the installation's\n * stored key (mcp/api-key-manager.ts). Over stdio the client supplies both the process and its\n * environment, so a key minted per launch would only be checked against itself; the launch\n * instead resolves the installation key (creating it on first use, as `dr mcp` does) and puts\n * it only in a private temp config file (0600, in a 0700 directory under the OS temp dir),\n * never in the repo, the model directory or the process arguments. The file is removed when the\n * agent process exits.\n *\n * Tolerance: if anything here fails (no key, no temp dir), the caller launches DrBot without\n * MCP and it falls back to the read-safe Bash rules; the failure is logged, not swallowed.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdtemp, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ApiKeyManager } from \"../mcp/api-key-manager.js\";\nimport { CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\n\n/** Name the dr MCP server is registered under; Claude Code exposes its tools as mcp__dr__<tool>. */\nexport const DR_MCP_SERVER_NAME = \"dr\";\n\n/** Env var `dr mcp` honours to serve a model from a directory other than its cwd. */\nexport const DR_MCP_ROOT_PATH_ENV = \"DR_MCP_ROOT_PATH\";\n\n/** Env var that overrides the dr CLI entry script DrBot's MCP server runs (tests, custom installs). */\nexport const DR_CLI_ENTRY_ENV = \"DR_CLI_ENTRY\";\n\n/** Tools that only read: allowed in every DrBot mode. */\nexport const DR_MCP_READ_TOOLS: readonly string[] = Object.freeze([\n \"model_list\",\n \"model_show\",\n \"model_search\",\n \"model_stats\",\n \"model_info\",\n \"model_trace\",\n \"model_validate\",\n \"model_export\",\n \"model_reload\",\n \"changeset_list\",\n \"changeset_show\",\n \"changeset_impact\",\n \"changeset_brief\",\n \"changeset_verify\",\n \"spec_type\",\n \"annotation_list\",\n \"annotation_get\",\n \"chat_status\",\n]);\n\n/**\n * Tools added in changeset-only mode (`dr visualize --with-changesets`): open and work in a\n * changeset and stage element and relationship changes. The MCP server runs with\n * DR_CHANGESET_ONLY set, so any of these that would write the base model (no active changeset)\n * is refused there (mcp/changeset-only.ts). changeset_update and changeset_unstage revise the\n * plan. Commit and apply are not MCP tools at all, and changeset_discard (which deletes the\n * staged changes) stays out: abandoning a changeset is the user's call.\n * chat_launch (an agent launching another agent) and the annotation writers stay out.\n */\nexport const DR_MCP_CHANGESET_TOOLS: readonly string[] = Object.freeze([\n \"changeset_create\",\n \"changeset_activate\",\n \"changeset_deactivate\",\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n \"changeset_update\",\n \"changeset_unstage\",\n]);\n\nexport type DrBotMode = \"read-safe\" | \"changesets\" | \"danger\";\n\n/** Claude Code permission rule for one dr MCP tool. */\nexport function drMcpToolRule(tool: string): string {\n return `mcp__${DR_MCP_SERVER_NAME}__${tool}`;\n}\n\n/** The mcp__dr__* rules DrBot may use without prompting in a mode (danger mode skips permissions). */\nexport function drMcpAllowedTools(mode: DrBotMode): string[] {\n if (mode === \"danger\") return [];\n const tools = mode === \"changesets\" ? [...DR_MCP_READ_TOOLS, ...DR_MCP_CHANGESET_TOOLS] : [...DR_MCP_READ_TOOLS];\n return tools.map(drMcpToolRule);\n}\n\n/** How to run this dr installation: a runtime plus the CLI entry script, or `dr` from PATH. */\nexport interface DrCommand {\n command: string;\n args: string[];\n}\n\n/**\n * Resolves the command that runs this same dr installation. The built CLI lives next to this\n * module's parent (dist/coding-agents/ \u2192 dist/cli.js) and runs under whichever runtime is\n * running now; DR_CLI_ENTRY overrides the script. Falls back to `dr` on PATH.\n */\nexport function resolveDrCommand(\n env: NodeJS.ProcessEnv = process.env,\n exists: (path: string) => boolean = existsSync\n): DrCommand {\n const override = env[DR_CLI_ENTRY_ENV];\n if (override && exists(override)) {\n return { command: process.execPath, args: [override] };\n }\n const bundled = fileURLToPath(new URL(\"../cli.js\", import.meta.url));\n if (exists(bundled)) {\n return { command: process.execPath, args: [bundled] };\n }\n return { command: \"dr\", args: [] };\n}\n\nexport interface DrMcpConfigOptions {\n drCommand: DrCommand;\n apiKey: string;\n /** Project root of the served model. */\n rootPath: string;\n /** Run the server in changeset-only mode (DR_CHANGESET_ONLY=true). */\n changesetOnly: boolean;\n}\n\n/** The Claude Code MCP config (the --mcp-config file's content) for DrBot's dr server. */\nexport function buildDrMcpConfig(options: DrMcpConfigOptions): {\n mcpServers: Record<\n string,\n {\n type: \"stdio\";\n command: string;\n args: string[];\n env: Record<string, string>;\n }\n >;\n} {\n const env: Record<string, string> = {\n DR_MCP_API_KEY: options.apiKey,\n [DR_MCP_ROOT_PATH_ENV]: options.rootPath,\n // The key is already in this private config; don't print it into the client's MCP logs.\n DR_MCP_QUIET_KEY: \"1\",\n };\n if (options.changesetOnly) {\n env[CHANGESET_ONLY_ENV] = \"true\";\n }\n return {\n mcpServers: {\n [DR_MCP_SERVER_NAME]: {\n type: \"stdio\",\n command: options.drCommand.command,\n args: [...options.drCommand.args, \"mcp\"],\n env,\n },\n },\n };\n}\n\n/** A written MCP config and the function that removes it again (safe to call more than once). */\nexport interface DrMcpLaunch {\n configPath: string;\n cleanup: () => Promise<void>;\n}\n\nexport interface PrepareDrMcpLaunchOptions {\n rootPath: string;\n changesetOnly: boolean;\n keyManager?: Pick<ApiKeyManager, \"ensureKey\">;\n drCommand?: DrCommand;\n /** Parent directory for the private config directory; defaults to the OS temp dir. */\n tempRoot?: string;\n}\n\n/**\n * Writes DrBot's MCP config to a private temp file. Throws on failure; the caller decides to\n * launch without MCP.\n */\nexport async function prepareDrMcpLaunch(options: PrepareDrMcpLaunchOptions): Promise<DrMcpLaunch> {\n const keyManager = options.keyManager ?? new ApiKeyManager();\n // No prompt: the server is non-interactive, so a first-time key goes to the default path.\n const { key } = await keyManager.ensureKey();\n const config = buildDrMcpConfig({\n drCommand: options.drCommand ?? resolveDrCommand(),\n apiKey: key,\n rootPath: options.rootPath,\n changesetOnly: options.changesetOnly,\n });\n\n const dir = await mkdtemp(join(options.tempRoot ?? tmpdir(), \"dr-drbot-mcp-\"));\n let removed = false;\n const cleanup = async () => {\n if (removed) return;\n removed = true;\n try {\n await rm(dir, { recursive: true, force: true });\n } catch (error) {\n console.warn(`[DrBot] Could not remove MCP config directory ${dir}: ${(error as Error).message}`);\n }\n };\n\n try {\n await chmod(dir, 0o700);\n const configPath = join(dir, \"mcp.json\");\n await writeFile(configPath, JSON.stringify(config, null, 2), {\n mode: 0o600,\n });\n return { configPath, cleanup };\n } catch (error) {\n await cleanup();\n throw error;\n }\n}\n\n/** Appended to DrBot's system prompt when the dr MCP server is connected. */\nexport function drBotMcpPrompt(mode: DrBotMode): string {\n const lines = [\n `The Documentation Robotics MCP server \"${DR_MCP_SERVER_NAME}\" is connected for the model this session serves ` +\n `(tools named mcp__${DR_MCP_SERVER_NAME}__*). Prefer these tools to running dr through Bash: ` +\n \"model_search/model_show/model_list to look things up, spec_type to check what a type takes and which \" +\n \"relationships the catalog allows, changeset_impact and changeset_brief to pressure-test a change.\",\n ];\n if (mode === \"changesets\") {\n lines.push(\n \"This session may change the model only through a changeset. Call changeset_create first (it activates the \" +\n \"new changeset), then stage changes with model_add, model_update, model_delete, relationship_add and \" +\n \"relationship_delete, and review them with changeset_brief. Writes with no active changeset are refused. \" +\n \"Never commit or apply a changeset: the user reviews and commits it.\"\n );\n } else if (mode === \"read-safe\") {\n lines.push(\"This session is read-only: answer from the model, and do not try to change it.\");\n }\n return lines.join(\"\\n\\n\");\n}\n\n/** The Claude Code agent DrBot runs as (`claude --agent dr-architect`). */\nexport const DR_ARCHITECT_AGENT = \"dr-architect\";\n\n/**\n * The tool names in an agent file's frontmatter `tools:` field, or null when the file has no\n * `tools:` field (Claude Code then gives the agent every tool, MCP tools included). Accepts the\n * comma-separated form, a flow list (`[a, b]`) and a block list.\n */\nexport function parseAgentFrontmatterTools(markdown: string): string[] | null {\n const match = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---/.exec(markdown);\n if (!match) return null;\n const lines = match[1].split(/\\r?\\n/);\n const index = lines.findIndex((line) => /^tools\\s*:/.test(line));\n if (index === -1) return null;\n const unquote = (value: string) =>\n value\n .trim()\n .replace(/^[\"']|[\"']$/g, \"\")\n .trim();\n const inline = lines[index].replace(/^tools\\s*:/, \"\").trim();\n if (inline) {\n return inline\n .replace(/^\\[|\\]$/g, \"\")\n .split(\",\")\n .map(unquote)\n .filter(Boolean);\n }\n const tools: string[] = [];\n for (const line of lines.slice(index + 1)) {\n const item = /^\\s+-\\s*(.+)$/.exec(line);\n if (!item) break;\n tools.push(unquote(item[1]));\n }\n return tools;\n}\n\n/**\n * The dr-architect agent file Claude Code loads for a session run in `rootPath`: the project's\n * .claude/agents copy wins over the user's ~/.claude/agents copy. Null when neither exists.\n */\nexport function findInstalledAgentFile(\n rootPath: string,\n homeDir: string = homedir(),\n exists: (path: string) => boolean = existsSync\n): string | null {\n for (const base of [rootPath, homeDir]) {\n const candidate = join(base, \".claude\", \"agents\", `${DR_ARCHITECT_AGENT}.md`);\n if (exists(candidate)) return candidate;\n }\n return null;\n}\n\n/** The dr MCP tools a mode uses that the installed dr-architect agent can't see. */\nexport interface AgentMcpToolCheck {\n /** The agent file that was checked. */\n path: string;\n /** The mcp__dr__* rules the mode uses that the agent's `tools:` field leaves out. */\n missing: string[];\n /** True when the agent can see none of them (a copy installed before DrBot used MCP). */\n noneVisible: boolean;\n}\n\n/** Every dr MCP tool DrBot can use in a mode (danger mode may use them all). */\nfunction drMcpToolsForMode(mode: DrBotMode): string[] {\n return mode === \"danger\"\n ? [...DR_MCP_READ_TOOLS, ...DR_MCP_CHANGESET_TOOLS].map(drMcpToolRule)\n : drMcpAllowedTools(mode);\n}\n\n/**\n * Checks that the installed dr-architect agent lists the dr MCP tools DrBot's mode uses. An\n * agent's `tools:` field limits what it can call, so a copy installed before DrBot moved to MCP\n * can't see the server at all, whatever --allowedTools says. Null when there's nothing to report:\n * no agent file found, no `tools:` field, a server-wide `mcp__dr` entry, or every tool listed.\n */\nexport async function checkInstalledAgentMcpTools(\n rootPath: string,\n mode: DrBotMode,\n homeDir: string = homedir()\n): Promise<AgentMcpToolCheck | null> {\n const path = findInstalledAgentFile(rootPath, homeDir);\n if (!path) return null;\n const tools = parseAgentFrontmatterTools(await readFile(path, \"utf8\"));\n if (tools === null) return null;\n const listed = new Set(tools);\n if (listed.has(`mcp__${DR_MCP_SERVER_NAME}`) || listed.has(`mcp__${DR_MCP_SERVER_NAME}__*`)) return null;\n const required = drMcpToolsForMode(mode);\n const missing = required.filter((rule) => !listed.has(rule));\n if (missing.length === 0) return null;\n return { path, missing, noneVisible: missing.length === required.length };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -4,6 +4,7 @@ import { Layer } from "../core/layer.js";
4
4
  import { Element } from "../core/element.js";
5
5
  import { MutationHandler } from "../core/mutation-handler.js";
6
6
  import { validateSourceReferenceOptions, buildSourceReference } from "../utils/source-reference.js";
7
+ import { schemaLookupHint } from "../utils/error-hints.js";
7
8
  import { SchemaValidator } from "../validators/schema-validator.js";
8
9
  import { getActiveSpan } from "../telemetry/index.js";
9
10
  import { generateElementId, generateUUID } from "../utils/id-generator.js";
@@ -36,7 +37,7 @@ async function addCommand(layer, type, name, options) {
36
37
  span?.setAttribute("layer.name", layer), span?.setAttribute("element.type", resolvedType), span?.setAttribute("element.id", elementPath), validateSourceReferenceOptions(options);
37
38
  let model;
38
39
  try {
39
- model = await Model.load(options.model || process.cwd(), { layers: [layer] });
40
+ model = await Model.load(options.model, { layers: [layer] });
40
41
  } catch (error) {
41
42
  const message = getErrorMessage(error);
42
43
  throw message.includes("No DR project") || message.includes("Model not found") ? new ModelNotFoundError() : error;
@@ -78,7 +79,7 @@ async function addCommand(layer, type, name, options) {
78
79
  const errorMessages = attrValidation.errors.map((e) => ` ${e.message}`).join(`
79
80
  `);
80
81
  throw new CLIError(`Element ${elementPath} has invalid attributes:
81
- ${errorMessages}`, ErrorCategory.USER, [`Run "dr schema ${layer} ${type}" to see the required attributes`]);
82
+ ${errorMessages}`, ErrorCategory.USER, [schemaLookupHint("cli", layer, resolvedType)]);
82
83
  }
83
84
  }
84
85
  if (layerObj.getElement(elementPath))
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/add.ts"],
4
- "sourcesContent": ["/**\n * Add an element to a layer\n */\n\nimport ansis from \"ansis\";\nimport { Model } from \"../core/model.js\";\nimport { Layer } from \"../core/layer.js\";\nimport { Element } from \"../core/element.js\";\nimport { MutationHandler } from \"../core/mutation-handler.js\";\nimport { validateSourceReferenceOptions, buildSourceReference } from \"../utils/source-reference.js\";\nimport { SchemaValidator } from \"../validators/schema-validator.js\";\nimport { getActiveSpan } from \"../telemetry/index.js\";\nimport { generateElementId, generateUUID } from \"../utils/id-generator.js\";\nimport { getAllLayerIds, isValidLayer } from \"../generated/layer-registry.js\";\nimport { isValidNodeType, getNodeTypesForLayer, normalizeNodeType } from \"../generated/node-types.js\";\nimport {\n CLIError,\n ErrorCategory,\n InvalidJSONError,\n ModelNotFoundError,\n findSimilar,\n formatValidOptions,\n getErrorMessage,\n handleError,\n handleSuccess,\n} from \"../utils/errors.js\";\n\n\nexport interface AddOptions {\n model?: string;\n name?: string;\n description?: string;\n attributes?: string;\n sourceFile?: string;\n sourceSymbol?: string;\n sourceProvenance?: string;\n sourceRepoRemote?: string;\n sourceRepoCommit?: string;\n verbose?: boolean;\n debug?: boolean;\n}\n\nexport async function addCommand(\n layer: string,\n type: string,\n name: string,\n options: AddOptions\n): Promise<void> {\n try {\n // Validate layer name\n if (!isValidLayer(layer)) {\n const validLayers = getAllLayerIds();\n const similar = findSimilar(layer, validLayers, 3);\n const suggestions: string[] = [`Use a valid layer name: ${formatValidOptions(validLayers)}`];\n if (similar.length > 0) {\n suggestions.unshift(`Did you mean: ${similar.join(\" or \")}?`);\n }\n throw new CLIError(`Unknown layer \"${layer}\"`, ErrorCategory.USER, suggestions, {\n operation: \"add\",\n context: `Layer: ${layer}, Type: ${type}, Name: ${name}`,\n });\n }\n\n // Validate element type for this layer\n if (!isValidNodeType(layer, type)) {\n const validNodeTypes = getNodeTypesForLayer(layer);\n const typeNames = validNodeTypes.map((t) => t.type).sort();\n const similar = findSimilar(type, typeNames, 3);\n const suggestions: string[] = [`Valid types for ${layer}: ${formatValidOptions(typeNames)}`];\n if (similar.length > 0) {\n suggestions.unshift(`Did you mean: ${similar.join(\" or \")}?`);\n }\n throw new CLIError(\n `Invalid element type \"${type}\" for layer \"${layer}\"`,\n ErrorCategory.USER,\n suggestions,\n {\n operation: \"add\",\n context: `Layer: ${layer}, Type: ${type}, Name: ${name}`,\n }\n );\n }\n\n // Resolve abbreviated type names to their canonical spec names\n // e.g., \"service\" \u2192 \"applicationservice\" for the application layer\n const resolvedType = normalizeNodeType(layer, type);\n\n // Generate slug path: {layer}.{resolvedType}.{kebab-name}\n const elementPath = generateElementId(layer, resolvedType, name); // slug\n const elementUUID = generateUUID(); // UUIDv4\n\n // Validate the generated slug is non-empty (name may collapse to empty after stripping special chars)\n const slugPart = elementPath.split(\".\")[2];\n if (!slugPart) {\n throw new CLIError(\n `Cannot generate a valid element ID from name \"${name}\"`,\n ErrorCategory.USER,\n [\n \"Element names must contain at least one letter or digit\",\n `Generated ID would be: ${elementPath}`,\n ]\n );\n }\n\n const span = getActiveSpan();\n span?.setAttribute(\"layer.name\", layer);\n span?.setAttribute(\"element.type\", resolvedType);\n span?.setAttribute(\"element.id\", elementPath);\n\n // Validate source reference options\n validateSourceReferenceOptions(options);\n\n // Load model (with error handling for missing models)\n let model: Model;\n try {\n model = await Model.load(options.model || process.cwd(), { layers: [layer] });\n } catch (error) {\n const message = getErrorMessage(error);\n if (message.includes(\"No DR project\") || message.includes(\"Model not found\")) {\n throw new ModelNotFoundError();\n }\n throw error;\n }\n\n // Get or create layer\n let layerObj = await model.getLayer(layer);\n if (!layerObj) {\n layerObj = new Layer(layer);\n model.addLayer(layerObj);\n }\n\n // Parse attributes if provided\n let attributes: Record<string, unknown> = {};\n if (options.attributes) {\n try {\n attributes = JSON.parse(options.attributes);\n } catch (e) {\n throw new InvalidJSONError(options.attributes, \"--attributes\");\n }\n }\n\n // Create element in spec-node format\n // id = UUIDv4 for schema compliance; path = slug for human-readable identifier\n const element = new Element({\n id: elementUUID, // UUIDv4\n path: elementPath, // slug: {layer}.{resolvedType}.{kebab-name}\n spec_node_id: `${layer}.${resolvedType}`,\n layer_id: layer,\n type: resolvedType,\n name: options.name || name,\n description: options.description,\n attributes: attributes,\n metadata: {\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n version: 1,\n },\n layer, // Set layer so setSourceReference can use it\n });\n\n\n // Add source reference if provided\n const sourceRef = buildSourceReference(options);\n if (sourceRef) {\n element.setSourceReference(sourceRef);\n }\n\n // Validate element attributes against spec schema before persisting\n {\n const schemaValidator = new SchemaValidator();\n const tempLayer = new Layer(layer);\n tempLayer.addElement(element);\n const attrValidation = await schemaValidator.validateLayer(tempLayer);\n if (!attrValidation.isValid()) {\n const errorMessages = attrValidation.errors\n .map((e) => ` ${e.message}`)\n .join(\"\\n\");\n throw new CLIError(\n `Element ${elementPath} has invalid attributes:\\n${errorMessages}`,\n ErrorCategory.USER,\n [`Run \"dr schema ${layer} ${type}\" to see the required attributes`]\n );\n }\n }\n\n // Check if element already exists (look up by path)\n if (layerObj.getElement(elementPath)) {\n throw new CLIError(\n `Element ${elementPath} already exists in ${layer} layer`,\n ErrorCategory.USER,\n [\n `Use \"dr show ${elementPath}\" to view the existing element`,\n `Use \"dr update ${elementPath}\" to modify it`,\n `Use \"dr delete ${elementPath}\" to remove it first if you want to recreate it`,\n ],\n { operation: \"add\", context: `Duplicate element path` }\n );\n }\n\n // Unified mutation handler for add operation\n const handler = new MutationHandler(model, elementPath, layer);\n\n // Execute add through unified path (handles staging and base model consistently)\n await handler.executeAdd(element, (elem) => {\n // This mutator is called by executeAdd for base model path only\n layerObj.addElement(elem);\n });\n\n // Determine if operation was staged or applied to base model\n if (handler.getAfterState()) {\n // Check if we went through staging path\n const stagingManager = handler.getStagingManager();\n const activeChangeset = await stagingManager.getActive();\n if (activeChangeset && activeChangeset.status === \"staged\") {\n // Staging path\n handleSuccess(\n `Staged element ${ansis.bold(elementPath)} to ${ansis.bold(activeChangeset.name)}`,\n {\n elementId: elementPath,\n layer,\n type: resolvedType,\n name: options.name || name,\n changesetStatus: \"staged\",\n changeset: activeChangeset.name,\n },\n { verbose: options.verbose }\n );\n } else {\n // Base model path\n handleSuccess(`Added element ${ansis.bold(elementPath)} to ${ansis.bold(layer)} layer`, {\n elementId: elementPath,\n layer,\n type: resolvedType,\n name: options.name || name,\n description: options.description || \"(none)\",\n }, { verbose: true });\n }\n }\n } catch (error) {\n handleError(error);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Add an element to a layer\n */\n\nimport ansis from \"ansis\";\nimport { Model } from \"../core/model.js\";\nimport { Layer } from \"../core/layer.js\";\nimport { Element } from \"../core/element.js\";\nimport { MutationHandler } from \"../core/mutation-handler.js\";\nimport { validateSourceReferenceOptions, buildSourceReference } from \"../utils/source-reference.js\";\nimport { schemaLookupHint } from \"../utils/error-hints.js\";\nimport { SchemaValidator } from \"../validators/schema-validator.js\";\nimport { getActiveSpan } from \"../telemetry/index.js\";\nimport { generateElementId, generateUUID } from \"../utils/id-generator.js\";\nimport { getAllLayerIds, isValidLayer } from \"../generated/layer-registry.js\";\nimport { isValidNodeType, getNodeTypesForLayer, normalizeNodeType } from \"../generated/node-types.js\";\nimport {\n CLIError,\n ErrorCategory,\n InvalidJSONError,\n ModelNotFoundError,\n findSimilar,\n formatValidOptions,\n getErrorMessage,\n handleError,\n handleSuccess,\n} from \"../utils/errors.js\";\n\n\nexport interface AddOptions {\n model?: string;\n name?: string;\n description?: string;\n attributes?: string;\n sourceFile?: string;\n sourceSymbol?: string;\n sourceProvenance?: string;\n sourceRepoRemote?: string;\n sourceRepoCommit?: string;\n verbose?: boolean;\n debug?: boolean;\n}\n\nexport async function addCommand(\n layer: string,\n type: string,\n name: string,\n options: AddOptions\n): Promise<void> {\n try {\n // Validate layer name\n if (!isValidLayer(layer)) {\n const validLayers = getAllLayerIds();\n const similar = findSimilar(layer, validLayers, 3);\n const suggestions: string[] = [`Use a valid layer name: ${formatValidOptions(validLayers)}`];\n if (similar.length > 0) {\n suggestions.unshift(`Did you mean: ${similar.join(\" or \")}?`);\n }\n throw new CLIError(`Unknown layer \"${layer}\"`, ErrorCategory.USER, suggestions, {\n operation: \"add\",\n context: `Layer: ${layer}, Type: ${type}, Name: ${name}`,\n });\n }\n\n // Validate element type for this layer\n if (!isValidNodeType(layer, type)) {\n const validNodeTypes = getNodeTypesForLayer(layer);\n const typeNames = validNodeTypes.map((t) => t.type).sort();\n const similar = findSimilar(type, typeNames, 3);\n const suggestions: string[] = [`Valid types for ${layer}: ${formatValidOptions(typeNames)}`];\n if (similar.length > 0) {\n suggestions.unshift(`Did you mean: ${similar.join(\" or \")}?`);\n }\n throw new CLIError(\n `Invalid element type \"${type}\" for layer \"${layer}\"`,\n ErrorCategory.USER,\n suggestions,\n {\n operation: \"add\",\n context: `Layer: ${layer}, Type: ${type}, Name: ${name}`,\n }\n );\n }\n\n // Resolve abbreviated type names to their canonical spec names\n // e.g., \"service\" \u2192 \"applicationservice\" for the application layer\n const resolvedType = normalizeNodeType(layer, type);\n\n // Generate slug path: {layer}.{resolvedType}.{kebab-name}\n const elementPath = generateElementId(layer, resolvedType, name); // slug\n const elementUUID = generateUUID(); // UUIDv4\n\n // Validate the generated slug is non-empty (name may collapse to empty after stripping special chars)\n const slugPart = elementPath.split(\".\")[2];\n if (!slugPart) {\n throw new CLIError(\n `Cannot generate a valid element ID from name \"${name}\"`,\n ErrorCategory.USER,\n [\n \"Element names must contain at least one letter or digit\",\n `Generated ID would be: ${elementPath}`,\n ]\n );\n }\n\n const span = getActiveSpan();\n span?.setAttribute(\"layer.name\", layer);\n span?.setAttribute(\"element.type\", resolvedType);\n span?.setAttribute(\"element.id\", elementPath);\n\n // Validate source reference options\n validateSourceReferenceOptions(options);\n\n // Load model (with error handling for missing models)\n let model: Model;\n try {\n model = await Model.load(options.model, { layers: [layer] });\n } catch (error) {\n const message = getErrorMessage(error);\n if (message.includes(\"No DR project\") || message.includes(\"Model not found\")) {\n throw new ModelNotFoundError();\n }\n throw error;\n }\n\n // Get or create layer\n let layerObj = await model.getLayer(layer);\n if (!layerObj) {\n layerObj = new Layer(layer);\n model.addLayer(layerObj);\n }\n\n // Parse attributes if provided\n let attributes: Record<string, unknown> = {};\n if (options.attributes) {\n try {\n attributes = JSON.parse(options.attributes);\n } catch (e) {\n throw new InvalidJSONError(options.attributes, \"--attributes\");\n }\n }\n\n // Create element in spec-node format\n // id = UUIDv4 for schema compliance; path = slug for human-readable identifier\n const element = new Element({\n id: elementUUID, // UUIDv4\n path: elementPath, // slug: {layer}.{resolvedType}.{kebab-name}\n spec_node_id: `${layer}.${resolvedType}`,\n layer_id: layer,\n type: resolvedType,\n name: options.name || name,\n description: options.description,\n attributes: attributes,\n metadata: {\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n version: 1,\n },\n layer, // Set layer so setSourceReference can use it\n });\n\n\n // Add source reference if provided\n const sourceRef = buildSourceReference(options);\n if (sourceRef) {\n element.setSourceReference(sourceRef);\n }\n\n // Validate element attributes against spec schema before persisting\n {\n const schemaValidator = new SchemaValidator();\n const tempLayer = new Layer(layer);\n tempLayer.addElement(element);\n const attrValidation = await schemaValidator.validateLayer(tempLayer);\n if (!attrValidation.isValid()) {\n const errorMessages = attrValidation.errors\n .map((e) => ` ${e.message}`)\n .join(\"\\n\");\n throw new CLIError(\n `Element ${elementPath} has invalid attributes:\\n${errorMessages}`,\n ErrorCategory.USER,\n [schemaLookupHint(\"cli\", layer, resolvedType)]\n );\n }\n }\n\n // Check if element already exists (look up by path)\n if (layerObj.getElement(elementPath)) {\n throw new CLIError(\n `Element ${elementPath} already exists in ${layer} layer`,\n ErrorCategory.USER,\n [\n `Use \"dr show ${elementPath}\" to view the existing element`,\n `Use \"dr update ${elementPath}\" to modify it`,\n `Use \"dr delete ${elementPath}\" to remove it first if you want to recreate it`,\n ],\n { operation: \"add\", context: `Duplicate element path` }\n );\n }\n\n // Unified mutation handler for add operation\n const handler = new MutationHandler(model, elementPath, layer);\n\n // Execute add through unified path (handles staging and base model consistently)\n await handler.executeAdd(element, (elem) => {\n // This mutator is called by executeAdd for base model path only\n layerObj.addElement(elem);\n });\n\n // Determine if operation was staged or applied to base model\n if (handler.getAfterState()) {\n // Check if we went through staging path\n const stagingManager = handler.getStagingManager();\n const activeChangeset = await stagingManager.getActive();\n if (activeChangeset && activeChangeset.status === \"staged\") {\n // Staging path\n handleSuccess(\n `Staged element ${ansis.bold(elementPath)} to ${ansis.bold(activeChangeset.name)}`,\n {\n elementId: elementPath,\n layer,\n type: resolvedType,\n name: options.name || name,\n changesetStatus: \"staged\",\n changeset: activeChangeset.name,\n },\n { verbose: options.verbose }\n );\n } else {\n // Base model path\n handleSuccess(`Added element ${ansis.bold(elementPath)} to ${ansis.bold(layer)} layer`, {\n elementId: elementPath,\n layer,\n type: resolvedType,\n name: options.name || name,\n description: options.description || \"(none)\",\n }, { verbose: true });\n }\n }\n } catch (error) {\n handleError(error);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ import { CLIError, ModelNotFoundError, ErrorCategory, categorizeError } from "..
9
9
  import { findProjectRoot } from "../utils/project-paths.js";
10
10
  import { performDiscover } from "./discover-logic.js";
11
11
  import { formatVerifyReport } from "../export/verify-formatters.js";
12
+ import { Model } from "../core/model.js";
12
13
  function analyzerCommands(program) {
13
14
  const analyzer = program.command("analyzer").description("Manage analyzer discovery and indexing");
14
15
  analyzer.command("discover").description("Discover and select an analyzer").option("--json", "Output as JSON").option("--reselect", "Force re-selection even if analyzer already selected").addHelpText("after", `
@@ -131,11 +132,16 @@ Examples:
131
132
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND, ["Check the analyzer name with `dr analyzer discover`"]);
132
133
  const actualProjectRoot = await findProjectRoot();
133
134
  let status;
134
- if (actualProjectRoot ? status = await backend.status(actualProjectRoot) : status = {
135
- detected: await backend.detect(),
136
- indexed: !1,
137
- fresh: !1
138
- }, jsonMode) {
135
+ if (actualProjectRoot) {
136
+ const model = await Model.load(actualProjectRoot);
137
+ status = await backend.status(actualProjectRoot, { codebaseRoot: model.codebaseRoot });
138
+ } else
139
+ status = {
140
+ detected: await backend.detect(),
141
+ indexed: !1,
142
+ fresh: !1
143
+ };
144
+ if (jsonMode) {
139
145
  console.log(JSON.stringify(status, null, 2)), actualProjectRoot && await writeStatus(status, actualProjectRoot, analyzerName);
140
146
  return;
141
147
  }
@@ -174,8 +180,9 @@ Examples:
174
180
  `Visit: ${homepageUrl}`
175
181
  ]);
176
182
  }
183
+ const model = await Model.load(projectRoot);
177
184
  console.log(`Indexing project with ${analyzerName}...`);
178
- const result = await backend.index(projectRoot, { force: options.force });
185
+ const result = await backend.index(projectRoot, { force: options.force, codebaseRoot: model.codebaseRoot });
179
186
  console.log(ansis.green("\u2713 Indexing complete")), console.log(` Nodes: ${result.node_count}`), console.log(` Edges: ${result.edge_count}`), console.log(` Git HEAD: ${result.git_head}`);
180
187
  } catch (error) {
181
188
  if (error instanceof CLIError || error instanceof ModelNotFoundError)
@@ -205,9 +212,10 @@ Examples:
205
212
  const backend = await registry.getAnalyzer(analyzerName);
206
213
  if (!backend)
207
214
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
208
- if (!(await backend.status(projectRoot)).indexed)
215
+ const model = await Model.load(projectRoot);
216
+ if (!(await backend.status(projectRoot, { codebaseRoot: model.codebaseRoot })).indexed)
209
217
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` to index the project first"]);
210
- const endpoints = await backend.endpoints(projectRoot);
218
+ const endpoints = await backend.endpoints(projectRoot, { codebaseRoot: model.codebaseRoot });
211
219
  if (jsonMode) {
212
220
  console.log(JSON.stringify(endpoints, null, 2));
213
221
  return;
@@ -252,9 +260,10 @@ Examples:
252
260
  const backend = await registry.getAnalyzer(analyzerName);
253
261
  if (!backend)
254
262
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
263
+ const model = await Model.load(projectRoot);
255
264
  if (!await readIndexMeta(projectRoot, analyzerName))
256
265
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
257
- let services = await backend.services(projectRoot);
266
+ let services = await backend.services(projectRoot, { codebaseRoot: model.codebaseRoot });
258
267
  if (options.layer && (services = services.filter((s) => s.suggested_layer === options.layer)), jsonMode) {
259
268
  console.log(JSON.stringify(services, null, 2));
260
269
  return;
@@ -298,9 +307,10 @@ Examples:
298
307
  const backend = await registry.getAnalyzer(analyzerName);
299
308
  if (!backend)
300
309
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
310
+ const model = await Model.load(projectRoot);
301
311
  if (!await readIndexMeta(projectRoot, analyzerName))
302
312
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
303
- const datastores = await backend.datastores(projectRoot);
313
+ const datastores = await backend.datastores(projectRoot, { codebaseRoot: model.codebaseRoot });
304
314
  if (jsonMode) {
305
315
  console.log(JSON.stringify(datastores, null, 2));
306
316
  return;
@@ -348,9 +358,10 @@ Examples:
348
358
  const backend = await registry.getAnalyzer(analyzerName);
349
359
  if (!backend)
350
360
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
361
+ const model = await Model.load(projectRoot);
351
362
  if (!await readIndexMeta(projectRoot, analyzerName))
352
363
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
353
- const callers = await backend.callers(projectRoot, qualifiedName, depth);
364
+ const callers = await backend.callers(projectRoot, qualifiedName, depth, { codebaseRoot: model.codebaseRoot });
354
365
  if (jsonMode) {
355
366
  console.log(JSON.stringify(callers, null, 2));
356
367
  return;
@@ -394,9 +405,10 @@ Examples:
394
405
  const backend = await registry.getAnalyzer(analyzerName);
395
406
  if (!backend)
396
407
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
408
+ const model = await Model.load(projectRoot);
397
409
  if (!await readIndexMeta(projectRoot, analyzerName))
398
410
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
399
- const callees = await backend.callees(projectRoot, qualifiedName, depth);
411
+ const callees = await backend.callees(projectRoot, qualifiedName, depth, { codebaseRoot: model.codebaseRoot });
400
412
  if (jsonMode) {
401
413
  console.log(JSON.stringify(callees, null, 2));
402
414
  return;
@@ -441,9 +453,10 @@ Examples:
441
453
  const backend = await registry.getAnalyzer(analyzerName);
442
454
  if (!backend)
443
455
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
456
+ const model = await Model.load(projectRoot);
444
457
  if (!await readIndexMeta(projectRoot, analyzerName))
445
458
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
446
- const result = await backend.query(projectRoot, cypher);
459
+ const result = await backend.query(projectRoot, cypher, { codebaseRoot: model.codebaseRoot });
447
460
  options.json, console.log(JSON.stringify(result, null, 2));
448
461
  } catch (error) {
449
462
  if (error instanceof CLIError || error instanceof ModelNotFoundError)
@@ -493,11 +506,13 @@ Examples:
493
506
  const backend = await registry.getAnalyzer(analyzerName);
494
507
  if (!backend)
495
508
  throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
509
+ const model = await Model.load(projectRoot);
496
510
  if (!await readIndexMeta(projectRoot, analyzerName))
497
511
  throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
498
512
  const report = await backend.verify(projectRoot, {
499
513
  layers: apiLayers,
500
- changesetAware: !0
514
+ changesetAware: !0,
515
+ codebaseRoot: model.codebaseRoot
501
516
  }), formatted = formatVerifyReport(report, { format });
502
517
  if (options.output) {
503
518
  const outputPath = path.resolve(options.output);
@@ -511,6 +526,94 @@ Examples:
511
526
  const cliError = new CLIError(error instanceof Error ? error.message : String(error), categorizeError(error));
512
527
  throw error instanceof Error && (cliError.cause = error), cliError;
513
528
  }
529
+ }), analyzer.command("classify").description("Classify the indexed project's symbols into model layers and types (Tier 0: rules only)").option("--name <n>", "Analyzer name (defaults to active from session.json)").option("--tier0-only", "Apply only the analyzer's Tier-0 rules: no model calls, no network (required for now)").option("--label <labels>", "Comma-separated analyzer labels to classify (default: Class, Interface, Function, Method, Module, Route and every label a rule names)").option("--file <glob>", "Only symbols in files matching this glob").option("--exclude <regex>", "Also leave out files matching this regular expression (build and vendored paths always are)").option("--limit <n>", "Stop after this many symbols").option("--cards", "Include each symbol's full card (source and graph neighbourhood) in JSON output").option("--output <file>", "Write the JSON report to a file").option("--json", "Output as JSON").addHelpText("after", `
530
+ Tier 0 is the first step of the classification cascade (docs/analyzers/CASCADE_DESIGN.md):
531
+ the analyzer's rules decide a symbol's layer and type, reject it as not a model element,
532
+ or narrow its choices for a later tier. Symbols no rule covers are reported as undecided.
533
+ Nothing is written to the model.
534
+
535
+ Examples:
536
+ $ dr analyzer classify --tier0-only
537
+ $ dr analyzer classify --tier0-only --json > tier0.json
538
+ $ dr analyzer classify --tier0-only --label Class,Method --file "src/main/**"
539
+ $ dr analyzer classify --tier0-only --cards --output tier0-cards.json`).action(async (options) => {
540
+ const jsonMode = options.json ?? program.opts().json;
541
+ try {
542
+ if (!options.tier0Only)
543
+ throw new CLIError("Only Tier-0 classification is available so far: pass --tier0-only", ErrorCategory.USER, ["Model-backed classification (Tier 1) needs the per-project opt-in and is not built yet"]);
544
+ let exclude;
545
+ if (options.exclude !== void 0)
546
+ try {
547
+ exclude = new RegExp(String(options.exclude));
548
+ } catch (e) {
549
+ throw new CLIError(`Invalid --exclude regular expression: ${e instanceof Error ? e.message : String(e)}`, ErrorCategory.USER);
550
+ }
551
+ let limit;
552
+ if (options.limit !== void 0 && (limit = Number(options.limit), !Number.isInteger(limit) || limit < 1))
553
+ throw new CLIError(`--limit must be a positive integer, got '${options.limit}'`, ErrorCategory.USER);
554
+ const labels = options.label ? String(options.label).split(",").map((l) => l.trim()).filter(Boolean) : void 0;
555
+ options.cards && !jsonMode && !options.output && console.error(ansis.yellow("--cards only affects JSON output: add --json or --output <file>"));
556
+ const projectRoot = await findProjectRoot();
557
+ if (!projectRoot)
558
+ throw new ModelNotFoundError();
559
+ let analyzerName = options.name;
560
+ if (!analyzerName) {
561
+ const session = await readSession(projectRoot);
562
+ if (!session)
563
+ throw new CLIError("No analyzer selected. Run `dr analyzer discover` first.", ErrorCategory.USER);
564
+ analyzerName = session.active_analyzer;
565
+ }
566
+ const registry = AnalyzerRegistry.getInstance();
567
+ await registry.initialize();
568
+ const backend = await registry.getAnalyzer(analyzerName);
569
+ if (!backend)
570
+ throw new CLIError(`Analyzer not found: ${analyzerName}`, ErrorCategory.NOT_FOUND);
571
+ if (!backend.classify)
572
+ throw new CLIError(`The ${analyzerName} analyzer does not support classification`, ErrorCategory.USER);
573
+ if (!await readIndexMeta(projectRoot, analyzerName))
574
+ throw new CLIError("Project not indexed", ErrorCategory.USER, ["Run `dr analyzer index` first"]);
575
+ const model = await Model.load(projectRoot), showProgress = !jsonMode && !options.output && process.stderr.isTTY, report = await backend.classify(projectRoot, {
576
+ codebaseRoot: model.codebaseRoot,
577
+ labels,
578
+ filePattern: options.file,
579
+ exclude,
580
+ limit,
581
+ includeCards: !!options.cards,
582
+ onProgress: showProgress ? (done, total) => {
583
+ (done % 50 === 0 || done === total) && process.stderr.write(`\r ${done}/${total} symbols`), done === total && process.stderr.write(`
584
+ `);
585
+ } : void 0
586
+ });
587
+ if (options.output) {
588
+ const outputPath = path.resolve(options.output);
589
+ await fs.writeFile(outputPath, JSON.stringify(report, null, 2) + `
590
+ `, "utf-8"), jsonMode || console.log(ansis.green(`\u2713 Report written to ${outputPath}`));
591
+ }
592
+ if (jsonMode) {
593
+ console.log(JSON.stringify(report, null, 2));
594
+ return;
595
+ }
596
+ const { summary } = report, pct = (n) => summary.cards ? `${(100 * n / summary.cards).toFixed(0)}%` : "0%";
597
+ console.log(""), console.log(ansis.bold(`Tier-0 classification: ${summary.cards} symbols, ${report.rules} rules`)), console.log(ansis.dim("\u2500".repeat(60))), console.log(` ${ansis.green("decided")} ${String(summary.decided).padStart(6)} ${pct(summary.decided)}`), console.log(` ${ansis.yellow("rejected")} ${String(summary.rejected).padStart(6)} ${pct(summary.rejected)} (not model elements)`), console.log(` ${ansis.cyan("narrowed")} ${String(summary.narrowed).padStart(6)} ${pct(summary.narrowed)} (choices restricted for Tier 1)`), console.log(` ${ansis.dim("undecided")} ${String(summary.undecided).padStart(6)} ${pct(summary.undecided)}`), summary.no_source && console.log(ansis.yellow(` ${summary.no_source} symbol(s) had no source, so rules that read the declaration could not apply`));
598
+ const targets = Object.entries(summary.by_target).sort((a, b) => b[1] - a[1]);
599
+ if (targets.length) {
600
+ console.log(""), console.log(ansis.bold("Decided, by type:"));
601
+ for (const [target, n] of targets)
602
+ console.log(` ${target.padEnd(40)} ${String(n).padStart(6)}`);
603
+ }
604
+ const rules = Object.entries(summary.by_rule).sort((a, b) => b[1] - a[1]);
605
+ if (rules.length) {
606
+ console.log(""), console.log(ansis.bold("Matches, by rule:"));
607
+ for (const [rule, n] of rules)
608
+ console.log(` ${rule.padEnd(40)} ${String(n).padStart(6)}`);
609
+ }
610
+ console.log(ansis.dim("\u2500".repeat(60))), console.log(ansis.dim("Per-symbol outcomes: --json or --output <file>")), console.log("");
611
+ } catch (error) {
612
+ if (error instanceof CLIError || error instanceof ModelNotFoundError)
613
+ throw error;
614
+ const cliError = new CLIError(error instanceof Error ? error.message : String(error), categorizeError(error));
615
+ throw error instanceof Error && (cliError.cause = error), cliError;
616
+ }
514
617
  });
515
618
  }
516
619
  export {