@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.
- package/README.md +36 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +3 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +117 -14
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +43 -22
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/mcp.js +19 -4
- package/dist/commands/mcp.js.map +2 -2
- package/dist/commands/relationship.js +207 -26
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/update.js +42 -23
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +75 -46
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2236 -7790
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +48 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/tool-registry.js +19 -3
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +19 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +6 -5
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +35 -19
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +25 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +218 -34
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +14 -12
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- 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
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
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, [
|
|
82
|
+
${errorMessages}`, ErrorCategory.USER, [schemaLookupHint("cli", layer, resolvedType)]);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
if (layerObj.getElement(elementPath))
|
package/dist/commands/add.js.map
CHANGED
|
@@ -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
|
|
5
|
-
"mappings": "AAIA,OAAO,WAAW;AAClB,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,gCAAgC,4BAA4B;AACrE,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB,oBAAoB;AAChD,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,iBAAiB,sBAAsB,yBAAyB;AACzE,SACE,UACA,eACA,kBACA,oBACA,aACA,oBACA,iBACA,aACA,qBACK;AAiBP,eAAsB,WACpB,OACA,MACA,MACA,SAAmB;AAEnB,MAAI;AAEF,QAAI,CAAC,aAAa,KAAK,GAAG;AACxB,YAAM,cAAc,eAAc,GAC5B,UAAU,YAAY,OAAO,aAAa,CAAC,GAC3C,cAAwB,CAAC,2BAA2B,mBAAmB,WAAW,CAAC,EAAE;AAC3F,YAAI,QAAQ,SAAS,KACnB,YAAY,QAAQ,iBAAiB,QAAQ,KAAK,MAAM,CAAC,GAAG,GAExD,IAAI,SAAS,kBAAkB,KAAK,KAAK,cAAc,MAAM,aAAa;QAC9E,WAAW;QACX,SAAS,UAAU,KAAK,WAAW,IAAI,WAAW,IAAI;OACvD;IACH;AAGA,QAAI,CAAC,gBAAgB,OAAO,IAAI,GAAG;AAEjC,YAAM,YADiB,qBAAqB,KAAK,EAChB,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAI,GAClD,UAAU,YAAY,MAAM,WAAW,CAAC,GACxC,cAAwB,CAAC,mBAAmB,KAAK,KAAK,mBAAmB,SAAS,CAAC,EAAE;AAC3F,YAAI,QAAQ,SAAS,KACnB,YAAY,QAAQ,iBAAiB,QAAQ,KAAK,MAAM,CAAC,GAAG,GAExD,IAAI,SACR,yBAAyB,IAAI,gBAAgB,KAAK,KAClD,cAAc,MACd,aACA;QACE,WAAW;QACX,SAAS,UAAU,KAAK,WAAW,IAAI,WAAW,IAAI;OACvD;IAEL;AAIA,UAAM,eAAe,kBAAkB,OAAO,IAAI,GAG5C,cAAc,kBAAkB,OAAO,cAAc,IAAI,GACzD,cAAc,aAAY;AAIhC,QAAI,CADa,YAAY,MAAM,GAAG,EAAE,CAAC;AAEvC,YAAM,IAAI,SACR,iDAAiD,IAAI,KACrD,cAAc,MACd;QACE;QACA,0BAA0B,WAAW;OACtC;AAIL,UAAM,OAAO,cAAa;AAC1B,UAAM,aAAa,cAAc,KAAK,GACtC,MAAM,aAAa,gBAAgB,YAAY,GAC/C,MAAM,aAAa,cAAc,WAAW,GAG5C,+BAA+B,OAAO;AAGtC,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,MAAM,KAAK,QAAQ,
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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 {
|