@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.
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +298 -3
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/next.js +19 -7
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/config/guides.json +44 -0
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-presets.js +82 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +281 -3
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
import {
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
495
|
-
projects.push(await inspectProject(
|
|
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
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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.
|
|
515
|
-
projectRoots: uniqueSorted(selectedProjects
|
|
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
|
};
|