@arcbridge/mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3750 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3750 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
|
|
9
|
+
// src/context.ts
|
|
10
|
+
function createContext() {
|
|
11
|
+
return {
|
|
12
|
+
db: null,
|
|
13
|
+
projectRoot: null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/tools/init-project.ts
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import {
|
|
22
|
+
generateConfig,
|
|
23
|
+
generateArc42,
|
|
24
|
+
generatePlan,
|
|
25
|
+
generateAgentRoles,
|
|
26
|
+
generateDatabase,
|
|
27
|
+
generateSyncFiles,
|
|
28
|
+
indexProject
|
|
29
|
+
} from "@arcbridge/core";
|
|
30
|
+
import { getAdapter } from "@arcbridge/adapters";
|
|
31
|
+
function registerInitProject(server, ctx) {
|
|
32
|
+
server.tool(
|
|
33
|
+
"arcbridge_init_project",
|
|
34
|
+
"Initialize ArcBridge in a project directory. Creates .arcbridge/ with arc42 documentation, phase plan, agent roles, SQLite database, and platform-specific configs.",
|
|
35
|
+
{
|
|
36
|
+
name: z.string().min(1).describe("Project name"),
|
|
37
|
+
template: z.enum(["nextjs-app-router", "react-vite", "api-service", "dotnet-webapi"]).default("nextjs-app-router").describe("Project template"),
|
|
38
|
+
features: z.array(z.enum(["auth", "database", "api"])).default([]).describe("Features to scaffold"),
|
|
39
|
+
quality_priorities: z.array(z.string()).default(["security", "performance", "accessibility"]).describe("Quality priorities in order"),
|
|
40
|
+
platforms: z.array(z.string()).default(["claude"]).describe("Target platforms for agent config generation"),
|
|
41
|
+
target_dir: z.string().describe("Absolute path to the target project directory")
|
|
42
|
+
},
|
|
43
|
+
async (params) => {
|
|
44
|
+
const targetDir = params.target_dir;
|
|
45
|
+
if (existsSync(join(targetDir, ".arcbridge", "config.yaml"))) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `ArcBridge is already initialized in ${targetDir}. Use arcbridge_get_project_status to see the current state.`
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const input = {
|
|
56
|
+
name: params.name,
|
|
57
|
+
template: params.template,
|
|
58
|
+
features: params.features,
|
|
59
|
+
quality_priorities: params.quality_priorities,
|
|
60
|
+
platforms: params.platforms
|
|
61
|
+
};
|
|
62
|
+
const config = generateConfig(targetDir, input);
|
|
63
|
+
generateArc42(targetDir, input);
|
|
64
|
+
generatePlan(targetDir, input);
|
|
65
|
+
const roles = generateAgentRoles(targetDir, params.template);
|
|
66
|
+
const { db, warnings } = generateDatabase(targetDir, input);
|
|
67
|
+
ctx.db = db;
|
|
68
|
+
ctx.projectRoot = targetDir;
|
|
69
|
+
const syncFiles = generateSyncFiles(targetDir, config);
|
|
70
|
+
const platformWarnings = [];
|
|
71
|
+
for (const platform of params.platforms) {
|
|
72
|
+
try {
|
|
73
|
+
const adapter = getAdapter(platform);
|
|
74
|
+
adapter.generateProjectConfig(targetDir, config);
|
|
75
|
+
adapter.generateAgentConfigs(targetDir, roles);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
platformWarnings.push(`Platform '${platform}': ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let indexResult = null;
|
|
82
|
+
try {
|
|
83
|
+
const result = await indexProject(db, { projectRoot: targetDir });
|
|
84
|
+
indexResult = {
|
|
85
|
+
symbolsIndexed: result.symbolsIndexed,
|
|
86
|
+
dependenciesIndexed: result.dependenciesIndexed,
|
|
87
|
+
componentsAnalyzed: result.componentsAnalyzed,
|
|
88
|
+
routesAnalyzed: result.routesAnalyzed
|
|
89
|
+
};
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
const blockCount = db.prepare("SELECT COUNT(*) as count FROM building_blocks").get();
|
|
93
|
+
const scenarioCount = db.prepare("SELECT COUNT(*) as count FROM quality_scenarios").get();
|
|
94
|
+
const phaseCount = db.prepare("SELECT COUNT(*) as count FROM phases").get();
|
|
95
|
+
const taskCount = db.prepare("SELECT COUNT(*) as count FROM tasks").get();
|
|
96
|
+
const allWarnings = [...warnings, ...platformWarnings];
|
|
97
|
+
const summary = [
|
|
98
|
+
`# ArcBridge Initialized: ${input.name}`,
|
|
99
|
+
"",
|
|
100
|
+
`**Template:** ${input.template}`,
|
|
101
|
+
`**Features:** ${input.features.length > 0 ? input.features.join(", ") : "none"}`,
|
|
102
|
+
`**Platforms:** ${params.platforms.join(", ")}`,
|
|
103
|
+
"",
|
|
104
|
+
"## Created",
|
|
105
|
+
"",
|
|
106
|
+
`- **Building blocks:** ${blockCount.count}`,
|
|
107
|
+
`- **Quality scenarios:** ${scenarioCount.count}`,
|
|
108
|
+
`- **Phases:** ${phaseCount.count}`,
|
|
109
|
+
`- **Tasks:** ${taskCount.count}`,
|
|
110
|
+
`- **Agent roles:** ${roles.length}`,
|
|
111
|
+
...indexResult ? [
|
|
112
|
+
`- **Symbols indexed:** ${indexResult.symbolsIndexed}`,
|
|
113
|
+
`- **Dependencies indexed:** ${indexResult.dependenciesIndexed}`,
|
|
114
|
+
`- **Components analyzed:** ${indexResult.componentsAnalyzed}`,
|
|
115
|
+
`- **Routes analyzed:** ${indexResult.routesAnalyzed}`
|
|
116
|
+
] : [input.template === "dotnet-webapi" ? `- **Code indexing:** not available yet for .NET projects (C# indexer planned)` : `- **Code indexing:** skipped (no tsconfig.json found \u2014 run \`arcbridge_reindex\` later)`],
|
|
117
|
+
"",
|
|
118
|
+
"## Files",
|
|
119
|
+
"",
|
|
120
|
+
"- `.arcbridge/config.yaml` \u2014 Project configuration",
|
|
121
|
+
"- `.arcbridge/arc42/` \u2014 Architecture documentation (arc42)",
|
|
122
|
+
"- `.arcbridge/plan/` \u2014 Phase plan and tasks",
|
|
123
|
+
"- `.arcbridge/agents/` \u2014 Canonical agent role definitions",
|
|
124
|
+
"- `.arcbridge/index.db` \u2014 SQLite database",
|
|
125
|
+
...params.platforms.includes("claude") ? ["- `CLAUDE.md` \u2014 Claude Code project instructions", "- `.claude/agents/` \u2014 Claude agent configs"] : [],
|
|
126
|
+
...params.platforms.includes("copilot") ? ["- `.github/copilot-instructions.md` \u2014 Copilot instructions", "- `.github/agents/` \u2014 Copilot agent configs"] : [],
|
|
127
|
+
...syncFiles.map((f) => `- \`${f}\` \u2014 Sync loop trigger`),
|
|
128
|
+
...allWarnings.length > 0 ? [
|
|
129
|
+
"",
|
|
130
|
+
"## Warnings",
|
|
131
|
+
"",
|
|
132
|
+
...allWarnings.map((w) => `- ${w}`)
|
|
133
|
+
] : [],
|
|
134
|
+
"",
|
|
135
|
+
"Use `arcbridge_get_project_status` to see the full project status."
|
|
136
|
+
];
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: summary.join("\n") }]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/tools/get-project-status.ts
|
|
145
|
+
import { z as z2 } from "zod";
|
|
146
|
+
import { refreshFromDocs } from "@arcbridge/core";
|
|
147
|
+
|
|
148
|
+
// src/helpers.ts
|
|
149
|
+
import { join as join2 } from "path";
|
|
150
|
+
import { existsSync as existsSync2 } from "fs";
|
|
151
|
+
import { openDatabase } from "@arcbridge/core";
|
|
152
|
+
function ensureDb(ctx, targetDir) {
|
|
153
|
+
if (ctx.db) return ctx.db;
|
|
154
|
+
const dbPath = join2(targetDir, ".arcbridge", "index.db");
|
|
155
|
+
if (!existsSync2(dbPath)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
ctx.db = openDatabase(dbPath);
|
|
159
|
+
ctx.projectRoot = targetDir;
|
|
160
|
+
return ctx.db;
|
|
161
|
+
}
|
|
162
|
+
function notInitialized() {
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: "ArcBridge is not initialized in this directory. Run `arcbridge_init_project` first."
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function textResult(text) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text }]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function escapeLike(value) {
|
|
178
|
+
return value.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
179
|
+
}
|
|
180
|
+
function safeParseJson(value, fallback) {
|
|
181
|
+
if (value === null || value === void 0) return fallback;
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(value);
|
|
184
|
+
} catch {
|
|
185
|
+
return fallback;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function normalizeCodePath(codePath) {
|
|
189
|
+
return codePath.replace(/\*+\/?$/, "");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/tools/get-project-status.ts
|
|
193
|
+
function registerGetProjectStatus(server, ctx) {
|
|
194
|
+
server.tool(
|
|
195
|
+
"arcbridge_get_project_status",
|
|
196
|
+
"Get the current status of the ArcBridge project: current phase, task completion, building blocks, quality scenarios, and drift warnings.",
|
|
197
|
+
{
|
|
198
|
+
target_dir: z2.string().describe("Absolute path to the project directory")
|
|
199
|
+
},
|
|
200
|
+
async (params) => {
|
|
201
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
202
|
+
if (!db) {
|
|
203
|
+
return notInitialized();
|
|
204
|
+
}
|
|
205
|
+
refreshFromDocs(db, params.target_dir);
|
|
206
|
+
const projectName = db.prepare(
|
|
207
|
+
"SELECT value FROM arcbridge_meta WHERE key = 'project_name'"
|
|
208
|
+
).get()?.value ?? "Unknown";
|
|
209
|
+
const phases = db.prepare(
|
|
210
|
+
"SELECT id, name, phase_number, status FROM phases ORDER BY phase_number"
|
|
211
|
+
).all();
|
|
212
|
+
const currentPhase = phases.find(
|
|
213
|
+
(p) => p.status === "in-progress"
|
|
214
|
+
) ?? phases[0];
|
|
215
|
+
const taskStats = db.prepare(
|
|
216
|
+
"SELECT status, COUNT(*) as count FROM tasks GROUP BY status"
|
|
217
|
+
).all();
|
|
218
|
+
const totalTasks = taskStats.reduce((sum, r) => sum + r.count, 0);
|
|
219
|
+
const doneTasks = taskStats.find((r) => r.status === "done")?.count ?? 0;
|
|
220
|
+
const completionPct = totalTasks > 0 ? Math.round(doneTasks / totalTasks * 100) : 0;
|
|
221
|
+
const blocks = db.prepare("SELECT id, name, responsibility FROM building_blocks").all();
|
|
222
|
+
const scenarios = db.prepare(
|
|
223
|
+
"SELECT id, name, category, status, priority FROM quality_scenarios ORDER BY category, id"
|
|
224
|
+
).all();
|
|
225
|
+
const symbolCount = db.prepare("SELECT COUNT(*) as count FROM symbols").get().count;
|
|
226
|
+
const depCount = db.prepare("SELECT COUNT(*) as count FROM dependencies").get().count;
|
|
227
|
+
const componentCount = db.prepare("SELECT COUNT(*) as count FROM components").get().count;
|
|
228
|
+
const routeCount = db.prepare("SELECT COUNT(*) as count FROM routes").get().count;
|
|
229
|
+
const lastIndexed = db.prepare("SELECT MAX(indexed_at) as value FROM symbols").get()?.value;
|
|
230
|
+
const driftCount = db.prepare(
|
|
231
|
+
"SELECT COUNT(*) as count FROM drift_log WHERE resolution IS NULL"
|
|
232
|
+
).get().count;
|
|
233
|
+
const lines = [
|
|
234
|
+
`# Project Status: ${projectName}`,
|
|
235
|
+
"",
|
|
236
|
+
"## Current Phase",
|
|
237
|
+
"",
|
|
238
|
+
currentPhase ? `**${currentPhase.name}** (${currentPhase.status})` : "*No phases defined*",
|
|
239
|
+
"",
|
|
240
|
+
"## Phases",
|
|
241
|
+
"",
|
|
242
|
+
...phases.map(
|
|
243
|
+
(p) => `- ${p.status === "complete" ? "[x]" : p.status === "in-progress" ? "[>]" : "[ ]"} Phase ${p.phase_number}: ${p.name} (${p.status})`
|
|
244
|
+
),
|
|
245
|
+
"",
|
|
246
|
+
"## Task Progress",
|
|
247
|
+
"",
|
|
248
|
+
`**${doneTasks}/${totalTasks}** tasks complete (${completionPct}%)`,
|
|
249
|
+
"",
|
|
250
|
+
...taskStats.map((r) => `- ${r.status}: ${r.count}`),
|
|
251
|
+
"",
|
|
252
|
+
"## Building Blocks",
|
|
253
|
+
"",
|
|
254
|
+
...blocks.map((b) => `- **${b.name}** (\`${b.id}\`): ${b.responsibility}`),
|
|
255
|
+
"",
|
|
256
|
+
"## Quality Scenarios",
|
|
257
|
+
"",
|
|
258
|
+
...scenarios.map(
|
|
259
|
+
(s) => `- ${s.status === "passing" ? "pass" : s.status === "failing" ? "FAIL" : s.status === "partial" ? "partial" : "untested"} ${s.id}: ${s.name} [${s.category}] (${s.priority})`
|
|
260
|
+
),
|
|
261
|
+
""
|
|
262
|
+
];
|
|
263
|
+
lines.push("## Code Intelligence", "");
|
|
264
|
+
if (symbolCount > 0) {
|
|
265
|
+
lines.push(
|
|
266
|
+
`- **Symbols indexed:** ${symbolCount}`,
|
|
267
|
+
`- **Dependencies indexed:** ${depCount}`,
|
|
268
|
+
`- **Components analyzed:** ${componentCount}`,
|
|
269
|
+
`- **Routes analyzed:** ${routeCount}`,
|
|
270
|
+
`- **Last indexed:** ${lastIndexed ?? "unknown"}`,
|
|
271
|
+
""
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
lines.push(
|
|
275
|
+
"*Not indexed yet.* Run `arcbridge_reindex` to index TypeScript symbols.",
|
|
276
|
+
""
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (driftCount > 0) {
|
|
280
|
+
lines.push(
|
|
281
|
+
"## Drift Warnings",
|
|
282
|
+
"",
|
|
283
|
+
`**${driftCount}** unresolved drift issue(s) detected.`,
|
|
284
|
+
""
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/tools/get-building-blocks.ts
|
|
295
|
+
import { z as z3 } from "zod";
|
|
296
|
+
function registerGetBuildingBlocks(server, ctx) {
|
|
297
|
+
server.tool(
|
|
298
|
+
"arcbridge_get_building_blocks",
|
|
299
|
+
"Get all architecture building blocks with their code mappings, responsibilities, and linked quality scenarios.",
|
|
300
|
+
{
|
|
301
|
+
target_dir: z3.string().describe("Absolute path to the project directory")
|
|
302
|
+
},
|
|
303
|
+
async (params) => {
|
|
304
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
305
|
+
if (!db) return notInitialized();
|
|
306
|
+
const blocks = db.prepare(
|
|
307
|
+
"SELECT id, name, level, parent_id, responsibility, code_paths, interfaces, service, last_synced FROM building_blocks ORDER BY level, name"
|
|
308
|
+
).all();
|
|
309
|
+
if (blocks.length === 0) {
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{ type: "text", text: "No building blocks defined yet." }
|
|
313
|
+
]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const lines = ["# Building Blocks", ""];
|
|
317
|
+
for (const block of blocks) {
|
|
318
|
+
const indent = " ".repeat(block.level - 1);
|
|
319
|
+
const codePaths = safeParseJson(block.code_paths, []);
|
|
320
|
+
const interfaces = safeParseJson(block.interfaces, []);
|
|
321
|
+
lines.push(`${indent}## ${block.name} (\`${block.id}\`)`);
|
|
322
|
+
lines.push("");
|
|
323
|
+
lines.push(`${indent}**Responsibility:** ${block.responsibility}`);
|
|
324
|
+
lines.push(`${indent}**Service:** ${block.service}`);
|
|
325
|
+
if (codePaths.length > 0) {
|
|
326
|
+
lines.push(
|
|
327
|
+
`${indent}**Code:** ${codePaths.map((p) => `\`${p}\``).join(", ")}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (interfaces.length > 0) {
|
|
331
|
+
lines.push(
|
|
332
|
+
`${indent}**Interfaces:** ${interfaces.map((i) => `\`${i}\``).join(", ")}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (block.parent_id) {
|
|
336
|
+
lines.push(`${indent}**Parent:** \`${block.parent_id}\``);
|
|
337
|
+
}
|
|
338
|
+
const scenarios = db.prepare(
|
|
339
|
+
"SELECT id, name, category, status FROM quality_scenarios WHERE linked_blocks LIKE ?"
|
|
340
|
+
).all(`%"${block.id}"%`);
|
|
341
|
+
if (scenarios.length > 0) {
|
|
342
|
+
lines.push(`${indent}**Quality Scenarios:**`);
|
|
343
|
+
for (const s of scenarios) {
|
|
344
|
+
lines.push(
|
|
345
|
+
`${indent}- ${s.id}: ${s.name} [${s.category}] (${s.status})`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
lines.push("");
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/tools/get-building-block.ts
|
|
359
|
+
import { z as z4 } from "zod";
|
|
360
|
+
function registerGetBuildingBlock(server, ctx) {
|
|
361
|
+
server.tool(
|
|
362
|
+
"arcbridge_get_building_block",
|
|
363
|
+
"Get detailed information about a single building block: its arc42 description, code modules, interfaces, quality scenarios, ADRs, and tasks.",
|
|
364
|
+
{
|
|
365
|
+
target_dir: z4.string().describe("Absolute path to the project directory"),
|
|
366
|
+
block_id: z4.string().describe("Building block ID (e.g., 'auth-module')")
|
|
367
|
+
},
|
|
368
|
+
async (params) => {
|
|
369
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
370
|
+
if (!db) return notInitialized();
|
|
371
|
+
const block = db.prepare("SELECT * FROM building_blocks WHERE id = ?").get(params.block_id);
|
|
372
|
+
if (!block) {
|
|
373
|
+
return {
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: `Building block '${params.block_id}' not found. Use \`arcbridge_get_building_blocks\` to see all blocks.`
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const codePaths = safeParseJson(block.code_paths, []);
|
|
383
|
+
const interfaces = safeParseJson(block.interfaces, []);
|
|
384
|
+
const escapedBlockId = escapeLike(block.id);
|
|
385
|
+
const lines = [
|
|
386
|
+
`# ${block.name} (\`${block.id}\`)`,
|
|
387
|
+
"",
|
|
388
|
+
`**Responsibility:** ${block.responsibility}`,
|
|
389
|
+
`**Level:** ${block.level}`,
|
|
390
|
+
`**Service:** ${block.service}`
|
|
391
|
+
];
|
|
392
|
+
if (block.parent_id) {
|
|
393
|
+
lines.push(`**Parent:** \`${block.parent_id}\``);
|
|
394
|
+
}
|
|
395
|
+
if (block.last_synced) {
|
|
396
|
+
lines.push(`**Last synced:** ${block.last_synced}`);
|
|
397
|
+
}
|
|
398
|
+
lines.push("", "## Code Paths", "");
|
|
399
|
+
if (codePaths.length > 0) {
|
|
400
|
+
for (const p of codePaths) {
|
|
401
|
+
lines.push(`- \`${p}\``);
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
lines.push("*No code paths mapped yet.*");
|
|
405
|
+
}
|
|
406
|
+
if (codePaths.length > 0) {
|
|
407
|
+
const pathConditions = codePaths.map(() => "file_path LIKE ? ESCAPE '\\'");
|
|
408
|
+
const pathParams = codePaths.map((cp) => {
|
|
409
|
+
const prefix = normalizeCodePath(cp);
|
|
410
|
+
return `${escapeLike(prefix)}%`;
|
|
411
|
+
});
|
|
412
|
+
const symbolQuery = `
|
|
413
|
+
SELECT name, kind, file_path, is_exported
|
|
414
|
+
FROM symbols
|
|
415
|
+
WHERE (${pathConditions.join(" OR ")})
|
|
416
|
+
ORDER BY file_path, name
|
|
417
|
+
LIMIT 30
|
|
418
|
+
`;
|
|
419
|
+
const mappedSymbols = db.prepare(symbolQuery).all(...pathParams);
|
|
420
|
+
if (mappedSymbols.length > 0) {
|
|
421
|
+
const totalCount = db.prepare(
|
|
422
|
+
`SELECT COUNT(*) as count FROM symbols WHERE (${pathConditions.join(" OR ")})`
|
|
423
|
+
).get(...pathParams);
|
|
424
|
+
lines.push("", `## Mapped Symbols (${totalCount.count} total)`, "");
|
|
425
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const s of mappedSymbols) {
|
|
427
|
+
byKind.set(s.kind, (byKind.get(s.kind) ?? 0) + 1);
|
|
428
|
+
}
|
|
429
|
+
lines.push(
|
|
430
|
+
[...byKind.entries()].map(([k, c]) => `**${c}** ${k}s`).join(", "),
|
|
431
|
+
""
|
|
432
|
+
);
|
|
433
|
+
for (const s of mappedSymbols) {
|
|
434
|
+
const exported = s.is_exported ? "" : " (internal)";
|
|
435
|
+
lines.push(`- \`${s.file_path}\` \u2192 **${s.name}** (${s.kind})${exported}`);
|
|
436
|
+
}
|
|
437
|
+
if (totalCount.count > 30) {
|
|
438
|
+
lines.push(`- *... and ${totalCount.count - 30} more*`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (interfaces.length > 0) {
|
|
443
|
+
lines.push("", "## Interfaces", "");
|
|
444
|
+
for (const i of interfaces) {
|
|
445
|
+
lines.push(`- \`${i}\``);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const children = db.prepare(
|
|
449
|
+
"SELECT id, name, responsibility FROM building_blocks WHERE parent_id = ?"
|
|
450
|
+
).all(params.block_id);
|
|
451
|
+
if (children.length > 0) {
|
|
452
|
+
lines.push("", "## Sub-blocks", "");
|
|
453
|
+
for (const child of children) {
|
|
454
|
+
lines.push(`- **${child.name}** (\`${child.id}\`): ${child.responsibility}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const scenarios = db.prepare(
|
|
458
|
+
"SELECT id, name, category, priority, status, scenario, expected FROM quality_scenarios WHERE linked_blocks LIKE ? ESCAPE '\\'"
|
|
459
|
+
).all(`%"${escapedBlockId}"%`);
|
|
460
|
+
if (scenarios.length > 0) {
|
|
461
|
+
lines.push("", "## Quality Scenarios", "");
|
|
462
|
+
for (const s of scenarios) {
|
|
463
|
+
lines.push(
|
|
464
|
+
`### ${s.id}: ${s.name} [${s.category}]`,
|
|
465
|
+
`- **Priority:** ${s.priority}`,
|
|
466
|
+
`- **Status:** ${s.status}`,
|
|
467
|
+
`- **Scenario:** ${s.scenario}`,
|
|
468
|
+
`- **Expected:** ${s.expected}`,
|
|
469
|
+
""
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const adrs = db.prepare(
|
|
474
|
+
"SELECT id, title, status, date FROM adrs WHERE affected_blocks LIKE ? ESCAPE '\\'"
|
|
475
|
+
).all(`%"${escapedBlockId}"%`);
|
|
476
|
+
if (adrs.length > 0) {
|
|
477
|
+
lines.push("", "## Related ADRs", "");
|
|
478
|
+
for (const adr of adrs) {
|
|
479
|
+
lines.push(`- **${adr.id}:** ${adr.title} (${adr.status}, ${adr.date})`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const tasks = db.prepare(
|
|
483
|
+
"SELECT id, title, status, phase_id FROM tasks WHERE building_block = ?"
|
|
484
|
+
).all(params.block_id);
|
|
485
|
+
if (tasks.length > 0) {
|
|
486
|
+
lines.push("", "## Tasks", "");
|
|
487
|
+
for (const task of tasks) {
|
|
488
|
+
const check = task.status === "done" ? "[x]" : task.status === "in-progress" ? "[>]" : "[ ]";
|
|
489
|
+
lines.push(`- ${check} ${task.id}: ${task.title} (${task.status})`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/tools/get-quality-scenarios.ts
|
|
500
|
+
import { z as z5 } from "zod";
|
|
501
|
+
import { QualityCategorySchema, QualityPrioritySchema, QualityScenarioStatusSchema } from "@arcbridge/core";
|
|
502
|
+
function registerGetQualityScenarios(server, ctx) {
|
|
503
|
+
server.tool(
|
|
504
|
+
"arcbridge_get_quality_scenarios",
|
|
505
|
+
"Get quality scenarios, optionally filtered by category. Shows scenario details, linked code/tests, and current status.",
|
|
506
|
+
{
|
|
507
|
+
target_dir: z5.string().describe("Absolute path to the project directory"),
|
|
508
|
+
category: QualityCategorySchema.optional().describe("Filter by category"),
|
|
509
|
+
status: QualityScenarioStatusSchema.optional().describe("Filter by status"),
|
|
510
|
+
priority: QualityPrioritySchema.optional().describe("Filter by priority (must/should/could)")
|
|
511
|
+
},
|
|
512
|
+
async (params) => {
|
|
513
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
514
|
+
if (!db) return notInitialized();
|
|
515
|
+
let query = "SELECT id, name, category, scenario, expected, priority, linked_code, linked_tests, linked_blocks, verification, status FROM quality_scenarios";
|
|
516
|
+
const conditions = [];
|
|
517
|
+
const queryParams = [];
|
|
518
|
+
if (params.category) {
|
|
519
|
+
conditions.push("category = ?");
|
|
520
|
+
queryParams.push(params.category);
|
|
521
|
+
}
|
|
522
|
+
if (params.status) {
|
|
523
|
+
conditions.push("status = ?");
|
|
524
|
+
queryParams.push(params.status);
|
|
525
|
+
}
|
|
526
|
+
if (params.priority) {
|
|
527
|
+
conditions.push("priority = ?");
|
|
528
|
+
queryParams.push(params.priority);
|
|
529
|
+
}
|
|
530
|
+
if (conditions.length > 0) {
|
|
531
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
532
|
+
}
|
|
533
|
+
query += " ORDER BY category, id";
|
|
534
|
+
const scenarios = db.prepare(query).all(...queryParams);
|
|
535
|
+
if (scenarios.length === 0) {
|
|
536
|
+
const filter = [params.category, params.status, params.priority].filter(Boolean).join(", ");
|
|
537
|
+
return {
|
|
538
|
+
content: [
|
|
539
|
+
{
|
|
540
|
+
type: "text",
|
|
541
|
+
text: filter ? `No quality scenarios found matching: ${filter}` : "No quality scenarios defined yet."
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const s of scenarios) {
|
|
548
|
+
const list = byCategory.get(s.category) ?? [];
|
|
549
|
+
list.push(s);
|
|
550
|
+
byCategory.set(s.category, list);
|
|
551
|
+
}
|
|
552
|
+
const statusIcon = (s) => s === "passing" ? "PASS" : s === "failing" ? "FAIL" : s === "partial" ? "PARTIAL" : "UNTESTED";
|
|
553
|
+
const lines = ["# Quality Scenarios", ""];
|
|
554
|
+
const passing = scenarios.filter((s) => s.status === "passing").length;
|
|
555
|
+
const failing = scenarios.filter((s) => s.status === "failing").length;
|
|
556
|
+
const untested = scenarios.filter((s) => s.status === "untested").length;
|
|
557
|
+
const partial = scenarios.filter((s) => s.status === "partial").length;
|
|
558
|
+
lines.push(
|
|
559
|
+
`**Total:** ${scenarios.length} | **Passing:** ${passing} | **Failing:** ${failing} | **Untested:** ${untested} | **Partial:** ${partial}`,
|
|
560
|
+
""
|
|
561
|
+
);
|
|
562
|
+
for (const [category, items] of byCategory) {
|
|
563
|
+
lines.push(
|
|
564
|
+
`## ${category.charAt(0).toUpperCase() + category.slice(1)}`,
|
|
565
|
+
""
|
|
566
|
+
);
|
|
567
|
+
for (const s of items) {
|
|
568
|
+
const linkedCode = safeParseJson(s.linked_code, []);
|
|
569
|
+
const linkedTests = safeParseJson(s.linked_tests, []);
|
|
570
|
+
const linkedBlocks = safeParseJson(s.linked_blocks, []);
|
|
571
|
+
lines.push(
|
|
572
|
+
`### ${statusIcon(s.status)} ${s.id}: ${s.name}`,
|
|
573
|
+
"",
|
|
574
|
+
`- **Priority:** ${s.priority}`,
|
|
575
|
+
`- **Verification:** ${s.verification}`,
|
|
576
|
+
`- **Scenario:** ${s.scenario}`,
|
|
577
|
+
`- **Expected:** ${s.expected}`
|
|
578
|
+
);
|
|
579
|
+
if (linkedCode.length > 0) {
|
|
580
|
+
lines.push(
|
|
581
|
+
`- **Linked code:** ${linkedCode.map((c) => `\`${c}\``).join(", ")}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (linkedTests.length > 0) {
|
|
585
|
+
lines.push(
|
|
586
|
+
`- **Linked tests:** ${linkedTests.map((t) => `\`${t}\``).join(", ")}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (linkedBlocks.length > 0) {
|
|
590
|
+
lines.push(
|
|
591
|
+
`- **Linked blocks:** ${linkedBlocks.map((b) => `\`${b}\``).join(", ")}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
lines.push("");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/tools/get-phase-plan.ts
|
|
605
|
+
import { z as z6 } from "zod";
|
|
606
|
+
import { refreshFromDocs as refreshFromDocs2 } from "@arcbridge/core";
|
|
607
|
+
function registerGetPhasePlan(server, ctx) {
|
|
608
|
+
server.tool(
|
|
609
|
+
"arcbridge_get_phase_plan",
|
|
610
|
+
"Get the complete phase plan with all phases, their tasks, status, and gate requirements.",
|
|
611
|
+
{
|
|
612
|
+
target_dir: z6.string().describe("Absolute path to the project directory")
|
|
613
|
+
},
|
|
614
|
+
async (params) => {
|
|
615
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
616
|
+
if (!db) return notInitialized();
|
|
617
|
+
refreshFromDocs2(db, params.target_dir);
|
|
618
|
+
const phases = db.prepare(
|
|
619
|
+
"SELECT id, name, phase_number, status, description, gate_status, started_at, completed_at FROM phases ORDER BY phase_number"
|
|
620
|
+
).all();
|
|
621
|
+
if (phases.length === 0) {
|
|
622
|
+
return {
|
|
623
|
+
content: [
|
|
624
|
+
{ type: "text", text: "No phases defined yet." }
|
|
625
|
+
]
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const lines = ["# Phase Plan", ""];
|
|
629
|
+
for (const phase of phases) {
|
|
630
|
+
const icon = phase.status === "complete" ? "[x]" : phase.status === "in-progress" ? "[>]" : phase.status === "blocked" ? "[!]" : "[ ]";
|
|
631
|
+
lines.push(
|
|
632
|
+
`## ${icon} Phase ${phase.phase_number}: ${phase.name}`,
|
|
633
|
+
"",
|
|
634
|
+
`**Status:** ${phase.status}`,
|
|
635
|
+
`**Description:** ${phase.description}`
|
|
636
|
+
);
|
|
637
|
+
if (phase.started_at) {
|
|
638
|
+
lines.push(`**Started:** ${phase.started_at}`);
|
|
639
|
+
}
|
|
640
|
+
if (phase.completed_at) {
|
|
641
|
+
lines.push(`**Completed:** ${phase.completed_at}`);
|
|
642
|
+
}
|
|
643
|
+
const tasks = db.prepare(
|
|
644
|
+
"SELECT id, title, status, building_block, quality_scenarios, acceptance_criteria FROM tasks WHERE phase_id = ? ORDER BY id"
|
|
645
|
+
).all(phase.id);
|
|
646
|
+
if (tasks.length > 0) {
|
|
647
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
648
|
+
lines.push(
|
|
649
|
+
"",
|
|
650
|
+
`### Tasks (${done}/${tasks.length} complete)`,
|
|
651
|
+
""
|
|
652
|
+
);
|
|
653
|
+
for (const task of tasks) {
|
|
654
|
+
const check = task.status === "done" ? "[x]" : task.status === "in-progress" ? "[>]" : task.status === "blocked" ? "[!]" : "[ ]";
|
|
655
|
+
lines.push(`- ${check} **${task.id}:** ${task.title}`);
|
|
656
|
+
if (task.building_block) {
|
|
657
|
+
lines.push(` - Block: \`${task.building_block}\``);
|
|
658
|
+
}
|
|
659
|
+
const qScenarios = safeParseJson(task.quality_scenarios, []);
|
|
660
|
+
if (qScenarios.length > 0) {
|
|
661
|
+
lines.push(
|
|
662
|
+
` - Quality: ${qScenarios.join(", ")}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const criteria = safeParseJson(task.acceptance_criteria, []);
|
|
666
|
+
if (criteria.length > 0) {
|
|
667
|
+
for (const c of criteria) {
|
|
668
|
+
lines.push(` - [ ] ${c}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
lines.push("");
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/tools/get-current-tasks.ts
|
|
683
|
+
import { z as z7 } from "zod";
|
|
684
|
+
import { refreshFromDocs as refreshFromDocs3 } from "@arcbridge/core";
|
|
685
|
+
function registerGetCurrentTasks(server, ctx) {
|
|
686
|
+
server.tool(
|
|
687
|
+
"arcbridge_get_current_tasks",
|
|
688
|
+
"Get tasks for the current in-progress phase, with their building blocks, quality scenarios, and acceptance criteria.",
|
|
689
|
+
{
|
|
690
|
+
target_dir: z7.string().describe("Absolute path to the project directory"),
|
|
691
|
+
status: z7.enum(["todo", "in-progress", "done", "blocked"]).optional().describe("Filter tasks by status")
|
|
692
|
+
},
|
|
693
|
+
async (params) => {
|
|
694
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
695
|
+
if (!db) return notInitialized();
|
|
696
|
+
refreshFromDocs3(db, params.target_dir);
|
|
697
|
+
const currentPhase = db.prepare(
|
|
698
|
+
"SELECT id, name FROM phases WHERE status = 'in-progress' ORDER BY phase_number LIMIT 1"
|
|
699
|
+
).get();
|
|
700
|
+
if (!currentPhase) {
|
|
701
|
+
return {
|
|
702
|
+
content: [
|
|
703
|
+
{
|
|
704
|
+
type: "text",
|
|
705
|
+
text: "No phase is currently in-progress. Use `arcbridge_get_phase_plan` to see all phases."
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
let query = "SELECT id, phase_id, title, description, status, building_block, quality_scenarios, acceptance_criteria FROM tasks WHERE phase_id = ?";
|
|
711
|
+
const queryParams = [currentPhase.id];
|
|
712
|
+
if (params.status) {
|
|
713
|
+
query += " AND status = ?";
|
|
714
|
+
queryParams.push(params.status);
|
|
715
|
+
}
|
|
716
|
+
query += " ORDER BY id";
|
|
717
|
+
const tasks = db.prepare(query).all(...queryParams);
|
|
718
|
+
const lines = [
|
|
719
|
+
`# Current Tasks: ${currentPhase.name}`,
|
|
720
|
+
""
|
|
721
|
+
];
|
|
722
|
+
if (tasks.length === 0) {
|
|
723
|
+
lines.push(
|
|
724
|
+
params.status ? `No tasks with status '${params.status}' in this phase.` : "No tasks in this phase."
|
|
725
|
+
);
|
|
726
|
+
} else {
|
|
727
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
728
|
+
lines.push(`**Progress:** ${done}/${tasks.length} complete`, "");
|
|
729
|
+
for (const task of tasks) {
|
|
730
|
+
const check = task.status === "done" ? "[x]" : task.status === "in-progress" ? "[>]" : task.status === "blocked" ? "[!]" : "[ ]";
|
|
731
|
+
lines.push(`## ${check} ${task.id}: ${task.title}`, "");
|
|
732
|
+
lines.push(`**Status:** ${task.status}`);
|
|
733
|
+
if (task.building_block) {
|
|
734
|
+
lines.push(`**Building block:** \`${task.building_block}\``);
|
|
735
|
+
}
|
|
736
|
+
const qScenarios = safeParseJson(task.quality_scenarios, []);
|
|
737
|
+
if (qScenarios.length > 0) {
|
|
738
|
+
lines.push(
|
|
739
|
+
`**Quality scenarios:** ${qScenarios.join(", ")}`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const criteria = safeParseJson(task.acceptance_criteria, []);
|
|
743
|
+
if (criteria.length > 0) {
|
|
744
|
+
lines.push("", "**Acceptance criteria:**");
|
|
745
|
+
for (const c of criteria) {
|
|
746
|
+
lines.push(
|
|
747
|
+
`- ${task.status === "done" ? "[x]" : "[ ]"} ${c}`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
lines.push("");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/tools/update-task.ts
|
|
762
|
+
import { z as z8 } from "zod";
|
|
763
|
+
import { syncTaskToYaml } from "@arcbridge/core";
|
|
764
|
+
function registerUpdateTask(server, ctx) {
|
|
765
|
+
server.tool(
|
|
766
|
+
"arcbridge_update_task",
|
|
767
|
+
"Update a task's status. Use this to mark tasks as in-progress, done, or blocked as you work.",
|
|
768
|
+
{
|
|
769
|
+
target_dir: z8.string().describe("Absolute path to the project directory"),
|
|
770
|
+
task_id: z8.string().describe("Task ID (e.g., 'task-0.1-init-nextjs')"),
|
|
771
|
+
status: z8.enum(["in-progress", "done", "blocked"]).describe("New status"),
|
|
772
|
+
notes: z8.string().optional().describe("Optional notes about the status change")
|
|
773
|
+
},
|
|
774
|
+
async (params) => {
|
|
775
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
776
|
+
if (!db) return notInitialized();
|
|
777
|
+
const task = db.prepare("SELECT id, title, status, phase_id FROM tasks WHERE id = ?").get(params.task_id);
|
|
778
|
+
if (!task) {
|
|
779
|
+
return {
|
|
780
|
+
content: [
|
|
781
|
+
{
|
|
782
|
+
type: "text",
|
|
783
|
+
text: `Task '${params.task_id}' not found. Use \`arcbridge_get_current_tasks\` to see available tasks.`
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const oldStatus = task.status;
|
|
789
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
790
|
+
if (params.status === "done") {
|
|
791
|
+
db.prepare(
|
|
792
|
+
"UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?"
|
|
793
|
+
).run(params.status, now, params.task_id);
|
|
794
|
+
} else {
|
|
795
|
+
db.prepare("UPDATE tasks SET status = ? WHERE id = ?").run(
|
|
796
|
+
params.status,
|
|
797
|
+
params.task_id
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
syncTaskToYaml(
|
|
801
|
+
params.target_dir,
|
|
802
|
+
task.phase_id,
|
|
803
|
+
params.task_id,
|
|
804
|
+
params.status,
|
|
805
|
+
params.status === "done" ? now : null
|
|
806
|
+
);
|
|
807
|
+
const lines = [
|
|
808
|
+
`Task **${task.id}** updated: ${oldStatus} \u2192 ${params.status}`,
|
|
809
|
+
"",
|
|
810
|
+
`**${task.title}**`
|
|
811
|
+
];
|
|
812
|
+
if (params.notes) {
|
|
813
|
+
lines.push("", `**Notes:** ${params.notes}`);
|
|
814
|
+
}
|
|
815
|
+
if (params.status === "done") {
|
|
816
|
+
const phaseStats = db.prepare(
|
|
817
|
+
"SELECT COUNT(*) as total, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done FROM tasks WHERE phase_id = ?"
|
|
818
|
+
).get(task.phase_id);
|
|
819
|
+
lines.push(
|
|
820
|
+
"",
|
|
821
|
+
`**Phase progress:** ${phaseStats.done}/${phaseStats.total} tasks complete`
|
|
822
|
+
);
|
|
823
|
+
if (phaseStats.done === phaseStats.total) {
|
|
824
|
+
lines.push(
|
|
825
|
+
"",
|
|
826
|
+
"All tasks in this phase are complete! The phase is ready to advance."
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/tools/create-task.ts
|
|
838
|
+
import { z as z9 } from "zod";
|
|
839
|
+
import { addTaskToYaml } from "@arcbridge/core";
|
|
840
|
+
function registerCreateTask(server, ctx) {
|
|
841
|
+
server.tool(
|
|
842
|
+
"arcbridge_create_task",
|
|
843
|
+
"Create a new task in a phase. Links it to a building block and quality scenarios.",
|
|
844
|
+
{
|
|
845
|
+
target_dir: z9.string().describe("Absolute path to the project directory"),
|
|
846
|
+
phase_id: z9.string().describe("Phase ID to add the task to"),
|
|
847
|
+
title: z9.string().min(1).describe("Task title"),
|
|
848
|
+
building_block: z9.string().optional().describe("Building block this task belongs to"),
|
|
849
|
+
quality_scenarios: z9.array(z9.string()).default([]).describe("Quality scenario IDs this task addresses"),
|
|
850
|
+
acceptance_criteria: z9.array(z9.string()).default([]).describe("Acceptance criteria for this task")
|
|
851
|
+
},
|
|
852
|
+
async (params) => {
|
|
853
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
854
|
+
if (!db) return notInitialized();
|
|
855
|
+
const phase = db.prepare("SELECT id, name, phase_number FROM phases WHERE id = ?").get(params.phase_id);
|
|
856
|
+
if (!phase) {
|
|
857
|
+
return {
|
|
858
|
+
content: [
|
|
859
|
+
{
|
|
860
|
+
type: "text",
|
|
861
|
+
text: `Phase '${params.phase_id}' not found. Use \`arcbridge_get_phase_plan\` to see phases.`
|
|
862
|
+
}
|
|
863
|
+
]
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const existingCount = db.prepare(
|
|
867
|
+
"SELECT COUNT(*) as count FROM tasks WHERE phase_id = ?"
|
|
868
|
+
).get(params.phase_id).count;
|
|
869
|
+
const taskNum = existingCount + 1;
|
|
870
|
+
const slug = params.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
|
|
871
|
+
const taskId = `task-${phase.phase_number}.${taskNum}-${slug}`;
|
|
872
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
873
|
+
db.prepare(
|
|
874
|
+
"INSERT INTO tasks (id, phase_id, title, description, status, building_block, quality_scenarios, acceptance_criteria, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
875
|
+
).run(
|
|
876
|
+
taskId,
|
|
877
|
+
params.phase_id,
|
|
878
|
+
params.title,
|
|
879
|
+
null,
|
|
880
|
+
"todo",
|
|
881
|
+
params.building_block ?? null,
|
|
882
|
+
JSON.stringify(params.quality_scenarios),
|
|
883
|
+
JSON.stringify(params.acceptance_criteria),
|
|
884
|
+
now
|
|
885
|
+
);
|
|
886
|
+
addTaskToYaml(params.target_dir, params.phase_id, {
|
|
887
|
+
id: taskId,
|
|
888
|
+
title: params.title,
|
|
889
|
+
status: "todo",
|
|
890
|
+
building_block: params.building_block,
|
|
891
|
+
quality_scenarios: params.quality_scenarios,
|
|
892
|
+
acceptance_criteria: params.acceptance_criteria
|
|
893
|
+
});
|
|
894
|
+
const lines = [
|
|
895
|
+
`Task created: **${taskId}**`,
|
|
896
|
+
"",
|
|
897
|
+
`**Title:** ${params.title}`,
|
|
898
|
+
`**Phase:** ${phase.name}`,
|
|
899
|
+
`**Status:** todo`
|
|
900
|
+
];
|
|
901
|
+
if (params.building_block) {
|
|
902
|
+
lines.push(`**Block:** \`${params.building_block}\``);
|
|
903
|
+
}
|
|
904
|
+
if (params.quality_scenarios.length > 0) {
|
|
905
|
+
lines.push(
|
|
906
|
+
`**Quality scenarios:** ${params.quality_scenarios.join(", ")}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (params.acceptance_criteria.length > 0) {
|
|
910
|
+
lines.push("", "**Acceptance criteria:**");
|
|
911
|
+
for (const c of params.acceptance_criteria) {
|
|
912
|
+
lines.push(`- [ ] ${c}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/tools/get-relevant-adrs.ts
|
|
923
|
+
import { z as z10 } from "zod";
|
|
924
|
+
function registerGetRelevantAdrs(server, ctx) {
|
|
925
|
+
server.tool(
|
|
926
|
+
"arcbridge_get_relevant_adrs",
|
|
927
|
+
"Get architectural decision records (ADRs) relevant to a specific file path or building block.",
|
|
928
|
+
{
|
|
929
|
+
target_dir: z10.string().describe("Absolute path to the project directory"),
|
|
930
|
+
file_path: z10.string().optional().describe("File path to find relevant ADRs for"),
|
|
931
|
+
building_block: z10.string().optional().describe("Building block ID to find relevant ADRs for")
|
|
932
|
+
},
|
|
933
|
+
async (params) => {
|
|
934
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
935
|
+
if (!db) return notInitialized();
|
|
936
|
+
if (!params.file_path && !params.building_block) {
|
|
937
|
+
const adrs2 = db.prepare(
|
|
938
|
+
"SELECT id, title, status, date, context, decision, consequences, affected_blocks, affected_files, quality_scenarios FROM adrs ORDER BY id"
|
|
939
|
+
).all();
|
|
940
|
+
return formatAdrs(adrs2, "All ADRs");
|
|
941
|
+
}
|
|
942
|
+
const adrs = [];
|
|
943
|
+
const seen = /* @__PURE__ */ new Set();
|
|
944
|
+
if (params.building_block) {
|
|
945
|
+
const blockAdrs = db.prepare(
|
|
946
|
+
"SELECT id, title, status, date, context, decision, consequences, affected_blocks, affected_files, quality_scenarios FROM adrs WHERE affected_blocks LIKE ? ESCAPE '\\'"
|
|
947
|
+
).all(`%"${escapeLike(params.building_block)}"%`);
|
|
948
|
+
for (const adr of blockAdrs) {
|
|
949
|
+
if (!seen.has(adr.id)) {
|
|
950
|
+
adrs.push(adr);
|
|
951
|
+
seen.add(adr.id);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (params.file_path) {
|
|
956
|
+
const fileAdrs = db.prepare(
|
|
957
|
+
"SELECT id, title, status, date, context, decision, consequences, affected_blocks, affected_files, quality_scenarios FROM adrs WHERE affected_files LIKE ? ESCAPE '\\'"
|
|
958
|
+
).all(`%${escapeLike(params.file_path)}%`);
|
|
959
|
+
for (const adr of fileAdrs) {
|
|
960
|
+
if (!seen.has(adr.id)) {
|
|
961
|
+
adrs.push(adr);
|
|
962
|
+
seen.add(adr.id);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const scope = [
|
|
967
|
+
params.file_path ? `file: ${params.file_path}` : "",
|
|
968
|
+
params.building_block ? `block: ${params.building_block}` : ""
|
|
969
|
+
].filter(Boolean).join(", ");
|
|
970
|
+
return formatAdrs(adrs, `ADRs for ${scope}`);
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
function formatAdrs(adrs, title) {
|
|
975
|
+
if (adrs.length === 0) {
|
|
976
|
+
return {
|
|
977
|
+
content: [
|
|
978
|
+
{
|
|
979
|
+
type: "text",
|
|
980
|
+
text: `No ADRs found for the specified scope.`
|
|
981
|
+
}
|
|
982
|
+
]
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const lines = [`# ${title}`, ""];
|
|
986
|
+
for (const adr of adrs) {
|
|
987
|
+
const affectedBlocks = safeParseJson(adr.affected_blocks, []);
|
|
988
|
+
const affectedFiles = safeParseJson(adr.affected_files, []);
|
|
989
|
+
lines.push(
|
|
990
|
+
`## ${adr.id}: ${adr.title}`,
|
|
991
|
+
"",
|
|
992
|
+
`**Status:** ${adr.status} | **Date:** ${adr.date}`
|
|
993
|
+
);
|
|
994
|
+
if (affectedBlocks.length > 0) {
|
|
995
|
+
lines.push(
|
|
996
|
+
`**Affected blocks:** ${affectedBlocks.map((b) => `\`${b}\``).join(", ")}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (affectedFiles.length > 0) {
|
|
1000
|
+
lines.push(
|
|
1001
|
+
`**Affected files:** ${affectedFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (adr.decision) {
|
|
1005
|
+
lines.push("", adr.decision);
|
|
1006
|
+
}
|
|
1007
|
+
lines.push("");
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/tools/reindex.ts
|
|
1015
|
+
import { z as z11 } from "zod";
|
|
1016
|
+
import { indexProject as indexProject2, refreshFromDocs as refreshFromDocs4 } from "@arcbridge/core";
|
|
1017
|
+
function registerReindex(server, ctx) {
|
|
1018
|
+
server.tool(
|
|
1019
|
+
"arcbridge_reindex",
|
|
1020
|
+
"Re-index code symbols in the project. Supports TypeScript and C# (.NET). Incrementally processes only changed files.",
|
|
1021
|
+
{
|
|
1022
|
+
target_dir: z11.string().describe("Absolute path to the project directory"),
|
|
1023
|
+
tsconfig_path: z11.string().optional().describe("Override tsconfig.json path (default: auto-detect). Only used for TypeScript projects."),
|
|
1024
|
+
service: z11.string().optional().describe("Service name for monorepo projects (default: 'main')"),
|
|
1025
|
+
language: z11.enum(["typescript", "csharp", "auto"]).optional().describe("Project language. 'auto' detects from project files (default: 'auto')")
|
|
1026
|
+
},
|
|
1027
|
+
async (params) => {
|
|
1028
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1029
|
+
if (!db) return notInitialized();
|
|
1030
|
+
try {
|
|
1031
|
+
const docWarnings = refreshFromDocs4(db, params.target_dir);
|
|
1032
|
+
const result = await indexProject2(db, {
|
|
1033
|
+
projectRoot: params.target_dir,
|
|
1034
|
+
tsconfigPath: params.tsconfig_path,
|
|
1035
|
+
service: params.service,
|
|
1036
|
+
language: params.language
|
|
1037
|
+
});
|
|
1038
|
+
const lines = [
|
|
1039
|
+
"# Indexing Complete",
|
|
1040
|
+
"",
|
|
1041
|
+
`- **Docs refreshed:** ${docWarnings.length === 0 ? "OK" : docWarnings.join(", ")}`,
|
|
1042
|
+
`- **Files processed:** ${result.filesProcessed}`,
|
|
1043
|
+
`- **Files skipped (unchanged):** ${result.filesSkipped}`,
|
|
1044
|
+
`- **Files removed:** ${result.filesRemoved}`,
|
|
1045
|
+
`- **Symbols indexed:** ${result.symbolsIndexed}`,
|
|
1046
|
+
`- **Dependencies indexed:** ${result.dependenciesIndexed}`,
|
|
1047
|
+
`- **Components analyzed:** ${result.componentsAnalyzed}`,
|
|
1048
|
+
`- **Routes analyzed:** ${result.routesAnalyzed}`,
|
|
1049
|
+
`- **Duration:** ${result.durationMs}ms`
|
|
1050
|
+
];
|
|
1051
|
+
return textResult(lines.join("\n"));
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
return textResult(`Indexing failed: ${message}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/tools/search-symbols.ts
|
|
1061
|
+
import { z as z12 } from "zod";
|
|
1062
|
+
function registerSearchSymbols(server, ctx) {
|
|
1063
|
+
server.tool(
|
|
1064
|
+
"arcbridge_search_symbols",
|
|
1065
|
+
"Search code symbols by name, kind, file path, or building block. Supports TypeScript and C#. Returns matching symbols with type signatures.",
|
|
1066
|
+
{
|
|
1067
|
+
target_dir: z12.string().describe("Absolute path to the project directory"),
|
|
1068
|
+
query: z12.string().optional().describe("Search term to match against symbol names"),
|
|
1069
|
+
service: z12.string().optional().describe("Filter by service name (for multi-project solutions). Omit to search all services."),
|
|
1070
|
+
kind: z12.enum([
|
|
1071
|
+
"function",
|
|
1072
|
+
"class",
|
|
1073
|
+
"type",
|
|
1074
|
+
"constant",
|
|
1075
|
+
"interface",
|
|
1076
|
+
"enum",
|
|
1077
|
+
"variable",
|
|
1078
|
+
"component",
|
|
1079
|
+
"hook",
|
|
1080
|
+
"context"
|
|
1081
|
+
]).optional().describe("Filter by symbol kind"),
|
|
1082
|
+
file_path: z12.string().optional().describe("Filter by file path (prefix match)"),
|
|
1083
|
+
is_exported: z12.boolean().optional().describe("Filter by export status"),
|
|
1084
|
+
building_block: z12.string().optional().describe("Filter by building block ID (matches against code_paths)"),
|
|
1085
|
+
limit: z12.number().int().min(1).max(200).default(50).describe("Maximum results to return (default: 50)")
|
|
1086
|
+
},
|
|
1087
|
+
async (params) => {
|
|
1088
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1089
|
+
if (!db) return notInitialized();
|
|
1090
|
+
const conditions = [];
|
|
1091
|
+
const queryParams = [];
|
|
1092
|
+
if (params.service) {
|
|
1093
|
+
conditions.push("s.service = ?");
|
|
1094
|
+
queryParams.push(params.service);
|
|
1095
|
+
}
|
|
1096
|
+
if (params.query) {
|
|
1097
|
+
conditions.push("s.name LIKE ? ESCAPE '\\'");
|
|
1098
|
+
queryParams.push(`%${escapeLike(params.query)}%`);
|
|
1099
|
+
}
|
|
1100
|
+
if (params.kind) {
|
|
1101
|
+
conditions.push("s.kind = ?");
|
|
1102
|
+
queryParams.push(params.kind);
|
|
1103
|
+
}
|
|
1104
|
+
if (params.file_path) {
|
|
1105
|
+
conditions.push("s.file_path LIKE ? ESCAPE '\\'");
|
|
1106
|
+
queryParams.push(`${escapeLike(params.file_path)}%`);
|
|
1107
|
+
}
|
|
1108
|
+
if (params.is_exported !== void 0) {
|
|
1109
|
+
conditions.push("s.is_exported = ?");
|
|
1110
|
+
queryParams.push(params.is_exported ? 1 : 0);
|
|
1111
|
+
}
|
|
1112
|
+
if (params.building_block) {
|
|
1113
|
+
const block = db.prepare("SELECT code_paths FROM building_blocks WHERE id = ?").get(params.building_block);
|
|
1114
|
+
if (block) {
|
|
1115
|
+
try {
|
|
1116
|
+
const codePaths = JSON.parse(block.code_paths);
|
|
1117
|
+
if (codePaths.length > 0) {
|
|
1118
|
+
const pathConditions = codePaths.map(() => "s.file_path LIKE ? ESCAPE '\\'");
|
|
1119
|
+
conditions.push(`(${pathConditions.join(" OR ")})`);
|
|
1120
|
+
for (const cp of codePaths) {
|
|
1121
|
+
const prefix = cp.replace(/\*\*?\/?\*?$/, "");
|
|
1122
|
+
queryParams.push(`${escapeLike(prefix)}%`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
let query = "SELECT s.id, s.name, s.qualified_name, s.kind, s.file_path, s.start_line, s.signature, s.return_type, s.is_exported, s.is_async, s.doc_comment FROM symbols s";
|
|
1130
|
+
if (conditions.length > 0) {
|
|
1131
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
1132
|
+
}
|
|
1133
|
+
query += " ORDER BY s.name LIMIT ?";
|
|
1134
|
+
queryParams.push(params.limit);
|
|
1135
|
+
const rows = db.prepare(query).all(...queryParams);
|
|
1136
|
+
if (rows.length === 0) {
|
|
1137
|
+
return textResult("No symbols found matching the search criteria.");
|
|
1138
|
+
}
|
|
1139
|
+
const lines = [
|
|
1140
|
+
`# Symbol Search Results (${rows.length}${rows.length === params.limit ? "+" : ""})`,
|
|
1141
|
+
""
|
|
1142
|
+
];
|
|
1143
|
+
for (const s of rows) {
|
|
1144
|
+
const flags = [
|
|
1145
|
+
s.is_exported ? "exported" : "internal",
|
|
1146
|
+
s.is_async ? "async" : ""
|
|
1147
|
+
].filter(Boolean).join(", ");
|
|
1148
|
+
lines.push(
|
|
1149
|
+
`## \`${s.qualified_name}\` (${s.kind})`,
|
|
1150
|
+
"",
|
|
1151
|
+
`- **ID:** \`${s.id}\``,
|
|
1152
|
+
`- **Location:** \`${s.file_path}:${s.start_line}\``,
|
|
1153
|
+
`- **Flags:** ${flags}`
|
|
1154
|
+
);
|
|
1155
|
+
if (s.signature) {
|
|
1156
|
+
lines.push(`- **Signature:** \`${s.signature}\``);
|
|
1157
|
+
}
|
|
1158
|
+
if (s.return_type) {
|
|
1159
|
+
lines.push(`- **Returns:** \`${s.return_type}\``);
|
|
1160
|
+
}
|
|
1161
|
+
if (s.doc_comment) {
|
|
1162
|
+
lines.push(`- **Docs:** ${s.doc_comment}`);
|
|
1163
|
+
}
|
|
1164
|
+
lines.push("");
|
|
1165
|
+
}
|
|
1166
|
+
return textResult(lines.join("\n"));
|
|
1167
|
+
}
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/tools/get-symbol.ts
|
|
1172
|
+
import { z as z13 } from "zod";
|
|
1173
|
+
import { readFileSync, existsSync as existsSync3 } from "fs";
|
|
1174
|
+
import { join as join3 } from "path";
|
|
1175
|
+
function registerGetSymbol(server, ctx) {
|
|
1176
|
+
server.tool(
|
|
1177
|
+
"arcbridge_get_symbol",
|
|
1178
|
+
"Get detailed information about a specific TypeScript symbol including its source code, type signature, and relationships.",
|
|
1179
|
+
{
|
|
1180
|
+
target_dir: z13.string().describe("Absolute path to the project directory"),
|
|
1181
|
+
symbol_id: z13.string().describe(
|
|
1182
|
+
"Symbol ID (e.g. 'src/utils.ts::formatName#function')"
|
|
1183
|
+
),
|
|
1184
|
+
include_source: z13.boolean().default(true).describe("Include source code snippet (default: true)")
|
|
1185
|
+
},
|
|
1186
|
+
async (params) => {
|
|
1187
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1188
|
+
if (!db) return notInitialized();
|
|
1189
|
+
const symbol = db.prepare("SELECT * FROM symbols WHERE id = ?").get(params.symbol_id);
|
|
1190
|
+
if (!symbol) {
|
|
1191
|
+
return textResult(
|
|
1192
|
+
`Symbol not found: \`${params.symbol_id}\`
|
|
1193
|
+
|
|
1194
|
+
Use \`arcbridge_search_symbols\` to find symbols by name.`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const lines = [
|
|
1198
|
+
`# ${symbol.qualified_name}`,
|
|
1199
|
+
"",
|
|
1200
|
+
`| Field | Value |`,
|
|
1201
|
+
`|-------|-------|`,
|
|
1202
|
+
`| **Kind** | ${symbol.kind} |`,
|
|
1203
|
+
`| **File** | \`${symbol.file_path}:${symbol.start_line}\` |`,
|
|
1204
|
+
`| **Exported** | ${symbol.is_exported ? "yes" : "no"} |`,
|
|
1205
|
+
`| **Async** | ${symbol.is_async ? "yes" : "no"} |`,
|
|
1206
|
+
`| **Service** | ${symbol.service} |`
|
|
1207
|
+
];
|
|
1208
|
+
if (symbol.signature) {
|
|
1209
|
+
lines.push(`| **Signature** | \`${symbol.signature}\` |`);
|
|
1210
|
+
}
|
|
1211
|
+
if (symbol.return_type) {
|
|
1212
|
+
lines.push(`| **Return type** | \`${symbol.return_type}\` |`);
|
|
1213
|
+
}
|
|
1214
|
+
lines.push("");
|
|
1215
|
+
if (symbol.doc_comment) {
|
|
1216
|
+
lines.push("## Documentation", "", symbol.doc_comment, "");
|
|
1217
|
+
}
|
|
1218
|
+
if (params.include_source) {
|
|
1219
|
+
const absPath = join3(params.target_dir, symbol.file_path);
|
|
1220
|
+
if (existsSync3(absPath)) {
|
|
1221
|
+
try {
|
|
1222
|
+
const content = readFileSync(absPath, "utf-8");
|
|
1223
|
+
const fileLines = content.split("\n");
|
|
1224
|
+
const contextBefore = 2;
|
|
1225
|
+
const startIdx = Math.max(0, symbol.start_line - 1 - contextBefore);
|
|
1226
|
+
const endIdx = Math.min(fileLines.length, symbol.end_line);
|
|
1227
|
+
const snippet = fileLines.slice(startIdx, endIdx).map((line, i) => {
|
|
1228
|
+
const lineNum = startIdx + i + 1;
|
|
1229
|
+
const marker = lineNum >= symbol.start_line && lineNum <= symbol.end_line ? ">" : " ";
|
|
1230
|
+
return `${marker} ${String(lineNum).padStart(4)} | ${line}`;
|
|
1231
|
+
}).join("\n");
|
|
1232
|
+
lines.push("## Source", "", "```typescript", snippet, "```", "");
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const callees = db.prepare(
|
|
1238
|
+
`SELECT d.target_symbol as symbol_id, s.name as symbol_name, d.kind, s.file_path
|
|
1239
|
+
FROM dependencies d
|
|
1240
|
+
JOIN symbols s ON s.id = d.target_symbol
|
|
1241
|
+
WHERE d.source_symbol = ?
|
|
1242
|
+
ORDER BY d.kind, s.name`
|
|
1243
|
+
).all(params.symbol_id);
|
|
1244
|
+
const callers = db.prepare(
|
|
1245
|
+
`SELECT d.source_symbol as symbol_id, s.name as symbol_name, d.kind, s.file_path
|
|
1246
|
+
FROM dependencies d
|
|
1247
|
+
JOIN symbols s ON s.id = d.source_symbol
|
|
1248
|
+
WHERE d.target_symbol = ?
|
|
1249
|
+
ORDER BY d.kind, s.name`
|
|
1250
|
+
).all(params.symbol_id);
|
|
1251
|
+
if (callees.length > 0) {
|
|
1252
|
+
lines.push("## Dependencies (this symbol uses)", "");
|
|
1253
|
+
for (const dep of callees) {
|
|
1254
|
+
lines.push(`- **${dep.kind}** \u2192 \`${dep.symbol_name}\` (\`${dep.file_path}\`)`);
|
|
1255
|
+
}
|
|
1256
|
+
lines.push("");
|
|
1257
|
+
}
|
|
1258
|
+
if (callers.length > 0) {
|
|
1259
|
+
lines.push("## Dependents (uses this symbol)", "");
|
|
1260
|
+
for (const dep of callers) {
|
|
1261
|
+
lines.push(`- **${dep.kind}** \u2190 \`${dep.symbol_name}\` (\`${dep.file_path}\`)`);
|
|
1262
|
+
}
|
|
1263
|
+
lines.push("");
|
|
1264
|
+
}
|
|
1265
|
+
const blocks = db.prepare("SELECT id, name, code_paths FROM building_blocks").all();
|
|
1266
|
+
for (const block of blocks) {
|
|
1267
|
+
const codePaths = safeParseJson(block.code_paths, []);
|
|
1268
|
+
for (const cp of codePaths) {
|
|
1269
|
+
const prefix = cp.replace(/\*\*?\/?\*?$/, "");
|
|
1270
|
+
if (symbol.file_path.startsWith(prefix)) {
|
|
1271
|
+
lines.push(`## Building Block`, "", `Part of **${block.name}** (\`${block.id}\`)`, "");
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return textResult(lines.join("\n"));
|
|
1277
|
+
}
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/tools/get-dependency-graph.ts
|
|
1282
|
+
import { z as z14 } from "zod";
|
|
1283
|
+
function registerGetDependencyGraph(server, ctx) {
|
|
1284
|
+
server.tool(
|
|
1285
|
+
"arcbridge_get_dependency_graph",
|
|
1286
|
+
"Get the dependency graph for a module or file. Shows imports, calls, type usage, and inheritance relationships between symbols.",
|
|
1287
|
+
{
|
|
1288
|
+
target_dir: z14.string().describe("Absolute path to the project directory"),
|
|
1289
|
+
module: z14.string().describe(
|
|
1290
|
+
"Module path relative to project root (e.g. 'src/lib/auth')"
|
|
1291
|
+
),
|
|
1292
|
+
direction: z14.enum(["dependencies", "dependents", "both"]).default("both").describe(
|
|
1293
|
+
"Graph direction: 'dependencies' (what this module uses), 'dependents' (what uses this module), or 'both'"
|
|
1294
|
+
),
|
|
1295
|
+
depth: z14.number().int().min(1).max(5).default(1).describe("How many levels to traverse (default: 1, max: 5)"),
|
|
1296
|
+
service: z14.string().optional().describe("Filter by service name (for multi-project solutions). Omit to search all services.")
|
|
1297
|
+
},
|
|
1298
|
+
async (params) => {
|
|
1299
|
+
const maybeDb = ensureDb(ctx, params.target_dir);
|
|
1300
|
+
if (!maybeDb) return notInitialized();
|
|
1301
|
+
const db = maybeDb;
|
|
1302
|
+
const depCount = db.prepare("SELECT COUNT(*) as count FROM dependencies").get().count;
|
|
1303
|
+
if (depCount === 0) {
|
|
1304
|
+
return getFileImportGraph(db, params.module, params.direction);
|
|
1305
|
+
}
|
|
1306
|
+
const edges = [];
|
|
1307
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1308
|
+
const serviceFilter = params.service ? " AND s1.service = ?" : "";
|
|
1309
|
+
const serviceFilter2 = params.service ? " AND s2.service = ?" : "";
|
|
1310
|
+
function collectEdges(modulePath, currentDepth) {
|
|
1311
|
+
if (currentDepth > params.depth || visited.has(modulePath)) return;
|
|
1312
|
+
visited.add(modulePath);
|
|
1313
|
+
const prefix = `${escapeLike(modulePath)}%`;
|
|
1314
|
+
if (params.direction === "dependencies" || params.direction === "both") {
|
|
1315
|
+
const queryArgs = [prefix];
|
|
1316
|
+
if (params.service) queryArgs.push(params.service);
|
|
1317
|
+
const deps = db.prepare(
|
|
1318
|
+
`SELECT d.source_symbol as source_id, s1.name as source_name, s1.file_path as source_file,
|
|
1319
|
+
d.target_symbol as target_id, s2.name as target_name, s2.file_path as target_file,
|
|
1320
|
+
d.kind
|
|
1321
|
+
FROM dependencies d
|
|
1322
|
+
JOIN symbols s1 ON s1.id = d.source_symbol
|
|
1323
|
+
JOIN symbols s2 ON s2.id = d.target_symbol
|
|
1324
|
+
WHERE s1.file_path LIKE ? ESCAPE '\\'${serviceFilter}
|
|
1325
|
+
ORDER BY d.kind, s2.name`
|
|
1326
|
+
).all(...queryArgs);
|
|
1327
|
+
for (const dep of deps) {
|
|
1328
|
+
const key = `${dep.source_id}->${dep.target_id}:${dep.kind}`;
|
|
1329
|
+
if (!visited.has(key)) {
|
|
1330
|
+
edges.push(dep);
|
|
1331
|
+
visited.add(key);
|
|
1332
|
+
if (currentDepth < params.depth) {
|
|
1333
|
+
const targetDir = dep.target_file.replace(/\/[^/]+$/, "");
|
|
1334
|
+
collectEdges(targetDir, currentDepth + 1);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (params.direction === "dependents" || params.direction === "both") {
|
|
1340
|
+
const queryArgs2 = [prefix];
|
|
1341
|
+
if (params.service) queryArgs2.push(params.service);
|
|
1342
|
+
const deps = db.prepare(
|
|
1343
|
+
`SELECT d.source_symbol as source_id, s1.name as source_name, s1.file_path as source_file,
|
|
1344
|
+
d.target_symbol as target_id, s2.name as target_name, s2.file_path as target_file,
|
|
1345
|
+
d.kind
|
|
1346
|
+
FROM dependencies d
|
|
1347
|
+
JOIN symbols s1 ON s1.id = d.source_symbol
|
|
1348
|
+
JOIN symbols s2 ON s2.id = d.target_symbol
|
|
1349
|
+
WHERE s2.file_path LIKE ? ESCAPE '\\'${serviceFilter2}
|
|
1350
|
+
ORDER BY d.kind, s1.name`
|
|
1351
|
+
).all(...queryArgs2);
|
|
1352
|
+
for (const dep of deps) {
|
|
1353
|
+
const key = `${dep.source_id}->${dep.target_id}:${dep.kind}`;
|
|
1354
|
+
if (!visited.has(key)) {
|
|
1355
|
+
edges.push(dep);
|
|
1356
|
+
visited.add(key);
|
|
1357
|
+
if (currentDepth < params.depth) {
|
|
1358
|
+
const sourceDir = dep.source_file.replace(/\/[^/]+$/, "");
|
|
1359
|
+
collectEdges(sourceDir, currentDepth + 1);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
collectEdges(params.module, 1);
|
|
1366
|
+
if (edges.length === 0) {
|
|
1367
|
+
return textResult(
|
|
1368
|
+
`No dependency edges found for module \`${params.module}\`.
|
|
1369
|
+
|
|
1370
|
+
This may mean dependencies haven't been indexed yet (Phase 1b). Run \`arcbridge_reindex\` to update.`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return formatEdges(edges, params.module, params.direction);
|
|
1374
|
+
}
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
function getFileImportGraph(db, modulePath, _direction) {
|
|
1378
|
+
const prefix = `${escapeLike(modulePath)}%`;
|
|
1379
|
+
const symbols = db.prepare(
|
|
1380
|
+
`SELECT file_path, name, kind, is_exported
|
|
1381
|
+
FROM symbols
|
|
1382
|
+
WHERE file_path LIKE ? ESCAPE '\\'
|
|
1383
|
+
ORDER BY file_path, name`
|
|
1384
|
+
).all(prefix);
|
|
1385
|
+
if (symbols.length === 0) {
|
|
1386
|
+
return textResult(
|
|
1387
|
+
`No symbols found in module \`${modulePath}\`. Run \`arcbridge_reindex\` first.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1391
|
+
for (const s of symbols) {
|
|
1392
|
+
const list = byFile.get(s.file_path) ?? [];
|
|
1393
|
+
list.push(s);
|
|
1394
|
+
byFile.set(s.file_path, list);
|
|
1395
|
+
}
|
|
1396
|
+
const lines = [
|
|
1397
|
+
`# Module: ${modulePath}`,
|
|
1398
|
+
"",
|
|
1399
|
+
`> Dependency edges not yet indexed. Showing file-level symbol map.`,
|
|
1400
|
+
`> Run \`arcbridge_reindex\` after Phase 1b to see full dependency graph.`,
|
|
1401
|
+
""
|
|
1402
|
+
];
|
|
1403
|
+
for (const [file, syms] of byFile) {
|
|
1404
|
+
lines.push(`## \`${file}\``, "");
|
|
1405
|
+
for (const s of syms) {
|
|
1406
|
+
const exported = s.is_exported ? " (exported)" : "";
|
|
1407
|
+
lines.push(`- \`${s.name}\` \u2014 ${s.kind}${exported}`);
|
|
1408
|
+
}
|
|
1409
|
+
lines.push("");
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function formatEdges(edges, modulePath, direction) {
|
|
1416
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const e of edges) {
|
|
1418
|
+
const list = byKind.get(e.kind) ?? [];
|
|
1419
|
+
list.push(e);
|
|
1420
|
+
byKind.set(e.kind, list);
|
|
1421
|
+
}
|
|
1422
|
+
const lines = [
|
|
1423
|
+
`# Dependency Graph: ${modulePath}`,
|
|
1424
|
+
"",
|
|
1425
|
+
`**Direction:** ${direction} | **Edges:** ${edges.length}`,
|
|
1426
|
+
""
|
|
1427
|
+
];
|
|
1428
|
+
for (const [kind, kindEdges] of byKind) {
|
|
1429
|
+
lines.push(`## ${kind}`, "");
|
|
1430
|
+
for (const e of kindEdges) {
|
|
1431
|
+
lines.push(
|
|
1432
|
+
`- \`${e.source_name}\` (\`${e.source_file}\`) \u2192 \`${e.target_name}\` (\`${e.target_file}\`)`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/tools/get-component-graph.ts
|
|
1443
|
+
import { z as z15 } from "zod";
|
|
1444
|
+
function registerGetComponentGraph(server, ctx) {
|
|
1445
|
+
server.tool(
|
|
1446
|
+
"arcbridge_get_component_graph",
|
|
1447
|
+
"Get the React component graph: component hierarchy, props, state, context usage, and server/client boundaries.",
|
|
1448
|
+
{
|
|
1449
|
+
target_dir: z15.string().describe("Absolute path to the project directory"),
|
|
1450
|
+
file_path: z15.string().optional().describe("Filter to components in a specific file or directory prefix"),
|
|
1451
|
+
client_only: z15.boolean().optional().describe("Only show client components ('use client')"),
|
|
1452
|
+
with_state: z15.boolean().optional().describe("Only show components that use state (useState/useReducer)")
|
|
1453
|
+
},
|
|
1454
|
+
async (params) => {
|
|
1455
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1456
|
+
if (!db) return notInitialized();
|
|
1457
|
+
let query = `
|
|
1458
|
+
SELECT
|
|
1459
|
+
c.symbol_id, s.name, s.file_path,
|
|
1460
|
+
c.is_client, c.is_server_action, c.has_state,
|
|
1461
|
+
c.context_providers, c.context_consumers, c.props_type,
|
|
1462
|
+
s.is_exported
|
|
1463
|
+
FROM components c
|
|
1464
|
+
JOIN symbols s ON c.symbol_id = s.id
|
|
1465
|
+
`;
|
|
1466
|
+
const conditions = [];
|
|
1467
|
+
const queryParams = [];
|
|
1468
|
+
if (params.file_path) {
|
|
1469
|
+
conditions.push("s.file_path LIKE ? ESCAPE '\\'");
|
|
1470
|
+
queryParams.push(`${escapeLike(params.file_path)}%`);
|
|
1471
|
+
}
|
|
1472
|
+
if (params.client_only) {
|
|
1473
|
+
conditions.push("c.is_client = 1");
|
|
1474
|
+
}
|
|
1475
|
+
if (params.with_state) {
|
|
1476
|
+
conditions.push("c.has_state = 1");
|
|
1477
|
+
}
|
|
1478
|
+
if (conditions.length > 0) {
|
|
1479
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
1480
|
+
}
|
|
1481
|
+
query += " ORDER BY s.file_path, s.name";
|
|
1482
|
+
const components = db.prepare(query).all(...queryParams);
|
|
1483
|
+
if (components.length === 0) {
|
|
1484
|
+
return textResult("No components found. Run `arcbridge_reindex` to analyze React components.");
|
|
1485
|
+
}
|
|
1486
|
+
const renderEdges = db.prepare(
|
|
1487
|
+
`SELECT
|
|
1488
|
+
ss.name as source_name, ss.file_path as source_file,
|
|
1489
|
+
st.name as target_name, st.file_path as target_file
|
|
1490
|
+
FROM dependencies d
|
|
1491
|
+
JOIN symbols ss ON d.source_symbol = ss.id
|
|
1492
|
+
JOIN symbols st ON d.target_symbol = st.id
|
|
1493
|
+
WHERE d.kind = 'renders'
|
|
1494
|
+
AND d.source_symbol IN (SELECT symbol_id FROM components)
|
|
1495
|
+
AND d.target_symbol IN (SELECT symbol_id FROM components)`
|
|
1496
|
+
).all();
|
|
1497
|
+
const lines = [
|
|
1498
|
+
`# Component Graph (${components.length} components)`,
|
|
1499
|
+
""
|
|
1500
|
+
];
|
|
1501
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1502
|
+
for (const c of components) {
|
|
1503
|
+
const existing = byFile.get(c.file_path) ?? [];
|
|
1504
|
+
existing.push(c);
|
|
1505
|
+
byFile.set(c.file_path, existing);
|
|
1506
|
+
}
|
|
1507
|
+
for (const [file, comps] of byFile) {
|
|
1508
|
+
lines.push(`## \`${file}\``, "");
|
|
1509
|
+
for (const c of comps) {
|
|
1510
|
+
const badges = [];
|
|
1511
|
+
if (c.is_client) badges.push("client");
|
|
1512
|
+
if (c.is_server_action) badges.push("server-action");
|
|
1513
|
+
if (c.has_state) badges.push("stateful");
|
|
1514
|
+
if (!c.is_exported) badges.push("internal");
|
|
1515
|
+
const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
|
|
1516
|
+
lines.push(`### ${c.name}${badgeStr}`, "");
|
|
1517
|
+
if (c.props_type) {
|
|
1518
|
+
lines.push(`- **Props:** \`${c.props_type}\``);
|
|
1519
|
+
}
|
|
1520
|
+
const providers = safeParseJson(c.context_providers, []);
|
|
1521
|
+
if (providers.length > 0) {
|
|
1522
|
+
lines.push(`- **Provides context:** ${providers.join(", ")}`);
|
|
1523
|
+
}
|
|
1524
|
+
const consumers = safeParseJson(c.context_consumers, []);
|
|
1525
|
+
if (consumers.length > 0) {
|
|
1526
|
+
lines.push(`- **Consumes context:** ${consumers.join(", ")}`);
|
|
1527
|
+
}
|
|
1528
|
+
const children = renderEdges.filter((e) => e.source_name === c.name && e.source_file === file).map((e) => e.target_name);
|
|
1529
|
+
if (children.length > 0) {
|
|
1530
|
+
lines.push(`- **Renders:** ${children.join(", ")}`);
|
|
1531
|
+
}
|
|
1532
|
+
const parents = renderEdges.filter((e) => e.target_name === c.name && e.target_file === file).map((e) => e.source_name);
|
|
1533
|
+
if (parents.length > 0) {
|
|
1534
|
+
lines.push(`- **Rendered by:** ${parents.join(", ")}`);
|
|
1535
|
+
}
|
|
1536
|
+
lines.push("");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const clientCount = components.filter((c) => c.is_client).length;
|
|
1540
|
+
const statefulCount = components.filter((c) => c.has_state).length;
|
|
1541
|
+
lines.push(
|
|
1542
|
+
"## Summary",
|
|
1543
|
+
"",
|
|
1544
|
+
`- **Total components:** ${components.length}`,
|
|
1545
|
+
`- **Client components:** ${clientCount}`,
|
|
1546
|
+
`- **Stateful components:** ${statefulCount}`,
|
|
1547
|
+
`- **Render edges:** ${renderEdges.length}`,
|
|
1548
|
+
""
|
|
1549
|
+
);
|
|
1550
|
+
return textResult(lines.join("\n"));
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/tools/get-route-map.ts
|
|
1556
|
+
import { z as z16 } from "zod";
|
|
1557
|
+
function registerGetRouteMap(server, ctx) {
|
|
1558
|
+
server.tool(
|
|
1559
|
+
"arcbridge_get_route_map",
|
|
1560
|
+
"Get the route map: pages, layouts, API routes, and their hierarchy. Works with Next.js, ASP.NET controllers, and minimal APIs.",
|
|
1561
|
+
{
|
|
1562
|
+
target_dir: z16.string().describe("Absolute path to the project directory"),
|
|
1563
|
+
kind: z16.enum(["page", "layout", "loading", "error", "not-found", "api-route", "middleware"]).optional().describe("Filter by route kind"),
|
|
1564
|
+
route_prefix: z16.string().optional().describe("Filter by route path prefix (e.g. '/dashboard' or '/api/orders')"),
|
|
1565
|
+
service: z16.string().optional().describe("Filter by service name (for multi-project solutions). Omit to show all services.")
|
|
1566
|
+
},
|
|
1567
|
+
async (params) => {
|
|
1568
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1569
|
+
if (!db) return notInitialized();
|
|
1570
|
+
let query = "SELECT * FROM routes";
|
|
1571
|
+
const conditions = [];
|
|
1572
|
+
const queryParams = [];
|
|
1573
|
+
if (params.service) {
|
|
1574
|
+
conditions.push("service = ?");
|
|
1575
|
+
queryParams.push(params.service);
|
|
1576
|
+
}
|
|
1577
|
+
if (params.kind) {
|
|
1578
|
+
conditions.push("kind = ?");
|
|
1579
|
+
queryParams.push(params.kind);
|
|
1580
|
+
}
|
|
1581
|
+
if (params.route_prefix) {
|
|
1582
|
+
conditions.push("route_path LIKE ? ESCAPE '\\'");
|
|
1583
|
+
queryParams.push(`${escapeLike(params.route_prefix)}%`);
|
|
1584
|
+
}
|
|
1585
|
+
if (conditions.length > 0) {
|
|
1586
|
+
query += " WHERE " + conditions.join(" AND ");
|
|
1587
|
+
}
|
|
1588
|
+
query += " ORDER BY route_path, kind";
|
|
1589
|
+
const routes = db.prepare(query).all(...queryParams);
|
|
1590
|
+
if (routes.length === 0) {
|
|
1591
|
+
return textResult("No routes found. Run `arcbridge_reindex` to analyze the Next.js app/ directory.");
|
|
1592
|
+
}
|
|
1593
|
+
const lines = [
|
|
1594
|
+
`# Route Map (${routes.length} routes)`,
|
|
1595
|
+
""
|
|
1596
|
+
];
|
|
1597
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1598
|
+
for (const r of routes) {
|
|
1599
|
+
const existing = byPath.get(r.route_path) ?? [];
|
|
1600
|
+
existing.push(r);
|
|
1601
|
+
byPath.set(r.route_path, existing);
|
|
1602
|
+
}
|
|
1603
|
+
for (const [path, routeGroup] of byPath) {
|
|
1604
|
+
lines.push(`## \`${path}\``);
|
|
1605
|
+
lines.push("");
|
|
1606
|
+
for (const r of routeGroup) {
|
|
1607
|
+
const parts = [`- **${r.kind}**`];
|
|
1608
|
+
const methods = safeParseJson(r.http_methods, []);
|
|
1609
|
+
if (methods.length > 0) {
|
|
1610
|
+
parts.push(`Methods: ${methods.join(", ")}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (r.has_auth) {
|
|
1613
|
+
parts.push("(auth)");
|
|
1614
|
+
}
|
|
1615
|
+
if (r.parent_layout) {
|
|
1616
|
+
parts.push(`Layout: \`${r.parent_layout}\``);
|
|
1617
|
+
}
|
|
1618
|
+
lines.push(parts.join(" | "));
|
|
1619
|
+
}
|
|
1620
|
+
lines.push("");
|
|
1621
|
+
}
|
|
1622
|
+
const pages = routes.filter((r) => r.kind === "page").length;
|
|
1623
|
+
const layouts = routes.filter((r) => r.kind === "layout").length;
|
|
1624
|
+
const apiRoutes = routes.filter((r) => r.kind === "api-route").length;
|
|
1625
|
+
const loadingStates = routes.filter((r) => r.kind === "loading").length;
|
|
1626
|
+
lines.push(
|
|
1627
|
+
"## Summary",
|
|
1628
|
+
"",
|
|
1629
|
+
`- **Pages:** ${pages}`,
|
|
1630
|
+
`- **Layouts:** ${layouts}`,
|
|
1631
|
+
`- **API routes:** ${apiRoutes}`,
|
|
1632
|
+
`- **Loading states:** ${loadingStates}`,
|
|
1633
|
+
""
|
|
1634
|
+
);
|
|
1635
|
+
return textResult(lines.join("\n"));
|
|
1636
|
+
}
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/tools/get-boundary-analysis.ts
|
|
1641
|
+
import { z as z17 } from "zod";
|
|
1642
|
+
function registerGetBoundaryAnalysis(server, ctx) {
|
|
1643
|
+
server.tool(
|
|
1644
|
+
"arcbridge_get_boundary_analysis",
|
|
1645
|
+
"Analyze server/client boundaries in a Next.js project. Identifies client components, server components, server actions, and potential boundary violations.",
|
|
1646
|
+
{
|
|
1647
|
+
target_dir: z17.string().describe("Absolute path to the project directory")
|
|
1648
|
+
},
|
|
1649
|
+
async (params) => {
|
|
1650
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1651
|
+
if (!db) return notInitialized();
|
|
1652
|
+
const components = db.prepare(
|
|
1653
|
+
`SELECT c.symbol_id, s.name, s.file_path,
|
|
1654
|
+
c.is_client, c.is_server_action, c.has_state
|
|
1655
|
+
FROM components c
|
|
1656
|
+
JOIN symbols s ON c.symbol_id = s.id
|
|
1657
|
+
ORDER BY s.file_path`
|
|
1658
|
+
).all();
|
|
1659
|
+
if (components.length === 0) {
|
|
1660
|
+
return textResult("No components found. Run `arcbridge_reindex` to analyze server/client boundaries.");
|
|
1661
|
+
}
|
|
1662
|
+
const crossEdges = db.prepare(
|
|
1663
|
+
`SELECT
|
|
1664
|
+
ss.name as source_name, ss.file_path as source_file, cs.is_client as source_is_client,
|
|
1665
|
+
st.name as target_name, st.file_path as target_file, ct.is_client as target_is_client,
|
|
1666
|
+
d.kind
|
|
1667
|
+
FROM dependencies d
|
|
1668
|
+
JOIN symbols ss ON d.source_symbol = ss.id
|
|
1669
|
+
JOIN symbols st ON d.target_symbol = st.id
|
|
1670
|
+
LEFT JOIN components cs ON d.source_symbol = cs.symbol_id
|
|
1671
|
+
LEFT JOIN components ct ON d.target_symbol = ct.symbol_id
|
|
1672
|
+
WHERE d.kind IN ('renders', 'imports')
|
|
1673
|
+
AND (cs.symbol_id IS NOT NULL OR ct.symbol_id IS NOT NULL)`
|
|
1674
|
+
).all();
|
|
1675
|
+
const clientComponents = components.filter((c) => c.is_client);
|
|
1676
|
+
const serverComponents = components.filter((c) => !c.is_client && !c.is_server_action);
|
|
1677
|
+
const serverActions = components.filter((c) => c.is_server_action);
|
|
1678
|
+
const lines = [
|
|
1679
|
+
"# Server/Client Boundary Analysis",
|
|
1680
|
+
"",
|
|
1681
|
+
"## Overview",
|
|
1682
|
+
"",
|
|
1683
|
+
`- **Server components:** ${serverComponents.length}`,
|
|
1684
|
+
`- **Client components:** ${clientComponents.length}`,
|
|
1685
|
+
`- **Server actions:** ${serverActions.length}`,
|
|
1686
|
+
""
|
|
1687
|
+
];
|
|
1688
|
+
if (clientComponents.length > 0) {
|
|
1689
|
+
lines.push("## Client Components (`'use client'`)", "");
|
|
1690
|
+
for (const c of clientComponents) {
|
|
1691
|
+
const badges = [];
|
|
1692
|
+
if (c.has_state) badges.push("stateful");
|
|
1693
|
+
const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
|
|
1694
|
+
lines.push(`- \`${c.file_path}\` \u2192 **${c.name}**${badgeStr}`);
|
|
1695
|
+
}
|
|
1696
|
+
lines.push("");
|
|
1697
|
+
}
|
|
1698
|
+
if (serverComponents.length > 0) {
|
|
1699
|
+
lines.push("## Server Components (default)", "");
|
|
1700
|
+
for (const c of serverComponents) {
|
|
1701
|
+
lines.push(`- \`${c.file_path}\` \u2192 **${c.name}**`);
|
|
1702
|
+
}
|
|
1703
|
+
lines.push("");
|
|
1704
|
+
}
|
|
1705
|
+
if (serverActions.length > 0) {
|
|
1706
|
+
lines.push("## Server Actions (`'use server'`)", "");
|
|
1707
|
+
for (const c of serverActions) {
|
|
1708
|
+
lines.push(`- \`${c.file_path}\` \u2192 **${c.name}**`);
|
|
1709
|
+
}
|
|
1710
|
+
lines.push("");
|
|
1711
|
+
}
|
|
1712
|
+
const boundaryViolations = [];
|
|
1713
|
+
const validCrossings = [];
|
|
1714
|
+
for (const edge of crossEdges) {
|
|
1715
|
+
if (edge.kind !== "renders") continue;
|
|
1716
|
+
const sourceIsClient = edge.source_is_client === 1;
|
|
1717
|
+
const targetIsClient = edge.target_is_client === 1;
|
|
1718
|
+
if (sourceIsClient === targetIsClient) continue;
|
|
1719
|
+
if (!sourceIsClient && targetIsClient) {
|
|
1720
|
+
validCrossings.push(
|
|
1721
|
+
`- **${edge.source_name}** (server) \u2192 **${edge.target_name}** (client)`
|
|
1722
|
+
);
|
|
1723
|
+
} else if (sourceIsClient && !targetIsClient) {
|
|
1724
|
+
boundaryViolations.push(
|
|
1725
|
+
`- **${edge.source_name}** (client, \`${edge.source_file}\`) renders **${edge.target_name}** (server, \`${edge.target_file}\`)`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (validCrossings.length > 0) {
|
|
1730
|
+
lines.push("## Boundary Crossings (valid)", "");
|
|
1731
|
+
lines.push(...validCrossings, "");
|
|
1732
|
+
}
|
|
1733
|
+
if (boundaryViolations.length > 0) {
|
|
1734
|
+
lines.push("## Potential Boundary Violations", "");
|
|
1735
|
+
lines.push(
|
|
1736
|
+
"These client components appear to render server components, which is not allowed in Next.js App Router:",
|
|
1737
|
+
"",
|
|
1738
|
+
...boundaryViolations,
|
|
1739
|
+
""
|
|
1740
|
+
);
|
|
1741
|
+
} else {
|
|
1742
|
+
lines.push("## Boundary Check", "");
|
|
1743
|
+
lines.push("No boundary violations detected.", "");
|
|
1744
|
+
}
|
|
1745
|
+
return textResult(lines.join("\n"));
|
|
1746
|
+
}
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/tools/check-drift.ts
|
|
1751
|
+
import { z as z18 } from "zod";
|
|
1752
|
+
import { detectDrift, writeDriftLog } from "@arcbridge/core";
|
|
1753
|
+
function registerCheckDrift(server, ctx) {
|
|
1754
|
+
server.tool(
|
|
1755
|
+
"arcbridge_check_drift",
|
|
1756
|
+
"Detect architecture drift: undocumented modules, missing code paths, cross-block dependency violations, and stale ADR references.",
|
|
1757
|
+
{
|
|
1758
|
+
target_dir: z18.string().describe("Absolute path to the project directory"),
|
|
1759
|
+
persist: z18.boolean().default(true).describe("Write findings to drift_log table (default: true)")
|
|
1760
|
+
},
|
|
1761
|
+
async (params) => {
|
|
1762
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1763
|
+
if (!db) return notInitialized();
|
|
1764
|
+
const entries = detectDrift(db);
|
|
1765
|
+
if (params.persist) {
|
|
1766
|
+
writeDriftLog(db, entries);
|
|
1767
|
+
}
|
|
1768
|
+
if (entries.length === 0) {
|
|
1769
|
+
return textResult(
|
|
1770
|
+
"# Drift Check\n\nNo architecture drift detected. Code aligns with documented building blocks."
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1774
|
+
for (const e of entries) {
|
|
1775
|
+
const existing = byKind.get(e.kind) ?? [];
|
|
1776
|
+
existing.push(e);
|
|
1777
|
+
byKind.set(e.kind, existing);
|
|
1778
|
+
}
|
|
1779
|
+
const kindLabels = {
|
|
1780
|
+
undocumented_module: "Undocumented Modules",
|
|
1781
|
+
missing_module: "Missing Modules",
|
|
1782
|
+
dependency_violation: "Dependency Violations",
|
|
1783
|
+
stale_adr: "Stale ADR References",
|
|
1784
|
+
unlinked_test: "Unlinked Tests"
|
|
1785
|
+
};
|
|
1786
|
+
const severityIcon = {
|
|
1787
|
+
error: "ERROR",
|
|
1788
|
+
warning: "WARN",
|
|
1789
|
+
info: "INFO"
|
|
1790
|
+
};
|
|
1791
|
+
const lines = [
|
|
1792
|
+
`# Drift Check (${entries.length} issues)`,
|
|
1793
|
+
""
|
|
1794
|
+
];
|
|
1795
|
+
const errors = entries.filter((e) => e.severity === "error").length;
|
|
1796
|
+
const warnings = entries.filter((e) => e.severity === "warning").length;
|
|
1797
|
+
const infos = entries.filter((e) => e.severity === "info").length;
|
|
1798
|
+
lines.push(
|
|
1799
|
+
`**${errors}** errors, **${warnings}** warnings, **${infos}** info`,
|
|
1800
|
+
""
|
|
1801
|
+
);
|
|
1802
|
+
for (const [kind, items] of byKind) {
|
|
1803
|
+
lines.push(`## ${kindLabels[kind] ?? kind}`, "");
|
|
1804
|
+
for (const item of items) {
|
|
1805
|
+
const icon = severityIcon[item.severity] ?? item.severity;
|
|
1806
|
+
lines.push(`- [${icon}] ${item.description}`);
|
|
1807
|
+
}
|
|
1808
|
+
lines.push("");
|
|
1809
|
+
}
|
|
1810
|
+
if (params.persist) {
|
|
1811
|
+
lines.push(
|
|
1812
|
+
"---",
|
|
1813
|
+
"*Findings saved to drift_log. Use `arcbridge_update_task` or resolve drift by updating `.arcbridge/arc42/05-building-blocks.md`.*",
|
|
1814
|
+
""
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
return textResult(lines.join("\n"));
|
|
1818
|
+
}
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/tools/get-guidance.ts
|
|
1823
|
+
import { z as z19 } from "zod";
|
|
1824
|
+
function registerGetGuidance(server, ctx) {
|
|
1825
|
+
server.tool(
|
|
1826
|
+
"arcbridge_get_guidance",
|
|
1827
|
+
"Get context-aware architectural guidance for a code change. Surfaces relevant quality scenarios, patterns, constraints, and questions to consider.",
|
|
1828
|
+
{
|
|
1829
|
+
target_dir: z19.string().describe("Absolute path to the project directory"),
|
|
1830
|
+
file_path: z19.string().optional().describe("File path you're working on (to determine building block and context)"),
|
|
1831
|
+
action: z19.enum([
|
|
1832
|
+
"adding-component",
|
|
1833
|
+
"adding-api-route",
|
|
1834
|
+
"adding-hook",
|
|
1835
|
+
"modifying-auth",
|
|
1836
|
+
"new-dependency",
|
|
1837
|
+
"refactoring",
|
|
1838
|
+
"general"
|
|
1839
|
+
]).default("general").describe("Type of change you're making")
|
|
1840
|
+
},
|
|
1841
|
+
async (params) => {
|
|
1842
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
1843
|
+
if (!db) return notInitialized();
|
|
1844
|
+
const lines = ["# Architectural Guidance", ""];
|
|
1845
|
+
let matchedBlock = null;
|
|
1846
|
+
if (params.file_path) {
|
|
1847
|
+
const blocks = db.prepare("SELECT id, name, responsibility, code_paths, interfaces FROM building_blocks").all();
|
|
1848
|
+
for (const block of blocks) {
|
|
1849
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
1850
|
+
for (const cp of paths) {
|
|
1851
|
+
const prefix = normalizeCodePath(cp);
|
|
1852
|
+
if (params.file_path.startsWith(prefix) || params.file_path === prefix) {
|
|
1853
|
+
matchedBlock = block;
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (matchedBlock) break;
|
|
1858
|
+
}
|
|
1859
|
+
if (matchedBlock) {
|
|
1860
|
+
lines.push(
|
|
1861
|
+
`## Building Block: ${matchedBlock.name} (\`${matchedBlock.id}\`)`,
|
|
1862
|
+
"",
|
|
1863
|
+
`**Responsibility:** ${matchedBlock.responsibility}`,
|
|
1864
|
+
""
|
|
1865
|
+
);
|
|
1866
|
+
const interfaces = safeParseJson(matchedBlock.interfaces, []);
|
|
1867
|
+
if (interfaces.length > 0) {
|
|
1868
|
+
lines.push(
|
|
1869
|
+
`**Declared interfaces:** ${interfaces.join(", ")}`,
|
|
1870
|
+
""
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
} else {
|
|
1874
|
+
lines.push(
|
|
1875
|
+
"## Warning: Unmapped File",
|
|
1876
|
+
"",
|
|
1877
|
+
`\`${params.file_path}\` is not mapped to any building block. Consider updating \`.arcbridge/arc42/05-building-blocks.md\` to include this path.`,
|
|
1878
|
+
""
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
const existingSymbols = db.prepare(
|
|
1882
|
+
"SELECT name, kind, file_path FROM symbols WHERE file_path LIKE ? ESCAPE '\\' ORDER BY kind, name LIMIT 20"
|
|
1883
|
+
).all(`${escapeLike(params.file_path.replace(/\/[^/]+$/, "/"))}%`);
|
|
1884
|
+
if (existingSymbols.length > 0) {
|
|
1885
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const s of existingSymbols) {
|
|
1887
|
+
const existing = byKind.get(s.kind) ?? [];
|
|
1888
|
+
existing.push(s.name);
|
|
1889
|
+
byKind.set(s.kind, existing);
|
|
1890
|
+
}
|
|
1891
|
+
lines.push("## Existing Patterns Nearby", "");
|
|
1892
|
+
for (const [kind, names] of byKind) {
|
|
1893
|
+
lines.push(`- **${kind}s:** ${names.join(", ")}`);
|
|
1894
|
+
}
|
|
1895
|
+
lines.push("");
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const scenarios = db.prepare(
|
|
1899
|
+
"SELECT id, name, category, scenario, expected, priority FROM quality_scenarios ORDER BY priority, category"
|
|
1900
|
+
).all();
|
|
1901
|
+
const relevantScenarios = filterRelevantScenarios(
|
|
1902
|
+
scenarios,
|
|
1903
|
+
params.action,
|
|
1904
|
+
matchedBlock?.id ?? null
|
|
1905
|
+
);
|
|
1906
|
+
if (relevantScenarios.length > 0) {
|
|
1907
|
+
lines.push("## Relevant Quality Scenarios", "");
|
|
1908
|
+
for (const s of relevantScenarios) {
|
|
1909
|
+
lines.push(
|
|
1910
|
+
`### ${s.id}: ${s.name} [${s.category}] (${s.priority})`,
|
|
1911
|
+
"",
|
|
1912
|
+
`**Scenario:** ${s.scenario}`,
|
|
1913
|
+
`**Expected:** ${s.expected}`,
|
|
1914
|
+
""
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (matchedBlock) {
|
|
1919
|
+
const tasks = db.prepare(
|
|
1920
|
+
"SELECT id, title, status, building_block FROM tasks WHERE building_block = ? AND status IN ('todo', 'in-progress')"
|
|
1921
|
+
).all(matchedBlock.id);
|
|
1922
|
+
if (tasks.length > 0) {
|
|
1923
|
+
lines.push("## Active Tasks in This Block", "");
|
|
1924
|
+
for (const t of tasks) {
|
|
1925
|
+
lines.push(`- [${t.status}] ${t.id}: ${t.title}`);
|
|
1926
|
+
}
|
|
1927
|
+
lines.push("");
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const adrRows = db.prepare("SELECT id, title, status, decision, affected_blocks, affected_files FROM adrs").all();
|
|
1931
|
+
const relevantAdrs = adrRows.filter((adr) => {
|
|
1932
|
+
if (params.file_path) {
|
|
1933
|
+
const affectedFiles = safeParseJson(adr.affected_files, []);
|
|
1934
|
+
if (affectedFiles.some((f) => params.file_path.includes(f) || f.includes(params.file_path))) return true;
|
|
1935
|
+
}
|
|
1936
|
+
if (matchedBlock) {
|
|
1937
|
+
const affectedBlocks = safeParseJson(adr.affected_blocks, []);
|
|
1938
|
+
if (affectedBlocks.includes(matchedBlock.id)) return true;
|
|
1939
|
+
}
|
|
1940
|
+
return false;
|
|
1941
|
+
});
|
|
1942
|
+
if (relevantAdrs.length > 0) {
|
|
1943
|
+
lines.push("## Relevant ADRs", "");
|
|
1944
|
+
for (const adr of relevantAdrs) {
|
|
1945
|
+
lines.push(`### ${adr.id}: ${adr.title} [${adr.status}]`, "");
|
|
1946
|
+
lines.push(`**Decision:** ${adr.decision}`, "");
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
const driftEntries = params.file_path ? db.prepare(
|
|
1950
|
+
"SELECT kind, description FROM drift_log WHERE resolution IS NULL AND (affected_file = ? OR affected_block = ?)"
|
|
1951
|
+
).all(params.file_path, matchedBlock?.id ?? "") : db.prepare(
|
|
1952
|
+
"SELECT kind, description FROM drift_log WHERE resolution IS NULL LIMIT 5"
|
|
1953
|
+
).all();
|
|
1954
|
+
if (driftEntries.length > 0) {
|
|
1955
|
+
lines.push("## Unresolved Drift", "");
|
|
1956
|
+
for (const d of driftEntries) {
|
|
1957
|
+
lines.push(`- [${d.kind}] ${d.description}`);
|
|
1958
|
+
}
|
|
1959
|
+
lines.push("");
|
|
1960
|
+
}
|
|
1961
|
+
const actionGuidance = getActionGuidance(params.action);
|
|
1962
|
+
if (actionGuidance) {
|
|
1963
|
+
lines.push("## Guidance", "", actionGuidance, "");
|
|
1964
|
+
}
|
|
1965
|
+
return textResult(lines.join("\n"));
|
|
1966
|
+
}
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
function filterRelevantScenarios(scenarios, action, _blockId) {
|
|
1970
|
+
const categoryMap = {
|
|
1971
|
+
"adding-component": ["accessibility", "performance", "maintainability"],
|
|
1972
|
+
"adding-api-route": ["security", "performance", "reliability"],
|
|
1973
|
+
"adding-hook": ["maintainability", "performance"],
|
|
1974
|
+
"modifying-auth": ["security", "reliability"],
|
|
1975
|
+
"new-dependency": ["maintainability", "performance", "security"],
|
|
1976
|
+
"refactoring": ["maintainability", "reliability"],
|
|
1977
|
+
"general": []
|
|
1978
|
+
};
|
|
1979
|
+
const relevantCategories = categoryMap[action] ?? [];
|
|
1980
|
+
if (relevantCategories.length === 0) {
|
|
1981
|
+
return scenarios.filter((s) => s.priority === "must");
|
|
1982
|
+
}
|
|
1983
|
+
return scenarios.filter((s) => relevantCategories.includes(s.category));
|
|
1984
|
+
}
|
|
1985
|
+
function getActionGuidance(action) {
|
|
1986
|
+
const guidance = {
|
|
1987
|
+
"adding-component": "- Follow existing component patterns in this directory\n- Add props interface alongside the component\n- Consider server vs. client: does this need interactivity (`'use client'`)?\n- Check accessibility: keyboard navigation, ARIA labels, screen reader support\n- **Arc42:** If this introduces a new UI pattern, document it in `08-crosscutting.md`",
|
|
1988
|
+
"adding-api-route": "- Ensure authentication middleware covers this route\n- Validate all input with zod or equivalent\n- Follow existing error response patterns\n- Consider rate limiting for public endpoints\n- If this introduces a new API pattern or convention, document it in an ADR\n- **Arc42:** Update `03-context.md` if this route exposes a new external integration; update `06-runtime-views.md` if it's a key workflow",
|
|
1989
|
+
"adding-hook": "- Follow the `use` prefix convention\n- Keep hooks focused \u2014 one responsibility per hook\n- Consider memoization for expensive computations\n- Document the hook's return type",
|
|
1990
|
+
"modifying-auth": "- Check all API routes still have auth coverage after changes\n- Verify no secrets leak to client components\n- Test edge cases: expired tokens, revoked sessions, role changes\n- Update security quality scenarios if behavior changes\n- Document the auth strategy and any changes in an ADR \u2014 auth decisions are critical to trace\n- **Arc42:** Update `08-crosscutting.md` with the auth pattern; update `06-runtime-views.md` with the auth flow",
|
|
1991
|
+
"new-dependency": "- Document the dependency rationale in an ADR\n- Check bundle size impact (client-side deps)\n- Verify the dependency doesn't introduce known CVEs\n- Ensure the dependency's license is compatible\n- **Arc42:** If this dependency introduces a new external system, update `03-context.md`",
|
|
1992
|
+
"refactoring": "- Ensure no cross-block boundary violations are introduced\n- Maintain existing public API contracts\n- Run tests before and after to verify behavior preservation\n- Check that no quality scenarios regress\n- If the refactoring changes architectural patterns, update or create an ADR to explain why\n- **Arc42:** Update `05-building-blocks.md` if module structure changed; update `08-crosscutting.md` if patterns changed",
|
|
1993
|
+
"general": "- Check `arcbridge_get_relevant_adrs` for existing decisions that may constrain this change\n- If you're choosing between approaches, document the decision in an ADR\n- **Arc42:** Consider which documentation sections may need updating (check `.arcbridge/arc42/` \u2014 especially `05-building-blocks.md` and `08-crosscutting.md`)"
|
|
1994
|
+
};
|
|
1995
|
+
return guidance[action] ?? null;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/tools/get-open-questions.ts
|
|
1999
|
+
import { z as z20 } from "zod";
|
|
2000
|
+
function registerGetOpenQuestions(server, ctx) {
|
|
2001
|
+
server.tool(
|
|
2002
|
+
"arcbridge_get_open_questions",
|
|
2003
|
+
"Surface architectural gaps: untested quality scenarios, building blocks without boundaries, unresolved drift, and tasks missing acceptance criteria.",
|
|
2004
|
+
{
|
|
2005
|
+
target_dir: z20.string().describe("Absolute path to the project directory"),
|
|
2006
|
+
scope: z20.string().optional().describe("Focus scope: 'current-phase', 'building-block:<id>', or omit for project-wide")
|
|
2007
|
+
},
|
|
2008
|
+
async (params) => {
|
|
2009
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
2010
|
+
if (!db) return notInitialized();
|
|
2011
|
+
const lines = ["# Open Questions & Gaps", ""];
|
|
2012
|
+
let totalGaps = 0;
|
|
2013
|
+
const scenarios = db.prepare(
|
|
2014
|
+
"SELECT id, name, category, status, priority, linked_tests, linked_code FROM quality_scenarios ORDER BY priority, category"
|
|
2015
|
+
).all();
|
|
2016
|
+
const untestedMust = scenarios.filter(
|
|
2017
|
+
(s) => s.priority === "must" && (s.status === "untested" || s.status === "failing")
|
|
2018
|
+
);
|
|
2019
|
+
const untestedShould = scenarios.filter(
|
|
2020
|
+
(s) => s.priority === "should" && (s.status === "untested" || s.status === "failing")
|
|
2021
|
+
);
|
|
2022
|
+
const unlinked = scenarios.filter((s) => {
|
|
2023
|
+
const tests = safeParseJson(s.linked_tests, []);
|
|
2024
|
+
return tests.length === 0;
|
|
2025
|
+
});
|
|
2026
|
+
if (untestedMust.length > 0) {
|
|
2027
|
+
lines.push("## Critical: Untested/Failing Must-Have Scenarios", "");
|
|
2028
|
+
for (const s of untestedMust) {
|
|
2029
|
+
lines.push(`- **${s.id}: ${s.name}** [${s.category}] \u2014 ${s.status}`);
|
|
2030
|
+
totalGaps++;
|
|
2031
|
+
}
|
|
2032
|
+
lines.push("");
|
|
2033
|
+
}
|
|
2034
|
+
if (untestedShould.length > 0) {
|
|
2035
|
+
lines.push("## Untested/Failing Should-Have Scenarios", "");
|
|
2036
|
+
for (const s of untestedShould) {
|
|
2037
|
+
lines.push(`- ${s.id}: ${s.name} [${s.category}] \u2014 ${s.status}`);
|
|
2038
|
+
totalGaps++;
|
|
2039
|
+
}
|
|
2040
|
+
lines.push("");
|
|
2041
|
+
}
|
|
2042
|
+
if (unlinked.length > 0) {
|
|
2043
|
+
lines.push("## Scenarios Without Linked Tests", "");
|
|
2044
|
+
for (const s of unlinked) {
|
|
2045
|
+
lines.push(`- ${s.id}: ${s.name} [${s.category}] (${s.priority})`);
|
|
2046
|
+
totalGaps++;
|
|
2047
|
+
}
|
|
2048
|
+
lines.push("");
|
|
2049
|
+
}
|
|
2050
|
+
const blocks = db.prepare("SELECT id, name, code_paths, description FROM building_blocks").all();
|
|
2051
|
+
const emptyBlocks = blocks.filter((b) => {
|
|
2052
|
+
const paths = safeParseJson(b.code_paths, []);
|
|
2053
|
+
return paths.length === 0;
|
|
2054
|
+
});
|
|
2055
|
+
const undescribed = blocks.filter(
|
|
2056
|
+
(b) => !b.description || b.description.trim().length === 0
|
|
2057
|
+
);
|
|
2058
|
+
if (emptyBlocks.length > 0) {
|
|
2059
|
+
lines.push("## Building Blocks Without Code Paths", "");
|
|
2060
|
+
for (const b of emptyBlocks) {
|
|
2061
|
+
lines.push(`- **${b.name}** (\`${b.id}\`) \u2014 no code_paths defined`);
|
|
2062
|
+
totalGaps++;
|
|
2063
|
+
}
|
|
2064
|
+
lines.push("");
|
|
2065
|
+
}
|
|
2066
|
+
if (undescribed.length > 0) {
|
|
2067
|
+
lines.push("## Building Blocks Without Descriptions", "");
|
|
2068
|
+
for (const b of undescribed) {
|
|
2069
|
+
lines.push(`- **${b.name}** (\`${b.id}\`)`);
|
|
2070
|
+
totalGaps++;
|
|
2071
|
+
}
|
|
2072
|
+
lines.push("");
|
|
2073
|
+
}
|
|
2074
|
+
const drift = db.prepare(
|
|
2075
|
+
"SELECT kind, severity, description FROM drift_log WHERE resolution IS NULL ORDER BY severity DESC"
|
|
2076
|
+
).all();
|
|
2077
|
+
if (drift.length > 0) {
|
|
2078
|
+
lines.push("## Unresolved Architecture Drift", "");
|
|
2079
|
+
for (const d of drift) {
|
|
2080
|
+
lines.push(`- [${d.severity.toUpperCase()}] ${d.description}`);
|
|
2081
|
+
totalGaps++;
|
|
2082
|
+
}
|
|
2083
|
+
lines.push("");
|
|
2084
|
+
}
|
|
2085
|
+
let phaseTasks = [];
|
|
2086
|
+
if (params.scope === "current-phase" || !params.scope) {
|
|
2087
|
+
const currentPhase = db.prepare("SELECT id, name, status FROM phases WHERE status = 'in-progress' LIMIT 1").get();
|
|
2088
|
+
if (currentPhase) {
|
|
2089
|
+
phaseTasks = db.prepare("SELECT id, title, status, acceptance_criteria FROM tasks WHERE phase_id = ?").all(currentPhase.id);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const tasksWithoutCriteria = phaseTasks.filter((t) => {
|
|
2093
|
+
const criteria = safeParseJson(t.acceptance_criteria, []);
|
|
2094
|
+
return criteria.length === 0 && t.status !== "done";
|
|
2095
|
+
});
|
|
2096
|
+
if (tasksWithoutCriteria.length > 0) {
|
|
2097
|
+
lines.push("## Tasks Without Acceptance Criteria", "");
|
|
2098
|
+
for (const t of tasksWithoutCriteria) {
|
|
2099
|
+
lines.push(`- ${t.id}: ${t.title} (${t.status})`);
|
|
2100
|
+
totalGaps++;
|
|
2101
|
+
}
|
|
2102
|
+
lines.push("");
|
|
2103
|
+
}
|
|
2104
|
+
if (totalGaps === 0) {
|
|
2105
|
+
return textResult(
|
|
2106
|
+
"# Open Questions & Gaps\n\nNo significant architectural gaps found. Quality scenarios are linked, building blocks are defined, and no drift is unresolved."
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
lines[0] = `# Open Questions & Gaps (${totalGaps} items)`;
|
|
2110
|
+
return textResult(lines.join("\n"));
|
|
2111
|
+
}
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/tools/propose-arc42-update.ts
|
|
2116
|
+
import { z as z21 } from "zod";
|
|
2117
|
+
import {
|
|
2118
|
+
resolveRef,
|
|
2119
|
+
getChangedFiles,
|
|
2120
|
+
getHeadSha,
|
|
2121
|
+
setSyncCommit
|
|
2122
|
+
} from "@arcbridge/core";
|
|
2123
|
+
function registerProposeArc42Update(server, ctx) {
|
|
2124
|
+
server.tool(
|
|
2125
|
+
"arcbridge_propose_arc42_update",
|
|
2126
|
+
"Analyze code changes since a reference point and generate specific, actionable proposals for updating arc42 documentation.",
|
|
2127
|
+
{
|
|
2128
|
+
target_dir: z21.string().describe("Absolute path to the project directory"),
|
|
2129
|
+
changes_since: z21.string().default("last-sync").describe("Reference point: 'last-commit', 'last-sync', 'last-phase', or a git ref"),
|
|
2130
|
+
update_sync_point: z21.boolean().default(false).describe("Update the stored sync commit to HEAD after generating proposals")
|
|
2131
|
+
},
|
|
2132
|
+
async (params) => {
|
|
2133
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
2134
|
+
if (!db) return notInitialized();
|
|
2135
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2136
|
+
const ref = resolveRef(projectRoot, params.changes_since, db);
|
|
2137
|
+
const changedFiles = getChangedFiles(projectRoot, ref.sha);
|
|
2138
|
+
if (changedFiles.length === 0) {
|
|
2139
|
+
return textResult(
|
|
2140
|
+
`# Arc42 Update Proposals
|
|
2141
|
+
|
|
2142
|
+
No code changes detected since ${ref.label}. Documentation is up to date.`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
const blocks = db.prepare("SELECT id, name, code_paths, interfaces, description FROM building_blocks").all();
|
|
2146
|
+
const proposals = generateProposals(db, blocks, changedFiles, projectRoot);
|
|
2147
|
+
const lines = [
|
|
2148
|
+
`# Arc42 Update Proposals`,
|
|
2149
|
+
"",
|
|
2150
|
+
`**Changes since:** ${ref.label}`,
|
|
2151
|
+
`**Files changed:** ${changedFiles.length}`,
|
|
2152
|
+
`**Proposals:** ${proposals.length}`,
|
|
2153
|
+
""
|
|
2154
|
+
];
|
|
2155
|
+
if (proposals.length === 0) {
|
|
2156
|
+
lines.push(
|
|
2157
|
+
"No documentation updates needed \u2014 all changes are within documented building blocks and don't introduce new patterns."
|
|
2158
|
+
);
|
|
2159
|
+
} else {
|
|
2160
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
2161
|
+
for (const p of proposals) {
|
|
2162
|
+
const existing = bySection.get(p.section) ?? [];
|
|
2163
|
+
existing.push(p);
|
|
2164
|
+
bySection.set(p.section, existing);
|
|
2165
|
+
}
|
|
2166
|
+
for (const [section, items] of bySection) {
|
|
2167
|
+
lines.push(`## ${section}`, "");
|
|
2168
|
+
for (const item of items) {
|
|
2169
|
+
lines.push(`### ${item.title}`, "");
|
|
2170
|
+
lines.push(item.description, "");
|
|
2171
|
+
if (item.suggestedChange) {
|
|
2172
|
+
lines.push("**Suggested change:**", "", item.suggestedChange, "");
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (params.update_sync_point) {
|
|
2178
|
+
const headSha = getHeadSha(projectRoot);
|
|
2179
|
+
if (headSha) {
|
|
2180
|
+
setSyncCommit(db, "last_sync_commit", headSha);
|
|
2181
|
+
lines.push("---", `*Sync point updated to ${headSha.slice(0, 7)}.*`, "");
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return textResult(lines.join("\n"));
|
|
2185
|
+
}
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
function generateProposals(db, blocks, changedFiles, _projectRoot) {
|
|
2189
|
+
const proposals = [];
|
|
2190
|
+
const fileToBlock = /* @__PURE__ */ new Map();
|
|
2191
|
+
const unmappedFiles = [];
|
|
2192
|
+
for (const cf of changedFiles) {
|
|
2193
|
+
if (cf.status === "deleted") continue;
|
|
2194
|
+
let matched = false;
|
|
2195
|
+
for (const block of blocks) {
|
|
2196
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
2197
|
+
for (const cp of paths) {
|
|
2198
|
+
const prefix = normalizeCodePath(cp);
|
|
2199
|
+
if (cf.path.startsWith(prefix) || cf.path === prefix) {
|
|
2200
|
+
fileToBlock.set(cf.path, block);
|
|
2201
|
+
matched = true;
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (matched) break;
|
|
2206
|
+
}
|
|
2207
|
+
if (!matched) {
|
|
2208
|
+
unmappedFiles.push(cf);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (unmappedFiles.length > 0) {
|
|
2212
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
2213
|
+
for (const f of unmappedFiles) {
|
|
2214
|
+
const dir = f.path.replace(/\/[^/]+$/, "/");
|
|
2215
|
+
const existing = byDir.get(dir) ?? [];
|
|
2216
|
+
existing.push(f);
|
|
2217
|
+
byDir.set(dir, existing);
|
|
2218
|
+
}
|
|
2219
|
+
for (const [dir, files] of byDir) {
|
|
2220
|
+
const fileList = files.map((f) => `\`${f.path}\``).join(", ");
|
|
2221
|
+
proposals.push({
|
|
2222
|
+
section: "05 Building Block View",
|
|
2223
|
+
title: `Unmapped files in \`${dir}\``,
|
|
2224
|
+
description: `${files.length} file(s) in \`${dir}\` are not covered by any building block: ${fileList}`,
|
|
2225
|
+
suggestedChange: `Add \`${dir}\` to an existing building block's \`code_paths\`, or create a new building block for this directory.`
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const addedOrModified = changedFiles.filter(
|
|
2230
|
+
(f) => f.status === "added" || f.status === "modified"
|
|
2231
|
+
);
|
|
2232
|
+
for (const cf of addedOrModified) {
|
|
2233
|
+
const block = fileToBlock.get(cf.path);
|
|
2234
|
+
if (!block) continue;
|
|
2235
|
+
const symbols = db.prepare(
|
|
2236
|
+
"SELECT name, kind, is_exported FROM symbols WHERE file_path = ? AND is_exported = 1"
|
|
2237
|
+
).all(cf.path);
|
|
2238
|
+
if (symbols.length === 0) continue;
|
|
2239
|
+
for (const sym of symbols) {
|
|
2240
|
+
const consumers = findCrossBlockConsumers(
|
|
2241
|
+
db,
|
|
2242
|
+
`${cf.path}::${sym.name}#${sym.kind}`,
|
|
2243
|
+
block.id,
|
|
2244
|
+
blocks
|
|
2245
|
+
);
|
|
2246
|
+
if (consumers.length > 0) {
|
|
2247
|
+
const consumerNames = consumers.map((c) => `\`${c}\``).join(", ");
|
|
2248
|
+
proposals.push({
|
|
2249
|
+
section: "05 Building Block View",
|
|
2250
|
+
title: `New cross-block interface: \`${sym.name}\``,
|
|
2251
|
+
description: `Exported ${sym.kind} \`${sym.name}\` in block \`${block.name}\` is consumed by blocks: ${consumerNames}. This should be documented as an interface.`,
|
|
2252
|
+
suggestedChange: `Add \`${sym.name}\` to the interfaces section of building block \`${block.id}\` in \`.arcbridge/arc42/05-building-blocks.md\`.`
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
const deletedFiles = changedFiles.filter((f) => f.status === "deleted");
|
|
2258
|
+
for (const cf of deletedFiles) {
|
|
2259
|
+
for (const block of blocks) {
|
|
2260
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
2261
|
+
for (const cp of paths) {
|
|
2262
|
+
const prefix = normalizeCodePath(cp);
|
|
2263
|
+
if (cf.path.startsWith(prefix)) {
|
|
2264
|
+
const remaining = db.prepare("SELECT 1 FROM symbols WHERE file_path LIKE ? LIMIT 1").get(`${prefix}%`);
|
|
2265
|
+
if (!remaining) {
|
|
2266
|
+
proposals.push({
|
|
2267
|
+
section: "05 Building Block View",
|
|
2268
|
+
title: `Empty code path in \`${block.name}\``,
|
|
2269
|
+
description: `All files under \`${cp}\` in block \`${block.name}\` (${block.id}) have been deleted. The code_path no longer matches any code.`,
|
|
2270
|
+
suggestedChange: `Remove \`${cp}\` from building block \`${block.id}\`, or update it to reflect the new file structure.`
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const routeFiles = addedOrModified.filter(
|
|
2279
|
+
(f) => f.path.includes("/app/") && /\/(page|layout|route)\.(ts|tsx|js|jsx)$/.test(f.path)
|
|
2280
|
+
);
|
|
2281
|
+
if (routeFiles.length > 0) {
|
|
2282
|
+
const routeList = routeFiles.map((f) => `\`${f.path}\``).join(", ");
|
|
2283
|
+
proposals.push({
|
|
2284
|
+
section: "06 Runtime View",
|
|
2285
|
+
title: "New route files detected",
|
|
2286
|
+
description: `${routeFiles.length} route file(s) were added or modified: ${routeList}. Consider updating the runtime view to reflect new user flows.`,
|
|
2287
|
+
suggestedChange: null
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
return proposals;
|
|
2291
|
+
}
|
|
2292
|
+
function findCrossBlockConsumers(db, symbolId, sourceBlockId, blocks) {
|
|
2293
|
+
const consumers = db.prepare(
|
|
2294
|
+
`SELECT DISTINCT s.file_path
|
|
2295
|
+
FROM dependencies d
|
|
2296
|
+
JOIN symbols s ON d.source_symbol = s.id
|
|
2297
|
+
WHERE d.target_symbol = ?
|
|
2298
|
+
AND d.kind IN ('imports', 'calls', 'renders')`
|
|
2299
|
+
).all(symbolId);
|
|
2300
|
+
const consumerBlocks = /* @__PURE__ */ new Set();
|
|
2301
|
+
for (const { file_path } of consumers) {
|
|
2302
|
+
for (const block of blocks) {
|
|
2303
|
+
if (block.id === sourceBlockId) continue;
|
|
2304
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
2305
|
+
for (const cp of paths) {
|
|
2306
|
+
const prefix = normalizeCodePath(cp);
|
|
2307
|
+
if (file_path.startsWith(prefix)) {
|
|
2308
|
+
consumerBlocks.add(block.name);
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return [...consumerBlocks];
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/tools/get-practice-review.ts
|
|
2318
|
+
import { z as z22 } from "zod";
|
|
2319
|
+
import {
|
|
2320
|
+
resolveRef as resolveRef2,
|
|
2321
|
+
getChangedFiles as getChangedFiles2,
|
|
2322
|
+
detectDrift as detectDrift2
|
|
2323
|
+
} from "@arcbridge/core";
|
|
2324
|
+
function registerGetPracticeReview(server, ctx) {
|
|
2325
|
+
server.tool(
|
|
2326
|
+
"arcbridge_get_practice_review",
|
|
2327
|
+
"Structured, practice-aware review of recent code changes across 5 dimensions: Architecture, Security, Testing, Documentation, and Complexity.",
|
|
2328
|
+
{
|
|
2329
|
+
target_dir: z22.string().describe("Absolute path to the project directory"),
|
|
2330
|
+
since: z22.string().default("last-commit").describe("Reference point: 'last-commit', 'last-session', or 'last-phase'")
|
|
2331
|
+
},
|
|
2332
|
+
async (params) => {
|
|
2333
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
2334
|
+
if (!db) return notInitialized();
|
|
2335
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2336
|
+
const ref = resolveRef2(projectRoot, params.since, db);
|
|
2337
|
+
const changedFiles = getChangedFiles2(projectRoot, ref.sha);
|
|
2338
|
+
if (changedFiles.length === 0) {
|
|
2339
|
+
return textResult(
|
|
2340
|
+
`# Practice Review
|
|
2341
|
+
|
|
2342
|
+
No changes detected since ${ref.label}. Nothing to review.`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
const lines = [
|
|
2346
|
+
"# Practice Review",
|
|
2347
|
+
"",
|
|
2348
|
+
`**Since:** ${ref.label}`,
|
|
2349
|
+
`**Files changed:** ${changedFiles.length}`,
|
|
2350
|
+
""
|
|
2351
|
+
];
|
|
2352
|
+
const findings = [];
|
|
2353
|
+
reviewArchitecture(db, changedFiles, findings);
|
|
2354
|
+
reviewSecurity(db, changedFiles, findings);
|
|
2355
|
+
reviewTesting(db, changedFiles, findings);
|
|
2356
|
+
reviewDocumentation(db, changedFiles, findings);
|
|
2357
|
+
reviewComplexity(db, changedFiles, findings);
|
|
2358
|
+
if (findings.length === 0) {
|
|
2359
|
+
lines.push("All checks passed. No issues found across the 5 practice dimensions.");
|
|
2360
|
+
return textResult(lines.join("\n"));
|
|
2361
|
+
}
|
|
2362
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
2363
|
+
for (const f of findings) {
|
|
2364
|
+
const existing = byDimension.get(f.dimension) ?? [];
|
|
2365
|
+
existing.push(f);
|
|
2366
|
+
byDimension.set(f.dimension, existing);
|
|
2367
|
+
}
|
|
2368
|
+
const dimensionIcons = {
|
|
2369
|
+
Architecture: "1",
|
|
2370
|
+
Security: "2",
|
|
2371
|
+
Testing: "3",
|
|
2372
|
+
Documentation: "4",
|
|
2373
|
+
Complexity: "5"
|
|
2374
|
+
};
|
|
2375
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
2376
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
2377
|
+
const infos = findings.filter((f) => f.severity === "info").length;
|
|
2378
|
+
lines.push(
|
|
2379
|
+
`**${errors}** errors, **${warnings}** warnings, **${infos}** info`,
|
|
2380
|
+
""
|
|
2381
|
+
);
|
|
2382
|
+
for (const dim of ["Architecture", "Security", "Testing", "Documentation", "Complexity"]) {
|
|
2383
|
+
const items = byDimension.get(dim);
|
|
2384
|
+
const num = dimensionIcons[dim] ?? "?";
|
|
2385
|
+
if (!items || items.length === 0) {
|
|
2386
|
+
lines.push(`## ${num}. ${dim} \u2713`, "", `No issues found.`, "");
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
lines.push(`## ${num}. ${dim} (${items.length} findings)`, "");
|
|
2390
|
+
for (const item of items) {
|
|
2391
|
+
const icon = item.severity === "error" ? "ERROR" : item.severity === "warning" ? "WARN" : "INFO";
|
|
2392
|
+
lines.push(`- [${icon}] ${item.description}`);
|
|
2393
|
+
if (item.action) {
|
|
2394
|
+
lines.push(` \u2192 **Action:** ${item.action}`);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
lines.push("");
|
|
2398
|
+
}
|
|
2399
|
+
return textResult(lines.join("\n"));
|
|
2400
|
+
}
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
function reviewArchitecture(db, changedFiles, findings) {
|
|
2404
|
+
const blocks = db.prepare("SELECT id, name, code_paths, interfaces FROM building_blocks").all();
|
|
2405
|
+
if (blocks.length === 0) return;
|
|
2406
|
+
const changedPaths = new Set(
|
|
2407
|
+
changedFiles.filter((f) => f.status !== "deleted").map((f) => f.path)
|
|
2408
|
+
);
|
|
2409
|
+
const changedByBlock = /* @__PURE__ */ new Map();
|
|
2410
|
+
const unmapped = [];
|
|
2411
|
+
for (const path of changedPaths) {
|
|
2412
|
+
let matched = false;
|
|
2413
|
+
for (const block of blocks) {
|
|
2414
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
2415
|
+
for (const cp of paths) {
|
|
2416
|
+
const prefix = normalizeCodePath(cp);
|
|
2417
|
+
if (path.startsWith(prefix)) {
|
|
2418
|
+
const existing = changedByBlock.get(block.id) ?? [];
|
|
2419
|
+
existing.push(path);
|
|
2420
|
+
changedByBlock.set(block.id, existing);
|
|
2421
|
+
matched = true;
|
|
2422
|
+
break;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (matched) break;
|
|
2426
|
+
}
|
|
2427
|
+
if (!matched) unmapped.push(path);
|
|
2428
|
+
}
|
|
2429
|
+
if (unmapped.length > 0) {
|
|
2430
|
+
findings.push({
|
|
2431
|
+
dimension: "Architecture",
|
|
2432
|
+
severity: "warning",
|
|
2433
|
+
description: `${unmapped.length} changed file(s) not mapped to any building block: ${unmapped.slice(0, 3).map((f) => `\`${f}\``).join(", ")}${unmapped.length > 3 ? ` and ${unmapped.length - 3} more` : ""}`,
|
|
2434
|
+
action: "Map these files to building blocks in `.arcbridge/arc42/05-building-blocks.md`"
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
for (const [blockId, files] of changedByBlock) {
|
|
2438
|
+
const block = blocks.find((b) => b.id === blockId);
|
|
2439
|
+
if (!block) continue;
|
|
2440
|
+
const declaredInterfaces = new Set(safeParseJson(block.interfaces, []));
|
|
2441
|
+
for (const filePath of files) {
|
|
2442
|
+
const outgoing = db.prepare(
|
|
2443
|
+
`SELECT DISTINCT st.file_path as target_file
|
|
2444
|
+
FROM dependencies d
|
|
2445
|
+
JOIN symbols ss ON d.source_symbol = ss.id
|
|
2446
|
+
JOIN symbols st ON d.target_symbol = st.id
|
|
2447
|
+
WHERE ss.file_path = ?
|
|
2448
|
+
AND d.kind IN ('imports', 'calls', 'renders')`
|
|
2449
|
+
).all(filePath);
|
|
2450
|
+
for (const { target_file } of outgoing) {
|
|
2451
|
+
for (const targetBlock of blocks) {
|
|
2452
|
+
if (targetBlock.id === blockId) continue;
|
|
2453
|
+
const tPaths = safeParseJson(targetBlock.code_paths, []);
|
|
2454
|
+
for (const cp of tPaths) {
|
|
2455
|
+
const prefix = normalizeCodePath(cp);
|
|
2456
|
+
if (target_file.startsWith(prefix) && !declaredInterfaces.has(targetBlock.id)) {
|
|
2457
|
+
findings.push({
|
|
2458
|
+
dimension: "Architecture",
|
|
2459
|
+
severity: "error",
|
|
2460
|
+
description: `\`${filePath}\` in block \`${block.name}\` depends on block \`${targetBlock.name}\` without declaring it as an interface.`,
|
|
2461
|
+
action: `Add \`${targetBlock.id}\` to the interfaces of block \`${blockId}\``
|
|
2462
|
+
});
|
|
2463
|
+
break;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
function reviewSecurity(db, changedFiles, findings) {
|
|
2472
|
+
const changedPaths = changedFiles.filter((f) => f.status !== "deleted").map((f) => f.path);
|
|
2473
|
+
const routeFiles = changedPaths.filter(
|
|
2474
|
+
(p) => /\/(route)\.(ts|tsx|js|jsx)$/.test(p)
|
|
2475
|
+
);
|
|
2476
|
+
for (const routePath of routeFiles) {
|
|
2477
|
+
const appMatch = routePath.match(/app\/(.+)\/route\./);
|
|
2478
|
+
if (!appMatch) continue;
|
|
2479
|
+
const urlPath = "/" + appMatch[1].replace(/\([^)]+\)\//g, "").replace(/\[\.{3}(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
|
|
2480
|
+
const route = db.prepare("SELECT route_path, has_auth FROM routes WHERE route_path = ? AND kind = 'api-route'").get(urlPath);
|
|
2481
|
+
if (route && !route.has_auth) {
|
|
2482
|
+
findings.push({
|
|
2483
|
+
dimension: "Security",
|
|
2484
|
+
severity: "warning",
|
|
2485
|
+
description: `API route \`${route.route_path}\` does not have auth middleware detected.`,
|
|
2486
|
+
action: "Verify this route has authentication. Add auth middleware or mark as intentionally public."
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const sensitivePatterns = [/\.env/, /secrets?\./, /credentials/, /\.pem$/, /\.key$/];
|
|
2491
|
+
for (const path of changedPaths) {
|
|
2492
|
+
if (sensitivePatterns.some((p) => p.test(path))) {
|
|
2493
|
+
findings.push({
|
|
2494
|
+
dimension: "Security",
|
|
2495
|
+
severity: "error",
|
|
2496
|
+
description: `Potentially sensitive file changed: \`${path}\``,
|
|
2497
|
+
action: "Ensure this file is in .gitignore and not committed to version control."
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
const clientFiles = changedPaths.filter((p) => p.endsWith(".tsx") || p.endsWith(".ts"));
|
|
2502
|
+
for (const filePath of clientFiles) {
|
|
2503
|
+
const comp = db.prepare(
|
|
2504
|
+
`SELECT s.name, c.is_client
|
|
2505
|
+
FROM components c
|
|
2506
|
+
JOIN symbols s ON c.symbol_id = s.id
|
|
2507
|
+
WHERE s.file_path = ? AND c.is_client = 1`
|
|
2508
|
+
).all(filePath);
|
|
2509
|
+
if (comp.length === 0) continue;
|
|
2510
|
+
const serverImports = db.prepare(
|
|
2511
|
+
`SELECT DISTINCT st.file_path
|
|
2512
|
+
FROM dependencies d
|
|
2513
|
+
JOIN symbols ss ON d.source_symbol = ss.id
|
|
2514
|
+
JOIN symbols st ON d.target_symbol = st.id
|
|
2515
|
+
JOIN components ct ON d.target_symbol = ct.symbol_id
|
|
2516
|
+
WHERE ss.file_path = ?
|
|
2517
|
+
AND ct.is_server_action = 1
|
|
2518
|
+
AND d.kind = 'imports'`
|
|
2519
|
+
).all(filePath);
|
|
2520
|
+
for (const imp of serverImports) {
|
|
2521
|
+
findings.push({
|
|
2522
|
+
dimension: "Security",
|
|
2523
|
+
severity: "warning",
|
|
2524
|
+
description: `Client component in \`${filePath}\` imports from server action file \`${imp.file_path}\`. Verify no server secrets are exposed.`,
|
|
2525
|
+
action: null
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
function reviewTesting(db, changedFiles, findings) {
|
|
2531
|
+
const changedPaths = changedFiles.filter((f) => f.status !== "deleted").map((f) => f.path);
|
|
2532
|
+
const testFilePattern = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
|
|
2533
|
+
const codeFiles = changedPaths.filter((p) => !testFilePattern.test(p) && /\.(ts|tsx|js|jsx)$/.test(p));
|
|
2534
|
+
const testFiles = changedPaths.filter((p) => testFilePattern.test(p));
|
|
2535
|
+
if (codeFiles.length > 0 && testFiles.length === 0) {
|
|
2536
|
+
findings.push({
|
|
2537
|
+
dimension: "Testing",
|
|
2538
|
+
severity: "info",
|
|
2539
|
+
description: `${codeFiles.length} code file(s) changed but no test files were modified.`,
|
|
2540
|
+
action: "Consider whether existing tests still cover the changed behavior."
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
const scenarios = db.prepare(
|
|
2544
|
+
"SELECT id, name, linked_code, linked_blocks, status FROM quality_scenarios WHERE priority IN ('must', 'should')"
|
|
2545
|
+
).all();
|
|
2546
|
+
for (const scenario of scenarios) {
|
|
2547
|
+
const linkedCode = safeParseJson(scenario.linked_code, []);
|
|
2548
|
+
const linkedBlocks = safeParseJson(scenario.linked_blocks, []);
|
|
2549
|
+
const affectedByCode = linkedCode.some(
|
|
2550
|
+
(lc) => changedPaths.some((cp) => cp.startsWith(lc) || cp === lc)
|
|
2551
|
+
);
|
|
2552
|
+
let affectedByBlock = false;
|
|
2553
|
+
if (linkedBlocks.length > 0) {
|
|
2554
|
+
const blocks = db.prepare("SELECT id, code_paths FROM building_blocks").all();
|
|
2555
|
+
for (const blockId of linkedBlocks) {
|
|
2556
|
+
const block = blocks.find((b) => b.id === blockId);
|
|
2557
|
+
if (!block) continue;
|
|
2558
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
2559
|
+
for (const cp of paths) {
|
|
2560
|
+
const prefix = normalizeCodePath(cp);
|
|
2561
|
+
if (changedPaths.some((p) => p.startsWith(prefix))) {
|
|
2562
|
+
affectedByBlock = true;
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (affectedByBlock) break;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
if (affectedByCode || affectedByBlock) {
|
|
2570
|
+
const severity = scenario.status === "passing" ? "warning" : "error";
|
|
2571
|
+
findings.push({
|
|
2572
|
+
dimension: "Testing",
|
|
2573
|
+
severity,
|
|
2574
|
+
description: `Quality scenario \`${scenario.id}: ${scenario.name}\` may be affected by these changes (status: ${scenario.status}).`,
|
|
2575
|
+
action: "Re-run tests for this quality scenario to verify it still passes."
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function reviewDocumentation(db, changedFiles, findings) {
|
|
2581
|
+
const driftEntries = detectDrift2(db);
|
|
2582
|
+
if (driftEntries.length > 0) {
|
|
2583
|
+
const errors = driftEntries.filter((d) => d.severity === "error");
|
|
2584
|
+
const warnings = driftEntries.filter((d) => d.severity === "warning");
|
|
2585
|
+
if (errors.length > 0) {
|
|
2586
|
+
findings.push({
|
|
2587
|
+
dimension: "Documentation",
|
|
2588
|
+
severity: "error",
|
|
2589
|
+
description: `${errors.length} architecture drift error(s) detected (dependency violations, etc.).`,
|
|
2590
|
+
action: "Run `arcbridge_check_drift` for details and resolve violations."
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
if (warnings.length > 0) {
|
|
2594
|
+
findings.push({
|
|
2595
|
+
dimension: "Documentation",
|
|
2596
|
+
severity: "warning",
|
|
2597
|
+
description: `${warnings.length} documentation gap(s) found (undocumented modules, missing code paths, etc.).`,
|
|
2598
|
+
action: "Run `arcbridge_propose_arc42_update` to generate update proposals."
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const arc42Changes = changedFiles.filter(
|
|
2603
|
+
(f) => f.path.includes(".arcbridge/arc42/")
|
|
2604
|
+
);
|
|
2605
|
+
if (arc42Changes.length > 0) {
|
|
2606
|
+
findings.push({
|
|
2607
|
+
dimension: "Documentation",
|
|
2608
|
+
severity: "info",
|
|
2609
|
+
description: `${arc42Changes.length} arc42 documentation file(s) were updated.`,
|
|
2610
|
+
action: "Run `arcbridge_reindex` to ensure the database reflects documentation changes."
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
function reviewComplexity(db, changedFiles, findings) {
|
|
2615
|
+
const changedPaths = changedFiles.filter((f) => f.status !== "deleted").map((f) => f.path);
|
|
2616
|
+
for (const filePath of changedPaths) {
|
|
2617
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(filePath)) continue;
|
|
2618
|
+
const symbolCount = db.prepare("SELECT COUNT(*) as count FROM symbols WHERE file_path = ?").get(filePath).count;
|
|
2619
|
+
if (symbolCount > 30) {
|
|
2620
|
+
findings.push({
|
|
2621
|
+
dimension: "Complexity",
|
|
2622
|
+
severity: "warning",
|
|
2623
|
+
description: `\`${filePath}\` has ${symbolCount} symbols \u2014 consider splitting into smaller modules.`,
|
|
2624
|
+
action: null
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
const depCount = db.prepare(
|
|
2628
|
+
`SELECT COUNT(DISTINCT d.target_symbol) as count
|
|
2629
|
+
FROM dependencies d
|
|
2630
|
+
JOIN symbols s ON d.source_symbol = s.id
|
|
2631
|
+
WHERE s.file_path = ?`
|
|
2632
|
+
).get(filePath).count;
|
|
2633
|
+
if (depCount > 20) {
|
|
2634
|
+
findings.push({
|
|
2635
|
+
dimension: "Complexity",
|
|
2636
|
+
severity: "warning",
|
|
2637
|
+
description: `\`${filePath}\` has ${depCount} outgoing dependencies \u2014 high coupling detected.`,
|
|
2638
|
+
action: "Consider reducing dependencies or introducing an abstraction layer."
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// src/tools/complete-phase.ts
|
|
2645
|
+
import { z as z23 } from "zod";
|
|
2646
|
+
import {
|
|
2647
|
+
detectDrift as detectDrift3,
|
|
2648
|
+
writeDriftLog as writeDriftLog2,
|
|
2649
|
+
getHeadSha as getHeadSha2,
|
|
2650
|
+
setSyncCommit as setSyncCommit2,
|
|
2651
|
+
inferTaskStatuses,
|
|
2652
|
+
applyInferences,
|
|
2653
|
+
verifyScenarios,
|
|
2654
|
+
loadConfig,
|
|
2655
|
+
refreshFromDocs as refreshFromDocs5,
|
|
2656
|
+
syncPhaseToYaml
|
|
2657
|
+
} from "@arcbridge/core";
|
|
2658
|
+
function registerCompletePhase(server, ctx) {
|
|
2659
|
+
server.tool(
|
|
2660
|
+
"arcbridge_complete_phase",
|
|
2661
|
+
"Attempt to complete a phase by validating all gates: tasks done, no critical drift, quality scenarios passing. Transitions the phase to 'complete' if all gates pass.",
|
|
2662
|
+
{
|
|
2663
|
+
target_dir: z23.string().describe("Absolute path to the project directory"),
|
|
2664
|
+
phase_id: z23.string().optional().describe("Phase ID to complete (defaults to current in-progress phase)"),
|
|
2665
|
+
notes: z23.string().optional().describe("Optional notes about this phase completion"),
|
|
2666
|
+
auto_infer: z23.boolean().default(true).describe("Automatically infer task statuses from code state before checking gates"),
|
|
2667
|
+
run_tests: z23.boolean().default(false).describe("Run linked tests for quality scenarios before checking the quality gate")
|
|
2668
|
+
},
|
|
2669
|
+
async (params) => {
|
|
2670
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
2671
|
+
if (!db) return notInitialized();
|
|
2672
|
+
refreshFromDocs5(db, params.target_dir);
|
|
2673
|
+
let phase;
|
|
2674
|
+
if (params.phase_id) {
|
|
2675
|
+
phase = db.prepare("SELECT id, name, phase_number, status, gate_status FROM phases WHERE id = ?").get(params.phase_id);
|
|
2676
|
+
} else {
|
|
2677
|
+
phase = db.prepare(
|
|
2678
|
+
"SELECT id, name, phase_number, status, gate_status FROM phases WHERE status = 'in-progress' LIMIT 1"
|
|
2679
|
+
).get();
|
|
2680
|
+
}
|
|
2681
|
+
if (!phase) {
|
|
2682
|
+
return textResult(
|
|
2683
|
+
"No in-progress phase found. Use `arcbridge_get_phase_plan` to see all phases."
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
if (phase.status === "complete") {
|
|
2687
|
+
return textResult(`Phase \`${phase.name}\` is already complete.`);
|
|
2688
|
+
}
|
|
2689
|
+
const lines = [
|
|
2690
|
+
`# Phase Completion: ${phase.name}`,
|
|
2691
|
+
""
|
|
2692
|
+
];
|
|
2693
|
+
if (params.auto_infer) {
|
|
2694
|
+
const inferences = inferTaskStatuses(db, phase.id);
|
|
2695
|
+
if (inferences.length > 0) {
|
|
2696
|
+
applyInferences(db, inferences, ctx.projectRoot ?? params.target_dir);
|
|
2697
|
+
lines.push("## Task Status Inference", "");
|
|
2698
|
+
for (const inf of inferences) {
|
|
2699
|
+
lines.push(
|
|
2700
|
+
`- **${inf.taskId}**: ${inf.previousStatus} \u2192 **${inf.inferredStatus}** (${inf.reason})`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
lines.push("");
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const tasks = db.prepare("SELECT id, title, status FROM tasks WHERE phase_id = ?").all(phase.id);
|
|
2707
|
+
const incompleteTasks = tasks.filter((t) => t.status !== "done");
|
|
2708
|
+
const tasksPass = incompleteTasks.length === 0;
|
|
2709
|
+
const driftEntries = detectDrift3(db);
|
|
2710
|
+
writeDriftLog2(db, driftEntries);
|
|
2711
|
+
const criticalDrift = driftEntries.filter((d) => d.severity === "error");
|
|
2712
|
+
const driftPass = criticalDrift.length === 0;
|
|
2713
|
+
if (params.run_tests) {
|
|
2714
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2715
|
+
let testCommand = "npx vitest run";
|
|
2716
|
+
let timeoutMs = 6e4;
|
|
2717
|
+
const configResult = loadConfig(params.target_dir);
|
|
2718
|
+
if (configResult.config) {
|
|
2719
|
+
testCommand = configResult.config.testing.test_command;
|
|
2720
|
+
timeoutMs = configResult.config.testing.timeout_ms;
|
|
2721
|
+
}
|
|
2722
|
+
const verifyResult = verifyScenarios(db, projectRoot, {
|
|
2723
|
+
testCommand,
|
|
2724
|
+
timeoutMs
|
|
2725
|
+
});
|
|
2726
|
+
if (verifyResult.results.length > 0) {
|
|
2727
|
+
lines.push("## Test Verification", "");
|
|
2728
|
+
for (const r of verifyResult.results) {
|
|
2729
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
2730
|
+
lines.push(
|
|
2731
|
+
`- [${icon}] **${r.scenarioId}: ${r.scenarioName}** (${r.durationMs}ms)`
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
lines.push("");
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
const mustScenarios = db.prepare(
|
|
2738
|
+
"SELECT id, name, status, priority FROM quality_scenarios WHERE priority = 'must'"
|
|
2739
|
+
).all();
|
|
2740
|
+
const failingMust = mustScenarios.filter(
|
|
2741
|
+
(s) => s.status === "failing"
|
|
2742
|
+
);
|
|
2743
|
+
const qualityPass = failingMust.length === 0;
|
|
2744
|
+
const gates = [
|
|
2745
|
+
{ name: "All tasks complete", pass: tasksPass },
|
|
2746
|
+
{ name: "No critical drift", pass: driftPass },
|
|
2747
|
+
{ name: "Must-have quality scenarios not failing", pass: qualityPass }
|
|
2748
|
+
];
|
|
2749
|
+
const allPass = gates.every((g) => g.pass);
|
|
2750
|
+
lines.push("## Gate Results", "");
|
|
2751
|
+
for (const gate of gates) {
|
|
2752
|
+
const icon = gate.pass ? "PASS" : "FAIL";
|
|
2753
|
+
lines.push(`- [${icon}] ${gate.name}`);
|
|
2754
|
+
}
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
if (!tasksPass) {
|
|
2757
|
+
lines.push("### Incomplete Tasks", "");
|
|
2758
|
+
for (const t of incompleteTasks) {
|
|
2759
|
+
const icon = t.status === "in-progress" ? "[>]" : t.status === "blocked" ? "[!]" : "[ ]";
|
|
2760
|
+
lines.push(`- ${icon} ${t.id}: ${t.title} (${t.status})`);
|
|
2761
|
+
}
|
|
2762
|
+
lines.push("");
|
|
2763
|
+
}
|
|
2764
|
+
if (!driftPass) {
|
|
2765
|
+
lines.push("### Critical Drift", "");
|
|
2766
|
+
for (const d of criticalDrift) {
|
|
2767
|
+
lines.push(`- [ERROR] ${d.description}`);
|
|
2768
|
+
}
|
|
2769
|
+
lines.push("");
|
|
2770
|
+
}
|
|
2771
|
+
if (!qualityPass) {
|
|
2772
|
+
lines.push("### Failing Must-Have Scenarios", "");
|
|
2773
|
+
for (const s of failingMust) {
|
|
2774
|
+
lines.push(`- **${s.id}: ${s.name}** \u2014 ${s.status}`);
|
|
2775
|
+
}
|
|
2776
|
+
lines.push("");
|
|
2777
|
+
}
|
|
2778
|
+
const adrCount = db.prepare("SELECT COUNT(*) as count FROM adrs").get().count;
|
|
2779
|
+
const architecturalKeywords = ["auth", "database", "middleware", "validation", "caching", "dependency injection", "error handling"];
|
|
2780
|
+
const phaseTasks = tasks.map((t) => t.title.toLowerCase());
|
|
2781
|
+
const hasArchitecturalWork = phaseTasks.some(
|
|
2782
|
+
(title) => architecturalKeywords.some((kw) => title.includes(kw))
|
|
2783
|
+
);
|
|
2784
|
+
if (hasArchitecturalWork && adrCount <= 1) {
|
|
2785
|
+
lines.push(
|
|
2786
|
+
"## ADR Reminder",
|
|
2787
|
+
"",
|
|
2788
|
+
"This phase involved architectural decisions that should be documented as ADRs.",
|
|
2789
|
+
"Use `arcbridge_get_relevant_adrs` to review existing ADRs and create new ones in `.arcbridge/arc42/09-decisions/` for:",
|
|
2790
|
+
""
|
|
2791
|
+
);
|
|
2792
|
+
for (const t of tasks) {
|
|
2793
|
+
const lower = t.title.toLowerCase();
|
|
2794
|
+
if (architecturalKeywords.some((kw) => lower.includes(kw))) {
|
|
2795
|
+
lines.push(`- **${t.title}** \u2014 document the chosen approach and alternatives considered`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
lines.push("");
|
|
2799
|
+
}
|
|
2800
|
+
if (allPass) {
|
|
2801
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2802
|
+
const gateStatus = JSON.stringify({
|
|
2803
|
+
tasks: "pass",
|
|
2804
|
+
drift: "pass",
|
|
2805
|
+
quality: "pass",
|
|
2806
|
+
completed_at: now,
|
|
2807
|
+
notes: params.notes ?? null
|
|
2808
|
+
});
|
|
2809
|
+
const nextPhase = db.prepare(
|
|
2810
|
+
"SELECT id, name FROM phases WHERE phase_number = ? AND status = 'planned'"
|
|
2811
|
+
).get(phase.phase_number + 1);
|
|
2812
|
+
const transition = db.transaction(() => {
|
|
2813
|
+
db.prepare(
|
|
2814
|
+
"UPDATE phases SET status = 'complete', completed_at = ?, gate_status = ? WHERE id = ?"
|
|
2815
|
+
).run(now, gateStatus, phase.id);
|
|
2816
|
+
if (nextPhase) {
|
|
2817
|
+
db.prepare(
|
|
2818
|
+
"UPDATE phases SET status = 'in-progress', started_at = ? WHERE id = ?"
|
|
2819
|
+
).run(now, nextPhase.id);
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
transition();
|
|
2823
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2824
|
+
syncPhaseToYaml(projectRoot, phase.id, "complete", void 0, now);
|
|
2825
|
+
if (nextPhase) {
|
|
2826
|
+
syncPhaseToYaml(projectRoot, nextPhase.id, "in-progress", now);
|
|
2827
|
+
}
|
|
2828
|
+
const headSha = getHeadSha2(projectRoot);
|
|
2829
|
+
if (headSha) {
|
|
2830
|
+
setSyncCommit2(db, "phase_sync_commit", headSha);
|
|
2831
|
+
}
|
|
2832
|
+
lines.push("## Result: PASS", "");
|
|
2833
|
+
lines.push(`Phase \`${phase.name}\` is now **complete**.`);
|
|
2834
|
+
if (!headSha) {
|
|
2835
|
+
lines.push("", "*Warning: Could not determine git HEAD \u2014 sync point not stored.*");
|
|
2836
|
+
}
|
|
2837
|
+
if (params.notes) {
|
|
2838
|
+
lines.push("", `**Notes:** ${params.notes}`);
|
|
2839
|
+
}
|
|
2840
|
+
if (nextPhase) {
|
|
2841
|
+
lines.push("", `Next phase **${nextPhase.name}** is now in-progress.`);
|
|
2842
|
+
}
|
|
2843
|
+
lines.push(
|
|
2844
|
+
"",
|
|
2845
|
+
"---",
|
|
2846
|
+
"## Arc42 Documentation Review",
|
|
2847
|
+
"",
|
|
2848
|
+
"Before moving on, review and update these arc42 sections for changes made in this phase:",
|
|
2849
|
+
"",
|
|
2850
|
+
"- [ ] **01 Introduction** \u2014 Do project goals still reflect reality?",
|
|
2851
|
+
"- [ ] **03 Context** \u2014 Any new external systems or integrations added?",
|
|
2852
|
+
"- [ ] **05 Building Blocks** \u2014 Are all new modules mapped? Run `arcbridge_check_drift` to verify.",
|
|
2853
|
+
"- [ ] **06 Runtime Views** \u2014 Any new key workflows to document (auth flow, data processing, etc.)?",
|
|
2854
|
+
"- [ ] **07 Deployment** \u2014 Any changes to infrastructure, environments, or deployment strategy?",
|
|
2855
|
+
"- [ ] **08 Crosscutting Concepts** \u2014 Any new patterns established (error handling, validation, logging)?",
|
|
2856
|
+
"- [ ] **09 Decisions** \u2014 ADRs for all significant choices? Run `arcbridge_get_relevant_adrs` to check.",
|
|
2857
|
+
"- [ ] **10 Quality Scenarios** \u2014 Any new quality requirements or changed thresholds?",
|
|
2858
|
+
"- [ ] **11 Risks & Debt** \u2014 Any known limitations or tech debt introduced?",
|
|
2859
|
+
"",
|
|
2860
|
+
"*Run `arcbridge_propose_arc42_update` to auto-detect documentation gaps.*"
|
|
2861
|
+
);
|
|
2862
|
+
} else {
|
|
2863
|
+
const failCount = gates.filter((g) => !g.pass).length;
|
|
2864
|
+
lines.push(
|
|
2865
|
+
`## Result: BLOCKED (${failCount} gate${failCount > 1 ? "s" : ""} failed)`,
|
|
2866
|
+
"",
|
|
2867
|
+
"Resolve the issues above before completing this phase."
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
return textResult(lines.join("\n"));
|
|
2871
|
+
}
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// src/tools/activate-role.ts
|
|
2876
|
+
import { z as z24 } from "zod";
|
|
2877
|
+
import { loadRole, loadRoles } from "@arcbridge/core";
|
|
2878
|
+
function registerActivateRole(server, ctx) {
|
|
2879
|
+
server.tool(
|
|
2880
|
+
"arcbridge_activate_role",
|
|
2881
|
+
"Activate an agent role: loads the role's system prompt, required tools, quality focus, and pre-loaded architectural context.",
|
|
2882
|
+
{
|
|
2883
|
+
target_dir: z24.string().describe("Absolute path to the project directory"),
|
|
2884
|
+
role: z24.string().describe(
|
|
2885
|
+
"Role ID to activate (e.g., 'architect', 'implementer', 'security-reviewer')"
|
|
2886
|
+
),
|
|
2887
|
+
building_block: z24.string().optional().describe("Focus on a specific building block (for implementer/code-reviewer roles)")
|
|
2888
|
+
},
|
|
2889
|
+
async (params) => {
|
|
2890
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
2891
|
+
if (!db) return notInitialized();
|
|
2892
|
+
const lines = [];
|
|
2893
|
+
const fileResult = loadRole(params.target_dir, params.role);
|
|
2894
|
+
const role = fileResult.role ?? null;
|
|
2895
|
+
const roleDef = role ? {
|
|
2896
|
+
name: role.name,
|
|
2897
|
+
description: role.description,
|
|
2898
|
+
requiredTools: role.required_tools,
|
|
2899
|
+
deniedTools: role.denied_tools,
|
|
2900
|
+
readOnly: role.read_only,
|
|
2901
|
+
qualityFocus: role.quality_focus,
|
|
2902
|
+
systemPrompt: role.system_prompt,
|
|
2903
|
+
modelPreferences: role.model_preferences
|
|
2904
|
+
} : getRoleDefinition(params.role);
|
|
2905
|
+
if (!roleDef) {
|
|
2906
|
+
const builtInIds = ["architect", "implementer", "security-reviewer", "quality-guardian", "phase-manager", "onboarding", "code-reviewer"];
|
|
2907
|
+
const fileRoles = loadRoles(params.target_dir);
|
|
2908
|
+
const fileIds = fileRoles.roles.map((r) => r.role_id);
|
|
2909
|
+
const availableIds = [.../* @__PURE__ */ new Set([...fileIds, ...builtInIds])].sort();
|
|
2910
|
+
return textResult(
|
|
2911
|
+
`Unknown role: \`${params.role}\`. Available roles: ${availableIds.map((r) => `\`${r}\``).join(", ")}`
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
const source = role ? "file" : "built-in";
|
|
2915
|
+
lines.push(
|
|
2916
|
+
`# Role Activated: ${roleDef.name}`,
|
|
2917
|
+
"",
|
|
2918
|
+
roleDef.description,
|
|
2919
|
+
""
|
|
2920
|
+
);
|
|
2921
|
+
if (source === "file") {
|
|
2922
|
+
lines.push(`*Loaded from .arcbridge/agents/${params.role}.md*`, "");
|
|
2923
|
+
}
|
|
2924
|
+
lines.push("## Required Tools", "");
|
|
2925
|
+
for (const tool of roleDef.requiredTools) {
|
|
2926
|
+
lines.push(`- \`${tool}\``);
|
|
2927
|
+
}
|
|
2928
|
+
lines.push("");
|
|
2929
|
+
if (roleDef.deniedTools.length > 0) {
|
|
2930
|
+
lines.push("## Denied Tools", "");
|
|
2931
|
+
for (const tool of roleDef.deniedTools) {
|
|
2932
|
+
lines.push(`- \`${tool}\``);
|
|
2933
|
+
}
|
|
2934
|
+
lines.push("");
|
|
2935
|
+
}
|
|
2936
|
+
if (roleDef.readOnly) {
|
|
2937
|
+
lines.push("**Access:** Read-only", "");
|
|
2938
|
+
}
|
|
2939
|
+
if (roleDef.qualityFocus.length > 0) {
|
|
2940
|
+
lines.push(
|
|
2941
|
+
"## Quality Focus",
|
|
2942
|
+
"",
|
|
2943
|
+
roleDef.qualityFocus.map((q) => `- ${q}`).join("\n"),
|
|
2944
|
+
""
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
if (roleDef.modelPreferences) {
|
|
2948
|
+
const mp = roleDef.modelPreferences;
|
|
2949
|
+
lines.push("## Model Preferences", "");
|
|
2950
|
+
lines.push(`- **Reasoning depth:** ${mp.reasoning_depth}`);
|
|
2951
|
+
lines.push(`- **Speed priority:** ${mp.speed_priority}`);
|
|
2952
|
+
if (mp.suggested_models) {
|
|
2953
|
+
const models = Object.entries(mp.suggested_models).filter(([, v]) => v).map(([k, v]) => `${k}: ${v}`);
|
|
2954
|
+
if (models.length > 0) {
|
|
2955
|
+
lines.push(`- **Suggested models:** ${models.join(", ")}`);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
lines.push("");
|
|
2959
|
+
}
|
|
2960
|
+
lines.push("## Pre-loaded Context", "");
|
|
2961
|
+
const blocks = db.prepare("SELECT id, name, responsibility FROM building_blocks").all();
|
|
2962
|
+
if (blocks.length > 0) {
|
|
2963
|
+
lines.push("### Building Blocks", "");
|
|
2964
|
+
for (const b of blocks) {
|
|
2965
|
+
lines.push(`- **${b.name}** (\`${b.id}\`): ${b.responsibility}`);
|
|
2966
|
+
}
|
|
2967
|
+
lines.push("");
|
|
2968
|
+
}
|
|
2969
|
+
if (roleDef.qualityFocus.length > 0) {
|
|
2970
|
+
const allScenarios = db.prepare(
|
|
2971
|
+
"SELECT id, name, category, priority, status FROM quality_scenarios ORDER BY priority, category"
|
|
2972
|
+
).all();
|
|
2973
|
+
const focused = allScenarios.filter(
|
|
2974
|
+
(s) => roleDef.qualityFocus.includes(s.category) || s.priority === "must"
|
|
2975
|
+
);
|
|
2976
|
+
if (focused.length > 0) {
|
|
2977
|
+
lines.push("### Relevant Quality Scenarios", "");
|
|
2978
|
+
for (const s of focused) {
|
|
2979
|
+
const statusIcon = s.status === "passing" ? "PASS" : s.status === "failing" ? "FAIL" : "?";
|
|
2980
|
+
lines.push(
|
|
2981
|
+
`- [${statusIcon}] **${s.id}: ${s.name}** [${s.category}] (${s.priority})`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
lines.push("");
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
if (["phase-manager", "onboarding", "architect"].includes(params.role)) {
|
|
2988
|
+
const phases = db.prepare("SELECT id, name, status FROM phases ORDER BY id").all();
|
|
2989
|
+
if (phases.length > 0) {
|
|
2990
|
+
lines.push("### Phase Plan", "");
|
|
2991
|
+
for (const p of phases) {
|
|
2992
|
+
const icon = p.status === "complete" ? "[x]" : p.status === "in-progress" ? "[>]" : "[ ]";
|
|
2993
|
+
lines.push(`- ${icon} ${p.name} (${p.status})`);
|
|
2994
|
+
}
|
|
2995
|
+
lines.push("");
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
if (["implementer", "code-reviewer"].includes(params.role) && params.building_block) {
|
|
2999
|
+
const blockDetail = db.prepare("SELECT id, name, responsibility FROM building_blocks WHERE id = ?").get(params.building_block);
|
|
3000
|
+
if (!blockDetail) {
|
|
3001
|
+
lines.push(
|
|
3002
|
+
`### Warning: Unknown Building Block`,
|
|
3003
|
+
"",
|
|
3004
|
+
`Building block \`${params.building_block}\` not found. Use \`arcbridge_get_building_blocks\` to see available blocks.`,
|
|
3005
|
+
""
|
|
3006
|
+
);
|
|
3007
|
+
} else {
|
|
3008
|
+
lines.push(
|
|
3009
|
+
`### Focus Block: ${blockDetail.name}`,
|
|
3010
|
+
"",
|
|
3011
|
+
`**Responsibility:** ${blockDetail.responsibility}`,
|
|
3012
|
+
""
|
|
3013
|
+
);
|
|
3014
|
+
const blockTasks = db.prepare(
|
|
3015
|
+
"SELECT id, title, status FROM tasks WHERE building_block = ? AND status IN ('todo', 'in-progress')"
|
|
3016
|
+
).all(params.building_block);
|
|
3017
|
+
if (blockTasks.length > 0) {
|
|
3018
|
+
lines.push("### Active Tasks", "");
|
|
3019
|
+
for (const t of blockTasks) {
|
|
3020
|
+
lines.push(`- [${t.status}] ${t.id}: ${t.title}`);
|
|
3021
|
+
}
|
|
3022
|
+
lines.push("");
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
if (params.role === "phase-manager") {
|
|
3027
|
+
const currentPhase = db.prepare(
|
|
3028
|
+
"SELECT id, name FROM phases WHERE status = 'in-progress' LIMIT 1"
|
|
3029
|
+
).get();
|
|
3030
|
+
if (currentPhase) {
|
|
3031
|
+
const phaseTasks = db.prepare("SELECT id, title, status FROM tasks WHERE phase_id = ?").all(currentPhase.id);
|
|
3032
|
+
const done = phaseTasks.filter((t) => t.status === "done").length;
|
|
3033
|
+
lines.push(
|
|
3034
|
+
`### Current Phase: ${currentPhase.name} (${done}/${phaseTasks.length} tasks done)`,
|
|
3035
|
+
""
|
|
3036
|
+
);
|
|
3037
|
+
for (const t of phaseTasks) {
|
|
3038
|
+
const icon = t.status === "done" ? "[x]" : t.status === "in-progress" ? "[>]" : t.status === "blocked" ? "[!]" : "[ ]";
|
|
3039
|
+
lines.push(`- ${icon} ${t.id}: ${t.title}`);
|
|
3040
|
+
}
|
|
3041
|
+
lines.push("");
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
lines.push("## Instructions", "", roleDef.systemPrompt, "");
|
|
3045
|
+
return textResult(lines.join("\n"));
|
|
3046
|
+
}
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
function getRoleDefinition(roleId) {
|
|
3050
|
+
const roles = {
|
|
3051
|
+
architect: {
|
|
3052
|
+
name: "Architect",
|
|
3053
|
+
description: "Designs system structure, makes architectural decisions, and maintains the arc42 documentation",
|
|
3054
|
+
requiredTools: [
|
|
3055
|
+
"arcbridge_get_building_blocks",
|
|
3056
|
+
"arcbridge_get_quality_scenarios",
|
|
3057
|
+
"arcbridge_get_relevant_adrs",
|
|
3058
|
+
"arcbridge_search_symbols",
|
|
3059
|
+
"arcbridge_get_symbol",
|
|
3060
|
+
"arcbridge_get_dependency_graph",
|
|
3061
|
+
"arcbridge_get_component_graph",
|
|
3062
|
+
"arcbridge_get_route_map",
|
|
3063
|
+
"arcbridge_get_boundary_analysis",
|
|
3064
|
+
"arcbridge_propose_arc42_update",
|
|
3065
|
+
"arcbridge_check_drift",
|
|
3066
|
+
"arcbridge_get_open_questions"
|
|
3067
|
+
],
|
|
3068
|
+
deniedTools: [],
|
|
3069
|
+
readOnly: false,
|
|
3070
|
+
qualityFocus: ["maintainability", "reliability", "security", "performance"],
|
|
3071
|
+
systemPrompt: "You are the Architect agent. Design building blocks, make ADRs, maintain arc42 docs, ensure code-to-architecture mapping, review quality scenarios, and detect drift. Think at the system level."
|
|
3072
|
+
},
|
|
3073
|
+
implementer: {
|
|
3074
|
+
name: "Implementer",
|
|
3075
|
+
description: "Writes code within defined building block boundaries, follows existing patterns, and completes phase tasks",
|
|
3076
|
+
requiredTools: [
|
|
3077
|
+
"arcbridge_get_building_block",
|
|
3078
|
+
"arcbridge_get_current_tasks",
|
|
3079
|
+
"arcbridge_update_task",
|
|
3080
|
+
"arcbridge_search_symbols",
|
|
3081
|
+
"arcbridge_get_symbol",
|
|
3082
|
+
"arcbridge_get_guidance",
|
|
3083
|
+
"arcbridge_get_component_graph"
|
|
3084
|
+
],
|
|
3085
|
+
deniedTools: ["arcbridge_propose_arc42_update"],
|
|
3086
|
+
readOnly: false,
|
|
3087
|
+
qualityFocus: ["maintainability", "performance"],
|
|
3088
|
+
systemPrompt: "You are the Implementer agent. Write code within your assigned building block boundaries. Follow existing patterns, check guidance before making changes, and update task status when complete."
|
|
3089
|
+
},
|
|
3090
|
+
"security-reviewer": {
|
|
3091
|
+
name: "Security Reviewer",
|
|
3092
|
+
description: "Reviews code for security vulnerabilities, verifies security quality scenarios, and checks auth coverage",
|
|
3093
|
+
requiredTools: [
|
|
3094
|
+
"arcbridge_get_quality_scenarios",
|
|
3095
|
+
"arcbridge_get_building_blocks",
|
|
3096
|
+
"arcbridge_get_relevant_adrs",
|
|
3097
|
+
"arcbridge_search_symbols",
|
|
3098
|
+
"arcbridge_get_symbol",
|
|
3099
|
+
"arcbridge_get_route_map",
|
|
3100
|
+
"arcbridge_get_boundary_analysis",
|
|
3101
|
+
"arcbridge_get_practice_review"
|
|
3102
|
+
],
|
|
3103
|
+
deniedTools: ["arcbridge_propose_arc42_update"],
|
|
3104
|
+
readOnly: true,
|
|
3105
|
+
qualityFocus: ["security"],
|
|
3106
|
+
systemPrompt: "You are the Security Reviewer agent. Review code for vulnerabilities, verify auth coverage on routes, check server/client boundary safety, and validate security quality scenarios."
|
|
3107
|
+
},
|
|
3108
|
+
"quality-guardian": {
|
|
3109
|
+
name: "Quality Guardian",
|
|
3110
|
+
description: "Verifies quality scenarios are met, checks test coverage, and monitors performance budgets",
|
|
3111
|
+
requiredTools: [
|
|
3112
|
+
"arcbridge_get_quality_scenarios",
|
|
3113
|
+
"arcbridge_get_building_blocks",
|
|
3114
|
+
"arcbridge_search_symbols",
|
|
3115
|
+
"arcbridge_get_component_graph",
|
|
3116
|
+
"arcbridge_get_boundary_analysis"
|
|
3117
|
+
],
|
|
3118
|
+
deniedTools: [],
|
|
3119
|
+
readOnly: true,
|
|
3120
|
+
qualityFocus: [
|
|
3121
|
+
"security",
|
|
3122
|
+
"performance",
|
|
3123
|
+
"accessibility",
|
|
3124
|
+
"reliability",
|
|
3125
|
+
"maintainability"
|
|
3126
|
+
],
|
|
3127
|
+
systemPrompt: "You are the Quality Guardian agent. Verify all quality scenarios are met, check test coverage, monitor performance budgets, and flag regressions."
|
|
3128
|
+
},
|
|
3129
|
+
"phase-manager": {
|
|
3130
|
+
name: "Phase Manager",
|
|
3131
|
+
description: "Manages phase transitions, enforces gates, triggers sync, and tracks task completion",
|
|
3132
|
+
requiredTools: [
|
|
3133
|
+
"arcbridge_get_phase_plan",
|
|
3134
|
+
"arcbridge_get_current_tasks",
|
|
3135
|
+
"arcbridge_update_task",
|
|
3136
|
+
"arcbridge_check_drift",
|
|
3137
|
+
"arcbridge_get_open_questions",
|
|
3138
|
+
"arcbridge_propose_arc42_update",
|
|
3139
|
+
"arcbridge_complete_phase"
|
|
3140
|
+
],
|
|
3141
|
+
deniedTools: [],
|
|
3142
|
+
readOnly: false,
|
|
3143
|
+
qualityFocus: [],
|
|
3144
|
+
systemPrompt: "You are the Phase Manager agent. Track task completion, enforce phase gates, trigger architecture sync at boundaries, and manage phase transitions. Do not skip gates."
|
|
3145
|
+
},
|
|
3146
|
+
onboarding: {
|
|
3147
|
+
name: "Onboarding Guide",
|
|
3148
|
+
description: "Helps new team members understand the project architecture, conventions, and current state",
|
|
3149
|
+
requiredTools: [
|
|
3150
|
+
"arcbridge_get_project_status",
|
|
3151
|
+
"arcbridge_get_building_blocks",
|
|
3152
|
+
"arcbridge_get_quality_scenarios",
|
|
3153
|
+
"arcbridge_get_phase_plan",
|
|
3154
|
+
"arcbridge_get_relevant_adrs",
|
|
3155
|
+
"arcbridge_get_component_graph",
|
|
3156
|
+
"arcbridge_get_route_map"
|
|
3157
|
+
],
|
|
3158
|
+
deniedTools: [],
|
|
3159
|
+
readOnly: true,
|
|
3160
|
+
qualityFocus: [],
|
|
3161
|
+
systemPrompt: "You are the Onboarding Guide agent. Help new team members understand the project: architecture, conventions, current phase, and how to contribute. Be welcoming and thorough."
|
|
3162
|
+
},
|
|
3163
|
+
"code-reviewer": {
|
|
3164
|
+
name: "Code Reviewer",
|
|
3165
|
+
description: "On-demand code review: checks correctness, patterns, edge cases, and simplicity",
|
|
3166
|
+
requiredTools: [
|
|
3167
|
+
"arcbridge_get_building_block",
|
|
3168
|
+
"arcbridge_get_quality_scenarios",
|
|
3169
|
+
"arcbridge_get_relevant_adrs",
|
|
3170
|
+
"arcbridge_get_current_tasks",
|
|
3171
|
+
"arcbridge_search_symbols",
|
|
3172
|
+
"arcbridge_get_symbol",
|
|
3173
|
+
"arcbridge_get_dependency_graph",
|
|
3174
|
+
"arcbridge_get_component_graph",
|
|
3175
|
+
"arcbridge_get_route_map",
|
|
3176
|
+
"arcbridge_get_practice_review",
|
|
3177
|
+
"arcbridge_get_boundary_analysis"
|
|
3178
|
+
],
|
|
3179
|
+
deniedTools: [],
|
|
3180
|
+
readOnly: true,
|
|
3181
|
+
qualityFocus: ["maintainability", "reliability"],
|
|
3182
|
+
systemPrompt: "You are the Code Reviewer agent. Review for correctness, adherence to patterns, edge cases, simplicity, and alignment with quality scenarios. Be constructive and specific."
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
return roles[roleId] ?? null;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// src/tools/verify-scenarios.ts
|
|
3189
|
+
import { z as z25 } from "zod";
|
|
3190
|
+
import { verifyScenarios as verifyScenarios2, loadConfig as loadConfig2 } from "@arcbridge/core";
|
|
3191
|
+
function registerVerifyScenarios(server, ctx) {
|
|
3192
|
+
server.tool(
|
|
3193
|
+
"arcbridge_verify_scenarios",
|
|
3194
|
+
"Run linked tests for quality scenarios and update their pass/fail status. Only runs scenarios with verification='automatic' or 'semi-automatic' and non-empty linked_tests.",
|
|
3195
|
+
{
|
|
3196
|
+
target_dir: z25.string().describe("Absolute path to the project directory"),
|
|
3197
|
+
scenario_ids: z25.array(z25.string()).optional().describe(
|
|
3198
|
+
"Specific scenario IDs to verify (e.g., ['SEC-01', 'PERF-01']). If omitted, verifies all automatic scenarios."
|
|
3199
|
+
),
|
|
3200
|
+
test_command: z25.string().optional().describe(
|
|
3201
|
+
"Override the test command from config (e.g., 'npx jest'). File paths are appended as arguments."
|
|
3202
|
+
)
|
|
3203
|
+
},
|
|
3204
|
+
async (params) => {
|
|
3205
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
3206
|
+
if (!db) return notInitialized();
|
|
3207
|
+
let testCommand = "npx vitest run";
|
|
3208
|
+
let timeoutMs = 6e4;
|
|
3209
|
+
const configResult = loadConfig2(params.target_dir);
|
|
3210
|
+
if (configResult.config) {
|
|
3211
|
+
testCommand = configResult.config.testing.test_command;
|
|
3212
|
+
timeoutMs = configResult.config.testing.timeout_ms;
|
|
3213
|
+
}
|
|
3214
|
+
if (params.test_command) {
|
|
3215
|
+
testCommand = params.test_command;
|
|
3216
|
+
}
|
|
3217
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
3218
|
+
const result = verifyScenarios2(db, projectRoot, {
|
|
3219
|
+
testCommand,
|
|
3220
|
+
timeoutMs,
|
|
3221
|
+
scenarioIds: params.scenario_ids
|
|
3222
|
+
});
|
|
3223
|
+
const lines = ["# Scenario Verification Results", ""];
|
|
3224
|
+
if (result.results.length === 0) {
|
|
3225
|
+
lines.push(
|
|
3226
|
+
"No testable scenarios found. Scenarios need `verification: automatic` (or `semi-automatic`) and non-empty `linked_tests` to be verified."
|
|
3227
|
+
);
|
|
3228
|
+
return textResult(lines.join("\n"));
|
|
3229
|
+
}
|
|
3230
|
+
lines.push(`Ran tests for ${result.results.length} scenario(s).`, "");
|
|
3231
|
+
const passing = result.results.filter((r) => r.passed).length;
|
|
3232
|
+
const failing = result.results.length - passing;
|
|
3233
|
+
lines.push(
|
|
3234
|
+
`**Summary:** ${passing} passing, ${failing} failing`,
|
|
3235
|
+
""
|
|
3236
|
+
);
|
|
3237
|
+
for (const r of result.results) {
|
|
3238
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
3239
|
+
lines.push(
|
|
3240
|
+
`### [${icon}] ${r.scenarioId}: ${r.scenarioName} (${r.durationMs}ms)`,
|
|
3241
|
+
""
|
|
3242
|
+
);
|
|
3243
|
+
lines.push(`Tests: ${r.testPaths.join(", ")}`, "");
|
|
3244
|
+
if (!r.passed && r.output) {
|
|
3245
|
+
const trimmed = r.output.length > 500 ? `...${r.output.slice(-500)}` : r.output;
|
|
3246
|
+
lines.push("```", trimmed, "```", "");
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (result.updated > 0) {
|
|
3250
|
+
lines.push(
|
|
3251
|
+
`---`,
|
|
3252
|
+
`Updated status for ${result.updated} scenario(s) in the database.`
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
if (result.errors.length > 0) {
|
|
3256
|
+
lines.push("", "## Errors", "");
|
|
3257
|
+
for (const e of result.errors) {
|
|
3258
|
+
lines.push(`- ${e}`);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return textResult(lines.join("\n"));
|
|
3262
|
+
}
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// src/tools/run-role-check.ts
|
|
3267
|
+
import { z as z26 } from "zod";
|
|
3268
|
+
import {
|
|
3269
|
+
loadRole as loadRole2,
|
|
3270
|
+
loadRoles as loadRoles2,
|
|
3271
|
+
detectDrift as detectDrift4,
|
|
3272
|
+
resolveRef as resolveRef3,
|
|
3273
|
+
getChangedFiles as getChangedFiles3
|
|
3274
|
+
} from "@arcbridge/core";
|
|
3275
|
+
var SCOPE_VALUES = ["last-commit", "current-phase", "full-project"];
|
|
3276
|
+
function registerRunRoleCheck(server, ctx) {
|
|
3277
|
+
server.tool(
|
|
3278
|
+
"arcbridge_run_role_check",
|
|
3279
|
+
"Run a role-specific architectural analysis: resolves the role and executes relevant checks (drift, quality scenarios, boundaries, changed files) based on the role's focus areas.",
|
|
3280
|
+
{
|
|
3281
|
+
target_dir: z26.string().describe("Absolute path to the project directory"),
|
|
3282
|
+
role: z26.string().describe(
|
|
3283
|
+
"Role ID to run checks for (e.g., 'security-reviewer', 'quality-guardian', 'architect', 'phase-manager', 'code-reviewer')"
|
|
3284
|
+
),
|
|
3285
|
+
scope: z26.enum(SCOPE_VALUES).default("current-phase").describe(
|
|
3286
|
+
"Scope of analysis: 'last-commit' (recent changes), 'current-phase' (since phase start), 'full-project' (everything)"
|
|
3287
|
+
)
|
|
3288
|
+
},
|
|
3289
|
+
async (params) => {
|
|
3290
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
3291
|
+
if (!db) return notInitialized();
|
|
3292
|
+
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
3293
|
+
const fileResult = loadRole2(projectRoot, params.role);
|
|
3294
|
+
const role = fileResult.role ?? null;
|
|
3295
|
+
const roleDef = role ? { name: role.name, qualityFocus: role.quality_focus } : getBuiltInRoleDef(params.role);
|
|
3296
|
+
if (!roleDef) {
|
|
3297
|
+
const builtInIds = [
|
|
3298
|
+
"architect",
|
|
3299
|
+
"implementer",
|
|
3300
|
+
"security-reviewer",
|
|
3301
|
+
"quality-guardian",
|
|
3302
|
+
"phase-manager",
|
|
3303
|
+
"onboarding",
|
|
3304
|
+
"code-reviewer"
|
|
3305
|
+
];
|
|
3306
|
+
const fileRoles = loadRoles2(projectRoot);
|
|
3307
|
+
const fileIds = fileRoles.roles.map((r) => r.role_id);
|
|
3308
|
+
const availableIds = [.../* @__PURE__ */ new Set([...fileIds, ...builtInIds])].sort();
|
|
3309
|
+
return textResult(
|
|
3310
|
+
`Unknown role: \`${params.role}\`. Available roles: ${availableIds.map((r) => `\`${r}\``).join(", ")}`
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
const lines = [
|
|
3314
|
+
`# Role Check: ${roleDef.name}`,
|
|
3315
|
+
"",
|
|
3316
|
+
`**Scope:** ${params.scope}`,
|
|
3317
|
+
""
|
|
3318
|
+
];
|
|
3319
|
+
const changedFiles = getChangedFilesForScope(db, projectRoot, params.scope);
|
|
3320
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
3321
|
+
lines.push(`**Changed files:** ${changedFiles.length}`, "");
|
|
3322
|
+
}
|
|
3323
|
+
switch (params.role) {
|
|
3324
|
+
case "security-reviewer":
|
|
3325
|
+
runSecurityReviewerCheck(db, lines, changedFiles);
|
|
3326
|
+
break;
|
|
3327
|
+
case "quality-guardian":
|
|
3328
|
+
runQualityGuardianCheck(db, lines);
|
|
3329
|
+
break;
|
|
3330
|
+
case "architect":
|
|
3331
|
+
runArchitectCheck(db, lines);
|
|
3332
|
+
break;
|
|
3333
|
+
case "phase-manager":
|
|
3334
|
+
runPhaseManagerCheck(db, projectRoot, lines);
|
|
3335
|
+
break;
|
|
3336
|
+
case "code-reviewer":
|
|
3337
|
+
runCodeReviewerCheck(db, lines, changedFiles);
|
|
3338
|
+
break;
|
|
3339
|
+
default:
|
|
3340
|
+
runCustomRoleCheck(db, lines, roleDef);
|
|
3341
|
+
break;
|
|
3342
|
+
}
|
|
3343
|
+
return textResult(lines.join("\n"));
|
|
3344
|
+
}
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
function getChangedFilesForScope(db, projectRoot, scope) {
|
|
3348
|
+
if (scope === "full-project") return null;
|
|
3349
|
+
const since = scope === "last-commit" ? "last-commit" : "last-phase";
|
|
3350
|
+
const ref = resolveRef3(projectRoot, since, db);
|
|
3351
|
+
try {
|
|
3352
|
+
return getChangedFiles3(projectRoot, ref.sha);
|
|
3353
|
+
} catch {
|
|
3354
|
+
return null;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function getBuiltInRoleDef(roleId) {
|
|
3358
|
+
const defs = {
|
|
3359
|
+
architect: { name: "Architect", qualityFocus: ["maintainability", "reliability", "security", "performance"] },
|
|
3360
|
+
implementer: { name: "Implementer", qualityFocus: ["maintainability", "performance"] },
|
|
3361
|
+
"security-reviewer": { name: "Security Reviewer", qualityFocus: ["security"] },
|
|
3362
|
+
"quality-guardian": { name: "Quality Guardian", qualityFocus: ["security", "performance", "accessibility", "reliability", "maintainability"] },
|
|
3363
|
+
"phase-manager": { name: "Phase Manager", qualityFocus: [] },
|
|
3364
|
+
onboarding: { name: "Onboarding Guide", qualityFocus: [] },
|
|
3365
|
+
"code-reviewer": { name: "Code Reviewer", qualityFocus: ["maintainability", "reliability"] }
|
|
3366
|
+
};
|
|
3367
|
+
return defs[roleId] ?? null;
|
|
3368
|
+
}
|
|
3369
|
+
function appendDriftSection(db, lines) {
|
|
3370
|
+
const entries = detectDrift4(db);
|
|
3371
|
+
lines.push("## Drift Check", "");
|
|
3372
|
+
if (entries.length === 0) {
|
|
3373
|
+
lines.push("No architecture drift detected.", "");
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
const errors = entries.filter((e) => e.severity === "error").length;
|
|
3377
|
+
const warnings = entries.filter((e) => e.severity === "warning").length;
|
|
3378
|
+
const infos = entries.filter((e) => e.severity === "info").length;
|
|
3379
|
+
lines.push(`**${errors}** errors, **${warnings}** warnings, **${infos}** info`, "");
|
|
3380
|
+
const byKind = groupBy(entries, (e) => e.kind);
|
|
3381
|
+
const kindLabels = {
|
|
3382
|
+
undocumented_module: "Undocumented Modules",
|
|
3383
|
+
missing_module: "Missing Modules",
|
|
3384
|
+
dependency_violation: "Dependency Violations",
|
|
3385
|
+
stale_adr: "Stale ADR References",
|
|
3386
|
+
unlinked_test: "Unlinked Tests"
|
|
3387
|
+
};
|
|
3388
|
+
const severityIcon = {
|
|
3389
|
+
error: "ERROR",
|
|
3390
|
+
warning: "WARN",
|
|
3391
|
+
info: "INFO"
|
|
3392
|
+
};
|
|
3393
|
+
for (const [kind, items] of byKind) {
|
|
3394
|
+
lines.push(`### ${kindLabels[kind] ?? kind}`, "");
|
|
3395
|
+
for (const item of items) {
|
|
3396
|
+
const icon = severityIcon[item.severity] ?? item.severity;
|
|
3397
|
+
lines.push(`- [${icon}] ${item.description}`);
|
|
3398
|
+
}
|
|
3399
|
+
lines.push("");
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
function appendQualityScenarios(db, lines, categoryFilter) {
|
|
3403
|
+
const allScenarios = db.prepare(
|
|
3404
|
+
"SELECT id, name, category, priority, status FROM quality_scenarios ORDER BY priority, category"
|
|
3405
|
+
).all();
|
|
3406
|
+
const scenarios = categoryFilter ? allScenarios.filter(
|
|
3407
|
+
(s) => categoryFilter.includes(s.category) || s.priority === "must"
|
|
3408
|
+
) : allScenarios;
|
|
3409
|
+
const title = categoryFilter ? "## Quality Scenarios (Filtered)" : "## Quality Scenarios";
|
|
3410
|
+
lines.push(title, "");
|
|
3411
|
+
if (scenarios.length === 0) {
|
|
3412
|
+
lines.push("No matching quality scenarios found.", "");
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
const passing = scenarios.filter((s) => s.status === "passing").length;
|
|
3416
|
+
const failing = scenarios.filter((s) => s.status === "failing").length;
|
|
3417
|
+
const untested = scenarios.filter(
|
|
3418
|
+
(s) => s.status === "untested" || s.status === "partial"
|
|
3419
|
+
).length;
|
|
3420
|
+
lines.push(
|
|
3421
|
+
`**Total:** ${scenarios.length} | **Passing:** ${passing} | **Failing:** ${failing} | **Untested/Partial:** ${untested}`,
|
|
3422
|
+
""
|
|
3423
|
+
);
|
|
3424
|
+
for (const s of scenarios) {
|
|
3425
|
+
const icon = s.status === "passing" ? "PASS" : s.status === "failing" ? "FAIL" : "?";
|
|
3426
|
+
lines.push(
|
|
3427
|
+
`- [${icon}] **${s.id}: ${s.name}** [${s.category}] (${s.priority})`
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
lines.push("");
|
|
3431
|
+
}
|
|
3432
|
+
function appendChangedFilesList(lines, changedFiles) {
|
|
3433
|
+
if (!changedFiles || changedFiles.length === 0) return;
|
|
3434
|
+
lines.push("## Changed Files", "");
|
|
3435
|
+
const limit = 30;
|
|
3436
|
+
const display = changedFiles.slice(0, limit);
|
|
3437
|
+
for (const f of display) {
|
|
3438
|
+
const tag = f.status === "added" ? "A" : f.status === "deleted" ? "D" : "M";
|
|
3439
|
+
lines.push(`- [${tag}] \`${f.path}\``);
|
|
3440
|
+
}
|
|
3441
|
+
if (changedFiles.length > limit) {
|
|
3442
|
+
lines.push(`- ... and ${changedFiles.length - limit} more`);
|
|
3443
|
+
}
|
|
3444
|
+
lines.push("");
|
|
3445
|
+
}
|
|
3446
|
+
function appendBuildingBlocks(db, lines) {
|
|
3447
|
+
const blocks = db.prepare("SELECT id, name, responsibility FROM building_blocks").all();
|
|
3448
|
+
lines.push("## Building Blocks", "");
|
|
3449
|
+
if (blocks.length === 0) {
|
|
3450
|
+
lines.push("No building blocks defined.", "");
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
for (const b of blocks) {
|
|
3454
|
+
lines.push(`- **${b.name}** (\`${b.id}\`): ${b.responsibility}`);
|
|
3455
|
+
}
|
|
3456
|
+
lines.push("");
|
|
3457
|
+
}
|
|
3458
|
+
function mapFilesToBlocks(db, changedFiles) {
|
|
3459
|
+
const blocks = db.prepare("SELECT id, name, code_paths FROM building_blocks").all();
|
|
3460
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
3461
|
+
const unmapped = [];
|
|
3462
|
+
const changedPaths = changedFiles.filter((f) => f.status !== "deleted").map((f) => f.path);
|
|
3463
|
+
for (const path of changedPaths) {
|
|
3464
|
+
let matched = false;
|
|
3465
|
+
for (const block of blocks) {
|
|
3466
|
+
const paths = safeParseJson(block.code_paths, []);
|
|
3467
|
+
for (const cp of paths) {
|
|
3468
|
+
const prefix = normalizeCodePath(cp);
|
|
3469
|
+
if (path.startsWith(prefix)) {
|
|
3470
|
+
const existing = mapped.get(block.id) ?? [];
|
|
3471
|
+
existing.push(path);
|
|
3472
|
+
mapped.set(block.id, existing);
|
|
3473
|
+
matched = true;
|
|
3474
|
+
break;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (matched) break;
|
|
3478
|
+
}
|
|
3479
|
+
if (!matched) unmapped.push(path);
|
|
3480
|
+
}
|
|
3481
|
+
return { mapped, unmapped };
|
|
3482
|
+
}
|
|
3483
|
+
function groupBy(items, keyFn) {
|
|
3484
|
+
const map = /* @__PURE__ */ new Map();
|
|
3485
|
+
for (const item of items) {
|
|
3486
|
+
const key = keyFn(item);
|
|
3487
|
+
const existing = map.get(key) ?? [];
|
|
3488
|
+
existing.push(item);
|
|
3489
|
+
map.set(key, existing);
|
|
3490
|
+
}
|
|
3491
|
+
return map;
|
|
3492
|
+
}
|
|
3493
|
+
function runSecurityReviewerCheck(db, lines, changedFiles) {
|
|
3494
|
+
appendQualityScenarios(db, lines, ["security"]);
|
|
3495
|
+
appendDriftSection(db, lines);
|
|
3496
|
+
const clientComponents = db.prepare(
|
|
3497
|
+
`SELECT COUNT(*) as count FROM components WHERE is_client = 1`
|
|
3498
|
+
).get();
|
|
3499
|
+
const serverActions = db.prepare(
|
|
3500
|
+
`SELECT COUNT(*) as count FROM components WHERE is_server_action = 1`
|
|
3501
|
+
).get();
|
|
3502
|
+
const crossBoundary = db.prepare(
|
|
3503
|
+
`SELECT COUNT(*) as count
|
|
3504
|
+
FROM dependencies d
|
|
3505
|
+
JOIN components cs ON d.source_symbol = cs.symbol_id
|
|
3506
|
+
JOIN components ct ON d.target_symbol = ct.symbol_id
|
|
3507
|
+
WHERE cs.is_client != ct.is_client
|
|
3508
|
+
AND d.kind IN ('imports', 'calls', 'renders')`
|
|
3509
|
+
).get();
|
|
3510
|
+
lines.push("## Boundary Summary", "");
|
|
3511
|
+
lines.push(`- **Client components:** ${clientComponents.count}`);
|
|
3512
|
+
lines.push(`- **Server actions:** ${serverActions.count}`);
|
|
3513
|
+
lines.push(`- **Cross-boundary edges:** ${crossBoundary.count}`);
|
|
3514
|
+
lines.push("");
|
|
3515
|
+
const unauthRoutes = db.prepare(
|
|
3516
|
+
`SELECT route_path FROM routes WHERE kind = 'api-route' AND has_auth = 0`
|
|
3517
|
+
).all();
|
|
3518
|
+
if (unauthRoutes.length > 0) {
|
|
3519
|
+
lines.push("## Unauthenticated API Routes", "");
|
|
3520
|
+
for (const r of unauthRoutes) {
|
|
3521
|
+
lines.push(`- \`${r.route_path}\``);
|
|
3522
|
+
}
|
|
3523
|
+
lines.push("");
|
|
3524
|
+
}
|
|
3525
|
+
appendChangedFilesList(lines, changedFiles);
|
|
3526
|
+
}
|
|
3527
|
+
function runQualityGuardianCheck(db, lines) {
|
|
3528
|
+
appendQualityScenarios(db, lines);
|
|
3529
|
+
appendDriftSection(db, lines);
|
|
3530
|
+
const scenarios = db.prepare(
|
|
3531
|
+
`SELECT verification, status, COUNT(*) as count
|
|
3532
|
+
FROM quality_scenarios
|
|
3533
|
+
GROUP BY verification, status`
|
|
3534
|
+
).all();
|
|
3535
|
+
if (scenarios.length > 0) {
|
|
3536
|
+
lines.push("## Verification Coverage", "");
|
|
3537
|
+
for (const s of scenarios) {
|
|
3538
|
+
lines.push(
|
|
3539
|
+
`- **${s.verification}** / ${s.status}: ${s.count} scenario(s)`
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
lines.push("");
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
function runArchitectCheck(db, lines) {
|
|
3546
|
+
appendBuildingBlocks(db, lines);
|
|
3547
|
+
appendDriftSection(db, lines);
|
|
3548
|
+
const blocks = db.prepare("SELECT id, name, code_paths, interfaces FROM building_blocks").all();
|
|
3549
|
+
if (blocks.length > 1) {
|
|
3550
|
+
const violations = [];
|
|
3551
|
+
for (const block of blocks) {
|
|
3552
|
+
const declaredInterfaces = new Set(
|
|
3553
|
+
safeParseJson(block.interfaces, [])
|
|
3554
|
+
);
|
|
3555
|
+
const codePaths = safeParseJson(block.code_paths, []);
|
|
3556
|
+
for (const cp of codePaths) {
|
|
3557
|
+
const prefix = normalizeCodePath(cp);
|
|
3558
|
+
const outgoing = db.prepare(
|
|
3559
|
+
`SELECT DISTINCT st.file_path as target_file
|
|
3560
|
+
FROM dependencies d
|
|
3561
|
+
JOIN symbols ss ON d.source_symbol = ss.id
|
|
3562
|
+
JOIN symbols st ON d.target_symbol = st.id
|
|
3563
|
+
WHERE ss.file_path LIKE ? || '%'
|
|
3564
|
+
AND d.kind IN ('imports', 'calls', 'renders')`
|
|
3565
|
+
).all(prefix);
|
|
3566
|
+
for (const { target_file } of outgoing) {
|
|
3567
|
+
for (const targetBlock of blocks) {
|
|
3568
|
+
if (targetBlock.id === block.id) continue;
|
|
3569
|
+
const tPaths = safeParseJson(targetBlock.code_paths, []);
|
|
3570
|
+
for (const tp of tPaths) {
|
|
3571
|
+
const tPrefix = normalizeCodePath(tp);
|
|
3572
|
+
if (target_file.startsWith(tPrefix) && !declaredInterfaces.has(targetBlock.id)) {
|
|
3573
|
+
violations.push(
|
|
3574
|
+
`Block \`${block.name}\` depends on \`${targetBlock.name}\` (undeclared interface)`
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
const unique = [...new Set(violations)];
|
|
3583
|
+
if (unique.length > 0) {
|
|
3584
|
+
lines.push("## Undeclared Cross-Block Dependencies", "");
|
|
3585
|
+
for (const v of unique) {
|
|
3586
|
+
lines.push(`- [ERROR] ${v}`);
|
|
3587
|
+
}
|
|
3588
|
+
lines.push("");
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
function runPhaseManagerCheck(db, projectRoot, lines) {
|
|
3593
|
+
const currentPhase = db.prepare(
|
|
3594
|
+
"SELECT id, name, status FROM phases WHERE status = 'in-progress' LIMIT 1"
|
|
3595
|
+
).get();
|
|
3596
|
+
if (currentPhase) {
|
|
3597
|
+
const tasks = db.prepare("SELECT id, title, status, phase_id FROM tasks WHERE phase_id = ?").all(currentPhase.id);
|
|
3598
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
3599
|
+
const inProgress = tasks.filter((t) => t.status === "in-progress").length;
|
|
3600
|
+
const todo = tasks.filter((t) => t.status === "todo").length;
|
|
3601
|
+
const blocked = tasks.filter((t) => t.status === "blocked").length;
|
|
3602
|
+
lines.push(
|
|
3603
|
+
`## Current Phase: ${currentPhase.name}`,
|
|
3604
|
+
"",
|
|
3605
|
+
`**Progress:** ${done}/${tasks.length} done, ${inProgress} in-progress, ${todo} todo, ${blocked} blocked`,
|
|
3606
|
+
""
|
|
3607
|
+
);
|
|
3608
|
+
for (const t of tasks) {
|
|
3609
|
+
const icon = t.status === "done" ? "[x]" : t.status === "in-progress" ? "[>]" : t.status === "blocked" ? "[!]" : "[ ]";
|
|
3610
|
+
lines.push(`- ${icon} ${t.id}: ${t.title}`);
|
|
3611
|
+
}
|
|
3612
|
+
lines.push("");
|
|
3613
|
+
} else {
|
|
3614
|
+
lines.push("## Phase Status", "", "No phase currently in progress.", "");
|
|
3615
|
+
}
|
|
3616
|
+
appendDriftSection(db, lines);
|
|
3617
|
+
lines.push("## Quality Gate Status", "");
|
|
3618
|
+
const mustScenarios = db.prepare(
|
|
3619
|
+
"SELECT id, name, status FROM quality_scenarios WHERE priority = 'must'"
|
|
3620
|
+
).all();
|
|
3621
|
+
if (mustScenarios.length === 0) {
|
|
3622
|
+
lines.push("No must-have quality scenarios defined.", "");
|
|
3623
|
+
} else {
|
|
3624
|
+
const passing = mustScenarios.filter((s) => s.status === "passing").length;
|
|
3625
|
+
const failing = mustScenarios.filter((s) => s.status === "failing").length;
|
|
3626
|
+
const gateOk = failing === 0;
|
|
3627
|
+
lines.push(
|
|
3628
|
+
`**Must-have scenarios:** ${passing}/${mustScenarios.length} passing`,
|
|
3629
|
+
`**Gate status:** ${gateOk ? "READY" : "BLOCKED"}`,
|
|
3630
|
+
""
|
|
3631
|
+
);
|
|
3632
|
+
if (failing > 0) {
|
|
3633
|
+
for (const s of mustScenarios.filter((s2) => s2.status !== "passing")) {
|
|
3634
|
+
lines.push(`- [FAIL] ${s.id}: ${s.name} (${s.status})`);
|
|
3635
|
+
}
|
|
3636
|
+
lines.push("");
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
const ref = resolveRef3(projectRoot, "last-sync", db);
|
|
3640
|
+
try {
|
|
3641
|
+
const changedFiles = getChangedFiles3(projectRoot, ref.sha);
|
|
3642
|
+
if (changedFiles.length > 0) {
|
|
3643
|
+
lines.push(`## Changes Since Last Sync (${ref.label})`, "");
|
|
3644
|
+
appendChangedFilesList(lines, changedFiles);
|
|
3645
|
+
}
|
|
3646
|
+
} catch {
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
function runCodeReviewerCheck(db, lines, changedFiles) {
|
|
3650
|
+
if (!changedFiles || changedFiles.length === 0) {
|
|
3651
|
+
lines.push(
|
|
3652
|
+
"## Changed Files",
|
|
3653
|
+
"",
|
|
3654
|
+
"No changed files in the selected scope. Use a different scope or specify 'full-project'.",
|
|
3655
|
+
""
|
|
3656
|
+
);
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
appendChangedFilesList(lines, changedFiles);
|
|
3660
|
+
const { mapped, unmapped } = mapFilesToBlocks(db, changedFiles);
|
|
3661
|
+
const blocks = db.prepare("SELECT id, name, responsibility FROM building_blocks").all();
|
|
3662
|
+
if (mapped.size > 0) {
|
|
3663
|
+
lines.push("## Affected Building Blocks", "");
|
|
3664
|
+
for (const [blockId, files] of mapped) {
|
|
3665
|
+
const block = blocks.find((b) => b.id === blockId);
|
|
3666
|
+
const name = block ? block.name : blockId;
|
|
3667
|
+
const resp = block ? block.responsibility : "";
|
|
3668
|
+
lines.push(`### ${name} (\`${blockId}\`)`, "");
|
|
3669
|
+
if (resp) lines.push(`**Responsibility:** ${resp}`, "");
|
|
3670
|
+
lines.push(`**Changed files:** ${files.length}`, "");
|
|
3671
|
+
for (const f of files.slice(0, 10)) {
|
|
3672
|
+
lines.push(`- \`${f}\``);
|
|
3673
|
+
}
|
|
3674
|
+
if (files.length > 10) {
|
|
3675
|
+
lines.push(`- ... and ${files.length - 10} more`);
|
|
3676
|
+
}
|
|
3677
|
+
lines.push("");
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
if (unmapped.length > 0) {
|
|
3681
|
+
lines.push("## Unmapped Files", "");
|
|
3682
|
+
lines.push(
|
|
3683
|
+
`${unmapped.length} file(s) not mapped to any building block:`,
|
|
3684
|
+
""
|
|
3685
|
+
);
|
|
3686
|
+
for (const f of unmapped.slice(0, 10)) {
|
|
3687
|
+
lines.push(`- \`${f}\``);
|
|
3688
|
+
}
|
|
3689
|
+
if (unmapped.length > 10) {
|
|
3690
|
+
lines.push(`- ... and ${unmapped.length - 10} more`);
|
|
3691
|
+
}
|
|
3692
|
+
lines.push("");
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
function runCustomRoleCheck(db, lines, roleDef) {
|
|
3696
|
+
if (roleDef.qualityFocus.length > 0) {
|
|
3697
|
+
appendQualityScenarios(db, lines, roleDef.qualityFocus);
|
|
3698
|
+
} else {
|
|
3699
|
+
appendQualityScenarios(db, lines);
|
|
3700
|
+
}
|
|
3701
|
+
appendDriftSection(db, lines);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/server.ts
|
|
3705
|
+
function createArcBridgeServer() {
|
|
3706
|
+
const server = new McpServer({
|
|
3707
|
+
name: "arcbridge",
|
|
3708
|
+
version: "0.1.0"
|
|
3709
|
+
});
|
|
3710
|
+
const ctx = createContext();
|
|
3711
|
+
registerInitProject(server, ctx);
|
|
3712
|
+
registerGetProjectStatus(server, ctx);
|
|
3713
|
+
registerGetBuildingBlocks(server, ctx);
|
|
3714
|
+
registerGetBuildingBlock(server, ctx);
|
|
3715
|
+
registerGetQualityScenarios(server, ctx);
|
|
3716
|
+
registerGetRelevantAdrs(server, ctx);
|
|
3717
|
+
registerGetPhasePlan(server, ctx);
|
|
3718
|
+
registerGetCurrentTasks(server, ctx);
|
|
3719
|
+
registerUpdateTask(server, ctx);
|
|
3720
|
+
registerCreateTask(server, ctx);
|
|
3721
|
+
registerReindex(server, ctx);
|
|
3722
|
+
registerSearchSymbols(server, ctx);
|
|
3723
|
+
registerGetSymbol(server, ctx);
|
|
3724
|
+
registerGetDependencyGraph(server, ctx);
|
|
3725
|
+
registerGetComponentGraph(server, ctx);
|
|
3726
|
+
registerGetRouteMap(server, ctx);
|
|
3727
|
+
registerGetBoundaryAnalysis(server, ctx);
|
|
3728
|
+
registerCheckDrift(server, ctx);
|
|
3729
|
+
registerGetGuidance(server, ctx);
|
|
3730
|
+
registerGetOpenQuestions(server, ctx);
|
|
3731
|
+
registerProposeArc42Update(server, ctx);
|
|
3732
|
+
registerGetPracticeReview(server, ctx);
|
|
3733
|
+
registerCompletePhase(server, ctx);
|
|
3734
|
+
registerActivateRole(server, ctx);
|
|
3735
|
+
registerVerifyScenarios(server, ctx);
|
|
3736
|
+
registerRunRoleCheck(server, ctx);
|
|
3737
|
+
return server;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// src/index.ts
|
|
3741
|
+
async function main() {
|
|
3742
|
+
const server = createArcBridgeServer();
|
|
3743
|
+
const transport = new StdioServerTransport();
|
|
3744
|
+
await server.connect(transport);
|
|
3745
|
+
}
|
|
3746
|
+
main().catch((error) => {
|
|
3747
|
+
console.error("Fatal error:", error);
|
|
3748
|
+
process.exit(1);
|
|
3749
|
+
});
|
|
3750
|
+
//# sourceMappingURL=index.js.map
|