@cassiomc1/forgeloop 1.11.1 → 1.13.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.
Files changed (71) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +90 -0
  5. package/DOCS_INDEX.md +13 -11
  6. package/ENG/c-development-eng.md +112 -0
  7. package/ENG/cpp-development-eng.md +109 -0
  8. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  9. package/ENG/flutter-development-eng.md +2106 -0
  10. package/ENG/go-development-eng.md +103 -0
  11. package/ENG/java-development-eng.md +125 -0
  12. package/ENG/nodejs-backend-development-eng.md +605 -0
  13. package/ENG/php-development-eng.md +104 -0
  14. package/ENG/rust-development-eng.md +422 -0
  15. package/ENG/sql-development-eng.md +108 -0
  16. package/ENG/swift-development-eng.md +111 -0
  17. package/ENG/typescript-development-eng.md +108 -0
  18. package/GUIDE_ROUTER.md +455 -4
  19. package/LOOP_SYSTEM_DESIGN.md +10 -6
  20. package/QUALITY_SCORECARD.md +2 -0
  21. package/README.md +50 -38
  22. package/THIRD_PARTY_NOTICES.md +19 -7
  23. package/completions/_forgeloop +3 -3
  24. package/completions/forgeloop.bash +3 -3
  25. package/completions/forgeloop.fish +7 -0
  26. package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
  27. package/docs/CLI_REFERENCE.md +28 -6
  28. package/docs/DOCUMENTATION_GUIDE.md +2 -1
  29. package/docs/GETTING_STARTED.md +59 -0
  30. package/docs/MCP.md +1 -1
  31. package/docs/PACKAGE_CONTENTS.md +29 -8
  32. package/docs/RECIPES.md +23 -0
  33. package/docs/RELEASE_CHECKLIST.md +51 -9
  34. package/docs/TROUBLESHOOTING.md +100 -2
  35. package/docs/assets/diagrams/forgeloop-engineering-flow.html +19 -6
  36. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  37. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5 -5
  38. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +12 -2
  39. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
  40. package/docs/documentation-manifest.json +652 -0
  41. package/docs/protocol-requirements.json +77 -0
  42. package/package.json +19 -4
  43. package/schemas/routing-input.schema.json +14 -1
  44. package/scripts/CI_VALIDATORS.md +84 -11
  45. package/scripts/generate-agent-protocol-summary.mjs +36 -0
  46. package/src/commands/next.js +19 -7
  47. package/src/commands/route.js +5 -0
  48. package/src/commands/task-create.js +84 -25
  49. package/src/commands/task-list.js +22 -2
  50. package/src/config/guides.json +48 -0
  51. package/src/core/build-script.js +151 -0
  52. package/src/core/c-cpp-project.js +143 -0
  53. package/src/core/cli-command-definitions.js +8 -1
  54. package/src/core/command-executors.js +5 -3
  55. package/src/core/command-input.js +140 -102
  56. package/src/core/contract-presets.js +82 -0
  57. package/src/core/error-codes.js +3 -3
  58. package/src/core/filesystem.js +1 -10
  59. package/src/core/go-project.js +206 -0
  60. package/src/core/java-project.js +403 -0
  61. package/src/core/multi-language-project.js +117 -0
  62. package/src/core/next-explanation.js +63 -0
  63. package/src/core/php-project.js +85 -0
  64. package/src/core/project-detection.js +2227 -0
  65. package/src/core/reconcile-closure.js +4 -1
  66. package/src/core/router.js +233 -7
  67. package/src/core/rust-project.js +400 -0
  68. package/src/core/sql-project.js +141 -0
  69. package/src/core/swift-project.js +200 -0
  70. package/src/core/typescript-project.js +349 -0
  71. package/src/core/xml-structure.js +123 -0
@@ -0,0 +1,2227 @@
1
+ import { open, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ cargoWorkspaceContains,
6
+ isRustSharedFile,
7
+ parseCargoManifest,
8
+ rustSharedFileKind,
9
+ rustSharedScopeDirectory,
10
+ } from "./rust-project.js";
11
+ import {
12
+ languageProjectKinds,
13
+ languageSourceExtension,
14
+ portablePath as languagePortablePath,
15
+ C_SOURCE_EXTENSIONS,
16
+ CPP_SOURCE_EXTENSIONS,
17
+ JAVA_SOURCE_EXTENSIONS,
18
+ PHP_SOURCE_EXTENSIONS,
19
+ SQL_SOURCE_EXTENSIONS,
20
+ SWIFT_SOURCE_EXTENSIONS,
21
+ } from "./multi-language-project.js";
22
+ import { inspectGoProject } from "./go-project.js";
23
+ import {
24
+ inspectTypeScriptProject,
25
+ isDeclarationFile,
26
+ isTypeScriptConfigName,
27
+ resolveTypeScriptConfigGraph,
28
+ resolveTypeScriptConfigOwnershipRoots,
29
+ } from "./typescript-project.js";
30
+ import { inspectPhpProject, phpSourceLooksExecutable } from "./php-project.js";
31
+ import { inspectJavaProject } from "./java-project.js";
32
+ import { inspectNativeProject, nativeFrameworkForSource } from "./c-cpp-project.js";
33
+ import { inspectSwiftProject, swiftSourceLooksExecutable } from "./swift-project.js";
34
+ import { looksLikeSql, parseSqlProject } from "./sql-project.js";
35
+
36
+ export const PROJECT_EVIDENCE_SCHEMA_VERSION = 1;
37
+
38
+ export const PROJECT_EVIDENCE_SCOPES = Object.freeze([
39
+ "MATCH",
40
+ "UNSCOPED",
41
+ "NO_MATCH",
42
+ "NONE",
43
+ ]);
44
+
45
+ export const PROJECT_DETECTION_LIMITS = Object.freeze({
46
+ maxManifests: 256,
47
+ maxSolutionFiles: 64,
48
+ maxManifestBytes: 1024 * 1024,
49
+ maxSourceFiles: 256,
50
+ maxSourceBytes: 512 * 1024,
51
+ maxVisitedDirectories: 4096,
52
+ maxVisitedEntries: 20000,
53
+ });
54
+ const MAX_MANIFESTS = PROJECT_DETECTION_LIMITS.maxManifests;
55
+ const MAX_SOLUTION_FILES = PROJECT_DETECTION_LIMITS.maxSolutionFiles;
56
+ const MAX_MANIFEST_BYTES = PROJECT_DETECTION_LIMITS.maxManifestBytes;
57
+ const MAX_SOURCE_FILES = PROJECT_DETECTION_LIMITS.maxSourceFiles;
58
+ const MAX_SOURCE_BYTES = PROJECT_DETECTION_LIMITS.maxSourceBytes;
59
+ const MAX_XML_TEXT_LENGTH = 4096;
60
+ const UNSUPPORTED_YAML_VALUE = Symbol("unsupported-yaml-value");
61
+ const IGNORED_DIRECTORIES = new Set([
62
+ ".dart_tool",
63
+ ".forgeloop",
64
+ ".git",
65
+ ".idea",
66
+ ".vscode",
67
+ "artifacts",
68
+ "bin",
69
+ "build",
70
+ "coverage",
71
+ "DerivedData",
72
+ "deriveddata",
73
+ "dist",
74
+ "external",
75
+ "generated",
76
+ "gen",
77
+ ".gradle",
78
+ "node_modules",
79
+ "obj",
80
+ "out",
81
+ "target",
82
+ "TestResults",
83
+ "vendor",
84
+ "third_party",
85
+ ]);
86
+ const DOTNET_PROJECT_EXTENSIONS = new Set([".csproj", ".fsproj", ".vbproj"]);
87
+ const DOTNET_SDKS = new Set([
88
+ "Microsoft.NET.Sdk",
89
+ "Microsoft.NET.Sdk.Web",
90
+ "Microsoft.NET.Sdk.Worker",
91
+ "Microsoft.NET.Sdk.Razor",
92
+ "Microsoft.NET.Sdk.BlazorWebAssembly",
93
+ "Aspire.AppHost.Sdk",
94
+ "MSTest.Sdk",
95
+ ]);
96
+ const ASPNETCORE_SDKS = new Set([
97
+ "Microsoft.NET.Sdk.Web",
98
+ "Microsoft.NET.Sdk.Razor",
99
+ "Microsoft.NET.Sdk.BlazorWebAssembly",
100
+ ]);
101
+ const SHARED_DOTNET_FILES = new Set([
102
+ "directory.build.props",
103
+ "directory.build.targets",
104
+ "directory.packages.props",
105
+ "global.json",
106
+ "nuget.config",
107
+ ]);
108
+ const NODE_BACKEND_DEPENDENCIES = new Set([
109
+ "express",
110
+ "fastify",
111
+ "@nestjs/core",
112
+ "koa",
113
+ "@hapi/hapi",
114
+ ]);
115
+ const NODE_SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
116
+ const NON_TYPESCRIPT_JSON_FILES = new Set([
117
+ "composer.lock",
118
+ "global.json",
119
+ "launchsettings.json",
120
+ "npm-shrinkwrap.json",
121
+ "package-lock.json",
122
+ "packages.lock.json",
123
+ ]);
124
+ const NODE_NON_RUNTIME_DIRECTORIES = new Set([
125
+ "test",
126
+ "tests",
127
+ "__tests__",
128
+ "fixtures",
129
+ "mocks",
130
+ "examples",
131
+ "docs",
132
+ "coverage",
133
+ "dist",
134
+ "build",
135
+ "node_modules",
136
+ ".next",
137
+ ".turbo",
138
+ ".storybook",
139
+ "storybook",
140
+ "scripts",
141
+ "tools",
142
+ "tooling",
143
+ "config",
144
+ "configs",
145
+ "codegen",
146
+ "generator",
147
+ "generators",
148
+ ]);
149
+ const NODE_RUNTIME_SOURCE_DIRECTORIES = new Set([
150
+ "api",
151
+ "app",
152
+ "apps",
153
+ "backend",
154
+ "lib",
155
+ "runtime",
156
+ "server",
157
+ "servers",
158
+ "service",
159
+ "services",
160
+ "src",
161
+ "worker",
162
+ "workers",
163
+ ]);
164
+ const NODE_RUNTIME_ENTRY_NAMES = new Set([
165
+ "api",
166
+ "app",
167
+ "bootstrap",
168
+ "daemon",
169
+ "entry",
170
+ "http",
171
+ "https",
172
+ "index",
173
+ "main",
174
+ "network",
175
+ "runtime",
176
+ "server",
177
+ "service",
178
+ "worker",
179
+ ]);
180
+ const SQL_RUNTIME_DIRECTORIES = new Set([
181
+ "database",
182
+ "db",
183
+ "migration",
184
+ "migrations",
185
+ "schema",
186
+ "schemas",
187
+ "sql",
188
+ ]);
189
+ const SQL_NON_RUNTIME_DIRECTORIES = new Set([
190
+ "build",
191
+ "coverage",
192
+ "dist",
193
+ "docs",
194
+ "example",
195
+ "examples",
196
+ "fixtures",
197
+ "generated",
198
+ "gen",
199
+ "node_modules",
200
+ "target",
201
+ "test",
202
+ "tests",
203
+ "vendor",
204
+ ]);
205
+ const NODE_SERVER_BUILTINS = Object.freeze([
206
+ "node:http",
207
+ "node:https",
208
+ "node:http2",
209
+ "node:net",
210
+ "node:tls",
211
+ "node:dgram",
212
+ "http",
213
+ "https",
214
+ "http2",
215
+ "net",
216
+ "tls",
217
+ "dgram",
218
+ ]);
219
+ const SHARED_NODE_FILES = new Set([
220
+ "package-lock.json",
221
+ "npm-shrinkwrap.json",
222
+ "pnpm-lock.yaml",
223
+ "yarn.lock",
224
+ ".npmrc",
225
+ ".yarnrc.yml",
226
+ "pnpm-workspace.yaml",
227
+ ".nvmrc",
228
+ ".node-version",
229
+ ]);
230
+ const SHARED_GRADLE_FILES = new Set(["gradle.properties"]);
231
+ const PLATFORM_DIRECTORIES = Object.freeze([
232
+ "android",
233
+ "ios",
234
+ "linux",
235
+ "macos",
236
+ "web",
237
+ "windows",
238
+ ]);
239
+ const SOURCE_DIRECTORIES = Object.freeze([
240
+ "integration_test",
241
+ "lib",
242
+ "test",
243
+ ]);
244
+
245
+ function portableRelative(root, absolutePath) {
246
+ const relative = path.relative(root, absolutePath).split(path.sep).join("/");
247
+ return relative === "" ? "." : relative;
248
+ }
249
+
250
+ function uniqueSorted(values) {
251
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]
252
+ .sort((left, right) => left.localeCompare(right));
253
+ }
254
+
255
+ function stripYamlComment(value) {
256
+ let quote = null;
257
+ let braceDepth = 0;
258
+ let bracketDepth = 0;
259
+ for (let index = 0; index < value.length; index += 1) {
260
+ const character = value[index];
261
+ if (quote === "\"") {
262
+ if (character === "\\") index += 1;
263
+ else if (character === "\"") quote = null;
264
+ continue;
265
+ }
266
+ if (quote === "'") {
267
+ if (character === "'" && value[index + 1] === "'") index += 1;
268
+ else if (character === "'") quote = null;
269
+ continue;
270
+ }
271
+ if (character === "\"" || character === "'") {
272
+ quote = character;
273
+ continue;
274
+ }
275
+ if (character === "{") braceDepth += 1;
276
+ else if (character === "}") braceDepth = Math.max(0, braceDepth - 1);
277
+ else if (character === "[") bracketDepth += 1;
278
+ else if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
279
+ else if (character === "#" && braceDepth === 0 && bracketDepth === 0
280
+ && (index === 0 || /\s/u.test(value[index - 1]))) {
281
+ return value.slice(0, index).trimEnd();
282
+ }
283
+ }
284
+ return value.trimEnd();
285
+ }
286
+
287
+ function findTopLevelColon(value) {
288
+ let quote = null;
289
+ let braceDepth = 0;
290
+ let bracketDepth = 0;
291
+ for (let index = 0; index < value.length; index += 1) {
292
+ const character = value[index];
293
+ if (quote === "\"") {
294
+ if (character === "\\") index += 1;
295
+ else if (character === "\"") quote = null;
296
+ continue;
297
+ }
298
+ if (quote === "'") {
299
+ if (character === "'" && value[index + 1] === "'") index += 1;
300
+ else if (character === "'") quote = null;
301
+ continue;
302
+ }
303
+ if (character === "\"" || character === "'") {
304
+ quote = character;
305
+ continue;
306
+ }
307
+ if (character === "{") braceDepth += 1;
308
+ else if (character === "}") braceDepth = Math.max(0, braceDepth - 1);
309
+ else if (character === "[") bracketDepth += 1;
310
+ else if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
311
+ else if (character === ":" && braceDepth === 0 && bracketDepth === 0) return index;
312
+ }
313
+ return -1;
314
+ }
315
+
316
+ function unquoteYamlScalar(value) {
317
+ const trimmed = value.trim();
318
+ if (trimmed.length < 2) return trimmed;
319
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
320
+ try {
321
+ return JSON.parse(trimmed);
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
327
+ return trimmed.slice(1, -1).replaceAll("''", "'");
328
+ }
329
+ return trimmed;
330
+ }
331
+
332
+ function splitTopLevel(value, delimiter = ",") {
333
+ const parts = [];
334
+ let start = 0;
335
+ let quote = null;
336
+ let braceDepth = 0;
337
+ let bracketDepth = 0;
338
+ for (let index = 0; index < value.length; index += 1) {
339
+ const character = value[index];
340
+ if (quote === "\"") {
341
+ if (character === "\\") index += 1;
342
+ else if (character === "\"") quote = null;
343
+ continue;
344
+ }
345
+ if (quote === "'") {
346
+ if (character === "'" && value[index + 1] === "'") index += 1;
347
+ else if (character === "'") quote = null;
348
+ continue;
349
+ }
350
+ if (character === "\"" || character === "'") {
351
+ quote = character;
352
+ continue;
353
+ }
354
+ if (character === "{") braceDepth += 1;
355
+ else if (character === "}") braceDepth = Math.max(0, braceDepth - 1);
356
+ else if (character === "[") bracketDepth += 1;
357
+ else if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
358
+ else if (character === delimiter && braceDepth === 0 && bracketDepth === 0) {
359
+ parts.push(value.slice(start, index).trim());
360
+ start = index + 1;
361
+ }
362
+ }
363
+ parts.push(value.slice(start).trim());
364
+ return parts.filter(Boolean);
365
+ }
366
+
367
+ function parseInlineMap(value) {
368
+ const trimmed = value.trim();
369
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
370
+ const body = trimmed.slice(1, -1).trim();
371
+ if (body === "") return {};
372
+ const result = {};
373
+ for (const pair of splitTopLevel(body)) {
374
+ const separator = findTopLevelColon(pair);
375
+ if (separator <= 0) return null;
376
+ const key = unquoteYamlScalar(pair.slice(0, separator));
377
+ const rawValue = pair.slice(separator + 1).trim();
378
+ if (!key || rawValue === "" || Object.prototype.hasOwnProperty.call(result, key)) return null;
379
+ const parsedValue = parseYamlValue(rawValue);
380
+ if (parsedValue === null) return null;
381
+ result[key] = parsedValue;
382
+ }
383
+ return result;
384
+ }
385
+
386
+ function parseYamlValue(value) {
387
+ const withoutComment = stripYamlComment(value).trim();
388
+ if (withoutComment === "") return "";
389
+ if (withoutComment.startsWith("{")) {
390
+ return withoutComment.endsWith("}") ? parseInlineMap(withoutComment) : null;
391
+ }
392
+ if (withoutComment.startsWith("[")) return UNSUPPORTED_YAML_VALUE;
393
+ return unquoteYamlScalar(withoutComment);
394
+ }
395
+
396
+ function containsUnsupportedYamlValue(value) {
397
+ if (value === UNSUPPORTED_YAML_VALUE) return true;
398
+ if (!value || typeof value !== "object") return false;
399
+ return Object.values(value).some(containsUnsupportedYamlValue);
400
+ }
401
+
402
+ function parseYamlLine(line) {
403
+ if (/^\t/u.test(line)) return { invalid: true };
404
+ const indent = line.match(/^ */u)?.[0].length ?? 0;
405
+ const content = stripYamlComment(line.slice(indent)).trim();
406
+ if (content === "" || content === "---" || content === "..." || content.startsWith("#")) {
407
+ return null;
408
+ }
409
+ if (content.startsWith("- ") || content === "-") return { indent, sequence: true };
410
+ const separator = findTopLevelColon(content);
411
+ if (separator <= 0) return null;
412
+ const key = unquoteYamlScalar(content.slice(0, separator));
413
+ if (!key) return null;
414
+ const rawValue = content.slice(separator + 1).trim();
415
+ const value = rawValue === "" ? "" : parseYamlValue(rawValue);
416
+ if (value === null) return { invalid: true };
417
+ return { indent, key, value };
418
+ }
419
+
420
+ function dependencySdkValue(entry) {
421
+ if (!entry) return null;
422
+ if (entry.value && typeof entry.value === "object" && !Array.isArray(entry.value)) {
423
+ return entry.value.sdk === "flutter" ? "flutter" : null;
424
+ }
425
+ return entry.value === "" ? null : null;
426
+ }
427
+
428
+ function parseYamlLines(lines) {
429
+ const parsedLines = [];
430
+ let valid = true;
431
+ for (const line of lines) {
432
+ const parsed = parseYamlLine(line);
433
+ if (parsed?.invalid) valid = false;
434
+ parsedLines.push(parsed);
435
+ }
436
+ return { parsedLines, valid };
437
+ }
438
+
439
+ function findSectionIndexes(parsedLines, sectionName) {
440
+ const sectionIndexes = [];
441
+ for (let index = 0; index < parsedLines.length; index += 1) {
442
+ const parsed = parsedLines[index];
443
+ if (parsed?.indent === 0 && parsed.key === sectionName) sectionIndexes.push(index);
444
+ }
445
+ return sectionIndexes;
446
+ }
447
+
448
+ function dependencyEntriesFromInline(value) {
449
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
450
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, { value: entryValue }]));
451
+ }
452
+
453
+ function findSectionIndent(parsedLines, sectionIndex) {
454
+ for (let index = sectionIndex + 1; index < parsedLines.length; index += 1) {
455
+ const parsed = parsedLines[index];
456
+ if (!parsed) continue;
457
+ if (parsed.indent === 0) break;
458
+ return parsed.indent;
459
+ }
460
+ return null;
461
+ }
462
+
463
+ function readNestedDependencyMap(parsedLines, entryIndex, parentIndent) {
464
+ let nestedIndent = null;
465
+ const nested = {};
466
+ for (let index = entryIndex + 1; index < parsedLines.length; index += 1) {
467
+ const parsed = parsedLines[index];
468
+ if (!parsed) continue;
469
+ if (parsed.invalid) return null;
470
+ if (parsed.sequence) {
471
+ if (parsed.indent > parentIndent) return null;
472
+ break;
473
+ }
474
+ if (parsed.indent <= parentIndent) break;
475
+ if (containsUnsupportedYamlValue(parsed.value)) return null;
476
+ nestedIndent ??= parsed.indent;
477
+ if (parsed.indent !== nestedIndent) continue;
478
+ if (Object.prototype.hasOwnProperty.call(nested, parsed.key)) return null;
479
+ nested[parsed.key] = parsed.value;
480
+ }
481
+ return nested;
482
+ }
483
+
484
+ function readBlockDependencyEntries(parsedLines, sectionIndex, sectionIndent) {
485
+ const entries = {};
486
+ for (let index = sectionIndex + 1; index < parsedLines.length; index += 1) {
487
+ const parsed = parsedLines[index];
488
+ if (!parsed) continue;
489
+ if (parsed.invalid) return null;
490
+ if (parsed.indent === 0) break;
491
+ if (parsed.sequence || containsUnsupportedYamlValue(parsed.value)) return null;
492
+ if (parsed.indent !== sectionIndent) continue;
493
+ if (Object.prototype.hasOwnProperty.call(entries, parsed.key)) return null;
494
+ const entry = { value: parsed.value };
495
+ if (parsed.value === "") {
496
+ const nested = readNestedDependencyMap(parsedLines, index, parsed.indent);
497
+ if (nested === null) return null;
498
+ entry.value = nested;
499
+ }
500
+ entries[parsed.key] = entry;
501
+ }
502
+ return entries;
503
+ }
504
+
505
+ function readDependencySection(lines, sectionName) {
506
+ const { parsedLines, valid } = parseYamlLines(lines);
507
+ const sectionIndexes = findSectionIndexes(parsedLines, sectionName);
508
+ if (sectionIndexes.length > 1) return { present: true, valid: false, entries: {} };
509
+ if (sectionIndexes.length === 0) return { present: false, valid, entries: {} };
510
+
511
+ const sectionIndex = sectionIndexes[0];
512
+ const section = parsedLines[sectionIndex];
513
+ const entries = {};
514
+ if (section.value !== "") {
515
+ if (containsUnsupportedYamlValue(section.value)) {
516
+ return { present: true, valid: false, entries: {} };
517
+ }
518
+ const inlineEntries = dependencyEntriesFromInline(section.value);
519
+ if (inlineEntries === null) {
520
+ return { present: true, valid: false, entries: {} };
521
+ }
522
+ return { present: true, valid, entries: inlineEntries };
523
+ }
524
+
525
+ const sectionIndent = findSectionIndent(parsedLines, sectionIndex);
526
+ if (sectionIndent === null) return { present: true, valid, entries };
527
+ const blockEntries = readBlockDependencyEntries(parsedLines, sectionIndex, sectionIndent);
528
+ if (blockEntries === null) return { present: true, valid: false, entries: {} };
529
+ return { present: true, valid, entries: blockEntries };
530
+ }
531
+
532
+ export function parsePubspec(text) {
533
+ if (typeof text !== "string" || text.length > MAX_MANIFEST_BYTES || /\r(?!\n)/u.test(text)) {
534
+ return { valid: false, primary: false, flutterTest: false, override: false };
535
+ }
536
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
537
+ const dependencies = readDependencySection(lines, "dependencies");
538
+ const devDependencies = readDependencySection(lines, "dev_dependencies");
539
+ const dependencyOverrides = readDependencySection(lines, "dependency_overrides");
540
+ const valid = dependencies.valid && devDependencies.valid && dependencyOverrides.valid;
541
+ const primary = valid && dependencySdkValue(dependencies.entries.flutter) === "flutter";
542
+ const flutterTest = valid && dependencySdkValue(devDependencies.entries.flutter_test) === "flutter";
543
+ const override = valid && dependencySdkValue(dependencyOverrides.entries.flutter) === "flutter";
544
+ return {
545
+ valid,
546
+ primary,
547
+ flutterTest,
548
+ override,
549
+ };
550
+ }
551
+
552
+ function invalidNodePackage() {
553
+ return {
554
+ valid: false,
555
+ backendDependencies: [],
556
+ backendDependencySignals: [],
557
+ runtimeScripts: [],
558
+ serverBuiltins: [],
559
+ supportingDependencies: [],
560
+ enginesNode: false,
561
+ moduleType: false,
562
+ packageManager: false,
563
+ workspaceRoot: false,
564
+ };
565
+ }
566
+
567
+ function isPlainObject(value) {
568
+ return value !== null && typeof value === "object" && !Array.isArray(value);
569
+ }
570
+
571
+ function validStringMap(value) {
572
+ return isPlainObject(value) && Object.values(value).every((entry) => typeof entry === "string");
573
+ }
574
+
575
+ function packageWorkspaceRoot(value) {
576
+ if (Array.isArray(value)) return value.length > 0 && value.every((entry) => typeof entry === "string");
577
+ return isPlainObject(value)
578
+ && Array.isArray(value.packages)
579
+ && value.packages.length > 0
580
+ && value.packages.every((entry) => typeof entry === "string");
581
+ }
582
+
583
+ function commandStartsWithNode(command) {
584
+ if (typeof command !== "string") return false;
585
+ let remaining = command.trim();
586
+ if (remaining === "") return false;
587
+
588
+ // Allow the ordinary shell environment-prefix form while keeping the
589
+ // executable decision conservative. Wrappers such as npx, npm, and
590
+ // cross-env are intentionally not followed.
591
+ while (true) {
592
+ const assignment = remaining.match(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|<>]+)\s+/u);
593
+ if (!assignment) break;
594
+ remaining = remaining.slice(assignment[0].length).trimStart();
595
+ }
596
+ if (remaining.startsWith("env ")) {
597
+ remaining = remaining.slice(4).trimStart();
598
+ while (true) {
599
+ const assignment = remaining.match(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|<>]+)\s+/u);
600
+ if (!assignment) break;
601
+ remaining = remaining.slice(assignment[0].length).trimStart();
602
+ }
603
+ }
604
+
605
+ const executableMatch = remaining.match(/^(?:"([^"]+)"|'([^']+)'|([^\s;&|<>]+))/u);
606
+ const executable = executableMatch?.[1] ?? executableMatch?.[2] ?? executableMatch?.[3] ?? "";
607
+ return executable === "node" || executable === "node.exe";
608
+ }
609
+
610
+ function maskJavaScriptCommentsAndTemplates(text) {
611
+ let result = "";
612
+ let state = "code";
613
+ let escaped = false;
614
+ for (let index = 0; index < text.length; index += 1) {
615
+ const character = text[index];
616
+ const next = text[index + 1];
617
+ if (state === "line-comment") {
618
+ if (character === "\n") {
619
+ result += character;
620
+ state = "code";
621
+ } else {
622
+ result += " ";
623
+ }
624
+ continue;
625
+ }
626
+ if (state === "block-comment") {
627
+ if (character === "*" && next === "/") {
628
+ result += " ";
629
+ index += 1;
630
+ state = "code";
631
+ } else {
632
+ result += character === "\n" ? "\n" : " ";
633
+ }
634
+ continue;
635
+ }
636
+ if (state === "template") {
637
+ if (escaped) {
638
+ result += character === "\n" ? "\n" : " ";
639
+ escaped = false;
640
+ } else if (character === "\\") {
641
+ result += " ";
642
+ escaped = true;
643
+ } else if (character === "`") {
644
+ result += " ";
645
+ state = "code";
646
+ } else {
647
+ result += character === "\n" ? "\n" : " ";
648
+ }
649
+ continue;
650
+ }
651
+ if (character === "/" && next === "/") {
652
+ result += " ";
653
+ index += 1;
654
+ state = "line-comment";
655
+ } else if (character === "/" && next === "*") {
656
+ result += " ";
657
+ index += 1;
658
+ state = "block-comment";
659
+ } else if (character === "`") {
660
+ result += " ";
661
+ state = "template";
662
+ } else {
663
+ result += character;
664
+ }
665
+ }
666
+ return result;
667
+ }
668
+
669
+ function isNodeToolingConfigFile(filePath) {
670
+ const fileName = path.posix.basename(filePath).toLowerCase();
671
+ return /\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u.test(fileName);
672
+ }
673
+
674
+ function isNodeRuntimeSourceFile(filePath) {
675
+ const normalizedPath = filePath.replaceAll("\\", "/").toLowerCase();
676
+ if (isNodeToolingConfigFile(normalizedPath)) return false;
677
+ const segments = normalizedPath.split("/");
678
+ const fileName = path.posix.basename(normalizedPath);
679
+ const stem = fileName.slice(0, fileName.lastIndexOf("."));
680
+ return segments.slice(0, -1).some((segment) => NODE_RUNTIME_SOURCE_DIRECTORIES.has(segment))
681
+ || NODE_RUNTIME_ENTRY_NAMES.has(stem)
682
+ || /(?:^|-)(?:server|worker|daemon|service|bootstrap|entry|runtime)(?:-|$)/u.test(stem);
683
+ }
684
+
685
+ function classifyImportSpecifier(specifier) {
686
+ if (/^type\s+[A-Za-z_$][\w$]*(?:\s+as\s+[A-Za-z_$][\w$]*)?$/u.test(specifier)) return "type-only";
687
+ if (/^[A-Za-z_$][\w$]*(?:\s+as\s+[A-Za-z_$][\w$]*)?$/u.test(specifier)) return "runtime";
688
+ return "unknown";
689
+ }
690
+
691
+ function classifyImportExportClause(clause) {
692
+ const trimmed = clause.trim();
693
+ if (trimmed === "" || /^type(?:\s|\{|\*)/u.test(trimmed)) return "type-only";
694
+ const openBrace = trimmed.indexOf("{");
695
+ if (openBrace < 0) return "runtime";
696
+ const closeBrace = trimmed.lastIndexOf("}");
697
+ if (closeBrace < openBrace || trimmed.slice(closeBrace + 1).trim() !== "") return "unknown";
698
+ const prefix = trimmed.slice(0, openBrace).trim();
699
+ if (prefix && !/^[A-Za-z_$][\w$]*,?$/u.test(prefix)) return "unknown";
700
+ const specifiers = splitTopLevel(trimmed.slice(openBrace + 1, closeBrace));
701
+ if (specifiers.length === 0) return "unknown";
702
+ const classifications = specifiers.map((specifier) => classifyImportSpecifier(specifier.trim()));
703
+ if (classifications.includes("unknown")) return "unknown";
704
+ if (prefix || classifications.includes("runtime")) return "runtime";
705
+ return "type-only";
706
+ }
707
+
708
+ function nodeServerBuiltinPattern() {
709
+ return [...NODE_SERVER_BUILTINS]
710
+ .sort((left, right) => right.length - left.length)
711
+ .map((value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"))
712
+ .join("|");
713
+ }
714
+
715
+ function packageImportExportServerBuiltin(source, moduleAlternation) {
716
+ const declarationPattern = new RegExp(
717
+ `^\\s*(?:import|export)\\b([\\s\\S]*?)\\bfrom\\s*["'](${moduleAlternation})["']`,
718
+ "gmu",
719
+ );
720
+ for (const match of source.matchAll(declarationPattern)) {
721
+ const clause = match[1];
722
+ if (clause.includes(";") || /^\s*(?:import|export)\b/m.test(clause)) continue;
723
+ if (classifyImportExportClause(clause) === "runtime") return match[2];
724
+ }
725
+ return null;
726
+ }
727
+
728
+ function packageServerBuiltin(text) {
729
+ if (typeof text !== "string") return null;
730
+ const source = maskJavaScriptCommentsAndTemplates(text);
731
+ const moduleAlternation = nodeServerBuiltinPattern();
732
+ const importedModule = packageImportExportServerBuiltin(source, moduleAlternation);
733
+ if (importedModule) return importedModule;
734
+ const patterns = [
735
+ new RegExp(`^\\s*import\\s*["'](${moduleAlternation})["']`, "mu"),
736
+ new RegExp(`^\\s*(?:const|let|var)\\b[^\\n=]*=\\s*require\\(\\s*["'](${moduleAlternation})["']\\s*\\)`, "mu"),
737
+ new RegExp(`^\\s*require\\(\\s*["'](${moduleAlternation})["']\\s*\\)`, "mu"),
738
+ new RegExp(`^\\s*(?:await\\s+)?import\\(\\s*["'](${moduleAlternation})["']\\s*\\)`, "mu"),
739
+ ];
740
+ for (const pattern of patterns) {
741
+ const match = source.match(pattern);
742
+ if (match?.[1]) return match[1];
743
+ }
744
+ return null;
745
+ }
746
+
747
+ export function parsePackageJson(text) {
748
+ const invalid = invalidNodePackage();
749
+ if (typeof text !== "string" || text.length > MAX_MANIFEST_BYTES || /\r(?!\n)/u.test(text)) return invalid;
750
+
751
+ let value;
752
+ try {
753
+ value = JSON.parse(text);
754
+ } catch {
755
+ return invalid;
756
+ }
757
+ if (!isPlainObject(value)) return invalid;
758
+
759
+ const dependencyFields = ["dependencies", "optionalDependencies", "devDependencies", "peerDependencies"];
760
+ if (dependencyFields.some((field) => value[field] !== undefined && !validStringMap(value[field]))) return invalid;
761
+ if (value.scripts !== undefined && !validStringMap(value.scripts)) return invalid;
762
+ if (value.engines !== undefined && !validStringMap(value.engines)) return invalid;
763
+ if (value.workspaces !== undefined && !packageWorkspaceRoot(value.workspaces)) return invalid;
764
+ if (value.type !== undefined && typeof value.type !== "string") return invalid;
765
+ if (value.packageManager !== undefined && typeof value.packageManager !== "string") return invalid;
766
+
767
+ const backendDependencySignals = ["dependencies", "optionalDependencies"]
768
+ .flatMap((field) => Object.keys(value[field] ?? {})
769
+ .filter((name) => NODE_BACKEND_DEPENDENCIES.has(name))
770
+ .map((name) => `${field}.${name}`));
771
+ const runtimeScripts = Object.entries(value.scripts ?? {})
772
+ .filter(([, command]) => commandStartsWithNode(command))
773
+ .map(([name]) => name)
774
+ .sort((left, right) => left.localeCompare(right));
775
+ const supportingDependencies = ["devDependencies", "peerDependencies"]
776
+ .flatMap((field) => Object.keys(value[field] ?? {}))
777
+ .filter((name) => ["@types/node", "typescript", "tsx"].includes(name));
778
+
779
+ return {
780
+ valid: true,
781
+ backendDependencies: uniqueSorted(backendDependencySignals.map((signal) => signal.split(".").slice(1).join("."))),
782
+ backendDependencySignals: uniqueSorted(backendDependencySignals),
783
+ runtimeScripts,
784
+ serverBuiltins: [],
785
+ supportingDependencies: uniqueSorted(supportingDependencies),
786
+ enginesNode: typeof value.engines?.node === "string",
787
+ moduleType: value.type === "module" || value.type === "commonjs",
788
+ packageManager: typeof value.packageManager === "string",
789
+ workspaceRoot: packageWorkspaceRoot(value.workspaces),
790
+ };
791
+ }
792
+
793
+ function invalidDotNetProject() {
794
+ return {
795
+ valid: false,
796
+ sdkStyle: false,
797
+ projectSdks: [],
798
+ targetFrameworks: [],
799
+ frameworkReferences: [],
800
+ packageReferences: [],
801
+ projectReferences: [],
802
+ dotnet: false,
803
+ aspnetcore: false,
804
+ abp: false,
805
+ };
806
+ }
807
+
808
+ function findXmlTagEnd(text, start) {
809
+ let quote = null;
810
+ for (let index = start; index < text.length; index += 1) {
811
+ const character = text[index];
812
+ if (quote) {
813
+ if (character === quote) quote = null;
814
+ } else if (character === "\"" || character === "'") {
815
+ quote = character;
816
+ } else if (character === ">") {
817
+ return index;
818
+ }
819
+ }
820
+ return -1;
821
+ }
822
+
823
+ function parseXmlAttributes(text) {
824
+ const attributes = {};
825
+ let index = 0;
826
+ while (index < text.length) {
827
+ while (index < text.length && /\s/u.test(text[index])) index += 1;
828
+ if (index >= text.length) break;
829
+ const name = text.slice(index).match(/^[A-Za-z_][A-Za-z0-9_.:-]*/u)?.[0];
830
+ if (!name) return null;
831
+ index += name.length;
832
+ while (index < text.length && /\s/u.test(text[index])) index += 1;
833
+ if (text[index] !== "=") return null;
834
+ index += 1;
835
+ while (index < text.length && /\s/u.test(text[index])) index += 1;
836
+ const quote = text[index];
837
+ if (quote !== "\"" && quote !== "'") return null;
838
+ index += 1;
839
+ const end = text.indexOf(quote, index);
840
+ if (end < 0 || Object.prototype.hasOwnProperty.call(attributes, name)) return null;
841
+ attributes[name] = text.slice(index, end);
842
+ index = end + 1;
843
+ }
844
+ return attributes;
845
+ }
846
+
847
+ function appendXmlText(stack, value) {
848
+ if (value === "") return true;
849
+ const current = stack[stack.length - 1];
850
+ if (!current) return value.trim() === "";
851
+ current.textParts.push(value);
852
+ return true;
853
+ }
854
+
855
+ function consumeXmlSpecialToken(text, tagStart, stack) {
856
+ if (text.startsWith("<!--", tagStart)) {
857
+ const end = text.indexOf("-->", tagStart + 4);
858
+ return end < 0 ? { valid: false } : { valid: true, nextIndex: end + 3 };
859
+ }
860
+ if (text.startsWith("<![CDATA[", tagStart)) {
861
+ const end = text.indexOf("]]>", tagStart + 9);
862
+ if (end < 0 || stack.length === 0) return { valid: false };
863
+ stack[stack.length - 1].textParts.push(text.slice(tagStart + 9, end));
864
+ return { valid: true, nextIndex: end + 3 };
865
+ }
866
+ if (text.startsWith("<?", tagStart)) {
867
+ const end = text.indexOf("?>", tagStart + 2);
868
+ return end < 0 ? { valid: false } : { valid: true, nextIndex: end + 2 };
869
+ }
870
+ if (text.startsWith("<!", tagStart)) return { valid: false };
871
+ return null;
872
+ }
873
+
874
+ function closeXmlNode(raw, state) {
875
+ const closing = raw.slice(1).trim().match(/^([A-Za-z_][A-Za-z0-9_.:-]*)\s*$/u)?.[1];
876
+ const open = state.stack.pop();
877
+ if (!closing || !open || open.name !== closing) return false;
878
+ open.text = open.textParts.join("");
879
+ if (state.stack.length === 0) {
880
+ if (state.rootClosed || open !== state.root) return false;
881
+ state.rootClosed = true;
882
+ }
883
+ return true;
884
+ }
885
+
886
+ function openXmlNode(raw, state) {
887
+ const selfClosing = /\/\s*$/u.test(raw);
888
+ const body = selfClosing ? raw.replace(/\/\s*$/u, "").trimEnd() : raw;
889
+ const name = body.match(/^([A-Za-z_][A-Za-z0-9_.:-]*)/u)?.[1];
890
+ if (!name) return false;
891
+ if (state.stack.length === 0 && (state.root || state.rootClosed)) return false;
892
+ if (!state.root && state.expectedRoot && name !== state.expectedRoot) return false;
893
+ const attributes = parseXmlAttributes(body.slice(name.length).trim());
894
+ if (!attributes) return false;
895
+ const node = { name, attributes, textParts: [], children: [], text: "" };
896
+ state.nodes.push(node);
897
+ if (state.stack.length > 0) state.stack[state.stack.length - 1].children.push(node);
898
+ if (!state.root) state.root = node;
899
+ if (!selfClosing) state.stack.push(node);
900
+ else if (node === state.root) state.rootClosed = true;
901
+ return true;
902
+ }
903
+
904
+ function consumeXmlTag(text, tagStart, state) {
905
+ const tagEnd = findXmlTagEnd(text, tagStart + 1);
906
+ if (tagEnd < 0) return null;
907
+ const raw = text.slice(tagStart + 1, tagEnd).trim();
908
+ if (raw === "") return null;
909
+ const valid = raw.startsWith("/") ? closeXmlNode(raw, state) : openXmlNode(raw, state);
910
+ return { valid, nextIndex: tagEnd + 1 };
911
+ }
912
+
913
+ function parseXmlStructure(text, expectedRoot = null) {
914
+ if (typeof text !== "string" || text.length > MAX_MANIFEST_BYTES || /\r(?!\n)/u.test(text)) return null;
915
+ const state = { nodes: [], stack: [], root: null, rootClosed: false, expectedRoot };
916
+ let index = 0;
917
+ while (index < text.length) {
918
+ const tagStart = text.indexOf("<", index);
919
+ if (tagStart < 0) break;
920
+ if (!appendXmlText(state.stack, text.slice(index, tagStart))) return null;
921
+ const special = consumeXmlSpecialToken(text, tagStart, state.stack);
922
+ if (special) {
923
+ if (!special.valid) return null;
924
+ index = special.nextIndex;
925
+ continue;
926
+ }
927
+ const tag = consumeXmlTag(text, tagStart, state);
928
+ if (!tag?.valid) return null;
929
+ index = tag.nextIndex;
930
+ }
931
+ if (!appendXmlText(state.stack, text.slice(index))) return null;
932
+ if (!state.root || state.stack.length > 0 || !state.rootClosed) return null;
933
+ return { root: state.root, nodes: state.nodes };
934
+ }
935
+
936
+ function normalizeSdkName(value) {
937
+ return typeof value === "string" ? value.trim().split("/")[0] : "";
938
+ }
939
+
940
+ function xmlValues(nodes, name, attribute = "Include") {
941
+ return uniqueSorted(nodes
942
+ .filter((node) => node.name === name && typeof node.attributes?.[attribute] === "string")
943
+ .map((node) => node.attributes[attribute].trim())
944
+ .filter(Boolean));
945
+ }
946
+
947
+ function targetFrameworkValues(nodes) {
948
+ return uniqueSorted(nodes
949
+ .filter((node) => (node.name === "TargetFramework" || node.name === "TargetFrameworks")
950
+ && node.children.length === 0
951
+ && node.text.length <= MAX_XML_TEXT_LENGTH)
952
+ .flatMap((node) => node.text.split(";"))
953
+ .map((value) => value.trim())
954
+ .filter(Boolean));
955
+ }
956
+
957
+ export function parseDotNetProject(text) {
958
+ const invalid = invalidDotNetProject();
959
+ if (typeof text !== "string" || text.length > MAX_MANIFEST_BYTES) return invalid;
960
+ const document = parseXmlStructure(text, "Project");
961
+ if (!document) return invalid;
962
+
963
+ const sdkValues = [
964
+ document.root.attributes.Sdk ?? "",
965
+ ...document.nodes.filter((node) => node.name === "Sdk").map((node) => node.attributes.Name ?? ""),
966
+ ];
967
+ const projectSdks = uniqueSorted(sdkValues
968
+ .flatMap((value) => value.split(";"))
969
+ .map(normalizeSdkName)
970
+ .filter(Boolean));
971
+ const recognizedSdks = projectSdks.filter((sdk) => DOTNET_SDKS.has(sdk));
972
+ const frameworkReferences = xmlValues(document.nodes, "FrameworkReference");
973
+ const packageReferences = xmlValues(document.nodes, "PackageReference");
974
+ const projectReferences = xmlValues(document.nodes, "ProjectReference");
975
+ const dotnet = recognizedSdks.length > 0;
976
+ const aspnetcore = dotnet && (recognizedSdks.some((sdk) => ASPNETCORE_SDKS.has(sdk))
977
+ || frameworkReferences.some((reference) => reference.toLowerCase() === "microsoft.aspnetcore.app"));
978
+ const abp = dotnet && packageReferences.some((reference) => reference.toLowerCase().startsWith("volo.abp."));
979
+ return {
980
+ valid: true,
981
+ sdkStyle: dotnet,
982
+ projectSdks,
983
+ targetFrameworks: targetFrameworkValues(document.nodes),
984
+ frameworkReferences,
985
+ packageReferences,
986
+ projectReferences,
987
+ dotnet,
988
+ aspnetcore,
989
+ abp,
990
+ };
991
+ }
992
+
993
+ function createTraversalBudget(limits = {}) {
994
+ const positiveLimit = (value, fallback) => Number.isInteger(value) && value > 0 ? value : fallback;
995
+ return {
996
+ maxVisitedDirectories: positiveLimit(limits.maxVisitedDirectories, PROJECT_DETECTION_LIMITS.maxVisitedDirectories),
997
+ maxVisitedEntries: positiveLimit(limits.maxVisitedEntries, PROJECT_DETECTION_LIMITS.maxVisitedEntries),
998
+ visitedDirectories: 0,
999
+ visitedEntries: 0,
1000
+ exhausted: false,
1001
+ };
1002
+ }
1003
+
1004
+ function consumeDirectory(budget) {
1005
+ if (budget.exhausted || budget.visitedDirectories >= budget.maxVisitedDirectories) {
1006
+ budget.exhausted = true;
1007
+ return false;
1008
+ }
1009
+ budget.visitedDirectories += 1;
1010
+ return true;
1011
+ }
1012
+
1013
+ function consumeEntry(budget) {
1014
+ if (budget.exhausted || budget.visitedEntries >= budget.maxVisitedEntries) {
1015
+ budget.exhausted = true;
1016
+ return false;
1017
+ }
1018
+ budget.visitedEntries += 1;
1019
+ return true;
1020
+ }
1021
+
1022
+ async function findProjectFiles(root, budget) {
1023
+ const manifests = [];
1024
+ const languageFiles = [];
1025
+ const sourceFiles = [];
1026
+ const solutions = [];
1027
+ const sharedFiles = [];
1028
+
1029
+ function projectFileCapacityAvailable() {
1030
+ return manifests.length + languageFiles.length < MAX_MANIFESTS;
1031
+ }
1032
+
1033
+ function addManifest(entry, absolutePath) {
1034
+ if (!projectFileCapacityAvailable()) return;
1035
+ const extension = path.extname(entry.name).toLowerCase();
1036
+ const kind = entry.name === "pubspec.yaml"
1037
+ ? "flutter"
1038
+ : entry.name === "package.json"
1039
+ ? "nodejs"
1040
+ : entry.name === "Cargo.toml"
1041
+ ? "rust"
1042
+ : DOTNET_PROJECT_EXTENSIONS.has(extension) ? "dotnet" : null;
1043
+ if (kind) manifests.push({ path: absolutePath, kind });
1044
+ }
1045
+
1046
+ function addSolution(entry, absolutePath) {
1047
+ if (solutions.length >= MAX_SOLUTION_FILES) return;
1048
+ const extension = path.extname(entry.name).toLowerCase();
1049
+ if (extension === ".sln" || extension === ".slnx") solutions.push(absolutePath);
1050
+ }
1051
+
1052
+ function addLanguageFiles(entry, absolutePath) {
1053
+ if (!projectFileCapacityAvailable()) return;
1054
+ const kinds = languageProjectKinds(entry.name);
1055
+ if (kinds.length === 0
1056
+ && entry.name.toLowerCase().endsWith(".json")
1057
+ && entry.name.toLowerCase() !== "package.json"
1058
+ && !NON_TYPESCRIPT_JSON_FILES.has(entry.name.toLowerCase())) {
1059
+ kinds.push("typescript-config");
1060
+ }
1061
+ for (const kind of kinds) {
1062
+ if (!projectFileCapacityAvailable()) break;
1063
+ languageFiles.push({
1064
+ path: absolutePath,
1065
+ kind,
1066
+ name: entry.name,
1067
+ relative: portableRelative(root, absolutePath).toLowerCase(),
1068
+ });
1069
+ }
1070
+ }
1071
+
1072
+ function addSourceFile(entry, absolutePath) {
1073
+ if (sourceFiles.length >= MAX_SOURCE_FILES) return;
1074
+ if (languageProjectKinds(entry.name).length > 0) return;
1075
+ const extension = languageSourceExtension(entry.name);
1076
+ if (!extension || isDeclarationFile(entry.name)) return;
1077
+ sourceFiles.push({ path: absolutePath, name: entry.name, extension });
1078
+ }
1079
+
1080
+ function classifyFile(entry, absolutePath) {
1081
+ addManifest(entry, absolutePath);
1082
+ addLanguageFiles(entry, absolutePath);
1083
+ addSourceFile(entry, absolutePath);
1084
+ addSolution(entry, absolutePath);
1085
+ const relative = portableRelative(root, absolutePath);
1086
+ if (isRustSharedFile(relative)) sharedFiles.push({ path: absolutePath, kind: "rust" });
1087
+ if (SHARED_NODE_FILES.has(entry.name.toLowerCase())) sharedFiles.push({ path: absolutePath, kind: "nodejs" });
1088
+ if (SHARED_GRADLE_FILES.has(entry.name.toLowerCase())) sharedFiles.push({ path: absolutePath, kind: "gradle" });
1089
+ }
1090
+
1091
+ async function visit(directory) {
1092
+ if (!projectFileCapacityAvailable() && solutions.length >= MAX_SOLUTION_FILES) return;
1093
+ if (!consumeDirectory(budget)) return;
1094
+ let entries;
1095
+ try {
1096
+ entries = await readdir(directory, { withFileTypes: true });
1097
+ } catch {
1098
+ return;
1099
+ }
1100
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1101
+ for (const entry of entries) {
1102
+ if (!projectFileCapacityAvailable() && solutions.length >= MAX_SOLUTION_FILES) return;
1103
+ if (!consumeEntry(budget)) return;
1104
+ const absolutePath = path.join(directory, entry.name);
1105
+ if (entry.isSymbolicLink()) continue;
1106
+ if (entry.isDirectory()) {
1107
+ if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(absolutePath);
1108
+ continue;
1109
+ }
1110
+ if (!entry.isFile()) continue;
1111
+ classifyFile(entry, absolutePath);
1112
+ }
1113
+ }
1114
+ await visit(root);
1115
+ const sortPaths = (left, right) => portableRelative(root, left.path ?? left).localeCompare(portableRelative(root, right.path ?? right));
1116
+ return {
1117
+ manifests: manifests.sort(sortPaths),
1118
+ languageFiles: languageFiles.sort(sortPaths),
1119
+ sourceFiles: sourceFiles.sort(sortPaths),
1120
+ solutions: solutions.sort(sortPaths),
1121
+ sharedFiles: sharedFiles.sort(sortPaths),
1122
+ };
1123
+ }
1124
+
1125
+ export async function readBounded(filePath, maxBytes) {
1126
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) return null;
1127
+ let handle;
1128
+ try {
1129
+ handle = await open(filePath, "r");
1130
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
1131
+ let bytesRead = 0;
1132
+ while (bytesRead < buffer.length) {
1133
+ const result = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
1134
+ if (result.bytesRead === 0) break;
1135
+ bytesRead += result.bytesRead;
1136
+ }
1137
+ if (bytesRead > maxBytes) return null;
1138
+ return buffer.subarray(0, bytesRead).toString("utf8");
1139
+ } catch {
1140
+ return null;
1141
+ } finally {
1142
+ if (handle) await handle.close().catch(() => {});
1143
+ }
1144
+ }
1145
+
1146
+ async function directDirectoryNames(directory, budget) {
1147
+ if (!consumeDirectory(budget)) return new Set();
1148
+ try {
1149
+ const entries = await readdir(directory, { withFileTypes: true });
1150
+ const directories = new Set();
1151
+ for (const entry of entries) {
1152
+ if (!consumeEntry(budget)) break;
1153
+ if (entry.isDirectory()) directories.add(entry.name);
1154
+ }
1155
+ return directories;
1156
+ } catch {
1157
+ return new Set();
1158
+ }
1159
+ }
1160
+
1161
+ async function findDartFiles(root, budget) {
1162
+ const result = [];
1163
+ async function visit(directory) {
1164
+ if (result.length >= MAX_SOURCE_FILES) return;
1165
+ if (!consumeDirectory(budget)) return;
1166
+ let entries;
1167
+ try {
1168
+ entries = await readdir(directory, { withFileTypes: true });
1169
+ } catch {
1170
+ return;
1171
+ }
1172
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1173
+ for (const entry of entries) {
1174
+ if (result.length >= MAX_SOURCE_FILES) return;
1175
+ if (!consumeEntry(budget)) return;
1176
+ if (entry.isSymbolicLink()) continue;
1177
+ const absolutePath = path.join(directory, entry.name);
1178
+ if (entry.isDirectory()) {
1179
+ if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(absolutePath);
1180
+ } else if (entry.isFile() && entry.name.endsWith(".dart")) {
1181
+ result.push(absolutePath);
1182
+ }
1183
+ }
1184
+ }
1185
+ await visit(root);
1186
+ return result;
1187
+ }
1188
+
1189
+ async function findNodeSourceFiles(root, budget, nestedRoots = []) {
1190
+ const result = [];
1191
+ const boundaries = nestedRoots.map((nestedRoot) => path.resolve(nestedRoot));
1192
+ async function visit(directory) {
1193
+ if (result.length >= MAX_SOURCE_FILES) return;
1194
+ if (boundaries.some((boundary) => directory !== root && (directory === boundary || directory.startsWith(`${boundary}${path.sep}`)))) return;
1195
+ if (!consumeDirectory(budget)) return;
1196
+ let entries;
1197
+ try {
1198
+ entries = await readdir(directory, { withFileTypes: true });
1199
+ } catch {
1200
+ return;
1201
+ }
1202
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1203
+ for (const entry of entries) {
1204
+ if (result.length >= MAX_SOURCE_FILES) return;
1205
+ if (!consumeEntry(budget)) return;
1206
+ if (entry.isSymbolicLink()) continue;
1207
+ const absolutePath = path.join(directory, entry.name);
1208
+ if (entry.isDirectory()) {
1209
+ const directoryName = entry.name.toLowerCase();
1210
+ if (!IGNORED_DIRECTORIES.has(entry.name) && !NODE_NON_RUNTIME_DIRECTORIES.has(directoryName)) {
1211
+ await visit(absolutePath);
1212
+ }
1213
+ } else if (entry.isFile()
1214
+ && !/\.d\.(?:ts|mts|cts)$/iu.test(entry.name)
1215
+ && NODE_SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())
1216
+ && !isNodeToolingConfigFile(entry.name)) {
1217
+ result.push(absolutePath);
1218
+ }
1219
+ }
1220
+ }
1221
+ await visit(root);
1222
+ return result;
1223
+ }
1224
+
1225
+ async function hasFlutterImport(projectRoot, budget) {
1226
+ for (const directory of SOURCE_DIRECTORIES) {
1227
+ const files = await findDartFiles(path.join(projectRoot, directory), budget);
1228
+ for (const filePath of files) {
1229
+ const text = await readBounded(filePath, MAX_SOURCE_BYTES);
1230
+ if (text && /^\s*(?:import|export)\s+["']package:flutter\//mu.test(text)) return true;
1231
+ }
1232
+ }
1233
+ return false;
1234
+ }
1235
+
1236
+ async function findNodeServerImports(projectRoot, targetRoot, budget, nestedRoots = []) {
1237
+ const imports = [];
1238
+ const files = await findNodeSourceFiles(projectRoot, budget, nestedRoots);
1239
+ for (const filePath of files) {
1240
+ const relative = portableRelative(targetRoot, filePath);
1241
+ if (!isNodeRuntimeSourceFile(relative)) continue;
1242
+ const text = await readBounded(filePath, MAX_SOURCE_BYTES);
1243
+ const moduleName = packageServerBuiltin(text);
1244
+ if (!moduleName) continue;
1245
+ imports.push({
1246
+ file: relative,
1247
+ module: moduleName,
1248
+ });
1249
+ }
1250
+ return imports;
1251
+ }
1252
+
1253
+ function normalizeClaim(value) {
1254
+ if (typeof value !== "string" || value.trim() === "") return null;
1255
+ const replaced = value.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
1256
+ if (replaced === "." || replaced === "") return ".";
1257
+ if (replaced.startsWith("../") || replaced.startsWith("/")) return null;
1258
+ return path.posix.normalize(replaced).replace(/^\.\//u, "").toLowerCase();
1259
+ }
1260
+
1261
+ function nestedProjectRoots(projectRoot, projectRoots) {
1262
+ const root = projectRoot.toLowerCase();
1263
+ return projectRoots
1264
+ .map((candidate) => candidate.toLowerCase())
1265
+ .filter((candidate) => candidate !== root
1266
+ && (root === "." ? candidate !== "." : candidate.startsWith(`${root}/`)));
1267
+ }
1268
+
1269
+ function projectPathHasNestedBoundary(candidate, projectRoot, projectRoots) {
1270
+ const normalizedCandidate = candidate.toLowerCase();
1271
+ return nestedProjectRoots(projectRoot, projectRoots)
1272
+ .some((nestedRoot) => normalizedCandidate === nestedRoot
1273
+ || normalizedCandidate.startsWith(`${nestedRoot}/`));
1274
+ }
1275
+
1276
+ function projectOwnsPath(candidate, projectRoot, projectRoots) {
1277
+ const normalizedCandidate = candidate.toLowerCase();
1278
+ const root = projectRoot.toLowerCase();
1279
+ if (projectPathHasNestedBoundary(normalizedCandidate, root, projectRoots)) return false;
1280
+ if (root === ".") return true;
1281
+ return normalizedCandidate === root
1282
+ || normalizedCandidate.startsWith(`${root}/`)
1283
+ || root.startsWith(`${normalizedCandidate}/`);
1284
+ }
1285
+
1286
+ function sharedFileWithinProjectScope(directory, projectRoot, projectRoots) {
1287
+ if (directory === ".") return true;
1288
+ return projectOwnsPath(directory, projectRoot, projectRoots);
1289
+ }
1290
+
1291
+ function claimMatchesProject(claim, project, projectRoots) {
1292
+ if (claim === ".") return true;
1293
+ const root = project.root.toLowerCase();
1294
+ const manifest = project.manifest.toLowerCase();
1295
+ if (claim === manifest || claim === root) return true;
1296
+ return projectOwnsPath(claim, root, projectRoots);
1297
+ }
1298
+
1299
+ function typeScriptProjectConsumesClaim(claim, project, projects, visited = new Set()) {
1300
+ if (project.manifest.toLowerCase() === claim) return true;
1301
+ const identity = project.manifest.toLowerCase();
1302
+ if (visited.has(identity)) return false;
1303
+ visited.add(identity);
1304
+ return (project.internal?.localExtends ?? []).some((reference) => {
1305
+ if (reference === claim) return true;
1306
+ const consumer = projects.find((candidate) => candidate.kind === "typescript"
1307
+ && candidate.manifest.toLowerCase() === reference);
1308
+ return consumer ? typeScriptProjectConsumesClaim(claim, consumer, projects, visited) : false;
1309
+ });
1310
+ }
1311
+
1312
+ function isSharedDotNetClaim(claim) {
1313
+ return SHARED_DOTNET_FILES.has(path.posix.basename(claim).toLowerCase());
1314
+ }
1315
+
1316
+ function claimMatchesSharedDotNetFile(claim, project, projectRoots) {
1317
+ if (!project.dotnet || !isSharedDotNetClaim(claim)) return false;
1318
+ const directory = path.posix.dirname(claim).toLowerCase();
1319
+ return sharedFileWithinProjectScope(directory, project.root, projectRoots);
1320
+ }
1321
+
1322
+ function isSharedNodeClaim(claim) {
1323
+ return SHARED_NODE_FILES.has(path.posix.basename(claim).toLowerCase());
1324
+ }
1325
+
1326
+ function claimMatchesSharedNodeFile(claim, project, projectRoots) {
1327
+ if (!project.nodejs || !isSharedNodeClaim(claim)) return false;
1328
+ const directory = path.posix.dirname(claim).toLowerCase();
1329
+ return sharedFileWithinProjectScope(directory, project.root, projectRoots);
1330
+ }
1331
+
1332
+ function isSharedGradleClaim(claim) {
1333
+ return SHARED_GRADLE_FILES.has(path.posix.basename(claim).toLowerCase());
1334
+ }
1335
+
1336
+ function isGradleBuildProject(project) {
1337
+ return project?.kind === "java" && project.internal?.gradleBuild;
1338
+ }
1339
+
1340
+ function gradlePropertiesWithinScope(claim, project, projects) {
1341
+ if (!isGradleBuildProject(project) || !isSharedGradleClaim(claim)) return false;
1342
+ const directory = path.posix.dirname(claim).toLowerCase();
1343
+ const projectRoot = project.root.toLowerCase();
1344
+ const withinDirectory = directory === "."
1345
+ || projectRoot === directory
1346
+ || projectRoot.startsWith(`${directory}/`);
1347
+ if (!withinDirectory) return false;
1348
+ return !projects.some((candidate) => {
1349
+ if (!candidate.internal?.gradleSettings || !candidate.validManifest) return false;
1350
+ const settingsRoot = candidate.root.toLowerCase();
1351
+ if (settingsRoot === directory || !isStrictRootAncestor(directory, settingsRoot)) return false;
1352
+ return projectRoot === settingsRoot || projectRoot.startsWith(`${settingsRoot}/`);
1353
+ });
1354
+ }
1355
+
1356
+ function claimMatchesSharedGradleFile(claim, project, projects) {
1357
+ return gradlePropertiesWithinScope(claim, project, projects);
1358
+ }
1359
+
1360
+ function claimMatchesGradleSettings(claim, project, projects) {
1361
+ if (!isGradleBuildProject(project)) return false;
1362
+ return projects.some((settingsProject) => {
1363
+ if (settingsProject.kind !== "java"
1364
+ || !settingsProject.validManifest
1365
+ || !settingsProject.internal?.gradleSettings
1366
+ || settingsProject.manifest.toLowerCase() !== claim) return false;
1367
+ if (settingsProject.root.toLowerCase() === project.root.toLowerCase()) return true;
1368
+ return (settingsProject.internal.includes ?? [])
1369
+ .map((include) => normalizedGradleProjectRoot(settingsProject.root, include)?.toLowerCase())
1370
+ .includes(project.root.toLowerCase());
1371
+ });
1372
+ }
1373
+
1374
+ function isRustToolchainFile(relativePath) {
1375
+ return ["rust-toolchain", "rust-toolchain.toml"].includes(path.posix.basename(relativePath).toLowerCase());
1376
+ }
1377
+
1378
+ function sharedRustSignalApplies(relativePath, projectRoot, projectRoots) {
1379
+ if (!isRustSharedFile(relativePath)) return false;
1380
+ const directory = rustSharedScopeDirectory(relativePath).toLowerCase();
1381
+ return sharedFileWithinProjectScope(directory, projectRoot, projectRoots);
1382
+ }
1383
+
1384
+ function hasCloserRustToolchain(claim, projectRoot, sharedFiles, targetRoot) {
1385
+ if (!isRustToolchainFile(claim)) return false;
1386
+ const claimDirectory = rustSharedScopeDirectory(claim).toLowerCase();
1387
+ return sharedFiles.some((sharedFile) => {
1388
+ const relative = portableRelative(targetRoot, sharedFile.path).toLowerCase();
1389
+ if (!isRustToolchainFile(relative) || relative === claim) return false;
1390
+ const otherDirectory = rustSharedScopeDirectory(relative).toLowerCase();
1391
+ return otherDirectory !== claimDirectory
1392
+ && (claimDirectory === "." || otherDirectory.startsWith(`${claimDirectory}/`))
1393
+ && (projectRoot === otherDirectory || projectRoot.startsWith(`${otherDirectory}/`));
1394
+ });
1395
+ }
1396
+
1397
+ function claimMatchesRustWorkspaceManifest(claim, project, projects) {
1398
+ if (path.posix.basename(claim) !== "cargo.toml") return false;
1399
+ const workspace = projects.find((candidate) => candidate.kind === "rust"
1400
+ && candidate.rust
1401
+ && candidate.workspaceRoot
1402
+ && candidate.manifest.toLowerCase() === claim);
1403
+ if (!workspace) return false;
1404
+ return project === workspace || cargoWorkspaceContains(workspace, project, projects);
1405
+ }
1406
+
1407
+ function claimMatchesRustLockfile(claim, project, projects) {
1408
+ const directory = rustSharedScopeDirectory(claim).toLowerCase();
1409
+ const workspace = projects.find((candidate) => candidate.kind === "rust"
1410
+ && candidate.rust
1411
+ && candidate.workspaceRoot
1412
+ && candidate.root.toLowerCase() === directory);
1413
+ if (workspace) return cargoWorkspaceContains(workspace, project, projects);
1414
+ return project.packageRoot && project.root.toLowerCase() === directory;
1415
+ }
1416
+
1417
+ function claimMatchesSharedRustFile(claim, project, projects, projectRoots, sharedFiles, targetRoot) {
1418
+ if (!project.rust || !project.packageRoot || !isRustSharedFile(claim)) return false;
1419
+ if (rustSharedFileKind(claim) === "lockfile") return claimMatchesRustLockfile(claim, project, projects);
1420
+ if (hasCloserRustToolchain(claim, project.root.toLowerCase(), sharedFiles, targetRoot)) return false;
1421
+ const scopeDirectory = rustSharedScopeDirectory(claim).toLowerCase();
1422
+ const workspace = projects.find((candidate) => candidate.kind === "rust"
1423
+ && candidate.rust
1424
+ && candidate.workspaceRoot
1425
+ && candidate.root.toLowerCase() === scopeDirectory);
1426
+ if (workspace) return cargoWorkspaceContains(workspace, project, projects);
1427
+ return sharedRustSignalApplies(claim, project.root, projectRoots);
1428
+ }
1429
+
1430
+ function claimMatchesNodeWorkspaceManifest(claim, project, projects) {
1431
+ if (path.posix.basename(claim) !== "package.json" || project.kind !== "nodejs" || !project.nodejs) return false;
1432
+ const workspace = projects.find((candidate) => candidate.kind === "nodejs"
1433
+ && candidate.manifest.toLowerCase() === claim
1434
+ && candidate.workspaceRoot);
1435
+ if (!workspace) return false;
1436
+ const workspacePrefix = workspace.root === "." ? "" : `${workspace.root}/`;
1437
+ return project.root === workspace.root || project.root.startsWith(workspacePrefix);
1438
+ }
1439
+
1440
+ function normalizedJavaModuleRoot(aggregatorRoot, module) {
1441
+ if (typeof module !== "string" || module.trim() === "") return null;
1442
+ const portable = module.trim().replaceAll("\\", "/");
1443
+ if (portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable)) return null;
1444
+ const normalized = path.posix.normalize(path.posix.join(aggregatorRoot === "." ? "" : aggregatorRoot, portable));
1445
+ if (normalized === ".." || normalized.startsWith("../")) return null;
1446
+ return normalized.replace(/^\.\//u, "") || ".";
1447
+ }
1448
+
1449
+ function claimMatchesJavaAggregator(claim, project, projects) {
1450
+ if (!project?.frameworks?.includes("java")) return false;
1451
+ return projects.some((aggregator) => {
1452
+ if (aggregator === project || aggregator.kind !== "java" || aggregator.manifest.toLowerCase() !== claim) return false;
1453
+ return (aggregator.internal?.modules ?? []).some((module) => {
1454
+ return normalizedJavaModuleRoot(aggregator.root, module)?.toLowerCase() === project.root.toLowerCase();
1455
+ });
1456
+ });
1457
+ }
1458
+
1459
+ function normalizedSolutionMember(relativePath, solutionPath, targetRoot) {
1460
+ if (typeof relativePath !== "string" || relativePath.trim() === "") return null;
1461
+ const portable = relativePath.trim().replaceAll("\\", "/");
1462
+ if (portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable)) return null;
1463
+ const absolute = path.resolve(path.dirname(solutionPath), portable);
1464
+ const member = portableRelative(targetRoot, absolute).toLowerCase();
1465
+ if (member === ".." || member.startsWith("../")) return null;
1466
+ return member;
1467
+ }
1468
+
1469
+ function parseSolutionMembership(text, extension, solutionPath, targetRoot) {
1470
+ if (typeof text !== "string") return null;
1471
+ const members = [];
1472
+ if (extension === ".sln") {
1473
+ const pattern = /^Project\("[^\r\n]*"\)\s*=\s*"[^"\r\n]*",\s*"([^"\r\n]+)"\s*,\s*"[^"\r\n]*"\s*$/u;
1474
+ for (const line of text.split(/\r?\n/u)) {
1475
+ if (!/^Project\(/u.test(line.trim())) continue;
1476
+ const match = line.trim().match(pattern);
1477
+ if (!match) return null;
1478
+ if (!/(?:\.csproj|\.fsproj|\.vbproj)$/iu.test(match[1])) continue;
1479
+ const member = normalizedSolutionMember(match[1], solutionPath, targetRoot);
1480
+ if (!member) return null;
1481
+ members.push(member);
1482
+ }
1483
+ return uniqueSorted(members);
1484
+ }
1485
+
1486
+ const document = parseXmlStructure(text, "Solution");
1487
+ if (!document) return null;
1488
+ for (const node of document.nodes) {
1489
+ const projectPath = node.attributes.Path ?? node.attributes.path;
1490
+ if (node.name !== "Project") continue;
1491
+ if (typeof projectPath !== "string" || !/(?:\.csproj|\.fsproj|\.vbproj)$/iu.test(projectPath)) return null;
1492
+ const member = normalizedSolutionMember(projectPath, solutionPath, targetRoot);
1493
+ if (!member) return null;
1494
+ members.push(member);
1495
+ }
1496
+ return uniqueSorted(members);
1497
+ }
1498
+
1499
+ function sharedNodeSignalApplies(sharedFile, projectRoot, projectRoots) {
1500
+ const sharedDirectory = path.posix.dirname(sharedFile).toLowerCase();
1501
+ return sharedFileWithinProjectScope(sharedDirectory, projectRoot, projectRoots);
1502
+ }
1503
+
1504
+ function inspectDotNetProject({ projectRoot, manifestRelative, manifestText }) {
1505
+ const parsed = parseDotNetProject(manifestText ?? "");
1506
+ const primarySignals = parsed.dotnet
1507
+ ? parsed.projectSdks
1508
+ .filter((sdk) => DOTNET_SDKS.has(sdk))
1509
+ .map((sdk) => `${manifestRelative}:project.sdk=${sdk}`)
1510
+ : [];
1511
+ const supportingSignals = [];
1512
+ for (const targetFramework of parsed.targetFrameworks) supportingSignals.push(`${manifestRelative}:targetFramework=${targetFramework}`);
1513
+ for (const reference of parsed.frameworkReferences) supportingSignals.push(`${manifestRelative}:frameworkReference=${reference}`);
1514
+ for (const reference of parsed.packageReferences) supportingSignals.push(`${manifestRelative}:packageReference=${reference}`);
1515
+ for (const reference of parsed.projectReferences) supportingSignals.push(`${manifestRelative}:projectReference=${reference}`);
1516
+ const frameworks = [];
1517
+ if (parsed.dotnet) frameworks.push("dotnet");
1518
+ if (parsed.aspnetcore) frameworks.push("aspnetcore");
1519
+ if (parsed.abp) frameworks.push("abp");
1520
+ return {
1521
+ kind: "dotnet",
1522
+ root: projectRoot,
1523
+ manifest: manifestRelative,
1524
+ validManifest: parsed.valid,
1525
+ primary: parsed.dotnet,
1526
+ dotnet: parsed.dotnet,
1527
+ frameworks,
1528
+ primarySignals,
1529
+ supportingSignals,
1530
+ };
1531
+ }
1532
+
1533
+ function inspectRustProject({ projectRoot, manifestRelative, manifestText, targetRoot, projectRoots, sharedFiles }) {
1534
+ const parsed = parseCargoManifest(manifestText ?? "");
1535
+ const primarySignals = [];
1536
+ if (parsed.package.present) primarySignals.push(`${manifestRelative}:package`);
1537
+ if (parsed.workspace.present) primarySignals.push(`${manifestRelative}:workspace`);
1538
+
1539
+ const supportingSignals = [];
1540
+ if (parsed.package.edition) supportingSignals.push(`${manifestRelative}:edition=${parsed.package.edition}`);
1541
+ if (parsed.package.editionInherited) supportingSignals.push(`${manifestRelative}:edition=workspace`);
1542
+ if (parsed.package.rustVersion) supportingSignals.push(`${manifestRelative}:rust-version=${parsed.package.rustVersion}`);
1543
+ if (parsed.package.rustVersionInherited) supportingSignals.push(`${manifestRelative}:rust-version=workspace`);
1544
+ if (parsed.workspace.resolver) supportingSignals.push(`${manifestRelative}:resolver=${parsed.workspace.resolver}`);
1545
+ if (parsed.workspace.package?.edition) {
1546
+ supportingSignals.push(`${manifestRelative}:workspace.package.edition=${parsed.workspace.package.edition}`);
1547
+ }
1548
+ if (parsed.workspace.package?.rustVersion) {
1549
+ supportingSignals.push(`${manifestRelative}:workspace.package.rust-version=${parsed.workspace.package.rustVersion}`);
1550
+ }
1551
+ for (const dependency of uniqueSorted([...parsed.dependencies, ...parsed.workspaceDependencies])) {
1552
+ supportingSignals.push(`${manifestRelative}:dependency=${dependency}`);
1553
+ }
1554
+ for (const dependency of parsed.devDependencies) supportingSignals.push(`${manifestRelative}:devDependency=${dependency}`);
1555
+ for (const dependency of parsed.buildDependencies) supportingSignals.push(`${manifestRelative}:buildDependency=${dependency}`);
1556
+ for (const dependency of parsed.targetDependencies) supportingSignals.push(`${manifestRelative}:targetDependency=${dependency}`);
1557
+ for (const feature of parsed.features) supportingSignals.push(`${manifestRelative}:feature=${feature}`);
1558
+
1559
+ for (const sharedFile of sharedFiles) {
1560
+ const relative = portableRelative(targetRoot, sharedFile.path);
1561
+ if (!parsed.rust || !parsed.package.present || !sharedRustSignalApplies(relative, projectRoot, projectRoots)) continue;
1562
+ const kind = rustSharedFileKind(relative);
1563
+ supportingSignals.push(`${relative}:${kind === "lockfile" ? "workspace-shared" : "rust-scope"}`);
1564
+ }
1565
+
1566
+ return {
1567
+ kind: "rust",
1568
+ root: projectRoot,
1569
+ manifest: manifestRelative,
1570
+ validManifest: parsed.valid,
1571
+ primary: parsed.rust,
1572
+ rust: parsed.rust,
1573
+ packageRoot: parsed.package.present,
1574
+ workspaceRoot: parsed.workspace.present,
1575
+ virtualWorkspace: parsed.workspace.present && !parsed.package.present,
1576
+ frameworks: parsed.rust ? ["rust"] : [],
1577
+ primarySignals,
1578
+ supportingSignals,
1579
+ internal: parsed,
1580
+ };
1581
+ }
1582
+
1583
+ function invalidateRustVirtualWorkspace(project) {
1584
+ project.rust = false;
1585
+ project.primary = false;
1586
+ project.frameworks = [];
1587
+ project.primarySignals = [];
1588
+ project.supportingSignals = [];
1589
+ }
1590
+
1591
+ function resolveRustWorkspaceEvidence(projects) {
1592
+ const workspaces = projects
1593
+ .filter((project) => project.kind === "rust" && project.virtualWorkspace)
1594
+ .sort((left, right) => right.root.split("/").length - left.root.split("/").length);
1595
+ for (const workspace of workspaces) {
1596
+ if (!workspace.rust) continue;
1597
+ const hasConfirmedMember = projects.some((candidate) => candidate !== workspace
1598
+ && cargoWorkspaceContains(workspace, candidate, projects));
1599
+ if (!hasConfirmedMember) invalidateRustVirtualWorkspace(workspace);
1600
+ }
1601
+ }
1602
+
1603
+ async function inspectNodeProject({
1604
+ projectRootPath,
1605
+ projectRoot,
1606
+ manifestRelative,
1607
+ manifestText,
1608
+ targetRoot,
1609
+ budget,
1610
+ projectRoots,
1611
+ sharedFiles,
1612
+ includeSourceEvidence = true,
1613
+ }) {
1614
+ const parsed = parsePackageJson(manifestText ?? "");
1615
+ const nestedRoots = nestedProjectRoots(projectRoot, projectRoots)
1616
+ .map((candidate) => path.resolve(targetRoot, candidate));
1617
+ const primarySignals = [];
1618
+ for (const dependency of parsed.backendDependencySignals) primarySignals.push(`${manifestRelative}:${dependency}`);
1619
+ for (const script of parsed.runtimeScripts) primarySignals.push(`${manifestRelative}:scripts.${script}=node`);
1620
+
1621
+ const serverImports = parsed.valid && includeSourceEvidence && primarySignals.length === 0
1622
+ ? await findNodeServerImports(projectRootPath, targetRoot, budget, nestedRoots)
1623
+ : [];
1624
+ for (const serverImport of serverImports) {
1625
+ primarySignals.push(`${serverImport.file}:import=${serverImport.module}`);
1626
+ }
1627
+
1628
+ const supportingSignals = [];
1629
+ if (parsed.enginesNode) supportingSignals.push(`${manifestRelative}:engines.node`);
1630
+ if (parsed.moduleType) supportingSignals.push(`${manifestRelative}:type`);
1631
+ if (parsed.packageManager) supportingSignals.push(`${manifestRelative}:packageManager`);
1632
+ if (parsed.workspaceRoot) supportingSignals.push(`${manifestRelative}:workspaces`);
1633
+ for (const dependency of parsed.supportingDependencies) supportingSignals.push(`${manifestRelative}:supportingDependency=${dependency}`);
1634
+ for (const sharedFile of sharedFiles) {
1635
+ const relative = portableRelative(targetRoot, sharedFile.path);
1636
+ if (sharedNodeSignalApplies(relative, projectRoot, projectRoots)) {
1637
+ supportingSignals.push(`${relative}:node-scope`);
1638
+ }
1639
+ }
1640
+
1641
+ const primary = parsed.valid && primarySignals.length > 0;
1642
+ return {
1643
+ kind: "nodejs",
1644
+ root: projectRoot,
1645
+ manifest: manifestRelative,
1646
+ validManifest: parsed.valid,
1647
+ primary,
1648
+ nodejs: primary,
1649
+ workspaceRoot: parsed.workspaceRoot,
1650
+ frameworks: primary ? ["nodejs"] : [],
1651
+ primarySignals,
1652
+ supportingSignals,
1653
+ };
1654
+ }
1655
+
1656
+ function inspectGoLanguageProject(context) {
1657
+ return inspectGoProject(context);
1658
+ }
1659
+
1660
+ function inspectTypeScriptLanguageProject(context) {
1661
+ return inspectTypeScriptProject(context);
1662
+ }
1663
+
1664
+ function inspectSqlLanguageProject(context) {
1665
+ const parsed = parseSqlProject(context.manifestText);
1666
+ return {
1667
+ kind: "sql",
1668
+ root: context.projectRoot,
1669
+ manifest: context.manifestRelative,
1670
+ validManifest: parsed.valid,
1671
+ primary: parsed.database,
1672
+ frameworks: parsed.database ? ["sql"] : [],
1673
+ primarySignals: parsed.database ? [`${context.manifestRelative}:database-project`] : [],
1674
+ supportingSignals: [],
1675
+ internal: parsed,
1676
+ };
1677
+ }
1678
+
1679
+ const LANGUAGE_PROJECT_INSPECTORS = new Map([
1680
+ ["go", inspectGoLanguageProject],
1681
+ ["go-work", inspectGoLanguageProject],
1682
+ ["typescript", inspectTypeScriptLanguageProject],
1683
+ ["javascript-config", inspectTypeScriptLanguageProject],
1684
+ ["typescript-config", inspectTypeScriptLanguageProject],
1685
+ ["php", inspectPhpProject],
1686
+ ["java-maven", inspectJavaProject],
1687
+ ["java-gradle", inspectJavaProject],
1688
+ ["java-gradle-settings", inspectJavaProject],
1689
+ ["java-gradle-properties", inspectJavaProject],
1690
+ ["java-bazel", inspectJavaProject],
1691
+ ["c-cpp-cmake", inspectNativeProject],
1692
+ ["c-cpp-meson", inspectNativeProject],
1693
+ ["c-cpp-meson-options", inspectNativeProject],
1694
+ ["c-cpp-make", inspectNativeProject],
1695
+ ["native-bazel", inspectNativeProject],
1696
+ ["swift-package", inspectSwiftProject],
1697
+ ["swift-xcode", inspectSwiftProject],
1698
+ ["swift-cmake", inspectSwiftProject],
1699
+ ["swift-meson", inspectSwiftProject],
1700
+ ["sql-project", inspectSqlLanguageProject],
1701
+ ]);
1702
+
1703
+ function inspectLanguageProject(
1704
+ languageFile,
1705
+ targetRoot,
1706
+ projectRoots,
1707
+ sourceFiles,
1708
+ languageFiles,
1709
+ claims = [],
1710
+ activeConfigRelatives = null,
1711
+ ownershipConfigRelatives = null,
1712
+ ) {
1713
+ const projectRootPath = languageFile.kind === "swift-xcode"
1714
+ ? path.dirname(path.dirname(languageFile.path))
1715
+ : path.dirname(languageFile.path);
1716
+ const manifestRelative = languagePortablePath(targetRoot, languageFile.path);
1717
+ const context = {
1718
+ projectRoot: languagePortablePath(targetRoot, projectRootPath),
1719
+ manifestRelative,
1720
+ manifestText: languageFile.text,
1721
+ manifestName: languageFile.name.toLowerCase(),
1722
+ targetRoot,
1723
+ projectRoots,
1724
+ sourceFiles,
1725
+ projectFiles: languageFiles,
1726
+ directClaim: claims.includes(manifestRelative.toLowerCase()),
1727
+ configKind: languageFile.kind,
1728
+ activeConfigRelatives,
1729
+ ownershipConfigRelatives,
1730
+ };
1731
+ return LANGUAGE_PROJECT_INSPECTORS.get(languageFile.kind)?.(context) ?? null;
1732
+ }
1733
+
1734
+ function directSourceFrameworks(sourceFile, text) {
1735
+ if (!sourceFile || typeof text !== "string") return [];
1736
+ if (SQL_SOURCE_EXTENSIONS.has(sourceFile.extension)) return looksLikeSql(text) ? ["sql"] : [];
1737
+ if (PHP_SOURCE_EXTENSIONS.has(sourceFile.extension)) return phpSourceLooksExecutable(text) ? ["php"] : [];
1738
+ if (JAVA_SOURCE_EXTENSIONS.has(sourceFile.extension)) return ["java"];
1739
+ if (SWIFT_SOURCE_EXTENSIONS.has(sourceFile.extension)) return swiftSourceLooksExecutable(text) ? ["swift"] : [];
1740
+ const nativeFramework = nativeFrameworkForSource(sourceFile.name);
1741
+ if (nativeFramework && (C_SOURCE_EXTENSIONS.has(sourceFile.extension) || CPP_SOURCE_EXTENSIONS.has(sourceFile.extension))) {
1742
+ return [nativeFramework];
1743
+ }
1744
+ return [];
1745
+ }
1746
+
1747
+ function sourceProjectRoot(claim, projectRoots) {
1748
+ const candidates = projectRoots
1749
+ .filter((root) => root === "." || claim === root || claim.startsWith(`${root}/`))
1750
+ .sort((left, right) => right.length - left.length);
1751
+ return candidates.find((root) => !projectPathHasNestedBoundary(claim, root, projectRoots)) ?? null;
1752
+ }
1753
+
1754
+ function isDiscoveredSqlArtifact(relativePath) {
1755
+ const segments = relativePath.split("/");
1756
+ if (segments.some((segment) => SQL_NON_RUNTIME_DIRECTORIES.has(segment))) return false;
1757
+ return segments.slice(0, -1).some((segment) => SQL_RUNTIME_DIRECTORIES.has(segment));
1758
+ }
1759
+
1760
+ function attachSourceFramework(project, relative, framework) {
1761
+ if (!project.frameworks.includes(framework)) project.frameworks.push(framework);
1762
+ project.primary = true;
1763
+ project.primarySignals.push(relative + ":source=" + framework);
1764
+ }
1765
+
1766
+ function createDirectSourceProject(relative, frameworks, root = null) {
1767
+ return {
1768
+ kind: "direct-source",
1769
+ root,
1770
+ manifest: relative,
1771
+ validManifest: true,
1772
+ primary: true,
1773
+ frameworks,
1774
+ primarySignals: [relative + ":source=" + frameworks.join(",")],
1775
+ supportingSignals: [],
1776
+ internal: { direct: true },
1777
+ };
1778
+ }
1779
+
1780
+ function directSourceProjectForClaim({ relative, frameworks, projects, projectRoots }) {
1781
+ const hostRoot = sourceProjectRoot(relative, projectRoots);
1782
+ const host = hostRoot ? projects.find((project) => project.root.toLowerCase() === hostRoot.toLowerCase()) : null;
1783
+ if (host) {
1784
+ for (const framework of frameworks) attachSourceFramework(host, relative, framework);
1785
+ return null;
1786
+ }
1787
+ return createDirectSourceProject(relative, frameworks);
1788
+ }
1789
+
1790
+ function sqlProjectRoot(relative, projectRoots) {
1791
+ return sourceProjectRoot(relative, projectRoots) ?? path.posix.dirname(relative);
1792
+ }
1793
+
1794
+ async function addDiscoveredSqlProjects({ targetRoot, sourceFiles, projects, projectRoots }) {
1795
+ const directProjects = [];
1796
+ for (const sourceFile of sourceFiles) {
1797
+ if (!SQL_SOURCE_EXTENSIONS.has(sourceFile.extension)) continue;
1798
+ const relative = languagePortablePath(targetRoot, sourceFile.path).toLowerCase();
1799
+ if (!isDiscoveredSqlArtifact(relative)) continue;
1800
+ const text = await readBounded(sourceFile.path, MAX_SOURCE_BYTES);
1801
+ if (!looksLikeSql(text)) continue;
1802
+ const root = sqlProjectRoot(relative, projectRoots);
1803
+ const host = projects.find((project) => project.root.toLowerCase() === root.toLowerCase());
1804
+ if (host) {
1805
+ attachSourceFramework(host, relative, "sql");
1806
+ } else {
1807
+ directProjects.push(createDirectSourceProject(relative, ["sql"], root));
1808
+ }
1809
+ }
1810
+ return directProjects;
1811
+ }
1812
+
1813
+ async function addDirectSourceProjects({ targetRoot, claims, sourceFiles, projects, projectRoots }) {
1814
+ const directProjects = [];
1815
+ const handled = new Set();
1816
+ for (const claim of claims) {
1817
+ const claimedFiles = sourceFiles.filter((file) => {
1818
+ const relative = languagePortablePath(targetRoot, file.path).toLowerCase();
1819
+ return relative === claim || (SQL_SOURCE_EXTENSIONS.has(file.extension) && relative.startsWith(`${claim}/`));
1820
+ });
1821
+ for (const sourceFile of claimedFiles) {
1822
+ const relativeLower = languagePortablePath(targetRoot, sourceFile.path).toLowerCase();
1823
+ if (handled.has(relativeLower)) continue;
1824
+ handled.add(relativeLower);
1825
+ if (relativeLower !== claim && (!SQL_SOURCE_EXTENSIONS.has(sourceFile.extension)
1826
+ || !isDiscoveredSqlArtifact(relativeLower))) continue;
1827
+ const text = await readBounded(sourceFile.path, MAX_SOURCE_BYTES);
1828
+ const frameworks = directSourceFrameworks(sourceFile, text);
1829
+ if (frameworks.length === 0) continue;
1830
+ const relative = languagePortablePath(targetRoot, sourceFile.path);
1831
+ const directProject = directSourceProjectForClaim({ relative, frameworks, projects, projectRoots });
1832
+ if (directProject) directProjects.push(directProject);
1833
+ }
1834
+ }
1835
+ return directProjects;
1836
+ }
1837
+
1838
+ async function inspectFlutterProject({ projectRootPath, projectRoot, manifestRelative, manifestText, budget, includeSupportingEvidence = true }) {
1839
+ const parsed = parsePubspec(manifestText ?? "");
1840
+ const primarySignals = parsed.primary ? [`${manifestRelative}:dependencies.flutter.sdk`] : [];
1841
+ const supportingSignals = [];
1842
+ if (!includeSupportingEvidence) {
1843
+ return {
1844
+ kind: "flutter",
1845
+ root: projectRoot,
1846
+ manifest: manifestRelative,
1847
+ validManifest: parsed.valid,
1848
+ primary: parsed.primary,
1849
+ nodejs: false,
1850
+ dotnet: false,
1851
+ frameworks: parsed.primary ? ["flutter"] : [],
1852
+ primarySignals,
1853
+ supportingSignals,
1854
+ };
1855
+ }
1856
+ if (parsed.flutterTest) supportingSignals.push(`${manifestRelative}:dev_dependencies.flutter_test.sdk`);
1857
+ if (parsed.override) supportingSignals.push(`${manifestRelative}:dependency_overrides.flutter.sdk`);
1858
+
1859
+ const metadataText = await readBounded(path.join(projectRootPath, ".metadata"), MAX_SOURCE_BYTES);
1860
+ if (metadataText && /^project_type:\s*[^\s#]+/mu.test(metadataText)) {
1861
+ supportingSignals.push(`${projectRoot === "." ? ".metadata" : `${projectRoot}/.metadata`}:project_type`);
1862
+ }
1863
+
1864
+ const directories = await directDirectoryNames(projectRootPath, budget);
1865
+ for (const platform of PLATFORM_DIRECTORIES) {
1866
+ if (directories.has(platform)) supportingSignals.push(`${projectRoot === "." ? platform : `${projectRoot}/${platform}`}:directory`);
1867
+ }
1868
+ if (await hasFlutterImport(projectRootPath, budget)) {
1869
+ supportingSignals.push(`${projectRoot === "." ? "source" : `${projectRoot}/source`}:package:flutter`);
1870
+ }
1871
+
1872
+ return {
1873
+ kind: "flutter",
1874
+ root: projectRoot,
1875
+ manifest: manifestRelative,
1876
+ validManifest: parsed.valid,
1877
+ primary: parsed.primary,
1878
+ nodejs: false,
1879
+ dotnet: false,
1880
+ frameworks: parsed.primary ? ["flutter"] : [],
1881
+ primarySignals,
1882
+ supportingSignals,
1883
+ };
1884
+ }
1885
+
1886
+ async function inspectProject(
1887
+ manifestInfo,
1888
+ targetRoot,
1889
+ budget,
1890
+ projectRoots,
1891
+ sharedFiles,
1892
+ includeSupportingEvidence = true,
1893
+ ) {
1894
+ const manifestPath = manifestInfo.path;
1895
+ const projectRootPath = path.dirname(manifestPath);
1896
+ const context = {
1897
+ projectRootPath,
1898
+ projectRoot: portableRelative(targetRoot, projectRootPath),
1899
+ manifestRelative: portableRelative(targetRoot, manifestPath),
1900
+ manifestText: manifestInfo.text ?? await readBounded(manifestPath, MAX_MANIFEST_BYTES),
1901
+ };
1902
+
1903
+ if (manifestInfo.kind === "dotnet") return inspectDotNetProject(context);
1904
+ if (manifestInfo.kind === "rust") {
1905
+ return inspectRustProject({
1906
+ ...context,
1907
+ targetRoot,
1908
+ projectRoots,
1909
+ sharedFiles,
1910
+ });
1911
+ }
1912
+ if (manifestInfo.kind === "nodejs") {
1913
+ return inspectNodeProject({
1914
+ ...context,
1915
+ targetRoot,
1916
+ budget,
1917
+ projectRoots,
1918
+ sharedFiles,
1919
+ includeSourceEvidence: includeSupportingEvidence,
1920
+ });
1921
+ }
1922
+ return inspectFlutterProject({ ...context, budget, includeSupportingEvidence });
1923
+ }
1924
+
1925
+ function isSelectableProject(project) {
1926
+ if (isAuxiliaryNativeProject(project)
1927
+ || project.internal?.gradleSettings
1928
+ || project.internal?.gradleProperties) return project.primary;
1929
+ if (project.kind === "typescript") return project.primary;
1930
+ if (project.kind === "rust" && !project.rust) return false;
1931
+ return project.primary || project.validManifest;
1932
+ }
1933
+
1934
+ async function readLanguageFiles(entries, budget) {
1935
+ const languageFiles = entries.map((entry) => ({ ...entry, text: null }));
1936
+ for (const languageFile of languageFiles) {
1937
+ languageFile.text = await readBounded(languageFile.path, MAX_MANIFEST_BYTES);
1938
+ if (budget.exhausted) return null;
1939
+ }
1940
+ return languageFiles;
1941
+ }
1942
+
1943
+ async function readManifestFiles(entries, budget) {
1944
+ const manifests = entries.map((entry) => ({ ...entry, text: null }));
1945
+ for (const manifest of manifests) {
1946
+ manifest.text = await readBounded(manifest.path, MAX_MANIFEST_BYTES);
1947
+ if (budget.exhausted) return null;
1948
+ }
1949
+ return manifests;
1950
+ }
1951
+
1952
+ async function inspectDiscoveredProjects({
1953
+ targetRoot,
1954
+ manifests,
1955
+ languageFiles,
1956
+ sourceFiles,
1957
+ projectRoots,
1958
+ claims,
1959
+ activeConfigRelatives,
1960
+ ownershipConfigRelatives,
1961
+ sharedFiles,
1962
+ budget,
1963
+ includeSupportingEvidence = true,
1964
+ }) {
1965
+ const projects = [];
1966
+ for (const manifestInfo of manifests) {
1967
+ projects.push(await inspectProject(
1968
+ manifestInfo,
1969
+ targetRoot,
1970
+ budget,
1971
+ projectRoots,
1972
+ sharedFiles,
1973
+ includeSupportingEvidence,
1974
+ ));
1975
+ if (budget.exhausted) return null;
1976
+ }
1977
+ for (const languageFile of languageFiles) {
1978
+ const project = inspectLanguageProject(
1979
+ languageFile,
1980
+ targetRoot,
1981
+ projectRoots,
1982
+ sourceFiles,
1983
+ languageFiles,
1984
+ claims,
1985
+ activeConfigRelatives,
1986
+ ownershipConfigRelatives,
1987
+ );
1988
+ if (project) projects.push(project);
1989
+ if (budget.exhausted) return null;
1990
+ }
1991
+ return projects;
1992
+ }
1993
+
1994
+ function isStrictRootAncestor(parent, candidate) {
1995
+ return parent !== candidate && (parent === "." ? candidate !== "." : candidate.startsWith(`${parent}/`));
1996
+ }
1997
+
1998
+ function normalizedGradleProjectRoot(aggregatorRoot, include) {
1999
+ if (typeof include !== "string" || include.trim() === "") return null;
2000
+ const portable = include.trim().replaceAll("\\", "/").replace(/^:/u, "").replaceAll(":", "/");
2001
+ if (portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable)) return null;
2002
+ const normalized = path.posix.normalize(path.posix.join(aggregatorRoot === "." ? "" : aggregatorRoot, portable));
2003
+ if (normalized === ".." || normalized.startsWith("../")) return null;
2004
+ return normalized.replace(/^\.\//u, "") || ".";
2005
+ }
2006
+
2007
+ function hasKnownGradleChild(settingsProject, projects) {
2008
+ const includes = settingsProject.internal?.includes ?? [];
2009
+ const includedRoots = includes
2010
+ .map((include) => normalizedGradleProjectRoot(settingsProject.root, include))
2011
+ .filter(Boolean)
2012
+ .map((root) => root.toLowerCase());
2013
+ return projects.some((candidate) => candidate !== settingsProject
2014
+ && candidate.internal?.gradleBuild
2015
+ && candidate.primary
2016
+ && candidate.validManifest
2017
+ && (candidate.root.toLowerCase() === settingsProject.root.toLowerCase()
2018
+ || includedRoots.includes(candidate.root.toLowerCase())));
2019
+ }
2020
+
2021
+ function isAuxiliaryNativeProject(project) {
2022
+ const name = path.posix.basename(project.manifest ?? "").toLowerCase();
2023
+ return project.kind === "native" && ["makefile", "gnumakefile", "meson_options.txt"].includes(name);
2024
+ }
2025
+
2026
+ function isConfirmedProjectCandidate(project, projects) {
2027
+ if (!project?.root || project.root === "..") return false;
2028
+ if (project.internal?.gradleProperties) return false;
2029
+ if (project.internal?.gradleSettings) return project.validManifest && hasKnownGradleChild(project, projects);
2030
+ if (project.kind === "typescript") return project.primary && project.validManifest;
2031
+ if (project.kind === "rust") return Boolean(project.rust);
2032
+ if (isAuxiliaryNativeProject(project)) return false;
2033
+ if (project.internal?.gradleBuild) return project.primary && project.validManifest;
2034
+ if (project.kind === "native") return Boolean(project.internal?.valid);
2035
+ if (project.kind === "sql") return Boolean(project.internal?.database);
2036
+ return project.validManifest;
2037
+ }
2038
+
2039
+ function nativeSourceExistsUnderRoot(project, sourceFiles, targetRoot, boundaryRoots) {
2040
+ return sourceFiles.some((file) => {
2041
+ if (!C_SOURCE_EXTENSIONS.has(file.extension) && !CPP_SOURCE_EXTENSIONS.has(file.extension)) return false;
2042
+ const relative = portableRelative(targetRoot, file.path).toLowerCase();
2043
+ const root = project.root.toLowerCase();
2044
+ const underRoot = root === "." || relative === root || relative.startsWith(`${root}/`);
2045
+ return underRoot && !projectPathHasNestedBoundary(relative, root, boundaryRoots);
2046
+ });
2047
+ }
2048
+
2049
+ function hasConfirmedNativeAncestor(projects, root) {
2050
+ return projects.some((ancestor) => {
2051
+ const ancestorRoot = ancestor.root?.toLowerCase();
2052
+ return ancestorRoot && isStrictRootAncestor(ancestorRoot, root)
2053
+ && ancestor.kind === "native"
2054
+ && !isAuxiliaryNativeProject(ancestor)
2055
+ && ancestor.internal?.valid;
2056
+ });
2057
+ }
2058
+
2059
+ function confirmedProjectRoots(projects, sourceFiles, targetRoot) {
2060
+ const roots = new Set(projects
2061
+ .filter((project) => isConfirmedProjectCandidate(project, projects))
2062
+ .map((project) => project.root.toLowerCase()));
2063
+ for (const project of projects) {
2064
+ if (!isAuxiliaryNativeProject(project) || roots.has(project.root.toLowerCase())) continue;
2065
+ if (hasConfirmedNativeAncestor(projects, project.root.toLowerCase())) continue;
2066
+ if (nativeSourceExistsUnderRoot(project, sourceFiles, targetRoot, [...roots])) roots.add(project.root.toLowerCase());
2067
+ }
2068
+ return uniqueSorted([...roots]);
2069
+ }
2070
+
2071
+ function demoteNestedAuxiliaryProjects(projects, projectRoots, sourceFiles, targetRoot) {
2072
+ for (const project of projects) {
2073
+ if (!isAuxiliaryNativeProject(project)) continue;
2074
+ const root = project.root.toLowerCase();
2075
+ if (!projectRoots.some((candidate) => isStrictRootAncestor(candidate, root))) continue;
2076
+ const independentSource = nativeSourceExistsUnderRoot(project, sourceFiles, targetRoot, projectRoots);
2077
+ if (independentSource && !hasConfirmedNativeAncestor(projects, root)) continue;
2078
+ project.primary = false;
2079
+ project.frameworks = [];
2080
+ project.primarySignals = [];
2081
+ }
2082
+ }
2083
+
2084
+ async function readSolutionMembership(targetRoot, solutions) {
2085
+ const membership = new Map();
2086
+ for (const solutionPath of solutions) {
2087
+ const solutionRelative = portableRelative(targetRoot, solutionPath).toLowerCase();
2088
+ const text = await readBounded(solutionPath, MAX_MANIFEST_BYTES);
2089
+ membership.set(
2090
+ solutionRelative,
2091
+ parseSolutionMembership(text, path.extname(solutionPath).toLowerCase(), solutionPath, targetRoot) ?? [],
2092
+ );
2093
+ }
2094
+ return membership;
2095
+ }
2096
+
2097
+ function projectMatchesClaim(claim, project, { solutionClaims, solutionMembership, projectRoots, projects, sharedFiles, targetRoot }) {
2098
+ if (!project.root) return false;
2099
+ if (project.kind === "typescript") {
2100
+ const knownConfigClaim = isTypeScriptConfigName(path.posix.basename(claim))
2101
+ || projects.some((candidate) => candidate.kind === "typescript"
2102
+ && candidate.manifest.toLowerCase() === claim);
2103
+ if (knownConfigClaim) {
2104
+ return typeScriptProjectConsumesClaim(claim, project, projects);
2105
+ }
2106
+ }
2107
+ if (solutionClaims.includes(claim)) {
2108
+ return project.dotnet && solutionMembership.get(claim).includes(project.manifest.toLowerCase());
2109
+ }
2110
+ if (isSharedDotNetClaim(claim)) return claimMatchesSharedDotNetFile(claim, project, projectRoots);
2111
+ if (isSharedNodeClaim(claim)) return claimMatchesSharedNodeFile(claim, project, projectRoots);
2112
+ if (isSharedGradleClaim(claim)) return claimMatchesSharedGradleFile(claim, project, projects);
2113
+ if (isRustSharedFile(claim)) {
2114
+ return claimMatchesSharedRustFile(claim, project, projects, projectRoots, sharedFiles, targetRoot);
2115
+ }
2116
+ return claimMatchesProject(claim, project, projectRoots)
2117
+ || claimMatchesNodeWorkspaceManifest(claim, project, projects)
2118
+ || claimMatchesGradleSettings(claim, project, projects)
2119
+ || claimMatchesJavaAggregator(claim, project, projects)
2120
+ || claimMatchesRustWorkspaceManifest(claim, project, projects);
2121
+ }
2122
+
2123
+ function selectProjects(projects, claims, hasClaims, solutionMembership, projectRoots, sharedFiles, targetRoot) {
2124
+ const selectableProjects = projects.filter(isSelectableProject);
2125
+ if (!hasClaims) return selectableProjects;
2126
+ const solutionClaims = claims.filter((claim) => solutionMembership.has(claim));
2127
+ const context = { solutionClaims, solutionMembership, projectRoots, projects, sharedFiles, targetRoot };
2128
+ return selectableProjects.filter((project) => claims.some((claim) => projectMatchesClaim(claim, project, context)));
2129
+ }
2130
+
2131
+ export async function detectProjectEvidence(target, { claims = [], limits = {} } = {}) {
2132
+ const targetRoot = path.resolve(target);
2133
+ const budget = createTraversalBudget(limits);
2134
+ const discovered = await findProjectFiles(targetRoot, budget);
2135
+ if (budget.exhausted) return null;
2136
+ const projectFiles = [
2137
+ ...discovered.manifests,
2138
+ ...discovered.languageFiles,
2139
+ ];
2140
+ const normalizedClaims = uniqueSorted((Array.isArray(claims) ? claims : []).map(normalizeClaim).filter(Boolean));
2141
+ const hasClaims = Array.isArray(claims) && claims.length > 0;
2142
+ if (projectFiles.length === 0 && discovered.sourceFiles.length === 0 && !hasClaims) return null;
2143
+ const languageFiles = await readLanguageFiles(discovered.languageFiles, budget);
2144
+ if (!languageFiles) return null;
2145
+ const activeConfigRelatives = resolveTypeScriptConfigGraph(languageFiles, normalizedClaims);
2146
+ const manifestFiles = await readManifestFiles(discovered.manifests, budget);
2147
+ if (!manifestFiles) return null;
2148
+ const ownershipConfigRelatives = resolveTypeScriptConfigOwnershipRoots(
2149
+ languageFiles,
2150
+ activeConfigRelatives,
2151
+ normalizedClaims,
2152
+ );
2153
+ const preliminaryProjects = await inspectDiscoveredProjects({
2154
+ targetRoot,
2155
+ manifests: manifestFiles,
2156
+ languageFiles,
2157
+ sourceFiles: [],
2158
+ projectRoots: [],
2159
+ claims: normalizedClaims,
2160
+ activeConfigRelatives,
2161
+ ownershipConfigRelatives,
2162
+ sharedFiles: discovered.sharedFiles,
2163
+ budget,
2164
+ includeSupportingEvidence: false,
2165
+ });
2166
+ if (!preliminaryProjects) return null;
2167
+ resolveRustWorkspaceEvidence(preliminaryProjects);
2168
+ const projectRoots = confirmedProjectRoots(preliminaryProjects, discovered.sourceFiles, targetRoot);
2169
+ const projects = await inspectDiscoveredProjects({
2170
+ targetRoot,
2171
+ manifests: manifestFiles,
2172
+ languageFiles,
2173
+ sourceFiles: discovered.sourceFiles,
2174
+ projectRoots,
2175
+ claims: normalizedClaims,
2176
+ activeConfigRelatives,
2177
+ ownershipConfigRelatives,
2178
+ sharedFiles: discovered.sharedFiles,
2179
+ budget,
2180
+ });
2181
+ if (!projects) return null;
2182
+ demoteNestedAuxiliaryProjects(projects, projectRoots, discovered.sourceFiles, targetRoot);
2183
+ if (!hasClaims) {
2184
+ projects.push(...await addDiscoveredSqlProjects({
2185
+ targetRoot,
2186
+ sourceFiles: discovered.sourceFiles,
2187
+ projects,
2188
+ projectRoots,
2189
+ }));
2190
+ }
2191
+ resolveRustWorkspaceEvidence(projects);
2192
+
2193
+ const solutionMembership = await readSolutionMembership(targetRoot, discovered.solutions);
2194
+ let selectedProjects = selectProjects(
2195
+ projects,
2196
+ normalizedClaims,
2197
+ hasClaims,
2198
+ solutionMembership,
2199
+ projectRoots,
2200
+ discovered.sharedFiles,
2201
+ targetRoot,
2202
+ );
2203
+ if (hasClaims) {
2204
+ const directProjects = await addDirectSourceProjects({
2205
+ targetRoot,
2206
+ claims: normalizedClaims,
2207
+ sourceFiles: discovered.sourceFiles,
2208
+ projects,
2209
+ projectRoots,
2210
+ });
2211
+ selectedProjects = [...selectedProjects, ...directProjects];
2212
+ }
2213
+ if (projects.length === 0 && selectedProjects.length === 0) return null;
2214
+ const scope = hasClaims
2215
+ ? (selectedProjects.length > 0 ? "MATCH" : "NO_MATCH")
2216
+ : "UNSCOPED";
2217
+ return {
2218
+ schemaVersion: PROJECT_EVIDENCE_SCHEMA_VERSION,
2219
+ scope,
2220
+ frameworks: uniqueSorted(selectedProjects.flatMap((project) => project.frameworks)),
2221
+ projectRoots: uniqueSorted(selectedProjects
2222
+ .map((project) => project.virtualWorkspace ? null : project.root)
2223
+ .filter(Boolean)),
2224
+ primarySignals: uniqueSorted(selectedProjects.flatMap((project) => project.primarySignals)),
2225
+ supportingSignals: uniqueSorted(selectedProjects.flatMap((project) => project.supportingSignals)),
2226
+ };
2227
+ }