driftdetect-mcp 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
- import { PatternStore, ManifestStore, } from 'driftdetect-core';
8
+ import { PatternStore, ManifestStore, HistoryStore, DNAStore, PlaybookGenerator, AIContextBuilder, GENE_IDS, BoundaryStore, } from 'driftdetect-core';
9
9
  import { PackManager } from './packs.js';
10
10
  import { FeedbackManager } from './feedback.js';
11
11
  const PATTERN_CATEGORIES = [
@@ -234,11 +234,178 @@ const TOOLS = [
234
234
  required: [],
235
235
  },
236
236
  },
237
+ {
238
+ name: 'drift_trends',
239
+ description: 'Get pattern trend analysis and regression alerts. Shows how patterns have changed over time. Use this to check codebase health before generating code or to identify patterns that need attention.',
240
+ inputSchema: {
241
+ type: 'object',
242
+ properties: {
243
+ period: {
244
+ type: 'string',
245
+ enum: ['7d', '30d', '90d'],
246
+ description: 'Time period to analyze (default: 7d)',
247
+ },
248
+ category: {
249
+ type: 'string',
250
+ description: `Filter trends by category. Valid: ${PATTERN_CATEGORIES.join(', ')}`,
251
+ },
252
+ severity: {
253
+ type: 'string',
254
+ enum: ['all', 'critical', 'warning'],
255
+ description: 'Filter by severity level (default: all)',
256
+ },
257
+ type: {
258
+ type: 'string',
259
+ enum: ['all', 'regressions', 'improvements'],
260
+ description: 'Filter by trend type (default: all)',
261
+ },
262
+ },
263
+ required: [],
264
+ },
265
+ },
266
+ {
267
+ name: 'drift_parser_info',
268
+ description: 'Get information about parser capabilities and status. Shows which parsers are available (tree-sitter vs regex), their features, and supported frameworks. Use this to understand parsing capabilities before analyzing Python, C#, TypeScript, or Java code.',
269
+ inputSchema: {
270
+ type: 'object',
271
+ properties: {
272
+ language: {
273
+ type: 'string',
274
+ enum: ['python', 'csharp', 'typescript', 'java', 'all'],
275
+ description: 'Language to get parser info for (default: all)',
276
+ },
277
+ },
278
+ required: [],
279
+ },
280
+ },
281
+ // DNA Tools
282
+ {
283
+ name: 'drift_dna',
284
+ description: 'Get the styling DNA profile for the codebase. Shows how components are styled (variants, responsive, states, theming, spacing, animation) with confidence scores and exemplar files.',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ gene: {
289
+ type: 'string',
290
+ enum: ['variant-handling', 'responsive-approach', 'state-styling', 'theming', 'spacing-philosophy', 'animation-approach'],
291
+ description: 'Specific gene to query (optional)',
292
+ },
293
+ format: {
294
+ type: 'string',
295
+ enum: ['full', 'summary', 'ai-context'],
296
+ description: 'Output format (default: ai-context)',
297
+ },
298
+ level: {
299
+ type: 'number',
300
+ enum: [1, 2, 3, 4],
301
+ description: 'AI context detail level 1-4 (default: 3)',
302
+ },
303
+ },
304
+ required: [],
305
+ },
306
+ },
307
+ {
308
+ name: 'drift_playbook',
309
+ description: 'Generate or retrieve the styling playbook documentation. Contains conventions, code examples, and patterns to avoid for each styling concern.',
310
+ inputSchema: {
311
+ type: 'object',
312
+ properties: {
313
+ regenerate: {
314
+ type: 'boolean',
315
+ description: 'Force regeneration of playbook (default: false)',
316
+ },
317
+ section: {
318
+ type: 'string',
319
+ enum: ['variant-handling', 'responsive-approach', 'state-styling', 'theming', 'spacing-philosophy', 'animation-approach'],
320
+ description: 'Specific section to retrieve (optional)',
321
+ },
322
+ },
323
+ required: [],
324
+ },
325
+ },
326
+ {
327
+ name: 'drift_mutations',
328
+ description: 'Get files that deviate from established styling patterns. Mutations are styling inconsistencies that fragment AI context.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ gene: {
333
+ type: 'string',
334
+ enum: ['variant-handling', 'responsive-approach', 'state-styling', 'theming', 'spacing-philosophy', 'animation-approach'],
335
+ description: 'Filter by gene (optional)',
336
+ },
337
+ impact: {
338
+ type: 'string',
339
+ enum: ['low', 'medium', 'high'],
340
+ description: 'Filter by impact level (optional)',
341
+ },
342
+ suggest: {
343
+ type: 'boolean',
344
+ description: 'Include resolution suggestions (default: false)',
345
+ },
346
+ },
347
+ required: [],
348
+ },
349
+ },
350
+ {
351
+ name: 'drift_dna_check',
352
+ description: 'Check if code follows the established styling DNA. Use before generating component code to ensure consistency.',
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ code: {
357
+ type: 'string',
358
+ description: 'Code snippet to check against DNA',
359
+ },
360
+ file: {
361
+ type: 'string',
362
+ description: 'File path to check (alternative to code)',
363
+ },
364
+ genes: {
365
+ type: 'array',
366
+ items: { type: 'string' },
367
+ description: 'Specific genes to check (optional, defaults to all)',
368
+ },
369
+ },
370
+ required: [],
371
+ },
372
+ },
373
+ // Data Boundaries Tool
374
+ {
375
+ name: 'drift_boundaries',
376
+ description: 'Get data access boundaries and check for violations. Shows which code accesses which database tables/fields. Use this before generating data access code to understand access rules and restrictions.',
377
+ inputSchema: {
378
+ type: 'object',
379
+ properties: {
380
+ action: {
381
+ type: 'string',
382
+ enum: ['overview', 'table', 'file', 'sensitive', 'check', 'rules'],
383
+ description: 'Action to perform (default: overview). "overview" shows summary, "table" shows specific table access, "file" shows what data a file accesses, "sensitive" shows sensitive field access, "check" validates against rules, "rules" shows configured boundaries',
384
+ },
385
+ table: {
386
+ type: 'string',
387
+ description: 'Table name (required for "table" action)',
388
+ },
389
+ file: {
390
+ type: 'string',
391
+ description: 'File path or glob pattern (required for "file" action)',
392
+ },
393
+ includeViolations: {
394
+ type: 'boolean',
395
+ description: 'Include boundary violations in response (default: true)',
396
+ },
397
+ },
398
+ required: [],
399
+ },
400
+ },
237
401
  ];
238
402
  export function createDriftMCPServer(config) {
239
403
  const server = new Server({ name: 'drift', version: '0.1.0' }, { capabilities: { tools: {} } });
240
404
  const patternStore = new PatternStore({ rootDir: config.projectRoot });
241
405
  const manifestStore = new ManifestStore(config.projectRoot);
406
+ const historyStore = new HistoryStore({ rootDir: config.projectRoot });
407
+ const dnaStore = new DNAStore({ rootDir: config.projectRoot });
408
+ const boundaryStore = new BoundaryStore({ rootDir: config.projectRoot });
242
409
  const packManager = new PackManager(config.projectRoot, patternStore);
243
410
  const feedbackManager = new FeedbackManager(config.projectRoot);
244
411
  // List available tools
@@ -268,6 +435,20 @@ export function createDriftMCPServer(config) {
268
435
  return await handlePack(packManager, args);
269
436
  case 'drift_feedback':
270
437
  return await handleFeedback(feedbackManager, args);
438
+ case 'drift_trends':
439
+ return await handleTrends(historyStore, args);
440
+ case 'drift_parser_info':
441
+ return await handleParserInfo(args);
442
+ case 'drift_dna':
443
+ return await handleDNA(config.projectRoot, dnaStore, args);
444
+ case 'drift_playbook':
445
+ return await handlePlaybook(config.projectRoot, dnaStore, args);
446
+ case 'drift_mutations':
447
+ return await handleMutations(dnaStore, args);
448
+ case 'drift_dna_check':
449
+ return await handleDNACheck(config.projectRoot, dnaStore, args);
450
+ case 'drift_boundaries':
451
+ return await handleBoundaries(boundaryStore, args);
271
452
  default:
272
453
  return {
273
454
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -973,4 +1154,967 @@ async function handleFeedback(feedbackManager, args) {
973
1154
  };
974
1155
  }
975
1156
  }
1157
+ async function handleTrends(historyStore, args) {
1158
+ await historyStore.initialize();
1159
+ const period = args.period ?? '7d';
1160
+ const summary = await historyStore.getTrendSummary(period);
1161
+ if (!summary) {
1162
+ return {
1163
+ content: [{
1164
+ type: 'text',
1165
+ text: '# No Trend Data Available\n\n' +
1166
+ 'Pattern trends require at least 2 snapshots. Snapshots are created automatically when you run `drift scan`.\n\n' +
1167
+ 'Run a few scans over time to start tracking trends.',
1168
+ }],
1169
+ };
1170
+ }
1171
+ // Filter by category if specified
1172
+ let regressions = summary.regressions;
1173
+ let improvements = summary.improvements;
1174
+ if (args.category) {
1175
+ regressions = regressions.filter(t => t.category === args.category);
1176
+ improvements = improvements.filter(t => t.category === args.category);
1177
+ }
1178
+ // Filter by severity
1179
+ if (args.severity && args.severity !== 'all') {
1180
+ regressions = regressions.filter(t => t.severity === args.severity);
1181
+ }
1182
+ // Filter by type
1183
+ if (args.type === 'regressions') {
1184
+ improvements = [];
1185
+ }
1186
+ else if (args.type === 'improvements') {
1187
+ regressions = [];
1188
+ }
1189
+ // Build output
1190
+ let output = `# Pattern Trends (${period})\n\n`;
1191
+ output += `Period: ${summary.startDate} → ${summary.endDate}\n`;
1192
+ output += `Overall trend: **${summary.overallTrend.toUpperCase()}** (${summary.healthDelta >= 0 ? '+' : ''}${(summary.healthDelta * 100).toFixed(1)}% health)\n\n`;
1193
+ // Summary stats
1194
+ output += `## Summary\n`;
1195
+ output += `- Regressions: ${regressions.length}\n`;
1196
+ output += `- Improvements: ${improvements.length}\n`;
1197
+ output += `- Stable patterns: ${summary.stable}\n\n`;
1198
+ // Critical regressions first
1199
+ const criticalRegressions = regressions.filter(t => t.severity === 'critical');
1200
+ if (criticalRegressions.length > 0) {
1201
+ output += `## ⚠️ Critical Regressions\n\n`;
1202
+ for (const t of criticalRegressions) {
1203
+ output += `### ${t.patternName} (${t.category})\n`;
1204
+ output += `- ${t.details}\n`;
1205
+ output += `- Metric: ${t.metric}\n`;
1206
+ output += `- Change: ${t.changePercent >= 0 ? '+' : ''}${t.changePercent.toFixed(1)}%\n\n`;
1207
+ }
1208
+ }
1209
+ // Warning regressions
1210
+ const warningRegressions = regressions.filter(t => t.severity === 'warning');
1211
+ if (warningRegressions.length > 0) {
1212
+ output += `## ⚡ Warning Regressions\n\n`;
1213
+ for (const t of warningRegressions) {
1214
+ output += `### ${t.patternName} (${t.category})\n`;
1215
+ output += `- ${t.details}\n`;
1216
+ output += `- Metric: ${t.metric}\n`;
1217
+ output += `- Change: ${t.changePercent >= 0 ? '+' : ''}${t.changePercent.toFixed(1)}%\n\n`;
1218
+ }
1219
+ }
1220
+ // Improvements
1221
+ if (improvements.length > 0) {
1222
+ output += `## ✅ Improvements\n\n`;
1223
+ for (const t of improvements) {
1224
+ output += `### ${t.patternName} (${t.category})\n`;
1225
+ output += `- ${t.details}\n`;
1226
+ output += `- Metric: ${t.metric}\n`;
1227
+ output += `- Change: +${t.changePercent.toFixed(1)}%\n\n`;
1228
+ }
1229
+ }
1230
+ // Category breakdown
1231
+ const categoryEntries = Object.entries(summary.categoryTrends);
1232
+ if (categoryEntries.length > 0) {
1233
+ output += `## Category Trends\n\n`;
1234
+ for (const [category, trend] of categoryEntries) {
1235
+ const emoji = trend.trend === 'improving' ? '📈' : trend.trend === 'declining' ? '📉' : '➡️';
1236
+ output += `- ${emoji} **${category}**: ${trend.trend} (confidence: ${trend.avgConfidenceChange >= 0 ? '+' : ''}${(trend.avgConfidenceChange * 100).toFixed(1)}%, compliance: ${trend.complianceChange >= 0 ? '+' : ''}${(trend.complianceChange * 100).toFixed(1)}%)\n`;
1237
+ }
1238
+ }
1239
+ return {
1240
+ content: [{ type: 'text', text: output }],
1241
+ };
1242
+ }
1243
+ /**
1244
+ * Handle drift_parser_info tool
1245
+ *
1246
+ * Returns information about parser capabilities and status.
1247
+ */
1248
+ async function handleParserInfo(args) {
1249
+ const language = args.language ?? 'all';
1250
+ const info = {};
1251
+ // Python parser info
1252
+ if (language === 'python' || language === 'all') {
1253
+ let treeSitterAvailable = false;
1254
+ let loadingError;
1255
+ try {
1256
+ const core = await import('driftdetect-core');
1257
+ // Check if the functions exist (they may not be exported yet)
1258
+ if ('isTreeSitterAvailable' in core && 'getLoadingError' in core) {
1259
+ treeSitterAvailable = core.isTreeSitterAvailable();
1260
+ loadingError = core.getLoadingError() ?? undefined;
1261
+ }
1262
+ }
1263
+ catch {
1264
+ loadingError = 'driftdetect-core not available';
1265
+ }
1266
+ info.python = {
1267
+ treeSitterAvailable,
1268
+ activeParser: treeSitterAvailable ? 'tree-sitter' : 'regex',
1269
+ capabilities: {
1270
+ basicRouteDetection: true,
1271
+ simplePydanticModels: true,
1272
+ pydanticModels: treeSitterAvailable,
1273
+ nestedTypes: treeSitterAvailable,
1274
+ fieldConstraints: treeSitterAvailable,
1275
+ inheritance: treeSitterAvailable,
1276
+ generics: treeSitterAvailable,
1277
+ django: treeSitterAvailable,
1278
+ typeHints: treeSitterAvailable,
1279
+ },
1280
+ supportedFrameworks: treeSitterAvailable
1281
+ ? ['fastapi', 'flask', 'django', 'starlette']
1282
+ : ['fastapi', 'flask'],
1283
+ expectedConfidence: treeSitterAvailable ? 'high (0.7-0.9)' : 'low (0.3-0.5)',
1284
+ loadingError,
1285
+ };
1286
+ }
1287
+ // C# parser info
1288
+ if (language === 'csharp' || language === 'all') {
1289
+ let treeSitterAvailable = false;
1290
+ let loadingError;
1291
+ try {
1292
+ const { isCSharpTreeSitterAvailable, getCSharpLoadingError } = await import('driftdetect-core');
1293
+ treeSitterAvailable = isCSharpTreeSitterAvailable();
1294
+ loadingError = getCSharpLoadingError() ?? undefined;
1295
+ }
1296
+ catch {
1297
+ loadingError = 'C# parser not available';
1298
+ }
1299
+ info.csharp = {
1300
+ treeSitterAvailable,
1301
+ activeParser: treeSitterAvailable ? 'tree-sitter' : 'regex',
1302
+ capabilities: {
1303
+ basicParsing: true,
1304
+ classExtraction: treeSitterAvailable,
1305
+ methodExtraction: treeSitterAvailable,
1306
+ attributeExtraction: treeSitterAvailable,
1307
+ aspNetControllers: treeSitterAvailable,
1308
+ minimalApis: treeSitterAvailable,
1309
+ recordTypes: treeSitterAvailable,
1310
+ },
1311
+ supportedFrameworks: treeSitterAvailable
1312
+ ? ['asp.net-core', 'minimal-apis', 'web-api']
1313
+ : [],
1314
+ loadingError,
1315
+ };
1316
+ }
1317
+ // TypeScript parser info
1318
+ if (language === 'typescript' || language === 'all') {
1319
+ info.typescript = {
1320
+ treeSitterAvailable: true, // TypeScript uses compiler API, always available
1321
+ activeParser: 'typescript-compiler-api',
1322
+ capabilities: {
1323
+ fullAST: true,
1324
+ typeInference: true,
1325
+ interfaces: true,
1326
+ generics: true,
1327
+ decorators: true,
1328
+ },
1329
+ supportedFrameworks: ['express', 'nestjs', 'fastify'],
1330
+ };
1331
+ }
1332
+ // Java parser info
1333
+ if (language === 'java' || language === 'all') {
1334
+ let javaTreeSitterAvailable = false;
1335
+ let javaLoadingError;
1336
+ try {
1337
+ const core = await import('driftdetect-core');
1338
+ // Check if the functions exist (they may not be exported yet)
1339
+ if ('isJavaTreeSitterAvailable' in core && 'getJavaLoadingError' in core) {
1340
+ javaTreeSitterAvailable = core.isJavaTreeSitterAvailable();
1341
+ javaLoadingError = core.getJavaLoadingError() ?? undefined;
1342
+ }
1343
+ else {
1344
+ javaLoadingError = 'Java parser functions not yet available in driftdetect-core';
1345
+ }
1346
+ }
1347
+ catch {
1348
+ javaLoadingError = 'Java parser not available';
1349
+ }
1350
+ info.java = {
1351
+ treeSitterAvailable: javaTreeSitterAvailable,
1352
+ activeParser: javaTreeSitterAvailable ? 'tree-sitter' : 'regex',
1353
+ capabilities: {
1354
+ basicParsing: true,
1355
+ classExtraction: javaTreeSitterAvailable,
1356
+ methodExtraction: javaTreeSitterAvailable,
1357
+ annotationExtraction: javaTreeSitterAvailable,
1358
+ springControllers: javaTreeSitterAvailable,
1359
+ springData: javaTreeSitterAvailable,
1360
+ springSecurity: javaTreeSitterAvailable,
1361
+ recordTypes: javaTreeSitterAvailable,
1362
+ },
1363
+ supportedFrameworks: javaTreeSitterAvailable
1364
+ ? ['spring-boot', 'spring-mvc', 'spring-data', 'spring-security']
1365
+ : [],
1366
+ loadingError: javaLoadingError,
1367
+ };
1368
+ }
1369
+ // PHP parser info
1370
+ if (language === 'php' || language === 'all') {
1371
+ let phpTreeSitterAvailable = false;
1372
+ let phpLoadingError;
1373
+ try {
1374
+ const core = await import('driftdetect-core');
1375
+ // Check if the functions exist
1376
+ if ('isPhpTreeSitterAvailable' in core && 'getPhpLoadingError' in core) {
1377
+ phpTreeSitterAvailable = core.isPhpTreeSitterAvailable();
1378
+ phpLoadingError = core.getPhpLoadingError() ?? undefined;
1379
+ }
1380
+ else {
1381
+ phpLoadingError = 'PHP parser functions not yet available in driftdetect-core';
1382
+ }
1383
+ }
1384
+ catch {
1385
+ phpLoadingError = 'PHP parser not available';
1386
+ }
1387
+ info.php = {
1388
+ treeSitterAvailable: phpTreeSitterAvailable,
1389
+ activeParser: phpTreeSitterAvailable ? 'tree-sitter' : 'regex',
1390
+ capabilities: {
1391
+ basicParsing: true,
1392
+ classExtraction: phpTreeSitterAvailable,
1393
+ methodExtraction: phpTreeSitterAvailable,
1394
+ attributeExtraction: phpTreeSitterAvailable,
1395
+ laravelControllers: phpTreeSitterAvailable,
1396
+ laravelModels: phpTreeSitterAvailable,
1397
+ traitExtraction: phpTreeSitterAvailable,
1398
+ enumExtraction: phpTreeSitterAvailable,
1399
+ },
1400
+ supportedFrameworks: phpTreeSitterAvailable
1401
+ ? ['laravel', 'symfony', 'php8']
1402
+ : ['laravel'],
1403
+ loadingError: phpLoadingError,
1404
+ };
1405
+ }
1406
+ // Build human-readable output
1407
+ let output = '# Parser Information\n\n';
1408
+ if (info.python) {
1409
+ const py = info.python;
1410
+ output += '## Python\n\n';
1411
+ output += `- **Active Parser:** ${py.activeParser}\n`;
1412
+ output += `- **Tree-sitter:** ${py.treeSitterAvailable ? '✓ available' : '✗ not installed'}\n`;
1413
+ output += `- **Expected Confidence:** ${py.expectedConfidence}\n`;
1414
+ output += `- **Supported Frameworks:** ${py.supportedFrameworks.join(', ')}\n\n`;
1415
+ output += '### Capabilities\n\n';
1416
+ for (const [cap, enabled] of Object.entries(py.capabilities)) {
1417
+ const emoji = enabled ? '✓' : '✗';
1418
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1419
+ output += `- ${emoji} ${capName}\n`;
1420
+ }
1421
+ output += '\n';
1422
+ if (py.loadingError) {
1423
+ output += `> ⚠️ Loading error: ${py.loadingError}\n\n`;
1424
+ }
1425
+ }
1426
+ if (info.csharp) {
1427
+ const cs = info.csharp;
1428
+ output += '## C#\n\n';
1429
+ output += `- **Active Parser:** ${cs.activeParser}\n`;
1430
+ output += `- **Tree-sitter:** ${cs.treeSitterAvailable ? '✓ available' : '✗ not installed'}\n`;
1431
+ if (cs.supportedFrameworks.length > 0) {
1432
+ output += `- **Supported Frameworks:** ${cs.supportedFrameworks.join(', ')}\n`;
1433
+ }
1434
+ output += '\n';
1435
+ output += '### Capabilities\n\n';
1436
+ for (const [cap, enabled] of Object.entries(cs.capabilities)) {
1437
+ const emoji = enabled ? '✓' : '✗';
1438
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1439
+ output += `- ${emoji} ${capName}\n`;
1440
+ }
1441
+ output += '\n';
1442
+ if (cs.loadingError) {
1443
+ output += `> ⚠️ Loading error: ${cs.loadingError}\n\n`;
1444
+ }
1445
+ }
1446
+ if (info.typescript) {
1447
+ const ts = info.typescript;
1448
+ output += '## TypeScript/JavaScript\n\n';
1449
+ output += `- **Active Parser:** ${ts.activeParser}\n\n`;
1450
+ output += '### Capabilities\n\n';
1451
+ for (const [cap, enabled] of Object.entries(ts.capabilities)) {
1452
+ const emoji = enabled ? '✓' : '✗';
1453
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1454
+ output += `- ${emoji} ${capName}\n`;
1455
+ }
1456
+ output += '\n';
1457
+ }
1458
+ if (info.java) {
1459
+ const java = info.java;
1460
+ output += '## Java\n\n';
1461
+ output += `- **Active Parser:** ${java.activeParser}\n`;
1462
+ output += `- **Tree-sitter:** ${java.treeSitterAvailable ? '✓ available' : '✗ not installed'}\n`;
1463
+ if (java.supportedFrameworks.length > 0) {
1464
+ output += `- **Supported Frameworks:** ${java.supportedFrameworks.join(', ')}\n`;
1465
+ }
1466
+ output += '\n';
1467
+ output += '### Capabilities\n\n';
1468
+ for (const [cap, enabled] of Object.entries(java.capabilities)) {
1469
+ const emoji = enabled ? '✓' : '✗';
1470
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1471
+ output += `- ${emoji} ${capName}\n`;
1472
+ }
1473
+ output += '\n';
1474
+ if (java.loadingError) {
1475
+ output += `> ⚠️ Loading error: ${java.loadingError}\n\n`;
1476
+ }
1477
+ }
1478
+ if (info.php) {
1479
+ const php = info.php;
1480
+ output += '## PHP\n\n';
1481
+ output += `- **Active Parser:** ${php.activeParser}\n`;
1482
+ output += `- **Tree-sitter:** ${php.treeSitterAvailable ? '✓ available' : '✗ not installed'}\n`;
1483
+ if (php.supportedFrameworks.length > 0) {
1484
+ output += `- **Supported Frameworks:** ${php.supportedFrameworks.join(', ')}\n`;
1485
+ }
1486
+ output += '\n';
1487
+ output += '### Capabilities\n\n';
1488
+ for (const [cap, enabled] of Object.entries(php.capabilities)) {
1489
+ const emoji = enabled ? '✓' : '✗';
1490
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1491
+ output += `- ${emoji} ${capName}\n`;
1492
+ }
1493
+ output += '\n';
1494
+ if (php.loadingError) {
1495
+ output += `> ⚠️ Loading error: ${php.loadingError}\n\n`;
1496
+ }
1497
+ }
1498
+ // Installation tips
1499
+ if ((info.python && !info.python.treeSitterAvailable) || (info.csharp && !info.csharp.treeSitterAvailable) || (info.java && !info.java.treeSitterAvailable) || (info.php && !info.php.treeSitterAvailable)) {
1500
+ output += '## Installation Tips\n\n';
1501
+ if (info.python && !info.python.treeSitterAvailable) {
1502
+ output += 'To enable full Python support (Pydantic, Django, nested types):\n';
1503
+ output += '```bash\npnpm add tree-sitter tree-sitter-python\n```\n\n';
1504
+ }
1505
+ if (info.csharp && !info.csharp.treeSitterAvailable) {
1506
+ output += 'To enable full C# support (ASP.NET, attributes, records):\n';
1507
+ output += '```bash\npnpm add tree-sitter tree-sitter-c-sharp\n```\n\n';
1508
+ }
1509
+ if (info.java && !info.java.treeSitterAvailable) {
1510
+ output += 'To enable full Java support (Spring Boot, annotations, records):\n';
1511
+ output += '```bash\npnpm add tree-sitter tree-sitter-java\n```\n\n';
1512
+ }
1513
+ if (info.php && !info.php.treeSitterAvailable) {
1514
+ output += 'To enable full PHP support (Laravel, Symfony, PHP 8 attributes):\n';
1515
+ output += '```bash\npnpm add tree-sitter tree-sitter-php\n```\n\n';
1516
+ }
1517
+ }
1518
+ return {
1519
+ content: [{ type: 'text', text: output }],
1520
+ };
1521
+ }
1522
+ // ============================================================================
1523
+ // DNA Handler Functions
1524
+ // ============================================================================
1525
+ /**
1526
+ * Handle drift_dna tool - Get styling DNA profile
1527
+ */
1528
+ async function handleDNA(_projectRoot, store, args) {
1529
+ await store.initialize();
1530
+ const profile = await store.load();
1531
+ if (!profile) {
1532
+ return {
1533
+ content: [{
1534
+ type: 'text',
1535
+ text: '# No DNA Profile Found\n\n' +
1536
+ 'Run `drift dna scan` to analyze your codebase styling patterns.\n\n' +
1537
+ 'The DNA profile captures how your codebase handles:\n' +
1538
+ '- Variant handling (primary/secondary buttons, sizes)\n' +
1539
+ '- Responsive design (mobile-first, breakpoints)\n' +
1540
+ '- State styling (hover, focus, disabled)\n' +
1541
+ '- Theming (dark mode, CSS variables)\n' +
1542
+ '- Spacing philosophy (design tokens, scales)\n' +
1543
+ '- Animation approach (transitions, motion)',
1544
+ }],
1545
+ };
1546
+ }
1547
+ const format = args.format ?? 'ai-context';
1548
+ // Filter by specific gene if requested
1549
+ if (args.gene && GENE_IDS.includes(args.gene)) {
1550
+ const gene = profile.genes[args.gene];
1551
+ const mutations = profile.mutations.filter(m => m.gene === args.gene);
1552
+ let output = `# Gene: ${gene.name}\n\n`;
1553
+ output += `${gene.description}\n\n`;
1554
+ output += `## Summary\n`;
1555
+ output += `- **Dominant Allele:** ${gene.dominant?.name ?? 'None established'}\n`;
1556
+ output += `- **Confidence:** ${Math.round(gene.confidence * 100)}%\n`;
1557
+ output += `- **Consistency:** ${Math.round(gene.consistency * 100)}%\n\n`;
1558
+ if (gene.alleles.length > 0) {
1559
+ output += `## Alleles Detected\n\n`;
1560
+ for (const allele of gene.alleles) {
1561
+ const marker = allele.isDominant ? ' ← DOMINANT' : '';
1562
+ output += `### ${allele.name}${marker}\n`;
1563
+ output += `- Frequency: ${Math.round(allele.frequency * 100)}% (${allele.fileCount} files)\n`;
1564
+ if (allele.examples.length > 0) {
1565
+ const ex = allele.examples[0];
1566
+ if (ex) {
1567
+ output += `- Example: \`${ex.file}:${ex.line}\`\n`;
1568
+ output += `\`\`\`\n${ex.code}\n\`\`\`\n`;
1569
+ }
1570
+ }
1571
+ output += '\n';
1572
+ }
1573
+ }
1574
+ if (gene.exemplars.length > 0) {
1575
+ output += `## Exemplar Files\n`;
1576
+ for (const f of gene.exemplars) {
1577
+ output += `- ${f}\n`;
1578
+ }
1579
+ output += '\n';
1580
+ }
1581
+ if (mutations.length > 0) {
1582
+ output += `## Mutations (${mutations.length})\n\n`;
1583
+ for (const m of mutations.slice(0, 5)) {
1584
+ output += `- **${m.file}:${m.line}** - ${m.actual} (expected: ${m.expected})\n`;
1585
+ }
1586
+ if (mutations.length > 5) {
1587
+ output += `- ... and ${mutations.length - 5} more\n`;
1588
+ }
1589
+ }
1590
+ return { content: [{ type: 'text', text: output }] };
1591
+ }
1592
+ // Full profile output
1593
+ if (format === 'full' || format === 'json') {
1594
+ return {
1595
+ content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }],
1596
+ };
1597
+ }
1598
+ if (format === 'summary') {
1599
+ let output = `# Styling DNA Summary\n\n`;
1600
+ output += `- **Health Score:** ${profile.summary.healthScore}/100\n`;
1601
+ output += `- **Framework:** ${profile.summary.dominantFramework}\n`;
1602
+ output += `- **Genetic Diversity:** ${profile.summary.geneticDiversity.toFixed(2)}\n`;
1603
+ output += `- **Components Analyzed:** ${profile.summary.totalComponentsAnalyzed}\n`;
1604
+ output += `- **Mutations:** ${profile.mutations.length}\n\n`;
1605
+ output += `## Genes\n\n`;
1606
+ output += `| Gene | Dominant | Confidence |\n`;
1607
+ output += `|------|----------|------------|\n`;
1608
+ for (const geneId of GENE_IDS) {
1609
+ const gene = profile.genes[geneId];
1610
+ output += `| ${gene.name} | ${gene.dominant?.name ?? 'None'} | ${Math.round(gene.confidence * 100)}% |\n`;
1611
+ }
1612
+ return { content: [{ type: 'text', text: output }] };
1613
+ }
1614
+ // AI context format (default)
1615
+ const level = (args.level ?? 3);
1616
+ const builder = new AIContextBuilder();
1617
+ const context = builder.build(profile, level);
1618
+ return { content: [{ type: 'text', text: context }] };
1619
+ }
1620
+ /**
1621
+ * Handle drift_playbook tool - Generate styling playbook
1622
+ */
1623
+ async function handlePlaybook(_projectRoot, store, args) {
1624
+ await store.initialize();
1625
+ const profile = await store.load();
1626
+ if (!profile) {
1627
+ return {
1628
+ content: [{
1629
+ type: 'text',
1630
+ text: '# No DNA Profile Found\n\n' +
1631
+ 'Run `drift dna scan` first to analyze your codebase styling patterns.',
1632
+ }],
1633
+ };
1634
+ }
1635
+ const generator = new PlaybookGenerator();
1636
+ const playbook = generator.generate(profile);
1637
+ // Filter to specific section if requested
1638
+ if (args.section && GENE_IDS.includes(args.section)) {
1639
+ const gene = profile.genes[args.section];
1640
+ const sectionRegex = new RegExp(`## ${gene.name}[\\s\\S]*?(?=\\n## |$)`, 'i');
1641
+ const match = playbook.match(sectionRegex);
1642
+ if (match) {
1643
+ return { content: [{ type: 'text', text: match[0] }] };
1644
+ }
1645
+ }
1646
+ return { content: [{ type: 'text', text: playbook }] };
1647
+ }
1648
+ /**
1649
+ * Handle drift_mutations tool - Get styling mutations
1650
+ */
1651
+ async function handleMutations(store, args) {
1652
+ await store.initialize();
1653
+ const profile = await store.load();
1654
+ if (!profile) {
1655
+ return {
1656
+ content: [{
1657
+ type: 'text',
1658
+ text: '# No DNA Profile Found\n\n' +
1659
+ 'Run `drift dna scan` first to analyze your codebase styling patterns.',
1660
+ }],
1661
+ };
1662
+ }
1663
+ let mutations = profile.mutations;
1664
+ // Filter by gene
1665
+ if (args.gene && GENE_IDS.includes(args.gene)) {
1666
+ mutations = mutations.filter(m => m.gene === args.gene);
1667
+ }
1668
+ // Filter by impact
1669
+ if (args.impact) {
1670
+ const validImpacts = ['low', 'medium', 'high'];
1671
+ if (validImpacts.includes(args.impact)) {
1672
+ mutations = mutations.filter(m => m.impact === args.impact);
1673
+ }
1674
+ }
1675
+ if (mutations.length === 0) {
1676
+ return {
1677
+ content: [{
1678
+ type: 'text',
1679
+ text: '# No Mutations Found\n\n' +
1680
+ '✓ Your codebase styling is consistent with the established DNA patterns.',
1681
+ }],
1682
+ };
1683
+ }
1684
+ // Group by impact
1685
+ const byImpact = { high: [], medium: [], low: [] };
1686
+ for (const m of mutations)
1687
+ byImpact[m.impact].push(m);
1688
+ let output = `# Styling Mutations (${mutations.length})\n\n`;
1689
+ output += 'Mutations are files that deviate from the established styling DNA.\n\n';
1690
+ if (byImpact.high.length > 0) {
1691
+ output += `## ⚠️ High Impact (${byImpact.high.length})\n\n`;
1692
+ for (const m of byImpact.high) {
1693
+ output += `### ${m.file}:${m.line}\n`;
1694
+ output += `- **Gene:** ${m.gene}\n`;
1695
+ output += `- **Found:** ${m.actual}\n`;
1696
+ output += `- **Expected:** ${m.expected}\n`;
1697
+ if (args.suggest && m.suggestion) {
1698
+ output += `- **Suggestion:** ${m.suggestion}\n`;
1699
+ }
1700
+ output += '\n';
1701
+ }
1702
+ }
1703
+ if (byImpact.medium.length > 0) {
1704
+ output += `## ⚡ Medium Impact (${byImpact.medium.length})\n\n`;
1705
+ for (const m of byImpact.medium.slice(0, 10)) {
1706
+ output += `- **${m.file}:${m.line}** - ${m.actual} → ${m.expected}\n`;
1707
+ if (args.suggest && m.suggestion) {
1708
+ output += ` - 💡 ${m.suggestion}\n`;
1709
+ }
1710
+ }
1711
+ if (byImpact.medium.length > 10) {
1712
+ output += `- ... and ${byImpact.medium.length - 10} more\n`;
1713
+ }
1714
+ output += '\n';
1715
+ }
1716
+ if (byImpact.low.length > 0) {
1717
+ output += `## Low Impact (${byImpact.low.length})\n\n`;
1718
+ for (const m of byImpact.low.slice(0, 5)) {
1719
+ output += `- ${m.file}:${m.line} - ${m.actual}\n`;
1720
+ }
1721
+ if (byImpact.low.length > 5) {
1722
+ output += `- ... and ${byImpact.low.length - 5} more\n`;
1723
+ }
1724
+ }
1725
+ return { content: [{ type: 'text', text: output }] };
1726
+ }
1727
+ /**
1728
+ * Handle drift_dna_check tool - Check code against DNA
1729
+ */
1730
+ async function handleDNACheck(projectRoot, store, args) {
1731
+ await store.initialize();
1732
+ const profile = await store.load();
1733
+ if (!profile) {
1734
+ return {
1735
+ content: [{
1736
+ type: 'text',
1737
+ text: '# No DNA Profile Found\n\n' +
1738
+ 'Run `drift dna scan` first to establish styling patterns.',
1739
+ }],
1740
+ };
1741
+ }
1742
+ if (!args.code && !args.file) {
1743
+ // Return DNA conventions for code generation
1744
+ const builder = new AIContextBuilder();
1745
+ const context = builder.build(profile, 3);
1746
+ return {
1747
+ content: [{
1748
+ type: 'text',
1749
+ text: '# Styling DNA Conventions\n\n' +
1750
+ 'Use these conventions when generating component code:\n\n' +
1751
+ context,
1752
+ }],
1753
+ };
1754
+ }
1755
+ // If file is provided, read it
1756
+ let code = args.code ?? '';
1757
+ if (args.file) {
1758
+ try {
1759
+ const fs = await import('node:fs/promises');
1760
+ const path = await import('node:path');
1761
+ code = await fs.readFile(path.join(projectRoot, args.file), 'utf-8');
1762
+ }
1763
+ catch {
1764
+ return {
1765
+ content: [{
1766
+ type: 'text',
1767
+ text: `Error: Could not read file "${args.file}"`,
1768
+ }],
1769
+ isError: true,
1770
+ };
1771
+ }
1772
+ }
1773
+ // Simple pattern matching check against DNA
1774
+ const issues = [];
1775
+ const genesToCheck = args.genes?.filter(g => GENE_IDS.includes(g)) ?? [...GENE_IDS];
1776
+ for (const geneId of genesToCheck) {
1777
+ const gene = profile.genes[geneId];
1778
+ if (!gene.dominant)
1779
+ continue;
1780
+ // Check for non-dominant alleles in the code
1781
+ for (const allele of gene.alleles) {
1782
+ if (allele.isDominant)
1783
+ continue;
1784
+ // Simple heuristic checks based on allele patterns
1785
+ const patterns = {
1786
+ 'styled-variants': [/styled\.\w+/, /\$\{props\s*=>/],
1787
+ 'inline-conditionals': [/className=\{[^}]*\?[^}]*\}/],
1788
+ 'tailwind-desktop-first': [/className="[^"]*\b(lg|xl|2xl):[^"]*\bsm:/],
1789
+ 'js-responsive': [/useMediaQuery|useBreakpoint/],
1790
+ 'hardcoded': [/style=\{\{[^}]*padding:\s*['"]?\d+px/],
1791
+ 'framer-motion': [/<motion\./],
1792
+ };
1793
+ const allelePatterns = patterns[allele.id];
1794
+ if (allelePatterns) {
1795
+ for (const pattern of allelePatterns) {
1796
+ if (pattern.test(code)) {
1797
+ issues.push({
1798
+ gene: gene.name,
1799
+ issue: `Found ${allele.name} pattern`,
1800
+ suggestion: `Consider using ${gene.dominant.name} instead`,
1801
+ });
1802
+ break;
1803
+ }
1804
+ }
1805
+ }
1806
+ }
1807
+ }
1808
+ if (issues.length === 0) {
1809
+ return {
1810
+ content: [{
1811
+ type: 'text',
1812
+ text: '# DNA Check: ✓ Passed\n\n' +
1813
+ 'The code appears to follow the established styling DNA patterns.',
1814
+ }],
1815
+ };
1816
+ }
1817
+ let output = `# DNA Check: ⚠️ ${issues.length} Issue(s) Found\n\n`;
1818
+ for (const issue of issues) {
1819
+ output += `## ${issue.gene}\n`;
1820
+ output += `- **Issue:** ${issue.issue}\n`;
1821
+ output += `- **Suggestion:** ${issue.suggestion}\n\n`;
1822
+ }
1823
+ return { content: [{ type: 'text', text: output }] };
1824
+ }
1825
+ /**
1826
+ * Handle drift_boundaries tool - Data access boundaries
1827
+ */
1828
+ async function handleBoundaries(store, args) {
1829
+ await store.initialize();
1830
+ const action = args.action ?? 'overview';
1831
+ const includeViolations = args.includeViolations ?? true;
1832
+ switch (action) {
1833
+ case 'overview': {
1834
+ const accessMap = store.getAccessMap();
1835
+ const sensitiveFields = store.getSensitiveAccess();
1836
+ let output = '# Data Boundaries Overview\n\n';
1837
+ output += `- **Tables:** ${accessMap.stats.totalTables}\n`;
1838
+ output += `- **Access Points:** ${accessMap.stats.totalAccessPoints}\n`;
1839
+ output += `- **Sensitive Fields:** ${accessMap.stats.totalSensitiveFields}\n`;
1840
+ output += `- **ORM Models:** ${accessMap.stats.totalModels}\n\n`;
1841
+ if (accessMap.stats.totalTables > 0) {
1842
+ output += '## Tables\n\n';
1843
+ const tableEntries = Object.entries(accessMap.tables)
1844
+ .map(([name, info]) => ({
1845
+ name,
1846
+ accessCount: info.accessedBy.length,
1847
+ hasSensitive: info.sensitiveFields.length > 0,
1848
+ }))
1849
+ .sort((a, b) => b.accessCount - a.accessCount)
1850
+ .slice(0, 10);
1851
+ for (const table of tableEntries) {
1852
+ const sensitive = table.hasSensitive ? ' ⚠️' : '';
1853
+ output += `- **${table.name}**${sensitive}: ${table.accessCount} access points\n`;
1854
+ }
1855
+ output += '\n';
1856
+ }
1857
+ if (sensitiveFields.length > 0) {
1858
+ output += '## Sensitive Fields\n\n';
1859
+ const fieldCounts = new Map();
1860
+ for (const field of sensitiveFields) {
1861
+ const key = field.table ? `${field.table}.${field.field}` : field.field;
1862
+ fieldCounts.set(key, (fieldCounts.get(key) ?? 0) + 1);
1863
+ }
1864
+ const sortedFields = Array.from(fieldCounts.entries())
1865
+ .sort((a, b) => b[1] - a[1])
1866
+ .slice(0, 10);
1867
+ for (const [fieldName, count] of sortedFields) {
1868
+ output += `- **${fieldName}**: ${count} locations\n`;
1869
+ }
1870
+ output += '\n';
1871
+ }
1872
+ if (includeViolations) {
1873
+ const rules = store.getRules();
1874
+ if (rules) {
1875
+ const violations = store.checkAllViolations();
1876
+ if (violations.length > 0) {
1877
+ output += `## ⚠️ Violations (${violations.length})\n\n`;
1878
+ for (const v of violations.slice(0, 5)) {
1879
+ output += `- **${v.file}:${v.line}** - ${v.message}\n`;
1880
+ }
1881
+ if (violations.length > 5) {
1882
+ output += `- ... and ${violations.length - 5} more\n`;
1883
+ }
1884
+ }
1885
+ else {
1886
+ output += '## ✓ No Violations\n\nAll data access follows configured boundaries.\n';
1887
+ }
1888
+ }
1889
+ else {
1890
+ output += '## No Rules Configured\n\n';
1891
+ output += 'Create `.drift/boundaries/rules.json` to enable boundary enforcement.\n';
1892
+ output += 'Run `drift boundaries init-rules` to generate a starter config.\n';
1893
+ }
1894
+ }
1895
+ return { content: [{ type: 'text', text: output }] };
1896
+ }
1897
+ case 'table': {
1898
+ if (!args.table) {
1899
+ return {
1900
+ content: [{ type: 'text', text: 'Error: table parameter required for "table" action' }],
1901
+ isError: true,
1902
+ };
1903
+ }
1904
+ const tableInfo = store.getTableAccess(args.table);
1905
+ if (!tableInfo) {
1906
+ return {
1907
+ content: [{ type: 'text', text: `Table '${args.table}' not found in access map.` }],
1908
+ };
1909
+ }
1910
+ let output = `# Table: ${args.table}\n\n`;
1911
+ if (tableInfo.model) {
1912
+ output += `**Model:** ${tableInfo.model}\n`;
1913
+ }
1914
+ output += `**Fields:** ${tableInfo.fields.join(', ') || 'none detected'}\n`;
1915
+ output += `**Access Points:** ${tableInfo.accessedBy.length}\n\n`;
1916
+ if (tableInfo.sensitiveFields.length > 0) {
1917
+ output += '## ⚠️ Sensitive Fields\n\n';
1918
+ for (const sf of tableInfo.sensitiveFields) {
1919
+ output += `- **${sf.field}** (${sf.sensitivityType})\n`;
1920
+ }
1921
+ output += '\n';
1922
+ }
1923
+ output += '## Access Points\n\n';
1924
+ const byFile = new Map();
1925
+ for (const ap of tableInfo.accessedBy) {
1926
+ if (!byFile.has(ap.file))
1927
+ byFile.set(ap.file, []);
1928
+ byFile.get(ap.file).push(ap);
1929
+ }
1930
+ for (const [file, accessPoints] of byFile) {
1931
+ output += `### ${file}\n`;
1932
+ for (const ap of accessPoints) {
1933
+ output += `- Line ${ap.line}: **${ap.operation}** ${ap.fields.join(', ')}\n`;
1934
+ }
1935
+ output += '\n';
1936
+ }
1937
+ return { content: [{ type: 'text', text: output }] };
1938
+ }
1939
+ case 'file': {
1940
+ if (!args.file) {
1941
+ return {
1942
+ content: [{ type: 'text', text: 'Error: file parameter required for "file" action' }],
1943
+ isError: true,
1944
+ };
1945
+ }
1946
+ const fileAccess = store.getFileAccess(args.file);
1947
+ let output = `# Data Access: ${args.file}\n\n`;
1948
+ if (fileAccess.length === 0) {
1949
+ output += 'No data access detected for this file/pattern.\n';
1950
+ }
1951
+ else {
1952
+ for (const fileInfo of fileAccess) {
1953
+ output += `## ${fileInfo.file}\n\n`;
1954
+ output += `**Tables:** ${fileInfo.tables.join(', ')}\n`;
1955
+ output += `**Access Points:** ${fileInfo.accessPoints.length}\n\n`;
1956
+ for (const ap of fileInfo.accessPoints) {
1957
+ output += `- Line ${ap.line}: **${ap.operation}** ${ap.table} ${ap.fields.join(', ')}\n`;
1958
+ }
1959
+ output += '\n';
1960
+ }
1961
+ }
1962
+ // Include boundary rules context if available
1963
+ const rules = store.getRules();
1964
+ if (rules && fileAccess.length > 0) {
1965
+ const allTables = new Set(fileAccess.flatMap(f => f.tables));
1966
+ const applicableRules = rules.boundaries.filter(b => b.tables?.some(t => allTables.has(t)) ||
1967
+ b.fields?.some(f => allTables.has(f.split('.')[0] || '')));
1968
+ if (applicableRules.length > 0) {
1969
+ output += '## Applicable Boundaries\n\n';
1970
+ for (const rule of applicableRules) {
1971
+ output += `- **${rule.id}**: ${rule.description}\n`;
1972
+ output += ` - Allowed: ${rule.allowedPaths.join(', ')}\n`;
1973
+ }
1974
+ }
1975
+ }
1976
+ return { content: [{ type: 'text', text: output }] };
1977
+ }
1978
+ case 'sensitive': {
1979
+ const sensitiveFields = store.getSensitiveAccess();
1980
+ let output = '# Sensitive Data Access\n\n';
1981
+ if (sensitiveFields.length === 0) {
1982
+ output += 'No sensitive fields detected.\n';
1983
+ return { content: [{ type: 'text', text: output }] };
1984
+ }
1985
+ // Group by sensitivity type
1986
+ const byType = new Map();
1987
+ for (const field of sensitiveFields) {
1988
+ const type = field.sensitivityType;
1989
+ if (!byType.has(type))
1990
+ byType.set(type, []);
1991
+ byType.get(type).push(field);
1992
+ }
1993
+ for (const [type, fields] of byType) {
1994
+ output += `## ${type.toUpperCase()} (${fields.length})\n\n`;
1995
+ for (const f of fields) {
1996
+ const fieldName = f.table ? `${f.table}.${f.field}` : f.field;
1997
+ output += `- **${fieldName}**\n`;
1998
+ output += ` - ${f.file}:${f.line}\n`;
1999
+ }
2000
+ output += '\n';
2001
+ }
2002
+ return { content: [{ type: 'text', text: output }] };
2003
+ }
2004
+ case 'check': {
2005
+ const rules = store.getRules();
2006
+ if (!rules) {
2007
+ return {
2008
+ content: [{
2009
+ type: 'text',
2010
+ text: '# No Boundary Rules Configured\n\n' +
2011
+ 'Create `.drift/boundaries/rules.json` to enable boundary enforcement.\n' +
2012
+ 'Run `drift boundaries init-rules` to generate a starter config.',
2013
+ }],
2014
+ };
2015
+ }
2016
+ const violations = store.checkAllViolations();
2017
+ if (violations.length === 0) {
2018
+ return {
2019
+ content: [{
2020
+ type: 'text',
2021
+ text: '# ✓ No Boundary Violations\n\n' +
2022
+ `Checked ${rules.boundaries.length} rules. All data access follows configured boundaries.`,
2023
+ }],
2024
+ };
2025
+ }
2026
+ let output = `# ⚠️ Boundary Violations (${violations.length})\n\n`;
2027
+ // Group by severity
2028
+ const errors = violations.filter(v => v.severity === 'error');
2029
+ const warnings = violations.filter(v => v.severity === 'warning');
2030
+ const infos = violations.filter(v => v.severity === 'info');
2031
+ if (errors.length > 0) {
2032
+ output += `## 🔴 Errors (${errors.length})\n\n`;
2033
+ for (const v of errors) {
2034
+ output += `### ${v.file}:${v.line}\n`;
2035
+ output += `- **Rule:** ${v.ruleId}\n`;
2036
+ output += `- **Message:** ${v.message}\n`;
2037
+ if (v.suggestion) {
2038
+ output += `- **Suggestion:** ${v.suggestion}\n`;
2039
+ }
2040
+ output += '\n';
2041
+ }
2042
+ }
2043
+ if (warnings.length > 0) {
2044
+ output += `## 🟡 Warnings (${warnings.length})\n\n`;
2045
+ for (const v of warnings) {
2046
+ output += `- **${v.file}:${v.line}** - ${v.message}\n`;
2047
+ }
2048
+ output += '\n';
2049
+ }
2050
+ if (infos.length > 0) {
2051
+ output += `## ℹ️ Info (${infos.length})\n\n`;
2052
+ for (const v of infos.slice(0, 5)) {
2053
+ output += `- ${v.file}:${v.line} - ${v.message}\n`;
2054
+ }
2055
+ if (infos.length > 5) {
2056
+ output += `- ... and ${infos.length - 5} more\n`;
2057
+ }
2058
+ }
2059
+ return { content: [{ type: 'text', text: output }] };
2060
+ }
2061
+ case 'rules': {
2062
+ const rules = store.getRules();
2063
+ if (!rules) {
2064
+ return {
2065
+ content: [{
2066
+ type: 'text',
2067
+ text: '# No Boundary Rules Configured\n\n' +
2068
+ 'Create `.drift/boundaries/rules.json` to define data access boundaries.\n\n' +
2069
+ '## Example Rules\n\n' +
2070
+ '```json\n' +
2071
+ '{\n' +
2072
+ ' "version": "1.0",\n' +
2073
+ ' "sensitivity": {\n' +
2074
+ ' "critical": ["users.password_hash", "users.ssn"],\n' +
2075
+ ' "sensitive": ["users.email", "users.phone"],\n' +
2076
+ ' "general": []\n' +
2077
+ ' },\n' +
2078
+ ' "boundaries": [\n' +
2079
+ ' {\n' +
2080
+ ' "id": "auth-owns-credentials",\n' +
2081
+ ' "description": "Only auth module can access credentials",\n' +
2082
+ ' "fields": ["users.password_hash"],\n' +
2083
+ ' "allowedPaths": ["**/auth/**"],\n' +
2084
+ ' "severity": "error"\n' +
2085
+ ' }\n' +
2086
+ ' ]\n' +
2087
+ '}\n' +
2088
+ '```',
2089
+ }],
2090
+ };
2091
+ }
2092
+ let output = '# Data Boundary Rules\n\n';
2093
+ output += '## Sensitivity Tiers\n\n';
2094
+ output += `- **Critical:** ${rules.sensitivity.critical.join(', ') || 'none'}\n`;
2095
+ output += `- **Sensitive:** ${rules.sensitivity.sensitive.join(', ') || 'none'}\n`;
2096
+ output += '\n';
2097
+ output += `## Boundaries (${rules.boundaries.length})\n\n`;
2098
+ for (const b of rules.boundaries) {
2099
+ const enabled = b.enabled !== false ? '✓' : '○';
2100
+ output += `### ${enabled} ${b.id}\n`;
2101
+ output += `${b.description}\n\n`;
2102
+ if (b.tables)
2103
+ output += `- **Tables:** ${b.tables.join(', ')}\n`;
2104
+ if (b.fields)
2105
+ output += `- **Fields:** ${b.fields.join(', ')}\n`;
2106
+ if (b.operations)
2107
+ output += `- **Operations:** ${b.operations.join(', ')}\n`;
2108
+ output += `- **Allowed:** ${b.allowedPaths.join(', ')}\n`;
2109
+ output += `- **Severity:** ${b.severity}\n\n`;
2110
+ }
2111
+ return { content: [{ type: 'text', text: output }] };
2112
+ }
2113
+ default:
2114
+ return {
2115
+ content: [{ type: 'text', text: `Unknown action: ${action}` }],
2116
+ isError: true,
2117
+ };
2118
+ }
2119
+ }
976
2120
  //# sourceMappingURL=server.js.map