@atlashub/smartstack-mcp 1.17.0 → 1.20.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/index.js CHANGED
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
82
82
  import path from "path";
83
83
  import { glob } from "glob";
84
84
  var FileSystemError = class extends Error {
85
- constructor(message, operation, path26, cause) {
85
+ constructor(message, operation, path27, cause) {
86
86
  super(message);
87
87
  this.operation = operation;
88
- this.path = path26;
88
+ this.path = path27;
89
89
  this.cause = cause;
90
90
  this.name = "FileSystemError";
91
91
  }
@@ -189,6 +189,288 @@ async function findFiles(pattern, options = {}) {
189
189
  return files;
190
190
  }
191
191
 
192
+ // src/types/index.ts
193
+ import { z } from "zod";
194
+ var ProjectConfigSchema = z.object({
195
+ projectType: z.enum(["client", "platform"]).default("platform"),
196
+ dbContext: z.enum(["core", "extensions"]).default("core"),
197
+ baseNamespace: z.string().optional(),
198
+ smartStackVersion: z.string().optional(),
199
+ initialized: z.string().optional()
200
+ });
201
+ var SmartStackConfigSchema = z.object({
202
+ projectPath: z.string(),
203
+ apiUrl: z.string().url().optional(),
204
+ apiEnabled: z.boolean().default(true)
205
+ });
206
+ var ConventionsConfigSchema = z.object({
207
+ schemas: z.object({
208
+ platform: z.string().default("core"),
209
+ extensions: z.string().default("extensions")
210
+ }),
211
+ tablePrefixes: z.array(z.string()).default([
212
+ "auth_",
213
+ "nav_",
214
+ "usr_",
215
+ "ai_",
216
+ "cfg_",
217
+ "wkf_",
218
+ "support_",
219
+ "entra_",
220
+ "ref_",
221
+ "loc_",
222
+ "lic_"
223
+ ]),
224
+ scopeTypes: z.array(z.string()).default(["Core", "Extension", "Partner", "Community"]),
225
+ migrationFormat: z.string().default("{context}_v{version}_{sequence}_{Description}"),
226
+ namespaces: z.object({
227
+ domain: z.string(),
228
+ application: z.string(),
229
+ infrastructure: z.string(),
230
+ api: z.string()
231
+ }),
232
+ servicePattern: z.object({
233
+ interface: z.string().default("I{Name}Service"),
234
+ implementation: z.string().default("{Name}Service")
235
+ })
236
+ });
237
+ var EfCoreContextSchema = z.object({
238
+ name: z.string(),
239
+ projectPath: z.string(),
240
+ migrationsFolder: z.string().default("Migrations")
241
+ });
242
+ var EfCoreConfigSchema = z.object({
243
+ contexts: z.array(EfCoreContextSchema),
244
+ validation: z.object({
245
+ checkModelSnapshot: z.boolean().default(true),
246
+ checkMigrationOrder: z.boolean().default(true),
247
+ requireBuildSuccess: z.boolean().default(true)
248
+ })
249
+ });
250
+ var ScaffoldingConfigSchema = z.object({
251
+ outputPath: z.string(),
252
+ templates: z.object({
253
+ service: z.string(),
254
+ entity: z.string(),
255
+ controller: z.string(),
256
+ component: z.string()
257
+ })
258
+ });
259
+ var ConfigSchema = z.object({
260
+ version: z.string(),
261
+ smartstack: SmartStackConfigSchema,
262
+ conventions: ConventionsConfigSchema,
263
+ efcore: EfCoreConfigSchema,
264
+ scaffolding: ScaffoldingConfigSchema,
265
+ // Loaded from .smartstack/config.json if present
266
+ projectConfig: ProjectConfigSchema.optional(),
267
+ // Resolved DbContext to use (from projectConfig or default)
268
+ defaultDbContext: z.enum(["core", "extensions"]).default("core")
269
+ });
270
+ var ValidateConventionsInputSchema = z.object({
271
+ path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
272
+ checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"])).default(["all"]).describe("Types of checks to perform")
273
+ });
274
+ var CheckMigrationsInputSchema = z.object({
275
+ projectPath: z.string().optional().describe("EF Core project path"),
276
+ branch: z.string().optional().describe("Git branch to check (default: current)"),
277
+ compareBranch: z.string().optional().describe("Branch to compare against")
278
+ });
279
+ var EntityPropertySchema = z.object({
280
+ name: z.string().describe("Property name (PascalCase)"),
281
+ type: z.string().describe("C# type (string, int, Guid, DateTime, etc.)"),
282
+ required: z.boolean().optional().describe("If true, property is required"),
283
+ maxLength: z.number().optional().describe("Max length for string properties")
284
+ });
285
+ var ScaffoldExtensionInputSchema = z.object({
286
+ type: z.enum(["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"]).describe('Type of extension to scaffold. Use "feature" for full-stack generation.'),
287
+ name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
288
+ options: z.object({
289
+ namespace: z.string().optional().describe("Custom namespace"),
290
+ baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
291
+ methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
292
+ outputPath: z.string().optional().describe("Custom output path"),
293
+ isSystemEntity: z.boolean().optional().describe("If true, creates a system entity without TenantId"),
294
+ tablePrefix: z.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
295
+ schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
296
+ dryRun: z.boolean().optional().describe("If true, preview generated code without writing files"),
297
+ skipService: z.boolean().optional().describe("For feature type: skip service generation"),
298
+ skipController: z.boolean().optional().describe("For feature type: skip controller generation"),
299
+ skipComponent: z.boolean().optional().describe("For feature type: skip React component generation"),
300
+ clientExtension: z.boolean().optional().describe("If true, use extensions schema for client-specific code"),
301
+ withTests: z.boolean().optional().describe("For feature type: also generate unit tests"),
302
+ withDtos: z.boolean().optional().describe("For feature type: generate DTOs (Create, Update, Response)"),
303
+ withValidation: z.boolean().optional().describe("For feature type: generate FluentValidation validators"),
304
+ withRepository: z.boolean().optional().describe("For feature type: generate repository pattern"),
305
+ entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
306
+ navRoute: z.string().optional().describe('Navigation route path for controller (e.g., "platform.administration.users"). Required for controllers.'),
307
+ navRouteSuffix: z.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'),
308
+ withHierarchyFunction: z.boolean().optional().describe("For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"),
309
+ hierarchyDirection: z.enum(["ancestors", "descendants", "both"]).optional().describe("Direction for hierarchy traversal function (default: both)")
310
+ }).optional()
311
+ });
312
+ var ApiDocsInputSchema = z.object({
313
+ endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
314
+ format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
315
+ controller: z.string().optional().describe("Filter by controller name")
316
+ });
317
+ var SuggestMigrationInputSchema = z.object({
318
+ description: z.string().describe('Description of what the migration does (e.g., "Add User Profiles", "Create Orders Table")'),
319
+ context: z.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
320
+ version: z.string().optional().describe('Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.')
321
+ });
322
+ var GeneratePermissionsInputSchema = z.object({
323
+ navRoute: z.string().optional().describe('NavRoute path (e.g., "platform.administration.entra"). If not provided, scans all controllers.'),
324
+ actions: z.array(z.string()).optional().describe("Custom actions to generate (default: read, create, update, delete)"),
325
+ includeStandardActions: z.boolean().default(true).describe("Include standard CRUD actions (read, create, update, delete)"),
326
+ generateMigration: z.boolean().default(true).describe("Generate EF Core migration to seed permissions in database"),
327
+ dryRun: z.boolean().default(false).describe("Preview without writing files or creating migration")
328
+ });
329
+ var TestTypeSchema = z.enum(["unit", "integration", "security", "e2e"]);
330
+ var TestTargetSchema = z.enum(["entity", "service", "controller", "validator", "repository", "all"]);
331
+ var ScaffoldTestsInputSchema = z.object({
332
+ target: TestTargetSchema.describe("Type of component to test"),
333
+ name: z.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
334
+ testTypes: z.array(TestTypeSchema).default(["unit"]).describe("Types of tests to generate"),
335
+ options: z.object({
336
+ includeEdgeCases: z.boolean().default(true).describe("Include edge case tests"),
337
+ includeTenantIsolation: z.boolean().default(true).describe("Include tenant isolation tests"),
338
+ includeSoftDelete: z.boolean().default(true).describe("Include soft delete tests"),
339
+ includeAudit: z.boolean().default(true).describe("Include audit trail tests"),
340
+ includeValidation: z.boolean().default(true).describe("Include validation tests"),
341
+ includeAuthorization: z.boolean().default(false).describe("Include authorization tests"),
342
+ includePerformance: z.boolean().default(false).describe("Include performance tests"),
343
+ entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
344
+ isSystemEntity: z.boolean().default(false).describe("If true, entity has no TenantId"),
345
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
346
+ }).optional()
347
+ });
348
+ var AnalyzeTestCoverageInputSchema = z.object({
349
+ path: z.string().optional().describe("Project path to analyze"),
350
+ scope: z.enum(["entity", "service", "controller", "all"]).default("all").describe("Scope of analysis"),
351
+ outputFormat: z.enum(["summary", "detailed", "json"]).default("summary").describe("Output format"),
352
+ includeRecommendations: z.boolean().default(true).describe("Include recommendations for missing tests")
353
+ });
354
+ var ValidateTestConventionsInputSchema = z.object({
355
+ path: z.string().optional().describe("Project path to validate"),
356
+ checks: z.array(z.enum(["naming", "structure", "patterns", "assertions", "mocking", "all"])).default(["all"]).describe("Types of convention checks to perform"),
357
+ autoFix: z.boolean().default(false).describe("Automatically fix minor issues")
358
+ });
359
+ var SuggestTestScenariosInputSchema = z.object({
360
+ target: z.enum(["entity", "service", "controller", "file"]).describe("Type of target to analyze"),
361
+ name: z.string().min(1).describe("Component name or file path"),
362
+ depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
363
+ });
364
+ var SecurityCheckSchema = z.enum([
365
+ "hardcoded-secrets",
366
+ "sql-injection",
367
+ "tenant-isolation",
368
+ "authorization",
369
+ "dangerous-functions",
370
+ "input-validation",
371
+ "xss",
372
+ "csrf",
373
+ "logging-sensitive",
374
+ "all"
375
+ ]);
376
+ var ValidateSecurityInputSchema = z.object({
377
+ path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
378
+ checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
379
+ severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
380
+ });
381
+ var QualityMetricSchema = z.enum([
382
+ "cognitive-complexity",
383
+ "cyclomatic-complexity",
384
+ "function-size",
385
+ "nesting-depth",
386
+ "parameter-count",
387
+ "code-duplication",
388
+ "file-size",
389
+ "all"
390
+ ]);
391
+ var AnalyzeCodeQualityInputSchema = z.object({
392
+ path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
393
+ metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
394
+ threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
395
+ scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
396
+ });
397
+ var ScaffoldApiClientInputSchema = z.object({
398
+ path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
399
+ navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
400
+ name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
401
+ methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
402
+ options: z.object({
403
+ outputPath: z.string().optional().describe("Custom output path for generated files"),
404
+ includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
405
+ includeHook: z.boolean().default(true).describe("Generate React Query hook"),
406
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
407
+ }).optional()
408
+ });
409
+ var ScaffoldRoutesInputSchema = z.object({
410
+ path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
411
+ source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
412
+ scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
413
+ options: z.object({
414
+ outputPath: z.string().optional().describe("Custom output path"),
415
+ includeLayouts: z.boolean().default(true).describe("Generate layout components"),
416
+ includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
417
+ generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
418
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
419
+ }).optional()
420
+ });
421
+ var ValidateFrontendRoutesInputSchema = z.object({
422
+ path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
423
+ scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
424
+ options: z.object({
425
+ fix: z.boolean().default(false).describe("Auto-fix minor issues"),
426
+ strict: z.boolean().default(false).describe("Fail on warnings")
427
+ }).optional()
428
+ });
429
+ var SlotDefinitionSchema = z.object({
430
+ name: z.string().describe('Slot name (e.g., "users.form.fields.after")'),
431
+ location: z.string().describe("Where the slot is located in the page"),
432
+ contextProps: z.array(z.string()).optional().describe("Props passed to slot content")
433
+ });
434
+ var ScaffoldFrontendExtensionInputSchema = z.object({
435
+ type: z.enum(["infrastructure", "page-slots", "extension-example", "all"]).describe("Type of extension to scaffold: infrastructure (types, contexts), page-slots (add slots to a page), extension-example (client usage example), all"),
436
+ target: z.string().optional().describe('Target page or component (e.g., "UsersPage", "UserDetailPage")'),
437
+ options: z.object({
438
+ slots: z.array(SlotDefinitionSchema).optional().describe("Slot definitions to add"),
439
+ outputPath: z.string().optional().describe("Custom output path"),
440
+ dryRun: z.boolean().default(false).describe("Preview without writing files"),
441
+ overwrite: z.boolean().default(false).describe("Overwrite existing files")
442
+ }).optional()
443
+ });
444
+ var AnalyzeExtensionPointsInputSchema = z.object({
445
+ path: z.string().optional().describe("Path to SmartStack web project"),
446
+ target: z.enum(["pages", "components", "forms", "tables", "all"]).default("all").describe("Type of components to analyze"),
447
+ filter: z.string().optional().describe('Filter by file name pattern (e.g., "User*")')
448
+ });
449
+ var AnalyzeHierarchyPatternsInputSchema = z.object({
450
+ path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
451
+ entityFilter: z.string().optional().describe('Filter entities by name pattern (e.g., "User*", "*Group*")'),
452
+ includeRecommendations: z.boolean().default(true).describe("Include pattern recommendations for each hierarchy"),
453
+ outputFormat: z.enum(["summary", "detailed", "json"]).default("detailed").describe("Output format")
454
+ });
455
+ var ReviewCodeCheckSchema = z.enum([
456
+ "security",
457
+ "architecture",
458
+ "hardcoded-values",
459
+ "tests",
460
+ "ai-hallucinations",
461
+ "performance",
462
+ "dead-code",
463
+ "i18n",
464
+ "accessibility",
465
+ "all"
466
+ ]);
467
+ var ReviewCodeInputSchema = z.object({
468
+ path: z.string().optional().describe("Project path to review (default: SmartStack.app path)"),
469
+ scope: z.enum(["all", "changed", "staged"]).default("all").describe("Files to review: all, only changed (git diff), or staged files"),
470
+ checks: z.array(ReviewCodeCheckSchema).default(["all"]).describe("Categories of checks to run"),
471
+ severity: z.enum(["blocking", "critical", "warning", "all"]).default("all").describe("Filter results by minimum severity")
472
+ });
473
+
192
474
  // src/utils/dotnet.ts
193
475
  import { exec } from "child_process";
194
476
  import { promisify } from "util";
@@ -373,7 +655,11 @@ var defaultConfig = {
373
655
  controller: "templates/controller.cs.hbs",
374
656
  component: "templates/component.tsx.hbs"
375
657
  }
376
- }
658
+ },
659
+ // Will be loaded from .smartstack/config.json if present
660
+ projectConfig: void 0,
661
+ // Default to 'core', will be overridden by projectConfig.dbContext if present
662
+ defaultDbContext: "core"
377
663
  };
378
664
  var cachedConfig = null;
379
665
  function resolveProjectPath(configPath) {
@@ -408,12 +694,33 @@ async function getConfig() {
408
694
  cachedConfig.smartstack.projectPath = resolveProjectPath(
409
695
  cachedConfig.smartstack.projectPath
410
696
  );
411
- const namespacesEmpty = !cachedConfig.conventions.namespaces.domain || !cachedConfig.conventions.namespaces.application || !cachedConfig.conventions.namespaces.infrastructure || !cachedConfig.conventions.namespaces.api;
412
- if (namespacesEmpty && cachedConfig.smartstack.projectPath) {
697
+ const smartstackConfigPath = path3.join(
698
+ cachedConfig.smartstack.projectPath,
699
+ ".smartstack",
700
+ "config.json"
701
+ );
702
+ if (await fileExists(smartstackConfigPath)) {
413
703
  try {
414
- const csprojFiles = await findCsprojFiles(cachedConfig.smartstack.projectPath);
415
- const detected = await detectNamespaces(csprojFiles);
416
- if (detected) {
704
+ const projectConfigRaw = await readJson(smartstackConfigPath);
705
+ const projectConfig = ProjectConfigSchema.parse(projectConfigRaw);
706
+ cachedConfig.projectConfig = projectConfig;
707
+ cachedConfig.defaultDbContext = projectConfig.dbContext;
708
+ logger.info("Project config loaded from .smartstack/config.json", {
709
+ projectType: projectConfig.projectType,
710
+ dbContext: projectConfig.dbContext
711
+ });
712
+ } catch (error) {
713
+ logger.warn("Failed to load .smartstack/config.json, using defaults", { error });
714
+ }
715
+ } else {
716
+ logger.debug("No .smartstack/config.json found, assuming platform project (core context)");
717
+ }
718
+ const namespacesEmpty = !cachedConfig.conventions.namespaces.domain || !cachedConfig.conventions.namespaces.application || !cachedConfig.conventions.namespaces.infrastructure || !cachedConfig.conventions.namespaces.api;
719
+ if (namespacesEmpty && cachedConfig.smartstack.projectPath) {
720
+ try {
721
+ const csprojFiles = await findCsprojFiles(cachedConfig.smartstack.projectPath);
722
+ const detected = await detectNamespaces(csprojFiles);
723
+ if (detected) {
417
724
  if (!cachedConfig.conventions.namespaces.domain) {
418
725
  cachedConfig.conventions.namespaces.domain = detected.domain;
419
726
  }
@@ -489,251 +796,6 @@ function mergeConfig(base, override) {
489
796
  };
490
797
  }
491
798
 
492
- // src/types/index.ts
493
- import { z } from "zod";
494
- var SmartStackConfigSchema = z.object({
495
- projectPath: z.string(),
496
- apiUrl: z.string().url().optional(),
497
- apiEnabled: z.boolean().default(true)
498
- });
499
- var ConventionsConfigSchema = z.object({
500
- schemas: z.object({
501
- platform: z.string().default("core"),
502
- extensions: z.string().default("extensions")
503
- }),
504
- tablePrefixes: z.array(z.string()).default([
505
- "auth_",
506
- "nav_",
507
- "usr_",
508
- "ai_",
509
- "cfg_",
510
- "wkf_",
511
- "support_",
512
- "entra_",
513
- "ref_",
514
- "loc_",
515
- "lic_"
516
- ]),
517
- scopeTypes: z.array(z.string()).default(["Core", "Extension", "Partner", "Community"]),
518
- migrationFormat: z.string().default("{context}_v{version}_{sequence}_{Description}"),
519
- namespaces: z.object({
520
- domain: z.string(),
521
- application: z.string(),
522
- infrastructure: z.string(),
523
- api: z.string()
524
- }),
525
- servicePattern: z.object({
526
- interface: z.string().default("I{Name}Service"),
527
- implementation: z.string().default("{Name}Service")
528
- })
529
- });
530
- var EfCoreContextSchema = z.object({
531
- name: z.string(),
532
- projectPath: z.string(),
533
- migrationsFolder: z.string().default("Migrations")
534
- });
535
- var EfCoreConfigSchema = z.object({
536
- contexts: z.array(EfCoreContextSchema),
537
- validation: z.object({
538
- checkModelSnapshot: z.boolean().default(true),
539
- checkMigrationOrder: z.boolean().default(true),
540
- requireBuildSuccess: z.boolean().default(true)
541
- })
542
- });
543
- var ScaffoldingConfigSchema = z.object({
544
- outputPath: z.string(),
545
- templates: z.object({
546
- service: z.string(),
547
- entity: z.string(),
548
- controller: z.string(),
549
- component: z.string()
550
- })
551
- });
552
- var ConfigSchema = z.object({
553
- version: z.string(),
554
- smartstack: SmartStackConfigSchema,
555
- conventions: ConventionsConfigSchema,
556
- efcore: EfCoreConfigSchema,
557
- scaffolding: ScaffoldingConfigSchema
558
- });
559
- var ValidateConventionsInputSchema = z.object({
560
- path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
561
- checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"])).default(["all"]).describe("Types of checks to perform")
562
- });
563
- var CheckMigrationsInputSchema = z.object({
564
- projectPath: z.string().optional().describe("EF Core project path"),
565
- branch: z.string().optional().describe("Git branch to check (default: current)"),
566
- compareBranch: z.string().optional().describe("Branch to compare against")
567
- });
568
- var EntityPropertySchema = z.object({
569
- name: z.string().describe("Property name (PascalCase)"),
570
- type: z.string().describe("C# type (string, int, Guid, DateTime, etc.)"),
571
- required: z.boolean().optional().describe("If true, property is required"),
572
- maxLength: z.number().optional().describe("Max length for string properties")
573
- });
574
- var ScaffoldExtensionInputSchema = z.object({
575
- type: z.enum(["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"]).describe('Type of extension to scaffold. Use "feature" for full-stack generation.'),
576
- name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
577
- options: z.object({
578
- namespace: z.string().optional().describe("Custom namespace"),
579
- baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
580
- methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
581
- outputPath: z.string().optional().describe("Custom output path"),
582
- isSystemEntity: z.boolean().optional().describe("If true, creates a system entity without TenantId"),
583
- tablePrefix: z.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
584
- schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
585
- dryRun: z.boolean().optional().describe("If true, preview generated code without writing files"),
586
- skipService: z.boolean().optional().describe("For feature type: skip service generation"),
587
- skipController: z.boolean().optional().describe("For feature type: skip controller generation"),
588
- skipComponent: z.boolean().optional().describe("For feature type: skip React component generation"),
589
- clientExtension: z.boolean().optional().describe("If true, use extensions schema for client-specific code"),
590
- withTests: z.boolean().optional().describe("For feature type: also generate unit tests"),
591
- withDtos: z.boolean().optional().describe("For feature type: generate DTOs (Create, Update, Response)"),
592
- withValidation: z.boolean().optional().describe("For feature type: generate FluentValidation validators"),
593
- withRepository: z.boolean().optional().describe("For feature type: generate repository pattern"),
594
- entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
595
- navRoute: z.string().optional().describe('Navigation route path for controller (e.g., "platform.administration.users"). Required for controllers.'),
596
- navRouteSuffix: z.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)')
597
- }).optional()
598
- });
599
- var ApiDocsInputSchema = z.object({
600
- endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
601
- format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
602
- controller: z.string().optional().describe("Filter by controller name")
603
- });
604
- var SuggestMigrationInputSchema = z.object({
605
- description: z.string().describe('Description of what the migration does (e.g., "Add User Profiles", "Create Orders Table")'),
606
- context: z.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
607
- version: z.string().optional().describe('Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.')
608
- });
609
- var GeneratePermissionsInputSchema = z.object({
610
- navRoute: z.string().optional().describe('NavRoute path (e.g., "platform.administration.entra"). If not provided, scans all controllers.'),
611
- actions: z.array(z.string()).optional().describe("Custom actions to generate (default: read, create, update, delete)"),
612
- includeStandardActions: z.boolean().default(true).describe("Include standard CRUD actions (read, create, update, delete)"),
613
- generateMigration: z.boolean().default(true).describe("Generate EF Core migration to seed permissions in database"),
614
- dryRun: z.boolean().default(false).describe("Preview without writing files or creating migration")
615
- });
616
- var TestTypeSchema = z.enum(["unit", "integration", "security", "e2e"]);
617
- var TestTargetSchema = z.enum(["entity", "service", "controller", "validator", "repository", "all"]);
618
- var ScaffoldTestsInputSchema = z.object({
619
- target: TestTargetSchema.describe("Type of component to test"),
620
- name: z.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
621
- testTypes: z.array(TestTypeSchema).default(["unit"]).describe("Types of tests to generate"),
622
- options: z.object({
623
- includeEdgeCases: z.boolean().default(true).describe("Include edge case tests"),
624
- includeTenantIsolation: z.boolean().default(true).describe("Include tenant isolation tests"),
625
- includeSoftDelete: z.boolean().default(true).describe("Include soft delete tests"),
626
- includeAudit: z.boolean().default(true).describe("Include audit trail tests"),
627
- includeValidation: z.boolean().default(true).describe("Include validation tests"),
628
- includeAuthorization: z.boolean().default(false).describe("Include authorization tests"),
629
- includePerformance: z.boolean().default(false).describe("Include performance tests"),
630
- entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
631
- isSystemEntity: z.boolean().default(false).describe("If true, entity has no TenantId"),
632
- dryRun: z.boolean().default(false).describe("Preview without writing files")
633
- }).optional()
634
- });
635
- var AnalyzeTestCoverageInputSchema = z.object({
636
- path: z.string().optional().describe("Project path to analyze"),
637
- scope: z.enum(["entity", "service", "controller", "all"]).default("all").describe("Scope of analysis"),
638
- outputFormat: z.enum(["summary", "detailed", "json"]).default("summary").describe("Output format"),
639
- includeRecommendations: z.boolean().default(true).describe("Include recommendations for missing tests")
640
- });
641
- var ValidateTestConventionsInputSchema = z.object({
642
- path: z.string().optional().describe("Project path to validate"),
643
- checks: z.array(z.enum(["naming", "structure", "patterns", "assertions", "mocking", "all"])).default(["all"]).describe("Types of convention checks to perform"),
644
- autoFix: z.boolean().default(false).describe("Automatically fix minor issues")
645
- });
646
- var SuggestTestScenariosInputSchema = z.object({
647
- target: z.enum(["entity", "service", "controller", "file"]).describe("Type of target to analyze"),
648
- name: z.string().min(1).describe("Component name or file path"),
649
- depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
650
- });
651
- var SecurityCheckSchema = z.enum([
652
- "hardcoded-secrets",
653
- "sql-injection",
654
- "tenant-isolation",
655
- "authorization",
656
- "dangerous-functions",
657
- "input-validation",
658
- "xss",
659
- "csrf",
660
- "logging-sensitive",
661
- "all"
662
- ]);
663
- var ValidateSecurityInputSchema = z.object({
664
- path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
665
- checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
666
- severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
667
- });
668
- var QualityMetricSchema = z.enum([
669
- "cognitive-complexity",
670
- "cyclomatic-complexity",
671
- "function-size",
672
- "nesting-depth",
673
- "parameter-count",
674
- "code-duplication",
675
- "file-size",
676
- "all"
677
- ]);
678
- var AnalyzeCodeQualityInputSchema = z.object({
679
- path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
680
- metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
681
- threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
682
- scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
683
- });
684
- var ScaffoldApiClientInputSchema = z.object({
685
- path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
686
- navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
687
- name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
688
- methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
689
- options: z.object({
690
- outputPath: z.string().optional().describe("Custom output path for generated files"),
691
- includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
692
- includeHook: z.boolean().default(true).describe("Generate React Query hook"),
693
- dryRun: z.boolean().default(false).describe("Preview without writing files")
694
- }).optional()
695
- });
696
- var ScaffoldRoutesInputSchema = z.object({
697
- path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
698
- source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
699
- scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
700
- options: z.object({
701
- outputPath: z.string().optional().describe("Custom output path"),
702
- includeLayouts: z.boolean().default(true).describe("Generate layout components"),
703
- includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
704
- generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
705
- dryRun: z.boolean().default(false).describe("Preview without writing files")
706
- }).optional()
707
- });
708
- var ValidateFrontendRoutesInputSchema = z.object({
709
- path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
710
- scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
711
- options: z.object({
712
- fix: z.boolean().default(false).describe("Auto-fix minor issues"),
713
- strict: z.boolean().default(false).describe("Fail on warnings")
714
- }).optional()
715
- });
716
- var SlotDefinitionSchema = z.object({
717
- name: z.string().describe('Slot name (e.g., "users.form.fields.after")'),
718
- location: z.string().describe("Where the slot is located in the page"),
719
- contextProps: z.array(z.string()).optional().describe("Props passed to slot content")
720
- });
721
- var ScaffoldFrontendExtensionInputSchema = z.object({
722
- type: z.enum(["infrastructure", "page-slots", "extension-example", "all"]).describe("Type of extension to scaffold: infrastructure (types, contexts), page-slots (add slots to a page), extension-example (client usage example), all"),
723
- target: z.string().optional().describe('Target page or component (e.g., "UsersPage", "UserDetailPage")'),
724
- options: z.object({
725
- slots: z.array(SlotDefinitionSchema).optional().describe("Slot definitions to add"),
726
- outputPath: z.string().optional().describe("Custom output path"),
727
- dryRun: z.boolean().default(false).describe("Preview without writing files"),
728
- overwrite: z.boolean().default(false).describe("Overwrite existing files")
729
- }).optional()
730
- });
731
- var AnalyzeExtensionPointsInputSchema = z.object({
732
- path: z.string().optional().describe("Path to SmartStack web project"),
733
- target: z.enum(["pages", "components", "forms", "tables", "all"]).default("all").describe("Type of components to analyze"),
734
- filter: z.string().optional().describe('Filter by file name pattern (e.g., "User*")')
735
- });
736
-
737
799
  // src/lib/detector.ts
738
800
  import path5 from "path";
739
801
 
@@ -778,10 +840,19 @@ async function getCurrentBranch(cwd) {
778
840
  return null;
779
841
  }
780
842
  }
781
- async function branchExists(branch, cwd) {
843
+ async function getChangedFiles(cwd) {
782
844
  try {
783
- await git(`rev-parse --verify ${branch}`, cwd);
784
- return true;
845
+ const status = await git("status --porcelain", cwd);
846
+ if (!status) return [];
847
+ return status.split("\n").map((line) => line.substring(3).trim()).filter(Boolean);
848
+ } catch {
849
+ return [];
850
+ }
851
+ }
852
+ async function branchExists(branch, cwd) {
853
+ try {
854
+ await git(`rev-parse --verify ${branch}`, cwd);
855
+ return true;
785
856
  } catch {
786
857
  return false;
787
858
  }
@@ -801,6 +872,15 @@ async function getDiff(fromBranch, toBranch, filePath, cwd) {
801
872
  return "";
802
873
  }
803
874
  }
875
+ async function getStagedFiles(cwd) {
876
+ try {
877
+ const status = await git("diff --cached --name-only", cwd);
878
+ if (!status) return [];
879
+ return status.split("\n").filter(Boolean);
880
+ } catch {
881
+ return [];
882
+ }
883
+ }
804
884
 
805
885
  // src/lib/detector.ts
806
886
  import { glob as glob2 } from "glob";
@@ -947,7 +1027,7 @@ var validateConventionsTool = {
947
1027
  type: "array",
948
1028
  items: {
949
1029
  type: "string",
950
- enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"]
1030
+ enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"]
951
1031
  },
952
1032
  description: "Types of checks to perform",
953
1033
  default: ["all"]
@@ -958,7 +1038,7 @@ var validateConventionsTool = {
958
1038
  async function handleValidateConventions(args, config) {
959
1039
  const input = ValidateConventionsInputSchema.parse(args);
960
1040
  const projectPath = input.path || config.smartstack.projectPath;
961
- const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs"] : input.checks;
1041
+ const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions"] : input.checks;
962
1042
  logger.info("Validating conventions", { projectPath, checks });
963
1043
  const result = {
964
1044
  valid: true,
@@ -994,6 +1074,12 @@ async function handleValidateConventions(args, config) {
994
1074
  if (checks.includes("tabs")) {
995
1075
  await validateTabs(structure, config, result);
996
1076
  }
1077
+ if (checks.includes("hierarchies")) {
1078
+ await validateHierarchies(structure, config, result);
1079
+ }
1080
+ if (checks.includes("protected-actions")) {
1081
+ await validateProtectedActions(structure, config, result);
1082
+ }
997
1083
  result.valid = result.errors.length === 0;
998
1084
  result.summary = generateSummary(result, checks);
999
1085
  return formatResult(result);
@@ -1511,6 +1597,7 @@ async function validateTabs(structure, _config, result) {
1511
1597
  const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
1512
1598
  let tabPagesCount = 0;
1513
1599
  let hookUsageCount = 0;
1600
+ let lazyLoadingCount = 0;
1514
1601
  for (const file of pageFiles) {
1515
1602
  const content = await readText(file);
1516
1603
  const fileName = path6.basename(file);
@@ -1540,14 +1627,326 @@ async function validateTabs(structure, _config, result) {
1540
1627
  suggestion: "Use useTabNavigation hook to sync tab state with URL: const { activeTab, setActiveTab } = useTabNavigation(defaultTab, VALID_TABS)"
1541
1628
  });
1542
1629
  }
1630
+ const lazyLoadingPatterns = detectTabLazyLoading(content);
1631
+ if (lazyLoadingPatterns.hasLazyLoading) {
1632
+ lazyLoadingCount++;
1633
+ } else if (lazyLoadingPatterns.hasDataFetching) {
1634
+ result.warnings.push({
1635
+ type: "warning",
1636
+ category: "tabs",
1637
+ message: `Page "${fileName}" has tabs with data fetching but no lazy loading pattern detected`,
1638
+ file: path6.relative(structure.root, file),
1639
+ suggestion: 'Use lazy loading for tab data: useQuery([...], fetchFn, { enabled: activeTab === "tabName" }) or React.lazy() for tab components'
1640
+ });
1641
+ }
1543
1642
  }
1544
1643
  }
1545
1644
  if (tabPagesCount > 0) {
1645
+ const summaryParts = [
1646
+ `${hookUsageCount}/${tabPagesCount} use useTabNavigation hook`
1647
+ ];
1648
+ if (lazyLoadingCount > 0 || tabPagesCount > hookUsageCount) {
1649
+ summaryParts.push(`${lazyLoadingCount}/${tabPagesCount} use lazy loading`);
1650
+ }
1546
1651
  result.warnings.push({
1547
1652
  type: "warning",
1548
1653
  category: "tabs",
1549
- message: `Tab summary: ${hookUsageCount}/${tabPagesCount} pages with tabs use useTabNavigation hook`
1654
+ message: `Tab summary: ${summaryParts.join(", ")}`
1655
+ });
1656
+ }
1657
+ }
1658
+ function detectTabLazyLoading(content) {
1659
+ const patterns = [];
1660
+ let hasLazyLoading = false;
1661
+ let hasDataFetching = false;
1662
+ const hasUseQuery = /useQuery\s*\(/.test(content);
1663
+ const hasEnabledTabCondition = /enabled\s*:\s*(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]/.test(content);
1664
+ if (hasUseQuery && hasEnabledTabCondition) {
1665
+ hasLazyLoading = true;
1666
+ patterns.push("useQuery with enabled condition");
1667
+ }
1668
+ const reactLazyPattern = /(?:React\.)?lazy\s*\(\s*\(\)\s*=>\s*import\s*\(/;
1669
+ if (reactLazyPattern.test(content)) {
1670
+ hasLazyLoading = true;
1671
+ patterns.push("React.lazy() imports");
1672
+ }
1673
+ const suspensePattern = /<Suspense\s+[^>]*fallback\s*=/;
1674
+ if (suspensePattern.test(content)) {
1675
+ hasLazyLoading = true;
1676
+ patterns.push("Suspense with fallback");
1677
+ }
1678
+ const conditionalRenderPattern = /(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]\s*(?:&&|\?)\s*<[A-Z]/;
1679
+ if (conditionalRenderPattern.test(content)) {
1680
+ hasLazyLoading = true;
1681
+ patterns.push("Conditional tab rendering");
1682
+ }
1683
+ const useSWRConditionalPattern = /useSWR\s*\(\s*(?:activeTab|tab|currentTab)\s*===\s*['"][^'"]+['"]\s*\?/;
1684
+ if (useSWRConditionalPattern.test(content)) {
1685
+ hasLazyLoading = true;
1686
+ patterns.push("useSWR with conditional key");
1687
+ }
1688
+ const dataFetchingPatterns = [
1689
+ /useQuery\s*\(/,
1690
+ /useSWR\s*\(/,
1691
+ /useMutation\s*\(/,
1692
+ /fetch\s*\(/,
1693
+ /axios\./,
1694
+ /useEffect\s*\([^)]*\{[^}]*(?:fetch|axios|api\.)/s
1695
+ ];
1696
+ for (const pattern of dataFetchingPatterns) {
1697
+ if (pattern.test(content)) {
1698
+ hasDataFetching = true;
1699
+ break;
1700
+ }
1701
+ }
1702
+ return {
1703
+ hasLazyLoading,
1704
+ hasDataFetching,
1705
+ patterns
1706
+ };
1707
+ }
1708
+ async function validateHierarchies(structure, _config, result) {
1709
+ if (!structure.domain) {
1710
+ result.warnings.push({
1711
+ type: "warning",
1712
+ category: "hierarchies",
1713
+ message: "Domain project not found, skipping hierarchy validation"
1714
+ });
1715
+ return;
1716
+ }
1717
+ const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
1718
+ const hierarchies = [];
1719
+ const entityNames = /* @__PURE__ */ new Set();
1720
+ for (const file of entityFiles) {
1721
+ const content = await readText(file);
1722
+ const fileName = path6.basename(file, ".cs");
1723
+ if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
1724
+ continue;
1725
+ }
1726
+ const classMatch = content.match(
1727
+ /public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/
1728
+ );
1729
+ if (!classMatch) continue;
1730
+ const entityName = classMatch[1];
1731
+ const inheritance = classMatch[2]?.trim() || "";
1732
+ const hasBaseEntity = inheritance.includes("BaseEntity");
1733
+ const hasSystemEntity = inheritance.includes("SystemEntity");
1734
+ if (!hasBaseEntity && !hasSystemEntity) continue;
1735
+ entityNames.add(entityName);
1736
+ const hasITenantEntity = inheritance.includes("ITenantEntity");
1737
+ const selfRefMatch = content.match(
1738
+ new RegExp(
1739
+ `public\\s+(?:Guid\\??|${entityName}\\??)\\s+(Parent(?:Id)?|Parent${entityName}(?:Id)?)\\s*\\{`,
1740
+ "i"
1741
+ )
1742
+ );
1743
+ const childCollections = [];
1744
+ const collectionMatches = content.matchAll(
1745
+ /public\s+(?:virtual\s+)?ICollection<(\w+)>\s+(\w+)\s*\{/g
1746
+ );
1747
+ for (const match of collectionMatches) {
1748
+ childCollections.push(match[1]);
1749
+ }
1750
+ const fkMatches = content.matchAll(/public\s+Guid\s+(\w+Id)\s*\{[^}]+\}/g);
1751
+ let parentEntity;
1752
+ for (const match of fkMatches) {
1753
+ const propName = match[1];
1754
+ if (propName.toLowerCase().includes("parent")) continue;
1755
+ if (propName === "TenantId") continue;
1756
+ const refEntity = propName.replace(/Id$/, "");
1757
+ if (refEntity !== entityName) {
1758
+ parentEntity = refEntity;
1759
+ break;
1760
+ }
1761
+ }
1762
+ if (selfRefMatch || childCollections.length > 0 || parentEntity) {
1763
+ hierarchies.push({
1764
+ name: entityName,
1765
+ file: path6.relative(structure.root, file),
1766
+ isSelfReferencing: !!selfRefMatch,
1767
+ selfRefField: selfRefMatch ? selfRefMatch[1] : void 0,
1768
+ parentEntity,
1769
+ isTenantAware: hasITenantEntity,
1770
+ childCollections,
1771
+ depth: 0
1772
+ // Will be calculated later
1773
+ });
1774
+ }
1775
+ }
1776
+ let selfRefCount = 0;
1777
+ let parentChildCount = 0;
1778
+ let deepHierarchyCount = 0;
1779
+ for (const h of hierarchies) {
1780
+ if (h.isSelfReferencing) {
1781
+ selfRefCount++;
1782
+ result.warnings.push({
1783
+ type: "warning",
1784
+ category: "hierarchies",
1785
+ message: `Entity "${h.name}" is self-referencing via "${h.selfRefField}"`,
1786
+ file: h.file,
1787
+ suggestion: `Consider using TVF with CTE for efficient hierarchy traversal. Ensure index on ${h.selfRefField}.`
1788
+ });
1789
+ if (h.name.toLowerCase().includes("group") || h.name.toLowerCase().includes("permission") || h.name.toLowerCase().includes("role")) {
1790
+ result.warnings.push({
1791
+ type: "warning",
1792
+ category: "hierarchies",
1793
+ message: `Permission/Group entity "${h.name}" detected with self-reference`,
1794
+ file: h.file,
1795
+ suggestion: `CRITICAL: Use fn_Get${h.name}Hierarchy TVF for recursive permission resolution. Run analyze_hierarchy_patterns for SQL template.`
1796
+ });
1797
+ }
1798
+ }
1799
+ if (h.parentEntity) {
1800
+ parentChildCount++;
1801
+ if (!entityNames.has(h.parentEntity)) {
1802
+ result.errors.push({
1803
+ type: "error",
1804
+ category: "hierarchies",
1805
+ message: `Entity "${h.name}" references non-existent parent "${h.parentEntity}"`,
1806
+ file: h.file,
1807
+ suggestion: `Create the ${h.parentEntity} entity or fix the foreign key reference.`
1808
+ });
1809
+ }
1810
+ }
1811
+ if (h.parentEntity && entityNames.has(h.parentEntity)) {
1812
+ const parentInfo = hierarchies.find((x) => x.name === h.parentEntity);
1813
+ if (parentInfo && parentInfo.isTenantAware && !h.isTenantAware) {
1814
+ result.errors.push({
1815
+ type: "error",
1816
+ category: "hierarchies",
1817
+ message: `Entity "${h.name}" has tenant-aware parent "${h.parentEntity}" but is not tenant-aware itself`,
1818
+ file: h.file,
1819
+ suggestion: `Add ITenantEntity interface to ${h.name} for consistent tenant isolation.`
1820
+ });
1821
+ }
1822
+ }
1823
+ if (h.childCollections.length > 0) {
1824
+ for (const child of h.childCollections) {
1825
+ const childInfo = hierarchies.find((x) => x.name === child);
1826
+ if (childInfo && childInfo.childCollections.length > 0) {
1827
+ deepHierarchyCount++;
1828
+ result.warnings.push({
1829
+ type: "warning",
1830
+ category: "hierarchies",
1831
+ message: `Deep hierarchy detected: ${h.name} \u2192 ${child} \u2192 ...`,
1832
+ file: h.file,
1833
+ suggestion: `Consider TVF for efficient traversal. Run analyze_hierarchy_patterns for recommendations.`
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1838
+ }
1839
+ if (hierarchies.length > 0) {
1840
+ result.warnings.push({
1841
+ type: "warning",
1842
+ category: "hierarchies",
1843
+ message: `Hierarchy summary: ${selfRefCount} self-referencing, ${parentChildCount} parent-child, ${deepHierarchyCount} deep hierarchies detected`
1844
+ });
1845
+ }
1846
+ }
1847
+ async function validateProtectedActions(structure, _config, result) {
1848
+ if (!structure.web) {
1849
+ result.warnings.push({
1850
+ type: "warning",
1851
+ category: "protected-actions",
1852
+ message: "Web project not found, skipping protected actions validation"
1853
+ });
1854
+ return;
1855
+ }
1856
+ const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
1857
+ const componentFiles = await findFiles("**/components/**/*.tsx", { cwd: structure.web });
1858
+ const allFiles = [...pageFiles, ...componentFiles];
1859
+ const actionPatterns = {
1860
+ create: ["create", "add", "new", "cr\xE9er", "ajouter", "nouveau"],
1861
+ update: ["edit", "update", "modify", "modifier", "\xE9diter", "save", "enregistrer"],
1862
+ delete: ["delete", "remove", "supprimer", "retirer"],
1863
+ execute: ["execute", "run", "sync", "ex\xE9cuter", "lancer", "synchroniser"]
1864
+ };
1865
+ let protectedButtonCount = 0;
1866
+ let hasPermissionCount = 0;
1867
+ let unprotectedCount = 0;
1868
+ const unprotectedButtons = [];
1869
+ for (const file of allFiles) {
1870
+ const content = await readText(file);
1871
+ const lines = content.split("\n");
1872
+ const usesProtectedButton = content.includes("<ProtectedButton");
1873
+ if (usesProtectedButton) {
1874
+ protectedButtonCount++;
1875
+ continue;
1876
+ }
1877
+ for (let i = 0; i < lines.length; i++) {
1878
+ const line = lines[i];
1879
+ const trimmed = line.trim();
1880
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
1881
+ continue;
1882
+ }
1883
+ const buttonMatch = /<(?:button|Button)[^>]*>([^<]*)<\/|onClick\s*=\s*\{[^}]*handle(Create|Delete|Edit|Update|Remove|Save|Add|Sync|Execute)/i.exec(line);
1884
+ if (buttonMatch) {
1885
+ const buttonContent = buttonMatch[1]?.toLowerCase() || "";
1886
+ const handlerAction = buttonMatch[2]?.toLowerCase() || "";
1887
+ let detectedAction = null;
1888
+ for (const [action, keywords] of Object.entries(actionPatterns)) {
1889
+ if (keywords.some((kw) => buttonContent.includes(kw) || handlerAction.includes(kw))) {
1890
+ detectedAction = action;
1891
+ break;
1892
+ }
1893
+ }
1894
+ if (detectedAction) {
1895
+ const startLine = Math.max(0, i - 5);
1896
+ const endLine = Math.min(lines.length - 1, i + 5);
1897
+ const context = lines.slice(startLine, endLine + 1).join("\n");
1898
+ const hasPermissionCheck = context.includes("hasPermission") || context.includes("canCreate") || context.includes("canEdit") || context.includes("canUpdate") || context.includes("canDelete") || context.includes("canExecute") || context.includes("disabled={!can");
1899
+ if (!hasPermissionCheck) {
1900
+ unprotectedCount++;
1901
+ unprotectedButtons.push({
1902
+ file: path6.relative(structure.root, file),
1903
+ line: i + 1,
1904
+ action: detectedAction,
1905
+ code: line.trim().substring(0, 80)
1906
+ });
1907
+ } else {
1908
+ hasPermissionCount++;
1909
+ }
1910
+ }
1911
+ }
1912
+ }
1913
+ }
1914
+ const reportedButtons = unprotectedButtons.slice(0, 10);
1915
+ for (const btn of reportedButtons) {
1916
+ result.warnings.push({
1917
+ type: "warning",
1918
+ category: "protected-actions",
1919
+ message: `Button with "${btn.action}" action may be missing permission check`,
1920
+ file: btn.file,
1921
+ line: btn.line,
1922
+ suggestion: `Use <ProtectedButton action="${btn.action}" navRoute="..."> or add hasPermission check`
1923
+ });
1924
+ }
1925
+ if (unprotectedButtons.length > 10) {
1926
+ result.warnings.push({
1927
+ type: "warning",
1928
+ category: "protected-actions",
1929
+ message: `... and ${unprotectedButtons.length - 10} more unprotected buttons`
1930
+ });
1931
+ }
1932
+ const totalActionButtons = protectedButtonCount + hasPermissionCount + unprotectedCount;
1933
+ if (totalActionButtons > 0) {
1934
+ const protectedPercentage = Math.round(
1935
+ (protectedButtonCount + hasPermissionCount) / totalActionButtons * 100
1936
+ );
1937
+ result.warnings.push({
1938
+ type: "warning",
1939
+ category: "protected-actions",
1940
+ message: `Protected actions summary: ${protectedButtonCount} use ProtectedButton, ${hasPermissionCount} use hasPermission, ${unprotectedCount} potentially unprotected (${protectedPercentage}% protected)`
1550
1941
  });
1942
+ if (protectedPercentage < 80 && totalActionButtons > 5) {
1943
+ result.errors.push({
1944
+ type: "error",
1945
+ category: "protected-actions",
1946
+ message: `Less than 80% of action buttons are protected (${protectedPercentage}%)`,
1947
+ suggestion: "Migrate action buttons to use <ProtectedButton> component for consistent permission checking"
1948
+ });
1949
+ }
1551
1950
  }
1552
1951
  }
1553
1952
  function generateSummary(result, checks) {
@@ -2011,6 +2410,15 @@ var scaffoldExtensionTool = {
2011
2410
  navRouteSuffix: {
2012
2411
  type: "string",
2013
2412
  description: 'Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'
2413
+ },
2414
+ withHierarchyFunction: {
2415
+ type: "boolean",
2416
+ description: "For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"
2417
+ },
2418
+ hierarchyDirection: {
2419
+ type: "string",
2420
+ enum: ["ancestors", "descendants", "both"],
2421
+ description: "Direction for hierarchy traversal function (default: both)"
2014
2422
  }
2015
2423
  }
2016
2424
  }
@@ -2155,10 +2563,10 @@ async function scaffoldFeature(name, options, structure, config, result, dryRun
2155
2563
  result.instructions.push("---");
2156
2564
  result.instructions.push(`## Summary: Generated ${generated.join(" + ")}`);
2157
2565
  result.instructions.push("");
2158
- const schema = options?.schema || (isClientExtension ? "extensions" : "core");
2566
+ const schema = options?.schema || (isClientExtension ? "extensions" : config.defaultDbContext);
2159
2567
  const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
2160
2568
  const dbContextInterface = schema === "extensions" ? "IExtensionsDbContext" : "ICoreDbContext";
2161
- const migrationPrefix = schema === "extensions" ? "ext" : "core";
2569
+ const migrationPrefix = schema;
2162
2570
  result.instructions.push("### Next Steps:");
2163
2571
  result.instructions.push(`1. Add DbSet to ${dbContextInterface} and ${dbContextName}: \`public DbSet<${name}> ${name}s => Set<${name}>();\``);
2164
2572
  if (withRepository) {
@@ -2500,7 +2908,7 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
2500
2908
  result.files.push({ path: configFilePath, content: configContent, type: "created" });
2501
2909
  const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
2502
2910
  const dbContextInterface = schema === "extensions" ? "IExtensionsDbContext" : "ICoreDbContext";
2503
- const migrationPrefix = schema === "extensions" ? "ext" : "core";
2911
+ const migrationPrefix = schema;
2504
2912
  result.instructions.push(`Add DbSet to ${dbContextInterface}:`);
2505
2913
  result.instructions.push(`public DbSet<${name}> ${name}s => Set<${name}>();`);
2506
2914
  result.instructions.push("");
@@ -2520,6 +2928,11 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
2520
2928
  result.instructions.push("### Seed Data");
2521
2929
  await scaffoldSeedData(name, options, structure, config, result, dryRun);
2522
2930
  }
2931
+ if (options?.withHierarchyFunction) {
2932
+ result.instructions.push("");
2933
+ result.instructions.push("### Hierarchy Function (TVF)");
2934
+ await scaffoldHierarchyFunction(name, options, structure, config, result, dryRun);
2935
+ }
2523
2936
  }
2524
2937
  async function scaffoldSeedData(name, options, structure, config, result, dryRun = false) {
2525
2938
  const tablePrefix = options?.tablePrefix || "ref_";
@@ -2629,6 +3042,180 @@ public static class {{name}}SeedData
2629
3042
  result.instructions.push("3. Implement GetSeedData() returning object[]");
2630
3043
  result.instructions.push("4. Reference other SeedData IDs for foreign keys");
2631
3044
  }
3045
+ async function scaffoldHierarchyFunction(name, options, structure, config, result, dryRun = false) {
3046
+ const tablePrefix = options?.tablePrefix || "ref_";
3047
+ const schema = options?.schema || config.conventions.schemas.platform;
3048
+ const tableName = `${tablePrefix}${name}s`;
3049
+ const tvfTemplate = `-- ============================================================================
3050
+ -- Table-Valued Function: fn_Get{{name}}Hierarchy
3051
+ -- Purpose: Recursively traverse {{name}} hierarchy (ancestors/descendants)
3052
+ -- Generated by: SmartStack MCP scaffold_extension
3053
+ -- ============================================================================
3054
+
3055
+ -- Drop existing function if it exists
3056
+ IF OBJECT_ID('[{{schema}}].[fn_Get{{name}}Hierarchy]', 'IF') IS NOT NULL
3057
+ DROP FUNCTION [{{schema}}].[fn_Get{{name}}Hierarchy];
3058
+ GO
3059
+
3060
+ CREATE FUNCTION [{{schema}}].[fn_Get{{name}}Hierarchy]
3061
+ (
3062
+ @{{nameLower}}Id UNIQUEIDENTIFIER,
3063
+ @Direction VARCHAR(11) = 'descendants' -- 'ancestors', 'descendants', or 'both'
3064
+ )
3065
+ RETURNS TABLE
3066
+ AS
3067
+ RETURN
3068
+ (
3069
+ WITH HierarchyCTE AS (
3070
+ -- ========================================
3071
+ -- Base case: starting node
3072
+ -- ========================================
3073
+ SELECT
3074
+ Id,
3075
+ ParentId,
3076
+ Code,
3077
+ TenantId,
3078
+ 0 AS [Level],
3079
+ CAST(Id AS NVARCHAR(MAX)) AS [Path],
3080
+ 'self' AS Direction
3081
+ FROM [{{schema}}].[{{tableName}}]
3082
+ WHERE Id = @{{nameLower}}Id
3083
+ AND IsDeleted = 0
3084
+
3085
+ UNION ALL
3086
+
3087
+ -- ========================================
3088
+ -- Recursive: ancestors (climb up the tree)
3089
+ -- ========================================
3090
+ SELECT
3091
+ parent.Id,
3092
+ parent.ParentId,
3093
+ parent.Code,
3094
+ parent.TenantId,
3095
+ h.[Level] - 1,
3096
+ CAST(parent.Id AS NVARCHAR(MAX)) + '/' + h.[Path],
3097
+ 'ancestor' AS Direction
3098
+ FROM [{{schema}}].[{{tableName}}] parent
3099
+ INNER JOIN HierarchyCTE h ON parent.Id = h.ParentId
3100
+ WHERE parent.IsDeleted = 0
3101
+ AND @Direction IN ('ancestors', 'both')
3102
+ AND h.[Level] > -50 -- Prevent infinite recursion
3103
+
3104
+ UNION ALL
3105
+
3106
+ -- ========================================
3107
+ -- Recursive: descendants (go down the tree)
3108
+ -- ========================================
3109
+ SELECT
3110
+ child.Id,
3111
+ child.ParentId,
3112
+ child.Code,
3113
+ child.TenantId,
3114
+ h.[Level] + 1,
3115
+ h.[Path] + '/' + CAST(child.Id AS NVARCHAR(MAX)),
3116
+ 'descendant' AS Direction
3117
+ FROM [{{schema}}].[{{tableName}}] child
3118
+ INNER JOIN HierarchyCTE h ON child.ParentId = h.Id
3119
+ WHERE child.IsDeleted = 0
3120
+ AND @Direction IN ('descendants', 'both')
3121
+ AND h.[Level] < 50 -- Prevent infinite recursion
3122
+ )
3123
+ SELECT
3124
+ Id,
3125
+ ParentId,
3126
+ Code,
3127
+ TenantId,
3128
+ [Level],
3129
+ [Path],
3130
+ Direction,
3131
+ ABS([Level]) AS Depth -- Absolute depth from starting node
3132
+ FROM HierarchyCTE
3133
+ WHERE (
3134
+ @Direction = 'both'
3135
+ OR (@Direction = 'ancestors' AND Direction IN ('self', 'ancestor'))
3136
+ OR (@Direction = 'descendants' AND Direction IN ('self', 'descendant'))
3137
+ )
3138
+ );
3139
+ GO
3140
+
3141
+ -- ============================================================================
3142
+ -- Usage Examples
3143
+ -- ============================================================================
3144
+
3145
+ -- Get all descendants of a {{name}}
3146
+ -- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'descendants') ORDER BY [Level];
3147
+
3148
+ -- Get all ancestors of a {{name}}
3149
+ -- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'ancestors') ORDER BY [Level] DESC;
3150
+
3151
+ -- Get full hierarchy (ancestors + descendants)
3152
+ -- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@id, 'both') ORDER BY [Level];
3153
+
3154
+ -- Get all {{name}}s at a specific level
3155
+ -- SELECT * FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@rootId, 'descendants') WHERE [Level] = 2;
3156
+
3157
+ -- Check if user belongs to a specific {{name}} hierarchy
3158
+ -- SELECT CASE WHEN EXISTS (
3159
+ -- SELECT 1 FROM [{{schema}}].[fn_Get{{name}}Hierarchy](@userId{{name}}Id, 'ancestors')
3160
+ -- WHERE Id = @target{{name}}Id
3161
+ -- ) THEN 1 ELSE 0 END AS BelongsToHierarchy;
3162
+ `;
3163
+ const context = {
3164
+ name,
3165
+ nameLower: name.charAt(0).toLowerCase() + name.slice(1),
3166
+ tableName,
3167
+ schema
3168
+ };
3169
+ const tvfContent = Handlebars.compile(tvfTemplate)(context);
3170
+ const projectRoot = config.smartstack.projectPath;
3171
+ const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
3172
+ const scriptsPath = path8.join(infraPath, "Persistence", "Scripts", "Functions");
3173
+ const tvfFilePath = path8.join(scriptsPath, `fn_Get${name}Hierarchy.sql`);
3174
+ validatePathSecurity(tvfFilePath, projectRoot);
3175
+ if (!dryRun) {
3176
+ await ensureDirectory(scriptsPath);
3177
+ await writeText(tvfFilePath, tvfContent);
3178
+ }
3179
+ result.files.push({ path: tvfFilePath, content: tvfContent, type: "created" });
3180
+ result.instructions.push("TVF hierarchy function generated!");
3181
+ result.instructions.push("");
3182
+ result.instructions.push("**Next Steps:**");
3183
+ result.instructions.push(`1. Run the SQL script to create the function in your database`);
3184
+ result.instructions.push(`2. Add a method to I${name}Repository for hierarchy queries:`);
3185
+ result.instructions.push("```csharp");
3186
+ result.instructions.push(`Task<IReadOnlyList<${name}HierarchyDto>> GetHierarchyAsync(`);
3187
+ result.instructions.push(` Guid ${name.toLowerCase()}Id,`);
3188
+ result.instructions.push(` HierarchyDirection direction,`);
3189
+ result.instructions.push(` CancellationToken ct = default);`);
3190
+ result.instructions.push("```");
3191
+ result.instructions.push("");
3192
+ result.instructions.push(`3. Create ${name}HierarchyDto:`);
3193
+ result.instructions.push("```csharp");
3194
+ result.instructions.push(`public record ${name}HierarchyDto(`);
3195
+ result.instructions.push(" Guid Id,");
3196
+ result.instructions.push(" Guid? ParentId,");
3197
+ result.instructions.push(" string Code,");
3198
+ result.instructions.push(" Guid TenantId,");
3199
+ result.instructions.push(" int Level,");
3200
+ result.instructions.push(" string Path,");
3201
+ result.instructions.push(" string Direction,");
3202
+ result.instructions.push(" int Depth);");
3203
+ result.instructions.push("```");
3204
+ result.instructions.push("");
3205
+ result.instructions.push("4. Implement using raw SQL in repository:");
3206
+ result.instructions.push("```csharp");
3207
+ result.instructions.push(`var sql = "SELECT * FROM [${schema}].[fn_Get${name}Hierarchy]({0}, {1})";`);
3208
+ result.instructions.push(`return await _context.Database`);
3209
+ result.instructions.push(` .SqlQueryRaw<${name}HierarchyDto>(sql, id, direction.ToString().ToLower())`);
3210
+ result.instructions.push(` .ToListAsync(ct);`);
3211
+ result.instructions.push("```");
3212
+ result.instructions.push("");
3213
+ result.instructions.push(`**Pattern Recommendation**: TVF with CTE is optimal for:`);
3214
+ result.instructions.push("- Permission/role hierarchies (recursive permission check)");
3215
+ result.instructions.push("- Organization charts (reporting structure)");
3216
+ result.instructions.push("- Category trees (nested categories)");
3217
+ result.instructions.push("- Group memberships (user belongs to parent groups)");
3218
+ }
2632
3219
  async function scaffoldController(name, options, structure, config, result, dryRun = false) {
2633
3220
  const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
2634
3221
  const navRoute = options?.navRoute;
@@ -3858,7 +4445,7 @@ var suggestMigrationTool = {
3858
4445
  context: {
3859
4446
  type: "string",
3860
4447
  enum: ["core", "extensions"],
3861
- description: "DbContext name (default: core)"
4448
+ description: 'DbContext name (default: auto-detected from .smartstack/config.json, or "core" for platform projects)'
3862
4449
  },
3863
4450
  version: {
3864
4451
  type: "string",
@@ -3870,12 +4457,12 @@ var suggestMigrationTool = {
3870
4457
  };
3871
4458
  var SuggestMigrationInputSchema2 = z2.object({
3872
4459
  description: z2.string().describe("Description of what the migration does"),
3873
- context: z2.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
4460
+ context: z2.enum(["core", "extensions"]).optional().describe("DbContext name (default: auto-detected from project config)"),
3874
4461
  version: z2.string().optional().describe('Semver version (e.g., "1.0.0")')
3875
4462
  });
3876
4463
  async function handleSuggestMigration(args, config) {
3877
4464
  const input = SuggestMigrationInputSchema2.parse(args);
3878
- const context = input.context || "core";
4465
+ const context = input.context || config.defaultDbContext || "core";
3879
4466
  logger.info("Suggesting migration name", { description: input.description, context });
3880
4467
  const structure = await findSmartStackStructure(config.smartstack.projectPath);
3881
4468
  const existingMigrations = await findExistingMigrations(structure, config, context);
@@ -3898,7 +4485,8 @@ async function handleSuggestMigration(args, config) {
3898
4485
  const sequenceStr = sequence.toString().padStart(3, "0");
3899
4486
  const migrationName = `${context}_v${version}_${sequenceStr}_${pascalDescription}`;
3900
4487
  const dbContextName = context === "core" ? "CoreDbContext" : "ExtensionsDbContext";
3901
- const command = `dotnet ef migrations add ${migrationName} --context ${dbContextName}`;
4488
+ const outputPath = context === "extensions" ? "Persistence/Migrations/Extensions" : "Persistence/Migrations";
4489
+ const command = `dotnet ef migrations add ${migrationName} --context ${dbContextName} --project ../SmartStack.Infrastructure -o ${outputPath}`;
3902
4490
  const lines = [];
3903
4491
  lines.push("# Migration Name Suggestion");
3904
4492
  lines.push("");
@@ -10587,141 +11175,2864 @@ function calculateMetrics(functions, fileMetrics, thresholds) {
10587
11175
  fileSize: createStat(fileSizes, thresholds.fileSize)
10588
11176
  };
10589
11177
  }
10590
- function identifyHotspots(functions, fileMetrics, thresholds) {
10591
- const hotspots = [];
10592
- for (const func of functions) {
10593
- const issues = [];
10594
- if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
10595
- issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
11178
+ function identifyHotspots(functions, fileMetrics, thresholds) {
11179
+ const hotspots = [];
11180
+ for (const func of functions) {
11181
+ const issues = [];
11182
+ if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
11183
+ issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
11184
+ }
11185
+ if (func.cyclomaticComplexity > thresholds.cyclomaticComplexity) {
11186
+ issues.push(`Cyclomatic complexity: ${func.cyclomaticComplexity} (threshold: ${thresholds.cyclomaticComplexity})`);
11187
+ }
11188
+ if (func.lineCount > thresholds.functionSize) {
11189
+ issues.push(`Lines: ${func.lineCount} (threshold: ${thresholds.functionSize})`);
11190
+ }
11191
+ if (func.maxNestingDepth > thresholds.nestingDepth) {
11192
+ issues.push(`Nesting depth: ${func.maxNestingDepth} (threshold: ${thresholds.nestingDepth})`);
11193
+ }
11194
+ if (issues.length > 0) {
11195
+ const severity = issues.length >= 3 ? "high" : issues.length === 2 ? "medium" : "low";
11196
+ hotspots.push({
11197
+ file: func.file,
11198
+ function: func.name,
11199
+ issues,
11200
+ severity,
11201
+ metrics: {
11202
+ cognitiveComplexity: func.cognitiveComplexity,
11203
+ cyclomaticComplexity: func.cyclomaticComplexity,
11204
+ lineCount: func.lineCount,
11205
+ nestingDepth: func.maxNestingDepth
11206
+ }
11207
+ });
11208
+ }
11209
+ }
11210
+ for (const [file, metrics] of fileMetrics) {
11211
+ if (metrics.lineCount > thresholds.fileSize) {
11212
+ hotspots.push({
11213
+ file,
11214
+ issues: [`File size: ${metrics.lineCount} lines (threshold: ${thresholds.fileSize})`],
11215
+ severity: "medium",
11216
+ metrics: {
11217
+ lineCount: metrics.lineCount
11218
+ }
11219
+ });
11220
+ }
11221
+ }
11222
+ const severityOrder = { high: 0, medium: 1, low: 2 };
11223
+ hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
11224
+ return hotspots.slice(0, 20);
11225
+ }
11226
+ function calculateSummary(functions, fileMetrics, metrics, hotspots) {
11227
+ const totalViolations = metrics.cognitiveComplexity.violations + metrics.cyclomaticComplexity.violations + metrics.functionSize.violations + metrics.nestingDepth.violations + metrics.fileSize.violations;
11228
+ const totalFunctions = functions.length;
11229
+ const totalFiles = fileMetrics.size;
11230
+ const violationRate = totalFunctions > 0 ? totalViolations / totalFunctions : 0;
11231
+ const score = Math.max(0, Math.round(100 - violationRate * 100));
11232
+ let grade;
11233
+ if (score >= 90) grade = "A";
11234
+ else if (score >= 80) grade = "B";
11235
+ else if (score >= 70) grade = "C";
11236
+ else if (score >= 60) grade = "D";
11237
+ else grade = "F";
11238
+ return {
11239
+ score,
11240
+ grade,
11241
+ filesAnalyzed: totalFiles,
11242
+ functionsAnalyzed: totalFunctions,
11243
+ issuesFound: hotspots.length
11244
+ };
11245
+ }
11246
+ function isExcludedPath(filePath) {
11247
+ const exclusions = [
11248
+ /[/\\]bin[/\\]/,
11249
+ /[/\\]obj[/\\]/,
11250
+ /[/\\]node_modules[/\\]/,
11251
+ /[/\\]Migrations[/\\]/,
11252
+ /\.test\./,
11253
+ /\.spec\./,
11254
+ /Tests[/\\]/,
11255
+ /\.d\.ts$/,
11256
+ /\.min\./
11257
+ ];
11258
+ return exclusions.some((pattern) => pattern.test(filePath));
11259
+ }
11260
+ function getLineNumber2(content, index) {
11261
+ const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
11262
+ return normalizedContent.split("\n").length;
11263
+ }
11264
+ function formatQualityReport(result, thresholds) {
11265
+ const lines = [];
11266
+ lines.push("# Code Quality Report");
11267
+ lines.push("");
11268
+ lines.push("## Summary");
11269
+ lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
11270
+ lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
11271
+ lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
11272
+ lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
11273
+ lines.push("");
11274
+ lines.push("## Metrics Overview");
11275
+ lines.push("");
11276
+ lines.push("| Metric | Average | Max | Threshold | Status |");
11277
+ lines.push("|--------|---------|-----|-----------|--------|");
11278
+ const formatMetricRow = (name, stat2) => {
11279
+ const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
11280
+ return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
11281
+ };
11282
+ lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
11283
+ lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
11284
+ lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
11285
+ lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
11286
+ lines.push(formatMetricRow("File Size", result.metrics.fileSize));
11287
+ lines.push("");
11288
+ if (result.hotspots.length > 0) {
11289
+ lines.push("## Hotspots (Needs Attention)");
11290
+ lines.push("");
11291
+ for (const hotspot of result.hotspots) {
11292
+ const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
11293
+ const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
11294
+ lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
11295
+ for (const issue of hotspot.issues) {
11296
+ lines.push(`- ${issue}`);
11297
+ }
11298
+ if (hotspot.function) {
11299
+ if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
11300
+ lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
11301
+ } else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
11302
+ lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
11303
+ }
11304
+ } else {
11305
+ lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
11306
+ }
11307
+ lines.push("");
11308
+ }
11309
+ } else {
11310
+ lines.push("No hotspots found. Code quality is within acceptable thresholds.");
11311
+ }
11312
+ return lines.join("\n");
11313
+ }
11314
+
11315
+ // src/tools/analyze-hierarchy-patterns.ts
11316
+ import path22 from "path";
11317
+ var analyzeHierarchyPatternsTool = {
11318
+ name: "analyze_hierarchy_patterns",
11319
+ description: `Analyze parent-child relationships in SmartStack entities and recommend optimal hierarchy patterns.
11320
+
11321
+ Detects:
11322
+ - Direct parent-child relationships (via foreign keys)
11323
+ - Self-referencing hierarchies (ParentId pointing to same entity)
11324
+ - Inheritance chains (entity extending another entity)
11325
+ - Collection relationships (ICollection<T>)
11326
+
11327
+ Recommends patterns based on usage:
11328
+ - **TVF + CTE**: Deep hierarchies with frequent traversal (permissions, org charts)
11329
+ - **Materialized Path**: Deep hierarchies with frequent modifications
11330
+ - **Simple ParentId**: Shallow hierarchies (2-3 levels)
11331
+ - **Nested Sets**: Read-heavy, rarely modified trees
11332
+ - **Closure Table**: All operations need to be fast`,
11333
+ inputSchema: {
11334
+ type: "object",
11335
+ properties: {
11336
+ path: {
11337
+ type: "string",
11338
+ description: "Project path to analyze (default: SmartStack.app path from config)"
11339
+ },
11340
+ entityFilter: {
11341
+ type: "string",
11342
+ description: 'Filter entities by name pattern (e.g., "User*", "*Group*")'
11343
+ },
11344
+ includeRecommendations: {
11345
+ type: "boolean",
11346
+ default: true,
11347
+ description: "Include pattern recommendations for each hierarchy"
11348
+ },
11349
+ outputFormat: {
11350
+ type: "string",
11351
+ enum: ["summary", "detailed", "json"],
11352
+ default: "detailed",
11353
+ description: "Output format"
11354
+ }
11355
+ }
11356
+ }
11357
+ };
11358
+ async function handleAnalyzeHierarchyPatterns(args, config) {
11359
+ const input = AnalyzeHierarchyPatternsInputSchema.parse(args);
11360
+ const projectPath = input.path || config.smartstack.projectPath;
11361
+ const includeRecommendations = input.includeRecommendations ?? true;
11362
+ const outputFormat = input.outputFormat || "detailed";
11363
+ logger.info("Analyzing hierarchy patterns", { projectPath });
11364
+ const structure = await findSmartStackStructure(projectPath);
11365
+ if (!structure.domain) {
11366
+ return "# Error\n\nDomain project not found. Cannot analyze entity hierarchies.";
11367
+ }
11368
+ const entityGraph = await buildEntityGraph(structure.domain, input.entityFilter);
11369
+ const hierarchies = detectHierarchies(entityGraph);
11370
+ const recommendations = includeRecommendations ? generateRecommendations4(hierarchies, entityGraph) : [];
11371
+ const result = {
11372
+ totalEntities: entityGraph.size,
11373
+ hierarchicalEntities: hierarchies.filter((h) => h.isHierarchical).length,
11374
+ selfReferencingEntities: hierarchies.filter((h) => h.isSelfReferencing).length,
11375
+ maxDepth: Math.max(0, ...hierarchies.map((h) => h.depth)),
11376
+ hierarchies,
11377
+ recommendations,
11378
+ circularDependencies: detectCircularDependencies(entityGraph)
11379
+ };
11380
+ return formatResult8(result, outputFormat, structure.root);
11381
+ }
11382
+ async function buildEntityGraph(domainPath, filter) {
11383
+ const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
11384
+ const entityMap = /* @__PURE__ */ new Map();
11385
+ const parsedEntities = [];
11386
+ for (const file of entityFiles) {
11387
+ const content = await readText(file);
11388
+ const parsed = parseEntity(content, file, domainPath);
11389
+ if (parsed && (!filter || matchesFilter(parsed.name, filter))) {
11390
+ parsedEntities.push(parsed);
11391
+ }
11392
+ }
11393
+ for (const entity of parsedEntities) {
11394
+ entityMap.set(entity.name, {
11395
+ name: entity.name,
11396
+ file: entity.file,
11397
+ baseClass: entity.baseClass,
11398
+ isTenantAware: entity.isTenantAware,
11399
+ isSystemEntity: entity.isSystemEntity,
11400
+ parent: null,
11401
+ children: [],
11402
+ depth: 0,
11403
+ isRoot: true,
11404
+ isLeaf: true,
11405
+ isSelfReferencing: !!entity.selfReferenceField,
11406
+ selfReferenceField: entity.selfReferenceField,
11407
+ childCollections: entity.childCollections,
11408
+ foreignKeys: entity.foreignKeys,
11409
+ isHierarchical: false
11410
+ });
11411
+ }
11412
+ for (const entity of parsedEntities) {
11413
+ const node = entityMap.get(entity.name);
11414
+ if (entity.parentEntity && entityMap.has(entity.parentEntity)) {
11415
+ const parent = entityMap.get(entity.parentEntity);
11416
+ node.parent = parent.name;
11417
+ node.isRoot = false;
11418
+ parent.children.push(entity.name);
11419
+ parent.isLeaf = false;
11420
+ }
11421
+ if (entity.childCollections.length > 0 || entity.selfReferenceField) {
11422
+ node.isHierarchical = true;
11423
+ }
11424
+ }
11425
+ for (const node of entityMap.values()) {
11426
+ node.depth = calculateDepth(node.name, entityMap, /* @__PURE__ */ new Set());
11427
+ }
11428
+ for (const node of entityMap.values()) {
11429
+ if (node.children.length > 0) {
11430
+ node.isHierarchical = true;
11431
+ }
11432
+ }
11433
+ return entityMap;
11434
+ }
11435
+ function parseEntity(content, file, domainPath) {
11436
+ const fileName = path22.basename(file, ".cs");
11437
+ if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
11438
+ return null;
11439
+ }
11440
+ const classMatch = content.match(
11441
+ /public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/
11442
+ );
11443
+ if (!classMatch) return null;
11444
+ const entityName = classMatch[1];
11445
+ const inheritance = classMatch[2]?.trim() || "";
11446
+ const hasBaseEntity = inheritance.includes("BaseEntity");
11447
+ const hasSystemEntity = inheritance.includes("SystemEntity");
11448
+ if (!hasBaseEntity && !hasSystemEntity) return null;
11449
+ const hasITenantEntity = inheritance.includes("ITenantEntity");
11450
+ const selfRefMatch = content.match(
11451
+ new RegExp(`public\\s+(?:Guid\\??|${entityName}\\??)\\s+(Parent(?:Id)?|Parent${entityName}(?:Id)?)\\s*\\{`, "i")
11452
+ );
11453
+ const selfReferenceField = selfRefMatch ? selfRefMatch[1] : void 0;
11454
+ const childCollections = [];
11455
+ const collectionMatches = content.matchAll(
11456
+ /public\s+(?:virtual\s+)?ICollection<(\w+)>\s+(\w+)\s*\{/g
11457
+ );
11458
+ for (const match of collectionMatches) {
11459
+ childCollections.push(match[1]);
11460
+ }
11461
+ const foreignKeys = [];
11462
+ const fkMatches = content.matchAll(
11463
+ /public\s+Guid\s+(\w+Id)\s*\{[^}]+\}/g
11464
+ );
11465
+ for (const match of fkMatches) {
11466
+ const propName = match[1];
11467
+ if (selfReferenceField && propName.toLowerCase().includes("parent")) continue;
11468
+ const referencedEntity = propName.replace(/Id$/, "");
11469
+ if (referencedEntity !== entityName) {
11470
+ foreignKeys.push({
11471
+ propertyName: propName,
11472
+ referencedEntity,
11473
+ isNavigationProperty: content.includes(`public virtual ${referencedEntity}?`)
11474
+ });
11475
+ }
11476
+ }
11477
+ const parentEntity = foreignKeys.length > 0 ? foreignKeys[0].referencedEntity : void 0;
11478
+ return {
11479
+ name: entityName,
11480
+ file: path22.relative(domainPath, file),
11481
+ baseClass: hasSystemEntity ? "SystemEntity" : "BaseEntity",
11482
+ isTenantAware: hasITenantEntity,
11483
+ isSystemEntity: hasSystemEntity,
11484
+ parentEntity,
11485
+ selfReferenceField,
11486
+ childCollections,
11487
+ foreignKeys
11488
+ };
11489
+ }
11490
+ function matchesFilter(name, filter) {
11491
+ const pattern = filter.replace(/\*/g, ".*").replace(/\?/g, ".");
11492
+ return new RegExp(`^${pattern}$`, "i").test(name);
11493
+ }
11494
+ function calculateDepth(name, entityMap, visited) {
11495
+ if (visited.has(name)) return -1;
11496
+ const node = entityMap.get(name);
11497
+ if (!node || !node.parent) return 0;
11498
+ visited.add(name);
11499
+ const parentDepth = calculateDepth(node.parent, entityMap, visited);
11500
+ return parentDepth === -1 ? -1 : parentDepth + 1;
11501
+ }
11502
+ function detectHierarchies(entityMap) {
11503
+ return Array.from(entityMap.values()).filter(
11504
+ (node) => node.isHierarchical || node.isSelfReferencing || node.children.length > 0
11505
+ );
11506
+ }
11507
+ function detectCircularDependencies(entityMap) {
11508
+ const cycles = [];
11509
+ for (const [name] of entityMap) {
11510
+ let dfs2 = function(current) {
11511
+ if (path27.includes(current)) {
11512
+ const cycleStart = path27.indexOf(current);
11513
+ cycles.push([...path27.slice(cycleStart), current]);
11514
+ return true;
11515
+ }
11516
+ if (visited.has(current)) return false;
11517
+ visited.add(current);
11518
+ path27.push(current);
11519
+ const node = entityMap.get(current);
11520
+ if (node?.parent) {
11521
+ dfs2(node.parent);
11522
+ }
11523
+ path27.pop();
11524
+ return false;
11525
+ };
11526
+ var dfs = dfs2;
11527
+ const visited = /* @__PURE__ */ new Set();
11528
+ const path27 = [];
11529
+ dfs2(name);
11530
+ }
11531
+ const unique = /* @__PURE__ */ new Map();
11532
+ for (const cycle of cycles) {
11533
+ const key = [...cycle].sort().join(",");
11534
+ if (!unique.has(key)) {
11535
+ unique.set(key, cycle);
11536
+ }
11537
+ }
11538
+ return Array.from(unique.values());
11539
+ }
11540
+ function generateRecommendations4(hierarchies, entityMap) {
11541
+ const recommendations = [];
11542
+ for (const hierarchy of hierarchies) {
11543
+ const rec = analyzeHierarchy(hierarchy, entityMap);
11544
+ if (rec) {
11545
+ recommendations.push(rec);
11546
+ }
11547
+ }
11548
+ return recommendations;
11549
+ }
11550
+ function analyzeHierarchy(node, entityMap) {
11551
+ if (!node.isRoot && !node.isSelfReferencing) return null;
11552
+ const depth = calculateMaxChildDepth(node, entityMap);
11553
+ const totalNodes = countDescendants(node, entityMap) + 1;
11554
+ const hasPermissionPattern = node.name.toLowerCase().includes("permission") || node.name.toLowerCase().includes("role") || node.name.toLowerCase().includes("group");
11555
+ let pattern;
11556
+ let reason;
11557
+ let priority;
11558
+ let sqlTemplate;
11559
+ if (node.isSelfReferencing) {
11560
+ if (depth > 3 || hasPermissionPattern) {
11561
+ pattern = "tvf-cte";
11562
+ reason = `Self-referencing hierarchy with variable depth. ${hasPermissionPattern ? "Permission/Group pattern detected - TVF recommended for recursive permission resolution." : ""}`;
11563
+ priority = hasPermissionPattern ? "critical" : "high";
11564
+ sqlTemplate = generateTvfTemplate(node);
11565
+ } else if (depth <= 3) {
11566
+ pattern = "simple-parentid";
11567
+ reason = "Shallow self-referencing hierarchy (depth <= 3). Simple ParentId with eager loading is sufficient.";
11568
+ priority = "low";
11569
+ } else {
11570
+ pattern = "materialized-path";
11571
+ reason = "Deep self-referencing hierarchy. Consider Materialized Path for efficient ancestor/descendant queries.";
11572
+ priority = "medium";
11573
+ }
11574
+ } else if (node.children.length > 0) {
11575
+ if (depth > 2) {
11576
+ pattern = "tvf-cte";
11577
+ reason = `Multi-level parent-child hierarchy (depth: ${depth}). TVF with CTE recommended for efficient traversal.`;
11578
+ priority = "medium";
11579
+ sqlTemplate = generateTvfTemplate(node);
11580
+ } else {
11581
+ pattern = "simple-parentid";
11582
+ reason = `Simple two-level parent-child relationship. Standard FK relationship is optimal.`;
11583
+ priority = "low";
11584
+ }
11585
+ } else {
11586
+ return null;
11587
+ }
11588
+ return {
11589
+ entityName: node.name,
11590
+ currentPattern: node.isSelfReferencing ? "self-referencing" : "parent-child",
11591
+ recommendedPattern: pattern,
11592
+ reason,
11593
+ priority,
11594
+ hierarchyDepth: depth,
11595
+ estimatedNodes: totalNodes,
11596
+ sqlTemplate,
11597
+ implementationSteps: getImplementationSteps(pattern, node)
11598
+ };
11599
+ }
11600
+ function calculateMaxChildDepth(node, entityMap, visited = /* @__PURE__ */ new Set()) {
11601
+ if (visited.has(node.name)) return 0;
11602
+ visited.add(node.name);
11603
+ if (node.isSelfReferencing) {
11604
+ return 5;
11605
+ }
11606
+ if (node.children.length === 0) return 0;
11607
+ let maxChildDepth = 0;
11608
+ for (const childName of node.children) {
11609
+ const child = entityMap.get(childName);
11610
+ if (child) {
11611
+ const childDepth = calculateMaxChildDepth(child, entityMap, visited);
11612
+ maxChildDepth = Math.max(maxChildDepth, childDepth);
11613
+ }
11614
+ }
11615
+ return maxChildDepth + 1;
11616
+ }
11617
+ function countDescendants(node, entityMap, visited = /* @__PURE__ */ new Set()) {
11618
+ if (visited.has(node.name)) return 0;
11619
+ visited.add(node.name);
11620
+ let count = 0;
11621
+ for (const childName of node.children) {
11622
+ const child = entityMap.get(childName);
11623
+ if (child) {
11624
+ count += 1 + countDescendants(child, entityMap, visited);
11625
+ }
11626
+ }
11627
+ return count;
11628
+ }
11629
+ function generateTvfTemplate(node) {
11630
+ const tableName = `${node.name}s`;
11631
+ const idField = "Id";
11632
+ const parentField = node.selfReferenceField || "ParentId";
11633
+ return `-- Table-Valued Function with Recursive CTE
11634
+ -- Returns all ancestors/descendants for ${node.name}
11635
+ CREATE OR ALTER FUNCTION [core].[fn_Get${node.name}Hierarchy]
11636
+ (
11637
+ @${node.name}Id UNIQUEIDENTIFIER,
11638
+ @Direction VARCHAR(10) = 'ancestors' -- 'ancestors' or 'descendants'
11639
+ )
11640
+ RETURNS TABLE
11641
+ AS
11642
+ RETURN
11643
+ (
11644
+ WITH Hierarchy AS (
11645
+ -- Base case: starting node
11646
+ SELECT
11647
+ ${idField},
11648
+ ${parentField},
11649
+ 0 AS Level,
11650
+ CAST(${idField} AS VARCHAR(MAX)) AS Path
11651
+ FROM core.${tableName}
11652
+ WHERE ${idField} = @${node.name}Id
11653
+ AND IsDeleted = 0
11654
+
11655
+ UNION ALL
11656
+
11657
+ -- Recursive case: traverse hierarchy
11658
+ SELECT
11659
+ t.${idField},
11660
+ t.${parentField},
11661
+ h.Level + 1,
11662
+ h.Path + '/' + CAST(t.${idField} AS VARCHAR(MAX))
11663
+ FROM core.${tableName} t
11664
+ INNER JOIN Hierarchy h ON
11665
+ CASE @Direction
11666
+ WHEN 'ancestors' THEN t.${idField}
11667
+ WHEN 'descendants' THEN t.${parentField}
11668
+ END =
11669
+ CASE @Direction
11670
+ WHEN 'ancestors' THEN h.${parentField}
11671
+ WHEN 'descendants' THEN h.${idField}
11672
+ END
11673
+ WHERE t.IsDeleted = 0
11674
+ AND h.Level < 50 -- Prevent infinite recursion
11675
+ )
11676
+ SELECT
11677
+ ${idField},
11678
+ ${parentField},
11679
+ Level,
11680
+ Path
11681
+ FROM Hierarchy
11682
+ );
11683
+ GO
11684
+
11685
+ -- Usage example:
11686
+ -- SELECT * FROM core.fn_Get${node.name}Hierarchy(@id, 'descendants');
11687
+ -- SELECT * FROM core.fn_Get${node.name}Hierarchy(@id, 'ancestors');`;
11688
+ }
11689
+ function getImplementationSteps(pattern, node) {
11690
+ switch (pattern) {
11691
+ case "tvf-cte":
11692
+ return [
11693
+ `1. Create migration with TVF: fn_Get${node.name}Hierarchy`,
11694
+ `2. Add method to I${node.name}Repository: GetHierarchyAsync(Guid id, HierarchyDirection direction)`,
11695
+ `3. Implement using raw SQL: _context.Database.SqlQuery<${node.name}HierarchyDto>(...)`,
11696
+ `4. Create ${node.name}HierarchyDto with Id, ParentId, Level, Path`,
11697
+ `5. Add unit tests for hierarchy traversal (ancestors/descendants)`
11698
+ ];
11699
+ case "materialized-path":
11700
+ return [
11701
+ `1. Add Path column to ${node.name}: public string Path { get; private set; } // e.g., "/1/3/5/"`,
11702
+ `2. Add PathDepth computed column or property`,
11703
+ `3. Create index on Path for prefix queries`,
11704
+ `4. Update Create/Update methods to maintain Path`,
11705
+ `5. Query ancestors: WHERE @path LIKE Path + '%'`,
11706
+ `6. Query descendants: WHERE Path LIKE @path + '%'`
11707
+ ];
11708
+ case "simple-parentid":
11709
+ return [
11710
+ `1. Ensure ${node.selfReferenceField || "ParentId"} FK is configured`,
11711
+ `2. Add navigation property: public virtual ${node.name}? Parent { get; }`,
11712
+ `3. Add children collection: public virtual ICollection<${node.name}> Children { get; }`,
11713
+ `4. Use .Include() for eager loading when needed`
11714
+ ];
11715
+ case "nested-sets":
11716
+ return [
11717
+ `1. Add Left and Right columns (int)`,
11718
+ `2. Implement tree rebuild on modifications`,
11719
+ `3. Query descendants: WHERE Left > @left AND Right < @right`,
11720
+ `4. Query ancestors: WHERE Left < @left AND Right > @right`,
11721
+ `5. Warning: Updates are expensive - only use for read-heavy scenarios`
11722
+ ];
11723
+ case "closure-table":
11724
+ return [
11725
+ `1. Create ${node.name}Closure table: (AncestorId, DescendantId, Depth)`,
11726
+ `2. Populate closure table on Create/Update`,
11727
+ `3. Query all ancestors: WHERE DescendantId = @id`,
11728
+ `4. Query all descendants: WHERE AncestorId = @id`,
11729
+ `5. Maintain closure table on hierarchy changes`
11730
+ ];
11731
+ default:
11732
+ return [];
11733
+ }
11734
+ }
11735
+ function formatResult8(result, format, _rootPath) {
11736
+ if (format === "json") {
11737
+ return JSON.stringify(result, null, 2);
11738
+ }
11739
+ const lines = [];
11740
+ lines.push("# Entity Hierarchy Analysis");
11741
+ lines.push("");
11742
+ lines.push("## Summary");
11743
+ lines.push("");
11744
+ lines.push(`| Metric | Value |`);
11745
+ lines.push(`|--------|-------|`);
11746
+ lines.push(`| Total Entities | ${result.totalEntities} |`);
11747
+ lines.push(`| Hierarchical Entities | ${result.hierarchicalEntities} |`);
11748
+ lines.push(`| Self-Referencing | ${result.selfReferencingEntities} |`);
11749
+ lines.push(`| Max Depth | ${result.maxDepth} |`);
11750
+ lines.push("");
11751
+ if (result.circularDependencies.length > 0) {
11752
+ lines.push("## \u26A0\uFE0F Circular Dependencies Detected");
11753
+ lines.push("");
11754
+ for (const cycle of result.circularDependencies) {
11755
+ lines.push(`- ${cycle.join(" \u2192 ")}`);
11756
+ }
11757
+ lines.push("");
11758
+ }
11759
+ if (result.hierarchies.length > 0) {
11760
+ lines.push("## Detected Hierarchies");
11761
+ lines.push("");
11762
+ for (const h of result.hierarchies) {
11763
+ const icon = h.isSelfReferencing ? "\u{1F504}" : "\u{1F4C2}";
11764
+ lines.push(`### ${icon} ${h.name}`);
11765
+ lines.push("");
11766
+ lines.push(`| Property | Value |`);
11767
+ lines.push(`|----------|-------|`);
11768
+ lines.push(`| File | \`${h.file}\` |`);
11769
+ lines.push(`| Type | ${h.isSelfReferencing ? "Self-referencing" : "Parent-child"} |`);
11770
+ lines.push(`| Tenant-aware | ${h.isTenantAware ? "Yes" : "No"} |`);
11771
+ lines.push(`| Depth | ${h.depth} |`);
11772
+ if (h.selfReferenceField) {
11773
+ lines.push(`| Self-ref field | \`${h.selfReferenceField}\` |`);
11774
+ }
11775
+ if (h.children.length > 0) {
11776
+ lines.push(`| Children | ${h.children.join(", ")} |`);
11777
+ }
11778
+ if (h.childCollections.length > 0) {
11779
+ lines.push(`| Collections | ${h.childCollections.join(", ")} |`);
11780
+ }
11781
+ lines.push("");
11782
+ }
11783
+ }
11784
+ if (result.recommendations.length > 0) {
11785
+ lines.push("## Pattern Recommendations");
11786
+ lines.push("");
11787
+ const priorityOrder = ["critical", "high", "medium", "low"];
11788
+ const sorted = [...result.recommendations].sort(
11789
+ (a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority)
11790
+ );
11791
+ for (const rec of sorted) {
11792
+ const priorityIcon = {
11793
+ critical: "\u{1F534}",
11794
+ high: "\u{1F7E0}",
11795
+ medium: "\u{1F7E1}",
11796
+ low: "\u{1F7E2}"
11797
+ }[rec.priority];
11798
+ lines.push(`### ${priorityIcon} ${rec.entityName}`);
11799
+ lines.push("");
11800
+ lines.push(`**Recommended Pattern**: \`${rec.recommendedPattern}\``);
11801
+ lines.push("");
11802
+ lines.push(`**Reason**: ${rec.reason}`);
11803
+ lines.push("");
11804
+ lines.push(`**Priority**: ${rec.priority.toUpperCase()}`);
11805
+ lines.push("");
11806
+ if (format === "detailed") {
11807
+ lines.push("#### Implementation Steps");
11808
+ lines.push("");
11809
+ for (const step of rec.implementationSteps) {
11810
+ lines.push(step);
11811
+ }
11812
+ lines.push("");
11813
+ if (rec.sqlTemplate) {
11814
+ lines.push("#### SQL Template");
11815
+ lines.push("");
11816
+ lines.push("```sql");
11817
+ lines.push(rec.sqlTemplate);
11818
+ lines.push("```");
11819
+ lines.push("");
11820
+ }
11821
+ }
11822
+ }
11823
+ }
11824
+ lines.push("---");
11825
+ lines.push("");
11826
+ lines.push("## Pattern Legend");
11827
+ lines.push("");
11828
+ lines.push("| Pattern | Best For | Trade-offs |");
11829
+ lines.push("|---------|----------|------------|");
11830
+ lines.push("| **TVF + CTE** | Deep hierarchies, frequent traversal | Requires SQL Server, slightly more complex |");
11831
+ lines.push("| **Materialized Path** | Deep hierarchies, frequent modifications | Path maintenance overhead |");
11832
+ lines.push("| **Simple ParentId** | Shallow hierarchies (2-3 levels) | N+1 queries on deep traversal |");
11833
+ lines.push("| **Nested Sets** | Read-heavy, rarely modified | Expensive updates |");
11834
+ lines.push("| **Closure Table** | All operations fast | Storage overhead (N\xB2 entries) |");
11835
+ return lines.join("\n");
11836
+ }
11837
+
11838
+ // src/tools/review-code/types.ts
11839
+ var LAYER_RULES = {
11840
+ domain: {
11841
+ canImport: [],
11842
+ cannotImport: ["application", "infrastructure", "api", "web"]
11843
+ },
11844
+ application: {
11845
+ canImport: ["domain"],
11846
+ cannotImport: ["infrastructure", "api", "web"]
11847
+ },
11848
+ infrastructure: {
11849
+ canImport: ["domain", "application"],
11850
+ cannotImport: ["api", "web"]
11851
+ },
11852
+ api: {
11853
+ canImport: ["domain", "application", "infrastructure"],
11854
+ cannotImport: ["web"]
11855
+ },
11856
+ web: {
11857
+ canImport: ["domain", "application"],
11858
+ cannotImport: ["infrastructure"]
11859
+ }
11860
+ };
11861
+ var FINDING_PREFIXES = {
11862
+ security: "SEC",
11863
+ architecture: "ARCH",
11864
+ "hardcoded-values": "HARD",
11865
+ tests: "TEST",
11866
+ "ai-hallucinations": "AI",
11867
+ performance: "PERF",
11868
+ "dead-code": "DEAD",
11869
+ i18n: "I18N",
11870
+ accessibility: "A11Y"
11871
+ };
11872
+ var findingCounters = {};
11873
+ function generateFindingId(category) {
11874
+ const prefix = FINDING_PREFIXES[category];
11875
+ if (!findingCounters[prefix]) {
11876
+ findingCounters[prefix] = 0;
11877
+ }
11878
+ findingCounters[prefix]++;
11879
+ return `${prefix}-${String(findingCounters[prefix]).padStart(3, "0")}`;
11880
+ }
11881
+ function resetFindingCounters() {
11882
+ Object.keys(findingCounters).forEach((key) => {
11883
+ findingCounters[key] = 0;
11884
+ });
11885
+ }
11886
+
11887
+ // src/tools/review-code/checks/security.ts
11888
+ var SECRET_PATTERNS = [
11889
+ { pattern: /["']sk[-_]live[-_][a-zA-Z0-9]{20,}["']/gi, name: "Stripe Live Key" },
11890
+ { pattern: /["']sk[-_]test[-_][a-zA-Z0-9]{20,}["']/gi, name: "Stripe Test Key" },
11891
+ { pattern: /["'][a-zA-Z0-9]{40}["']/g, name: "Generic API Key (40 chars)" },
11892
+ { pattern: /password\s*[=:]\s*["'][^"']{4,}["']/gi, name: "Hardcoded Password" },
11893
+ { pattern: /apikey\s*[=:]\s*["'][^"']{8,}["']/gi, name: "Hardcoded API Key" },
11894
+ { pattern: /secret\s*[=:]\s*["'][^"']{8,}["']/gi, name: "Hardcoded Secret" },
11895
+ { pattern: /connectionstring\s*[=:]\s*["'][^"']+["']/gi, name: "Connection String" },
11896
+ { pattern: /Bearer\s+[a-zA-Z0-9\-_.]+/gi, name: "Bearer Token" },
11897
+ { pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/gi, name: "Private Key" }
11898
+ ];
11899
+ var SQL_INJECTION_PATTERNS = [
11900
+ { pattern: /\$["'`]SELECT\s+.+\s+FROM/gi, name: "String interpolation in SQL" },
11901
+ { pattern: /\+\s*["'].*SELECT.*FROM/gi, name: "Concatenation in SQL" },
11902
+ { pattern: /string\.Format\s*\(\s*["'].*SELECT/gi, name: "String.Format with SQL" },
11903
+ { pattern: /ExecuteSqlRaw\s*\(\s*\$"/gi, name: "ExecuteSqlRaw with interpolation" },
11904
+ { pattern: /FromSqlRaw\s*\(\s*\$"/gi, name: "FromSqlRaw with interpolation" }
11905
+ ];
11906
+ var XSS_PATTERNS = [
11907
+ { pattern: /innerHTML\s*=/gi, name: "innerHTML assignment" },
11908
+ { pattern: /dangerouslySetInnerHTML/gi, name: "dangerouslySetInnerHTML" },
11909
+ { pattern: /document\.write\s*\(/gi, name: "document.write" },
11910
+ { pattern: /eval\s*\(/gi, name: "eval() usage" }
11911
+ ];
11912
+ var CONTROLLER_WITHOUT_AUTH = /\[ApiController\][\s\S]*?public\s+class\s+\w+Controller/gi;
11913
+ async function checkSecurity(context) {
11914
+ const findings = [];
11915
+ for (const file of context.files) {
11916
+ if (!["csharp", "typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
11917
+ continue;
11918
+ }
11919
+ const lines = file.content.split("\n");
11920
+ for (const { pattern, name } of SECRET_PATTERNS) {
11921
+ let match;
11922
+ pattern.lastIndex = 0;
11923
+ while ((match = pattern.exec(file.content)) !== null) {
11924
+ const lineNumber = getLineNumber3(file.content, match.index);
11925
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
11926
+ if (isInComment(lineContent)) continue;
11927
+ findings.push({
11928
+ id: generateFindingId("security"),
11929
+ category: "security",
11930
+ severity: "blocking",
11931
+ title: `Hardcoded ${name}`,
11932
+ description: `Found a hardcoded ${name.toLowerCase()} in the source code. Secrets should never be committed to version control.`,
11933
+ file: file.relativePath,
11934
+ line: lineNumber,
11935
+ code: truncateCode2(lineContent),
11936
+ suggestion: "Move this secret to environment variables, Azure Key Vault, or a secure configuration provider.",
11937
+ autoFixable: false,
11938
+ cweId: "CWE-798",
11939
+ references: ["https://cwe.mitre.org/data/definitions/798.html"]
11940
+ });
11941
+ }
11942
+ }
11943
+ if (file.language === "csharp") {
11944
+ for (const { pattern, name } of SQL_INJECTION_PATTERNS) {
11945
+ let match;
11946
+ pattern.lastIndex = 0;
11947
+ while ((match = pattern.exec(file.content)) !== null) {
11948
+ const lineNumber = getLineNumber3(file.content, match.index);
11949
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
11950
+ if (isInComment(lineContent)) continue;
11951
+ findings.push({
11952
+ id: generateFindingId("security"),
11953
+ category: "security",
11954
+ severity: "blocking",
11955
+ title: "Potential SQL Injection",
11956
+ description: `${name} detected. This pattern can lead to SQL injection vulnerabilities.`,
11957
+ file: file.relativePath,
11958
+ line: lineNumber,
11959
+ code: truncateCode2(lineContent),
11960
+ suggestion: "Use parameterized queries or EF Core LINQ methods instead of string interpolation.",
11961
+ autoFixable: false,
11962
+ cweId: "CWE-89",
11963
+ references: ["https://cwe.mitre.org/data/definitions/89.html"]
11964
+ });
11965
+ }
11966
+ }
11967
+ const controllerMatch = CONTROLLER_WITHOUT_AUTH.exec(file.content);
11968
+ if (controllerMatch) {
11969
+ const hasAuthorize = /\[Authorize\]/i.test(file.content.slice(0, controllerMatch.index + 200));
11970
+ const hasAllowAnonymous = /\[AllowAnonymous\]/i.test(file.content.slice(0, controllerMatch.index + 200));
11971
+ const hasNavRoute = /\[NavRoute\(/i.test(file.content);
11972
+ if (!hasAuthorize && !hasAllowAnonymous && !hasNavRoute) {
11973
+ findings.push({
11974
+ id: generateFindingId("security"),
11975
+ category: "security",
11976
+ severity: "critical",
11977
+ title: "Missing Authorization Attribute",
11978
+ description: "This API controller does not have [Authorize], [AllowAnonymous], or [NavRoute] attribute.",
11979
+ file: file.relativePath,
11980
+ line: getLineNumber3(file.content, controllerMatch.index),
11981
+ suggestion: 'Add [Authorize] or [NavRoute("...")] attribute to secure the controller.',
11982
+ autoFixable: false,
11983
+ cweId: "CWE-862"
11984
+ });
11985
+ }
11986
+ }
11987
+ }
11988
+ if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
11989
+ for (const { pattern, name } of XSS_PATTERNS) {
11990
+ let match;
11991
+ pattern.lastIndex = 0;
11992
+ while ((match = pattern.exec(file.content)) !== null) {
11993
+ const lineNumber = getLineNumber3(file.content, match.index);
11994
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
11995
+ if (isInComment(lineContent)) continue;
11996
+ const contextStart = Math.max(0, match.index - 200);
11997
+ const contextEnd = Math.min(file.content.length, match.index + 200);
11998
+ const context2 = file.content.slice(contextStart, contextEnd).toLowerCase();
11999
+ if (context2.includes("sanitize") || context2.includes("dompurify") || context2.includes("escape")) {
12000
+ continue;
12001
+ }
12002
+ findings.push({
12003
+ id: generateFindingId("security"),
12004
+ category: "security",
12005
+ severity: "critical",
12006
+ title: "Potential XSS Vulnerability",
12007
+ description: `${name} detected without apparent sanitization. This can lead to Cross-Site Scripting attacks.`,
12008
+ file: file.relativePath,
12009
+ line: lineNumber,
12010
+ code: truncateCode2(lineContent),
12011
+ suggestion: "Sanitize user input before rendering. Use DOMPurify or React's built-in escaping.",
12012
+ autoFixable: false,
12013
+ cweId: "CWE-79",
12014
+ references: ["https://cwe.mitre.org/data/definitions/79.html"]
12015
+ });
12016
+ }
12017
+ }
12018
+ }
12019
+ }
12020
+ return findings;
12021
+ }
12022
+ function getLineNumber3(content, index) {
12023
+ return content.slice(0, index).split("\n").length;
12024
+ }
12025
+ function truncateCode2(code, maxLength = 100) {
12026
+ if (code.length <= maxLength) return code;
12027
+ return code.slice(0, maxLength - 3) + "...";
12028
+ }
12029
+ function isInComment(line) {
12030
+ const trimmed = line.trim();
12031
+ return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
12032
+ }
12033
+
12034
+ // src/tools/review-code/checks/architecture.ts
12035
+ var CSHARP_LAYER_PATTERNS = {
12036
+ domain: [
12037
+ /namespace\s+\w+\.Domain/i,
12038
+ /SmartStack\.Domain/i
12039
+ ],
12040
+ application: [
12041
+ /namespace\s+\w+\.Application/i,
12042
+ /SmartStack\.Application/i
12043
+ ],
12044
+ infrastructure: [
12045
+ /namespace\s+\w+\.Infrastructure/i,
12046
+ /SmartStack\.Infrastructure/i,
12047
+ /\.Persistence\./i,
12048
+ /DbContext/i
12049
+ ],
12050
+ api: [
12051
+ /namespace\s+\w+\.Api/i,
12052
+ /SmartStack\.Api/i,
12053
+ /\[ApiController\]/i
12054
+ ]
12055
+ };
12056
+ var CSHARP_IMPORT_PATTERNS = {
12057
+ domain: /using\s+[\w.]*\.Domain[.\w]*/gi,
12058
+ application: /using\s+[\w.]*\.Application[.\w]*/gi,
12059
+ infrastructure: /using\s+[\w.]*\.Infrastructure[.\w]*/gi,
12060
+ api: /using\s+[\w.]*\.Api[.\w]*/gi
12061
+ };
12062
+ var TS_LAYER_VIOLATIONS = [
12063
+ {
12064
+ pattern: /from\s+['"].*Infrastructure.*['"]/gi,
12065
+ layer: "infrastructure",
12066
+ message: "Web/React code should not import from Infrastructure layer"
12067
+ },
12068
+ {
12069
+ pattern: /from\s+['"].*\.Api.*['"]/gi,
12070
+ layer: "api",
12071
+ message: "Web/React code should not import from Api layer directly"
12072
+ }
12073
+ ];
12074
+ var DIRECT_DB_PATTERNS = [
12075
+ {
12076
+ pattern: /private\s+readonly\s+\w*DbContext/gi,
12077
+ name: "Direct DbContext field"
12078
+ },
12079
+ {
12080
+ pattern: /new\s+\w*DbContext\s*\(/gi,
12081
+ name: "DbContext instantiation"
12082
+ },
12083
+ {
12084
+ pattern: /IApplicationDbContext|ICoreDbContext/gi,
12085
+ name: "DbContext interface",
12086
+ allowed: ["service", "repository", "handler"]
12087
+ }
12088
+ ];
12089
+ async function checkArchitecture(context) {
12090
+ const findings = [];
12091
+ for (const file of context.files) {
12092
+ const lines = file.content.split("\n");
12093
+ if (file.language === "csharp") {
12094
+ const fileLayer = detectCsharpLayer(file.content, file.relativePath);
12095
+ if (fileLayer && LAYER_RULES[fileLayer]) {
12096
+ const rules = LAYER_RULES[fileLayer];
12097
+ for (const forbiddenLayer of rules.cannotImport) {
12098
+ const importPattern = CSHARP_IMPORT_PATTERNS[forbiddenLayer];
12099
+ if (!importPattern) continue;
12100
+ let match2;
12101
+ importPattern.lastIndex = 0;
12102
+ while ((match2 = importPattern.exec(file.content)) !== null) {
12103
+ const lineNumber = getLineNumber4(file.content, match2.index);
12104
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12105
+ findings.push({
12106
+ id: generateFindingId("architecture"),
12107
+ category: "architecture",
12108
+ severity: "critical",
12109
+ title: `Layer Violation: ${capitalize2(fileLayer)} \u2192 ${capitalize2(forbiddenLayer)}`,
12110
+ description: `The ${capitalize2(fileLayer)} layer should not import from the ${capitalize2(forbiddenLayer)} layer. This violates Clean Architecture principles.`,
12111
+ file: file.relativePath,
12112
+ line: lineNumber,
12113
+ code: truncateCode3(lineContent),
12114
+ suggestion: `Remove this import. ${getSuggestionForViolation(fileLayer, forbiddenLayer)}`,
12115
+ autoFixable: false
12116
+ });
12117
+ }
12118
+ }
12119
+ }
12120
+ const isController = /Controller\.cs$/i.test(file.relativePath);
12121
+ if (isController) {
12122
+ for (const { pattern, name: _name } of DIRECT_DB_PATTERNS) {
12123
+ let match2;
12124
+ pattern.lastIndex = 0;
12125
+ while ((match2 = pattern.exec(file.content)) !== null) {
12126
+ const hasComplexQuery = /\.Include\(/.test(file.content) && /\.ThenInclude\(/.test(file.content);
12127
+ if (hasComplexQuery) {
12128
+ const lineNumber = getLineNumber4(file.content, match2.index);
12129
+ findings.push({
12130
+ id: generateFindingId("architecture"),
12131
+ category: "architecture",
12132
+ severity: "warning",
12133
+ title: "Complex Query in Controller",
12134
+ description: "Controllers with complex queries (multiple Includes) should consider moving logic to a service.",
12135
+ file: file.relativePath,
12136
+ line: lineNumber,
12137
+ suggestion: "Consider extracting complex query logic to an Application layer service.",
12138
+ autoFixable: false
12139
+ });
12140
+ break;
12141
+ }
12142
+ }
12143
+ }
12144
+ }
12145
+ const newServicePattern = /new\s+(\w+Service)\s*\(/gi;
12146
+ let match;
12147
+ while ((match = newServicePattern.exec(file.content)) !== null) {
12148
+ const lineNumber = getLineNumber4(file.content, match.index);
12149
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12150
+ if (file.relativePath.includes("Test") || file.relativePath.includes(".test.")) continue;
12151
+ findings.push({
12152
+ id: generateFindingId("architecture"),
12153
+ category: "architecture",
12154
+ severity: "critical",
12155
+ title: "Service Instantiation Instead of DI",
12156
+ description: `Direct instantiation of ${match[1]} detected. Services should be injected via constructor, not instantiated with 'new'.`,
12157
+ file: file.relativePath,
12158
+ line: lineNumber,
12159
+ code: truncateCode3(lineContent),
12160
+ suggestion: "Inject the service interface via constructor dependency injection.",
12161
+ autoFixable: false
12162
+ });
12163
+ }
12164
+ }
12165
+ if (["typescript", "tsx"].includes(file.language) && file.layer === "web") {
12166
+ for (const { pattern, message } of TS_LAYER_VIOLATIONS) {
12167
+ let match;
12168
+ pattern.lastIndex = 0;
12169
+ while ((match = pattern.exec(file.content)) !== null) {
12170
+ const lineNumber = getLineNumber4(file.content, match.index);
12171
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12172
+ findings.push({
12173
+ id: generateFindingId("architecture"),
12174
+ category: "architecture",
12175
+ severity: "critical",
12176
+ title: "Web Layer Violation",
12177
+ description: message,
12178
+ file: file.relativePath,
12179
+ line: lineNumber,
12180
+ code: truncateCode3(lineContent),
12181
+ suggestion: "Use API client services to communicate with the backend instead of direct imports.",
12182
+ autoFixable: false
12183
+ });
12184
+ }
12185
+ }
12186
+ }
12187
+ }
12188
+ return findings;
12189
+ }
12190
+ function detectCsharpLayer(content, filePath) {
12191
+ const pathLower = filePath.toLowerCase();
12192
+ if (pathLower.includes("/domain/") || pathLower.includes("\\domain\\")) return "domain";
12193
+ if (pathLower.includes("/application/") || pathLower.includes("\\application\\")) return "application";
12194
+ if (pathLower.includes("/infrastructure/") || pathLower.includes("\\infrastructure\\")) return "infrastructure";
12195
+ if (pathLower.includes("/api/") || pathLower.includes("\\api\\")) return "api";
12196
+ for (const [layer, patterns] of Object.entries(CSHARP_LAYER_PATTERNS)) {
12197
+ for (const pattern of patterns) {
12198
+ if (pattern.test(content)) {
12199
+ return layer;
12200
+ }
12201
+ }
12202
+ }
12203
+ return null;
12204
+ }
12205
+ function getSuggestionForViolation(fromLayer, toLayer) {
12206
+ if (fromLayer === "domain" && toLayer === "application") {
12207
+ return "Domain entities should not depend on Application layer. Move shared interfaces to Domain.";
12208
+ }
12209
+ if (fromLayer === "application" && toLayer === "infrastructure") {
12210
+ return "Define an interface in Application layer and implement it in Infrastructure.";
12211
+ }
12212
+ if (fromLayer === "api" && toLayer === "web") {
12213
+ return "API controllers should not depend on Web layer.";
12214
+ }
12215
+ if (toLayer === "infrastructure") {
12216
+ return "Use dependency injection with interfaces defined in Application layer.";
12217
+ }
12218
+ return "Refactor to respect Clean Architecture layer boundaries.";
12219
+ }
12220
+ function getLineNumber4(content, index) {
12221
+ return content.slice(0, index).split("\n").length;
12222
+ }
12223
+ function truncateCode3(code, maxLength = 100) {
12224
+ if (code.length <= maxLength) return code;
12225
+ return code.slice(0, maxLength - 3) + "...";
12226
+ }
12227
+ function capitalize2(str) {
12228
+ return str.charAt(0).toUpperCase() + str.slice(1);
12229
+ }
12230
+
12231
+ // src/tools/review-code/checks/hardcoded-values.ts
12232
+ var ACCEPTABLE_NUMBERS = /* @__PURE__ */ new Set([
12233
+ 0,
12234
+ 1,
12235
+ -1,
12236
+ 2,
12237
+ 10,
12238
+ 100,
12239
+ 1e3,
12240
+ // Common sizes
12241
+ 8,
12242
+ 16,
12243
+ 32,
12244
+ 64,
12245
+ 128,
12246
+ 256,
12247
+ 512,
12248
+ 1024,
12249
+ // HTTP status codes (often used as comparisons)
12250
+ 200,
12251
+ 201,
12252
+ 204,
12253
+ 400,
12254
+ 401,
12255
+ 403,
12256
+ 404,
12257
+ 500
12258
+ ]);
12259
+ var HARDCODED_URL_PATTERN = /["'](https?:\/\/(?!localhost)[^"']+)["']/gi;
12260
+ var HARDCODED_EMAIL_PATTERN = /["']([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})["']/gi;
12261
+ var HARDCODED_IP_PATTERN = /["'](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})["']/gi;
12262
+ var TIMEOUT_PATTERNS = [
12263
+ { pattern: /timeout\s*[=:]\s*(\d{4,})/gi, name: "Timeout value" },
12264
+ { pattern: /TimeSpan\.FromSeconds\s*\(\s*(\d+)\s*\)/gi, name: "TimeSpan.FromSeconds" },
12265
+ { pattern: /TimeSpan\.FromMinutes\s*\(\s*(\d+)\s*\)/gi, name: "TimeSpan.FromMinutes" },
12266
+ { pattern: /setTimeout\s*\(\s*[^,]+,\s*(\d{4,})\s*\)/gi, name: "setTimeout" },
12267
+ { pattern: /delay\s*[=:]\s*(\d{4,})/gi, name: "Delay value" }
12268
+ ];
12269
+ var MAGIC_NUMBER_PATTERN = /(?<![a-zA-Z0-9_.])\b(\d+)\b(?![a-zA-Z0-9_])/g;
12270
+ var FEATURE_FLAG_PATTERNS = [
12271
+ { pattern: /if\s*\(\s*(true|false)\s*\)/gi, name: "Hardcoded boolean condition" },
12272
+ { pattern: /enabled\s*[=:]\s*(true|false)/gi, name: "Hardcoded enabled flag" }
12273
+ ];
12274
+ async function checkHardcodedValues(context) {
12275
+ const findings = [];
12276
+ for (const file of context.files) {
12277
+ if (isConfigOrConstFile(file.relativePath)) continue;
12278
+ const lines = file.content.split("\n");
12279
+ let match;
12280
+ HARDCODED_URL_PATTERN.lastIndex = 0;
12281
+ while ((match = HARDCODED_URL_PATTERN.exec(file.content)) !== null) {
12282
+ const lineNumber = getLineNumber5(file.content, match.index);
12283
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12284
+ if (isInComment2(lineContent) || isDocExample(lineContent)) continue;
12285
+ findings.push({
12286
+ id: generateFindingId("hardcoded-values"),
12287
+ category: "hardcoded-values",
12288
+ severity: "warning",
12289
+ title: "Hardcoded URL",
12290
+ description: `Found hardcoded URL: ${truncateCode4(match[1], 50)}. URLs should be in configuration.`,
12291
+ file: file.relativePath,
12292
+ line: lineNumber,
12293
+ code: truncateCode4(lineContent),
12294
+ suggestion: "Move this URL to appsettings.json, environment variables, or a configuration service.",
12295
+ autoFixable: false
12296
+ });
12297
+ }
12298
+ HARDCODED_EMAIL_PATTERN.lastIndex = 0;
12299
+ while ((match = HARDCODED_EMAIL_PATTERN.exec(file.content)) !== null) {
12300
+ const lineNumber = getLineNumber5(file.content, match.index);
12301
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12302
+ if (isInComment2(lineContent) || isDocExample(lineContent)) continue;
12303
+ if (match[1].includes("@test") || match[1].includes("@example")) continue;
12304
+ findings.push({
12305
+ id: generateFindingId("hardcoded-values"),
12306
+ category: "hardcoded-values",
12307
+ severity: "warning",
12308
+ title: "Hardcoded Email",
12309
+ description: `Found hardcoded email: ${match[1]}. Consider using configuration.`,
12310
+ file: file.relativePath,
12311
+ line: lineNumber,
12312
+ code: truncateCode4(lineContent),
12313
+ suggestion: "Move this email to configuration or use a configurable notification service.",
12314
+ autoFixable: false
12315
+ });
12316
+ }
12317
+ HARDCODED_IP_PATTERN.lastIndex = 0;
12318
+ while ((match = HARDCODED_IP_PATTERN.exec(file.content)) !== null) {
12319
+ const lineNumber = getLineNumber5(file.content, match.index);
12320
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12321
+ if (isInComment2(lineContent)) continue;
12322
+ if (match[1] === "127.0.0.1" || match[1] === "0.0.0.0") continue;
12323
+ findings.push({
12324
+ id: generateFindingId("hardcoded-values"),
12325
+ category: "hardcoded-values",
12326
+ severity: "warning",
12327
+ title: "Hardcoded IP Address",
12328
+ description: `Found hardcoded IP address: ${match[1]}. This should be configurable.`,
12329
+ file: file.relativePath,
12330
+ line: lineNumber,
12331
+ code: truncateCode4(lineContent),
12332
+ suggestion: "Move this IP address to configuration.",
12333
+ autoFixable: false
12334
+ });
12335
+ }
12336
+ for (const { pattern, name } of TIMEOUT_PATTERNS) {
12337
+ pattern.lastIndex = 0;
12338
+ while ((match = pattern.exec(file.content)) !== null) {
12339
+ const value = parseInt(match[1], 10);
12340
+ const lineNumber = getLineNumber5(file.content, match.index);
12341
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12342
+ if (isInComment2(lineContent)) continue;
12343
+ if (value > 1e3 || value > 60 && name.includes("Minutes")) {
12344
+ findings.push({
12345
+ id: generateFindingId("hardcoded-values"),
12346
+ category: "hardcoded-values",
12347
+ severity: "warning",
12348
+ title: "Hardcoded Timeout",
12349
+ description: `${name} with hardcoded value ${value}. Consider making timeouts configurable.`,
12350
+ file: file.relativePath,
12351
+ line: lineNumber,
12352
+ code: truncateCode4(lineContent),
12353
+ suggestion: "Extract timeout values to configuration for easier adjustment in different environments.",
12354
+ autoFixable: false
12355
+ });
12356
+ }
12357
+ }
12358
+ }
12359
+ if (isBusinessLogicFile(file.relativePath) && file.language === "csharp") {
12360
+ checkMagicNumbers(file.content, file.relativePath, lines, findings);
12361
+ }
12362
+ for (const { pattern, name } of FEATURE_FLAG_PATTERNS) {
12363
+ pattern.lastIndex = 0;
12364
+ while ((match = pattern.exec(file.content)) !== null) {
12365
+ const lineNumber = getLineNumber5(file.content, match.index);
12366
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12367
+ if (isInComment2(lineContent)) continue;
12368
+ if (lineContent.includes("DEBUG") || lineContent.includes("TEST")) continue;
12369
+ findings.push({
12370
+ id: generateFindingId("hardcoded-values"),
12371
+ category: "hardcoded-values",
12372
+ severity: "warning",
12373
+ title: "Hardcoded Feature Flag",
12374
+ description: `${name} detected. Feature toggles should be configurable, not hardcoded.`,
12375
+ file: file.relativePath,
12376
+ line: lineNumber,
12377
+ code: truncateCode4(lineContent),
12378
+ suggestion: "Use a feature flag system or configuration service instead of hardcoded boolean values.",
12379
+ autoFixable: false
12380
+ });
12381
+ }
12382
+ }
12383
+ }
12384
+ return findings;
12385
+ }
12386
+ function checkMagicNumbers(content, filePath, lines, findings) {
12387
+ const magicNumbersFound = /* @__PURE__ */ new Map();
12388
+ let match;
12389
+ MAGIC_NUMBER_PATTERN.lastIndex = 0;
12390
+ while ((match = MAGIC_NUMBER_PATTERN.exec(content)) !== null) {
12391
+ const value = parseInt(match[1], 10);
12392
+ const lineNumber = getLineNumber5(content, match.index);
12393
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12394
+ if (ACCEPTABLE_NUMBERS.has(value)) continue;
12395
+ if (isInComment2(lineContent)) continue;
12396
+ if (/const\s+\w+\s*=/.test(lineContent) || /readonly\s+\w+\s*=/.test(lineContent)) continue;
12397
+ if (/\[\s*\d+\s*\]/.test(lineContent)) continue;
12398
+ if (!magicNumbersFound.has(value)) {
12399
+ magicNumbersFound.set(value, []);
12400
+ }
12401
+ magicNumbersFound.get(value).push(lineNumber);
12402
+ }
12403
+ for (const [value, lineNumbers] of magicNumbersFound) {
12404
+ if (lineNumbers.length >= 2) {
12405
+ findings.push({
12406
+ id: generateFindingId("hardcoded-values"),
12407
+ category: "hardcoded-values",
12408
+ severity: "warning",
12409
+ title: "Magic Number",
12410
+ description: `Magic number ${value} appears ${lineNumbers.length} times. Consider extracting to a named constant.`,
12411
+ file: filePath,
12412
+ line: lineNumbers[0],
12413
+ suggestion: `Extract ${value} to a well-named constant that explains its purpose.`,
12414
+ autoFixable: true
12415
+ });
12416
+ }
12417
+ }
12418
+ }
12419
+ function isConfigOrConstFile(filePath) {
12420
+ const lower = filePath.toLowerCase();
12421
+ return lower.includes("config") || lower.includes("constant") || lower.includes("settings") || lower.includes("appsettings") || lower.includes(".json") || lower.includes("test") || lower.includes(".spec.") || lower.includes(".test.");
12422
+ }
12423
+ function isBusinessLogicFile(filePath) {
12424
+ const lower = filePath.toLowerCase();
12425
+ return lower.includes("service") || lower.includes("handler") || lower.includes("controller") || lower.includes("validator");
12426
+ }
12427
+ function isDocExample(line) {
12428
+ if (line.includes("/// ") || line.includes("* ")) return true;
12429
+ const hasExampleKeyword = /\bexample\b/i.test(line);
12430
+ const isExampleInUrl = /example\.(com|org|net)/i.test(line);
12431
+ return hasExampleKeyword && !isExampleInUrl;
12432
+ }
12433
+ function getLineNumber5(content, index) {
12434
+ return content.slice(0, index).split("\n").length;
12435
+ }
12436
+ function truncateCode4(code, maxLength = 100) {
12437
+ if (code.length <= maxLength) return code;
12438
+ return code.slice(0, maxLength - 3) + "...";
12439
+ }
12440
+ function isInComment2(line) {
12441
+ const trimmed = line.trim();
12442
+ return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
12443
+ }
12444
+
12445
+ // src/tools/review-code/checks/tests.ts
12446
+ var ENTITY_PATTERN = /public\s+class\s+(\w+)\s*:\s*(Base|System)Entity/gi;
12447
+ var SERVICE_PATTERN = /public\s+class\s+(\w+Service)\s*:\s*I\w+Service/gi;
12448
+ var CONTROLLER_PATTERN = /public\s+class\s+(\w+Controller)\s*:\s*ControllerBase/gi;
12449
+ var VALIDATOR_PATTERN = /public\s+class\s+(\w+Validator)\s*:\s*AbstractValidator/gi;
12450
+ var USELESS_ASSERTION_PATTERNS = [
12451
+ /Assert\.True\s*\(\s*true\s*\)/gi,
12452
+ /Assert\.False\s*\(\s*false\s*\)/gi,
12453
+ /\.Should\(\)\.BeTrue\(\)/gi,
12454
+ /expect\s*\(\s*true\s*\)\s*\.toBe\s*\(\s*true\s*\)/gi,
12455
+ /expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)/gi
12456
+ ];
12457
+ async function checkTests(context) {
12458
+ const findings = [];
12459
+ const entities = /* @__PURE__ */ new Set();
12460
+ const services = /* @__PURE__ */ new Set();
12461
+ const controllers = /* @__PURE__ */ new Set();
12462
+ const validators = /* @__PURE__ */ new Set();
12463
+ const existingTests = /* @__PURE__ */ new Set();
12464
+ for (const file of context.files) {
12465
+ if (file.language !== "csharp") continue;
12466
+ let match;
12467
+ ENTITY_PATTERN.lastIndex = 0;
12468
+ while ((match = ENTITY_PATTERN.exec(file.content)) !== null) {
12469
+ entities.add(match[1]);
12470
+ }
12471
+ SERVICE_PATTERN.lastIndex = 0;
12472
+ while ((match = SERVICE_PATTERN.exec(file.content)) !== null) {
12473
+ services.add(match[1]);
12474
+ }
12475
+ CONTROLLER_PATTERN.lastIndex = 0;
12476
+ while ((match = CONTROLLER_PATTERN.exec(file.content)) !== null) {
12477
+ controllers.add(match[1]);
12478
+ }
12479
+ VALIDATOR_PATTERN.lastIndex = 0;
12480
+ while ((match = VALIDATOR_PATTERN.exec(file.content)) !== null) {
12481
+ validators.add(match[1]);
12482
+ }
12483
+ if (file.relativePath.includes("Tests") || file.relativePath.includes(".test.")) {
12484
+ const testClassMatch = /public\s+class\s+(\w+)Tests/gi.exec(file.content);
12485
+ if (testClassMatch) {
12486
+ existingTests.add(testClassMatch[1]);
12487
+ }
12488
+ checkTestQuality(file, findings);
12489
+ }
12490
+ }
12491
+ checkMissingTests(entities, existingTests, "Entity", findings);
12492
+ checkMissingTests(services, existingTests, "Service", findings);
12493
+ checkMissingTests(controllers, existingTests, "Controller", findings);
12494
+ checkMissingTests(validators, existingTests, "Validator", findings);
12495
+ return findings;
12496
+ }
12497
+ function checkMissingTests(components, existingTests, type, findings) {
12498
+ for (const component of components) {
12499
+ const baseName = component.replace(/(Service|Controller|Validator)$/, "");
12500
+ const hasTest = existingTests.has(component) || existingTests.has(baseName) || existingTests.has(`${baseName}${type}`);
12501
+ if (!hasTest) {
12502
+ const priority = getPriority(type);
12503
+ findings.push({
12504
+ id: generateFindingId("tests"),
12505
+ category: "tests",
12506
+ severity: priority === "critical" ? "critical" : "warning",
12507
+ title: `Missing Tests for ${component}`,
12508
+ description: `No test file found for ${type.toLowerCase()} ${component}. ${type}s should have comprehensive test coverage.`,
12509
+ file: `Tests/Unit/${type}s/${component}Tests.cs`,
12510
+ suggestion: `Create a test file: Tests/Unit/${type}s/${component}Tests.cs`,
12511
+ autoFixable: false
12512
+ });
12513
+ }
12514
+ }
12515
+ }
12516
+ function checkTestQuality(file, findings) {
12517
+ for (const pattern of USELESS_ASSERTION_PATTERNS) {
12518
+ let match;
12519
+ pattern.lastIndex = 0;
12520
+ while ((match = pattern.exec(file.content)) !== null) {
12521
+ const lineNumber = getLineNumber6(file.content, match.index);
12522
+ findings.push({
12523
+ id: generateFindingId("tests"),
12524
+ category: "tests",
12525
+ severity: "warning",
12526
+ title: "Useless Test Assertion",
12527
+ description: "This assertion always passes and doesn't test any behavior.",
12528
+ file: file.relativePath,
12529
+ line: lineNumber,
12530
+ code: truncateCode5(match[0]),
12531
+ suggestion: "Replace with meaningful assertions that verify actual behavior.",
12532
+ autoFixable: false
12533
+ });
12534
+ }
12535
+ }
12536
+ const factMatches = file.content.matchAll(/\[Fact\]\s*\n\s*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)/gi);
12537
+ for (const match of factMatches) {
12538
+ const methodName = match[1];
12539
+ const methodStart = match.index || 0;
12540
+ const bodyStart = file.content.indexOf("{", methodStart);
12541
+ const bodyEnd = findMatchingBrace(file.content, bodyStart);
12542
+ if (bodyStart !== -1 && bodyEnd !== -1) {
12543
+ const methodBody = file.content.slice(bodyStart, bodyEnd);
12544
+ const hasAssertion = /Assert\./i.test(methodBody) || /\.Should\(/i.test(methodBody) || /expect\s*\(/i.test(methodBody) || /Verify\s*\(/i.test(methodBody);
12545
+ if (!hasAssertion) {
12546
+ const lineNumber = getLineNumber6(file.content, methodStart);
12547
+ findings.push({
12548
+ id: generateFindingId("tests"),
12549
+ category: "tests",
12550
+ severity: "warning",
12551
+ title: "Test Without Assertions",
12552
+ description: `Test method ${methodName} does not appear to have any assertions.`,
12553
+ file: file.relativePath,
12554
+ line: lineNumber,
12555
+ suggestion: "Add assertions to verify the expected behavior. A test without assertions doesn't prove anything.",
12556
+ autoFixable: false
12557
+ });
12558
+ }
12559
+ }
12560
+ }
12561
+ }
12562
+ function getPriority(type) {
12563
+ switch (type) {
12564
+ case "Service":
12565
+ return "critical";
12566
+ case "Controller":
12567
+ return "high";
12568
+ case "Validator":
12569
+ return "high";
12570
+ case "Entity":
12571
+ return "medium";
12572
+ default:
12573
+ return "low";
12574
+ }
12575
+ }
12576
+ function findMatchingBrace(content, start) {
12577
+ let depth = 0;
12578
+ for (let i = start; i < content.length; i++) {
12579
+ if (content[i] === "{") depth++;
12580
+ if (content[i] === "}") {
12581
+ depth--;
12582
+ if (depth === 0) return i;
12583
+ }
12584
+ }
12585
+ return -1;
12586
+ }
12587
+ function getLineNumber6(content, index) {
12588
+ return content.slice(0, index).split("\n").length;
12589
+ }
12590
+ function truncateCode5(code, maxLength = 100) {
12591
+ if (code.length <= maxLength) return code;
12592
+ return code.slice(0, maxLength - 3) + "...";
12593
+ }
12594
+
12595
+ // src/tools/review-code/checks/ai-hallucinations.ts
12596
+ var FAKE_DOTNET_NAMESPACES = [
12597
+ "System.AI",
12598
+ "System.Machine",
12599
+ "Microsoft.AI.OpenAI",
12600
+ "Microsoft.Extensions.AI",
12601
+ "Azure.AI.TextAnalytics",
12602
+ // Often confused
12603
+ "System.Data.Linq"
12604
+ // Obsolete
12605
+ ];
12606
+ var FAKE_NPM_PACKAGES = [
12607
+ "@react/router",
12608
+ // Should be react-router-dom
12609
+ "@microsoft/auth",
12610
+ // Various incorrect forms
12611
+ "react-query/v4",
12612
+ // Incorrect path
12613
+ "@tanstack/query"
12614
+ // Should be @tanstack/react-query
12615
+ ];
12616
+ var AI_CODE_PATTERNS = [
12617
+ {
12618
+ pattern: /\/\/\s*TODO:\s*implement/gi,
12619
+ name: "Unimplemented TODO",
12620
+ description: "AI-generated placeholder that was not completed"
12621
+ },
12622
+ {
12623
+ pattern: /throw\s+new\s+NotImplementedException\s*\(\s*\)/gi,
12624
+ name: "NotImplementedException",
12625
+ description: "Method stub that was not implemented"
12626
+ },
12627
+ {
12628
+ pattern: /\/\/\s*\.\.\./gi,
12629
+ name: "Ellipsis comment",
12630
+ description: "AI placeholder comment indicating incomplete code"
12631
+ },
12632
+ {
12633
+ pattern: /\/\*\s*\.\.\.\s*\*\//gi,
12634
+ name: "Ellipsis block comment",
12635
+ description: "AI placeholder indicating incomplete code"
12636
+ }
12637
+ ];
12638
+ var FAKE_METHODS = {
12639
+ string: ["IsEmpty", "IsNotEmpty", "ToInt", "ToDouble", "SafeTrim"],
12640
+ List: ["AddIfNotExists", "SafeAdd", "GetOrDefault", "FindOrCreate"],
12641
+ Dictionary: ["GetOrAdd", "TryAdd", "SafeGet"],
12642
+ // Some exist in ConcurrentDictionary
12643
+ Task: ["WaitAll", "WhenAny"],
12644
+ // Should be Task.WaitAll, Task.WhenAny (static)
12645
+ DateTime: ["ToISOString", "FromISOString"]
12646
+ // JavaScript confusion
12647
+ };
12648
+ var CSHARP_USING_PATTERN = /using\s+([\w.]+);/gi;
12649
+ var TS_IMPORT_PATTERN = /import\s+.*\s+from\s+['"]([^'"]+)['"]/gi;
12650
+ async function checkAiHallucinations(context) {
12651
+ const findings = [];
12652
+ const existingFiles = new Set(context.files.map((f) => f.relativePath.toLowerCase()));
12653
+ const existingModules = /* @__PURE__ */ new Set();
12654
+ for (const file of context.files) {
12655
+ if (file.language === "csharp") {
12656
+ const namespaceMatch = /namespace\s+([\w.]+)/gi.exec(file.content);
12657
+ if (namespaceMatch) {
12658
+ existingModules.add(namespaceMatch[1].toLowerCase());
12659
+ }
12660
+ }
12661
+ }
12662
+ for (const file of context.files) {
12663
+ const lines = file.content.split("\n");
12664
+ for (const { pattern, name, description } of AI_CODE_PATTERNS) {
12665
+ let match;
12666
+ pattern.lastIndex = 0;
12667
+ while ((match = pattern.exec(file.content)) !== null) {
12668
+ const lineNumber = getLineNumber7(file.content, match.index);
12669
+ findings.push({
12670
+ id: generateFindingId("ai-hallucinations"),
12671
+ category: "ai-hallucinations",
12672
+ severity: "critical",
12673
+ title: `Incomplete Code: ${name}`,
12674
+ description,
12675
+ file: file.relativePath,
12676
+ line: lineNumber,
12677
+ code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
12678
+ suggestion: "Complete the implementation or remove the placeholder.",
12679
+ autoFixable: false
12680
+ });
12681
+ }
12682
+ }
12683
+ if (file.language === "csharp") {
12684
+ let match;
12685
+ CSHARP_USING_PATTERN.lastIndex = 0;
12686
+ while ((match = CSHARP_USING_PATTERN.exec(file.content)) !== null) {
12687
+ const namespace = match[1];
12688
+ for (const fakeNs of FAKE_DOTNET_NAMESPACES) {
12689
+ if (namespace.startsWith(fakeNs)) {
12690
+ const lineNumber = getLineNumber7(file.content, match.index);
12691
+ findings.push({
12692
+ id: generateFindingId("ai-hallucinations"),
12693
+ category: "ai-hallucinations",
12694
+ severity: "blocking",
12695
+ title: "Non-existent Namespace",
12696
+ description: `The namespace '${namespace}' does not exist in .NET. This may be AI-hallucinated code.`,
12697
+ file: file.relativePath,
12698
+ line: lineNumber,
12699
+ code: truncateCode6(match[0]),
12700
+ suggestion: "Verify this namespace exists. Check NuGet packages or use the correct namespace.",
12701
+ autoFixable: false
12702
+ });
12703
+ }
12704
+ }
12705
+ }
12706
+ for (const [typeName, fakeMethods] of Object.entries(FAKE_METHODS)) {
12707
+ for (const fakeMethod of fakeMethods) {
12708
+ const methodPattern = new RegExp(`\\.${fakeMethod}\\s*\\(`, "gi");
12709
+ let methodMatch;
12710
+ while ((methodMatch = methodPattern.exec(file.content)) !== null) {
12711
+ const lineNumber = getLineNumber7(file.content, methodMatch.index);
12712
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12713
+ const contextStart = Math.max(0, methodMatch.index - 50);
12714
+ const context2 = file.content.slice(contextStart, methodMatch.index).toLowerCase();
12715
+ if (context2.includes(typeName.toLowerCase()) || typeName === "string") {
12716
+ findings.push({
12717
+ id: generateFindingId("ai-hallucinations"),
12718
+ category: "ai-hallucinations",
12719
+ severity: "blocking",
12720
+ title: `Non-existent Method: ${fakeMethod}`,
12721
+ description: `The method '${fakeMethod}' does not exist on ${typeName} in .NET. This is likely AI-hallucinated code.`,
12722
+ file: file.relativePath,
12723
+ line: lineNumber,
12724
+ code: truncateCode6(lineContent),
12725
+ suggestion: `Use the correct .NET method. For example: string.IsNullOrEmpty() instead of IsEmpty().`,
12726
+ autoFixable: false
12727
+ });
12728
+ }
12729
+ }
12730
+ }
12731
+ }
12732
+ }
12733
+ if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
12734
+ let match;
12735
+ TS_IMPORT_PATTERN.lastIndex = 0;
12736
+ while ((match = TS_IMPORT_PATTERN.exec(file.content)) !== null) {
12737
+ const importPath = match[1];
12738
+ for (const fakePkg of FAKE_NPM_PACKAGES) {
12739
+ if (importPath === fakePkg || importPath.startsWith(fakePkg + "/")) {
12740
+ const lineNumber = getLineNumber7(file.content, match.index);
12741
+ findings.push({
12742
+ id: generateFindingId("ai-hallucinations"),
12743
+ category: "ai-hallucinations",
12744
+ severity: "blocking",
12745
+ title: "Non-existent Package",
12746
+ description: `The package '${importPath}' does not exist. This may be AI-hallucinated code.`,
12747
+ file: file.relativePath,
12748
+ line: lineNumber,
12749
+ code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
12750
+ suggestion: "Verify this package exists on npm. Check the correct package name.",
12751
+ autoFixable: false
12752
+ });
12753
+ }
12754
+ }
12755
+ if (importPath.startsWith("./") || importPath.startsWith("../")) {
12756
+ const normalizedPath = resolveRelativePath(file.relativePath, importPath);
12757
+ const possibleExtensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
12758
+ const exists = possibleExtensions.some(
12759
+ (ext) => existingFiles.has(normalizedPath.toLowerCase() + ext) || existingFiles.has(normalizedPath.toLowerCase())
12760
+ );
12761
+ if (!exists && !importPath.includes("*")) {
12762
+ const lineNumber = getLineNumber7(file.content, match.index);
12763
+ findings.push({
12764
+ id: generateFindingId("ai-hallucinations"),
12765
+ category: "ai-hallucinations",
12766
+ severity: "critical",
12767
+ title: "Import Path Not Found",
12768
+ description: `The import path '${importPath}' does not resolve to an existing file.`,
12769
+ file: file.relativePath,
12770
+ line: lineNumber,
12771
+ code: truncateCode6(lines[lineNumber - 1]?.trim() || ""),
12772
+ suggestion: "Verify the file exists or fix the import path.",
12773
+ autoFixable: false
12774
+ });
12775
+ }
12776
+ }
12777
+ }
12778
+ }
12779
+ }
12780
+ return findings;
12781
+ }
12782
+ function resolveRelativePath(fromFile, importPath) {
12783
+ fromFile = fromFile.replace(/\\/g, "/");
12784
+ importPath = importPath.replace(/\\/g, "/");
12785
+ const fromDir = fromFile.substring(0, fromFile.lastIndexOf("/"));
12786
+ const parts = fromDir.split("/");
12787
+ for (const segment of importPath.split("/")) {
12788
+ if (segment === "..") {
12789
+ parts.pop();
12790
+ } else if (segment !== ".") {
12791
+ parts.push(segment);
12792
+ }
12793
+ }
12794
+ return parts.join("/");
12795
+ }
12796
+ function getLineNumber7(content, index) {
12797
+ return content.slice(0, index).split("\n").length;
12798
+ }
12799
+ function truncateCode6(code, maxLength = 100) {
12800
+ if (code.length <= maxLength) return code;
12801
+ return code.slice(0, maxLength - 3) + "...";
12802
+ }
12803
+
12804
+ // src/tools/review-code/checks/performance.ts
12805
+ var N_PLUS_ONE_PATTERNS = [
12806
+ {
12807
+ // Query inside foreach/for loop
12808
+ pattern: /(?:foreach|for)\s*\([^)]+\)\s*\{[^}]*(?:await\s+)?_context\.\w+\./gi,
12809
+ name: "Query in loop",
12810
+ description: "Database query inside a loop can cause N+1 performance issues"
12811
+ },
12812
+ {
12813
+ // Nested Select with query
12814
+ pattern: /\.Select\s*\([^)]*=>[^)]*\.(?:First|Single|Where|Any)\s*\(/gi,
12815
+ name: "Nested query in Select",
12816
+ description: "Nested query inside Select projection causes N+1"
12817
+ },
12818
+ {
12819
+ // Lazy loading trigger
12820
+ pattern: /(?:foreach|for)\s*\([^)]+\)\s*\{[^}]*\.\w+Navigation\./gi,
12821
+ name: "Navigation property in loop",
12822
+ description: "Accessing navigation property in loop triggers lazy loading"
12823
+ }
12824
+ ];
12825
+ var OVER_FETCHING_PATTERNS = [
12826
+ {
12827
+ // Multiple Include calls
12828
+ pattern: /\.Include\s*\([^)]+\)\s*\.Include\s*\([^)]+\)\s*\.Include\s*\([^)]+\)/gi,
12829
+ name: "Triple Include",
12830
+ description: "Too many Include calls can load excessive data"
12831
+ },
12832
+ {
12833
+ // ToList before Where
12834
+ pattern: /\.ToList(?:Async)?\s*\(\s*\)\s*\.(?:Where|Select|OrderBy)/gi,
12835
+ name: "ToList before filtering",
12836
+ description: "ToList() before filtering loads all records into memory"
12837
+ },
12838
+ {
12839
+ // GetAll without pagination
12840
+ pattern: /GetAll(?:Async)?\s*\(\s*\)/gi,
12841
+ name: "GetAll without pagination",
12842
+ description: "Fetching all records without pagination can cause memory issues"
12843
+ }
12844
+ ];
12845
+ var FRONTEND_PERF_PATTERNS = [
12846
+ {
12847
+ // Multiple fetch in useEffect
12848
+ pattern: /useEffect\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*fetch[^}]*fetch/gi,
12849
+ name: "Multiple fetches in useEffect",
12850
+ description: "Multiple sequential API calls in useEffect should be combined or parallelized"
12851
+ },
12852
+ {
12853
+ // Fetch without dependency array
12854
+ pattern: /useEffect\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*(?:fetch|axios|api\.)[^}]*\}\s*\)/gi,
12855
+ name: "Fetch without dependencies",
12856
+ description: "API call in useEffect without dependency array runs on every render"
12857
+ },
12858
+ {
12859
+ // setState in loop
12860
+ pattern: /(?:for|while|forEach)\s*\([^)]*\)[^{]*\{[^}]*set\w+\s*\(/gi,
12861
+ name: "setState in loop",
12862
+ description: "Calling setState in a loop causes multiple re-renders"
12863
+ },
12864
+ {
12865
+ // Large inline objects in JSX
12866
+ pattern: /style\s*=\s*\{\s*\{[^}]{100,}\}/gi,
12867
+ name: "Large inline style object",
12868
+ description: "Large inline objects create new references on each render"
12869
+ }
12870
+ ];
12871
+ var MISSING_OPTIMIZATION_PATTERNS = [
12872
+ {
12873
+ // No pagination in list queries
12874
+ pattern: /\.ToListAsync\s*\(\s*\)/gi,
12875
+ contextCheck: (content, index) => {
12876
+ const context = content.slice(Math.max(0, index - 200), index);
12877
+ return !context.includes("Take") && !context.includes("Skip") && !context.includes("PageSize");
12878
+ },
12879
+ name: "List query without pagination",
12880
+ description: "Large list queries should use pagination"
12881
+ },
12882
+ {
12883
+ // No AsNoTracking for read-only queries
12884
+ pattern: /\.(?:Where|Select|Include)[^;]*\.ToListAsync/gi,
12885
+ contextCheck: (content, index) => {
12886
+ const context = content.slice(Math.max(0, index - 300), index + 100);
12887
+ return !context.includes("AsNoTracking") && !context.includes("Update") && !context.includes("Add");
12888
+ },
12889
+ name: "Missing AsNoTracking",
12890
+ description: "Read-only queries should use AsNoTracking for better performance"
12891
+ }
12892
+ ];
12893
+ async function checkPerformance(context) {
12894
+ const findings = [];
12895
+ for (const file of context.files) {
12896
+ const lines = file.content.split("\n");
12897
+ if (file.language === "csharp") {
12898
+ for (const { pattern, name, description } of N_PLUS_ONE_PATTERNS) {
12899
+ let match;
12900
+ pattern.lastIndex = 0;
12901
+ while ((match = pattern.exec(file.content)) !== null) {
12902
+ const lineNumber = getLineNumber8(file.content, match.index);
12903
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12904
+ findings.push({
12905
+ id: generateFindingId("performance"),
12906
+ category: "performance",
12907
+ severity: "critical",
12908
+ title: `N+1 Query: ${name}`,
12909
+ description,
12910
+ file: file.relativePath,
12911
+ line: lineNumber,
12912
+ code: truncateCode7(lineContent),
12913
+ suggestion: "Use Include() or batch the queries. Consider using .Select() projection to load only needed data.",
12914
+ autoFixable: false,
12915
+ references: ["https://docs.microsoft.com/en-us/ef/core/querying/related-data"]
12916
+ });
12917
+ }
12918
+ }
12919
+ for (const { pattern, name, description } of OVER_FETCHING_PATTERNS) {
12920
+ let match;
12921
+ pattern.lastIndex = 0;
12922
+ while ((match = pattern.exec(file.content)) !== null) {
12923
+ const lineNumber = getLineNumber8(file.content, match.index);
12924
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12925
+ findings.push({
12926
+ id: generateFindingId("performance"),
12927
+ category: "performance",
12928
+ severity: "warning",
12929
+ title: `Over-fetching: ${name}`,
12930
+ description,
12931
+ file: file.relativePath,
12932
+ line: lineNumber,
12933
+ code: truncateCode7(lineContent),
12934
+ suggestion: "Consider using .Select() projection to load only the fields you need, or split into multiple queries.",
12935
+ autoFixable: false
12936
+ });
12937
+ }
12938
+ }
12939
+ for (const { pattern, contextCheck, name, description } of MISSING_OPTIMIZATION_PATTERNS) {
12940
+ let match;
12941
+ pattern.lastIndex = 0;
12942
+ while ((match = pattern.exec(file.content)) !== null) {
12943
+ if (contextCheck && !contextCheck(file.content, match.index)) continue;
12944
+ const lineNumber = getLineNumber8(file.content, match.index);
12945
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12946
+ if (file.relativePath.includes("Test")) continue;
12947
+ findings.push({
12948
+ id: generateFindingId("performance"),
12949
+ category: "performance",
12950
+ severity: "warning",
12951
+ title: `Performance: ${name}`,
12952
+ description,
12953
+ file: file.relativePath,
12954
+ line: lineNumber,
12955
+ code: truncateCode7(lineContent),
12956
+ suggestion: name.includes("pagination") ? "Add .Skip() and .Take() for pagination." : "Add .AsNoTracking() for read-only queries.",
12957
+ autoFixable: false
12958
+ });
12959
+ }
12960
+ }
12961
+ }
12962
+ if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
12963
+ for (const { pattern, name, description } of FRONTEND_PERF_PATTERNS) {
12964
+ let match;
12965
+ pattern.lastIndex = 0;
12966
+ while ((match = pattern.exec(file.content)) !== null) {
12967
+ const lineNumber = getLineNumber8(file.content, match.index);
12968
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
12969
+ findings.push({
12970
+ id: generateFindingId("performance"),
12971
+ category: "performance",
12972
+ severity: "warning",
12973
+ title: `Frontend Performance: ${name}`,
12974
+ description,
12975
+ file: file.relativePath,
12976
+ line: lineNumber,
12977
+ code: truncateCode7(lineContent),
12978
+ suggestion: getSuggestionForFrontendPerf(name),
12979
+ autoFixable: false
12980
+ });
12981
+ }
12982
+ }
12983
+ checkMissingMemoization(file, lines, findings);
12984
+ }
12985
+ }
12986
+ return findings;
12987
+ }
12988
+ function checkMissingMemoization(file, lines, findings) {
12989
+ const expensivePatterns = [
12990
+ /\.filter\s*\([^)]+\)\.map\s*\(/gi,
12991
+ /\.reduce\s*\([^)]+\)/gi,
12992
+ /\.sort\s*\([^)]*\)/gi,
12993
+ /JSON\.parse\s*\(/gi
12994
+ ];
12995
+ for (const pattern of expensivePatterns) {
12996
+ let match;
12997
+ pattern.lastIndex = 0;
12998
+ while ((match = pattern.exec(file.content)) !== null) {
12999
+ const lineNumber = getLineNumber8(file.content, match.index);
13000
+ const contextStart = Math.max(0, match.index - 100);
13001
+ const context = file.content.slice(contextStart, match.index);
13002
+ if (context.includes("useMemo") || context.includes("useCallback")) continue;
13003
+ const beforeMatch = file.content.slice(Math.max(0, match.index - 500), match.index);
13004
+ if (beforeMatch.includes("return (") || beforeMatch.includes("return <")) {
13005
+ findings.push({
13006
+ id: generateFindingId("performance"),
13007
+ category: "performance",
13008
+ severity: "warning",
13009
+ title: "Expensive Computation in Render",
13010
+ description: "Expensive array operations in render body should be memoized.",
13011
+ file: file.relativePath,
13012
+ line: lineNumber,
13013
+ code: truncateCode7(lines[lineNumber - 1]?.trim() || ""),
13014
+ suggestion: "Wrap this computation with useMemo() to prevent recalculation on every render.",
13015
+ autoFixable: false
13016
+ });
13017
+ }
13018
+ }
13019
+ }
13020
+ }
13021
+ function getSuggestionForFrontendPerf(name) {
13022
+ const suggestions = {
13023
+ "Multiple fetches in useEffect": "Use Promise.all() to parallelize requests or combine into a single API call.",
13024
+ "Fetch without dependencies": "Add a dependency array to prevent the effect from running on every render.",
13025
+ "setState in loop": "Batch state updates or use a reducer pattern.",
13026
+ "Large inline style object": "Extract styles to a useMemo() hook or a constant outside the component."
13027
+ };
13028
+ return suggestions[name] || "Optimize this code for better performance.";
13029
+ }
13030
+ function getLineNumber8(content, index) {
13031
+ return content.slice(0, index).split("\n").length;
13032
+ }
13033
+ function truncateCode7(code, maxLength = 100) {
13034
+ if (code.length <= maxLength) return code;
13035
+ return code.slice(0, maxLength - 3) + "...";
13036
+ }
13037
+
13038
+ // src/tools/review-code/checks/dead-code.ts
13039
+ var COMMENTED_CODE_PATTERNS = [
13040
+ {
13041
+ pattern: /\/\/\s*(?:if|for|while|switch|return|var|let|const|public|private|async)\s+/gi,
13042
+ name: "Commented code block"
13043
+ },
13044
+ {
13045
+ pattern: /\/\*[\s\S]*?(?:if|for|while|return|function)[\s\S]*?\*\//gi,
13046
+ name: "Commented code block"
13047
+ }
13048
+ ];
13049
+ var TODO_PATTERNS = [
13050
+ {
13051
+ pattern: /\/\/\s*TODO[:\s]/gi,
13052
+ name: "TODO comment"
13053
+ },
13054
+ {
13055
+ pattern: /\/\/\s*FIXME[:\s]/gi,
13056
+ name: "FIXME comment"
13057
+ },
13058
+ {
13059
+ pattern: /\/\/\s*HACK[:\s]/gi,
13060
+ name: "HACK comment"
13061
+ },
13062
+ {
13063
+ pattern: /\/\/\s*XXX[:\s]/gi,
13064
+ name: "XXX comment"
13065
+ }
13066
+ ];
13067
+ var CSHARP_UNUSED_IMPORT_PATTERN = /using\s+([\w.]+);/gi;
13068
+ var CSHARP_PRIVATE_METHOD_PATTERN = /private\s+(?:async\s+)?(?:static\s+)?(?:Task<?\w*>?|\w+)\s+(\w+)\s*\(/gi;
13069
+ var CSHARP_PRIVATE_FIELD_PATTERN = /private\s+(?:readonly\s+)?(?:static\s+)?\w+(?:<[^>]+>)?\s+_?(\w+)\s*[;=]/gi;
13070
+ async function checkDeadCode(context) {
13071
+ const findings = [];
13072
+ for (const file of context.files) {
13073
+ const lines = file.content.split("\n");
13074
+ for (const { pattern } of COMMENTED_CODE_PATTERNS) {
13075
+ let match;
13076
+ pattern.lastIndex = 0;
13077
+ while ((match = pattern.exec(file.content)) !== null) {
13078
+ const lineNumber = getLineNumber9(file.content, match.index);
13079
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
13080
+ if (lineContent.includes("///") || lineContent.includes("/**")) continue;
13081
+ findings.push({
13082
+ id: generateFindingId("dead-code"),
13083
+ category: "dead-code",
13084
+ severity: "warning",
13085
+ title: "Commented Code",
13086
+ description: "Commented-out code should be removed. Use version control to preserve history.",
13087
+ file: file.relativePath,
13088
+ line: lineNumber,
13089
+ code: truncateCode8(lineContent),
13090
+ suggestion: "Remove commented code. If needed later, it can be recovered from git history.",
13091
+ autoFixable: true
13092
+ });
13093
+ }
13094
+ }
13095
+ for (const { pattern, name } of TODO_PATTERNS) {
13096
+ let match;
13097
+ pattern.lastIndex = 0;
13098
+ while ((match = pattern.exec(file.content)) !== null) {
13099
+ const lineNumber = getLineNumber9(file.content, match.index);
13100
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
13101
+ findings.push({
13102
+ id: generateFindingId("dead-code"),
13103
+ category: "dead-code",
13104
+ severity: "info",
13105
+ title: `${name} Found`,
13106
+ description: `${name} indicates incomplete or temporary code that may need attention.`,
13107
+ file: file.relativePath,
13108
+ line: lineNumber,
13109
+ code: truncateCode8(lineContent),
13110
+ suggestion: "Address the TODO/FIXME or create a ticket to track it.",
13111
+ autoFixable: false
13112
+ });
13113
+ }
13114
+ }
13115
+ if (file.language === "csharp") {
13116
+ const privateMethods = /* @__PURE__ */ new Map();
13117
+ let match;
13118
+ CSHARP_PRIVATE_METHOD_PATTERN.lastIndex = 0;
13119
+ while ((match = CSHARP_PRIVATE_METHOD_PATTERN.exec(file.content)) !== null) {
13120
+ privateMethods.set(match[1], getLineNumber9(file.content, match.index));
13121
+ }
13122
+ for (const [methodName, lineNumber] of privateMethods) {
13123
+ const regex = new RegExp(`\\b${methodName}\\s*\\(`, "g");
13124
+ const occurrences = (file.content.match(regex) || []).length;
13125
+ if (occurrences === 1) {
13126
+ findings.push({
13127
+ id: generateFindingId("dead-code"),
13128
+ category: "dead-code",
13129
+ severity: "warning",
13130
+ title: "Unused Private Method",
13131
+ description: `Private method '${methodName}' is declared but never called.`,
13132
+ file: file.relativePath,
13133
+ line: lineNumber,
13134
+ suggestion: "Remove the unused method or make it public if it should be accessible.",
13135
+ autoFixable: true
13136
+ });
13137
+ }
13138
+ }
13139
+ const privateFields = /* @__PURE__ */ new Map();
13140
+ CSHARP_PRIVATE_FIELD_PATTERN.lastIndex = 0;
13141
+ while ((match = CSHARP_PRIVATE_FIELD_PATTERN.exec(file.content)) !== null) {
13142
+ const fieldName = match[1];
13143
+ if (fieldName.startsWith("_") && fieldName.length > 1) {
13144
+ privateFields.set(fieldName, getLineNumber9(file.content, match.index));
13145
+ }
13146
+ }
13147
+ for (const [fieldName, lineNumber] of privateFields) {
13148
+ const regex = new RegExp(`\\b${fieldName}\\b`, "g");
13149
+ const occurrences = (file.content.match(regex) || []).length;
13150
+ if (occurrences === 1) {
13151
+ findings.push({
13152
+ id: generateFindingId("dead-code"),
13153
+ category: "dead-code",
13154
+ severity: "warning",
13155
+ title: "Unused Private Field",
13156
+ description: `Private field '${fieldName}' is declared but never used.`,
13157
+ file: file.relativePath,
13158
+ line: lineNumber,
13159
+ suggestion: "Remove the unused field.",
13160
+ autoFixable: true
13161
+ });
13162
+ }
13163
+ }
13164
+ checkUnusedCsharpUsings(file, lines, findings);
13165
+ }
13166
+ if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
13167
+ checkUnusedJsImports(file, lines, findings);
13168
+ checkUnusedJsVariables(file, lines, findings);
13169
+ }
13170
+ }
13171
+ return findings;
13172
+ }
13173
+ function checkUnusedCsharpUsings(file, _lines, findings) {
13174
+ const usings = /* @__PURE__ */ new Map();
13175
+ let match;
13176
+ CSHARP_UNUSED_IMPORT_PATTERN.lastIndex = 0;
13177
+ while ((match = CSHARP_UNUSED_IMPORT_PATTERN.exec(file.content)) !== null) {
13178
+ const namespace = match[1];
13179
+ usings.set(namespace, getLineNumber9(file.content, match.index));
13180
+ }
13181
+ const contentAfterUsings = file.content.replace(/using\s+[\w.]+;/g, "");
13182
+ for (const [namespace, lineNumber] of usings) {
13183
+ const parts = namespace.split(".");
13184
+ const lastPart = parts[parts.length - 1];
13185
+ const isUsed = contentAfterUsings.includes(lastPart) || contentAfterUsings.includes(namespace);
13186
+ const alwaysNeeded = ["System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks"];
13187
+ if (alwaysNeeded.includes(namespace)) continue;
13188
+ if (!isUsed) {
13189
+ findings.push({
13190
+ id: generateFindingId("dead-code"),
13191
+ category: "dead-code",
13192
+ severity: "info",
13193
+ title: "Potentially Unused Using",
13194
+ description: `The using '${namespace}' may not be needed in this file.`,
13195
+ file: file.relativePath,
13196
+ line: lineNumber,
13197
+ suggestion: "Remove unused using statements to keep the file clean.",
13198
+ autoFixable: true
13199
+ });
13200
+ }
13201
+ }
13202
+ }
13203
+ function checkUnusedJsImports(file, _lines, findings) {
13204
+ const imports = /* @__PURE__ */ new Map();
13205
+ let match;
13206
+ const importPattern = /import\s+(?:\{([^}]+)\}|(\w+)(?:\s*,\s*\{([^}]+)\})?)\s+from\s+['"][^'"]+['"]/gi;
13207
+ while ((match = importPattern.exec(file.content)) !== null) {
13208
+ const lineNumber = getLineNumber9(file.content, match.index);
13209
+ const namedImports = match[1] || match[3];
13210
+ const defaultImport = match[2];
13211
+ if (namedImports) {
13212
+ const names = namedImports.split(",").map((n) => {
13213
+ const parts = n.trim().split(/\s+as\s+/);
13214
+ return parts[parts.length - 1].trim();
13215
+ });
13216
+ for (const name of names) {
13217
+ if (name) imports.set(name, { line: lineNumber, full: match[0] });
13218
+ }
13219
+ }
13220
+ if (defaultImport && defaultImport !== "React") {
13221
+ imports.set(defaultImport, { line: lineNumber, full: match[0] });
13222
+ }
13223
+ }
13224
+ const contentAfterImports = file.content.replace(/import\s+.*from\s+['"][^'"]+['"]/g, "");
13225
+ for (const [name, { line }] of imports) {
13226
+ const regex = new RegExp(`\\b${name}\\b`, "g");
13227
+ const occurrences = (contentAfterImports.match(regex) || []).length;
13228
+ if (occurrences === 0) {
13229
+ findings.push({
13230
+ id: generateFindingId("dead-code"),
13231
+ category: "dead-code",
13232
+ severity: "warning",
13233
+ title: "Unused Import",
13234
+ description: `Import '${name}' is declared but never used.`,
13235
+ file: file.relativePath,
13236
+ line,
13237
+ suggestion: "Remove unused imports.",
13238
+ autoFixable: true
13239
+ });
13240
+ }
13241
+ }
13242
+ }
13243
+ function checkUnusedJsVariables(file, _lines, findings) {
13244
+ const varPattern = /(?:const|let)\s+(\w+)\s*=\s*(?!function|async\s+function)/gi;
13245
+ const variables = /* @__PURE__ */ new Map();
13246
+ let match;
13247
+ while ((match = varPattern.exec(file.content)) !== null) {
13248
+ const varName = match[1];
13249
+ if (varName.startsWith("_") || varName === "i" || varName === "j") continue;
13250
+ variables.set(varName, getLineNumber9(file.content, match.index));
13251
+ }
13252
+ for (const [varName, lineNumber] of variables) {
13253
+ const regex = new RegExp(`\\b${varName}\\b`, "g");
13254
+ const occurrences = (file.content.match(regex) || []).length;
13255
+ if (occurrences === 1) {
13256
+ findings.push({
13257
+ id: generateFindingId("dead-code"),
13258
+ category: "dead-code",
13259
+ severity: "warning",
13260
+ title: "Unused Variable",
13261
+ description: `Variable '${varName}' is declared but never used.`,
13262
+ file: file.relativePath,
13263
+ line: lineNumber,
13264
+ suggestion: "Remove unused variables or prefix with underscore if intentionally unused.",
13265
+ autoFixable: true
13266
+ });
13267
+ }
13268
+ }
13269
+ }
13270
+ function getLineNumber9(content, index) {
13271
+ return content.slice(0, index).split("\n").length;
13272
+ }
13273
+ function truncateCode8(code, maxLength = 100) {
13274
+ if (code.length <= maxLength) return code;
13275
+ return code.slice(0, maxLength - 3) + "...";
13276
+ }
13277
+
13278
+ // src/tools/review-code/checks/i18n.ts
13279
+ var JSX_TEXT_PATTERN = />([A-Z][a-z]+(?:\s+[a-z]+)*[.!?]?)</gi;
13280
+ var JSX_ATTR_PATTERNS = [
13281
+ { pattern: /placeholder\s*=\s*["']([^"']+)["']/gi, name: "placeholder" },
13282
+ { pattern: /title\s*=\s*["']([A-Z][^"']+)["']/gi, name: "title" },
13283
+ { pattern: /aria-label\s*=\s*["']([^"']+)["']/gi, name: "aria-label" },
13284
+ { pattern: /alt\s*=\s*["']([A-Z][^"']+)["']/gi, name: "alt" },
13285
+ { pattern: /label\s*=\s*["']([A-Z][^"']+)["']/gi, name: "label" }
13286
+ ];
13287
+ var CSHARP_ERROR_PATTERNS = [
13288
+ { pattern: /throw\s+new\s+\w*Exception\s*\(\s*["']([^"']+)["']/gi, name: "Exception message" },
13289
+ { pattern: /ModelState\.AddModelError\s*\([^,]+,\s*["']([^"']+)["']/gi, name: "Validation error" },
13290
+ { pattern: /\.WithMessage\s*\(\s*["']([^"']+)["']/gi, name: "FluentValidation message" },
13291
+ { pattern: /BadRequest\s*\(\s*["']([^"']+)["']/gi, name: "BadRequest message" },
13292
+ { pattern: /NotFound\s*\(\s*["']([^"']+)["']/gi, name: "NotFound message" }
13293
+ ];
13294
+ var NOTIFICATION_PATTERNS = [
13295
+ { pattern: /toast\.\w+\s*\(\s*["']([^"']+)["']/gi, name: "Toast notification" },
13296
+ { pattern: /notify\s*\(\s*["']([^"']+)["']/gi, name: "Notification" },
13297
+ { pattern: /alert\s*\(\s*["']([^"']+)["']/gi, name: "Alert" },
13298
+ { pattern: /confirm\s*\(\s*["']([^"']+)["']/gi, name: "Confirm dialog" }
13299
+ ];
13300
+ var EXCLUDED_PATTERNS = [
13301
+ /^[a-z]+$/i,
13302
+ // Single words (likely identifiers)
13303
+ /^[A-Z_]+$/,
13304
+ // CONSTANTS
13305
+ /^\d+$/,
13306
+ // Numbers
13307
+ /^[a-z0-9-_]+$/i,
13308
+ // IDs, slugs
13309
+ /^https?:\/\//i,
13310
+ // URLs
13311
+ /^[a-zA-Z0-9._%+-]+@/,
13312
+ // Emails
13313
+ /^\{.*\}$/,
13314
+ // Template variables
13315
+ /^#[0-9a-f]+$/i
13316
+ // Color codes
13317
+ ];
13318
+ async function checkI18n(context) {
13319
+ const findings = [];
13320
+ for (const file of context.files) {
13321
+ if (!isUiFile(file.relativePath)) continue;
13322
+ const lines = file.content.split("\n");
13323
+ if (["tsx", "jsx"].includes(file.language)) {
13324
+ let match;
13325
+ JSX_TEXT_PATTERN.lastIndex = 0;
13326
+ while ((match = JSX_TEXT_PATTERN.exec(file.content)) !== null) {
13327
+ const text = match[1].trim();
13328
+ if (!shouldExclude(text) && text.length > 2) {
13329
+ const lineNumber = getLineNumber10(file.content, match.index);
13330
+ findings.push({
13331
+ id: generateFindingId("i18n"),
13332
+ category: "i18n",
13333
+ severity: "warning",
13334
+ title: "Hardcoded UI Text",
13335
+ description: `Text "${truncateCode9(text, 30)}" should use translation function.`,
13336
+ file: file.relativePath,
13337
+ line: lineNumber,
13338
+ code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
13339
+ suggestion: `Use {t('key')} instead of hardcoded text. Example: {t('buttons.submit')}`,
13340
+ autoFixable: false
13341
+ });
13342
+ }
13343
+ }
13344
+ for (const { pattern, name } of JSX_ATTR_PATTERNS) {
13345
+ pattern.lastIndex = 0;
13346
+ while ((match = pattern.exec(file.content)) !== null) {
13347
+ const text = match[1].trim();
13348
+ if (!shouldExclude(text) && text.length > 2) {
13349
+ const lineNumber = getLineNumber10(file.content, match.index);
13350
+ findings.push({
13351
+ id: generateFindingId("i18n"),
13352
+ category: "i18n",
13353
+ severity: "warning",
13354
+ title: `Non-translated ${name}`,
13355
+ description: `The ${name} attribute "${truncateCode9(text, 30)}" should be translated.`,
13356
+ file: file.relativePath,
13357
+ line: lineNumber,
13358
+ code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
13359
+ suggestion: `Use {t('key')} for the ${name} attribute.`,
13360
+ autoFixable: false
13361
+ });
13362
+ }
13363
+ }
13364
+ }
13365
+ for (const { pattern, name } of NOTIFICATION_PATTERNS) {
13366
+ pattern.lastIndex = 0;
13367
+ while ((match = pattern.exec(file.content)) !== null) {
13368
+ const text = match[1].trim();
13369
+ if (!shouldExclude(text) && text.length > 2) {
13370
+ const lineNumber = getLineNumber10(file.content, match.index);
13371
+ findings.push({
13372
+ id: generateFindingId("i18n"),
13373
+ category: "i18n",
13374
+ severity: "warning",
13375
+ title: `Non-translated ${name}`,
13376
+ description: `${name} message "${truncateCode9(text, 30)}" should be translated.`,
13377
+ file: file.relativePath,
13378
+ line: lineNumber,
13379
+ code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
13380
+ suggestion: `Use t('key') for notification messages.`,
13381
+ autoFixable: false
13382
+ });
13383
+ }
13384
+ }
13385
+ }
13386
+ if (!file.content.includes("useTranslation") && !file.content.includes("from 'react-i18next'")) {
13387
+ const hasHardcodedStrings = JSX_TEXT_PATTERN.test(file.content);
13388
+ if (hasHardcodedStrings) {
13389
+ findings.push({
13390
+ id: generateFindingId("i18n"),
13391
+ category: "i18n",
13392
+ severity: "info",
13393
+ title: "Missing i18n Setup",
13394
+ description: "This file has UI text but does not import useTranslation hook.",
13395
+ file: file.relativePath,
13396
+ line: 1,
13397
+ suggestion: "Add: import { useTranslation } from 'react-i18next';",
13398
+ autoFixable: true
13399
+ });
13400
+ }
13401
+ }
13402
+ }
13403
+ if (file.language === "csharp") {
13404
+ for (const { pattern, name } of CSHARP_ERROR_PATTERNS) {
13405
+ let match;
13406
+ pattern.lastIndex = 0;
13407
+ while ((match = pattern.exec(file.content)) !== null) {
13408
+ const text = match[1].trim();
13409
+ if (text.length > 10 && !shouldExclude(text)) {
13410
+ const lineNumber = getLineNumber10(file.content, match.index);
13411
+ findings.push({
13412
+ id: generateFindingId("i18n"),
13413
+ category: "i18n",
13414
+ severity: "warning",
13415
+ title: `Non-translated ${name}`,
13416
+ description: `${name} "${truncateCode9(text, 30)}" should use localization.`,
13417
+ file: file.relativePath,
13418
+ line: lineNumber,
13419
+ code: truncateCode9(lines[lineNumber - 1]?.trim() || ""),
13420
+ suggestion: "Use IStringLocalizer or resource files for error messages.",
13421
+ autoFixable: false
13422
+ });
13423
+ }
13424
+ }
13425
+ }
13426
+ }
13427
+ }
13428
+ return findings;
13429
+ }
13430
+ function isUiFile(filePath) {
13431
+ const lower = filePath.toLowerCase();
13432
+ return lower.includes("/pages/") || lower.includes("/components/") || lower.includes("/views/") || lower.includes("/ui/") || lower.endsWith(".tsx") || lower.endsWith(".jsx") || lower.includes("controller") && lower.endsWith(".cs");
13433
+ }
13434
+ function shouldExclude(text) {
13435
+ for (const pattern of EXCLUDED_PATTERNS) {
13436
+ if (pattern.test(text)) return true;
13437
+ }
13438
+ if (text.length <= 2) return true;
13439
+ if (text.includes("(") || text.includes(")") || text.includes("{")) return true;
13440
+ const alphaChars = text.replace(/[^a-zA-Z]/g, "").length;
13441
+ if (alphaChars < text.length * 0.5) return true;
13442
+ return false;
13443
+ }
13444
+ function getLineNumber10(content, index) {
13445
+ return content.slice(0, index).split("\n").length;
13446
+ }
13447
+ function truncateCode9(code, maxLength = 100) {
13448
+ if (code.length <= maxLength) return code;
13449
+ return code.slice(0, maxLength - 3) + "...";
13450
+ }
13451
+
13452
+ // src/tools/review-code/checks/accessibility.ts
13453
+ var IMG_WITHOUT_ALT = /<img(?![^>]*\balt\s*=)[^>]*>/gi;
13454
+ var IMG_EMPTY_ALT = /<img[^>]*\balt\s*=\s*["']\s*["'][^>]*>/gi;
13455
+ var LINK_WITHOUT_TEXT = /<a[^>]*>\s*<\/a>/gi;
13456
+ var LINK_ONLY_ICON = /<a[^>]*>\s*<(?:svg|i|span[^>]*class="[^"]*icon)[^>]*>\s*<\/a>/gi;
13457
+ var BUTTON_WITHOUT_TEXT = /<button[^>]*>\s*<(?:svg|i|span[^>]*icon)[^>]*>\s*<\/button>/gi;
13458
+ var ONCLICK_ON_DIV = /<div[^>]*onClick[^>]*>/gi;
13459
+ var ONCLICK_ON_SPAN = /<span[^>]*onClick[^>]*>/gi;
13460
+ var HEADING_PATTERN = /<h([1-6])/gi;
13461
+ var POSITIVE_TABINDEX = /tabIndex\s*=\s*\{?\s*["']?([1-9]\d*)["']?\s*\}?/gi;
13462
+ var OUTLINE_NONE = /outline\s*:\s*none|outline\s*:\s*0/gi;
13463
+ var ARIA_HIDDEN_FOCUSABLE = /aria-hidden\s*=\s*["']true["'][^>]*(?:tabIndex|onClick|onKeyDown)/gi;
13464
+ async function checkAccessibility(context) {
13465
+ const findings = [];
13466
+ for (const file of context.files) {
13467
+ if (!["tsx", "jsx", "typescript", "javascript"].includes(file.language)) continue;
13468
+ if (!isUiFile2(file.relativePath)) continue;
13469
+ const lines = file.content.split("\n");
13470
+ checkPattern(file, lines, IMG_WITHOUT_ALT, {
13471
+ title: "Image Missing Alt Text",
13472
+ description: "Images must have an alt attribute for screen readers.",
13473
+ severity: "critical",
13474
+ suggestion: 'Add alt="description" to the image. Use alt="" only for decorative images.',
13475
+ cweId: void 0
13476
+ }, findings);
13477
+ let match;
13478
+ IMG_EMPTY_ALT.lastIndex = 0;
13479
+ while ((match = IMG_EMPTY_ALT.exec(file.content)) !== null) {
13480
+ if (match[0].includes("aria-hidden") || match[0].includes('role="presentation"')) continue;
13481
+ const lineNumber = getLineNumber11(file.content, match.index);
13482
+ findings.push({
13483
+ id: generateFindingId("accessibility"),
13484
+ category: "accessibility",
13485
+ severity: "warning",
13486
+ title: "Empty Alt on Potentially Informative Image",
13487
+ description: "Empty alt text should only be used for decorative images.",
13488
+ file: file.relativePath,
13489
+ line: lineNumber,
13490
+ code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
13491
+ suggestion: 'Add descriptive alt text or add role="presentation" if truly decorative.',
13492
+ autoFixable: false
13493
+ });
13494
+ }
13495
+ checkPattern(file, lines, LINK_WITHOUT_TEXT, {
13496
+ title: "Empty Link",
13497
+ description: "Links must have accessible text content.",
13498
+ severity: "critical",
13499
+ suggestion: "Add text content or aria-label to the link."
13500
+ }, findings);
13501
+ checkPattern(file, lines, LINK_ONLY_ICON, {
13502
+ title: "Icon-Only Link",
13503
+ description: "Links containing only icons need accessible labels.",
13504
+ severity: "warning",
13505
+ suggestion: "Add aria-label or visually hidden text to describe the link."
13506
+ }, findings);
13507
+ checkPattern(file, lines, BUTTON_WITHOUT_TEXT, {
13508
+ title: "Icon-Only Button",
13509
+ description: "Buttons containing only icons need accessible labels.",
13510
+ severity: "warning",
13511
+ suggestion: "Add aria-label or visually hidden text to describe the button action."
13512
+ }, findings);
13513
+ checkPattern(file, lines, ONCLICK_ON_DIV, {
13514
+ title: "Click Handler on Non-Interactive Element",
13515
+ description: "div elements with onClick are not keyboard accessible by default.",
13516
+ severity: "warning",
13517
+ suggestion: 'Use a button element, or add role="button", tabIndex={0}, and onKeyDown handler.'
13518
+ }, findings);
13519
+ checkPattern(file, lines, ONCLICK_ON_SPAN, {
13520
+ title: "Click Handler on Non-Interactive Element",
13521
+ description: "span elements with onClick are not keyboard accessible by default.",
13522
+ severity: "warning",
13523
+ suggestion: 'Use a button element, or add role="button", tabIndex={0}, and onKeyDown handler.'
13524
+ }, findings);
13525
+ POSITIVE_TABINDEX.lastIndex = 0;
13526
+ while ((match = POSITIVE_TABINDEX.exec(file.content)) !== null) {
13527
+ const lineNumber = getLineNumber11(file.content, match.index);
13528
+ findings.push({
13529
+ id: generateFindingId("accessibility"),
13530
+ category: "accessibility",
13531
+ severity: "warning",
13532
+ title: "Positive tabIndex",
13533
+ description: `tabIndex="${match[1]}" disrupts natural tab order and confuses keyboard users.`,
13534
+ file: file.relativePath,
13535
+ line: lineNumber,
13536
+ code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
13537
+ suggestion: "Use tabIndex={0} to add to natural tab order, or tabIndex={-1} to remove. Avoid positive values.",
13538
+ autoFixable: true
13539
+ });
13540
+ }
13541
+ OUTLINE_NONE.lastIndex = 0;
13542
+ while ((match = OUTLINE_NONE.exec(file.content)) !== null) {
13543
+ const lineNumber = getLineNumber11(file.content, match.index);
13544
+ const context2 = file.content.slice(Math.max(0, match.index - 100), match.index + 200);
13545
+ if (context2.includes(":focus") && (context2.includes("box-shadow") || context2.includes("border"))) continue;
13546
+ findings.push({
13547
+ id: generateFindingId("accessibility"),
13548
+ category: "accessibility",
13549
+ severity: "critical",
13550
+ title: "Focus Indicator Removed",
13551
+ description: "Removing outline without an alternative makes it impossible for keyboard users to see focus.",
13552
+ file: file.relativePath,
13553
+ line: lineNumber,
13554
+ code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
13555
+ suggestion: "Provide an alternative focus indicator using box-shadow or border on :focus state.",
13556
+ autoFixable: false
13557
+ });
13558
+ }
13559
+ checkPattern(file, lines, ARIA_HIDDEN_FOCUSABLE, {
13560
+ title: "Focusable Element Hidden from Assistive Technology",
13561
+ description: 'Elements with aria-hidden="true" should not be focusable.',
13562
+ severity: "critical",
13563
+ suggestion: 'Remove aria-hidden="true" or add tabIndex={-1} and remove click handlers.'
13564
+ }, findings);
13565
+ checkHeadingHierarchy(file, lines, findings);
13566
+ }
13567
+ return findings;
13568
+ }
13569
+ function checkPattern(file, lines, pattern, issue, findings) {
13570
+ let match;
13571
+ pattern.lastIndex = 0;
13572
+ while ((match = pattern.exec(file.content)) !== null) {
13573
+ const lineNumber = getLineNumber11(file.content, match.index);
13574
+ findings.push({
13575
+ id: generateFindingId("accessibility"),
13576
+ category: "accessibility",
13577
+ severity: issue.severity,
13578
+ title: issue.title,
13579
+ description: issue.description,
13580
+ file: file.relativePath,
13581
+ line: lineNumber,
13582
+ code: truncateCode10(lines[lineNumber - 1]?.trim() || ""),
13583
+ suggestion: issue.suggestion,
13584
+ autoFixable: false,
13585
+ cweId: issue.cweId
13586
+ });
13587
+ }
13588
+ }
13589
+ function checkHeadingHierarchy(file, lines, findings) {
13590
+ const headings = [];
13591
+ let match;
13592
+ HEADING_PATTERN.lastIndex = 0;
13593
+ while ((match = HEADING_PATTERN.exec(file.content)) !== null) {
13594
+ headings.push({
13595
+ level: parseInt(match[1], 10),
13596
+ line: getLineNumber11(file.content, match.index)
13597
+ });
13598
+ }
13599
+ for (let i = 1; i < headings.length; i++) {
13600
+ const current = headings[i];
13601
+ const previous = headings[i - 1];
13602
+ if (current.level > previous.level + 1) {
13603
+ findings.push({
13604
+ id: generateFindingId("accessibility"),
13605
+ category: "accessibility",
13606
+ severity: "warning",
13607
+ title: "Heading Level Skipped",
13608
+ description: `Heading jumps from h${previous.level} to h${current.level}. Screen reader users may think content is missing.`,
13609
+ file: file.relativePath,
13610
+ line: current.line,
13611
+ code: truncateCode10(lines[current.line - 1]?.trim() || ""),
13612
+ suggestion: `Use h${previous.level + 1} instead of h${current.level}.`,
13613
+ autoFixable: true
13614
+ });
13615
+ }
13616
+ }
13617
+ if (headings.length > 0 && headings[0].level !== 1) {
13618
+ findings.push({
13619
+ id: generateFindingId("accessibility"),
13620
+ category: "accessibility",
13621
+ severity: "info",
13622
+ title: "Page Does Not Start with h1",
13623
+ description: "Pages should typically start with an h1 heading.",
13624
+ file: file.relativePath,
13625
+ line: headings[0].line,
13626
+ suggestion: "Consider adding an h1 as the main page heading.",
13627
+ autoFixable: false
13628
+ });
13629
+ }
13630
+ }
13631
+ function isUiFile2(filePath) {
13632
+ const lower = filePath.toLowerCase();
13633
+ return lower.includes("/pages/") || lower.includes("/components/") || lower.includes("/views/") || lower.includes("/ui/") || lower.endsWith(".tsx") || lower.endsWith(".jsx");
13634
+ }
13635
+ function getLineNumber11(content, index) {
13636
+ return content.slice(0, index).split("\n").length;
13637
+ }
13638
+ function truncateCode10(code, maxLength = 100) {
13639
+ if (code.length <= maxLength) return code;
13640
+ return code.slice(0, maxLength - 3) + "...";
13641
+ }
13642
+
13643
+ // src/tools/review-code/formatters/markdown.ts
13644
+ var SEVERITY_EMOJI = {
13645
+ blocking: "\u{1F534}",
13646
+ critical: "\u{1F7E0}",
13647
+ warning: "\u{1F7E1}",
13648
+ info: "\u{1F535}"
13649
+ };
13650
+ var GRADE_EMOJI = {
13651
+ A: "\u{1F7E2}",
13652
+ B: "\u{1F7E2}",
13653
+ C: "\u{1F7E1}",
13654
+ D: "\u{1F7E0}",
13655
+ F: "\u{1F534}"
13656
+ };
13657
+ function formatMarkdownReport(result) {
13658
+ const lines = [];
13659
+ lines.push("# Code Review Report");
13660
+ lines.push("");
13661
+ lines.push("## Summary");
13662
+ lines.push("");
13663
+ lines.push("| Metric | Value |");
13664
+ lines.push("|--------|-------|");
13665
+ lines.push(`| Status | ${result.summary.status === "passed" ? "\u2705 PASSED" : result.summary.status === "failed" ? "\u274C FAILED" : "\u26A0\uFE0F WARNING"} |`);
13666
+ lines.push(`| Score | ${result.summary.score}/100 |`);
13667
+ lines.push(`| Grade | ${GRADE_EMOJI[result.summary.grade]} ${result.summary.grade} |`);
13668
+ lines.push(`| Files Scanned | ${result.stats.filesScanned} |`);
13669
+ lines.push(`| Lines Scanned | ${result.stats.linesScanned.toLocaleString()} |`);
13670
+ lines.push(`| Duration | ${result.stats.duration}ms |`);
13671
+ lines.push("");
13672
+ lines.push("### Issues Found");
13673
+ lines.push("");
13674
+ lines.push("| Severity | Count |");
13675
+ lines.push("|----------|-------|");
13676
+ lines.push(`| ${SEVERITY_EMOJI.blocking} Blocking | ${result.stats.findingsCount.blocking} |`);
13677
+ lines.push(`| ${SEVERITY_EMOJI.critical} Critical | ${result.stats.findingsCount.critical} |`);
13678
+ lines.push(`| ${SEVERITY_EMOJI.warning} Warning | ${result.stats.findingsCount.warning} |`);
13679
+ lines.push(`| ${SEVERITY_EMOJI.info} Info | ${result.stats.findingsCount.info} |`);
13680
+ lines.push("");
13681
+ const categoriesWithIssues = Object.entries(result.stats.byCategory).filter(([_, count]) => count > 0).sort((a, b) => b[1] - a[1]);
13682
+ if (categoriesWithIssues.length > 0) {
13683
+ lines.push("### By Category");
13684
+ lines.push("");
13685
+ lines.push("| Category | Count |");
13686
+ lines.push("|----------|-------|");
13687
+ for (const [category, count] of categoriesWithIssues) {
13688
+ lines.push(`| ${formatCategoryName(category)} | ${count} |`);
13689
+ }
13690
+ lines.push("");
13691
+ }
13692
+ const blockingFindings = result.findings.filter((f) => f.severity === "blocking");
13693
+ const criticalFindings = result.findings.filter((f) => f.severity === "critical");
13694
+ const warningFindings = result.findings.filter((f) => f.severity === "warning");
13695
+ const infoFindings = result.findings.filter((f) => f.severity === "info");
13696
+ if (blockingFindings.length > 0) {
13697
+ lines.push("---");
13698
+ lines.push("");
13699
+ lines.push(`## ${SEVERITY_EMOJI.blocking} Blocking Issues (${blockingFindings.length})`);
13700
+ lines.push("");
13701
+ lines.push("> These issues MUST be fixed before merging.");
13702
+ lines.push("");
13703
+ formatFindings(lines, blockingFindings);
13704
+ }
13705
+ if (criticalFindings.length > 0) {
13706
+ lines.push("---");
13707
+ lines.push("");
13708
+ lines.push(`## ${SEVERITY_EMOJI.critical} Critical Issues (${criticalFindings.length})`);
13709
+ lines.push("");
13710
+ lines.push("> These issues should be fixed as soon as possible.");
13711
+ lines.push("");
13712
+ formatFindings(lines, criticalFindings);
13713
+ }
13714
+ if (warningFindings.length > 0) {
13715
+ lines.push("---");
13716
+ lines.push("");
13717
+ lines.push(`## ${SEVERITY_EMOJI.warning} Warnings (${warningFindings.length})`);
13718
+ lines.push("");
13719
+ formatFindings(lines, warningFindings);
13720
+ }
13721
+ if (infoFindings.length > 0) {
13722
+ lines.push("---");
13723
+ lines.push("");
13724
+ lines.push(`## ${SEVERITY_EMOJI.info} Info (${infoFindings.length})`);
13725
+ lines.push("");
13726
+ formatFindings(lines, infoFindings);
13727
+ }
13728
+ if (result.findings.length === 0) {
13729
+ lines.push("---");
13730
+ lines.push("");
13731
+ lines.push("## \u2728 No Issues Found");
13732
+ lines.push("");
13733
+ lines.push("Great job! Your code passed all checks.");
13734
+ lines.push("");
13735
+ }
13736
+ return lines.join("\n");
13737
+ }
13738
+ function formatFindings(lines, findings) {
13739
+ for (const finding of findings) {
13740
+ lines.push(`### ${finding.id}: ${finding.title}`);
13741
+ lines.push("");
13742
+ lines.push(`**Category**: ${formatCategoryName(finding.category)}`);
13743
+ lines.push(`**File**: \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
13744
+ if (finding.cweId) {
13745
+ lines.push(`**CWE**: ${finding.cweId}`);
10596
13746
  }
10597
- if (func.cyclomaticComplexity > thresholds.cyclomaticComplexity) {
10598
- issues.push(`Cyclomatic complexity: ${func.cyclomaticComplexity} (threshold: ${thresholds.cyclomaticComplexity})`);
13747
+ lines.push("");
13748
+ lines.push(finding.description);
13749
+ lines.push("");
13750
+ if (finding.code) {
13751
+ lines.push("**Code**:");
13752
+ lines.push("```");
13753
+ lines.push(finding.code);
13754
+ lines.push("```");
13755
+ lines.push("");
10599
13756
  }
10600
- if (func.lineCount > thresholds.functionSize) {
10601
- issues.push(`Lines: ${func.lineCount} (threshold: ${thresholds.functionSize})`);
13757
+ lines.push(`**Fix**: ${finding.suggestion}`);
13758
+ if (finding.autoFixable) {
13759
+ lines.push("");
13760
+ lines.push("> \u{1F4A1} This issue can be auto-fixed.");
10602
13761
  }
10603
- if (func.maxNestingDepth > thresholds.nestingDepth) {
10604
- issues.push(`Nesting depth: ${func.maxNestingDepth} (threshold: ${thresholds.nestingDepth})`);
13762
+ if (finding.references && finding.references.length > 0) {
13763
+ lines.push("");
13764
+ lines.push("**References**:");
13765
+ for (const ref of finding.references) {
13766
+ lines.push(`- ${ref}`);
13767
+ }
10605
13768
  }
10606
- if (issues.length > 0) {
10607
- const severity = issues.length >= 3 ? "high" : issues.length === 2 ? "medium" : "low";
10608
- hotspots.push({
10609
- file: func.file,
10610
- function: func.name,
10611
- issues,
10612
- severity,
10613
- metrics: {
10614
- cognitiveComplexity: func.cognitiveComplexity,
10615
- cyclomaticComplexity: func.cyclomaticComplexity,
10616
- lineCount: func.lineCount,
10617
- nestingDepth: func.maxNestingDepth
10618
- }
13769
+ lines.push("");
13770
+ }
13771
+ }
13772
+ function formatCategoryName(category) {
13773
+ const names = {
13774
+ security: "Security",
13775
+ architecture: "Architecture",
13776
+ "hardcoded-values": "Hardcoded Values",
13777
+ tests: "Tests",
13778
+ "ai-hallucinations": "AI Hallucinations",
13779
+ performance: "Performance",
13780
+ "dead-code": "Dead Code",
13781
+ i18n: "Internationalization",
13782
+ accessibility: "Accessibility"
13783
+ };
13784
+ return names[category] || category;
13785
+ }
13786
+
13787
+ // src/tools/review-code.ts
13788
+ var reviewCodeTool = {
13789
+ name: "review_code",
13790
+ description: `Comprehensive code review tool for SmartStack projects.
13791
+
13792
+ Analyzes code for 9 categories of issues:
13793
+ - **security**: OWASP vulnerabilities, hardcoded secrets, SQL injection, XSS
13794
+ - **architecture**: Clean Architecture layer violations
13795
+ - **hardcoded-values**: Magic numbers, hardcoded URLs, feature flags
13796
+ - **tests**: Missing tests, test quality issues
13797
+ - **ai-hallucinations**: Non-existent imports, phantom methods
13798
+ - **performance**: N+1 queries, over-fetching, missing memoization
13799
+ - **dead-code**: Unused imports, functions, variables
13800
+ - **i18n**: Non-internationalized UI text
13801
+ - **accessibility**: Missing alt text, ARIA issues, focus indicators
13802
+
13803
+ Returns a markdown report with findings grouped by severity (blocking, critical, warning, info).`,
13804
+ inputSchema: {
13805
+ type: "object",
13806
+ properties: {
13807
+ path: {
13808
+ type: "string",
13809
+ description: "Project path to review (default: SmartStack.app path)"
13810
+ },
13811
+ scope: {
13812
+ type: "string",
13813
+ enum: ["all", "changed", "staged"],
13814
+ default: "all",
13815
+ description: "Files to review: all, only changed (git diff), or staged files"
13816
+ },
13817
+ checks: {
13818
+ type: "array",
13819
+ items: {
13820
+ type: "string",
13821
+ enum: [
13822
+ "security",
13823
+ "architecture",
13824
+ "hardcoded-values",
13825
+ "tests",
13826
+ "ai-hallucinations",
13827
+ "performance",
13828
+ "dead-code",
13829
+ "i18n",
13830
+ "accessibility",
13831
+ "all"
13832
+ ]
13833
+ },
13834
+ default: ["all"],
13835
+ description: "Categories of checks to run"
13836
+ },
13837
+ severity: {
13838
+ type: "string",
13839
+ enum: ["blocking", "critical", "warning", "all"],
13840
+ default: "all",
13841
+ description: "Filter results by minimum severity"
13842
+ }
13843
+ }
13844
+ }
13845
+ };
13846
+ async function handleReviewCode(args, config) {
13847
+ const startTime = Date.now();
13848
+ const input = ReviewCodeInputSchema.parse(args);
13849
+ const projectPath = input.path || config.smartstack.projectPath;
13850
+ logger.info("Starting code review", { projectPath, scope: input.scope, checks: input.checks });
13851
+ resetFindingCounters();
13852
+ const structure = await findSmartStackStructure(projectPath);
13853
+ if (!structure) {
13854
+ return "# Code Review Error\n\nCould not detect SmartStack project structure at the specified path.";
13855
+ }
13856
+ const files = await getFilesToAnalyze(projectPath, input.scope, structure);
13857
+ if (files.length === 0) {
13858
+ return "# Code Review Report\n\nNo files to analyze for the specified scope.";
13859
+ }
13860
+ logger.info(`Analyzing ${files.length} files`);
13861
+ const context = {
13862
+ projectPath,
13863
+ files,
13864
+ scope: input.scope
13865
+ };
13866
+ const checksToRun = input.checks.includes("all") ? ["security", "architecture", "hardcoded-values", "tests", "ai-hallucinations", "performance", "dead-code", "i18n", "accessibility"] : input.checks;
13867
+ const checkFunctions = {
13868
+ security: checkSecurity,
13869
+ architecture: checkArchitecture,
13870
+ "hardcoded-values": checkHardcodedValues,
13871
+ tests: checkTests,
13872
+ "ai-hallucinations": checkAiHallucinations,
13873
+ performance: checkPerformance,
13874
+ "dead-code": checkDeadCode,
13875
+ i18n: checkI18n,
13876
+ accessibility: checkAccessibility
13877
+ };
13878
+ const checkPromises = checksToRun.filter((check) => checkFunctions[check]).map(async (check) => {
13879
+ try {
13880
+ logger.debug(`Running ${check} check`);
13881
+ return await checkFunctions[check](context);
13882
+ } catch (error) {
13883
+ logger.error(`Error in ${check} check`, { error });
13884
+ return [];
13885
+ }
13886
+ });
13887
+ const checkResults = await Promise.all(checkPromises);
13888
+ const allFindings = checkResults.flat();
13889
+ const filteredFindings = filterBySeverity(allFindings, input.severity);
13890
+ const stats = calculateStats(filteredFindings, files, startTime);
13891
+ const summary = calculateSummary2(filteredFindings, stats);
13892
+ const result = {
13893
+ summary,
13894
+ findings: filteredFindings,
13895
+ stats
13896
+ };
13897
+ logger.info("Code review complete", {
13898
+ filesScanned: stats.filesScanned,
13899
+ issuesFound: filteredFindings.length,
13900
+ duration: stats.duration
13901
+ });
13902
+ return formatMarkdownReport(result);
13903
+ }
13904
+ async function getFilesToAnalyze(projectPath, scope, _structure) {
13905
+ let filePaths = [];
13906
+ if (scope === "changed") {
13907
+ filePaths = await getChangedFiles(projectPath);
13908
+ } else if (scope === "staged") {
13909
+ filePaths = await getStagedFiles(projectPath);
13910
+ } else {
13911
+ const patterns = ["**/*.cs", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
13912
+ const excludePatterns = ["**/bin/**", "**/obj/**", "**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/Migrations/**"];
13913
+ for (const pattern of patterns) {
13914
+ const found = await findFiles(pattern, {
13915
+ cwd: projectPath,
13916
+ ignore: excludePatterns
10619
13917
  });
13918
+ filePaths.push(...found);
10620
13919
  }
10621
13920
  }
10622
- for (const [file, metrics] of fileMetrics) {
10623
- if (metrics.lineCount > thresholds.fileSize) {
10624
- hotspots.push({
10625
- file,
10626
- issues: [`File size: ${metrics.lineCount} lines (threshold: ${thresholds.fileSize})`],
10627
- severity: "medium",
10628
- metrics: {
10629
- lineCount: metrics.lineCount
10630
- }
13921
+ const files = [];
13922
+ for (const filePath of filePaths) {
13923
+ try {
13924
+ const fullPath = filePath.startsWith(projectPath) ? filePath : `${projectPath}/${filePath}`;
13925
+ const content = await readText(fullPath);
13926
+ const relativePath = filePath.replace(projectPath, "").replace(/^[/\\]/, "");
13927
+ files.push({
13928
+ path: fullPath,
13929
+ relativePath,
13930
+ content,
13931
+ language: detectLanguage(filePath),
13932
+ layer: detectLayer(relativePath),
13933
+ lineCount: content.split("\n").length
10631
13934
  });
13935
+ } catch {
13936
+ logger.debug(`Could not read file: ${filePath}`);
10632
13937
  }
10633
13938
  }
10634
- const severityOrder = { high: 0, medium: 1, low: 2 };
10635
- hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
10636
- return hotspots.slice(0, 20);
13939
+ return files;
10637
13940
  }
10638
- function calculateSummary(functions, fileMetrics, metrics, hotspots) {
10639
- const totalViolations = metrics.cognitiveComplexity.violations + metrics.cyclomaticComplexity.violations + metrics.functionSize.violations + metrics.nestingDepth.violations + metrics.fileSize.violations;
10640
- const totalFunctions = functions.length;
10641
- const totalFiles = fileMetrics.size;
10642
- const violationRate = totalFunctions > 0 ? totalViolations / totalFunctions : 0;
10643
- const score = Math.max(0, Math.round(100 - violationRate * 100));
13941
+ function detectLanguage(filePath) {
13942
+ const ext = filePath.toLowerCase().split(".").pop();
13943
+ switch (ext) {
13944
+ case "cs":
13945
+ return "csharp";
13946
+ case "ts":
13947
+ return "typescript";
13948
+ case "tsx":
13949
+ return "tsx";
13950
+ case "js":
13951
+ return "javascript";
13952
+ case "jsx":
13953
+ return "jsx";
13954
+ case "css":
13955
+ return "css";
13956
+ case "json":
13957
+ return "json";
13958
+ default:
13959
+ return "other";
13960
+ }
13961
+ }
13962
+ function detectLayer(relativePath) {
13963
+ const lower = relativePath.toLowerCase();
13964
+ if (lower.includes("/domain/") || lower.includes("\\domain\\")) return "domain";
13965
+ if (lower.includes("/application/") || lower.includes("\\application\\")) return "application";
13966
+ if (lower.includes("/infrastructure/") || lower.includes("\\infrastructure\\")) return "infrastructure";
13967
+ if (lower.includes("/api/") || lower.includes("\\api\\")) return "api";
13968
+ if (lower.includes("/web/") || lower.includes("\\web\\") || lower.includes("/src/")) return "web";
13969
+ return void 0;
13970
+ }
13971
+ function filterBySeverity(findings, minSeverity) {
13972
+ if (minSeverity === "all") return findings;
13973
+ const severityOrder = ["blocking", "critical", "warning", "info"];
13974
+ const minIndex = severityOrder.indexOf(minSeverity);
13975
+ return findings.filter((f) => severityOrder.indexOf(f.severity) <= minIndex);
13976
+ }
13977
+ function calculateStats(findings, files, startTime) {
13978
+ const byCategory = {
13979
+ security: 0,
13980
+ architecture: 0,
13981
+ "hardcoded-values": 0,
13982
+ tests: 0,
13983
+ "ai-hallucinations": 0,
13984
+ performance: 0,
13985
+ "dead-code": 0,
13986
+ i18n: 0,
13987
+ accessibility: 0
13988
+ };
13989
+ const findingsCount = {
13990
+ blocking: 0,
13991
+ critical: 0,
13992
+ warning: 0,
13993
+ info: 0
13994
+ };
13995
+ for (const finding of findings) {
13996
+ findingsCount[finding.severity]++;
13997
+ byCategory[finding.category]++;
13998
+ }
13999
+ return {
14000
+ filesScanned: files.length,
14001
+ linesScanned: files.reduce((sum, f) => sum + f.lineCount, 0),
14002
+ duration: Date.now() - startTime,
14003
+ findingsCount,
14004
+ byCategory
14005
+ };
14006
+ }
14007
+ function calculateSummary2(_findings, stats) {
14008
+ let score = 100;
14009
+ score -= stats.findingsCount.blocking * 20;
14010
+ score -= stats.findingsCount.critical * 5;
14011
+ score -= stats.findingsCount.warning * 1;
14012
+ score = Math.max(0, Math.min(100, score));
10644
14013
  let grade;
10645
14014
  if (score >= 90) grade = "A";
10646
14015
  else if (score >= 80) grade = "B";
10647
14016
  else if (score >= 70) grade = "C";
10648
14017
  else if (score >= 60) grade = "D";
10649
14018
  else grade = "F";
10650
- return {
10651
- score,
10652
- grade,
10653
- filesAnalyzed: totalFiles,
10654
- functionsAnalyzed: totalFunctions,
10655
- issuesFound: hotspots.length
10656
- };
10657
- }
10658
- function isExcludedPath(filePath) {
10659
- const exclusions = [
10660
- /[/\\]bin[/\\]/,
10661
- /[/\\]obj[/\\]/,
10662
- /[/\\]node_modules[/\\]/,
10663
- /[/\\]Migrations[/\\]/,
10664
- /\.test\./,
10665
- /\.spec\./,
10666
- /Tests[/\\]/,
10667
- /\.d\.ts$/,
10668
- /\.min\./
10669
- ];
10670
- return exclusions.some((pattern) => pattern.test(filePath));
10671
- }
10672
- function getLineNumber2(content, index) {
10673
- const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
10674
- return normalizedContent.split("\n").length;
10675
- }
10676
- function formatQualityReport(result, thresholds) {
10677
- const lines = [];
10678
- lines.push("# Code Quality Report");
10679
- lines.push("");
10680
- lines.push("## Summary");
10681
- lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
10682
- lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
10683
- lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
10684
- lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
10685
- lines.push("");
10686
- lines.push("## Metrics Overview");
10687
- lines.push("");
10688
- lines.push("| Metric | Average | Max | Threshold | Status |");
10689
- lines.push("|--------|---------|-----|-----------|--------|");
10690
- const formatMetricRow = (name, stat2) => {
10691
- const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
10692
- return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
10693
- };
10694
- lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
10695
- lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
10696
- lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
10697
- lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
10698
- lines.push(formatMetricRow("File Size", result.metrics.fileSize));
10699
- lines.push("");
10700
- if (result.hotspots.length > 0) {
10701
- lines.push("## Hotspots (Needs Attention)");
10702
- lines.push("");
10703
- for (const hotspot of result.hotspots) {
10704
- const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
10705
- const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
10706
- lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
10707
- for (const issue of hotspot.issues) {
10708
- lines.push(`- ${issue}`);
10709
- }
10710
- if (hotspot.function) {
10711
- if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
10712
- lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
10713
- } else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
10714
- lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
10715
- }
10716
- } else {
10717
- lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
10718
- }
10719
- lines.push("");
10720
- }
14019
+ let status;
14020
+ if (stats.findingsCount.blocking > 0) {
14021
+ status = "failed";
14022
+ } else if (stats.findingsCount.critical > 0) {
14023
+ status = "warning";
10721
14024
  } else {
10722
- lines.push("No hotspots found. Code quality is within acceptable thresholds.");
14025
+ status = "passed";
10723
14026
  }
10724
- return lines.join("\n");
14027
+ let message;
14028
+ if (status === "failed") {
14029
+ message = `Review failed with ${stats.findingsCount.blocking} blocking issue(s). These must be fixed before merging.`;
14030
+ } else if (status === "warning") {
14031
+ message = `Review passed with warnings. ${stats.findingsCount.critical} critical issue(s) should be addressed.`;
14032
+ } else {
14033
+ message = "Code review passed. Great job!";
14034
+ }
14035
+ return { status, score, grade, message };
10725
14036
  }
10726
14037
 
10727
14038
  // src/resources/conventions.ts
@@ -11650,12 +14961,141 @@ const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
11650
14961
  - Invalid tabs fallback to default
11651
14962
  - Uses \`replace: true\` to avoid polluting browser history
11652
14963
 
14964
+ #### Lazy Loading for Tabs (Performance)
14965
+
14966
+ Pages with tabs MUST implement lazy loading to avoid fetching all tab data upfront.
14967
+
14968
+ **Pattern 1: useQuery with enabled condition (Recommended)**
14969
+
14970
+ \`\`\`tsx
14971
+ // \u274C BAD - Loads all data on mount
14972
+ const { data: info } = useQuery(['info', id], fetchInfo);
14973
+ const { data: settings } = useQuery(['settings', id], fetchSettings);
14974
+
14975
+ // \u2705 GOOD - Only loads active tab data
14976
+ const { data: info } = useQuery(
14977
+ ['info', id],
14978
+ fetchInfo,
14979
+ { enabled: activeTab === 'info' }
14980
+ );
14981
+
14982
+ const { data: settings } = useQuery(
14983
+ ['settings', id],
14984
+ fetchSettings,
14985
+ { enabled: activeTab === 'settings' }
14986
+ );
14987
+ \`\`\`
14988
+
14989
+ **Pattern 2: React.lazy() for heavy tab components**
14990
+
14991
+ \`\`\`tsx
14992
+ // Lazy load tab components
14993
+ const TabInfo = React.lazy(() => import('./tabs/TabInfo'));
14994
+ const TabSettings = React.lazy(() => import('./tabs/TabSettings'));
14995
+
14996
+ // In JSX with Suspense
14997
+ <TabsContent value="info">
14998
+ <Suspense fallback={<Skeleton />}>
14999
+ <TabInfo entityId={id} />
15000
+ </Suspense>
15001
+ </TabsContent>
15002
+ \`\`\`
15003
+
15004
+ **Pattern 3: Conditional rendering**
15005
+
15006
+ \`\`\`tsx
15007
+ // Render tab content only when active
15008
+ {activeTab === 'settings' && <SettingsTab entityId={id} />}
15009
+ \`\`\`
15010
+
15011
+ | Pattern | Use Case |
15012
+ |---------|----------|
15013
+ | useQuery enabled | Simple data fetching per tab |
15014
+ | React.lazy + Suspense | Heavy components with own data fetching |
15015
+ | Conditional rendering | Light components, quick tab switches |
15016
+
15017
+ ### Breadcrumb Navigation
15018
+
15019
+ All pages MUST include a Breadcrumb component for consistent navigation and user orientation.
15020
+
15021
+ #### Breadcrumb Component
15022
+
15023
+ \`\`\`tsx
15024
+ import { Breadcrumb } from '@/components/ui/Breadcrumb';
15025
+
15026
+ // In your page component
15027
+ <Breadcrumb
15028
+ items={[
15029
+ { label: t('nav.administration'), href: '/platform/administration' },
15030
+ { label: t('nav.users'), href: '/platform/administration/users' },
15031
+ { label: user.fullName } // Last item = current page (no href)
15032
+ ]}
15033
+ />
15034
+ \`\`\`
15035
+
15036
+ #### Breadcrumb Rules
15037
+
15038
+ | Rule | Description |
15039
+ |------|-------------|
15040
+ | First item | Context/Application level with Home icon (auto-added if no icon) |
15041
+ | Intermediate items | Clickable links to parent pages |
15042
+ | Last item | Current page name, no href, bold text |
15043
+ | Labels | Use i18n translation keys from \`navigation.json\` |
15044
+ | Detail pages | Include entity name (e.g., user's full name) |
15045
+
15046
+ #### Hierarchy Pattern
15047
+
15048
+ \`\`\`
15049
+ Context > Application > Module > [Section] > [Entity Name]
15050
+ \`\`\`
15051
+
15052
+ **Examples:**
15053
+
15054
+ | Page Type | Breadcrumb Items |
15055
+ |-----------|------------------|
15056
+ | List page | Administration > Users |
15057
+ | Detail page | Administration > Users > John Doe |
15058
+ | Nested page | Administration > Users > John Doe > Permissions |
15059
+
15060
+ #### BreadcrumbItem Interface
15061
+
15062
+ \`\`\`tsx
15063
+ interface BreadcrumbItem {
15064
+ label: string; // Display text (use t() for i18n)
15065
+ href?: string; // Link destination (omit for current page)
15066
+ icon?: ReactNode; // Optional icon (Home auto-added to first item)
15067
+ }
15068
+ \`\`\`
15069
+
15070
+ #### Page Position
15071
+
15072
+ Breadcrumb should be the FIRST element in the page content:
15073
+
15074
+ \`\`\`tsx
15075
+ return (
15076
+ <div className="space-y-6">
15077
+ {/* 1. Breadcrumb - always first */}
15078
+ <Breadcrumb items={[...]} />
15079
+
15080
+ {/* 2. Scope Banner (if multi-tenant) */}
15081
+ <ScopeBanner />
15082
+
15083
+ {/* 3. Page Header */}
15084
+ <div className="flex items-center gap-4">...</div>
15085
+
15086
+ {/* 4. Page Content */}
15087
+ ...
15088
+ </div>
15089
+ );
15090
+ \`\`\`
15091
+
11653
15092
  ### Frontend Validation Checks
11654
15093
 
11655
15094
  | Check | Description |
11656
15095
  |-------|-------------|
11657
15096
  | \`layouts\` | Verify no \`max-w-*\` in content wrapper, standard padding present |
11658
- | \`tabs\` | Verify \`useTabNavigation\` hook usage for pages with tabs |
15097
+ | \`tabs\` | Verify \`useTabNavigation\` hook usage AND lazy loading patterns for pages with tabs |
15098
+ | \`breadcrumb\` | Verify Breadcrumb component present as first element in page |
11659
15099
 
11660
15100
  ---
11661
15101
 
@@ -11695,6 +15135,9 @@ const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
11695
15135
  | Layout padding | \`lg:px-10\` | Standard horizontal padding |
11696
15136
  | Tab navigation | \`useTabNavigation\` hook | Sync tabs with URL |
11697
15137
  | Tab URL | \`?tab=name\` | Shareable deep links to tabs |
15138
+ | Tab lazy loading | \`enabled: activeTab === 'x'\` | Load data only for active tab |
15139
+ | Breadcrumb | \`<Breadcrumb items={[...]}/>\` | Navigation hierarchy, first page element |
15140
+ | Breadcrumb items | \`{ label, href?, icon? }\` | Last item without href = current page |
11698
15141
 
11699
15142
  ---
11700
15143
 
@@ -11859,7 +15302,7 @@ Run specific or all checks:
11859
15302
  }
11860
15303
 
11861
15304
  // src/resources/project-info.ts
11862
- import path22 from "path";
15305
+ import path23 from "path";
11863
15306
  var projectInfoResourceTemplate = {
11864
15307
  uri: "smartstack://project",
11865
15308
  name: "SmartStack Project Info",
@@ -11896,16 +15339,16 @@ async function getProjectInfoResource(config) {
11896
15339
  lines.push("```");
11897
15340
  lines.push(`${projectInfo.name}/`);
11898
15341
  if (structure.domain) {
11899
- lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.domain)}/ # Domain layer (entities)`);
15342
+ lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.domain)}/ # Domain layer (entities)`);
11900
15343
  }
11901
15344
  if (structure.application) {
11902
- lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.application)}/ # Application layer (services)`);
15345
+ lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.application)}/ # Application layer (services)`);
11903
15346
  }
11904
15347
  if (structure.infrastructure) {
11905
- lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
15348
+ lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
11906
15349
  }
11907
15350
  if (structure.api) {
11908
- lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.api)}/ # API layer (controllers)`);
15351
+ lines.push(`\u251C\u2500\u2500 ${path23.basename(structure.api)}/ # API layer (controllers)`);
11909
15352
  }
11910
15353
  if (structure.web) {
11911
15354
  lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
@@ -11918,8 +15361,8 @@ async function getProjectInfoResource(config) {
11918
15361
  lines.push("| Project | Path |");
11919
15362
  lines.push("|---------|------|");
11920
15363
  for (const csproj of projectInfo.csprojFiles) {
11921
- const name = path22.basename(csproj, ".csproj");
11922
- const relativePath = path22.relative(projectPath, csproj);
15364
+ const name = path23.basename(csproj, ".csproj");
15365
+ const relativePath = path23.relative(projectPath, csproj);
11923
15366
  lines.push(`| ${name} | \`${relativePath}\` |`);
11924
15367
  }
11925
15368
  lines.push("");
@@ -11929,10 +15372,10 @@ async function getProjectInfoResource(config) {
11929
15372
  cwd: structure.migrations,
11930
15373
  ignore: ["*.Designer.cs"]
11931
15374
  });
11932
- const migrations = migrationFiles.map((f) => path22.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
15375
+ const migrations = migrationFiles.map((f) => path23.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
11933
15376
  lines.push("## EF Core Migrations");
11934
15377
  lines.push("");
11935
- lines.push(`**Location**: \`${path22.relative(projectPath, structure.migrations)}\``);
15378
+ lines.push(`**Location**: \`${path23.relative(projectPath, structure.migrations)}\``);
11936
15379
  lines.push(`**Total Migrations**: ${migrations.length}`);
11937
15380
  lines.push("");
11938
15381
  if (migrations.length > 0) {
@@ -11967,11 +15410,11 @@ async function getProjectInfoResource(config) {
11967
15410
  lines.push("dotnet build");
11968
15411
  lines.push("");
11969
15412
  lines.push("# Run API");
11970
- lines.push(`cd ${structure.api ? path22.relative(projectPath, structure.api) : "src/Api"}`);
15413
+ lines.push(`cd ${structure.api ? path23.relative(projectPath, structure.api) : "src/Api"}`);
11971
15414
  lines.push("dotnet run");
11972
15415
  lines.push("");
11973
15416
  lines.push("# Run frontend");
11974
- lines.push(`cd ${structure.web ? path22.relative(projectPath, structure.web) : "web"}`);
15417
+ lines.push(`cd ${structure.web ? path23.relative(projectPath, structure.web) : "web"}`);
11975
15418
  lines.push("npm run dev");
11976
15419
  lines.push("");
11977
15420
  lines.push("# Create migration");
@@ -11994,7 +15437,7 @@ async function getProjectInfoResource(config) {
11994
15437
  }
11995
15438
 
11996
15439
  // src/resources/api-endpoints.ts
11997
- import path23 from "path";
15440
+ import path24 from "path";
11998
15441
  var apiEndpointsResourceTemplate = {
11999
15442
  uri: "smartstack://api/",
12000
15443
  name: "SmartStack API Endpoints",
@@ -12019,7 +15462,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
12019
15462
  }
12020
15463
  async function parseController(filePath, _rootPath) {
12021
15464
  const content = await readText(filePath);
12022
- const fileName = path23.basename(filePath, ".cs");
15465
+ const fileName = path24.basename(filePath, ".cs");
12023
15466
  const controllerName = fileName.replace("Controller", "");
12024
15467
  const endpoints = [];
12025
15468
  const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
@@ -12166,7 +15609,7 @@ function getMethodEmoji(method) {
12166
15609
  }
12167
15610
 
12168
15611
  // src/resources/db-schema.ts
12169
- import path24 from "path";
15612
+ import path25 from "path";
12170
15613
  var dbSchemaResourceTemplate = {
12171
15614
  uri: "smartstack://schema/",
12172
15615
  name: "SmartStack Database Schema",
@@ -12182,7 +15625,7 @@ async function getDbSchemaResource(config, tableFilter) {
12182
15625
  if (structure.domain) {
12183
15626
  const entityFiles = await findEntityFiles(structure.domain);
12184
15627
  for (const file of entityFiles) {
12185
- const entity = await parseEntity(file, structure.root, config);
15628
+ const entity = await parseEntity2(file, structure.root, config);
12186
15629
  if (entity) {
12187
15630
  entities.push(entity);
12188
15631
  }
@@ -12196,7 +15639,7 @@ async function getDbSchemaResource(config, tableFilter) {
12196
15639
  ) : entities;
12197
15640
  return formatSchema(filteredEntities, tableFilter, config);
12198
15641
  }
12199
- async function parseEntity(filePath, rootPath, _config) {
15642
+ async function parseEntity2(filePath, rootPath, _config) {
12200
15643
  const content = await readText(filePath);
12201
15644
  const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*(\w+))?/);
12202
15645
  if (!classMatch) return null;
@@ -12256,7 +15699,7 @@ async function parseEntity(filePath, rootPath, _config) {
12256
15699
  tableName,
12257
15700
  properties,
12258
15701
  relationships,
12259
- file: path24.relative(rootPath, filePath)
15702
+ file: path25.relative(rootPath, filePath)
12260
15703
  };
12261
15704
  }
12262
15705
  async function enrichFromConfigurations(entities, infrastructurePath, _config) {
@@ -12402,7 +15845,7 @@ function formatSchema(entities, filter, _config) {
12402
15845
  }
12403
15846
 
12404
15847
  // src/resources/entities.ts
12405
- import path25 from "path";
15848
+ import path26 from "path";
12406
15849
  var entitiesResourceTemplate = {
12407
15850
  uri: "smartstack://entities/",
12408
15851
  name: "SmartStack Entities",
@@ -12462,7 +15905,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
12462
15905
  hasSoftDelete,
12463
15906
  hasRowVersion,
12464
15907
  file: filePath,
12465
- relativePath: path25.relative(rootPath, filePath)
15908
+ relativePath: path26.relative(rootPath, filePath)
12466
15909
  };
12467
15910
  }
12468
15911
  function inferTableInfo(entityName, config) {
@@ -12665,7 +16108,11 @@ async function createServer() {
12665
16108
  analyzeExtensionPointsTool,
12666
16109
  // Security & Code Quality Tools
12667
16110
  validateSecurityTool,
12668
- analyzeCodeQualityTool
16111
+ analyzeCodeQualityTool,
16112
+ // Hierarchy Analysis Tools
16113
+ analyzeHierarchyPatternsTool,
16114
+ // Code Review Tool
16115
+ reviewCodeTool
12669
16116
  ]
12670
16117
  };
12671
16118
  });
@@ -12731,6 +16178,14 @@ async function createServer() {
12731
16178
  case "analyze_code_quality":
12732
16179
  result = await handleAnalyzeCodeQuality(args ?? {}, config);
12733
16180
  break;
16181
+ // Hierarchy Analysis Tools
16182
+ case "analyze_hierarchy_patterns":
16183
+ result = await handleAnalyzeHierarchyPatterns(args ?? {}, config);
16184
+ break;
16185
+ // Code Review Tool
16186
+ case "review_code":
16187
+ result = await handleReviewCode(args ?? {}, config);
16188
+ break;
12734
16189
  default:
12735
16190
  throw new Error(`Unknown tool: ${name}`);
12736
16191
  }