@cassiomc1/forgeloop 1.12.0 → 1.14.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 (249) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/AGENT_COMPATIBILITY.md +8 -0
  4. package/CLAUDE.md +1 -1
  5. package/CONTRIBUTING.md +90 -0
  6. package/DOCS_INDEX.md +46 -12
  7. package/ENG/c-development-eng.md +112 -0
  8. package/ENG/cpp-development-eng.md +109 -0
  9. package/ENG/dotnet-aspnetcore-development-eng.md +401 -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/sec-code-eng.md +7 -7
  16. package/ENG/sql-development-eng.md +108 -0
  17. package/ENG/swift-development-eng.md +111 -0
  18. package/ENG/typescript-development-eng.md +108 -0
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +418 -9
  21. package/LOOP_ENGINEERING.md +28 -2
  22. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  23. package/PROTOCOL_INTEGRATION.md +55 -2
  24. package/QUALITY_SCORECARD.md +1 -0
  25. package/README.md +78 -52
  26. package/TERMINOLOGY.md +2 -0
  27. package/THIRD_PARTY_NOTICES.md +19 -7
  28. package/THREAT_MODEL.md +140 -1
  29. package/completions/_forgeloop +22 -4
  30. package/completions/forgeloop.bash +40 -4
  31. package/completions/forgeloop.fish +130 -1
  32. package/docs/ADVISORY_CONTEXT.md +25 -0
  33. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  34. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  35. package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
  36. package/docs/AGENT_SKILL.md +66 -0
  37. package/docs/ARTIFACT_REFERENCE.md +123 -0
  38. package/docs/AUDIT_UX.md +46 -0
  39. package/docs/BROWSER_VERIFICATION.md +136 -0
  40. package/docs/CLI_REFERENCE.md +392 -10
  41. package/docs/CODE_ATTESTATION.md +2 -2
  42. package/docs/DOCUMENTATION_GUIDE.md +34 -12
  43. package/docs/GETTING_STARTED.md +59 -0
  44. package/docs/JEV_BENCHMARKS.md +31 -0
  45. package/docs/MODEL_ROUTING.md +37 -0
  46. package/docs/OPENSRC_ADAPTER.md +241 -0
  47. package/docs/PACKAGE_CONTENTS.md +60 -19
  48. package/docs/PROVIDERS.md +126 -0
  49. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  50. package/docs/RECIPES.md +32 -0
  51. package/docs/RELEASE_CHECKLIST.md +66 -5
  52. package/docs/SECURITY_REVIEW.md +71 -0
  53. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  54. package/docs/TEST_INTELLIGENCE.md +29 -0
  55. package/docs/TEST_PRUNING.md +14 -0
  56. package/docs/TROUBLESHOOTING.md +298 -3
  57. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  58. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  59. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  61. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  62. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  64. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  65. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  67. package/docs/diagrams/README.md +13 -9
  68. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  69. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  70. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  71. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  72. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  73. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  74. package/docs/documentation-manifest.json +1397 -0
  75. package/docs/protocol-requirements.json +101 -0
  76. package/package.json +46 -4
  77. package/schemas/config.schema.json +14 -0
  78. package/schemas/context-plan.schema.json +18 -0
  79. package/schemas/routing-input.schema.json +1 -1
  80. package/schemas/semantic-decision.schema.json +46 -0
  81. package/schemas/test-utility.schema.json +44 -0
  82. package/scripts/CI_VALIDATORS.md +84 -11
  83. package/scripts/benchmark-jev.mjs +5 -0
  84. package/scripts/benchmark-test-intelligence.mjs +4 -0
  85. package/scripts/generate-agent-protocol-summary.mjs +40 -1
  86. package/scripts/generate-forgeloop-skill.mjs +133 -0
  87. package/scripts/jev-smoke.mjs +19 -0
  88. package/skills/forgeloop/README.md +9 -0
  89. package/skills/forgeloop/SKILL.md +77 -0
  90. package/skills/forgeloop/references/lifecycle.md +9 -0
  91. package/skills/forgeloop/references/recovery.md +7 -0
  92. package/skills/forgeloop/references/verification.md +7 -0
  93. package/src/adapters/agent-browser/assertions.js +47 -0
  94. package/src/adapters/agent-browser/commands.js +54 -0
  95. package/src/adapters/agent-browser/index.js +3 -0
  96. package/src/adapters/agent-browser/locator.js +40 -0
  97. package/src/adapters/agent-browser/process.js +215 -0
  98. package/src/adapters/agent-browser/provider.js +313 -0
  99. package/src/adapters/emulated-services/constants.js +24 -0
  100. package/src/adapters/emulated-services/index.js +7 -0
  101. package/src/adapters/emulated-services/process.js +162 -0
  102. package/src/adapters/emulated-services/provider.js +282 -0
  103. package/src/adapters/opensrc/normalize.js +90 -0
  104. package/src/adapters/opensrc/process.js +248 -0
  105. package/src/adapters/opensrc/provider.js +338 -0
  106. package/src/adapters/opensrc/search.js +264 -0
  107. package/src/adapters/typesafe/client.js +28 -0
  108. package/src/adapters/typesafe/engine.js +63 -0
  109. package/src/adapters/typesafe/normalize.js +41 -0
  110. package/src/cli.js +108 -0
  111. package/src/commands/checkpoint-revalidate.js +176 -0
  112. package/src/commands/context-plan.js +38 -0
  113. package/src/commands/contract-create.js +264 -0
  114. package/src/commands/contract-revise.js +236 -0
  115. package/src/commands/decision-show.js +14 -0
  116. package/src/commands/decision-status.js +22 -0
  117. package/src/commands/discover.js +41 -0
  118. package/src/commands/doctor.js +15 -0
  119. package/src/commands/gate-record.js +205 -0
  120. package/src/commands/gate-revalidate.js +137 -0
  121. package/src/commands/model-route.js +32 -0
  122. package/src/commands/next.js +19 -7
  123. package/src/commands/route.js +146 -18
  124. package/src/commands/semantic-plan.js +17 -0
  125. package/src/commands/task-abandon.js +224 -0
  126. package/src/commands/task-create.js +84 -25
  127. package/src/commands/task-list.js +22 -2
  128. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  129. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  130. package/src/commands/test-inventory.js +5 -0
  131. package/src/commands/test-prune-plan.js +5 -0
  132. package/src/commands/test-prune-probe.js +5 -0
  133. package/src/commands/test-utility.js +5 -0
  134. package/src/commands/validate-protocol.js +10 -1
  135. package/src/config/guides.json +44 -0
  136. package/src/core/artifact-registry.js +24 -0
  137. package/src/core/audit-ux.js +514 -0
  138. package/src/core/browser-verification/constants.js +149 -0
  139. package/src/core/browser-verification/normalize.js +254 -0
  140. package/src/core/browser-verification/provider.js +519 -0
  141. package/src/core/browser-verification/service.js +115 -0
  142. package/src/core/build-script.js +151 -0
  143. package/src/core/c-cpp-project.js +143 -0
  144. package/src/core/checkpoint-revalidation.js +319 -0
  145. package/src/core/cli-command-definitions.js +249 -1
  146. package/src/core/command-executors.js +115 -3
  147. package/src/core/command-input.js +212 -102
  148. package/src/core/completion-artifacts.js +14 -5
  149. package/src/core/completion.js +4 -6
  150. package/src/core/config.js +3 -0
  151. package/src/core/context-compiler/budget.js +9 -0
  152. package/src/core/context-compiler/candidates.js +39 -0
  153. package/src/core/context-compiler/compiler.js +63 -0
  154. package/src/core/context-compiler/fingerprint.js +11 -0
  155. package/src/core/context-compiler/policy.js +13 -0
  156. package/src/core/context-compiler/result.js +23 -0
  157. package/src/core/contract-bootstrap-recovery.js +655 -0
  158. package/src/core/contract-presets.js +82 -0
  159. package/src/core/contract-revision.js +210 -0
  160. package/src/core/decision/artifact.js +69 -0
  161. package/src/core/decision/benchmarks.js +103 -0
  162. package/src/core/decision/cache.js +27 -0
  163. package/src/core/decision/constants.js +58 -0
  164. package/src/core/decision/cutover.js +34 -0
  165. package/src/core/decision/engine.js +22 -0
  166. package/src/core/decision/errors.js +68 -0
  167. package/src/core/decision/events.js +101 -0
  168. package/src/core/decision/freshness.js +19 -0
  169. package/src/core/decision/normalizers/index.js +115 -0
  170. package/src/core/decision/policy.js +18 -0
  171. package/src/core/decision/projection.js +16 -0
  172. package/src/core/decision/question-registry.js +201 -0
  173. package/src/core/decision/request.js +26 -0
  174. package/src/core/decision/resolver.js +130 -0
  175. package/src/core/decision/result.js +58 -0
  176. package/src/core/decision/service.js +156 -0
  177. package/src/core/decision/state-builder.js +65 -0
  178. package/src/core/decision/task-bindings.js +30 -0
  179. package/src/core/decision/test-provider.js +32 -0
  180. package/src/core/decision/thresholds.js +15 -0
  181. package/src/core/error-codes.js +281 -3
  182. package/src/core/events.js +226 -57
  183. package/src/core/evidence-readiness.js +9 -0
  184. package/src/core/execution-prerequisites.js +14 -0
  185. package/src/core/execution-profile.js +63 -38
  186. package/src/core/filesystem.js +1 -10
  187. package/src/core/gate-provenance.js +124 -0
  188. package/src/core/go-project.js +206 -0
  189. package/src/core/integration-invocation-policy.js +27 -4
  190. package/src/core/integration-resources.js +86 -61
  191. package/src/core/java-project.js +403 -0
  192. package/src/core/model-router/constants.js +10 -0
  193. package/src/core/model-router/policy.js +103 -0
  194. package/src/core/model-router/router.js +37 -0
  195. package/src/core/multi-language-project.js +117 -0
  196. package/src/core/next-action-model.js +58 -0
  197. package/src/core/next-action-phases.js +130 -42
  198. package/src/core/next-action-refresh.js +43 -9
  199. package/src/core/next-action-review-phase.js +7 -2
  200. package/src/core/next-action.js +35 -7
  201. package/src/core/next-explanation.js +63 -0
  202. package/src/core/phase.js +128 -10
  203. package/src/core/php-project.js +85 -0
  204. package/src/core/preflight-consistency.js +23 -9
  205. package/src/core/preflight-loaders.js +37 -5
  206. package/src/core/project-detection.js +1760 -52
  207. package/src/core/protocol-info.js +65 -0
  208. package/src/core/protocol.js +20 -0
  209. package/src/core/reconcile-closure.js +132 -53
  210. package/src/core/recovery-history.js +1 -0
  211. package/src/core/resumability.js +154 -44
  212. package/src/core/route-artifact.js +15 -1
  213. package/src/core/router.js +223 -4
  214. package/src/core/runtime-context.js +118 -61
  215. package/src/core/rust-project.js +400 -0
  216. package/src/core/schema-validation.js +3 -0
  217. package/src/core/security-review/constants.js +64 -0
  218. package/src/core/security-review/normalize.js +245 -0
  219. package/src/core/security-review/provider.js +204 -0
  220. package/src/core/security-review/service.js +134 -0
  221. package/src/core/semantic-planning/constants.js +19 -0
  222. package/src/core/semantic-planning/projection.js +94 -0
  223. package/src/core/semantic-planning/service.js +15 -0
  224. package/src/core/sources.js +37 -0
  225. package/src/core/sql-project.js +141 -0
  226. package/src/core/swift-project.js +200 -0
  227. package/src/core/task-claim-state.js +201 -1
  228. package/src/core/task-conflict-inspection.js +31 -5
  229. package/src/core/task-paths.js +13 -0
  230. package/src/core/task-recovery.js +1 -0
  231. package/src/core/templates.js +3 -0
  232. package/src/core/test-intelligence/benchmarks.js +68 -0
  233. package/src/core/test-intelligence/inventory.js +73 -0
  234. package/src/core/test-intelligence/prune.js +90 -0
  235. package/src/core/test-intelligence/semantic-state.js +15 -0
  236. package/src/core/test-intelligence/service.js +40 -0
  237. package/src/core/test-intelligence/utility.js +50 -0
  238. package/src/core/trace.js +11 -7
  239. package/src/core/transaction.js +1 -0
  240. package/src/core/typescript-project.js +349 -0
  241. package/src/core/xml-structure.js +123 -0
  242. package/src/integration.d.ts +492 -0
  243. package/src/integration.js +54 -0
  244. package/src/providers/README.md +47 -0
  245. package/src/providers/capabilities.js +46 -0
  246. package/src/providers/errors.js +15 -0
  247. package/src/providers/index.js +29 -0
  248. package/src/providers/json-snapshot.js +105 -0
  249. package/src/providers/registry.js +152 -0
@@ -1,6 +1,38 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { open, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
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
+
4
36
  export const PROJECT_EVIDENCE_SCHEMA_VERSION = 1;
5
37
 
6
38
  export const PROJECT_EVIDENCE_SCOPES = Object.freeze([
@@ -10,10 +42,21 @@ export const PROJECT_EVIDENCE_SCOPES = Object.freeze([
10
42
  "NONE",
11
43
  ]);
12
44
 
13
- const MAX_MANIFESTS = 256;
14
- const MAX_MANIFEST_BYTES = 1024 * 1024;
15
- const MAX_SOURCE_FILES = 256;
16
- const MAX_SOURCE_BYTES = 512 * 1024;
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;
17
60
  const UNSUPPORTED_YAML_VALUE = Symbol("unsupported-yaml-value");
18
61
  const IGNORED_DIRECTORIES = new Set([
19
62
  ".dart_tool",
@@ -21,13 +64,170 @@ const IGNORED_DIRECTORIES = new Set([
21
64
  ".git",
22
65
  ".idea",
23
66
  ".vscode",
67
+ "artifacts",
68
+ "bin",
24
69
  "build",
25
70
  "coverage",
71
+ "DerivedData",
72
+ "deriveddata",
26
73
  "dist",
74
+ "external",
75
+ "generated",
76
+ "gen",
77
+ ".gradle",
27
78
  "node_modules",
79
+ "obj",
28
80
  "out",
81
+ "target",
82
+ "TestResults",
29
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",
30
229
  ]);
230
+ const SHARED_GRADLE_FILES = new Set(["gradle.properties"]);
31
231
  const PLATFORM_DIRECTORIES = Object.freeze([
32
232
  "android",
33
233
  "ios",
@@ -349,10 +549,548 @@ export function parsePubspec(text) {
349
549
  };
350
550
  }
351
551
 
352
- async function findPubspecFiles(root) {
353
- const result = [];
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
+
354
1091
  async function visit(directory) {
355
- if (result.length >= MAX_MANIFESTS) return;
1092
+ if (!projectFileCapacityAvailable() && solutions.length >= MAX_SOLUTION_FILES) return;
1093
+ if (!consumeDirectory(budget)) return;
356
1094
  let entries;
357
1095
  try {
358
1096
  entries = await readdir(directory, { withFileTypes: true });
@@ -361,43 +1099,70 @@ async function findPubspecFiles(root) {
361
1099
  }
362
1100
  entries.sort((left, right) => left.name.localeCompare(right.name));
363
1101
  for (const entry of entries) {
364
- if (result.length >= MAX_MANIFESTS) return;
1102
+ if (!projectFileCapacityAvailable() && solutions.length >= MAX_SOLUTION_FILES) return;
1103
+ if (!consumeEntry(budget)) return;
365
1104
  const absolutePath = path.join(directory, entry.name);
366
1105
  if (entry.isSymbolicLink()) continue;
367
1106
  if (entry.isDirectory()) {
368
1107
  if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(absolutePath);
369
- } else if (entry.isFile() && entry.name === "pubspec.yaml") {
370
- result.push(absolutePath);
1108
+ continue;
371
1109
  }
1110
+ if (!entry.isFile()) continue;
1111
+ classifyFile(entry, absolutePath);
372
1112
  }
373
1113
  }
374
1114
  await visit(root);
375
- return result.sort((left, right) => portableRelative(root, left).localeCompare(portableRelative(root, right)));
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
+ };
376
1123
  }
377
1124
 
378
- async function readBounded(filePath, maxBytes) {
1125
+ export async function readBounded(filePath, maxBytes) {
1126
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) return null;
1127
+ let handle;
379
1128
  try {
380
- const bytes = await readFile(filePath);
381
- if (bytes.length > maxBytes) return null;
382
- return bytes.toString("utf8");
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");
383
1139
  } catch {
384
1140
  return null;
1141
+ } finally {
1142
+ if (handle) await handle.close().catch(() => {});
385
1143
  }
386
1144
  }
387
1145
 
388
- async function directDirectoryNames(directory) {
1146
+ async function directDirectoryNames(directory, budget) {
1147
+ if (!consumeDirectory(budget)) return new Set();
389
1148
  try {
390
1149
  const entries = await readdir(directory, { withFileTypes: true });
391
- return new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
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;
392
1156
  } catch {
393
1157
  return new Set();
394
1158
  }
395
1159
  }
396
1160
 
397
- async function findDartFiles(root) {
1161
+ async function findDartFiles(root, budget) {
398
1162
  const result = [];
399
1163
  async function visit(directory) {
400
1164
  if (result.length >= MAX_SOURCE_FILES) return;
1165
+ if (!consumeDirectory(budget)) return;
401
1166
  let entries;
402
1167
  try {
403
1168
  entries = await readdir(directory, { withFileTypes: true });
@@ -407,6 +1172,7 @@ async function findDartFiles(root) {
407
1172
  entries.sort((left, right) => left.name.localeCompare(right.name));
408
1173
  for (const entry of entries) {
409
1174
  if (result.length >= MAX_SOURCE_FILES) return;
1175
+ if (!consumeEntry(budget)) return;
410
1176
  if (entry.isSymbolicLink()) continue;
411
1177
  const absolutePath = path.join(directory, entry.name);
412
1178
  if (entry.isDirectory()) {
@@ -420,9 +1186,45 @@ async function findDartFiles(root) {
420
1186
  return result;
421
1187
  }
422
1188
 
423
- async function hasFlutterImport(projectRoot) {
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) {
424
1226
  for (const directory of SOURCE_DIRECTORIES) {
425
- const files = await findDartFiles(path.join(projectRoot, directory));
1227
+ const files = await findDartFiles(path.join(projectRoot, directory), budget);
426
1228
  for (const filePath of files) {
427
1229
  const text = await readBounded(filePath, MAX_SOURCE_BYTES);
428
1230
  if (text && /^\s*(?:import|export)\s+["']package:flutter\//mu.test(text)) return true;
@@ -431,6 +1233,23 @@ async function hasFlutterImport(projectRoot) {
431
1233
  return false;
432
1234
  }
433
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
+
434
1253
  function normalizeClaim(value) {
435
1254
  if (typeof value !== "string" || value.trim() === "") return null;
436
1255
  const replaced = value.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
@@ -439,27 +1258,601 @@ function normalizeClaim(value) {
439
1258
  return path.posix.normalize(replaced).replace(/^\.\//u, "").toLowerCase();
440
1259
  }
441
1260
 
442
- function claimMatchesProject(claim, projectRoot, projectRoots) {
443
- if (claim === ".") return true;
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();
444
1278
  const root = projectRoot.toLowerCase();
445
- if (root === ".") {
446
- const nestedRoots = projectRoots
447
- .map((candidate) => candidate.toLowerCase())
448
- .filter((candidate) => candidate !== ".");
449
- // A root project owns claims outside confirmed nested project boundaries.
450
- return !nestedRoots.some((candidate) => claim === candidate || claim.startsWith(`${candidate}/`));
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);
451
1484
  }
452
- return claim === root || claim.startsWith(`${root}/`) || root.startsWith(`${claim}/`);
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);
453
1497
  }
454
1498
 
455
- async function inspectProject(root, manifestPath, targetRoot) {
456
- const projectRootPath = path.dirname(manifestPath);
457
- const projectRoot = portableRelative(targetRoot, projectRootPath);
458
- const manifestRelative = portableRelative(targetRoot, manifestPath);
459
- const manifestText = await readBounded(manifestPath, MAX_MANIFEST_BYTES);
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 }) {
460
1839
  const parsed = parsePubspec(manifestText ?? "");
461
1840
  const primarySignals = parsed.primary ? [`${manifestRelative}:dependencies.flutter.sdk`] : [];
462
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
+ }
463
1856
  if (parsed.flutterTest) supportingSignals.push(`${manifestRelative}:dev_dependencies.flutter_test.sdk`);
464
1857
  if (parsed.override) supportingSignals.push(`${manifestRelative}:dependency_overrides.flutter.sdk`);
465
1858
 
@@ -468,51 +1861,366 @@ async function inspectProject(root, manifestPath, targetRoot) {
468
1861
  supportingSignals.push(`${projectRoot === "." ? ".metadata" : `${projectRoot}/.metadata`}:project_type`);
469
1862
  }
470
1863
 
471
- const directories = await directDirectoryNames(projectRootPath);
1864
+ const directories = await directDirectoryNames(projectRootPath, budget);
472
1865
  for (const platform of PLATFORM_DIRECTORIES) {
473
1866
  if (directories.has(platform)) supportingSignals.push(`${projectRoot === "." ? platform : `${projectRoot}/${platform}`}:directory`);
474
1867
  }
475
- if (await hasFlutterImport(projectRootPath)) {
1868
+ if (await hasFlutterImport(projectRootPath, budget)) {
476
1869
  supportingSignals.push(`${projectRoot === "." ? "source" : `${projectRoot}/source`}:package:flutter`);
477
1870
  }
478
1871
 
479
1872
  return {
1873
+ kind: "flutter",
480
1874
  root: projectRoot,
481
1875
  manifest: manifestRelative,
482
1876
  validManifest: parsed.valid,
483
1877
  primary: parsed.primary,
1878
+ nodejs: false,
1879
+ dotnet: false,
1880
+ frameworks: parsed.primary ? ["flutter"] : [],
484
1881
  primarySignals,
485
1882
  supportingSignals,
486
1883
  };
487
1884
  }
488
1885
 
489
- export async function detectProjectEvidence(target, { claims = [] } = {}) {
490
- const targetRoot = path.resolve(target);
491
- const manifestPaths = await findPubspecFiles(targetRoot);
492
- if (manifestPaths.length === 0) return null;
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
+ }) {
493
1965
  const projects = [];
494
- for (const manifestPath of manifestPaths) {
495
- projects.push(await inspectProject(targetRoot, manifestPath, targetRoot));
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;
496
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
+ }
497
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
+ ];
498
2140
  const normalizedClaims = uniqueSorted((Array.isArray(claims) ? claims : []).map(normalizeClaim).filter(Boolean));
499
2141
  const hasClaims = Array.isArray(claims) && claims.length > 0;
500
- const projectRoots = projects.map((project) => project.root);
501
- const selectedProjects = hasClaims
502
- ? projects.filter((project) => normalizedClaims.some((claim) => claimMatchesProject(
503
- claim,
504
- project.root,
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,
505
2209
  projectRoots,
506
- )))
507
- : projects;
2210
+ });
2211
+ selectedProjects = [...selectedProjects, ...directProjects];
2212
+ }
2213
+ if (projects.length === 0 && selectedProjects.length === 0) return null;
508
2214
  const scope = hasClaims
509
2215
  ? (selectedProjects.length > 0 ? "MATCH" : "NO_MATCH")
510
2216
  : "UNSCOPED";
511
2217
  return {
512
2218
  schemaVersion: PROJECT_EVIDENCE_SCHEMA_VERSION,
513
2219
  scope,
514
- frameworks: selectedProjects.some((project) => project.primary) ? ["flutter"] : [],
515
- projectRoots: uniqueSorted(selectedProjects.map((project) => project.root)),
2220
+ frameworks: uniqueSorted(selectedProjects.flatMap((project) => project.frameworks)),
2221
+ projectRoots: uniqueSorted(selectedProjects
2222
+ .map((project) => project.virtualWorkspace ? null : project.root)
2223
+ .filter(Boolean)),
516
2224
  primarySignals: uniqueSorted(selectedProjects.flatMap((project) => project.primarySignals)),
517
2225
  supportingSignals: uniqueSorted(selectedProjects.flatMap((project) => project.supportingSignals)),
518
2226
  };