@arcbridge/core 0.4.2 → 0.6.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
@@ -54,7 +54,8 @@ var ArcBridgeConfigSchema = z2.object({
54
54
  "api-service",
55
55
  "dotnet-webapi",
56
56
  "unity-game",
57
- "angular-app"
57
+ "angular-app",
58
+ "fullstack-nextjs-dotnet"
58
59
  ]).default("nextjs-app-router"),
59
60
  services: z2.array(ServiceSchema).default([]),
60
61
  platforms: z2.array(z2.enum(["claude", "copilot", "gemini", "codex", "opencode"])).default(["claude"]),
@@ -809,6 +810,46 @@ function configTemplate6(input) {
809
810
  };
810
811
  }
811
812
 
813
+ // src/templates/config/fullstack-nextjs-dotnet.ts
814
+ function configTemplate7(input) {
815
+ return {
816
+ schema_version: 1,
817
+ project_name: input.name,
818
+ project_type: "fullstack-nextjs-dotnet",
819
+ services: [
820
+ { name: "frontend", path: "frontend", type: "nextjs", tsconfig: "frontend/tsconfig.json" },
821
+ { name: "api", path: "api", type: "dotnet" }
822
+ ],
823
+ platforms: input.platforms,
824
+ quality_priorities: input.quality_priorities,
825
+ indexing: {
826
+ include: [
827
+ "frontend/src/**/*",
828
+ "frontend/app/**/*",
829
+ "api/**/*.cs",
830
+ "shared/**/*",
831
+ "contracts/**/*"
832
+ ],
833
+ exclude: ["node_modules", "dist", ".next", "bin", "obj", "coverage"],
834
+ default_mode: "fast",
835
+ csharp_indexer: "auto"
836
+ },
837
+ testing: {
838
+ test_command: "npm test",
839
+ timeout_ms: 6e4
840
+ },
841
+ drift: {
842
+ ignore_paths: []
843
+ },
844
+ metrics: { auto_record: false },
845
+ sync: {
846
+ auto_detect_drift: true,
847
+ drift_severity_threshold: "warning",
848
+ propose_updates_on: "phase-complete"
849
+ }
850
+ };
851
+ }
852
+
812
853
  // src/generators/config-generator.ts
813
854
  var configTemplates = {
814
855
  "nextjs-app-router": configTemplate,
@@ -816,7 +857,8 @@ var configTemplates = {
816
857
  "api-service": configTemplate3,
817
858
  "dotnet-webapi": configTemplate4,
818
859
  "unity-game": configTemplate5,
819
- "angular-app": configTemplate6
860
+ "angular-app": configTemplate6,
861
+ "fullstack-nextjs-dotnet": configTemplate7
820
862
  };
821
863
  function generateConfig(targetDir, input) {
822
864
  const templateFn = configTemplates[input.template] ?? configTemplate;
@@ -845,7 +887,7 @@ function introductionTemplate(input) {
845
887
 
846
888
  ## Requirements Overview
847
889
 
848
- ${input.name} is a ${input.template === "nextjs-app-router" ? "Next.js application using the App Router" : input.template === "angular-app" ? "Angular application with standalone components" : input.template === "dotnet-webapi" ? "ASP.NET Core Web API" : input.template === "unity-game" ? "Unity game built with a code-heavy C# architecture" : "web application"}.
890
+ ${input.name} is a ${input.template === "nextjs-app-router" ? "Next.js application using the App Router" : input.template === "angular-app" ? "Angular application with standalone components" : input.template === "dotnet-webapi" ? "ASP.NET Core Web API" : input.template === "unity-game" ? "Unity game built with a code-heavy C# architecture" : input.template === "fullstack-nextjs-dotnet" ? "fullstack application with a Next.js frontend and .NET API backend" : "web application"}.
849
891
 
850
892
  ### Key Features
851
893
 
@@ -1047,7 +1089,66 @@ function getEntrypoints(template, srcPrefix, appPrefix) {
1047
1089
  function buildingBlocksTemplate(input) {
1048
1090
  const now = (/* @__PURE__ */ new Date()).toISOString();
1049
1091
  const layout = detectProjectLayout(input.projectRoot, input.template);
1050
- const defaultBlocks = input.template === "dotnet-webapi" ? buildDotnetBlocks(input) : input.template === "unity-game" ? buildUnityBlocks() : input.template === "angular-app" ? buildAngularBlocks(layout) : buildJsBlocks(input, layout);
1092
+ const defaultBlocks = input.template === "fullstack-nextjs-dotnet" ? buildFullstackBlocks() : input.template === "dotnet-webapi" ? buildDotnetBlocks(input) : input.template === "unity-game" ? buildUnityBlocks() : input.template === "angular-app" ? buildAngularBlocks(layout) : buildJsBlocks(input, layout);
1093
+ function buildFullstackBlocks() {
1094
+ return [
1095
+ {
1096
+ id: "frontend-shell",
1097
+ name: "Frontend Shell",
1098
+ level: 1,
1099
+ code_paths: ["frontend/app/", "frontend/src/app/"],
1100
+ interfaces: [],
1101
+ quality_scenarios: ["PERF-01"],
1102
+ adrs: [],
1103
+ responsibility: "Next.js App Router layouts, pages, and top-level page structure",
1104
+ service: "frontend"
1105
+ },
1106
+ {
1107
+ id: "frontend-components",
1108
+ name: "Frontend Components",
1109
+ level: 1,
1110
+ code_paths: ["frontend/src/components/", "frontend/components/"],
1111
+ interfaces: [],
1112
+ quality_scenarios: ["A11Y-01"],
1113
+ adrs: [],
1114
+ responsibility: "Shared, reusable React UI components",
1115
+ service: "frontend"
1116
+ },
1117
+ {
1118
+ id: "api-controllers",
1119
+ name: "API Controllers",
1120
+ level: 1,
1121
+ code_paths: ["api/Controllers/"],
1122
+ interfaces: [],
1123
+ quality_scenarios: ["SEC-03"],
1124
+ adrs: [],
1125
+ responsibility: "ASP.NET Core API endpoint definitions and request handling",
1126
+ service: "api"
1127
+ },
1128
+ {
1129
+ id: "api-services",
1130
+ name: "API Services",
1131
+ level: 1,
1132
+ code_paths: ["api/Services/"],
1133
+ interfaces: ["api-controllers"],
1134
+ quality_scenarios: [],
1135
+ adrs: [],
1136
+ responsibility: "Business logic, use cases, and orchestration between domain and infrastructure",
1137
+ service: "api"
1138
+ },
1139
+ {
1140
+ id: "shared-contracts",
1141
+ name: "Shared Contracts",
1142
+ level: 1,
1143
+ code_paths: ["shared/", "contracts/"],
1144
+ interfaces: ["frontend-shell", "api-controllers"],
1145
+ quality_scenarios: ["MAINT-01"],
1146
+ adrs: [],
1147
+ responsibility: "API types, schemas, and contracts shared between the Next.js frontend and .NET backend",
1148
+ service: "api"
1149
+ }
1150
+ ];
1151
+ }
1051
1152
  function buildJsBlocks(inp, lt) {
1052
1153
  const src = lt.srcPrefix;
1053
1154
  const entries = lt.entrypoints;
@@ -2275,13 +2376,169 @@ var ANGULAR_SCENARIOS = {
2275
2376
  }
2276
2377
  ]
2277
2378
  };
2379
+ var FULLSTACK_NEXTJS_DOTNET_SCENARIOS = {
2380
+ security: [
2381
+ {
2382
+ id: "SEC-02",
2383
+ name: "No secrets in client bundles or source",
2384
+ category: "security",
2385
+ priority: "must",
2386
+ scenario: "Client-side JavaScript bundle and .NET source/config files are analyzed",
2387
+ expected: "No API keys, tokens, or secrets are found in client-side code or hardcoded in appsettings",
2388
+ linked_code: [],
2389
+ linked_tests: [],
2390
+ linked_blocks: ["frontend-shell", "api-controllers"],
2391
+ verification: "automatic",
2392
+ status: "untested"
2393
+ },
2394
+ {
2395
+ id: "SEC-04",
2396
+ name: "CORS policy restricts origins",
2397
+ category: "security",
2398
+ priority: "should",
2399
+ scenario: "A cross-origin request is made from an untrusted domain",
2400
+ expected: "Request is rejected by CORS middleware; only the frontend origin is allowed",
2401
+ linked_code: [],
2402
+ linked_tests: [],
2403
+ linked_blocks: ["api-controllers"],
2404
+ verification: "automatic",
2405
+ status: "untested"
2406
+ },
2407
+ {
2408
+ id: "SEC-05",
2409
+ name: "Cross-service auth token handling",
2410
+ category: "security",
2411
+ priority: "must",
2412
+ scenario: "Frontend sends an auth token to the .NET API on every protected request",
2413
+ expected: "Token is validated server-side; expired or invalid tokens return 401; tokens are never stored in localStorage",
2414
+ linked_code: [],
2415
+ linked_tests: [],
2416
+ linked_blocks: ["frontend-shell", "api-services"],
2417
+ verification: "automatic",
2418
+ status: "untested"
2419
+ }
2420
+ ],
2421
+ performance: [
2422
+ {
2423
+ id: "PERF-01",
2424
+ name: "Initial page load under 3s",
2425
+ category: "performance",
2426
+ priority: "should",
2427
+ scenario: "User loads the landing page on a 3G connection",
2428
+ expected: "Largest Contentful Paint (LCP) is under 3 seconds",
2429
+ linked_code: [],
2430
+ linked_tests: [],
2431
+ linked_blocks: ["frontend-shell"],
2432
+ verification: "semi-automatic",
2433
+ status: "untested"
2434
+ },
2435
+ {
2436
+ id: "PERF-03",
2437
+ name: "Frontend bundle under 200KB gzipped",
2438
+ category: "performance",
2439
+ priority: "should",
2440
+ scenario: "Production build output is analyzed",
2441
+ expected: "Main JavaScript bundle is under 200KB gzipped",
2442
+ linked_code: [],
2443
+ linked_tests: [],
2444
+ linked_blocks: ["frontend-shell", "frontend-components"],
2445
+ verification: "semi-automatic",
2446
+ status: "untested"
2447
+ },
2448
+ {
2449
+ id: "PERF-04",
2450
+ name: "Async all the way in .NET API",
2451
+ category: "performance",
2452
+ priority: "must",
2453
+ scenario: "I/O-bound operations in the API are reviewed",
2454
+ expected: "All I/O operations use async/await; no sync-over-async or blocking calls",
2455
+ linked_code: [],
2456
+ linked_tests: [],
2457
+ linked_blocks: ["api-controllers", "api-services"],
2458
+ verification: "semi-automatic",
2459
+ status: "untested"
2460
+ }
2461
+ ],
2462
+ reliability: [
2463
+ {
2464
+ id: "REL-02",
2465
+ name: "Health check covers dependencies",
2466
+ category: "reliability",
2467
+ priority: "should",
2468
+ scenario: "Orchestrator calls the /health endpoint",
2469
+ expected: "Returns degraded or unhealthy if database or critical external service is unreachable",
2470
+ linked_code: [],
2471
+ linked_tests: [],
2472
+ linked_blocks: ["api-controllers"],
2473
+ verification: "automatic",
2474
+ status: "untested"
2475
+ },
2476
+ {
2477
+ id: "REL-03",
2478
+ name: "Structured logging with correlation",
2479
+ category: "reliability",
2480
+ priority: "should",
2481
+ scenario: "A request flows through frontend to API and back",
2482
+ expected: "All log entries share a correlation ID across services; logs are structured JSON",
2483
+ linked_code: [],
2484
+ linked_tests: [],
2485
+ linked_blocks: ["api-services"],
2486
+ verification: "semi-automatic",
2487
+ status: "untested"
2488
+ }
2489
+ ],
2490
+ accessibility: [
2491
+ {
2492
+ id: "A11Y-01",
2493
+ name: "WCAG 2.1 AA compliance",
2494
+ category: "accessibility",
2495
+ priority: "should",
2496
+ scenario: "All pages are audited with axe-core",
2497
+ expected: "No critical or serious accessibility violations",
2498
+ linked_code: [],
2499
+ linked_tests: [],
2500
+ linked_blocks: ["frontend-components"],
2501
+ verification: "semi-automatic",
2502
+ status: "untested"
2503
+ },
2504
+ {
2505
+ id: "A11Y-02",
2506
+ name: "Keyboard navigation",
2507
+ category: "accessibility",
2508
+ priority: "should",
2509
+ scenario: "User navigates the entire application using only keyboard",
2510
+ expected: "All interactive elements are reachable and operable via keyboard",
2511
+ linked_code: [],
2512
+ linked_tests: [],
2513
+ linked_blocks: ["frontend-components"],
2514
+ verification: "manual",
2515
+ status: "untested"
2516
+ }
2517
+ ],
2518
+ maintainability: [
2519
+ {
2520
+ id: "MAINT-03",
2521
+ name: "API contract consistency",
2522
+ category: "maintainability",
2523
+ priority: "must",
2524
+ scenario: "Frontend type definitions are compared against API OpenAPI spec or shared contracts",
2525
+ expected: "All frontend API types match the backend contract; CI check prevents drift",
2526
+ linked_code: [],
2527
+ linked_tests: [],
2528
+ linked_blocks: ["shared-contracts"],
2529
+ verification: "automatic",
2530
+ status: "untested"
2531
+ }
2532
+ ]
2533
+ };
2278
2534
  var TEMPLATE_SCENARIOS = {
2279
2535
  "nextjs-app-router": FRONTEND_SCENARIOS,
2280
2536
  "react-vite": FRONTEND_SCENARIOS,
2281
2537
  "angular-app": ANGULAR_SCENARIOS,
2282
2538
  "api-service": API_SCENARIOS,
2283
2539
  "dotnet-webapi": DOTNET_SCENARIOS,
2284
- "unity-game": UNITY_GAME_SCENARIOS
2540
+ "unity-game": UNITY_GAME_SCENARIOS,
2541
+ "fullstack-nextjs-dotnet": FULLSTACK_NEXTJS_DOTNET_SCENARIOS
2285
2542
  };
2286
2543
  function mergeScenarios(template, category) {
2287
2544
  const shared = SHARED_SCENARIOS[category] ?? [];
@@ -3949,45 +4206,401 @@ function phaseTasksTemplate6(_input, phaseId) {
3949
4206
  return tasksByPhase[phaseId] ?? null;
3950
4207
  }
3951
4208
 
3952
- // src/generators/plan-generator.ts
3953
- var planTemplates = {
3954
- "nextjs-app-router": { plan: phasePlanTemplate, tasks: phaseTasksTemplate },
3955
- "react-vite": { plan: phasePlanTemplate2, tasks: phaseTasksTemplate2 },
3956
- "api-service": { plan: phasePlanTemplate3, tasks: phaseTasksTemplate3 },
3957
- "dotnet-webapi": { plan: phasePlanTemplate4, tasks: phaseTasksTemplate4 },
3958
- "unity-game": { plan: phasePlanTemplate5, tasks: phaseTasksTemplate5 },
3959
- "angular-app": { plan: phasePlanTemplate6, tasks: phaseTasksTemplate6 }
3960
- };
3961
- function generatePlan(targetDir, input) {
3962
- const planDir = join5(targetDir, ".arcbridge", "plan");
3963
- const tasksDir = join5(planDir, "tasks");
3964
- mkdirSync3(planDir, { recursive: true });
3965
- mkdirSync3(tasksDir, { recursive: true });
3966
- const tmpl = planTemplates[input.template] ?? planTemplates["nextjs-app-router"];
3967
- const phasePlan = tmpl.plan(input);
3968
- writeFileSync3(join5(planDir, "phases.yaml"), stringify3(phasePlan), "utf-8");
3969
- for (const phase of phasePlan.phases) {
3970
- const taskFile = tmpl.tasks(input, phase.id);
3971
- if (taskFile) {
3972
- writeFileSync3(
3973
- join5(tasksDir, `${phase.id}.yaml`),
3974
- stringify3(taskFile),
3975
- "utf-8"
3976
- );
4209
+ // src/templates/phases/fullstack-nextjs-dotnet.ts
4210
+ function phasePlanTemplate7(_input) {
4211
+ const phases = [
4212
+ {
4213
+ id: "phase-0-setup",
4214
+ name: "Monorepo Setup",
4215
+ phase_number: 0,
4216
+ status: "planned",
4217
+ description: "Initialize monorepo structure, scaffold Next.js frontend and .NET API backend, configure shared tooling",
4218
+ gate_requirements: [
4219
+ "Monorepo structure with frontend/ and api/ directories",
4220
+ "Next.js app builds and runs at localhost:3000",
4221
+ ".NET API builds and runs with health check at /health",
4222
+ "Shared dev scripts (start, build, test) work from root"
4223
+ ]
4224
+ },
4225
+ {
4226
+ id: "phase-1-api-foundation",
4227
+ name: "API Foundation",
4228
+ phase_number: 1,
4229
+ status: "planned",
4230
+ description: "Build .NET API controllers, services, authentication middleware, and database access layer",
4231
+ gate_requirements: [
4232
+ "Auth middleware protects API endpoints",
4233
+ "Global exception handler returns ProblemDetails",
4234
+ "Database access layer configured with migrations",
4235
+ "OpenAPI/Swagger documentation generated",
4236
+ "Quality scenarios SEC-01, SEC-02 verified"
4237
+ ]
4238
+ },
4239
+ {
4240
+ id: "phase-2-frontend-foundation",
4241
+ name: "Frontend Foundation",
4242
+ phase_number: 2,
4243
+ status: "planned",
4244
+ description: "Build Next.js layouts, reusable components, API client layer, and authentication UI",
4245
+ gate_requirements: [
4246
+ "App shell with layout and navigation working",
4247
+ "API client layer communicates with .NET backend",
4248
+ "Auth UI (login, register, protected routes) functional",
4249
+ "Component library with consistent styling",
4250
+ "Quality scenarios A11Y-01, PERF-01 verified"
4251
+ ]
4252
+ },
4253
+ {
4254
+ id: "phase-3-feature-integration",
4255
+ name: "Feature Integration",
4256
+ phase_number: 3,
4257
+ status: "planned",
4258
+ description: "Implement end-to-end features across frontend and backend, API contract synchronization, and shared types",
4259
+ gate_requirements: [
4260
+ "At least one full CRUD feature works end-to-end",
4261
+ "API contracts validated between frontend and backend",
4262
+ "Shared type definitions keep frontend and API in sync",
4263
+ "Integration tests cover cross-service workflows"
4264
+ ]
4265
+ },
4266
+ {
4267
+ id: "phase-4-production",
4268
+ name: "Production Readiness",
4269
+ phase_number: 4,
4270
+ status: "planned",
4271
+ description: "CI/CD pipeline, deployment configuration, monitoring, and performance optimization",
4272
+ gate_requirements: [
4273
+ "CI pipeline builds and tests both services",
4274
+ "Docker Compose or equivalent runs the full stack",
4275
+ "Rate limiting and CORS configured for production",
4276
+ "All quality scenarios passing",
4277
+ "Production deployment successful"
4278
+ ]
3977
4279
  }
3978
- }
3979
- writeFileSync3(
3980
- join5(planDir, "sync-log.md"),
3981
- `# Sync Log
3982
-
3983
- Architecture sync events are recorded here.
3984
- `,
3985
- "utf-8"
3986
- );
4280
+ ];
4281
+ return { schema_version: 1, phases };
3987
4282
  }
3988
-
3989
- // src/generators/agent-generator.ts
3990
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
4283
+ function phaseTasksTemplate7(_input, phaseId) {
4284
+ const tasksByPhase = {
4285
+ "phase-0-setup": {
4286
+ schema_version: 1,
4287
+ phase_id: "phase-0-setup",
4288
+ tasks: [
4289
+ {
4290
+ id: "task-0.1-monorepo-structure",
4291
+ title: "Set up monorepo structure with frontend and api directories",
4292
+ status: "todo",
4293
+ building_block: "frontend-shell",
4294
+ quality_scenarios: [],
4295
+ acceptance_criteria: [
4296
+ "Root package.json with workspace scripts",
4297
+ "frontend/ directory with Next.js app (App Router)",
4298
+ "api/ directory with ASP.NET Core Web API project",
4299
+ "Shared .editorconfig and linting configuration"
4300
+ ]
4301
+ },
4302
+ {
4303
+ id: "task-0.2-frontend-scaffold",
4304
+ title: "Scaffold Next.js frontend with App Router",
4305
+ status: "todo",
4306
+ building_block: "frontend-shell",
4307
+ quality_scenarios: ["MAINT-01"],
4308
+ acceptance_criteria: [
4309
+ "Next.js app created with TypeScript and App Router",
4310
+ "Root layout with metadata and font configuration",
4311
+ "Landing page renders at localhost:3000",
4312
+ "ESLint and Prettier configured"
4313
+ ]
4314
+ },
4315
+ {
4316
+ id: "task-0.3-api-scaffold",
4317
+ title: "Scaffold .NET API with health check",
4318
+ status: "todo",
4319
+ building_block: "api-controllers",
4320
+ quality_scenarios: [],
4321
+ acceptance_criteria: [
4322
+ "ASP.NET Core Web API project created",
4323
+ "Health check endpoint responds at /health",
4324
+ "DI container configured with core services",
4325
+ "Logging and configuration in place"
4326
+ ]
4327
+ },
4328
+ {
4329
+ id: "task-0.4-shared-tooling",
4330
+ title: "Configure shared development tooling and scripts",
4331
+ status: "todo",
4332
+ building_block: "shared-contracts",
4333
+ quality_scenarios: ["MAINT-02"],
4334
+ acceptance_criteria: [
4335
+ "Root-level scripts to start, build, and test both services",
4336
+ "Shared TypeScript types or OpenAPI contract directory",
4337
+ "Testing infrastructure for both frontend and backend",
4338
+ "Git hooks for linting and formatting"
4339
+ ]
4340
+ }
4341
+ ]
4342
+ },
4343
+ "phase-1-api-foundation": {
4344
+ schema_version: 1,
4345
+ phase_id: "phase-1-api-foundation",
4346
+ tasks: [
4347
+ {
4348
+ id: "task-1.1-api-auth",
4349
+ title: "Implement API authentication and authorization",
4350
+ status: "todo",
4351
+ building_block: "api-services",
4352
+ quality_scenarios: ["SEC-01", "SEC-02"],
4353
+ acceptance_criteria: [
4354
+ "JWT bearer auth configured in .NET API",
4355
+ "Authorization policies defined for role-based access",
4356
+ "Auth middleware applied to protected endpoints",
4357
+ "Token validation and refresh logic implemented"
4358
+ ]
4359
+ },
4360
+ {
4361
+ id: "task-1.2-api-controllers",
4362
+ title: "Build core API controllers and endpoints",
4363
+ status: "todo",
4364
+ building_block: "api-controllers",
4365
+ quality_scenarios: ["SEC-03"],
4366
+ acceptance_criteria: [
4367
+ "RESTful controllers for core resources",
4368
+ "Input validation via FluentValidation or data annotations",
4369
+ "Consistent response format with ProblemDetails for errors",
4370
+ "OpenAPI/Swagger documentation generated and accessible"
4371
+ ]
4372
+ },
4373
+ {
4374
+ id: "task-1.3-api-services",
4375
+ title: "Implement business logic and service layer",
4376
+ status: "todo",
4377
+ building_block: "api-services",
4378
+ quality_scenarios: ["MAINT-01"],
4379
+ acceptance_criteria: [
4380
+ "Service classes encapsulate business logic",
4381
+ "Repository pattern for data access",
4382
+ "EF Core or Dapper configured with migrations",
4383
+ "Unit tests cover service layer logic"
4384
+ ]
4385
+ },
4386
+ {
4387
+ id: "task-1.4-api-middleware",
4388
+ title: "Configure middleware pipeline and error handling",
4389
+ status: "todo",
4390
+ building_block: "api-controllers",
4391
+ quality_scenarios: ["REL-01"],
4392
+ acceptance_criteria: [
4393
+ "Global exception handler returns RFC 7807 ProblemDetails",
4394
+ "Request/response logging with correlation IDs",
4395
+ "CORS policy configured for frontend origin",
4396
+ "No stack traces leaked in production"
4397
+ ]
4398
+ }
4399
+ ]
4400
+ },
4401
+ "phase-2-frontend-foundation": {
4402
+ schema_version: 1,
4403
+ phase_id: "phase-2-frontend-foundation",
4404
+ tasks: [
4405
+ {
4406
+ id: "task-2.1-frontend-layout",
4407
+ title: "Build app shell with layouts and navigation",
4408
+ status: "todo",
4409
+ building_block: "frontend-shell",
4410
+ quality_scenarios: ["A11Y-01"],
4411
+ acceptance_criteria: [
4412
+ "Root layout with header, sidebar, and main content area",
4413
+ "Navigation component with active state indicators",
4414
+ "Responsive layout that works on mobile and desktop",
4415
+ "Loading and error boundary components"
4416
+ ]
4417
+ },
4418
+ {
4419
+ id: "task-2.2-frontend-components",
4420
+ title: "Create reusable UI component library",
4421
+ status: "todo",
4422
+ building_block: "frontend-components",
4423
+ quality_scenarios: ["A11Y-02"],
4424
+ acceptance_criteria: [
4425
+ "Button, Input, Card, Modal, and Table components",
4426
+ "Consistent styling with design tokens or Tailwind",
4427
+ "All components meet WCAG 2.1 AA accessibility",
4428
+ "Component documentation or Storybook setup"
4429
+ ]
4430
+ },
4431
+ {
4432
+ id: "task-2.3-api-client",
4433
+ title: "Build API client layer for backend communication",
4434
+ status: "todo",
4435
+ building_block: "shared-contracts",
4436
+ quality_scenarios: ["REL-01"],
4437
+ acceptance_criteria: [
4438
+ "Typed API client generated from OpenAPI spec or manually defined",
4439
+ "Error handling with user-friendly messages",
4440
+ "Auth token injection in request headers",
4441
+ "Request/response interceptors for logging and retry"
4442
+ ]
4443
+ },
4444
+ {
4445
+ id: "task-2.4-frontend-auth",
4446
+ title: "Implement frontend authentication UI and flow",
4447
+ status: "todo",
4448
+ building_block: "frontend-shell",
4449
+ quality_scenarios: ["SEC-01"],
4450
+ acceptance_criteria: [
4451
+ "Login and registration pages",
4452
+ "Protected route middleware or layout",
4453
+ "Token storage and refresh handling",
4454
+ "Auth state management (context or store)"
4455
+ ]
4456
+ }
4457
+ ]
4458
+ },
4459
+ "phase-3-feature-integration": {
4460
+ schema_version: 1,
4461
+ phase_id: "phase-3-feature-integration",
4462
+ tasks: [
4463
+ {
4464
+ id: "task-3.1-e2e-feature",
4465
+ title: "Implement first end-to-end CRUD feature",
4466
+ status: "todo",
4467
+ building_block: "api-controllers",
4468
+ quality_scenarios: ["PERF-02"],
4469
+ acceptance_criteria: [
4470
+ "Full CRUD operations from frontend through API to database",
4471
+ "Optimistic UI updates where appropriate",
4472
+ "Server-side validation reflected in frontend forms",
4473
+ "List, detail, create, edit, and delete views working"
4474
+ ]
4475
+ },
4476
+ {
4477
+ id: "task-3.2-contract-sync",
4478
+ title: "Set up API contract synchronization",
4479
+ status: "todo",
4480
+ building_block: "shared-contracts",
4481
+ quality_scenarios: ["MAINT-01"],
4482
+ acceptance_criteria: [
4483
+ "OpenAPI spec or shared type definitions in contracts directory",
4484
+ "Frontend types generated from or validated against API contract",
4485
+ "CI check prevents contract drift between frontend and backend",
4486
+ "Documentation for updating contracts when API changes"
4487
+ ]
4488
+ },
4489
+ {
4490
+ id: "task-3.3-integration-tests",
4491
+ title: "Write cross-service integration tests",
4492
+ status: "todo",
4493
+ building_block: "shared-contracts",
4494
+ quality_scenarios: ["MAINT-02"],
4495
+ acceptance_criteria: [
4496
+ "Integration tests exercise frontend-to-API workflows",
4497
+ "Test environment starts both services",
4498
+ "Auth flow tested end-to-end",
4499
+ "Error scenarios covered (API down, validation failures)"
4500
+ ]
4501
+ }
4502
+ ]
4503
+ },
4504
+ "phase-4-production": {
4505
+ schema_version: 1,
4506
+ phase_id: "phase-4-production",
4507
+ tasks: [
4508
+ {
4509
+ id: "task-4.1-ci-cd",
4510
+ title: "Set up CI/CD pipeline for both services",
4511
+ status: "todo",
4512
+ quality_scenarios: ["MAINT-02"],
4513
+ acceptance_criteria: [
4514
+ "CI builds and tests both frontend and API",
4515
+ "Lint, type-check, and test steps for each service",
4516
+ "Build artifacts produced for deployment",
4517
+ "Pipeline fails fast on errors"
4518
+ ]
4519
+ },
4520
+ {
4521
+ id: "task-4.2-deployment",
4522
+ title: "Configure deployment for full stack",
4523
+ status: "todo",
4524
+ quality_scenarios: ["REL-01"],
4525
+ acceptance_criteria: [
4526
+ "Docker Compose for local full-stack development",
4527
+ "Dockerfiles for frontend and API",
4528
+ "Environment-specific configuration (dev, staging, prod)",
4529
+ "Database migration runs as part of deployment"
4530
+ ]
4531
+ },
4532
+ {
4533
+ id: "task-4.3-monitoring",
4534
+ title: "Add monitoring, logging, and rate limiting",
4535
+ status: "todo",
4536
+ building_block: "api-services",
4537
+ quality_scenarios: ["PERF-02", "SEC-04"],
4538
+ acceptance_criteria: [
4539
+ "Structured logging in API with Serilog or similar",
4540
+ "Frontend error tracking (Sentry or similar)",
4541
+ "Rate limiting configured on API endpoints",
4542
+ "Health check dashboard or monitoring alerts"
4543
+ ]
4544
+ },
4545
+ {
4546
+ id: "task-4.4-performance",
4547
+ title: "Optimize performance across the stack",
4548
+ status: "todo",
4549
+ building_block: "frontend-shell",
4550
+ quality_scenarios: ["PERF-01", "PERF-03"],
4551
+ acceptance_criteria: [
4552
+ "Frontend bundle size optimized (code splitting, tree shaking)",
4553
+ "API response times meet p95 < 200ms target",
4554
+ "Database queries optimized with proper indexing",
4555
+ "Static assets cached with appropriate headers"
4556
+ ]
4557
+ }
4558
+ ]
4559
+ }
4560
+ };
4561
+ return tasksByPhase[phaseId] ?? null;
4562
+ }
4563
+
4564
+ // src/generators/plan-generator.ts
4565
+ var planTemplates = {
4566
+ "nextjs-app-router": { plan: phasePlanTemplate, tasks: phaseTasksTemplate },
4567
+ "react-vite": { plan: phasePlanTemplate2, tasks: phaseTasksTemplate2 },
4568
+ "api-service": { plan: phasePlanTemplate3, tasks: phaseTasksTemplate3 },
4569
+ "dotnet-webapi": { plan: phasePlanTemplate4, tasks: phaseTasksTemplate4 },
4570
+ "unity-game": { plan: phasePlanTemplate5, tasks: phaseTasksTemplate5 },
4571
+ "angular-app": { plan: phasePlanTemplate6, tasks: phaseTasksTemplate6 },
4572
+ "fullstack-nextjs-dotnet": { plan: phasePlanTemplate7, tasks: phaseTasksTemplate7 }
4573
+ };
4574
+ function generatePlan(targetDir, input) {
4575
+ const planDir = join5(targetDir, ".arcbridge", "plan");
4576
+ const tasksDir = join5(planDir, "tasks");
4577
+ mkdirSync3(planDir, { recursive: true });
4578
+ mkdirSync3(tasksDir, { recursive: true });
4579
+ const tmpl = planTemplates[input.template] ?? planTemplates["nextjs-app-router"];
4580
+ const phasePlan = tmpl.plan(input);
4581
+ writeFileSync3(join5(planDir, "phases.yaml"), stringify3(phasePlan), "utf-8");
4582
+ for (const phase of phasePlan.phases) {
4583
+ const taskFile = tmpl.tasks(input, phase.id);
4584
+ if (taskFile) {
4585
+ writeFileSync3(
4586
+ join5(tasksDir, `${phase.id}.yaml`),
4587
+ stringify3(taskFile),
4588
+ "utf-8"
4589
+ );
4590
+ }
4591
+ }
4592
+ writeFileSync3(
4593
+ join5(planDir, "sync-log.md"),
4594
+ `# Sync Log
4595
+
4596
+ Architecture sync events are recorded here.
4597
+ `,
4598
+ "utf-8"
4599
+ );
4600
+ }
4601
+
4602
+ // src/generators/agent-generator.ts
4603
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3991
4604
  import { join as join6 } from "path";
3992
4605
  import matter2 from "gray-matter";
3993
4606
 
@@ -4630,7 +5243,7 @@ You cannot see screenshots, but you CAN reason about UI quality through code:
4630
5243
  }
4631
5244
 
4632
5245
  // src/generators/agent-generator.ts
4633
- var UI_TEMPLATES = /* @__PURE__ */ new Set(["nextjs-app-router", "react-vite", "angular-app", "unity-game"]);
5246
+ var UI_TEMPLATES = /* @__PURE__ */ new Set(["nextjs-app-router", "react-vite", "angular-app", "unity-game", "fullstack-nextjs-dotnet"]);
4634
5247
  function writeAgentRole(dir, role) {
4635
5248
  const { system_prompt, ...frontmatter } = role;
4636
5249
  const content = matter2.stringify(system_prompt, frontmatter);
@@ -4964,8 +5577,9 @@ function ensureGitignore(targetDir) {
4964
5577
  }
4965
5578
 
4966
5579
  // src/indexer/index.ts
4967
- import { relative as relative5, join as join14 } from "path";
4968
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
5580
+ import ts6 from "typescript";
5581
+ import { relative as relative5, join as join16 } from "path";
5582
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
4969
5583
  import { execFileSync as execFileSync2 } from "child_process";
4970
5584
  import YAML from "yaml";
4971
5585
 
@@ -5333,7 +5947,7 @@ function buildSymbolLookup(symbols) {
5333
5947
  function extractDependencies(sourceFile, checker, relativePath, projectRoot, lookup) {
5334
5948
  const deps = [];
5335
5949
  const seen = /* @__PURE__ */ new Set();
5336
- function addDep2(sourceId, targetId, kind) {
5950
+ function addDep4(sourceId, targetId, kind) {
5337
5951
  const key = `${sourceId}|${targetId}|${kind}`;
5338
5952
  if (seen.has(key)) return;
5339
5953
  if (!lookup.allIds.has(sourceId) || !lookup.allIds.has(targetId)) return;
@@ -5369,7 +5983,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5369
5983
  if (!targetId) continue;
5370
5984
  const fileSymbols = getFileTopLevelSymbols(relativePath, lookup);
5371
5985
  for (const sourceId of fileSymbols) {
5372
- addDep2(sourceId, targetId, "imports");
5986
+ addDep4(sourceId, targetId, "imports");
5373
5987
  }
5374
5988
  }
5375
5989
  }
@@ -5381,7 +5995,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5381
5995
  if (targetId) {
5382
5996
  const fileSymbols = getFileTopLevelSymbols(relativePath, lookup);
5383
5997
  for (const sourceId of fileSymbols) {
5384
- addDep2(sourceId, targetId, "imports");
5998
+ addDep4(sourceId, targetId, "imports");
5385
5999
  }
5386
6000
  }
5387
6001
  }
@@ -5403,7 +6017,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5403
6017
  const resolved = exprSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(exprSymbol) : exprSymbol;
5404
6018
  const targetId = resolveSymbolId(resolved);
5405
6019
  if (targetId) {
5406
- addDep2(classId, targetId, kind);
6020
+ addDep4(classId, targetId, kind);
5407
6021
  }
5408
6022
  }
5409
6023
  }
@@ -5426,7 +6040,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5426
6040
  const resolved = calledSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(calledSymbol) : calledSymbol;
5427
6041
  const targetId = resolveSymbolId(resolved);
5428
6042
  if (targetId) {
5429
- addDep2(ownerId, targetId, "calls");
6043
+ addDep4(ownerId, targetId, "calls");
5430
6044
  }
5431
6045
  }
5432
6046
  }
@@ -5456,7 +6070,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5456
6070
  const resolved = calledSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(calledSymbol) : calledSymbol;
5457
6071
  const targetId = resolveSymbolId(resolved);
5458
6072
  if (targetId) {
5459
- addDep2(callOwnerId, targetId, "calls");
6073
+ addDep4(callOwnerId, targetId, "calls");
5460
6074
  }
5461
6075
  }
5462
6076
  }
@@ -5488,7 +6102,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5488
6102
  const resolved = typeSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(typeSymbol) : typeSymbol;
5489
6103
  const targetId = resolveSymbolId(resolved);
5490
6104
  if (targetId) {
5491
- addDep2(ownerId, targetId, "uses_type");
6105
+ addDep4(ownerId, targetId, "uses_type");
5492
6106
  }
5493
6107
  }
5494
6108
  }
@@ -5512,7 +6126,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5512
6126
  const resolved = jsxSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(jsxSymbol) : jsxSymbol;
5513
6127
  const targetId = resolveSymbolId(resolved);
5514
6128
  if (targetId) {
5515
- addDep2(ownerId, targetId, "renders");
6129
+ addDep4(ownerId, targetId, "renders");
5516
6130
  }
5517
6131
  }
5518
6132
  if (ts4.isPropertyAccessExpression(tagName) && ts4.isIdentifier(tagName.name) && tagName.name.text === "Provider") {
@@ -5521,7 +6135,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5521
6135
  const resolved = ctxSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(ctxSymbol) : ctxSymbol;
5522
6136
  const targetId = resolveSymbolId(resolved);
5523
6137
  if (targetId) {
5524
- addDep2(ownerId, targetId, "provides_context");
6138
+ addDep4(ownerId, targetId, "provides_context");
5525
6139
  }
5526
6140
  }
5527
6141
  }
@@ -5568,7 +6182,7 @@ function extractDependencies(sourceFile, checker, relativePath, projectRoot, loo
5568
6182
  const resolved = ctxSymbol.flags & ts4.SymbolFlags.Alias ? checker.getAliasedSymbol(ctxSymbol) : ctxSymbol;
5569
6183
  const targetId = resolveSymbolId(resolved);
5570
6184
  if (targetId) {
5571
- addDep2(ownerId, targetId, "consumes_context");
6185
+ addDep4(ownerId, targetId, "consumes_context");
5572
6186
  }
5573
6187
  }
5574
6188
  }
@@ -5902,7 +6516,7 @@ function analyzeRoutes(projectRoot, db, service = "main") {
5902
6516
  }
5903
6517
  }
5904
6518
  walkAppDir(appDir, "/", routes, layoutStack, service, projectRoot);
5905
- writeRoutes(db, routes);
6519
+ writeRoutes(db, routes, service);
5906
6520
  return routes.length;
5907
6521
  }
5908
6522
  function walkAppDir(dir, routePath, routes, layoutStack, service, projectRoot) {
@@ -5985,9 +6599,9 @@ function extractHttpMethods(filePath) {
5985
6599
  }
5986
6600
  return methods;
5987
6601
  }
5988
- function writeRoutes(db, routes) {
6602
+ function writeRoutes(db, routes, service) {
6603
+ db.prepare("DELETE FROM routes WHERE service = ?").run(service);
5989
6604
  if (routes.length === 0) return;
5990
- db.prepare("DELETE FROM routes").run();
5991
6605
  const insert = db.prepare(`
5992
6606
  INSERT OR IGNORE INTO routes (
5993
6607
  id, route_path, kind, http_methods, has_auth, parent_layout, service
@@ -5996,7 +6610,7 @@ function writeRoutes(db, routes) {
5996
6610
  transaction(db, () => {
5997
6611
  for (const r of routes) {
5998
6612
  insert.run(
5999
- r.id,
6613
+ `${service}::${r.id}`,
6000
6614
  r.routePath,
6001
6615
  r.kind,
6002
6616
  JSON.stringify(r.httpMethods),
@@ -6015,36 +6629,37 @@ function hashContent(content) {
6015
6629
  }
6016
6630
 
6017
6631
  // src/indexer/db-writer.ts
6018
- function getExistingHashes(db, service) {
6019
- const rows = db.prepare(
6020
- "SELECT DISTINCT file_path, content_hash FROM symbols WHERE service = ?"
6021
- ).all(service);
6632
+ function getExistingHashes(db, service, language) {
6633
+ const query = language ? "SELECT DISTINCT file_path, content_hash FROM symbols WHERE service = ? AND language = ?" : "SELECT DISTINCT file_path, content_hash FROM symbols WHERE service = ?";
6634
+ const params = language ? [service, language] : [service];
6635
+ const rows = db.prepare(query).all(...params);
6022
6636
  const map = /* @__PURE__ */ new Map();
6023
6637
  for (const row of rows) {
6024
6638
  map.set(row.file_path, row.content_hash);
6025
6639
  }
6026
6640
  return map;
6027
6641
  }
6028
- function removeSymbolsForFiles(db, filePaths) {
6642
+ function removeScopedSymbolsForFiles(db, filePaths, service, language) {
6029
6643
  if (filePaths.length === 0) return;
6030
- const deleteSymbols = db.prepare(
6031
- "DELETE FROM symbols WHERE file_path = ?"
6032
- );
6644
+ const scope = "file_path = ? AND service = ? AND language = ?";
6033
6645
  const deleteDepsSource = db.prepare(
6034
- "DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE file_path = ?)"
6646
+ `DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE ${scope})`
6035
6647
  );
6036
6648
  const deleteDepsTarget = db.prepare(
6037
- "DELETE FROM dependencies WHERE target_symbol IN (SELECT id FROM symbols WHERE file_path = ?)"
6649
+ `DELETE FROM dependencies WHERE target_symbol IN (SELECT id FROM symbols WHERE ${scope})`
6038
6650
  );
6039
6651
  const deleteComponents = db.prepare(
6040
- "DELETE FROM components WHERE symbol_id IN (SELECT id FROM symbols WHERE file_path = ?)"
6652
+ `DELETE FROM components WHERE symbol_id IN (SELECT id FROM symbols WHERE ${scope})`
6653
+ );
6654
+ const deleteSymbols = db.prepare(
6655
+ `DELETE FROM symbols WHERE ${scope}`
6041
6656
  );
6042
6657
  transaction(db, () => {
6043
6658
  for (const fp of filePaths) {
6044
- deleteDepsSource.run(fp);
6045
- deleteDepsTarget.run(fp);
6046
- deleteComponents.run(fp);
6047
- deleteSymbols.run(fp);
6659
+ deleteDepsSource.run(fp, service, language);
6660
+ deleteDepsTarget.run(fp, service, language);
6661
+ deleteComponents.run(fp, service, language);
6662
+ deleteSymbols.run(fp, service, language);
6048
6663
  }
6049
6664
  });
6050
6665
  }
@@ -6177,11 +6792,11 @@ function hasGlobalTool() {
6177
6792
  return false;
6178
6793
  }
6179
6794
  function resolveIndexerProject() {
6180
- const currentDir2 = dirname2(fileURLToPath(import.meta.url));
6795
+ const currentDir4 = dirname2(fileURLToPath(import.meta.url));
6181
6796
  const candidates = [
6182
- resolve(currentDir2, "../../../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj"),
6183
- resolve(currentDir2, "../../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj"),
6184
- resolve(currentDir2, "../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj")
6797
+ resolve(currentDir4, "../../../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj"),
6798
+ resolve(currentDir4, "../../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj"),
6799
+ resolve(currentDir4, "../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj")
6185
6800
  ];
6186
6801
  for (const candidate of candidates) {
6187
6802
  if (existsSync5(candidate)) return candidate;
@@ -6247,7 +6862,7 @@ function indexDotnetProjectRoslyn(db, options) {
6247
6862
  "No .sln or .csproj file found in project root. The .NET indexer requires a project or solution file."
6248
6863
  );
6249
6864
  }
6250
- const existingHashes = getExistingHashes(db, service);
6865
+ const existingHashes = getExistingHashes(db, service, "csharp");
6251
6866
  const hashesJson = JSON.stringify(Object.fromEntries(existingHashes));
6252
6867
  const stdout = runDotnetIndexer(dotnetProject, hashesJson, projectRoot);
6253
6868
  const lines = stdout.trim().split("\n");
@@ -6264,7 +6879,7 @@ function indexDotnetProjectRoslyn(db, options) {
6264
6879
  );
6265
6880
  }
6266
6881
  const filesToClean = [...output.changedFiles, ...output.removedFiles];
6267
- removeSymbolsForFiles(db, filesToClean);
6882
+ removeScopedSymbolsForFiles(db, filesToClean, service, "csharp");
6268
6883
  const symbols = output.symbols.map((s) => ({
6269
6884
  id: s.id,
6270
6885
  name: s.name,
@@ -6301,7 +6916,7 @@ function indexDotnetProjectRoslyn(db, options) {
6301
6916
  transaction(db, () => {
6302
6917
  for (const route of output.routes) {
6303
6918
  insertRoute.run(
6304
- route.id,
6919
+ `${service}::${route.id}`,
6305
6920
  route.routePath,
6306
6921
  route.kind,
6307
6922
  JSON.stringify(route.httpMethods),
@@ -7184,7 +7799,7 @@ async function indexCSharpTreeSitter(db, options) {
7184
7799
  ignore: ignorePatterns,
7185
7800
  absolute: false
7186
7801
  });
7187
- const existingHashes = getExistingHashes(db, service);
7802
+ const existingHashes = getExistingHashes(db, service, "csharp");
7188
7803
  const currentPaths = /* @__PURE__ */ new Set();
7189
7804
  const fileCache = /* @__PURE__ */ new Map();
7190
7805
  const changedFiles = [];
@@ -7211,7 +7826,7 @@ async function indexCSharpTreeSitter(db, options) {
7211
7826
  }
7212
7827
  }
7213
7828
  const filesToClean = [...removed, ...changedFiles];
7214
- removeSymbolsForFiles(db, filesToClean);
7829
+ removeScopedSymbolsForFiles(db, filesToClean, service, "csharp");
7215
7830
  const allNewSymbols = [];
7216
7831
  for (const relPath of changedFiles) {
7217
7832
  const cached = fileCache.get(relPath);
@@ -7220,7 +7835,7 @@ async function indexCSharpTreeSitter(db, options) {
7220
7835
  allNewSymbols.push(...symbols);
7221
7836
  }
7222
7837
  writeSymbols(db, allNewSymbols, service, "csharp");
7223
- const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name, kind, start_line as startLine, end_line as endLine FROM symbols WHERE service = ?").all(service);
7838
+ const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name, kind, start_line as startLine, end_line as endLine FROM symbols WHERE service = ? AND language = 'csharp'").all(service);
7224
7839
  const symbolLookup = buildCSharpSymbolLookup(allDbSymbols);
7225
7840
  const allDeps = [];
7226
7841
  for (const [relPath, cached] of fileCache) {
@@ -7245,7 +7860,7 @@ async function indexCSharpTreeSitter(db, options) {
7245
7860
  transaction(db, () => {
7246
7861
  for (const route of allRoutes) {
7247
7862
  insertRoute.run(
7248
- route.id,
7863
+ `${service}::${route.id}`,
7249
7864
  route.routePath,
7250
7865
  route.kind,
7251
7866
  JSON.stringify(route.httpMethods),
@@ -7267,54 +7882,1580 @@ async function indexCSharpTreeSitter(db, options) {
7267
7882
  };
7268
7883
  }
7269
7884
 
7270
- // src/indexer/package-deps.ts
7885
+ // src/indexer/python/indexer.ts
7886
+ import { readFileSync as readFileSync5 } from "fs";
7271
7887
  import { join as join12 } from "path";
7272
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "fs";
7273
- function indexPackageDependencies(db, projectRoot, service = "main") {
7274
- const deps = [];
7275
- const pkgJsonPath = join12(projectRoot, "package.json");
7276
- if (existsSync6(pkgJsonPath)) {
7277
- deps.push(...parsePackageJson(pkgJsonPath));
7278
- }
7279
- const csprojFiles = findCsprojFiles(projectRoot);
7280
- for (const csproj of csprojFiles) {
7281
- deps.push(...parseCsproj(csproj));
7888
+ import { globbySync as globbySync2 } from "globby";
7889
+
7890
+ // src/indexer/python/parser.ts
7891
+ import { accessSync as accessSync3, constants as constants3 } from "fs";
7892
+ import { dirname as dirname4, resolve as resolve3 } from "path";
7893
+ import { fileURLToPath as fileURLToPath3 } from "url";
7894
+ import "web-tree-sitter";
7895
+ var currentDir2 = dirname4(fileURLToPath3(import.meta.url));
7896
+ var cachedParser2 = null;
7897
+ var initPromise2 = null;
7898
+ function resolveGrammarPath2() {
7899
+ const candidates = [
7900
+ resolve3(currentDir2, "../../../wasm/tree-sitter-python.wasm"),
7901
+ // from src/indexer/python/
7902
+ resolve3(currentDir2, "../wasm/tree-sitter-python.wasm")
7903
+ // from dist/ (tsup flat bundle)
7904
+ ];
7905
+ for (const candidate of candidates) {
7906
+ try {
7907
+ accessSync3(candidate, constants3.R_OK);
7908
+ return candidate;
7909
+ } catch {
7910
+ continue;
7911
+ }
7282
7912
  }
7283
- if (deps.length === 0) return 0;
7284
- db.prepare("DELETE FROM package_dependencies WHERE service = ?").run(service);
7285
- const insert = db.prepare(
7286
- "INSERT OR IGNORE INTO package_dependencies (name, version, source, service) VALUES (?, ?, ?, ?)"
7913
+ throw new Error(
7914
+ "Could not find tree-sitter-python.wasm. Ensure the wasm/ directory is present in the @arcbridge/core package."
7287
7915
  );
7288
- transaction(db, () => {
7289
- for (const dep of deps) {
7290
- insert.run(dep.name, dep.version, dep.source, service);
7916
+ }
7917
+ async function ensurePythonParser() {
7918
+ if (cachedParser2) return cachedParser2;
7919
+ if (initPromise2) return initPromise2;
7920
+ initPromise2 = (async () => {
7921
+ try {
7922
+ const TreeSitter = await import("web-tree-sitter");
7923
+ await TreeSitter.Parser.init();
7924
+ const grammarPath = resolveGrammarPath2();
7925
+ const Python = await TreeSitter.Language.load(grammarPath);
7926
+ const parser = new TreeSitter.Parser();
7927
+ parser.setLanguage(Python);
7928
+ cachedParser2 = parser;
7929
+ return parser;
7930
+ } catch (err) {
7931
+ initPromise2 = null;
7932
+ throw err;
7291
7933
  }
7292
- });
7293
- return deps.length;
7934
+ })();
7935
+ return initPromise2;
7294
7936
  }
7295
- function parsePackageJson(filePath) {
7296
- try {
7297
- const content = readFileSync5(filePath, "utf-8");
7298
- const pkg = JSON.parse(content);
7299
- const deps = [];
7300
- if (pkg.dependencies) {
7301
- for (const [name, version] of Object.entries(pkg.dependencies)) {
7302
- deps.push({ name, version, source: "npm" });
7937
+ function parsePython(content) {
7938
+ if (!cachedParser2) {
7939
+ throw new Error(
7940
+ "Python parser not initialized. Call await ensurePythonParser() first."
7941
+ );
7942
+ }
7943
+ const tree = cachedParser2.parse(content);
7944
+ if (!tree) {
7945
+ throw new Error("Failed to parse Python content");
7946
+ }
7947
+ return tree;
7948
+ }
7949
+
7950
+ // src/indexer/python/symbol-extractor.ts
7951
+ function extractPythonSymbols(tree, relativePath, contentHash) {
7952
+ const symbols = [];
7953
+ const root = tree.rootNode;
7954
+ for (const child of root.namedChildren) {
7955
+ walkNode2(child, null, relativePath, contentHash, symbols);
7956
+ }
7957
+ return symbols;
7958
+ }
7959
+ function walkNode2(node, currentClassName, relativePath, contentHash, symbols) {
7960
+ switch (node.type) {
7961
+ case "decorated_definition": {
7962
+ const decorators = extractDecorators(node);
7963
+ const definition = findChild4(node, "function_definition") ?? findChild4(node, "class_definition");
7964
+ if (definition) {
7965
+ walkDefinitionNode(definition, currentClassName, relativePath, contentHash, symbols, decorators);
7303
7966
  }
7967
+ return;
7304
7968
  }
7305
- if (pkg.devDependencies) {
7306
- for (const [name, version] of Object.entries(pkg.devDependencies)) {
7307
- deps.push({ name, version, source: "npm-dev" });
7969
+ case "function_definition":
7970
+ case "class_definition":
7971
+ walkDefinitionNode(node, currentClassName, relativePath, contentHash, symbols, []);
7972
+ return;
7973
+ case "expression_statement": {
7974
+ if (!currentClassName) {
7975
+ extractAssignment(node, relativePath, contentHash, symbols);
7308
7976
  }
7977
+ return;
7309
7978
  }
7310
- return deps;
7311
- } catch {
7312
- return [];
7313
7979
  }
7314
- }
7315
- function parseCsproj(filePath) {
7316
- try {
7317
- const content = readFileSync5(filePath, "utf-8");
7980
+ for (const child of node.namedChildren) {
7981
+ walkNode2(child, currentClassName, relativePath, contentHash, symbols);
7982
+ }
7983
+ }
7984
+ function walkDefinitionNode(node, currentClassName, relativePath, contentHash, symbols, decorators) {
7985
+ if (node.type === "function_definition") {
7986
+ const name = getIdentifierName2(node);
7987
+ if (!name) return;
7988
+ const isMethod = currentClassName !== null;
7989
+ const qualifiedName = isMethod ? `${currentClassName}.${name}` : name;
7990
+ const isAsync = hasAsyncKeyword(node);
7991
+ const signature = extractFunctionSignature(node);
7992
+ const returnType = extractReturnType2(node);
7993
+ const docComment = extractDocstring(node);
7994
+ const decoratorText = decorators.length > 0 ? decorators.join("\n") : null;
7995
+ const fullDocComment = decoratorText ? docComment ? `${decoratorText}
7996
+ ${docComment}` : decoratorText : docComment;
7997
+ symbols.push(makeSymbol2({
7998
+ name,
7999
+ qualifiedName,
8000
+ kind: "function",
8001
+ node,
8002
+ relativePath,
8003
+ contentHash,
8004
+ isExported: !name.startsWith("_"),
8005
+ isAsync,
8006
+ signature,
8007
+ returnType,
8008
+ docComment: fullDocComment
8009
+ }));
8010
+ return;
8011
+ }
8012
+ if (node.type === "class_definition") {
8013
+ const name = getIdentifierName2(node);
8014
+ if (!name) return;
8015
+ const qualifiedName = currentClassName ? `${currentClassName}.${name}` : name;
8016
+ const docComment = extractDocstring(node);
8017
+ const decoratorText = decorators.length > 0 ? decorators.join("\n") : null;
8018
+ const fullDocComment = decoratorText ? docComment ? `${decoratorText}
8019
+ ${docComment}` : decoratorText : docComment;
8020
+ symbols.push(makeSymbol2({
8021
+ name,
8022
+ qualifiedName,
8023
+ kind: "class",
8024
+ node,
8025
+ relativePath,
8026
+ contentHash,
8027
+ isExported: !name.startsWith("_"),
8028
+ docComment: fullDocComment
8029
+ }));
8030
+ const body = findChild4(node, "block");
8031
+ if (body) {
8032
+ for (const child of body.namedChildren) {
8033
+ walkNode2(child, qualifiedName, relativePath, contentHash, symbols);
8034
+ }
8035
+ }
8036
+ return;
8037
+ }
8038
+ }
8039
+ function extractAssignment(node, relativePath, contentHash, symbols) {
8040
+ const assignment = findChild4(node, "assignment");
8041
+ if (!assignment) return;
8042
+ const left = assignment.childForFieldName("left") ?? assignment.namedChildren[0];
8043
+ if (!left || left.type !== "identifier") return;
8044
+ const name = left.text;
8045
+ if (name.startsWith("__") && name.endsWith("__")) return;
8046
+ const isAllCaps = /^[A-Z][A-Z0-9_]*$/.test(name);
8047
+ const kind = isAllCaps ? "constant" : "variable";
8048
+ const typeNode = assignment.childForFieldName("type");
8049
+ const returnType = typeNode ? typeNode.text : null;
8050
+ symbols.push(makeSymbol2({
8051
+ name,
8052
+ qualifiedName: name,
8053
+ kind,
8054
+ node: assignment,
8055
+ relativePath,
8056
+ contentHash,
8057
+ isExported: !name.startsWith("_"),
8058
+ returnType
8059
+ }));
8060
+ }
8061
+ function makeSymbol2(args) {
8062
+ const { name, qualifiedName, kind, node, relativePath, contentHash } = args;
8063
+ return {
8064
+ id: `${relativePath}::${qualifiedName}#${kind}`,
8065
+ name,
8066
+ qualifiedName,
8067
+ kind,
8068
+ filePath: relativePath,
8069
+ startLine: node.startPosition.row + 1,
8070
+ endLine: node.endPosition.row + 1,
8071
+ startCol: node.startPosition.column + 1,
8072
+ endCol: node.endPosition.column + 1,
8073
+ signature: args.signature ?? null,
8074
+ returnType: args.returnType ?? null,
8075
+ docComment: args.docComment ?? null,
8076
+ isExported: args.isExported ?? true,
8077
+ isAsync: args.isAsync ?? false,
8078
+ contentHash
8079
+ };
8080
+ }
8081
+ function getIdentifierName2(node) {
8082
+ const nameNode = node.childForFieldName("name");
8083
+ if (nameNode) return nameNode.text;
8084
+ for (const child of node.namedChildren) {
8085
+ if (child.type === "identifier") return child.text;
8086
+ }
8087
+ return null;
8088
+ }
8089
+ function findChild4(node, type) {
8090
+ for (const child of node.namedChildren) {
8091
+ if (child.type === type) return child;
8092
+ }
8093
+ return null;
8094
+ }
8095
+ function hasAsyncKeyword(node) {
8096
+ for (const child of node.children) {
8097
+ if (child.type === "async") return true;
8098
+ if (child.type === "def") break;
8099
+ }
8100
+ return false;
8101
+ }
8102
+ function extractFunctionSignature(node) {
8103
+ const params = node.childForFieldName("parameters") ?? findChild4(node, "parameters");
8104
+ if (!params) return null;
8105
+ const name = getIdentifierName2(node) ?? "func";
8106
+ return `${name}${params.text}`;
8107
+ }
8108
+ function extractReturnType2(node) {
8109
+ const returnType = node.childForFieldName("return_type");
8110
+ if (returnType) return returnType.text;
8111
+ return null;
8112
+ }
8113
+ function extractDocstring(node) {
8114
+ const body = findChild4(node, "block");
8115
+ if (!body || body.namedChildren.length === 0) return null;
8116
+ const firstStmt = body.namedChildren[0];
8117
+ if (firstStmt.type !== "expression_statement") return null;
8118
+ const expr = firstStmt.namedChildren[0];
8119
+ if (!expr || expr.type !== "string") return null;
8120
+ let text = expr.text;
8121
+ if (text.startsWith('"""') && text.endsWith('"""')) {
8122
+ text = text.slice(3, -3);
8123
+ } else if (text.startsWith("'''") && text.endsWith("'''")) {
8124
+ text = text.slice(3, -3);
8125
+ } else if (text.startsWith('"') && text.endsWith('"')) {
8126
+ text = text.slice(1, -1);
8127
+ } else if (text.startsWith("'") && text.endsWith("'")) {
8128
+ text = text.slice(1, -1);
8129
+ }
8130
+ return text.trim() || null;
8131
+ }
8132
+ function extractDecorators(node) {
8133
+ const decorators = [];
8134
+ for (const child of node.namedChildren) {
8135
+ if (child.type === "decorator") {
8136
+ decorators.push(child.text);
8137
+ }
8138
+ }
8139
+ return decorators;
8140
+ }
8141
+
8142
+ // src/indexer/python/dependency-extractor.ts
8143
+ function buildPythonSymbolLookup(symbols) {
8144
+ const lookup = /* @__PURE__ */ new Map();
8145
+ for (const sym of symbols) {
8146
+ const existing = lookup.get(sym.name);
8147
+ if (existing) {
8148
+ existing.push(sym.id);
8149
+ } else {
8150
+ lookup.set(sym.name, [sym.id]);
8151
+ }
8152
+ }
8153
+ return lookup;
8154
+ }
8155
+ function extractPythonDependencies(tree, relativePath, allSymbols, symbolLookup) {
8156
+ const deps = [];
8157
+ const seen = /* @__PURE__ */ new Set();
8158
+ const fileSymbols = allSymbols.filter((s) => s.filePath === relativePath);
8159
+ walkForDependencies2(tree.rootNode, fileSymbols, symbolLookup, deps, seen);
8160
+ return deps;
8161
+ }
8162
+ function walkForDependencies2(node, fileSymbols, lookup, deps, seen) {
8163
+ switch (node.type) {
8164
+ case "class_definition":
8165
+ extractInheritanceDeps2(node, fileSymbols, lookup, deps, seen);
8166
+ break;
8167
+ case "call":
8168
+ extractCallDeps2(node, fileSymbols, lookup, deps, seen);
8169
+ break;
8170
+ }
8171
+ if (node.type === "typed_parameter" || node.type === "typed_default_parameter") {
8172
+ extractTypeAnnotationDeps(node, fileSymbols, lookup, deps, seen);
8173
+ }
8174
+ for (const child of node.namedChildren) {
8175
+ walkForDependencies2(child, fileSymbols, lookup, deps, seen);
8176
+ }
8177
+ }
8178
+ function extractInheritanceDeps2(node, fileSymbols, lookup, deps, seen) {
8179
+ const className = getNodeIdentifier2(node);
8180
+ if (!className) return;
8181
+ const sourceSymbol = findEnclosingSymbol2(className, fileSymbols, "class");
8182
+ if (!sourceSymbol) return;
8183
+ const argList = node.childForFieldName("superclasses") ?? findChild5(node, "argument_list");
8184
+ if (!argList) return;
8185
+ for (const child of argList.namedChildren) {
8186
+ const baseName = extractSimpleTypeName2(child);
8187
+ if (!baseName || baseName === className) continue;
8188
+ const targetIds = lookup.get(baseName);
8189
+ if (!targetIds) continue;
8190
+ for (const targetId of targetIds) {
8191
+ if (targetId === sourceSymbol.id) continue;
8192
+ addDep2(deps, seen, sourceSymbol.id, targetId, "extends");
8193
+ }
8194
+ }
8195
+ }
8196
+ function extractCallDeps2(node, fileSymbols, lookup, deps, seen) {
8197
+ const calledName = extractCalledFunctionName(node);
8198
+ if (!calledName) return;
8199
+ const enclosing = findEnclosingFunctionForNode(node, fileSymbols);
8200
+ if (!enclosing) return;
8201
+ const targetIds = lookup.get(calledName);
8202
+ if (!targetIds) return;
8203
+ for (const targetId of targetIds) {
8204
+ if (targetId === enclosing.id) continue;
8205
+ addDep2(deps, seen, enclosing.id, targetId, "calls");
8206
+ }
8207
+ }
8208
+ function extractTypeAnnotationDeps(node, fileSymbols, lookup, deps, seen) {
8209
+ const typeNames = collectTypeIdentifiers2(node);
8210
+ if (typeNames.length === 0) return;
8211
+ const enclosing = findEnclosingSymbolForNode2(node, fileSymbols);
8212
+ if (!enclosing) return;
8213
+ for (const typeName of typeNames) {
8214
+ const targetIds = lookup.get(typeName);
8215
+ if (!targetIds) continue;
8216
+ for (const targetId of targetIds) {
8217
+ if (targetId === enclosing.id) continue;
8218
+ addDep2(deps, seen, enclosing.id, targetId, "uses_type");
8219
+ }
8220
+ }
8221
+ }
8222
+ function addDep2(deps, seen, source, target, kind) {
8223
+ const key = `${source}|${target}|${kind}`;
8224
+ if (seen.has(key)) return;
8225
+ seen.add(key);
8226
+ deps.push({ sourceSymbolId: source, targetSymbolId: target, kind });
8227
+ }
8228
+ function findChild5(node, type) {
8229
+ for (const child of node.namedChildren) {
8230
+ if (child.type === type) return child;
8231
+ }
8232
+ return null;
8233
+ }
8234
+ function getNodeIdentifier2(node) {
8235
+ const nameNode = node.childForFieldName("name");
8236
+ if (nameNode) return nameNode.text;
8237
+ for (const child of node.namedChildren) {
8238
+ if (child.type === "identifier") return child.text;
8239
+ }
8240
+ return null;
8241
+ }
8242
+ function extractSimpleTypeName2(node) {
8243
+ if (node.type === "identifier") return node.text;
8244
+ if (node.type === "attribute") {
8245
+ const attrNode = node.childForFieldName("attribute");
8246
+ return attrNode?.text ?? null;
8247
+ }
8248
+ if (node.type === "subscript") {
8249
+ const value = node.childForFieldName("value") ?? node.namedChildren[0];
8250
+ if (value) return extractSimpleTypeName2(value);
8251
+ }
8252
+ if (node.type === "keyword_argument") {
8253
+ const value = node.childForFieldName("value") ?? node.namedChildren[1];
8254
+ if (value) return extractSimpleTypeName2(value);
8255
+ }
8256
+ for (const child of node.namedChildren) {
8257
+ if (child.type === "identifier") return child.text;
8258
+ }
8259
+ return null;
8260
+ }
8261
+ function extractCalledFunctionName(node) {
8262
+ const funcNode = node.childForFieldName("function") ?? node.namedChildren[0];
8263
+ if (!funcNode) return null;
8264
+ if (funcNode.type === "identifier") return funcNode.text;
8265
+ if (funcNode.type === "attribute") {
8266
+ const attrNode = funcNode.childForFieldName("attribute");
8267
+ return attrNode?.text ?? null;
8268
+ }
8269
+ return null;
8270
+ }
8271
+ function findEnclosingSymbol2(name, fileSymbols, ...kinds) {
8272
+ return fileSymbols.find(
8273
+ (s) => s.name === name && kinds.includes(s.kind)
8274
+ );
8275
+ }
8276
+ function findEnclosingFunctionForNode(node, fileSymbols) {
8277
+ let current = node.parent;
8278
+ while (current) {
8279
+ if (current.type === "function_definition") {
8280
+ const name = getNodeIdentifier2(current);
8281
+ if (name) {
8282
+ return fileSymbols.find(
8283
+ (s) => s.kind === "function" && s.name === name && s.startLine <= current.startPosition.row + 1 && s.endLine >= current.endPosition.row + 1
8284
+ );
8285
+ }
8286
+ }
8287
+ current = current.parent;
8288
+ }
8289
+ return void 0;
8290
+ }
8291
+ function findEnclosingSymbolForNode2(node, fileSymbols) {
8292
+ let current = node.parent;
8293
+ while (current) {
8294
+ if (current.type === "function_definition") {
8295
+ const name = getNodeIdentifier2(current);
8296
+ if (name) {
8297
+ const found = fileSymbols.find(
8298
+ (s) => s.kind === "function" && s.name === name && s.startLine <= current.startPosition.row + 1 && s.endLine >= current.endPosition.row + 1
8299
+ );
8300
+ if (found) return found;
8301
+ }
8302
+ }
8303
+ if (current.type === "class_definition") {
8304
+ const name = getNodeIdentifier2(current);
8305
+ if (name) {
8306
+ return fileSymbols.find(
8307
+ (s) => s.name === name && s.kind === "class"
8308
+ );
8309
+ }
8310
+ }
8311
+ current = current.parent;
8312
+ }
8313
+ return void 0;
8314
+ }
8315
+ function collectTypeIdentifiers2(node) {
8316
+ const names = [];
8317
+ collectTypeIdentifiersRecursive2(node, names);
8318
+ return [...new Set(names)];
8319
+ }
8320
+ function collectTypeIdentifiersRecursive2(node, names) {
8321
+ if (node.type === "attribute") {
8322
+ const attrNode = node.childForFieldName("attribute");
8323
+ if (attrNode && attrNode.type === "identifier") {
8324
+ names.push(attrNode.text);
8325
+ }
8326
+ return;
8327
+ }
8328
+ if (node.type === "identifier" && isTypeContext2(node)) {
8329
+ names.push(node.text);
8330
+ }
8331
+ for (const child of node.namedChildren) {
8332
+ collectTypeIdentifiersRecursive2(child, names);
8333
+ }
8334
+ }
8335
+ function isTypeContext2(node) {
8336
+ const parent = node.parent;
8337
+ if (!parent) return false;
8338
+ return parent.type === "type" || parent.type === "subscript" || // In typed_parameter, the type annotation is the second named child
8339
+ parent.type === "typed_parameter" && node !== parent.childForFieldName("name") || parent.type === "typed_default_parameter" && node !== parent.childForFieldName("name");
8340
+ }
8341
+
8342
+ // src/indexer/python/route-analyzer.ts
8343
+ var FASTAPI_HTTP_METHODS = /* @__PURE__ */ new Set([
8344
+ "get",
8345
+ "post",
8346
+ "put",
8347
+ "delete",
8348
+ "patch",
8349
+ "head",
8350
+ "options"
8351
+ ]);
8352
+ function extractPythonRoutes(tree, _relativePath) {
8353
+ const routes = [];
8354
+ const decoratedDefs = findAllNodes2(tree.rootNode, "decorated_definition");
8355
+ for (const decorated of decoratedDefs) {
8356
+ const decorators = decorated.namedChildren.filter(
8357
+ (c) => c.type === "decorator"
8358
+ );
8359
+ for (const decorator of decorators) {
8360
+ const routeInfo = parseDecorator(decorator);
8361
+ if (!routeInfo) continue;
8362
+ for (const method of routeInfo.methods) {
8363
+ const routePath = routeInfo.path.startsWith("/") ? routeInfo.path : `/${routeInfo.path}`;
8364
+ const id = `route::${routePath.slice(1)}::${method}`;
8365
+ routes.push({
8366
+ id,
8367
+ routePath,
8368
+ kind: "api-route",
8369
+ httpMethods: [method],
8370
+ hasAuth: routeInfo.hasAuth
8371
+ });
8372
+ }
8373
+ }
8374
+ }
8375
+ return routes;
8376
+ }
8377
+ function parseDecorator(decorator) {
8378
+ const callNode = findFirstChild(decorator, "call");
8379
+ if (!callNode) return null;
8380
+ const funcNode = callNode.childForFieldName("function");
8381
+ if (!funcNode || funcNode.type !== "attribute") return null;
8382
+ const attrName = funcNode.childForFieldName("attribute");
8383
+ if (!attrName) return null;
8384
+ const methodName = attrName.text;
8385
+ if (FASTAPI_HTTP_METHODS.has(methodName)) {
8386
+ const path = getFirstStringArgument2(callNode);
8387
+ if (path === null) return null;
8388
+ const hasAuth = detectAuthInCall(callNode);
8389
+ return {
8390
+ path,
8391
+ methods: [methodName.toUpperCase()],
8392
+ hasAuth
8393
+ };
8394
+ }
8395
+ if (methodName === "route") {
8396
+ const path = getFirstStringArgument2(callNode);
8397
+ if (path === null) return null;
8398
+ const methods = getMethodsKeywordArgument(callNode).map((m) => m.toUpperCase());
8399
+ const hasAuth = detectAuthInCall(callNode);
8400
+ return {
8401
+ path,
8402
+ methods: methods.length > 0 ? methods : ["GET"],
8403
+ hasAuth
8404
+ };
8405
+ }
8406
+ return null;
8407
+ }
8408
+ function detectAuthInCall(callNode) {
8409
+ const args = callNode.childForFieldName("arguments");
8410
+ if (!args) return false;
8411
+ const text = args.text;
8412
+ if (/Depends\s*\(\s*auth/.test(text)) return true;
8413
+ if (/login_required|auth_required|require_auth|jwt_required/.test(text))
8414
+ return true;
8415
+ return false;
8416
+ }
8417
+ function getFirstStringArgument2(callNode) {
8418
+ const args = callNode.childForFieldName("arguments");
8419
+ if (!args) return null;
8420
+ for (const child of args.namedChildren) {
8421
+ if (child.type === "keyword_argument") continue;
8422
+ if (child.type === "string") {
8423
+ return stripQuotes(child.text);
8424
+ }
8425
+ const strNode = findFirstChild(child, "string");
8426
+ if (strNode) {
8427
+ return stripQuotes(strNode.text);
8428
+ }
8429
+ }
8430
+ return null;
8431
+ }
8432
+ function getMethodsKeywordArgument(callNode) {
8433
+ const args = callNode.childForFieldName("arguments");
8434
+ if (!args) return [];
8435
+ for (const child of args.namedChildren) {
8436
+ if (child.type !== "keyword_argument") continue;
8437
+ const nameNode = child.childForFieldName("name");
8438
+ if (!nameNode || nameNode.text !== "methods") continue;
8439
+ const valueNode = child.childForFieldName("value");
8440
+ if (!valueNode) continue;
8441
+ return extractStringListValues(valueNode);
8442
+ }
8443
+ return [];
8444
+ }
8445
+ function extractStringListValues(node) {
8446
+ const values = [];
8447
+ if (node.type === "list") {
8448
+ for (const child of node.namedChildren) {
8449
+ if (child.type === "string") {
8450
+ values.push(stripQuotes(child.text));
8451
+ }
8452
+ }
8453
+ }
8454
+ return values;
8455
+ }
8456
+ function findAllNodes2(root, type) {
8457
+ const results = [];
8458
+ function walk(node) {
8459
+ if (node.type === type) results.push(node);
8460
+ for (const child of node.namedChildren) {
8461
+ walk(child);
8462
+ }
8463
+ }
8464
+ walk(root);
8465
+ return results;
8466
+ }
8467
+ function findFirstChild(node, type) {
8468
+ for (const child of node.namedChildren) {
8469
+ if (child.type === type) return child;
8470
+ }
8471
+ for (const child of node.children) {
8472
+ if (child.type === type) return child;
8473
+ }
8474
+ return null;
8475
+ }
8476
+ function stripQuotes(text) {
8477
+ return text.replace(/^[fFrRbBuU]*["']|["']$/g, "");
8478
+ }
8479
+
8480
+ // src/indexer/python/indexer.ts
8481
+ async function indexPythonTreeSitter(db, options) {
8482
+ const start = Date.now();
8483
+ await ensurePythonParser();
8484
+ const service = options.service ?? "main";
8485
+ const projectRoot = options.projectRoot;
8486
+ const ignorePatterns = [
8487
+ "**/__pycache__/**",
8488
+ "**/.venv/**",
8489
+ "**/venv/**",
8490
+ "**/.tox/**",
8491
+ "**/node_modules/**",
8492
+ "**/.git/**",
8493
+ "**/dist/**",
8494
+ "**/build/**",
8495
+ "**/.eggs/**",
8496
+ "**/*.egg-info/**"
8497
+ ];
8498
+ const pyFiles = globbySync2("**/*.py", {
8499
+ cwd: projectRoot,
8500
+ ignore: ignorePatterns,
8501
+ absolute: false
8502
+ });
8503
+ const existingHashes = getExistingHashes(db, service, "python");
8504
+ const currentPaths = /* @__PURE__ */ new Set();
8505
+ const fileCache = /* @__PURE__ */ new Map();
8506
+ const changedFiles = [];
8507
+ let filesSkipped = 0;
8508
+ for (const filePath of pyFiles) {
8509
+ const relPath = filePath.replace(/\\/g, "/");
8510
+ currentPaths.add(relPath);
8511
+ const fullPath = join12(projectRoot, relPath);
8512
+ const content = readFileSync5(fullPath, "utf-8");
8513
+ const hash = hashContent(content);
8514
+ const tree = parsePython(content);
8515
+ fileCache.set(relPath, { content, tree, hash });
8516
+ const existingHash = existingHashes.get(relPath);
8517
+ if (existingHash === hash) {
8518
+ filesSkipped++;
8519
+ } else {
8520
+ changedFiles.push(relPath);
8521
+ }
8522
+ }
8523
+ const removed = [];
8524
+ for (const existingPath of existingHashes.keys()) {
8525
+ if (!currentPaths.has(existingPath)) {
8526
+ removed.push(existingPath);
8527
+ }
8528
+ }
8529
+ const filesToClean = [...removed, ...changedFiles];
8530
+ removeScopedSymbolsForFiles(db, filesToClean, service, "python");
8531
+ const allNewSymbols = [];
8532
+ for (const relPath of changedFiles) {
8533
+ const cached = fileCache.get(relPath);
8534
+ if (!cached) continue;
8535
+ const symbols = extractPythonSymbols(cached.tree, relPath, cached.hash);
8536
+ allNewSymbols.push(...symbols);
8537
+ }
8538
+ writeSymbols(db, allNewSymbols, service, "python");
8539
+ const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name, kind, start_line as startLine, end_line as endLine FROM symbols WHERE service = ? AND language = 'python'").all(service);
8540
+ const symbolLookup = buildPythonSymbolLookup(allDbSymbols);
8541
+ const allDeps = [];
8542
+ for (const [relPath, cached] of fileCache) {
8543
+ const fileDeps = extractPythonDependencies(cached.tree, relPath, allDbSymbols, symbolLookup);
8544
+ allDeps.push(...fileDeps);
8545
+ }
8546
+ db.prepare(
8547
+ "DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE service = ? AND language = 'python')"
8548
+ ).run(service);
8549
+ writeDependencies(db, allDeps);
8550
+ const allRoutes = [];
8551
+ for (const [relPath, cached] of fileCache) {
8552
+ const routes = extractPythonRoutes(cached.tree, relPath);
8553
+ allRoutes.push(...routes);
8554
+ }
8555
+ db.prepare("DELETE FROM routes WHERE service = ?").run(service);
8556
+ if (allRoutes.length > 0) {
8557
+ const insertRoute = db.prepare(`
8558
+ INSERT OR REPLACE INTO routes (id, route_path, kind, http_methods, has_auth, service)
8559
+ VALUES (?, ?, ?, ?, ?, ?)
8560
+ `);
8561
+ transaction(db, () => {
8562
+ for (const route of allRoutes) {
8563
+ insertRoute.run(
8564
+ `${service}::${route.id}`,
8565
+ route.routePath,
8566
+ route.kind,
8567
+ JSON.stringify(route.httpMethods),
8568
+ route.hasAuth ? 1 : 0,
8569
+ service
8570
+ );
8571
+ }
8572
+ });
8573
+ }
8574
+ return {
8575
+ symbolsIndexed: allNewSymbols.length,
8576
+ dependenciesIndexed: allDeps.length,
8577
+ componentsAnalyzed: 0,
8578
+ routesAnalyzed: allRoutes.length,
8579
+ filesProcessed: changedFiles.length,
8580
+ filesSkipped,
8581
+ filesRemoved: removed.length,
8582
+ durationMs: Date.now() - start
8583
+ };
8584
+ }
8585
+
8586
+ // src/indexer/go/indexer.ts
8587
+ import { readFileSync as readFileSync6 } from "fs";
8588
+ import { join as join13 } from "path";
8589
+ import { globbySync as globbySync3 } from "globby";
8590
+
8591
+ // src/indexer/go/parser.ts
8592
+ import { accessSync as accessSync4, constants as constants4 } from "fs";
8593
+ import { dirname as dirname5, resolve as resolve4 } from "path";
8594
+ import { fileURLToPath as fileURLToPath4 } from "url";
8595
+ import "web-tree-sitter";
8596
+ var currentDir3 = dirname5(fileURLToPath4(import.meta.url));
8597
+ var cachedParser3 = null;
8598
+ var initPromise3 = null;
8599
+ function resolveGrammarPath3() {
8600
+ const candidates = [
8601
+ resolve4(currentDir3, "../../../wasm/tree-sitter-go.wasm"),
8602
+ // from src/indexer/go/
8603
+ resolve4(currentDir3, "../wasm/tree-sitter-go.wasm")
8604
+ // from dist/ (tsup flat bundle)
8605
+ ];
8606
+ for (const candidate of candidates) {
8607
+ try {
8608
+ accessSync4(candidate, constants4.R_OK);
8609
+ return candidate;
8610
+ } catch {
8611
+ continue;
8612
+ }
8613
+ }
8614
+ throw new Error(
8615
+ "Could not find tree-sitter-go.wasm. Ensure the wasm/ directory is present in the @arcbridge/core package."
8616
+ );
8617
+ }
8618
+ async function ensureGoParser() {
8619
+ if (cachedParser3) return cachedParser3;
8620
+ if (initPromise3) return initPromise3;
8621
+ initPromise3 = (async () => {
8622
+ try {
8623
+ const TreeSitter = await import("web-tree-sitter");
8624
+ await TreeSitter.Parser.init();
8625
+ const grammarPath = resolveGrammarPath3();
8626
+ const Go = await TreeSitter.Language.load(grammarPath);
8627
+ const parser = new TreeSitter.Parser();
8628
+ parser.setLanguage(Go);
8629
+ cachedParser3 = parser;
8630
+ return parser;
8631
+ } catch (err) {
8632
+ initPromise3 = null;
8633
+ throw err;
8634
+ }
8635
+ })();
8636
+ return initPromise3;
8637
+ }
8638
+ function parseGo(content) {
8639
+ if (!cachedParser3) {
8640
+ throw new Error(
8641
+ "Go parser not initialized. Call await ensureGoParser() first."
8642
+ );
8643
+ }
8644
+ const tree = cachedParser3.parse(content);
8645
+ if (!tree) {
8646
+ throw new Error("Failed to parse Go content");
8647
+ }
8648
+ return tree;
8649
+ }
8650
+
8651
+ // src/indexer/go/symbol-extractor.ts
8652
+ function extractGoSymbols(tree, relativePath, contentHash) {
8653
+ const symbols = [];
8654
+ const root = tree.rootNode;
8655
+ for (const child of root.namedChildren) {
8656
+ walkNode3(child, relativePath, contentHash, symbols);
8657
+ }
8658
+ return symbols;
8659
+ }
8660
+ function walkNode3(node, relativePath, contentHash, symbols) {
8661
+ switch (node.type) {
8662
+ case "function_declaration": {
8663
+ const name = getFieldName(node);
8664
+ if (!name) break;
8665
+ const signature = extractFunctionSignature2(node, name);
8666
+ const returnType = extractReturnType3(node);
8667
+ symbols.push(makeSymbol3({
8668
+ name,
8669
+ qualifiedName: name,
8670
+ kind: "function",
8671
+ node,
8672
+ relativePath,
8673
+ contentHash,
8674
+ docComment: extractDocComment2(node),
8675
+ signature,
8676
+ returnType
8677
+ }));
8678
+ return;
8679
+ }
8680
+ case "method_declaration": {
8681
+ const name = getFieldName(node);
8682
+ if (!name) break;
8683
+ const receiverType = extractReceiverType(node);
8684
+ const qualifiedName = receiverType ? `${receiverType}.${name}` : name;
8685
+ const signature = extractFunctionSignature2(node, name);
8686
+ const returnType = extractReturnType3(node);
8687
+ symbols.push(makeSymbol3({
8688
+ name,
8689
+ qualifiedName,
8690
+ kind: "function",
8691
+ node,
8692
+ relativePath,
8693
+ contentHash,
8694
+ docComment: extractDocComment2(node),
8695
+ signature,
8696
+ returnType
8697
+ }));
8698
+ return;
8699
+ }
8700
+ case "type_declaration": {
8701
+ for (const child of node.namedChildren) {
8702
+ if (child.type === "type_spec") {
8703
+ extractTypeSpec(child, node, relativePath, contentHash, symbols);
8704
+ }
8705
+ }
8706
+ return;
8707
+ }
8708
+ case "const_declaration": {
8709
+ for (const child of node.namedChildren) {
8710
+ if (child.type === "const_spec") {
8711
+ const names = getSpecNames(child);
8712
+ const doc = extractDocComment2(child) ?? extractDocComment2(node);
8713
+ for (const name of names) {
8714
+ symbols.push(makeSymbol3({
8715
+ name,
8716
+ qualifiedName: name,
8717
+ kind: "constant",
8718
+ node: child,
8719
+ relativePath,
8720
+ contentHash,
8721
+ docComment: doc
8722
+ }));
8723
+ }
8724
+ }
8725
+ }
8726
+ return;
8727
+ }
8728
+ case "var_declaration": {
8729
+ for (const child of node.namedChildren) {
8730
+ if (child.type === "var_spec") {
8731
+ const names = getSpecNames(child);
8732
+ const doc = extractDocComment2(child) ?? extractDocComment2(node);
8733
+ for (const name of names) {
8734
+ symbols.push(makeSymbol3({
8735
+ name,
8736
+ qualifiedName: name,
8737
+ kind: "variable",
8738
+ node: child,
8739
+ relativePath,
8740
+ contentHash,
8741
+ docComment: doc
8742
+ }));
8743
+ }
8744
+ }
8745
+ }
8746
+ return;
8747
+ }
8748
+ }
8749
+ for (const child of node.namedChildren) {
8750
+ walkNode3(child, relativePath, contentHash, symbols);
8751
+ }
8752
+ }
8753
+ function extractTypeSpec(spec, parentDecl, relativePath, contentHash, symbols) {
8754
+ const name = getFieldName(spec);
8755
+ if (!name) return;
8756
+ const typeNode = spec.childForFieldName("type");
8757
+ let kind = "type";
8758
+ if (typeNode) {
8759
+ if (typeNode.type === "struct_type") {
8760
+ kind = "class";
8761
+ } else if (typeNode.type === "interface_type") {
8762
+ kind = "interface";
8763
+ }
8764
+ }
8765
+ symbols.push(makeSymbol3({
8766
+ name,
8767
+ qualifiedName: name,
8768
+ kind,
8769
+ node: spec,
8770
+ relativePath,
8771
+ contentHash,
8772
+ docComment: extractDocComment2(spec) ?? extractDocComment2(parentDecl)
8773
+ }));
8774
+ }
8775
+ function makeSymbol3(args) {
8776
+ const { name, qualifiedName, kind, node, relativePath, contentHash, docComment } = args;
8777
+ return {
8778
+ id: `${relativePath}::${qualifiedName}#${kind}`,
8779
+ name,
8780
+ qualifiedName,
8781
+ kind,
8782
+ filePath: relativePath,
8783
+ startLine: node.startPosition.row + 1,
8784
+ endLine: node.endPosition.row + 1,
8785
+ startCol: node.startPosition.column + 1,
8786
+ endCol: node.endPosition.column + 1,
8787
+ signature: args.signature ?? null,
8788
+ returnType: args.returnType ?? null,
8789
+ docComment,
8790
+ isExported: /^[A-Z]/.test(name),
8791
+ isAsync: false,
8792
+ contentHash
8793
+ };
8794
+ }
8795
+ function getFieldName(node) {
8796
+ const nameNode = node.childForFieldName("name");
8797
+ if (nameNode) return nameNode.text;
8798
+ for (const child of node.namedChildren) {
8799
+ if (child.type === "identifier") return child.text;
8800
+ }
8801
+ return null;
8802
+ }
8803
+ function getSpecNames(node) {
8804
+ const names = [];
8805
+ for (const child of node.namedChildren) {
8806
+ if (child.type === "identifier") {
8807
+ names.push(child.text);
8808
+ } else {
8809
+ break;
8810
+ }
8811
+ }
8812
+ return names;
8813
+ }
8814
+ function extractReceiverType(node) {
8815
+ const receiver = node.childForFieldName("receiver");
8816
+ if (!receiver) return null;
8817
+ for (const child of receiver.namedChildren) {
8818
+ if (child.type === "parameter_declaration") {
8819
+ const typeNode = child.childForFieldName("type");
8820
+ if (typeNode) {
8821
+ if (typeNode.type === "pointer_type") {
8822
+ const inner = typeNode.namedChildren[0];
8823
+ return inner ? inner.text : typeNode.text.replace(/^\*/, "");
8824
+ }
8825
+ return typeNode.text;
8826
+ }
8827
+ }
8828
+ }
8829
+ return null;
8830
+ }
8831
+ function extractDocComment2(node) {
8832
+ const comments = [];
8833
+ let sibling = node.previousSibling;
8834
+ while (sibling) {
8835
+ if (sibling.type === "comment" && sibling.text.startsWith("//")) {
8836
+ comments.unshift(sibling.text);
8837
+ } else {
8838
+ break;
8839
+ }
8840
+ sibling = sibling.previousSibling;
8841
+ }
8842
+ if (comments.length === 0) return null;
8843
+ return comments.map((c) => c.replace(/^\s*\/\/\s?/, "").trim()).filter(Boolean).join(" ");
8844
+ }
8845
+ function extractFunctionSignature2(node, name) {
8846
+ const params = node.childForFieldName("parameters");
8847
+ if (!params) return null;
8848
+ return `${name}${params.text}`;
8849
+ }
8850
+ function extractReturnType3(node) {
8851
+ const result = node.childForFieldName("result");
8852
+ if (!result) return null;
8853
+ return result.text;
8854
+ }
8855
+
8856
+ // src/indexer/go/dependency-extractor.ts
8857
+ function buildGoSymbolLookup(symbols) {
8858
+ const lookup = /* @__PURE__ */ new Map();
8859
+ for (const sym of symbols) {
8860
+ const existing = lookup.get(sym.name);
8861
+ if (existing) {
8862
+ existing.push(sym.id);
8863
+ } else {
8864
+ lookup.set(sym.name, [sym.id]);
8865
+ }
8866
+ }
8867
+ return lookup;
8868
+ }
8869
+ function extractGoDependencies(tree, relativePath, allSymbols, symbolLookup) {
8870
+ const deps = [];
8871
+ const seen = /* @__PURE__ */ new Set();
8872
+ const fileSymbols = allSymbols.filter((s) => s.filePath === relativePath);
8873
+ walkForDependencies3(tree.rootNode, fileSymbols, symbolLookup, deps, seen);
8874
+ return deps;
8875
+ }
8876
+ function walkForDependencies3(node, fileSymbols, lookup, deps, seen) {
8877
+ switch (node.type) {
8878
+ case "type_declaration":
8879
+ for (const child of node.namedChildren) {
8880
+ if (child.type === "type_spec") {
8881
+ extractEmbeddingDeps(child, fileSymbols, lookup, deps, seen);
8882
+ }
8883
+ }
8884
+ break;
8885
+ case "call_expression":
8886
+ extractCallDeps3(node, fileSymbols, lookup, deps, seen);
8887
+ break;
8888
+ }
8889
+ if (node.type === "parameter_declaration" || node.type === "field_declaration" || node.type === "var_spec") {
8890
+ extractTypeRefDeps2(node, fileSymbols, lookup, deps, seen);
8891
+ }
8892
+ for (const child of node.namedChildren) {
8893
+ walkForDependencies3(child, fileSymbols, lookup, deps, seen);
8894
+ }
8895
+ }
8896
+ function extractEmbeddingDeps(typeSpec, fileSymbols, lookup, deps, seen) {
8897
+ const typeName = getNodeIdentifier3(typeSpec);
8898
+ if (!typeName) return;
8899
+ const sourceSymbol = findEnclosingSymbol3(typeName, fileSymbols, "class", "interface");
8900
+ if (!sourceSymbol) return;
8901
+ const typeNode = typeSpec.childForFieldName("type");
8902
+ if (!typeNode) return;
8903
+ if (typeNode.type === "struct_type" || typeNode.type === "interface_type") {
8904
+ const fieldList = findChild6(typeNode, "field_declaration_list") ?? typeNode;
8905
+ for (const field of fieldList.namedChildren) {
8906
+ if (field.type === "field_declaration") {
8907
+ const nameNode = field.childForFieldName("name");
8908
+ if (nameNode) continue;
8909
+ const embeddedTypeName = extractSimpleTypeName3(field);
8910
+ if (!embeddedTypeName || embeddedTypeName === typeName) continue;
8911
+ const targetIds = lookup.get(embeddedTypeName);
8912
+ if (!targetIds) continue;
8913
+ for (const targetId of targetIds) {
8914
+ if (targetId === sourceSymbol.id) continue;
8915
+ addDep3(deps, seen, sourceSymbol.id, targetId, "extends");
8916
+ }
8917
+ }
8918
+ if (field.type === "type_name" || field.type === "qualified_type" || field.type === "type_identifier") {
8919
+ const embeddedName = extractSimpleTypeNameFromNode(field);
8920
+ if (!embeddedName || embeddedName === typeName) continue;
8921
+ const targetIds = lookup.get(embeddedName);
8922
+ if (!targetIds) continue;
8923
+ for (const targetId of targetIds) {
8924
+ if (targetId === sourceSymbol.id) continue;
8925
+ addDep3(deps, seen, sourceSymbol.id, targetId, "extends");
8926
+ }
8927
+ }
8928
+ }
8929
+ }
8930
+ }
8931
+ function extractCallDeps3(node, fileSymbols, lookup, deps, seen) {
8932
+ const calledName = extractCalledFunctionName2(node);
8933
+ if (!calledName) return;
8934
+ const enclosing = findEnclosingFunctionForNode2(node, fileSymbols);
8935
+ if (!enclosing) return;
8936
+ const targetIds = lookup.get(calledName);
8937
+ if (!targetIds) return;
8938
+ for (const targetId of targetIds) {
8939
+ if (targetId === enclosing.id) continue;
8940
+ addDep3(deps, seen, enclosing.id, targetId, "calls");
8941
+ }
8942
+ }
8943
+ function extractTypeRefDeps2(node, fileSymbols, lookup, deps, seen) {
8944
+ const typeNames = collectTypeIdentifiers3(node);
8945
+ if (typeNames.length === 0) return;
8946
+ const enclosing = findEnclosingSymbolForNode3(node, fileSymbols);
8947
+ if (!enclosing) return;
8948
+ for (const typeName of typeNames) {
8949
+ const targetIds = lookup.get(typeName);
8950
+ if (!targetIds) continue;
8951
+ for (const targetId of targetIds) {
8952
+ if (targetId === enclosing.id) continue;
8953
+ addDep3(deps, seen, enclosing.id, targetId, "uses_type");
8954
+ }
8955
+ }
8956
+ }
8957
+ function addDep3(deps, seen, source, target, kind) {
8958
+ const key = `${source}|${target}|${kind}`;
8959
+ if (seen.has(key)) return;
8960
+ seen.add(key);
8961
+ deps.push({ sourceSymbolId: source, targetSymbolId: target, kind });
8962
+ }
8963
+ function findChild6(node, type) {
8964
+ for (const child of node.namedChildren) {
8965
+ if (child.type === type) return child;
8966
+ }
8967
+ return null;
8968
+ }
8969
+ function getNodeIdentifier3(node) {
8970
+ const nameNode = node.childForFieldName("name");
8971
+ if (nameNode) return nameNode.text;
8972
+ for (const child of node.namedChildren) {
8973
+ if (child.type === "identifier" || child.type === "type_identifier") return child.text;
8974
+ }
8975
+ return null;
8976
+ }
8977
+ function extractSimpleTypeName3(node) {
8978
+ for (const child of node.namedChildren) {
8979
+ if (child.type === "type_identifier" || child.type === "identifier") return child.text;
8980
+ if (child.type === "pointer_type") {
8981
+ const inner = child.namedChildren[0];
8982
+ return inner ? extractSimpleTypeNameFromNode(inner) : null;
8983
+ }
8984
+ if (child.type === "qualified_type") {
8985
+ const parts = child.text.split(".");
8986
+ return parts[parts.length - 1];
8987
+ }
8988
+ }
8989
+ return null;
8990
+ }
8991
+ function extractSimpleTypeNameFromNode(node) {
8992
+ if (node.type === "type_identifier" || node.type === "identifier") return node.text;
8993
+ if (node.type === "qualified_type") {
8994
+ const parts = node.text.split(".");
8995
+ return parts[parts.length - 1];
8996
+ }
8997
+ return null;
8998
+ }
8999
+ function extractCalledFunctionName2(node) {
9000
+ const funcNode = node.childForFieldName("function");
9001
+ if (!funcNode) return node.namedChildren[0]?.text ?? null;
9002
+ if (funcNode.type === "selector_expression") {
9003
+ const fieldNode = funcNode.childForFieldName("field");
9004
+ return fieldNode?.text ?? null;
9005
+ }
9006
+ if (funcNode.type === "identifier") {
9007
+ return funcNode.text;
9008
+ }
9009
+ return null;
9010
+ }
9011
+ function findEnclosingSymbol3(name, fileSymbols, ...kinds) {
9012
+ return fileSymbols.find(
9013
+ (s) => s.name === name && kinds.includes(s.kind)
9014
+ );
9015
+ }
9016
+ function findEnclosingFunctionForNode2(node, fileSymbols) {
9017
+ let current = node.parent;
9018
+ while (current) {
9019
+ if (current.type === "function_declaration" || current.type === "method_declaration") {
9020
+ const name = getNodeIdentifier3(current);
9021
+ if (name) {
9022
+ return fileSymbols.find(
9023
+ (s) => s.kind === "function" && s.name === name && s.startLine <= current.startPosition.row + 1 && s.endLine >= current.endPosition.row + 1
9024
+ );
9025
+ }
9026
+ }
9027
+ current = current.parent;
9028
+ }
9029
+ return void 0;
9030
+ }
9031
+ function findEnclosingSymbolForNode3(node, fileSymbols) {
9032
+ let current = node.parent;
9033
+ while (current) {
9034
+ if (current.type === "function_declaration" || current.type === "method_declaration") {
9035
+ const name = getNodeIdentifier3(current);
9036
+ if (name) {
9037
+ const found = fileSymbols.find(
9038
+ (s) => s.kind === "function" && s.name === name && s.startLine <= current.startPosition.row + 1 && s.endLine >= current.endPosition.row + 1
9039
+ );
9040
+ if (found) return found;
9041
+ }
9042
+ }
9043
+ if (current.type === "type_spec") {
9044
+ const name = getNodeIdentifier3(current);
9045
+ if (name) {
9046
+ return fileSymbols.find(
9047
+ (s) => s.name === name && (s.kind === "class" || s.kind === "interface")
9048
+ );
9049
+ }
9050
+ }
9051
+ current = current.parent;
9052
+ }
9053
+ return void 0;
9054
+ }
9055
+ function collectTypeIdentifiers3(node) {
9056
+ const names = [];
9057
+ collectTypeIdentifiersRecursive3(node, names);
9058
+ return [...new Set(names)];
9059
+ }
9060
+ function collectTypeIdentifiersRecursive3(node, names) {
9061
+ if (node.type === "type_identifier") {
9062
+ names.push(node.text);
9063
+ } else if (node.type === "qualified_type") {
9064
+ const parts = node.text.split(".");
9065
+ names.push(parts[parts.length - 1]);
9066
+ } else if (node.type === "pointer_type") {
9067
+ for (const child of node.namedChildren) {
9068
+ collectTypeIdentifiersRecursive3(child, names);
9069
+ }
9070
+ return;
9071
+ }
9072
+ for (const child of node.namedChildren) {
9073
+ collectTypeIdentifiersRecursive3(child, names);
9074
+ }
9075
+ }
9076
+
9077
+ // src/indexer/go/route-analyzer.ts
9078
+ var GIN_METHODS = {
9079
+ GET: "GET",
9080
+ POST: "POST",
9081
+ PUT: "PUT",
9082
+ DELETE: "DELETE",
9083
+ PATCH: "PATCH",
9084
+ HEAD: "HEAD",
9085
+ OPTIONS: "OPTIONS"
9086
+ };
9087
+ var CHI_METHODS = {
9088
+ Get: "GET",
9089
+ Post: "POST",
9090
+ Put: "PUT",
9091
+ Delete: "DELETE",
9092
+ Patch: "PATCH",
9093
+ Head: "HEAD",
9094
+ Options: "OPTIONS"
9095
+ };
9096
+ var NET_HTTP_METHODS = /* @__PURE__ */ new Set(["HandleFunc", "Handle"]);
9097
+ function extractGoRoutes(tree, _relativePath) {
9098
+ const routes = [];
9099
+ const scopedPrefixes = extractGroupPrefixes2(tree.rootNode);
9100
+ const authScopes = findAuthUseScopes(tree.rootNode);
9101
+ const calls = findAllNodes3(tree.rootNode, "call_expression");
9102
+ for (const call of calls) {
9103
+ const funcNode = call.childForFieldName("function");
9104
+ if (!funcNode || funcNode.type !== "selector_expression") continue;
9105
+ const fieldNode = funcNode.childForFieldName("field");
9106
+ if (!fieldNode) continue;
9107
+ const methodName = fieldNode.text;
9108
+ const receiverNode = funcNode.childForFieldName("operand");
9109
+ const receiverName = receiverNode?.type === "identifier" ? receiverNode.text : null;
9110
+ let httpMethods = null;
9111
+ let isNetHttp = false;
9112
+ if (GIN_METHODS[methodName]) {
9113
+ httpMethods = [GIN_METHODS[methodName]];
9114
+ } else if (CHI_METHODS[methodName]) {
9115
+ httpMethods = [CHI_METHODS[methodName]];
9116
+ } else if (NET_HTTP_METHODS.has(methodName)) {
9117
+ httpMethods = [];
9118
+ isNetHttp = true;
9119
+ }
9120
+ if (httpMethods === null) continue;
9121
+ const routeTemplate = getFirstStringArgument3(call);
9122
+ if (routeTemplate === null) continue;
9123
+ const prefix = receiverName ? resolvePrefix(receiverName, call, scopedPrefixes) : "";
9124
+ let routePath;
9125
+ if (prefix && routeTemplate) {
9126
+ routePath = `${prefix}/${routeTemplate}`.replace(/\/{2,}/g, "/");
9127
+ } else if (prefix) {
9128
+ routePath = prefix;
9129
+ } else {
9130
+ routePath = routeTemplate;
9131
+ }
9132
+ routePath = routePath.startsWith("/") ? routePath : `/${routePath}`;
9133
+ routePath = routePath.replace(/\/{2,}/g, "/").replace(/\/+$/, "") || "/";
9134
+ let hasAuth = false;
9135
+ if (!isNetHttp) {
9136
+ hasAuth = hasAuthArgument(call);
9137
+ if (!hasAuth && authScopes.length > 0) {
9138
+ hasAuth = isInsideAuthScope(call, authScopes);
9139
+ }
9140
+ }
9141
+ const cleanPath = routePath.slice(1) || "";
9142
+ const idMethod = httpMethods.length > 0 ? httpMethods.join(",") : "ANY";
9143
+ const id = `route::${cleanPath}::${idMethod}`;
9144
+ routes.push({
9145
+ id,
9146
+ routePath,
9147
+ kind: "api-route",
9148
+ httpMethods,
9149
+ hasAuth
9150
+ });
9151
+ }
9152
+ return routes;
9153
+ }
9154
+ function extractGroupPrefixes2(root) {
9155
+ const prefixes = [];
9156
+ const calls = findAllNodes3(root, "call_expression");
9157
+ for (const call of calls) {
9158
+ const funcNode = call.childForFieldName("function");
9159
+ if (funcNode?.type !== "selector_expression") continue;
9160
+ const fieldNode = funcNode.childForFieldName("field");
9161
+ const methodName = fieldNode?.text;
9162
+ if (methodName !== "Group" && methodName !== "Route") continue;
9163
+ const prefix = getFirstStringArgument3(call);
9164
+ if (prefix === null) continue;
9165
+ const assignNode = findAncestor(call, ["short_var_declaration", "assignment_statement"]);
9166
+ if (assignNode) {
9167
+ const left = assignNode.childForFieldName("left");
9168
+ if (left) {
9169
+ const varNode = left.type === "expression_list" ? left.namedChildren[0] : left;
9170
+ if (varNode?.type === "identifier") {
9171
+ const scope = findAncestor(call, [
9172
+ "func_literal",
9173
+ "function_declaration",
9174
+ "method_declaration"
9175
+ ]);
9176
+ prefixes.push({ varName: varNode.text, prefix, scope });
9177
+ }
9178
+ }
9179
+ }
9180
+ if (methodName === "Route") {
9181
+ const argList = findChild7(call, "argument_list");
9182
+ if (argList) {
9183
+ for (const arg of argList.namedChildren) {
9184
+ if (arg.type === "func_literal") {
9185
+ const paramList = arg.childForFieldName("parameters");
9186
+ if (paramList) {
9187
+ const firstParam = paramList.namedChildren[0];
9188
+ if (firstParam?.type === "parameter_declaration") {
9189
+ const paramName = firstParam.childForFieldName("name");
9190
+ if (paramName?.type === "identifier") {
9191
+ prefixes.push({ varName: paramName.text, prefix, scope: arg });
9192
+ }
9193
+ }
9194
+ }
9195
+ }
9196
+ }
9197
+ }
9198
+ }
9199
+ }
9200
+ return prefixes;
9201
+ }
9202
+ function resolvePrefix(receiverName, callNode, scopedPrefixes) {
9203
+ let best = null;
9204
+ for (const sp of scopedPrefixes) {
9205
+ if (sp.varName !== receiverName) continue;
9206
+ if (sp.scope === null) {
9207
+ if (!best) best = sp;
9208
+ } else {
9209
+ if (isDescendantOf(callNode, sp.scope)) {
9210
+ if (!best || best.scope === null || isDescendantOf(sp.scope, best.scope)) {
9211
+ best = sp;
9212
+ }
9213
+ }
9214
+ }
9215
+ }
9216
+ return best?.prefix ?? "";
9217
+ }
9218
+ function isSameNode(a, b) {
9219
+ return a.type === b.type && a.startPosition.row === b.startPosition.row && a.startPosition.column === b.startPosition.column && a.endPosition.row === b.endPosition.row && a.endPosition.column === b.endPosition.column;
9220
+ }
9221
+ function isDescendantOf(node, ancestor) {
9222
+ let current = node.parent;
9223
+ while (current) {
9224
+ if (isSameNode(current, ancestor)) return true;
9225
+ current = current.parent;
9226
+ }
9227
+ return false;
9228
+ }
9229
+ function findAncestor(node, types) {
9230
+ let current = node.parent;
9231
+ while (current) {
9232
+ if (types.includes(current.type)) return current;
9233
+ current = current.parent;
9234
+ }
9235
+ return null;
9236
+ }
9237
+ function findAuthUseScopes(root) {
9238
+ const scopes = [];
9239
+ const calls = findAllNodes3(root, "call_expression");
9240
+ for (const call of calls) {
9241
+ const funcNode = call.childForFieldName("function");
9242
+ if (funcNode?.type !== "selector_expression") continue;
9243
+ const fieldNode = funcNode.childForFieldName("field");
9244
+ if (fieldNode?.text !== "Use") continue;
9245
+ if (hasAuthArgument(call)) {
9246
+ let scope = call.parent;
9247
+ while (scope && scope.type !== "func_literal" && scope.type !== "function_declaration" && scope.type !== "method_declaration") {
9248
+ scope = scope.parent;
9249
+ }
9250
+ if (scope) {
9251
+ scopes.push(scope);
9252
+ }
9253
+ }
9254
+ }
9255
+ return scopes;
9256
+ }
9257
+ function isInsideAuthScope(node, scopes) {
9258
+ let current = node.parent;
9259
+ while (current) {
9260
+ for (const scope of scopes) {
9261
+ if (isSameNode(current, scope)) return true;
9262
+ }
9263
+ current = current.parent;
9264
+ }
9265
+ return false;
9266
+ }
9267
+ function hasAuthArgument(call) {
9268
+ const argList = findChild7(call, "argument_list");
9269
+ if (!argList) return false;
9270
+ for (const arg of argList.namedChildren) {
9271
+ const text = arg.text;
9272
+ if (/[Aa]uth/.test(text)) return true;
9273
+ }
9274
+ return false;
9275
+ }
9276
+ function findChild7(node, type) {
9277
+ for (const child of node.namedChildren) {
9278
+ if (child.type === type) return child;
9279
+ }
9280
+ return null;
9281
+ }
9282
+ function findAllNodes3(root, type) {
9283
+ const results = [];
9284
+ function walk(node) {
9285
+ if (node.type === type) results.push(node);
9286
+ for (const child of node.namedChildren) {
9287
+ walk(child);
9288
+ }
9289
+ }
9290
+ walk(root);
9291
+ return results;
9292
+ }
9293
+ function getFirstStringArgument3(node) {
9294
+ const argList = findChild7(node, "argument_list");
9295
+ if (!argList) return null;
9296
+ for (const arg of argList.namedChildren) {
9297
+ if (arg.type === "interpreted_string_literal") {
9298
+ return arg.text.replace(/^"|"$/g, "");
9299
+ }
9300
+ if (arg.type === "raw_string_literal") {
9301
+ return arg.text.replace(/^`|`$/g, "");
9302
+ }
9303
+ }
9304
+ return null;
9305
+ }
9306
+
9307
+ // src/indexer/go/indexer.ts
9308
+ async function indexGoTreeSitter(db, options) {
9309
+ const start = Date.now();
9310
+ await ensureGoParser();
9311
+ const service = options.service ?? "main";
9312
+ const projectRoot = options.projectRoot;
9313
+ const ignorePatterns = [
9314
+ "**/vendor/**",
9315
+ "**/node_modules/**",
9316
+ "**/.git/**",
9317
+ "**/testdata/**",
9318
+ "**/*_test.go",
9319
+ "**/*_gen.go",
9320
+ "**/*_generated.go",
9321
+ "**/*.pb.go"
9322
+ ];
9323
+ const goFiles = globbySync3("**/*.go", {
9324
+ cwd: projectRoot,
9325
+ ignore: ignorePatterns,
9326
+ absolute: false
9327
+ });
9328
+ const existingHashes = getExistingHashes(db, service, "go");
9329
+ const currentPaths = /* @__PURE__ */ new Set();
9330
+ const fileCache = /* @__PURE__ */ new Map();
9331
+ const changedFiles = [];
9332
+ let filesSkipped = 0;
9333
+ for (const filePath of goFiles) {
9334
+ const relPath = filePath.replace(/\\/g, "/");
9335
+ currentPaths.add(relPath);
9336
+ const fullPath = join13(projectRoot, relPath);
9337
+ const content = readFileSync6(fullPath, "utf-8");
9338
+ const hash = hashContent(content);
9339
+ const tree = parseGo(content);
9340
+ fileCache.set(relPath, { content, tree, hash });
9341
+ const existingHash = existingHashes.get(relPath);
9342
+ if (existingHash === hash) {
9343
+ filesSkipped++;
9344
+ } else {
9345
+ changedFiles.push(relPath);
9346
+ }
9347
+ }
9348
+ const removed = [];
9349
+ for (const existingPath of existingHashes.keys()) {
9350
+ if (!currentPaths.has(existingPath)) {
9351
+ removed.push(existingPath);
9352
+ }
9353
+ }
9354
+ const filesToClean = [...removed, ...changedFiles];
9355
+ removeScopedSymbolsForFiles(db, filesToClean, service, "go");
9356
+ const allNewSymbols = [];
9357
+ for (const relPath of changedFiles) {
9358
+ const cached = fileCache.get(relPath);
9359
+ if (!cached) continue;
9360
+ const symbols = extractGoSymbols(cached.tree, relPath, cached.hash);
9361
+ allNewSymbols.push(...symbols);
9362
+ }
9363
+ writeSymbols(db, allNewSymbols, service, "go");
9364
+ const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name, kind, start_line as startLine, end_line as endLine FROM symbols WHERE service = ? AND language = 'go'").all(service);
9365
+ const symbolLookup = buildGoSymbolLookup(allDbSymbols);
9366
+ const allDeps = [];
9367
+ for (const [relPath, cached] of fileCache) {
9368
+ const fileDeps = extractGoDependencies(cached.tree, relPath, allDbSymbols, symbolLookup);
9369
+ allDeps.push(...fileDeps);
9370
+ }
9371
+ db.prepare(
9372
+ "DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE service = ? AND language = 'go')"
9373
+ ).run(service);
9374
+ writeDependencies(db, allDeps);
9375
+ const allRoutes = [];
9376
+ for (const [relPath, cached] of fileCache) {
9377
+ const routes = extractGoRoutes(cached.tree, relPath);
9378
+ allRoutes.push(...routes);
9379
+ }
9380
+ db.prepare("DELETE FROM routes WHERE service = ?").run(service);
9381
+ if (allRoutes.length > 0) {
9382
+ const insertRoute = db.prepare(`
9383
+ INSERT OR REPLACE INTO routes (id, route_path, kind, http_methods, has_auth, service)
9384
+ VALUES (?, ?, ?, ?, ?, ?)
9385
+ `);
9386
+ transaction(db, () => {
9387
+ for (const route of allRoutes) {
9388
+ insertRoute.run(
9389
+ `${service}::${route.id}`,
9390
+ route.routePath,
9391
+ route.kind,
9392
+ JSON.stringify(route.httpMethods),
9393
+ route.hasAuth ? 1 : 0,
9394
+ service
9395
+ );
9396
+ }
9397
+ });
9398
+ }
9399
+ return {
9400
+ symbolsIndexed: allNewSymbols.length,
9401
+ dependenciesIndexed: allDeps.length,
9402
+ componentsAnalyzed: 0,
9403
+ routesAnalyzed: allRoutes.length,
9404
+ filesProcessed: changedFiles.length,
9405
+ filesSkipped,
9406
+ filesRemoved: removed.length,
9407
+ durationMs: Date.now() - start
9408
+ };
9409
+ }
9410
+
9411
+ // src/indexer/package-deps.ts
9412
+ import { join as join14 } from "path";
9413
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "fs";
9414
+ function indexPackageDependencies(db, projectRoot, service = "main") {
9415
+ const deps = [];
9416
+ const pkgJsonPath = join14(projectRoot, "package.json");
9417
+ if (existsSync6(pkgJsonPath)) {
9418
+ deps.push(...parsePackageJson(pkgJsonPath));
9419
+ }
9420
+ const csprojFiles = findCsprojFiles(projectRoot);
9421
+ for (const csproj of csprojFiles) {
9422
+ deps.push(...parseCsproj(csproj));
9423
+ }
9424
+ if (deps.length === 0) return 0;
9425
+ db.prepare("DELETE FROM package_dependencies WHERE service = ?").run(service);
9426
+ const insert = db.prepare(
9427
+ "INSERT OR IGNORE INTO package_dependencies (name, version, source, service) VALUES (?, ?, ?, ?)"
9428
+ );
9429
+ transaction(db, () => {
9430
+ for (const dep of deps) {
9431
+ insert.run(dep.name, dep.version, dep.source, service);
9432
+ }
9433
+ });
9434
+ return deps.length;
9435
+ }
9436
+ function parsePackageJson(filePath) {
9437
+ try {
9438
+ const content = readFileSync7(filePath, "utf-8");
9439
+ const pkg = JSON.parse(content);
9440
+ const deps = [];
9441
+ if (pkg.dependencies) {
9442
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
9443
+ deps.push({ name, version, source: "npm" });
9444
+ }
9445
+ }
9446
+ if (pkg.devDependencies) {
9447
+ for (const [name, version] of Object.entries(pkg.devDependencies)) {
9448
+ deps.push({ name, version, source: "npm-dev" });
9449
+ }
9450
+ }
9451
+ return deps;
9452
+ } catch {
9453
+ return [];
9454
+ }
9455
+ }
9456
+ function parseCsproj(filePath) {
9457
+ try {
9458
+ const content = readFileSync7(filePath, "utf-8");
7318
9459
  const deps = [];
7319
9460
  const pattern = /<PackageReference\s+Include="([^"]+)"(?:\s+Version="([^"]*)")?[^>]*\/?>/gi;
7320
9461
  let match;
@@ -7332,13 +9473,13 @@ function parseCsproj(filePath) {
7332
9473
  }
7333
9474
  function findCsprojFiles(dir, maxDepth = 4) {
7334
9475
  const results = [];
7335
- function walk(currentDir2, depth) {
9476
+ function walk(currentDir4, depth) {
7336
9477
  if (depth > maxDepth) return;
7337
9478
  try {
7338
- const entries = readdirSync5(currentDir2, { withFileTypes: true });
9479
+ const entries = readdirSync5(currentDir4, { withFileTypes: true });
7339
9480
  for (const entry of entries) {
7340
9481
  if (entry.name === "bin" || entry.name === "obj" || entry.name === "node_modules" || entry.name === ".git") continue;
7341
- const fullPath = join12(currentDir2, entry.name);
9482
+ const fullPath = join14(currentDir4, entry.name);
7342
9483
  if (entry.isFile() && entry.name.endsWith(".csproj")) {
7343
9484
  results.push(fullPath);
7344
9485
  } else if (entry.isDirectory()) {
@@ -7353,13 +9494,13 @@ function findCsprojFiles(dir, maxDepth = 4) {
7353
9494
  }
7354
9495
 
7355
9496
  // src/config/loader.ts
7356
- import { readFileSync as readFileSync6 } from "fs";
7357
- import { join as join13 } from "path";
9497
+ import { readFileSync as readFileSync8 } from "fs";
9498
+ import { join as join15 } from "path";
7358
9499
  import yaml from "yaml";
7359
9500
  function loadConfig(projectRoot) {
7360
- const configPath = join13(projectRoot, ".arcbridge", "config.yaml");
9501
+ const configPath = join15(projectRoot, ".arcbridge", "config.yaml");
7361
9502
  try {
7362
- const raw = readFileSync6(configPath, "utf-8");
9503
+ const raw = readFileSync8(configPath, "utf-8");
7363
9504
  const parsed = ArcBridgeConfigSchema.safeParse(yaml.parse(raw));
7364
9505
  if (parsed.success) {
7365
9506
  return { config: parsed.data, error: null };
@@ -7376,12 +9517,16 @@ function loadConfig(projectRoot) {
7376
9517
 
7377
9518
  // src/indexer/index.ts
7378
9519
  function detectProjectLanguage(projectRoot) {
7379
- if (existsSync7(join14(projectRoot, "ProjectSettings")) && existsSync7(join14(projectRoot, "Assets"))) {
9520
+ if (existsSync7(join16(projectRoot, "ProjectSettings")) && existsSync7(join16(projectRoot, "Assets"))) {
7380
9521
  return "csharp";
7381
9522
  }
7382
- if (existsSync7(join14(projectRoot, "tsconfig.json"))) return "typescript";
7383
- if (existsSync7(join14(projectRoot, "package.json"))) return "typescript";
9523
+ if (existsSync7(join16(projectRoot, "tsconfig.json"))) return "typescript";
7384
9524
  if (findDotnetProject(projectRoot)) return "csharp";
9525
+ if (existsSync7(join16(projectRoot, "go.mod"))) return "go";
9526
+ if (existsSync7(join16(projectRoot, "pyproject.toml")) || existsSync7(join16(projectRoot, "requirements.txt")) || existsSync7(join16(projectRoot, "setup.py"))) {
9527
+ return "python";
9528
+ }
9529
+ if (existsSync7(join16(projectRoot, "package.json"))) return "typescript";
7385
9530
  return "typescript";
7386
9531
  }
7387
9532
  async function indexProject(db, options) {
@@ -7401,14 +9546,43 @@ async function indexProject(db, options) {
7401
9546
  service: options.service
7402
9547
  });
7403
9548
  }
7404
- return indexTypeScriptProject(db, options);
9549
+ if (resolvedLanguage === "python") {
9550
+ return await indexPythonTreeSitter(db, {
9551
+ projectRoot: options.projectRoot,
9552
+ service: options.service
9553
+ });
9554
+ }
9555
+ if (resolvedLanguage === "go") {
9556
+ return await indexGoTreeSitter(db, {
9557
+ projectRoot: options.projectRoot,
9558
+ service: options.service
9559
+ });
9560
+ }
9561
+ const resolvedTsconfigPath = options.tsconfigPath ?? ts6.findConfigFile(options.projectRoot, ts6.sys.fileExists, "tsconfig.json");
9562
+ if (!resolvedTsconfigPath) {
9563
+ return {
9564
+ symbolsIndexed: 0,
9565
+ dependenciesIndexed: 0,
9566
+ componentsAnalyzed: 0,
9567
+ routesAnalyzed: 0,
9568
+ filesProcessed: 0,
9569
+ filesSkipped: 0,
9570
+ filesRemoved: 0,
9571
+ durationMs: 0,
9572
+ skippedReason: "no tsconfig.json found"
9573
+ };
9574
+ }
9575
+ return indexTypeScriptProject(db, {
9576
+ ...options,
9577
+ tsconfigPath: resolvedTsconfigPath
9578
+ });
7405
9579
  }
7406
9580
  function resolveCSharpBackend(projectRoot) {
7407
9581
  const { config, error } = loadConfig(projectRoot);
7408
9582
  let setting = config?.indexing?.csharp_indexer;
7409
9583
  if (!setting && error) {
7410
9584
  try {
7411
- const raw = readFileSync7(join14(projectRoot, ".arcbridge", "config.yaml"), "utf-8");
9585
+ const raw = readFileSync9(join16(projectRoot, ".arcbridge", "config.yaml"), "utf-8");
7412
9586
  const parsed = YAML.parse(raw);
7413
9587
  const rawSetting = parsed?.indexing?.csharp_indexer;
7414
9588
  if (rawSetting === "roslyn" || rawSetting === "tree-sitter") {
@@ -7439,7 +9613,7 @@ function indexTypeScriptProject(db, options) {
7439
9613
  const start = Date.now();
7440
9614
  const service = options.service ?? "main";
7441
9615
  const { checker, sourceFiles, projectRoot } = createTsProgram(options);
7442
- const existingHashes = getExistingHashes(db, service);
9616
+ const existingHashes = getExistingHashes(db, service, "typescript");
7443
9617
  const changed = [];
7444
9618
  const currentPaths = /* @__PURE__ */ new Set();
7445
9619
  let filesSkipped = 0;
@@ -7464,18 +9638,18 @@ function indexTypeScriptProject(db, options) {
7464
9638
  ...removed,
7465
9639
  ...changed.map((f) => f.relativePath)
7466
9640
  ];
7467
- removeSymbolsForFiles(db, filesToClean);
9641
+ removeScopedSymbolsForFiles(db, filesToClean, service, "typescript");
7468
9642
  const allSymbols = changed.flatMap(
7469
9643
  (f) => extractSymbols(f.sourceFile, checker, f.relativePath, f.hash)
7470
9644
  );
7471
9645
  writeSymbols(db, allSymbols, service, "typescript");
7472
- const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name FROM symbols WHERE service = ?").all(service);
9646
+ const allDbSymbols = db.prepare("SELECT id, file_path as filePath, name FROM symbols WHERE service = ? AND language = 'typescript'").all(service);
7473
9647
  const lookup = buildSymbolLookup(allDbSymbols);
7474
9648
  const allDeps = sourceFiles.flatMap((sf) => {
7475
9649
  const relPath = relative5(projectRoot, sf.fileName);
7476
9650
  return extractDependencies(sf, checker, relPath, projectRoot, lookup);
7477
9651
  });
7478
- db.prepare("DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE service = ?)").run(service);
9652
+ db.prepare("DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE service = ? AND language = 'typescript')").run(service);
7479
9653
  writeDependencies(db, allDeps);
7480
9654
  const CLIENT_ONLY_TEMPLATES = /* @__PURE__ */ new Set(["react-vite", "angular-app"]);
7481
9655
  const projectType = db.prepare("SELECT value FROM arcbridge_meta WHERE key = 'project_type'").get()?.value;
@@ -7806,21 +9980,21 @@ function safeParseJson(value, fallback) {
7806
9980
  }
7807
9981
 
7808
9982
  // src/sync/yaml-writer.ts
7809
- import { join as join15 } from "path";
7810
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync } from "fs";
9983
+ import { join as join17 } from "path";
9984
+ import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync } from "fs";
7811
9985
  import { parse as parse2, stringify as stringify4 } from "yaml";
7812
9986
  function readTaskFile(projectRoot, phaseId) {
7813
- const path = join15(projectRoot, ".arcbridge", "plan", "tasks", `${phaseId}.yaml`);
9987
+ const path = join17(projectRoot, ".arcbridge", "plan", "tasks", `${phaseId}.yaml`);
7814
9988
  if (!existsSync8(path)) return { error: "not-found" };
7815
- const raw = readFileSync8(path, "utf-8");
9989
+ const raw = readFileSync10(path, "utf-8");
7816
9990
  const result = TaskFileSchema.safeParse(parse2(raw));
7817
9991
  if (!result.success) return { error: "invalid" };
7818
9992
  return { data: result.data, path };
7819
9993
  }
7820
9994
  function readPhasesFile(projectRoot) {
7821
- const path = join15(projectRoot, ".arcbridge", "plan", "phases.yaml");
9995
+ const path = join17(projectRoot, ".arcbridge", "plan", "phases.yaml");
7822
9996
  if (!existsSync8(path)) return { error: "not-found" };
7823
- const raw = readFileSync8(path, "utf-8");
9997
+ const raw = readFileSync10(path, "utf-8");
7824
9998
  const result = PhasesFileSchema.safeParse(parse2(raw));
7825
9999
  if (!result.success) return { error: "invalid" };
7826
10000
  return { data: result.data, path };
@@ -7840,14 +10014,14 @@ function syncTaskToYaml(projectRoot, phaseId, taskId, status, completedAt) {
7840
10014
  writeFileSync5(taskPath, stringify4(taskFile), "utf-8");
7841
10015
  }
7842
10016
  function addTaskToYaml(projectRoot, phaseId, task) {
7843
- const tasksDir = join15(projectRoot, ".arcbridge", "plan", "tasks");
10017
+ const tasksDir = join17(projectRoot, ".arcbridge", "plan", "tasks");
7844
10018
  mkdirSync5(tasksDir, { recursive: true });
7845
10019
  const readResult = readTaskFile(projectRoot, phaseId);
7846
10020
  const taskFile = "error" in readResult ? { schema_version: 1, phase_id: phaseId, tasks: [] } : readResult.data;
7847
10021
  if (!taskFile.tasks.some((t) => t.id === task.id)) {
7848
10022
  taskFile.tasks.push(task);
7849
10023
  }
7850
- const taskPath = join15(tasksDir, `${phaseId}.yaml`);
10024
+ const taskPath = join17(tasksDir, `${phaseId}.yaml`);
7851
10025
  writeFileSync5(taskPath, stringify4(taskFile), "utf-8");
7852
10026
  }
7853
10027
  function syncPhaseToYaml(projectRoot, phaseId, status, startedAt, completedAt) {
@@ -7880,9 +10054,9 @@ function addPhaseToYaml(projectRoot, phase) {
7880
10054
  warning: `Phase number ${phase.phase_number} already used by '${conflicting?.id}'`
7881
10055
  };
7882
10056
  }
7883
- const tasksDir = join15(projectRoot, ".arcbridge", "plan", "tasks");
10057
+ const tasksDir = join17(projectRoot, ".arcbridge", "plan", "tasks");
7884
10058
  mkdirSync5(tasksDir, { recursive: true });
7885
- const taskFilePath = join15(tasksDir, `${phase.id}.yaml`);
10059
+ const taskFilePath = join17(tasksDir, `${phase.id}.yaml`);
7886
10060
  if (!existsSync8(taskFilePath)) {
7887
10061
  writeFileSync5(
7888
10062
  taskFilePath,
@@ -7912,14 +10086,14 @@ function addPhaseToYaml(projectRoot, phase) {
7912
10086
  }
7913
10087
  }
7914
10088
  function syncScenarioToYaml(projectRoot, scenarioId, status, linkedTests, verification) {
7915
- const scenarioPath = join15(
10089
+ const scenarioPath = join17(
7916
10090
  projectRoot,
7917
10091
  ".arcbridge",
7918
10092
  "arc42",
7919
10093
  "10-quality-scenarios.yaml"
7920
10094
  );
7921
10095
  if (!existsSync8(scenarioPath)) return;
7922
- const raw = readFileSync8(scenarioPath, "utf-8");
10096
+ const raw = readFileSync10(scenarioPath, "utf-8");
7923
10097
  const parsed = parse2(raw);
7924
10098
  const result = QualityScenariosFileSchema.safeParse(parsed);
7925
10099
  if (!result.success) return;
@@ -7978,7 +10152,7 @@ function deletePhaseFromYaml(projectRoot, phaseId) {
7978
10152
  return { success: false, warning: `Phase '${phaseId}' not found in phases.yaml` };
7979
10153
  }
7980
10154
  writeFileSync5(phasesPath, stringify4(phasesFile), "utf-8");
7981
- const taskFilePath = join15(projectRoot, ".arcbridge", "plan", "tasks", `${phaseId}.yaml`);
10155
+ const taskFilePath = join17(projectRoot, ".arcbridge", "plan", "tasks", `${phaseId}.yaml`);
7982
10156
  try {
7983
10157
  unlinkSync(taskFilePath);
7984
10158
  } catch (e) {
@@ -8122,7 +10296,7 @@ function safeParseJson2(value, fallback) {
8122
10296
 
8123
10297
  // src/generators/sync-generator.ts
8124
10298
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
8125
- import { join as join16, dirname as dirname4 } from "path";
10299
+ import { join as join18, dirname as dirname6 } from "path";
8126
10300
 
8127
10301
  // src/templates/sync/claude-skill.ts
8128
10302
  function claudeSkillTemplate(config) {
@@ -8300,21 +10474,21 @@ To enable automatic sync:
8300
10474
  function generateSyncFiles(targetDir, config) {
8301
10475
  const generated = [];
8302
10476
  const action = githubActionTemplate(config);
8303
- const actionPath = join16(targetDir, action.relativePath);
8304
- mkdirSync6(dirname4(actionPath), { recursive: true });
10477
+ const actionPath = join18(targetDir, action.relativePath);
10478
+ mkdirSync6(dirname6(actionPath), { recursive: true });
8305
10479
  writeFileSync6(actionPath, action.content, "utf-8");
8306
10480
  generated.push(action.relativePath);
8307
10481
  if (config.platforms.includes("claude")) {
8308
10482
  const skill = claudeSkillTemplate(config);
8309
- const skillPath = join16(targetDir, skill.relativePath);
8310
- mkdirSync6(dirname4(skillPath), { recursive: true });
10483
+ const skillPath = join18(targetDir, skill.relativePath);
10484
+ mkdirSync6(dirname6(skillPath), { recursive: true });
8311
10485
  writeFileSync6(skillPath, skill.content, "utf-8");
8312
10486
  generated.push(skill.relativePath);
8313
10487
  }
8314
10488
  if (config.platforms.includes("copilot")) {
8315
10489
  const hook = copilotHookTemplate(config);
8316
- const hookPath = join16(targetDir, hook.relativePath);
8317
- mkdirSync6(dirname4(hookPath), { recursive: true });
10490
+ const hookPath = join18(targetDir, hook.relativePath);
10491
+ mkdirSync6(dirname6(hookPath), { recursive: true });
8318
10492
  writeFileSync6(hookPath, hook.content, "utf-8");
8319
10493
  generated.push(hook.relativePath);
8320
10494
  }
@@ -8323,7 +10497,7 @@ function generateSyncFiles(targetDir, config) {
8323
10497
 
8324
10498
  // src/metrics/activity.ts
8325
10499
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
8326
- import { join as join17 } from "path";
10500
+ import { join as join19 } from "path";
8327
10501
  function insertActivity(db, params) {
8328
10502
  const totalTokens = params.totalTokens ?? (params.inputTokens != null && params.outputTokens != null ? params.inputTokens + params.outputTokens : null);
8329
10503
  const stmt = db.prepare(`
@@ -8481,7 +10655,7 @@ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
8481
10655
  });
8482
10656
  const rows = result.rows;
8483
10657
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
8484
- const dir = join17(projectRoot, ".arcbridge", "metrics");
10658
+ const dir = join19(projectRoot, ".arcbridge", "metrics");
8485
10659
  mkdirSync7(dir, { recursive: true });
8486
10660
  let content;
8487
10661
  let filename;
@@ -8578,7 +10752,7 @@ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
8578
10752
  break;
8579
10753
  }
8580
10754
  }
8581
- const filePath = join17(dir, filename);
10755
+ const filePath = join19(dir, filename);
8582
10756
  writeFileSync7(filePath, content, "utf-8");
8583
10757
  return filePath;
8584
10758
  }
@@ -8782,7 +10956,7 @@ function parseStatusCode(code) {
8782
10956
  // src/testing/runner.ts
8783
10957
  import { execFileSync as execFileSync4 } from "child_process";
8784
10958
  import { existsSync as existsSync9 } from "fs";
8785
- import { resolve as resolve3 } from "path";
10959
+ import { resolve as resolve5 } from "path";
8786
10960
  function verifyScenarios(db, projectRoot, options) {
8787
10961
  const results = [];
8788
10962
  const errors = [];
@@ -8819,7 +10993,7 @@ function verifyScenarios(db, projectRoot, options) {
8819
10993
  }
8820
10994
  if (testPaths.length === 0) continue;
8821
10995
  const missingPaths = testPaths.filter(
8822
- (tp) => !existsSync9(resolve3(projectRoot, tp))
10996
+ (tp) => !existsSync9(resolve5(projectRoot, tp))
8823
10997
  );
8824
10998
  if (missingPaths.length === testPaths.length) {
8825
10999
  results.push({
@@ -8834,7 +11008,7 @@ function verifyScenarios(db, projectRoot, options) {
8834
11008
  continue;
8835
11009
  }
8836
11010
  const existingPaths = testPaths.filter(
8837
- (tp) => existsSync9(resolve3(projectRoot, tp))
11011
+ (tp) => existsSync9(resolve5(projectRoot, tp))
8838
11012
  );
8839
11013
  const start = Date.now();
8840
11014
  let passed;
@@ -8888,11 +11062,11 @@ ${output}`;
8888
11062
  }
8889
11063
 
8890
11064
  // src/roles/loader.ts
8891
- import { readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
8892
- import { join as join18 } from "path";
11065
+ import { readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
11066
+ import { join as join20 } from "path";
8893
11067
  import matter4 from "gray-matter";
8894
11068
  function loadRoles(projectRoot) {
8895
- const agentsDir = join18(projectRoot, ".arcbridge", "agents");
11069
+ const agentsDir = join20(projectRoot, ".arcbridge", "agents");
8896
11070
  const roles = [];
8897
11071
  const errors = [];
8898
11072
  let files;
@@ -8902,9 +11076,9 @@ function loadRoles(projectRoot) {
8902
11076
  return { roles: [], errors: [`Agent directory not found: ${agentsDir}`] };
8903
11077
  }
8904
11078
  for (const file of files) {
8905
- const filePath = join18(agentsDir, file);
11079
+ const filePath = join20(agentsDir, file);
8906
11080
  try {
8907
- const raw = readFileSync9(filePath, "utf-8");
11081
+ const raw = readFileSync11(filePath, "utf-8");
8908
11082
  const parsed = matter4(raw);
8909
11083
  const input = {
8910
11084
  ...parsed.data,
@@ -8929,9 +11103,9 @@ function loadRole(projectRoot, roleId) {
8929
11103
  if (!/^[a-z0-9-]+$/.test(roleId)) {
8930
11104
  return { role: null, error: `Invalid role ID: "${roleId}" (must be kebab-case)` };
8931
11105
  }
8932
- const filePath = join18(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
11106
+ const filePath = join20(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
8933
11107
  try {
8934
- const raw = readFileSync9(filePath, "utf-8");
11108
+ const raw = readFileSync11(filePath, "utf-8");
8935
11109
  const parsed = matter4(raw);
8936
11110
  const input = {
8937
11111
  ...parsed.data,