@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.d.ts +20 -11
- package/dist/index.js +2346 -172
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
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/
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
3990
|
-
|
|
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
|
|
4968
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6020
|
-
|
|
6021
|
-
).all(
|
|
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
|
|
6642
|
+
function removeScopedSymbolsForFiles(db, filePaths, service, language) {
|
|
6029
6643
|
if (filePaths.length === 0) return;
|
|
6030
|
-
const
|
|
6031
|
-
"DELETE FROM symbols WHERE file_path = ?"
|
|
6032
|
-
);
|
|
6644
|
+
const scope = "file_path = ? AND service = ? AND language = ?";
|
|
6033
6645
|
const deleteDepsSource = db.prepare(
|
|
6034
|
-
|
|
6646
|
+
`DELETE FROM dependencies WHERE source_symbol IN (SELECT id FROM symbols WHERE ${scope})`
|
|
6035
6647
|
);
|
|
6036
6648
|
const deleteDepsTarget = db.prepare(
|
|
6037
|
-
|
|
6649
|
+
`DELETE FROM dependencies WHERE target_symbol IN (SELECT id FROM symbols WHERE ${scope})`
|
|
6038
6650
|
);
|
|
6039
6651
|
const deleteComponents = db.prepare(
|
|
6040
|
-
|
|
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
|
|
6795
|
+
const currentDir4 = dirname2(fileURLToPath(import.meta.url));
|
|
6181
6796
|
const candidates = [
|
|
6182
|
-
resolve(
|
|
6183
|
-
resolve(
|
|
6184
|
-
resolve(
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
7885
|
+
// src/indexer/python/indexer.ts
|
|
7886
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
7271
7887
|
import { join as join12 } from "path";
|
|
7272
|
-
import {
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
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
|
-
|
|
7284
|
-
|
|
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
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
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
|
|
7934
|
+
})();
|
|
7935
|
+
return initPromise2;
|
|
7294
7936
|
}
|
|
7295
|
-
function
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
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
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
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
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
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(
|
|
9476
|
+
function walk(currentDir4, depth) {
|
|
7336
9477
|
if (depth > maxDepth) return;
|
|
7337
9478
|
try {
|
|
7338
|
-
const entries = readdirSync5(
|
|
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 =
|
|
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
|
|
7357
|
-
import { join as
|
|
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 =
|
|
9501
|
+
const configPath = join15(projectRoot, ".arcbridge", "config.yaml");
|
|
7361
9502
|
try {
|
|
7362
|
-
const raw =
|
|
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(
|
|
9520
|
+
if (existsSync7(join16(projectRoot, "ProjectSettings")) && existsSync7(join16(projectRoot, "Assets"))) {
|
|
7380
9521
|
return "csharp";
|
|
7381
9522
|
}
|
|
7382
|
-
if (existsSync7(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
7810
|
-
import { existsSync as existsSync8, readFileSync as
|
|
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 =
|
|
9987
|
+
const path = join17(projectRoot, ".arcbridge", "plan", "tasks", `${phaseId}.yaml`);
|
|
7814
9988
|
if (!existsSync8(path)) return { error: "not-found" };
|
|
7815
|
-
const raw =
|
|
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 =
|
|
9995
|
+
const path = join17(projectRoot, ".arcbridge", "plan", "phases.yaml");
|
|
7822
9996
|
if (!existsSync8(path)) return { error: "not-found" };
|
|
7823
|
-
const raw =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
10057
|
+
const tasksDir = join17(projectRoot, ".arcbridge", "plan", "tasks");
|
|
7884
10058
|
mkdirSync5(tasksDir, { recursive: true });
|
|
7885
|
-
const taskFilePath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
8304
|
-
mkdirSync6(
|
|
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 =
|
|
8310
|
-
mkdirSync6(
|
|
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 =
|
|
8317
|
-
mkdirSync6(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
8892
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
11079
|
+
const filePath = join20(agentsDir, file);
|
|
8906
11080
|
try {
|
|
8907
|
-
const raw =
|
|
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 =
|
|
11106
|
+
const filePath = join20(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
|
|
8933
11107
|
try {
|
|
8934
|
-
const raw =
|
|
11108
|
+
const raw = readFileSync11(filePath, "utf-8");
|
|
8935
11109
|
const parsed = matter4(raw);
|
|
8936
11110
|
const input = {
|
|
8937
11111
|
...parsed.data,
|