@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.
Files changed (62) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +90 -0
  5. package/DOCS_INDEX.md +13 -11
  6. package/ENG/c-development-eng.md +112 -0
  7. package/ENG/cpp-development-eng.md +109 -0
  8. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  9. package/ENG/go-development-eng.md +103 -0
  10. package/ENG/java-development-eng.md +125 -0
  11. package/ENG/nodejs-backend-development-eng.md +605 -0
  12. package/ENG/php-development-eng.md +104 -0
  13. package/ENG/rust-development-eng.md +422 -0
  14. package/ENG/sql-development-eng.md +108 -0
  15. package/ENG/swift-development-eng.md +111 -0
  16. package/ENG/typescript-development-eng.md +108 -0
  17. package/GUIDE_ROUTER.md +418 -9
  18. package/QUALITY_SCORECARD.md +1 -0
  19. package/README.md +44 -33
  20. package/THIRD_PARTY_NOTICES.md +19 -7
  21. package/completions/_forgeloop +3 -3
  22. package/completions/forgeloop.bash +3 -3
  23. package/completions/forgeloop.fish +7 -0
  24. package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
  25. package/docs/CLI_REFERENCE.md +28 -6
  26. package/docs/DOCUMENTATION_GUIDE.md +2 -1
  27. package/docs/GETTING_STARTED.md +59 -0
  28. package/docs/PACKAGE_CONTENTS.md +28 -14
  29. package/docs/RECIPES.md +23 -0
  30. package/docs/RELEASE_CHECKLIST.md +30 -2
  31. package/docs/TROUBLESHOOTING.md +100 -2
  32. package/docs/documentation-manifest.json +652 -0
  33. package/docs/protocol-requirements.json +77 -0
  34. package/package.json +19 -4
  35. package/schemas/routing-input.schema.json +1 -1
  36. package/scripts/CI_VALIDATORS.md +84 -11
  37. package/scripts/generate-agent-protocol-summary.mjs +36 -0
  38. package/src/commands/next.js +19 -7
  39. package/src/commands/task-create.js +84 -25
  40. package/src/commands/task-list.js +22 -2
  41. package/src/config/guides.json +44 -0
  42. package/src/core/build-script.js +151 -0
  43. package/src/core/c-cpp-project.js +143 -0
  44. package/src/core/cli-command-definitions.js +8 -1
  45. package/src/core/command-executors.js +5 -3
  46. package/src/core/command-input.js +140 -102
  47. package/src/core/contract-presets.js +82 -0
  48. package/src/core/error-codes.js +3 -3
  49. package/src/core/filesystem.js +1 -10
  50. package/src/core/go-project.js +206 -0
  51. package/src/core/java-project.js +403 -0
  52. package/src/core/multi-language-project.js +117 -0
  53. package/src/core/next-explanation.js +63 -0
  54. package/src/core/php-project.js +85 -0
  55. package/src/core/project-detection.js +1760 -52
  56. package/src/core/reconcile-closure.js +4 -1
  57. package/src/core/router.js +156 -3
  58. package/src/core/rust-project.js +400 -0
  59. package/src/core/sql-project.js +141 -0
  60. package/src/core/swift-project.js +200 -0
  61. package/src/core/typescript-project.js +349 -0
  62. 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" && item.id === checkId && item.text === requirement;
137
+ return classifyRequirement(item).type === "VERIFICATION"
138
+ && item.id === checkId
139
+ && item.text === requirement;
137
140
  });
138
141
  if (!verificationItem) {
139
142
  throw reconcileError(
@@ -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(["flutter"]);
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
- add("clean", "PROJECT_FLUTTER_BASELINE");
138
- add("test", "PROJECT_FLUTTER_BASELINE");
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
+ }