@cassiomc1/forgeloop 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +13 -11
- 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/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/GUIDE_ROUTER.md +418 -9
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +44 -33
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/completions/_forgeloop +3 -3
- package/completions/forgeloop.bash +3 -3
- package/completions/forgeloop.fish +7 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
- package/docs/CLI_REFERENCE.md +28 -6
- package/docs/DOCUMENTATION_GUIDE.md +2 -1
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/PACKAGE_CONTENTS.md +28 -14
- package/docs/RECIPES.md +23 -0
- package/docs/RELEASE_CHECKLIST.md +30 -2
- package/docs/TROUBLESHOOTING.md +100 -2
- package/docs/documentation-manifest.json +652 -0
- package/docs/protocol-requirements.json +77 -0
- package/package.json +19 -4
- package/schemas/routing-input.schema.json +1 -1
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/generate-agent-protocol-summary.mjs +36 -0
- package/src/commands/next.js +19 -7
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/config/guides.json +44 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/cli-command-definitions.js +8 -1
- package/src/core/command-executors.js +5 -3
- package/src/core/command-input.js +140 -102
- package/src/core/contract-presets.js +82 -0
- package/src/core/error-codes.js +3 -3
- package/src/core/filesystem.js +1 -10
- package/src/core/go-project.js +206 -0
- package/src/core/java-project.js +403 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-explanation.js +63 -0
- package/src/core/php-project.js +85 -0
- package/src/core/project-detection.js +1760 -52
- package/src/core/reconcile-closure.js +4 -1
- package/src/core/router.js +156 -3
- package/src/core/rust-project.js +400 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
|
@@ -7,6 +7,7 @@ import { createReceipt } from "./receipt.js";
|
|
|
7
7
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
8
8
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
9
|
import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
|
|
10
|
+
import { classifyRequirement } from "./evidence-readiness.js";
|
|
10
11
|
|
|
11
12
|
export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
|
|
12
13
|
|
|
@@ -133,7 +134,9 @@ export async function runReconcileClosure({
|
|
|
133
134
|
if (typeof item === "string") {
|
|
134
135
|
return item === requirement;
|
|
135
136
|
}
|
|
136
|
-
return item.type === "VERIFICATION"
|
|
137
|
+
return classifyRequirement(item).type === "VERIFICATION"
|
|
138
|
+
&& item.id === checkId
|
|
139
|
+
&& item.text === requirement;
|
|
137
140
|
});
|
|
138
141
|
if (!verificationItem) {
|
|
139
142
|
throw reconcileError(
|
package/src/core/router.js
CHANGED
|
@@ -62,7 +62,21 @@ const SIGNALS = Object.freeze({
|
|
|
62
62
|
platforms: new Set(["web", "mobile", "desktop", "server", "ci", "cross-platform"]),
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
const PROJECT_FRAMEWORKS = new Set([
|
|
65
|
+
const PROJECT_FRAMEWORKS = new Set([
|
|
66
|
+
"flutter", "dotnet", "aspnetcore", "abp", "nodejs", "rust",
|
|
67
|
+
"c", "cpp", "java", "sql", "go", "typescript", "php", "swift",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const LANGUAGE_PROJECT_GUIDES = Object.freeze([
|
|
71
|
+
["c", "PROJECT_C_CONFIRMED"],
|
|
72
|
+
["cpp", "PROJECT_CPP_CONFIRMED"],
|
|
73
|
+
["java", "PROJECT_JAVA_CONFIRMED"],
|
|
74
|
+
["go", "PROJECT_GO_CONFIRMED"],
|
|
75
|
+
["typescript", "PROJECT_TYPESCRIPT_CONFIRMED"],
|
|
76
|
+
["php", "PROJECT_PHP_CONFIRMED"],
|
|
77
|
+
["swift", "PROJECT_SWIFT_CONFIRMED"],
|
|
78
|
+
["sql", "PROJECT_SQL_CONFIRMED"],
|
|
79
|
+
]);
|
|
66
80
|
|
|
67
81
|
export const PLATFORM_SEMANTICS = Object.freeze({
|
|
68
82
|
web: Object.freeze({
|
|
@@ -134,8 +148,100 @@ function addFlutterProjectGuides(input, add) {
|
|
|
134
148
|
|| !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)
|
|
135
149
|
|| !hasFlutterWorkContext(input.workType)) return;
|
|
136
150
|
add("flutter", "PROJECT_FLUTTER_SDK_DEPENDENCY");
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasDotNetWorkContext(workType) {
|
|
154
|
+
return !["documentation", "ui-copy"].includes(workType);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function addDotNetProjectGuides(input, add) {
|
|
158
|
+
const projectEvidence = input.projectEvidence;
|
|
159
|
+
if (!projectEvidence?.frameworks.includes("dotnet")
|
|
160
|
+
|| !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)
|
|
161
|
+
|| !hasDotNetWorkContext(input.workType)) return;
|
|
162
|
+
add("dotnet", "PROJECT_DOTNET_SDK_PROJECT");
|
|
163
|
+
if (projectEvidence.frameworks.includes("aspnetcore")) add("dotnet", "PROJECT_ASPNETCORE_CONFIRMED");
|
|
164
|
+
if (projectEvidence.frameworks.includes("abp")) add("dotnet", "PROJECT_ABP_CONFIRMED");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hasNodeJsWorkContext(workType) {
|
|
168
|
+
return !["documentation", "ui-copy", "mobile-ui"].includes(workType);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function addNodeJsProjectGuides(input, add) {
|
|
172
|
+
const projectEvidence = input.projectEvidence;
|
|
173
|
+
if (!projectEvidence?.frameworks.includes("nodejs")
|
|
174
|
+
|| !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)
|
|
175
|
+
|| !hasNodeJsWorkContext(input.workType)) return;
|
|
176
|
+
add("nodejs", "PROJECT_NODEJS_CONFIRMED");
|
|
177
|
+
if (projectEvidence.primarySignals.some((signal) => signal.includes(":dependencies.") || signal.includes(":optionalDependencies."))) {
|
|
178
|
+
add("nodejs", "PROJECT_NODEJS_BACKEND_FRAMEWORK");
|
|
179
|
+
}
|
|
180
|
+
if (projectEvidence.primarySignals.some((signal) => signal.includes(":scripts."))) {
|
|
181
|
+
add("nodejs", "PROJECT_NODEJS_RUNTIME_SCRIPT");
|
|
182
|
+
}
|
|
183
|
+
if (projectEvidence.primarySignals.some((signal) => signal.includes(":import="))) {
|
|
184
|
+
add("nodejs", "PROJECT_NODEJS_SERVER_RUNTIME");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hasRustWorkContext(workType) {
|
|
189
|
+
return !["documentation", "ui-copy", "mobile-ui"].includes(workType);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addRustProjectGuides(input, add) {
|
|
193
|
+
const projectEvidence = input.projectEvidence;
|
|
194
|
+
if (!projectEvidence?.frameworks.includes("rust")
|
|
195
|
+
|| !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)
|
|
196
|
+
|| !hasRustWorkContext(input.workType)) return;
|
|
197
|
+
add("rust", "PROJECT_RUST_CONFIRMED");
|
|
198
|
+
if (projectEvidence.primarySignals.some((signal) => signal.endsWith(":package"))) {
|
|
199
|
+
add("rust", "PROJECT_RUST_CARGO_PACKAGE");
|
|
200
|
+
}
|
|
201
|
+
if (projectEvidence.primarySignals.some((signal) => signal.endsWith(":workspace"))) {
|
|
202
|
+
add("rust", "PROJECT_RUST_CARGO_WORKSPACE");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasLanguageWorkContext(workType) {
|
|
207
|
+
return !["documentation", "ui-copy"].includes(workType);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function addLanguageProjectGuides(input, add) {
|
|
211
|
+
const projectEvidence = input.projectEvidence;
|
|
212
|
+
if (!projectEvidence || !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)
|
|
213
|
+
|| !hasLanguageWorkContext(input.workType)) return;
|
|
214
|
+
for (const [guide, reason] of LANGUAGE_PROJECT_GUIDES) {
|
|
215
|
+
if (projectEvidence.frameworks.includes(guide)) add(guide, reason);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function addProjectBaselineGuides(input, add) {
|
|
220
|
+
const projectEvidence = input.projectEvidence;
|
|
221
|
+
if (!projectEvidence || !["MATCH", "UNSCOPED"].includes(projectEvidence.scope)) return;
|
|
222
|
+
if (projectEvidence.frameworks.includes("flutter") && hasFlutterWorkContext(input.workType)) {
|
|
223
|
+
add("clean", "PROJECT_FLUTTER_BASELINE");
|
|
224
|
+
add("test", "PROJECT_FLUTTER_BASELINE");
|
|
225
|
+
}
|
|
226
|
+
if (projectEvidence.frameworks.includes("dotnet") && hasDotNetWorkContext(input.workType)) {
|
|
227
|
+
add("clean", "PROJECT_DOTNET_BASELINE");
|
|
228
|
+
add("test", "PROJECT_DOTNET_BASELINE");
|
|
229
|
+
}
|
|
230
|
+
if (projectEvidence.frameworks.includes("nodejs") && hasNodeJsWorkContext(input.workType)) {
|
|
231
|
+
add("clean", "PROJECT_NODEJS_BASELINE");
|
|
232
|
+
add("test", "PROJECT_NODEJS_BASELINE");
|
|
233
|
+
}
|
|
234
|
+
if (projectEvidence.frameworks.includes("rust") && hasRustWorkContext(input.workType)) {
|
|
235
|
+
add("clean", "PROJECT_RUST_BASELINE");
|
|
236
|
+
add("test", "PROJECT_RUST_BASELINE");
|
|
237
|
+
}
|
|
238
|
+
if (hasLanguageWorkContext(input.workType)) {
|
|
239
|
+
for (const [language] of LANGUAGE_PROJECT_GUIDES) {
|
|
240
|
+
if (!projectEvidence.frameworks.includes(language)) continue;
|
|
241
|
+
add("clean", `PROJECT_${language.toUpperCase()}_BASELINE`);
|
|
242
|
+
add("test", `PROJECT_${language.toUpperCase()}_BASELINE`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
139
245
|
}
|
|
140
246
|
|
|
141
247
|
function flutterExclusionReason(projectEvidence, workType) {
|
|
@@ -146,6 +252,30 @@ function flutterExclusionReason(projectEvidence, workType) {
|
|
|
146
252
|
return "NO_FLUTTER_PRIMARY_EVIDENCE";
|
|
147
253
|
}
|
|
148
254
|
|
|
255
|
+
function dotNetExclusionReason(projectEvidence, workType) {
|
|
256
|
+
if (!projectEvidence) return "NO_DOTNET_PROJECT_EVIDENCE";
|
|
257
|
+
if (projectEvidence.scope === "NO_MATCH") return "NO_DOTNET_SCOPE_MATCH";
|
|
258
|
+
if (!projectEvidence.frameworks.includes("dotnet")) return "NO_DOTNET_PRIMARY_EVIDENCE";
|
|
259
|
+
if (!hasDotNetWorkContext(workType)) return "NO_DOTNET_EXECUTABLE_WORK";
|
|
260
|
+
return "NO_DOTNET_PRIMARY_EVIDENCE";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function nodeJsExclusionReason(projectEvidence, workType) {
|
|
264
|
+
if (!projectEvidence) return "NO_NODEJS_PROJECT_EVIDENCE";
|
|
265
|
+
if (projectEvidence.scope === "NO_MATCH") return "NO_NODEJS_SCOPE_MATCH";
|
|
266
|
+
if (!projectEvidence.frameworks.includes("nodejs")) return "NO_NODEJS_PRIMARY_EVIDENCE";
|
|
267
|
+
if (!hasNodeJsWorkContext(workType)) return "NO_NODEJS_EXECUTABLE_WORK";
|
|
268
|
+
return "NO_NODEJS_PRIMARY_EVIDENCE";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function rustExclusionReason(projectEvidence, workType) {
|
|
272
|
+
if (!projectEvidence) return "NO_RUST_PROJECT_EVIDENCE";
|
|
273
|
+
if (projectEvidence.scope === "NO_MATCH") return "NO_RUST_SCOPE_MATCH";
|
|
274
|
+
if (!projectEvidence.frameworks.includes("rust")) return "NO_RUST_PRIMARY_EVIDENCE";
|
|
275
|
+
if (!hasRustWorkContext(workType)) return "NO_RUST_EXECUTABLE_WORK";
|
|
276
|
+
return "NO_RUST_PRIMARY_EVIDENCE";
|
|
277
|
+
}
|
|
278
|
+
|
|
149
279
|
function exclusionReasonForGuide(guide, projectEvidence, workType) {
|
|
150
280
|
if (guide === "security") return "NO_TRUST_BOUNDARY";
|
|
151
281
|
if (guide === "performance") return "NO_MEASURABLE_PERFORMANCE_RISK";
|
|
@@ -154,6 +284,16 @@ function exclusionReasonForGuide(guide, projectEvidence, workType) {
|
|
|
154
284
|
if (guide === "taste") return "NO_TASTE_FRONTEND_CONTEXT";
|
|
155
285
|
if (guide === "documentation") return "NO_DOCUMENTATION_SURFACE";
|
|
156
286
|
if (guide === "flutter") return flutterExclusionReason(projectEvidence, workType);
|
|
287
|
+
if (guide === "dotnet") return dotNetExclusionReason(projectEvidence, workType);
|
|
288
|
+
if (guide === "nodejs") return nodeJsExclusionReason(projectEvidence, workType);
|
|
289
|
+
if (guide === "rust") return rustExclusionReason(projectEvidence, workType);
|
|
290
|
+
if (LANGUAGE_PROJECT_GUIDES.some(([language]) => language === guide)) {
|
|
291
|
+
if (!projectEvidence) return `NO_${guide.toUpperCase()}_PROJECT_EVIDENCE`;
|
|
292
|
+
if (projectEvidence.scope === "NO_MATCH") return `NO_${guide.toUpperCase()}_SCOPE_MATCH`;
|
|
293
|
+
if (!projectEvidence.frameworks.includes(guide)) return `NO_${guide.toUpperCase()}_PRIMARY_EVIDENCE`;
|
|
294
|
+
if (!hasLanguageWorkContext(workType)) return `NO_${guide.toUpperCase()}_EXECUTABLE_WORK`;
|
|
295
|
+
return `NO_${guide.toUpperCase()}_PRIMARY_EVIDENCE`;
|
|
296
|
+
}
|
|
157
297
|
return "NO_BEHAVIOR_OR_EXECUTABLE_CHANGE";
|
|
158
298
|
}
|
|
159
299
|
|
|
@@ -209,6 +349,14 @@ function normalizeProjectEvidence(value) {
|
|
|
209
349
|
throw new RouteInputError(`Unsupported projectEvidence schema version: ${value.schemaVersion}`);
|
|
210
350
|
}
|
|
211
351
|
const frameworks = normalizeArray(value.frameworks, "frameworks", PROJECT_FRAMEWORKS);
|
|
352
|
+
const hasDotnet = frameworks.includes("dotnet");
|
|
353
|
+
const invalidOverlays = frameworks.filter((framework) => framework === "aspnetcore" || framework === "abp");
|
|
354
|
+
if (!hasDotnet && invalidOverlays.length > 0) {
|
|
355
|
+
const noun = invalidOverlays.length === 1 ? "framework" : "frameworks";
|
|
356
|
+
const verb = invalidOverlays.length === 1 ? "requires" : "require";
|
|
357
|
+
const labels = invalidOverlays.map((framework) => `"${framework}"`).join(", ");
|
|
358
|
+
throw new RouteInputError(`projectEvidence ${noun} ${labels} ${verb} "dotnet"`);
|
|
359
|
+
}
|
|
212
360
|
const scope = value.scope ?? (frameworks.length > 0 ? "UNSCOPED" : "NONE");
|
|
213
361
|
if (!PROJECT_EVIDENCE_SCOPES.includes(scope)) {
|
|
214
362
|
throw new RouteInputError(`Unknown project evidence scope: ${scope}`);
|
|
@@ -264,6 +412,11 @@ export function evaluateRoute(input = {}, profileOptions = {}) {
|
|
|
264
412
|
|
|
265
413
|
const projectEvidence = normalized.projectEvidence;
|
|
266
414
|
addFlutterProjectGuides(normalized, add);
|
|
415
|
+
addDotNetProjectGuides(normalized, add);
|
|
416
|
+
addNodeJsProjectGuides(normalized, add);
|
|
417
|
+
addRustProjectGuides(normalized, add);
|
|
418
|
+
addLanguageProjectGuides(normalized, add);
|
|
419
|
+
addProjectBaselineGuides(normalized, add);
|
|
267
420
|
|
|
268
421
|
const workReason = reasonForWorkType(normalized.workType);
|
|
269
422
|
for (const guide of WORK_GUIDES[normalized.workType]) add(guide, workReason);
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
|
|
5
|
+
export const CARGO_MANIFEST_MAX_BYTES = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export const RUST_SHARED_FILES = Object.freeze([
|
|
8
|
+
"Cargo.lock",
|
|
9
|
+
"rust-toolchain.toml",
|
|
10
|
+
"rust-toolchain",
|
|
11
|
+
"rustfmt.toml",
|
|
12
|
+
".rustfmt.toml",
|
|
13
|
+
"clippy.toml",
|
|
14
|
+
".clippy.toml",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const RUST_BACKEND_CONTEXTS = new Set([
|
|
18
|
+
"actix-web",
|
|
19
|
+
"axum",
|
|
20
|
+
"hyper",
|
|
21
|
+
"poem",
|
|
22
|
+
"rocket",
|
|
23
|
+
"tokio",
|
|
24
|
+
"tonic",
|
|
25
|
+
"tower",
|
|
26
|
+
"warp",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function uniqueSorted(values) {
|
|
34
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]
|
|
35
|
+
.sort((left, right) => left.localeCompare(right));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function invalidCargoManifest() {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
rust: false,
|
|
42
|
+
package: { present: false, name: null, edition: null, rustVersion: null },
|
|
43
|
+
workspace: { present: false, members: [], exclude: [], defaultMembers: [], resolver: null },
|
|
44
|
+
dependencies: [],
|
|
45
|
+
workspaceDependencies: [],
|
|
46
|
+
devDependencies: [],
|
|
47
|
+
buildDependencies: [],
|
|
48
|
+
targetDependencies: [],
|
|
49
|
+
pathDependencies: [],
|
|
50
|
+
workspaceDependencyPaths: [],
|
|
51
|
+
workspaceDependencyUses: [],
|
|
52
|
+
features: [],
|
|
53
|
+
backendContexts: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function optionalString(object, key) {
|
|
58
|
+
if (object[key] === undefined) return null;
|
|
59
|
+
return typeof object[key] === "string" && object[key].trim() !== "" ? object[key] : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cargoPackageString(object, key) {
|
|
63
|
+
if (object[key] === undefined) return { value: null, inherited: false };
|
|
64
|
+
if (typeof object[key] === "string" && object[key].trim() !== "") {
|
|
65
|
+
return { value: object[key], inherited: false };
|
|
66
|
+
}
|
|
67
|
+
if (isPlainObject(object[key])
|
|
68
|
+
&& object[key].workspace === true
|
|
69
|
+
&& Object.keys(object[key]).every((property) => property === "workspace")) {
|
|
70
|
+
return { value: null, inherited: true };
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function optionalStringArray(object, key) {
|
|
76
|
+
if (object[key] === undefined) return [];
|
|
77
|
+
if (!Array.isArray(object[key]) || object[key].some((value) => typeof value !== "string" || value.trim() === "")) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return [...object[key]];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dependencyNamesFromTable(value) {
|
|
84
|
+
if (value === undefined) return [];
|
|
85
|
+
if (!isPlainObject(value)) return null;
|
|
86
|
+
return Object.keys(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pathDependenciesFromTable(value) {
|
|
90
|
+
if (value === undefined) return [];
|
|
91
|
+
if (!isPlainObject(value)) return null;
|
|
92
|
+
return Object.entries(value)
|
|
93
|
+
.filter(([, specification]) => isPlainObject(specification)
|
|
94
|
+
&& specification.workspace !== true
|
|
95
|
+
&& typeof specification.path === "string"
|
|
96
|
+
&& specification.path.trim() !== "")
|
|
97
|
+
.map(([name, specification]) => ({ name, path: specification.path }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function workspaceDependencyUsesFromTable(value) {
|
|
101
|
+
if (value === undefined) return [];
|
|
102
|
+
if (!isPlainObject(value)) return null;
|
|
103
|
+
return Object.entries(value)
|
|
104
|
+
.filter(([, specification]) => isPlainObject(specification) && specification.workspace === true)
|
|
105
|
+
.map(([name]) => name);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function dependencyMetadataFromTable(value) {
|
|
109
|
+
const names = dependencyNamesFromTable(value);
|
|
110
|
+
const pathDependencies = pathDependenciesFromTable(value);
|
|
111
|
+
const workspaceDependencyUses = workspaceDependencyUsesFromTable(value);
|
|
112
|
+
if (names === null || pathDependencies === null || workspaceDependencyUses === null) return null;
|
|
113
|
+
return { names, pathDependencies, workspaceDependencyUses };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uniquePathDependencies(values) {
|
|
117
|
+
const entries = new Map();
|
|
118
|
+
for (const dependency of values) {
|
|
119
|
+
if (!dependency || typeof dependency.name !== "string" || typeof dependency.path !== "string") continue;
|
|
120
|
+
const name = dependency.name.trim();
|
|
121
|
+
const dependencyPath = dependency.path.trim();
|
|
122
|
+
if (name === "" || dependencyPath === "") continue;
|
|
123
|
+
entries.set(`${name}\u0000${dependencyPath}`, { name, path: dependencyPath });
|
|
124
|
+
}
|
|
125
|
+
return [...entries.values()].sort((left, right) => {
|
|
126
|
+
const byName = left.name.localeCompare(right.name);
|
|
127
|
+
return byName || left.path.localeCompare(right.path);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function dependencyMetadataFromTargets(value) {
|
|
132
|
+
if (value === undefined) return { names: [], pathDependencies: [], workspaceDependencyUses: [] };
|
|
133
|
+
if (!isPlainObject(value)) return null;
|
|
134
|
+
const names = [];
|
|
135
|
+
const pathDependencies = [];
|
|
136
|
+
const workspaceDependencyUses = [];
|
|
137
|
+
for (const target of Object.values(value)) {
|
|
138
|
+
if (!isPlainObject(target)) return null;
|
|
139
|
+
for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
|
|
140
|
+
const dependencies = dependencyMetadataFromTable(target[section]);
|
|
141
|
+
if (dependencies === null) return null;
|
|
142
|
+
names.push(...dependencies.names);
|
|
143
|
+
pathDependencies.push(...dependencies.pathDependencies);
|
|
144
|
+
workspaceDependencyUses.push(...dependencies.workspaceDependencyUses);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { names: uniqueSorted(names), pathDependencies, workspaceDependencyUses };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseCargoSections(document) {
|
|
151
|
+
const dependencies = dependencyMetadataFromTable(document.dependencies);
|
|
152
|
+
const workspaceDependencies = dependencyMetadataFromTable(document.workspace?.dependencies);
|
|
153
|
+
const devDependencies = dependencyMetadataFromTable(document["dev-dependencies"]);
|
|
154
|
+
const buildDependencies = dependencyMetadataFromTable(document["build-dependencies"]);
|
|
155
|
+
const targetDependencies = dependencyMetadataFromTargets(document.target);
|
|
156
|
+
const features = document.features === undefined
|
|
157
|
+
? []
|
|
158
|
+
: isPlainObject(document.features) ? Object.keys(document.features) : null;
|
|
159
|
+
if ([dependencies, workspaceDependencies, devDependencies, buildDependencies, targetDependencies, features]
|
|
160
|
+
.some((value) => value === null)) return null;
|
|
161
|
+
return {
|
|
162
|
+
dependencies: uniqueSorted(dependencies.names),
|
|
163
|
+
workspaceDependencies: uniqueSorted(workspaceDependencies.names),
|
|
164
|
+
devDependencies: uniqueSorted(devDependencies.names),
|
|
165
|
+
buildDependencies: uniqueSorted(buildDependencies.names),
|
|
166
|
+
targetDependencies: targetDependencies.names,
|
|
167
|
+
pathDependencies: uniquePathDependencies([
|
|
168
|
+
...dependencies.pathDependencies,
|
|
169
|
+
...devDependencies.pathDependencies,
|
|
170
|
+
...buildDependencies.pathDependencies,
|
|
171
|
+
...targetDependencies.pathDependencies,
|
|
172
|
+
]),
|
|
173
|
+
workspaceDependencyPaths: uniquePathDependencies(workspaceDependencies.pathDependencies),
|
|
174
|
+
workspaceDependencyUses: uniqueSorted([
|
|
175
|
+
...dependencies.workspaceDependencyUses,
|
|
176
|
+
...devDependencies.workspaceDependencyUses,
|
|
177
|
+
...buildDependencies.workspaceDependencyUses,
|
|
178
|
+
...targetDependencies.workspaceDependencyUses,
|
|
179
|
+
]),
|
|
180
|
+
features: uniqueSorted(features),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseCargoPackage(document) {
|
|
185
|
+
if (document.package === undefined) {
|
|
186
|
+
return { present: false, name: null, edition: null, rustVersion: null };
|
|
187
|
+
}
|
|
188
|
+
if (!isPlainObject(document.package)) return null;
|
|
189
|
+
const name = optionalString(document.package, "name");
|
|
190
|
+
const edition = cargoPackageString(document.package, "edition");
|
|
191
|
+
const rustVersion = cargoPackageString(document.package, "rust-version");
|
|
192
|
+
const workspace = optionalString(document.package, "workspace");
|
|
193
|
+
if (name === undefined || name === null || edition === null || rustVersion === null || workspace === undefined) return null;
|
|
194
|
+
const metadata = {
|
|
195
|
+
present: true,
|
|
196
|
+
name,
|
|
197
|
+
edition: edition.value,
|
|
198
|
+
rustVersion: rustVersion.value,
|
|
199
|
+
};
|
|
200
|
+
if (edition.inherited) metadata.editionInherited = true;
|
|
201
|
+
if (rustVersion.inherited) metadata.rustVersionInherited = true;
|
|
202
|
+
if (workspace !== null) metadata.workspace = workspace;
|
|
203
|
+
return metadata;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseCargoWorkspacePackage(workspace) {
|
|
207
|
+
if (workspace.package === undefined) return null;
|
|
208
|
+
if (!isPlainObject(workspace.package)) return undefined;
|
|
209
|
+
const edition = optionalString(workspace.package, "edition");
|
|
210
|
+
const rustVersion = optionalString(workspace.package, "rust-version");
|
|
211
|
+
if (edition === undefined || rustVersion === undefined) return undefined;
|
|
212
|
+
return { present: true, edition, rustVersion };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseCargoWorkspace(document) {
|
|
216
|
+
if (document.workspace === undefined) {
|
|
217
|
+
return { present: false, members: [], exclude: [], defaultMembers: [], resolver: null };
|
|
218
|
+
}
|
|
219
|
+
if (!isPlainObject(document.workspace)) return null;
|
|
220
|
+
const members = optionalStringArray(document.workspace, "members");
|
|
221
|
+
const exclude = optionalStringArray(document.workspace, "exclude");
|
|
222
|
+
const defaultMembers = optionalStringArray(document.workspace, "default-members");
|
|
223
|
+
const resolver = optionalString(document.workspace, "resolver");
|
|
224
|
+
if (members === null || exclude === null || defaultMembers === null || resolver === undefined) return null;
|
|
225
|
+
if (document.workspace.dependencies !== undefined && !isPlainObject(document.workspace.dependencies)) return null;
|
|
226
|
+
const workspacePackage = parseCargoWorkspacePackage(document.workspace);
|
|
227
|
+
if (workspacePackage === undefined) return null;
|
|
228
|
+
const metadata = { present: true, members, exclude, defaultMembers, resolver };
|
|
229
|
+
if (workspacePackage) metadata.package = workspacePackage;
|
|
230
|
+
return metadata;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function parseCargoManifest(text) {
|
|
234
|
+
const invalid = invalidCargoManifest();
|
|
235
|
+
if (typeof text !== "string" || text.length > CARGO_MANIFEST_MAX_BYTES || /\r(?!\n)/u.test(text)) return invalid;
|
|
236
|
+
|
|
237
|
+
let document;
|
|
238
|
+
try {
|
|
239
|
+
document = parseToml(text);
|
|
240
|
+
} catch {
|
|
241
|
+
return invalid;
|
|
242
|
+
}
|
|
243
|
+
if (!isPlainObject(document)) return invalid;
|
|
244
|
+
|
|
245
|
+
const packageMetadata = parseCargoPackage(document);
|
|
246
|
+
const workspaceMetadata = parseCargoWorkspace(document);
|
|
247
|
+
const sections = parseCargoSections(document);
|
|
248
|
+
if (packageMetadata?.present
|
|
249
|
+
&& Object.prototype.hasOwnProperty.call(packageMetadata, "workspace")
|
|
250
|
+
&& workspaceMetadata?.present) return invalid;
|
|
251
|
+
if (!packageMetadata || !workspaceMetadata || !sections
|
|
252
|
+
|| (!packageMetadata.present && !workspaceMetadata.present)) return invalid;
|
|
253
|
+
|
|
254
|
+
const allDependencies = [
|
|
255
|
+
...sections.dependencies,
|
|
256
|
+
...sections.workspaceDependencies,
|
|
257
|
+
...sections.devDependencies,
|
|
258
|
+
...sections.buildDependencies,
|
|
259
|
+
...sections.targetDependencies,
|
|
260
|
+
];
|
|
261
|
+
return {
|
|
262
|
+
valid: true,
|
|
263
|
+
rust: true,
|
|
264
|
+
package: packageMetadata,
|
|
265
|
+
workspace: workspaceMetadata,
|
|
266
|
+
...sections,
|
|
267
|
+
backendContexts: uniqueSorted(allDependencies.filter((name) => RUST_BACKEND_CONTEXTS.has(name))),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizedRelativePath(root, candidate) {
|
|
272
|
+
if (typeof root !== "string" || typeof candidate !== "string") return null;
|
|
273
|
+
if (root === candidate) return ".";
|
|
274
|
+
if (root === ".") return candidate.startsWith("./") ? candidate.slice(2) : candidate;
|
|
275
|
+
if (!candidate.startsWith(`${root}/`)) return null;
|
|
276
|
+
return candidate.slice(root.length + 1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizedCargoPath(root, relativePath) {
|
|
280
|
+
if (typeof root !== "string" || typeof relativePath !== "string" || relativePath.trim() === "") return null;
|
|
281
|
+
const portable = relativePath.trim().replaceAll("\\", "/");
|
|
282
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable)) return null;
|
|
283
|
+
const joined = path.posix.normalize(path.posix.join(root === "." ? "" : root, portable));
|
|
284
|
+
if (joined === ".." || joined.startsWith("../")) return null;
|
|
285
|
+
return joined.replace(/^\.\//u, "") || ".";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizedCargoPattern(pattern) {
|
|
289
|
+
if (typeof pattern !== "string" || pattern.trim() === "") return null;
|
|
290
|
+
const portable = pattern.trim().replaceAll("\\", "/");
|
|
291
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable)) return null;
|
|
292
|
+
const normalized = path.posix.normalize(portable).replace(/^\.\//u, "");
|
|
293
|
+
if (normalized === ".." || normalized.startsWith("../")) return null;
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cargoPatternRegex(pattern) {
|
|
298
|
+
const normalized = normalizedCargoPattern(pattern);
|
|
299
|
+
if (!normalized) return null;
|
|
300
|
+
let expression = "^";
|
|
301
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
302
|
+
const character = normalized[index];
|
|
303
|
+
if (character === "*" && normalized[index + 1] === "*") {
|
|
304
|
+
expression += ".*";
|
|
305
|
+
index += 1;
|
|
306
|
+
} else if (character === "*") {
|
|
307
|
+
expression += "[^/]*";
|
|
308
|
+
} else if (character === "?") {
|
|
309
|
+
expression += "[^/]";
|
|
310
|
+
} else {
|
|
311
|
+
expression += character.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return new RegExp(`${expression}$`, "u");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function cargoPatternMatches(patterns, relativePath) {
|
|
318
|
+
return patterns.some((pattern) => cargoPatternRegex(pattern)?.test(relativePath));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isDescendant(root, candidate) {
|
|
322
|
+
return root !== candidate && candidate.startsWith(`${root}/`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hasNestedWorkspaceBetween(workspaceProject, candidateProject, projects) {
|
|
326
|
+
return projects.some((project) => project !== workspaceProject
|
|
327
|
+
&& project !== candidateProject
|
|
328
|
+
&& project.kind === "rust"
|
|
329
|
+
&& project.rust
|
|
330
|
+
&& project.workspaceRoot
|
|
331
|
+
&& isDescendant(workspaceProject.root, project.root)
|
|
332
|
+
&& (project.root === candidateProject.root || isDescendant(project.root, candidateProject.root)));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function cargoPackageWorkspaceMatches(workspaceProject, candidateProject) {
|
|
336
|
+
const association = candidateProject.internal?.package?.workspace;
|
|
337
|
+
return typeof association === "string"
|
|
338
|
+
&& normalizedCargoPath(candidateProject.root, association) === workspaceProject.root;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function cargoPackageHasExplicitWorkspace(candidateProject) {
|
|
342
|
+
return typeof candidateProject.internal?.package?.workspace === "string";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function cargoWorkspaceDependencyMatches(workspaceProject, sourceProject, candidateProject) {
|
|
346
|
+
const definitions = workspaceProject.internal?.workspaceDependencyPaths ?? [];
|
|
347
|
+
const uses = sourceProject.internal?.workspaceDependencyUses ?? [];
|
|
348
|
+
return uses.some((name) => definitions.some((dependency) => dependency.name === name
|
|
349
|
+
&& normalizedCargoPath(workspaceProject.root, dependency.path) === candidateProject.root));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function cargoPathDependencyMatches(sourceProject, candidateProject, workspaceProject) {
|
|
353
|
+
const packagePathMatch = (sourceProject.internal?.pathDependencies ?? [])
|
|
354
|
+
.some((dependency) => normalizedCargoPath(sourceProject.root, dependency.path) === candidateProject.root);
|
|
355
|
+
return packagePathMatch || cargoWorkspaceDependencyMatches(workspaceProject, sourceProject, candidateProject);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function cargoWorkspaceContainsInternal(workspaceProject, candidateProject, projects, resolving) {
|
|
359
|
+
if (!workspaceProject?.rust || !workspaceProject.workspaceRoot
|
|
360
|
+
|| !candidateProject?.rust || !candidateProject.packageRoot) return false;
|
|
361
|
+
if (workspaceProject.root === candidateProject.root) return true;
|
|
362
|
+
if (candidateProject.workspaceRoot) return false;
|
|
363
|
+
if (cargoPackageHasExplicitWorkspace(candidateProject)) return cargoPackageWorkspaceMatches(workspaceProject, candidateProject);
|
|
364
|
+
const relative = normalizedRelativePath(workspaceProject.root, candidateProject.root);
|
|
365
|
+
if (!relative || hasNestedWorkspaceBetween(workspaceProject, candidateProject, projects)) return false;
|
|
366
|
+
const workspace = workspaceProject.internal.workspace;
|
|
367
|
+
if (workspace.exclude.some((pattern) => cargoPatternMatches([pattern], relative))) return false;
|
|
368
|
+
if (cargoPatternMatches(workspace.members, relative)) return true;
|
|
369
|
+
if (resolving.has(candidateProject.root)) return false;
|
|
370
|
+
resolving.add(candidateProject.root);
|
|
371
|
+
const contained = projects.some((project) => project !== candidateProject
|
|
372
|
+
&& cargoWorkspaceContainsInternal(workspaceProject, project, projects, resolving)
|
|
373
|
+
&& cargoPathDependencyMatches(project, candidateProject, workspaceProject));
|
|
374
|
+
resolving.delete(candidateProject.root);
|
|
375
|
+
return contained;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function cargoWorkspaceContains(workspaceProject, candidateProject, projects = []) {
|
|
379
|
+
return cargoWorkspaceContainsInternal(workspaceProject, candidateProject, projects, new Set());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function isRustSharedFile(relativePath) {
|
|
383
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
384
|
+
const fileName = path.posix.basename(normalized).toLowerCase();
|
|
385
|
+
if (RUST_SHARED_FILES.some((name) => name.toLowerCase() === fileName)) return true;
|
|
386
|
+
const parent = path.posix.basename(path.posix.dirname(normalized)).toLowerCase();
|
|
387
|
+
return parent === ".cargo" && ["config", "config.toml"].includes(fileName);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function rustSharedScopeDirectory(relativePath) {
|
|
391
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
392
|
+
const parent = path.posix.dirname(normalized);
|
|
393
|
+
if (path.posix.basename(parent).toLowerCase() === ".cargo") return path.posix.dirname(parent) || ".";
|
|
394
|
+
return parent || ".";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function rustSharedFileKind(relativePath) {
|
|
398
|
+
if (!isRustSharedFile(relativePath)) return null;
|
|
399
|
+
return path.posix.basename(relativePath).toLowerCase() === "cargo.lock" ? "lockfile" : "configuration";
|
|
400
|
+
}
|