human-to-code 0.0.1 → 0.1.1

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 (130) hide show
  1. package/Readme.md +366 -38
  2. package/SECURITY.md +175 -11
  3. package/dist/adapters/node.d.ts +8 -0
  4. package/dist/adapters/node.d.ts.map +1 -0
  5. package/dist/adapters/node.js +1071 -0
  6. package/dist/adapters/node.js.map +1 -0
  7. package/dist/adapters/python.d.ts +7 -0
  8. package/dist/adapters/python.d.ts.map +1 -0
  9. package/dist/adapters/python.js +540 -0
  10. package/dist/adapters/python.js.map +1 -0
  11. package/dist/adapters/rust.d.ts +7 -0
  12. package/dist/adapters/rust.d.ts.map +1 -0
  13. package/dist/adapters/rust.js +517 -0
  14. package/dist/adapters/rust.js.map +1 -0
  15. package/dist/analyzer-types.d.ts +153 -0
  16. package/dist/analyzer-types.d.ts.map +1 -0
  17. package/dist/analyzer-types.js +8 -0
  18. package/dist/analyzer-types.js.map +1 -0
  19. package/dist/analyzer-utils.d.ts +40 -0
  20. package/dist/analyzer-utils.d.ts.map +1 -0
  21. package/dist/analyzer-utils.js +591 -0
  22. package/dist/analyzer-utils.js.map +1 -0
  23. package/dist/analyzer.d.ts +21 -0
  24. package/dist/analyzer.d.ts.map +1 -0
  25. package/dist/analyzer.js +103 -0
  26. package/dist/analyzer.js.map +1 -0
  27. package/dist/certification.d.ts +102 -0
  28. package/dist/certification.d.ts.map +1 -0
  29. package/dist/certification.js +193 -0
  30. package/dist/certification.js.map +1 -0
  31. package/dist/cli.d.ts +4 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +562 -0
  34. package/dist/cli.js.map +1 -0
  35. package/dist/compiler-skills.d.ts +25 -0
  36. package/dist/compiler-skills.d.ts.map +1 -0
  37. package/dist/compiler-skills.js +158 -0
  38. package/dist/compiler-skills.js.map +1 -0
  39. package/dist/compiler-tools.d.ts +29 -0
  40. package/dist/compiler-tools.d.ts.map +1 -0
  41. package/dist/compiler-tools.js +337 -0
  42. package/dist/compiler-tools.js.map +1 -0
  43. package/dist/config.d.ts +122 -0
  44. package/dist/config.d.ts.map +1 -0
  45. package/dist/config.js +704 -0
  46. package/dist/config.js.map +1 -0
  47. package/dist/context.d.ts +153 -0
  48. package/dist/context.d.ts.map +1 -0
  49. package/dist/context.js +772 -0
  50. package/dist/context.js.map +1 -0
  51. package/dist/contracts.d.ts +196 -0
  52. package/dist/contracts.d.ts.map +1 -0
  53. package/dist/contracts.js +721 -0
  54. package/dist/contracts.js.map +1 -0
  55. package/dist/discovery.d.ts +34 -0
  56. package/dist/discovery.d.ts.map +1 -0
  57. package/dist/discovery.js +312 -0
  58. package/dist/discovery.js.map +1 -0
  59. package/dist/documentation.d.ts +44 -0
  60. package/dist/documentation.d.ts.map +1 -0
  61. package/dist/documentation.js +418 -0
  62. package/dist/documentation.js.map +1 -0
  63. package/dist/index.d.ts +23 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +21 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/patch.d.ts +71 -0
  68. package/dist/patch.d.ts.map +1 -0
  69. package/dist/patch.js +394 -0
  70. package/dist/patch.js.map +1 -0
  71. package/dist/pinned-http.d.ts +15 -0
  72. package/dist/pinned-http.d.ts.map +1 -0
  73. package/dist/pinned-http.js +170 -0
  74. package/dist/pinned-http.js.map +1 -0
  75. package/dist/planner.d.ts +30 -0
  76. package/dist/planner.d.ts.map +1 -0
  77. package/dist/planner.js +228 -0
  78. package/dist/planner.js.map +1 -0
  79. package/dist/provider.d.ts +168 -0
  80. package/dist/provider.d.ts.map +1 -0
  81. package/dist/provider.js +512 -0
  82. package/dist/provider.js.map +1 -0
  83. package/dist/providers.d.ts +41 -0
  84. package/dist/providers.d.ts.map +1 -0
  85. package/dist/providers.js +954 -0
  86. package/dist/providers.js.map +1 -0
  87. package/dist/run-store.d.ts +24 -0
  88. package/dist/run-store.d.ts.map +1 -0
  89. package/dist/run-store.js +285 -0
  90. package/dist/run-store.js.map +1 -0
  91. package/dist/schemas.d.ts +18 -0
  92. package/dist/schemas.d.ts.map +1 -0
  93. package/dist/schemas.js +228 -0
  94. package/dist/schemas.js.map +1 -0
  95. package/dist/secret-scan.d.ts +30 -0
  96. package/dist/secret-scan.d.ts.map +1 -0
  97. package/dist/secret-scan.js +122 -0
  98. package/dist/secret-scan.js.map +1 -0
  99. package/dist/snapshot.d.ts +33 -0
  100. package/dist/snapshot.d.ts.map +1 -0
  101. package/dist/snapshot.js +136 -0
  102. package/dist/snapshot.js.map +1 -0
  103. package/dist/support-matrix.d.ts +23 -0
  104. package/dist/support-matrix.d.ts.map +1 -0
  105. package/dist/support-matrix.js +187 -0
  106. package/dist/support-matrix.js.map +1 -0
  107. package/dist/types.d.ts +63 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +8 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/validation.d.ts +25 -0
  112. package/dist/validation.d.ts.map +1 -0
  113. package/dist/validation.js +388 -0
  114. package/dist/validation.js.map +1 -0
  115. package/dist/workflow.d.ts +65 -0
  116. package/dist/workflow.d.ts.map +1 -0
  117. package/dist/workflow.js +1623 -0
  118. package/dist/workflow.js.map +1 -0
  119. package/package.json +37 -9
  120. package/.claude/settings.local.json +0 -12
  121. package/.github/workflows/ci.yml +0 -18
  122. package/CONTRIBUTING.md +0 -40
  123. package/src/cli.ts +0 -218
  124. package/src/config.ts +0 -186
  125. package/src/discovery.ts +0 -186
  126. package/src/index.ts +0 -22
  127. package/src/types.ts +0 -74
  128. package/test/config.test.ts +0 -97
  129. package/test/discovery.test.ts +0 -91
  130. package/tsconfig.json +0 -17
@@ -0,0 +1,1071 @@
1
+ import { posix } from "node:path";
2
+ import { PROJECT_PROFILE_SCHEMA_VERSION } from "../analyzer-types.js";
3
+ import { asObject, evidenceFor, finalizeWorkspace, findAncestorFiles, globMatches, isBelow, joinProjectPath, nearestExistingRoots, normalizeProjectPath, parseJsonc, projectBasename, projectDirname, relativeToWorkspace, sortedUnique, } from "../analyzer-utils.js";
4
+ import { supportFor } from "../support-matrix.js";
5
+ const NODE_LOCKFILES = [
6
+ "package-lock.json",
7
+ "npm-shrinkwrap.json",
8
+ "pnpm-lock.yaml",
9
+ "yarn.lock",
10
+ "bun.lock",
11
+ "bun.lockb",
12
+ ];
13
+ const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
14
+ const MAX_SOURCE_FILES = 2_000;
15
+ function stringRecord(value) {
16
+ const object = asObject(value);
17
+ if (!object)
18
+ return {};
19
+ const result = {};
20
+ for (const [key, child] of Object.entries(object)) {
21
+ if (typeof child === "string")
22
+ result[key] = child;
23
+ }
24
+ return result;
25
+ }
26
+ function allDependencies(data) {
27
+ return {
28
+ ...stringRecord(data.dependencies),
29
+ ...stringRecord(data.devDependencies),
30
+ ...stringRecord(data.peerDependencies),
31
+ ...stringRecord(data.optionalDependencies),
32
+ };
33
+ }
34
+ function workspacePatterns(data) {
35
+ if (Array.isArray(data.workspaces)) {
36
+ return data.workspaces.filter((value) => typeof value === "string");
37
+ }
38
+ const object = asObject(data.workspaces);
39
+ if (object && Array.isArray(object.packages)) {
40
+ return object.packages.filter((value) => typeof value === "string");
41
+ }
42
+ return [];
43
+ }
44
+ async function readManifest(context, path) {
45
+ const text = await context.readText(path);
46
+ if (text === undefined)
47
+ return undefined;
48
+ try {
49
+ const data = asObject(JSON.parse(text));
50
+ if (!data)
51
+ throw new Error("root is not an object");
52
+ const dependencies = allDependencies(data);
53
+ return {
54
+ path,
55
+ root: projectDirname(path),
56
+ data,
57
+ ...(typeof data.name === "string" ? { name: data.name } : {}),
58
+ dependencies,
59
+ scripts: stringRecord(data.scripts),
60
+ workspacePatterns: workspacePatterns(data),
61
+ };
62
+ }
63
+ catch (error) {
64
+ context.addDiagnostic({
65
+ code: "INVALID_PACKAGE_MANIFEST",
66
+ message: `package.json could not be parsed statically: ${String(error)}`,
67
+ severity: "partial-scan",
68
+ paths: [path],
69
+ });
70
+ return undefined;
71
+ }
72
+ }
73
+ function frameworkKinds(manifest) {
74
+ const names = new Set(Object.keys(manifest.dependencies));
75
+ const result = [];
76
+ if (names.has("react") || names.has("react-dom") || names.has("next") || names.has("react-scripts")) {
77
+ result.push("react");
78
+ }
79
+ if (names.has("@nestjs/core") || names.has("@nestjs/common"))
80
+ result.push("nestjs");
81
+ return result;
82
+ }
83
+ function inferNxEcosystem(value) {
84
+ const serialized = JSON.stringify(value).toLowerCase();
85
+ if (/(@nx|@nrwl)\/(nest|node):/.test(serialized) || serialized.includes("@nestjs/")) {
86
+ return "nestjs";
87
+ }
88
+ if (/(@nx|@nrwl)\/(react|next|web):/.test(serialized) || serialized.includes("next:")) {
89
+ return "react";
90
+ }
91
+ return undefined;
92
+ }
93
+ async function loadNodeCandidates(context) {
94
+ const packagePaths = context.inventory.files.filter((path) => projectBasename(path) === "package.json");
95
+ const manifests = (await Promise.all(packagePaths.map((path) => readManifest(context, path)))).filter((manifest) => manifest !== undefined);
96
+ for (const manifest of manifests) {
97
+ const pnpmWorkspace = joinProjectPath(manifest.root, "pnpm-workspace.yaml");
98
+ if (!context.hasFile(pnpmWorkspace) || manifest.workspacePatterns.length > 0)
99
+ continue;
100
+ const text = await context.readText(pnpmWorkspace);
101
+ if (!text)
102
+ continue;
103
+ let inPackages = false;
104
+ for (const line of text.split(/\r?\n/)) {
105
+ if (/^packages\s*:/.test(line)) {
106
+ inPackages = true;
107
+ continue;
108
+ }
109
+ if (inPackages && /^\S/.test(line))
110
+ break;
111
+ const match = inPackages ? /^\s*-\s*["']?([^"'#]+?)["']?\s*(?:#.*)?$/.exec(line) : undefined;
112
+ if (match?.[1])
113
+ manifest.workspacePatterns.push(match[1].trim());
114
+ }
115
+ }
116
+ const candidates = [];
117
+ const rootsClaimedByVirtualProjects = new Set();
118
+ // Nx project.json files describe app boundaries without requiring per-app
119
+ // package.json files. Parse the data only; never load Nx plugins.
120
+ for (const projectPath of context.inventory.files.filter((path) => projectBasename(path) === "project.json")) {
121
+ const text = await context.readText(projectPath);
122
+ if (!text)
123
+ continue;
124
+ let data;
125
+ try {
126
+ data = asObject(JSON.parse(text));
127
+ }
128
+ catch {
129
+ context.addDiagnostic({
130
+ code: "INVALID_NX_PROJECT",
131
+ message: "Nx project.json is not valid static JSON.",
132
+ severity: "partial-scan",
133
+ paths: [projectPath],
134
+ });
135
+ continue;
136
+ }
137
+ if (!data)
138
+ continue;
139
+ const ecosystem = inferNxEcosystem(data);
140
+ if (!ecosystem)
141
+ continue;
142
+ const projectRoot = projectDirname(projectPath);
143
+ const owner = [...manifests]
144
+ .filter((manifest) => isBelow(projectRoot, manifest.root))
145
+ .sort((left, right) => right.root.length - left.root.length)[0];
146
+ if (!owner)
147
+ continue;
148
+ rootsClaimedByVirtualProjects.add(owner.root);
149
+ candidates.push({
150
+ ecosystem,
151
+ root: projectRoot,
152
+ manifest: owner,
153
+ projectConfig: projectPath,
154
+ forcedVariant: ecosystem === "react" ? "nx-react" : "nx-nest",
155
+ ...(typeof data.name === "string" ? { projectName: data.name } : {}),
156
+ ...(typeof data.sourceRoot === "string"
157
+ ? { sourceRoot: normalizeProjectPath(data.sourceRoot) }
158
+ : {}),
159
+ });
160
+ }
161
+ // Nest CLI monorepos also centralize dependencies at the root.
162
+ for (const manifest of manifests) {
163
+ const cliPath = joinProjectPath(manifest.root, "nest-cli.json");
164
+ if (!context.hasFile(cliPath))
165
+ continue;
166
+ const text = await context.readText(cliPath);
167
+ if (!text)
168
+ continue;
169
+ let data;
170
+ try {
171
+ data = asObject(JSON.parse(text));
172
+ }
173
+ catch {
174
+ context.addDiagnostic({
175
+ code: "INVALID_NEST_CLI_CONFIG",
176
+ message: "nest-cli.json is not valid static JSON.",
177
+ severity: "partial-scan",
178
+ paths: [cliPath],
179
+ });
180
+ continue;
181
+ }
182
+ const projects = asObject(data?.projects);
183
+ if (!projects || Object.keys(projects).length === 0)
184
+ continue;
185
+ rootsClaimedByVirtualProjects.add(manifest.root);
186
+ for (const [name, rawProject] of Object.entries(projects).sort(([a], [b]) => a.localeCompare(b))) {
187
+ const project = asObject(rawProject);
188
+ const projectRoot = typeof project?.root === "string"
189
+ ? joinProjectPath(manifest.root, project.root)
190
+ : joinProjectPath(manifest.root, "apps", name);
191
+ const sourceRoot = typeof project?.sourceRoot === "string"
192
+ ? joinProjectPath(manifest.root, project.sourceRoot)
193
+ : joinProjectPath(projectRoot, "src");
194
+ candidates.push({
195
+ ecosystem: "nestjs",
196
+ root: projectRoot,
197
+ manifest,
198
+ projectConfig: cliPath,
199
+ forcedVariant: "nest-monorepo",
200
+ projectName: name,
201
+ sourceRoot,
202
+ });
203
+ }
204
+ }
205
+ for (const manifest of manifests) {
206
+ const kinds = frameworkKinds(manifest);
207
+ for (const ecosystem of kinds) {
208
+ if (rootsClaimedByVirtualProjects.has(manifest.root)) {
209
+ // Keep an unrelated framework at a mixed root, but avoid duplicating
210
+ // the ecosystem already represented by explicit virtual projects.
211
+ if (candidates.some((candidate) => candidate.manifest === manifest && candidate.ecosystem === ecosystem)) {
212
+ continue;
213
+ }
214
+ }
215
+ candidates.push({ ecosystem, root: manifest.root, manifest });
216
+ }
217
+ if (kinds.length > 1 && !rootsClaimedByVirtualProjects.has(manifest.root)) {
218
+ context.addDiagnostic({
219
+ code: "MULTIPLE_FRAMEWORKS_ONE_WORKSPACE",
220
+ message: "One package manifest owns both React and NestJS; target ownership must be explicit.",
221
+ severity: "needs-input",
222
+ paths: [manifest.path],
223
+ });
224
+ }
225
+ }
226
+ const deduplicated = new Map();
227
+ for (const candidate of candidates) {
228
+ const key = `${candidate.ecosystem}\u0000${candidate.root}\u0000${candidate.projectConfig ?? ""}`;
229
+ deduplicated.set(key, candidate);
230
+ }
231
+ return { candidates: [...deduplicated.values()], manifests };
232
+ }
233
+ function packageManagerFromLock(path) {
234
+ const basename = projectBasename(path);
235
+ if (basename === "package-lock.json" || basename === "npm-shrinkwrap.json")
236
+ return "npm";
237
+ if (basename === "pnpm-lock.yaml")
238
+ return "pnpm";
239
+ if (basename === "yarn.lock")
240
+ return "yarn";
241
+ return "bun";
242
+ }
243
+ function declaredPackageManager(manifest) {
244
+ const value = manifest.data.packageManager;
245
+ return typeof value === "string" ? value.split("@")[0] : undefined;
246
+ }
247
+ function nearestLockfiles(context, root) {
248
+ const all = findAncestorFiles(context, root, NODE_LOCKFILES);
249
+ if (all.length === 0)
250
+ return [];
251
+ const closestLength = Math.max(...all.map((path) => projectDirname(path).length));
252
+ return all.filter((path) => projectDirname(path).length === closestLength);
253
+ }
254
+ async function resolveFromPackageLock(context, path, dependency) {
255
+ const text = await context.readText(path, 8 * 1024 * 1024);
256
+ if (!text)
257
+ return undefined;
258
+ try {
259
+ const root = asObject(JSON.parse(text));
260
+ const packages = asObject(root?.packages);
261
+ const installed = asObject(packages?.[`node_modules/${dependency}`]);
262
+ if (typeof installed?.version === "string")
263
+ return installed.version;
264
+ const dependencies = asObject(root?.dependencies);
265
+ const record = asObject(dependencies?.[dependency]);
266
+ return typeof record?.version === "string" ? record.version : undefined;
267
+ }
268
+ catch {
269
+ context.addDiagnostic({
270
+ code: "INVALID_NODE_LOCKFILE",
271
+ message: "The npm lockfile could not be parsed, so exact dependency versions are unresolved.",
272
+ severity: "partial-scan",
273
+ paths: [path],
274
+ });
275
+ return undefined;
276
+ }
277
+ }
278
+ function escapedRegExp(value) {
279
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
280
+ }
281
+ async function resolveFromTextLock(context, path, dependency) {
282
+ const text = await context.readText(path, 8 * 1024 * 1024);
283
+ if (!text)
284
+ return undefined;
285
+ const escaped = escapedRegExp(dependency);
286
+ if (projectBasename(path) === "yarn.lock") {
287
+ const block = new RegExp(`(?:^|\\n)[^\\n]*${escaped}@[^
\\n]*:\\s*\\n(?:[ \\t].*\\n)*?[ \\t]+version[ \\t]+["']?([^"'\\s]+)`, "m").exec(text);
288
+ return block?.[1];
289
+ }
290
+ const packageKey = new RegExp(`(?:^|\\n)\\s{2,}["']?/?${escaped}@([^:\\s"']+)["']?:`, "m").exec(text);
291
+ if (packageKey?.[1])
292
+ return packageKey[1];
293
+ const dependencyBlock = new RegExp(`(?:^|\\n)\\s{2,}["']?${escaped}["']?:\\s*\\n(?:\\s{4,}.*\\n){0,4}?\\s{4,}version:\\s*["']?([^"'\\s(]+)`, "m").exec(text);
294
+ return dependencyBlock?.[1];
295
+ }
296
+ function exactDeclaredVersion(value) {
297
+ if (!value)
298
+ return undefined;
299
+ const match = /^(?:npm:)?(?:v)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(value.trim());
300
+ return match?.[1];
301
+ }
302
+ async function dependencyEvidence(context, manifest, lockfiles, name) {
303
+ const declaredVersion = manifest.dependencies[name];
304
+ if (declaredVersion === undefined)
305
+ return undefined;
306
+ let resolvedVersion;
307
+ let source;
308
+ for (const lockfile of lockfiles) {
309
+ resolvedVersion =
310
+ packageManagerFromLock(lockfile) === "npm"
311
+ ? await resolveFromPackageLock(context, lockfile, name)
312
+ : await resolveFromTextLock(context, lockfile, name);
313
+ if (resolvedVersion) {
314
+ source = lockfile;
315
+ break;
316
+ }
317
+ }
318
+ resolvedVersion ??= exactDeclaredVersion(declaredVersion);
319
+ source ??= resolvedVersion ? manifest.path : undefined;
320
+ return {
321
+ name,
322
+ declaredVersion,
323
+ ...(resolvedVersion === undefined ? {} : { resolvedVersion }),
324
+ ...(source === undefined ? {} : { source }),
325
+ };
326
+ }
327
+ function packageScriptCommand(manager, workspaceRoot, manifestPath, script, category, required) {
328
+ const timeoutMs = category === "format"
329
+ ? 120_000
330
+ : category === "test" || category === "integration"
331
+ ? 600_000
332
+ : 300_000;
333
+ return {
334
+ id: `node:${workspaceRoot}:${category}:${script}`,
335
+ category,
336
+ cwd: workspaceRoot,
337
+ argv: [manager, "run", script],
338
+ source: "package-script",
339
+ sourcePath: manifestPath,
340
+ required,
341
+ network: false,
342
+ timeoutMs,
343
+ risk: "executes-project-code",
344
+ };
345
+ }
346
+ function validationFromScripts(manifest, workspaceRoot, manager) {
347
+ const plan = [];
348
+ const candidates = [
349
+ { names: ["format:check", "format-check"], category: "format", required: false },
350
+ { names: ["lint"], category: "lint", required: true },
351
+ { names: ["typecheck", "type-check", "check:types", "types"], category: "typecheck", required: true },
352
+ { names: ["build"], category: "build", required: true },
353
+ { names: ["test", "test:unit"], category: "test", required: true },
354
+ { names: ["test:e2e", "test:integration", "e2e"], category: "integration", required: false },
355
+ ];
356
+ for (const candidate of candidates) {
357
+ const selected = candidate.names.find((name) => manifest.scripts[name] !== undefined);
358
+ if (selected) {
359
+ plan.push(packageScriptCommand(manager, workspaceRoot, manifest.path, selected, candidate.category, candidate.required));
360
+ }
361
+ }
362
+ return plan;
363
+ }
364
+ async function readTsAliases(context, workspaceRoot, diagnostics) {
365
+ const names = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json", "jsconfig.json"];
366
+ const starting = names
367
+ .map((name) => joinProjectPath(workspaceRoot, name))
368
+ .find((path) => context.hasFile(path));
369
+ const aliases = {};
370
+ const configs = [];
371
+ const visited = new Set();
372
+ async function visit(path, depth) {
373
+ if (depth > 8 || visited.has(path))
374
+ return;
375
+ visited.add(path);
376
+ const text = await context.readText(path);
377
+ if (!text)
378
+ return;
379
+ configs.push(path);
380
+ let data;
381
+ try {
382
+ data = asObject(parseJsonc(text));
383
+ }
384
+ catch {
385
+ diagnostics.push({
386
+ code: "INVALID_TSCONFIG",
387
+ message: "TypeScript/JavaScript config is not valid static JSONC.",
388
+ severity: "partial-scan",
389
+ paths: [path],
390
+ });
391
+ return;
392
+ }
393
+ const compiler = asObject(data?.compilerOptions);
394
+ const paths = asObject(compiler?.paths);
395
+ if (paths) {
396
+ for (const [key, value] of Object.entries(paths)) {
397
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
398
+ aliases[key] = value;
399
+ }
400
+ }
401
+ }
402
+ if (typeof data?.extends !== "string")
403
+ return;
404
+ if (!data.extends.startsWith(".")) {
405
+ diagnostics.push({
406
+ code: "UNRESOLVED_TSCONFIG_EXTENDS",
407
+ message: "A package-based tsconfig extends target cannot be resolved without inspecting installed dependencies.",
408
+ severity: "needs-input",
409
+ paths: [path],
410
+ });
411
+ return;
412
+ }
413
+ let inherited = normalizeProjectPath(posix.normalize(posix.join(projectDirname(path), data.extends)));
414
+ if (!inherited.endsWith(".json"))
415
+ inherited += ".json";
416
+ if (!context.hasFile(inherited)) {
417
+ diagnostics.push({
418
+ code: "UNRESOLVED_TSCONFIG_EXTENDS",
419
+ message: "A relative tsconfig extends target does not exist in the static inventory.",
420
+ severity: "needs-input",
421
+ paths: [path, inherited],
422
+ });
423
+ return;
424
+ }
425
+ await visit(inherited, depth + 1);
426
+ }
427
+ if (starting)
428
+ await visit(starting, 0);
429
+ return { aliases, configs: sortedUnique(configs) };
430
+ }
431
+ function dependenciesMatching(dependencies, names) {
432
+ return names.filter((name) => dependencies[name] !== undefined);
433
+ }
434
+ function routeFromAppFile(file, appRoot) {
435
+ const relative = relativeToWorkspace(appRoot, file);
436
+ if (!/(^|\/)page\.(?:[cm]?[jt]sx?)$/.test(relative))
437
+ return undefined;
438
+ const directory = projectDirname(relative);
439
+ if (directory === ".")
440
+ return "/";
441
+ const segments = directory
442
+ .split("/")
443
+ .filter((segment) => !/^\(.*\)$/.test(segment) && !segment.startsWith("@"));
444
+ return `/${segments.join("/")}`.replace(/\/$/, "") || "/";
445
+ }
446
+ function routeFromPagesFile(file, pagesRoot) {
447
+ const relative = relativeToWorkspace(pagesRoot, file).replace(/\.(?:[cm]?[jt]sx?)$/, "");
448
+ if (relative.startsWith("_") || relative.startsWith("api/"))
449
+ return undefined;
450
+ const withoutIndex = relative === "index" ? "" : relative.replace(/\/index$/, "");
451
+ return `/${withoutIndex}`.replace(/\/$/, "") || "/";
452
+ }
453
+ async function analyzeReactSource(context, root, diagnostics) {
454
+ const sourceFiles = context
455
+ .filesBelow(root)
456
+ .filter((file) => SOURCE_EXTENSIONS.some((extension) => file.endsWith(extension)));
457
+ if (sourceFiles.length > MAX_SOURCE_FILES) {
458
+ diagnostics.push({
459
+ code: "SOURCE_ANALYSIS_LIMIT",
460
+ message: `The workspace has ${sourceFiles.length} source files; static source analysis is capped at ${MAX_SOURCE_FILES}.`,
461
+ severity: "partial-scan",
462
+ paths: [root],
463
+ });
464
+ }
465
+ const selected = sourceFiles.slice(0, MAX_SOURCE_FILES);
466
+ const clientFiles = [];
467
+ const serverFiles = [];
468
+ const routerPaths = [];
469
+ const evidence = [];
470
+ for (const file of selected) {
471
+ const text = await context.readText(file, 512 * 1024);
472
+ if (text === undefined)
473
+ continue;
474
+ const beginning = text.slice(0, 512);
475
+ if (/^[\s;]*["']use client["']/.test(beginning))
476
+ clientFiles.push(file);
477
+ if (/^[\s;]*["']use server["']/.test(beginning))
478
+ serverFiles.push(file);
479
+ for (const match of text.matchAll(/\bpath\s*=\s*["']([^"']+)["']/g)) {
480
+ if (match[1])
481
+ routerPaths.push(match[1]);
482
+ }
483
+ for (const match of text.matchAll(/\bpath\s*:\s*["']([^"']+)["']/g)) {
484
+ if (match[1])
485
+ routerPaths.push(match[1]);
486
+ }
487
+ evidence.push({ path: file, hash: await context.contentHash(file) });
488
+ }
489
+ const routes = [];
490
+ for (const appRoot of [joinProjectPath(root, "app"), joinProjectPath(root, "src/app")]) {
491
+ if (!context.hasDirectory(appRoot))
492
+ continue;
493
+ for (const file of selected.filter((candidate) => isBelow(candidate, appRoot))) {
494
+ const route = routeFromAppFile(file, appRoot);
495
+ if (route)
496
+ routes.push(route);
497
+ }
498
+ }
499
+ for (const pagesRoot of [joinProjectPath(root, "pages"), joinProjectPath(root, "src/pages")]) {
500
+ if (!context.hasDirectory(pagesRoot))
501
+ continue;
502
+ for (const file of selected.filter((candidate) => isBelow(candidate, pagesRoot))) {
503
+ const route = routeFromPagesFile(file, pagesRoot);
504
+ if (route)
505
+ routes.push(route);
506
+ }
507
+ }
508
+ routes.push(...routerPaths);
509
+ return {
510
+ routes: sortedUnique(routes),
511
+ clientFiles: sortedUnique(clientFiles),
512
+ serverFiles: sortedUnique(serverFiles),
513
+ routerPaths: sortedUnique(routerPaths),
514
+ sourceEvidence: evidence,
515
+ };
516
+ }
517
+ function reactVariant(context, candidate) {
518
+ const { dependencies } = candidate.manifest;
519
+ const app = [joinProjectPath(candidate.root, "app"), joinProjectPath(candidate.root, "src/app")].some((path) => context.hasDirectory(path));
520
+ const pages = [
521
+ joinProjectPath(candidate.root, "pages"),
522
+ joinProjectPath(candidate.root, "src/pages"),
523
+ ].some((path) => context.hasDirectory(path));
524
+ let hostVariant;
525
+ if (dependencies.next !== undefined) {
526
+ hostVariant = app && pages ? "next-hybrid" : app ? "next-app-router" : "next-pages-router";
527
+ }
528
+ else if (dependencies.vite !== undefined || context.hasFile(joinProjectPath(candidate.root, "vite.config.ts")) || context.hasFile(joinProjectPath(candidate.root, "vite.config.js"))) {
529
+ const ssr = context.inventory.files.some((path) => isBelow(path, candidate.root) && /(?:entry-server|server-entry)\.[cm]?[jt]sx?$/.test(path));
530
+ hostVariant = ssr ? "vite-ssr" : "vite-spa";
531
+ }
532
+ else if (dependencies["react-scripts"] !== undefined)
533
+ hostVariant = "cra";
534
+ else
535
+ hostVariant = "react-library";
536
+ return { variant: candidate.forcedVariant ?? hostVariant, hostVariant };
537
+ }
538
+ function publicExports(data) {
539
+ const result = [];
540
+ if (typeof data.main === "string")
541
+ result.push(data.main);
542
+ if (typeof data.module === "string")
543
+ result.push(data.module);
544
+ if (typeof data.types === "string")
545
+ result.push(data.types);
546
+ if (typeof data.exports === "string")
547
+ result.push(data.exports);
548
+ else {
549
+ const exportsObject = asObject(data.exports);
550
+ if (exportsObject)
551
+ result.push(...Object.keys(exportsObject));
552
+ }
553
+ return sortedUnique(result);
554
+ }
555
+ function workspaceOwner(manifest, all) {
556
+ return ([...all]
557
+ .filter((candidate) => candidate.workspacePatterns.length > 0 &&
558
+ candidate.root !== manifest.root &&
559
+ isBelow(manifest.root, candidate.root))
560
+ .sort((left, right) => right.root.length - left.root.length)[0] ?? manifest);
561
+ }
562
+ function membersOf(owner, all) {
563
+ if (owner.workspacePatterns.length === 0)
564
+ return [owner.root];
565
+ const includes = owner.workspacePatterns.filter((pattern) => !pattern.startsWith("!"));
566
+ const excludes = owner.workspacePatterns.filter((pattern) => pattern.startsWith("!"));
567
+ return all
568
+ .map((manifest) => manifest.root)
569
+ .filter((root) => {
570
+ const relative = relativeToWorkspace(owner.root, root);
571
+ return (includes.some((pattern) => globMatches(pattern, relative)) &&
572
+ !excludes.some((pattern) => globMatches(pattern.slice(1), relative)));
573
+ });
574
+ }
575
+ async function analyzeReact(context, candidate, manifests) {
576
+ const diagnostics = [];
577
+ const lockfiles = nearestLockfiles(context, candidate.root);
578
+ const managers = sortedUnique(lockfiles.map(packageManagerFromLock));
579
+ const declaredManager = declaredPackageManager(candidate.manifest);
580
+ if (managers.length > 1) {
581
+ diagnostics.push({
582
+ code: "CONFLICTING_NODE_LOCKFILES",
583
+ message: "Multiple package-manager lockfiles own this workspace.",
584
+ severity: "needs-input",
585
+ paths: lockfiles,
586
+ });
587
+ }
588
+ if (declaredManager && managers[0] && declaredManager !== managers[0]) {
589
+ diagnostics.push({
590
+ code: "PACKAGE_MANAGER_MISMATCH",
591
+ message: `package.json declares ${declaredManager}, but the owning lockfile belongs to ${managers[0]}.`,
592
+ severity: "needs-input",
593
+ paths: [candidate.manifest.path, ...lockfiles],
594
+ });
595
+ }
596
+ const manager = (managers[0] ?? declaredManager ?? "npm");
597
+ const versionNames = ["react", "react-dom", "next", "vite", "react-scripts"];
598
+ const dependencyVersions = (await Promise.all(versionNames.map((name) => dependencyEvidence(context, candidate.manifest, lockfiles, name)))).filter((value) => value !== undefined);
599
+ const primary = dependencyVersions.find((dependency) => dependency.name === "next") ??
600
+ dependencyVersions.find((dependency) => dependency.name === "react");
601
+ if (primary && !primary.resolvedVersion) {
602
+ diagnostics.push({
603
+ code: "UNRESOLVED_FRAMEWORK_VERSION",
604
+ message: `${primary.name} has no exact version in a statically readable lockfile.`,
605
+ severity: "needs-input",
606
+ paths: [candidate.manifest.path, ...lockfiles],
607
+ });
608
+ }
609
+ const { aliases, configs } = await readTsAliases(context, candidate.root, diagnostics);
610
+ const source = await analyzeReactSource(context, candidate.root, diagnostics);
611
+ const { variant, hostVariant } = reactVariant(context, candidate);
612
+ const supportDependency = hostVariant === "cra"
613
+ ? dependencyVersions.find((dependency) => dependency.name === "react-scripts")
614
+ : variant === "nx-react"
615
+ ? dependencyVersions.find((dependency) => dependency.name === "react")
616
+ : hostVariant.startsWith("next")
617
+ ? dependencyVersions.find((dependency) => dependency.name === "next")
618
+ : dependencyVersions.find((dependency) => dependency.name === "react");
619
+ const viteVersion = dependencyVersions.find((dependency) => dependency.name === "vite")?.resolvedVersion;
620
+ const viteMajor = viteVersion ? Number(viteVersion.split(".")[0]) : undefined;
621
+ if (hostVariant.startsWith("vite") && viteMajor !== undefined && (viteMajor < 5 || viteMajor > 7)) {
622
+ diagnostics.push({
623
+ code: "UNSUPPORTED_VITE_VERSION",
624
+ message: `Vite ${viteVersion} is outside the declared Vite 5-7 support range.`,
625
+ severity: "unsupported",
626
+ paths: [candidate.manifest.path, ...lockfiles],
627
+ });
628
+ }
629
+ if (candidate.manifest.dependencies.next !== undefined && hostVariant === "next-pages-router") {
630
+ const hasPages = [
631
+ joinProjectPath(candidate.root, "pages"),
632
+ joinProjectPath(candidate.root, "src/pages"),
633
+ ].some((path) => context.hasDirectory(path));
634
+ if (!hasPages) {
635
+ diagnostics.push({
636
+ code: "NEXT_ROUTER_UNDETERMINED",
637
+ message: "Next.js is declared, but neither an App Router nor Pages Router root was found.",
638
+ severity: "needs-input",
639
+ paths: [candidate.root],
640
+ });
641
+ }
642
+ }
643
+ const dependencies = candidate.manifest.dependencies;
644
+ const owner = workspaceOwner(candidate.manifest, manifests);
645
+ const members = membersOf(owner, manifests);
646
+ const packageNames = new Set(manifests.map((manifest) => manifest.name).filter(Boolean));
647
+ const workspaceDependencies = Object.entries(dependencies)
648
+ .filter(([name, version]) => version.startsWith("workspace:") || packageNames.has(name))
649
+ .map(([name]) => name);
650
+ const entryPoints = [
651
+ "src/main.tsx",
652
+ "src/main.jsx",
653
+ "src/index.tsx",
654
+ "src/index.jsx",
655
+ "app/layout.tsx",
656
+ "src/app/layout.tsx",
657
+ "pages/_app.tsx",
658
+ "src/pages/_app.tsx",
659
+ ]
660
+ .map((path) => joinProjectPath(candidate.root, path))
661
+ .filter((path) => context.hasFile(path));
662
+ const manifestsForProfile = sortedUnique([
663
+ candidate.manifest.path,
664
+ owner.path,
665
+ ...(context.hasFile(joinProjectPath(owner.root, "pnpm-workspace.yaml"))
666
+ ? [joinProjectPath(owner.root, "pnpm-workspace.yaml")]
667
+ : []),
668
+ ...(candidate.projectConfig ? [candidate.projectConfig] : []),
669
+ ...configs,
670
+ ]);
671
+ const evidence = await Promise.all([
672
+ evidenceFor(context, candidate.manifest.path, "manifest", "Node package manifest"),
673
+ ...(owner.path !== candidate.manifest.path
674
+ ? [evidenceFor(context, owner.path, "workspace", "Owning Node workspace manifest")]
675
+ : []),
676
+ ...(context.hasFile(joinProjectPath(owner.root, "pnpm-workspace.yaml"))
677
+ ? [evidenceFor(context, joinProjectPath(owner.root, "pnpm-workspace.yaml"), "workspace", "pnpm workspace ownership")]
678
+ : []),
679
+ ...lockfiles.map((path) => evidenceFor(context, path, "lockfile", "Owning dependency lockfile")),
680
+ ...configs.map((path) => evidenceFor(context, path, "config", "Static TypeScript alias configuration")),
681
+ ...(candidate.projectConfig
682
+ ? [evidenceFor(context, candidate.projectConfig, "workspace", "Static workspace project boundary")]
683
+ : []),
684
+ ]);
685
+ for (const item of source.sourceEvidence) {
686
+ evidence.push({
687
+ kind: "source",
688
+ path: item.path,
689
+ detail: "Inspected for routes and server/client directives",
690
+ ...(item.hash === undefined ? {} : { contentHash: item.hash }),
691
+ });
692
+ }
693
+ const sourceRoots = nearestExistingRoots(context, candidate.root, ["src", "app", "pages"]);
694
+ if (candidate.sourceRoot && context.hasDirectory(candidate.sourceRoot))
695
+ sourceRoots.push(candidate.sourceRoot);
696
+ const testRoots = nearestExistingRoots(context, candidate.root, ["test", "tests", "__tests__", "e2e"]);
697
+ const state = dependenciesMatching(dependencies, [
698
+ "@reduxjs/toolkit",
699
+ "redux",
700
+ "zustand",
701
+ "mobx",
702
+ "recoil",
703
+ "jotai",
704
+ ]);
705
+ const data = dependenciesMatching(dependencies, [
706
+ "@tanstack/react-query",
707
+ "swr",
708
+ "@apollo/client",
709
+ "urql",
710
+ ]);
711
+ const forms = dependenciesMatching(dependencies, ["react-hook-form", "formik"]);
712
+ const ui = dependenciesMatching(dependencies, [
713
+ "@mui/material",
714
+ "antd",
715
+ "@chakra-ui/react",
716
+ "@mantine/core",
717
+ "@radix-ui/react-dialog",
718
+ ]);
719
+ const styling = dependenciesMatching(dependencies, [
720
+ "tailwindcss",
721
+ "styled-components",
722
+ "@emotion/react",
723
+ "sass",
724
+ ]);
725
+ const tests = dependenciesMatching(dependencies, [
726
+ "vitest",
727
+ "jest",
728
+ "@playwright/test",
729
+ "cypress",
730
+ "@testing-library/react",
731
+ ]);
732
+ const apiClients = dependenciesMatching(dependencies, ["axios", "graphql-request", "@apollo/client"]);
733
+ const router = dependenciesMatching(dependencies, ["next", "react-router", "react-router-dom"]);
734
+ const workspaceSystem = context.hasFile("nx.json")
735
+ ? "nx"
736
+ : context.hasFile("turbo.json")
737
+ ? "turborepo"
738
+ : "package-workspaces";
739
+ const environmentPrefix = hostVariant.startsWith("vite")
740
+ ? "VITE_"
741
+ : hostVariant.startsWith("next")
742
+ ? "NEXT_PUBLIC_"
743
+ : hostVariant === "cra"
744
+ ? "REACT_APP_"
745
+ : "unknown";
746
+ return finalizeWorkspace({
747
+ schemaVersion: PROJECT_PROFILE_SCHEMA_VERSION,
748
+ id: `react:${candidate.root}${candidate.projectName ? `:${candidate.projectName}` : ""}`,
749
+ relativeRoot: candidate.root,
750
+ ecosystem: "react",
751
+ variant,
752
+ support: supportFor("react", variant, supportDependency?.resolvedVersion),
753
+ ownership: {
754
+ root: owner.root,
755
+ ...(owner.name ? { owner: owner.name } : {}),
756
+ members: sortedUnique(members),
757
+ },
758
+ framework: {
759
+ name: primary?.name === "next" ? "Next.js/React" : "React",
760
+ ...(primary?.declaredVersion ? { declaredVersion: primary.declaredVersion } : {}),
761
+ ...(primary?.resolvedVersion ? { resolvedVersion: primary.resolvedVersion } : {}),
762
+ ...(primary?.source ? { versionSource: primary.source } : {}),
763
+ dependencies: dependencyVersions,
764
+ },
765
+ packageManager: { name: manager, ...(lockfiles[0] ? { lockfile: lockfiles[0] } : {}) },
766
+ runtime: {
767
+ node: typeof candidate.manifest.data.engines === "object"
768
+ ? String(asObject(candidate.manifest.data.engines)?.node ?? "unspecified")
769
+ : "unspecified",
770
+ typescript: dependencies.typescript ?? "not-declared",
771
+ },
772
+ manifests: manifestsForProfile,
773
+ lockfiles,
774
+ sourceRoots,
775
+ testRoots,
776
+ generatedRoots: nearestExistingRoots(context, candidate.root, ["dist", "build", ".next", "generated"]),
777
+ migrationRoots: nearestExistingRoots(context, candidate.root, ["migrations", "prisma/migrations"]),
778
+ protectedRoots: sortedUnique([".git", "node_modules", joinProjectPath(candidate.root, ".env")]),
779
+ moduleAliases: aliases,
780
+ workspaceDependencies,
781
+ publicExports: publicExports(candidate.manifest.data),
782
+ entryPoints,
783
+ routes: source.routes,
784
+ signals: {
785
+ hostVariant,
786
+ workspaceSystem,
787
+ router,
788
+ stateLibraries: state,
789
+ dataLibraries: data,
790
+ formLibraries: forms,
791
+ uiLibraries: ui,
792
+ stylingLibraries: styling,
793
+ apiClients,
794
+ testLibraries: tests,
795
+ environmentExposurePrefix: environmentPrefix,
796
+ clientBoundaryFiles: source.clientFiles,
797
+ serverBoundaryFiles: source.serverFiles,
798
+ staticRouterPaths: source.routerPaths,
799
+ },
800
+ validationPlan: validationFromScripts(candidate.manifest, candidate.manifest.root, manager),
801
+ manualAcceptance: [
802
+ "Visually verify changed UI states and responsive behavior unless existing browser/component tests cover them.",
803
+ "Verify no new router, state, form, data, styling, or component system was introduced without contract permission.",
804
+ ],
805
+ diagnostics,
806
+ evidence,
807
+ });
808
+ }
809
+ function decoratorArray(source, field) {
810
+ const match = new RegExp(`${field}\\s*:\\s*\\[([^\\]]*)\\]`, "s").exec(source);
811
+ if (!match?.[1])
812
+ return [];
813
+ return match[1]
814
+ .split(",")
815
+ .map((value) => value.trim())
816
+ .filter((value) => /^[A-Za-z_$][\w$]*(?:\.(?:forRoot|forRootAsync)\([^)]*\))?$/.test(value));
817
+ }
818
+ async function analyzeNestSource(context, root, diagnostics) {
819
+ const files = context
820
+ .filesBelow(root)
821
+ .filter((file) => SOURCE_EXTENSIONS.some((extension) => file.endsWith(extension)));
822
+ if (files.length > MAX_SOURCE_FILES) {
823
+ diagnostics.push({
824
+ code: "SOURCE_ANALYSIS_LIMIT",
825
+ message: `The workspace has ${files.length} source files; Nest graph analysis is capped at ${MAX_SOURCE_FILES}.`,
826
+ severity: "partial-scan",
827
+ paths: [root],
828
+ });
829
+ }
830
+ const moduleGraph = {};
831
+ const routes = [];
832
+ const globals = [];
833
+ const injectionTokens = [];
834
+ const dynamicModules = [];
835
+ const authSignals = [];
836
+ const tenancySignals = [];
837
+ const evidence = [];
838
+ for (const file of files.slice(0, MAX_SOURCE_FILES)) {
839
+ const source = await context.readText(file, 512 * 1024);
840
+ if (source === undefined)
841
+ continue;
842
+ evidence.push({ path: file, hash: await context.contentHash(file) });
843
+ if (/\.module\.[cm]?[jt]s$/.test(file) && /@Module\s*\(/.test(source)) {
844
+ const imports = decoratorArray(source, "imports");
845
+ const providers = decoratorArray(source, "providers");
846
+ const controllers = decoratorArray(source, "controllers");
847
+ const exported = decoratorArray(source, "exports");
848
+ moduleGraph[file] = { imports, providers, controllers, exports: exported };
849
+ dynamicModules.push(...imports.filter((value) => /\.(?:forRoot|forRootAsync)\(/.test(value)));
850
+ }
851
+ const controller = /@Controller\s*\(\s*(?:["']([^"']*)["']|\{[^}]*path\s*:\s*["']([^"']+)["'][^}]*\})?/.exec(source);
852
+ if (controller) {
853
+ const base = controller[1] ?? controller[2] ?? "";
854
+ for (const method of source.matchAll(/@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:["']([^"']*)["'])?/g)) {
855
+ const suffix = method[2] ?? "";
856
+ routes.push(`/${[base, suffix].filter(Boolean).join("/")}`.replace(/\/{2,}/g, "/"));
857
+ }
858
+ }
859
+ for (const match of source.matchAll(/@Inject\s*\(\s*([^\n,)]+)/g)) {
860
+ if (match[1])
861
+ injectionTokens.push(match[1].trim());
862
+ }
863
+ for (const global of [
864
+ "useGlobalGuards",
865
+ "useGlobalPipes",
866
+ "useGlobalFilters",
867
+ "useGlobalInterceptors",
868
+ "setGlobalPrefix",
869
+ "enableVersioning",
870
+ ]) {
871
+ if (source.includes(`.${global}(`))
872
+ globals.push(global);
873
+ }
874
+ if (/@(?:UseGuards|Public|Roles|Permissions)\b|isPublic|AuthGuard/.test(source))
875
+ authSignals.push(file);
876
+ if (/\b(?:tenant|organisation|organization|ownerId|accountId)\b/i.test(source))
877
+ tenancySignals.push(file);
878
+ }
879
+ return {
880
+ moduleGraph,
881
+ routes: sortedUnique(routes),
882
+ globals: sortedUnique(globals),
883
+ injectionTokens: sortedUnique(injectionTokens),
884
+ dynamicModules: sortedUnique(dynamicModules),
885
+ authSignals: sortedUnique(authSignals),
886
+ tenancySignals: sortedUnique(tenancySignals),
887
+ evidence,
888
+ };
889
+ }
890
+ async function analyzeNest(context, candidate, manifests) {
891
+ const diagnostics = [];
892
+ const lockfiles = nearestLockfiles(context, candidate.root);
893
+ const managers = sortedUnique(lockfiles.map(packageManagerFromLock));
894
+ if (managers.length > 1) {
895
+ diagnostics.push({
896
+ code: "CONFLICTING_NODE_LOCKFILES",
897
+ message: "Multiple package-manager lockfiles own this workspace.",
898
+ severity: "needs-input",
899
+ paths: lockfiles,
900
+ });
901
+ }
902
+ const declaredManager = declaredPackageManager(candidate.manifest);
903
+ if (declaredManager && managers[0] && declaredManager !== managers[0]) {
904
+ diagnostics.push({
905
+ code: "PACKAGE_MANAGER_MISMATCH",
906
+ message: `package.json declares ${declaredManager}, but the owning lockfile belongs to ${managers[0]}.`,
907
+ severity: "needs-input",
908
+ paths: [candidate.manifest.path, ...lockfiles],
909
+ });
910
+ }
911
+ const manager = (managers[0] ?? declaredManager ?? "npm");
912
+ const dependencyNames = [
913
+ "@nestjs/core",
914
+ "@nestjs/common",
915
+ "@nestjs/platform-express",
916
+ "@nestjs/platform-fastify",
917
+ "@nestjs/typeorm",
918
+ "typeorm",
919
+ "@prisma/client",
920
+ "prisma",
921
+ "@nestjs/mongoose",
922
+ "mongoose",
923
+ "@mikro-orm/core",
924
+ "sequelize",
925
+ "@nestjs/sequelize",
926
+ ];
927
+ const dependencyVersions = (await Promise.all(dependencyNames.map((name) => dependencyEvidence(context, candidate.manifest, lockfiles, name)))).filter((value) => value !== undefined);
928
+ const primary = dependencyVersions.find((dependency) => dependency.name === "@nestjs/core");
929
+ if (primary && !primary.resolvedVersion) {
930
+ diagnostics.push({
931
+ code: "UNRESOLVED_FRAMEWORK_VERSION",
932
+ message: "@nestjs/core has no exact version in a statically readable lockfile.",
933
+ severity: "needs-input",
934
+ paths: [candidate.manifest.path, ...lockfiles],
935
+ });
936
+ }
937
+ const express = candidate.manifest.dependencies["@nestjs/platform-express"] !== undefined;
938
+ const fastify = candidate.manifest.dependencies["@nestjs/platform-fastify"] !== undefined;
939
+ if (express && fastify) {
940
+ diagnostics.push({
941
+ code: "CONFLICTING_HTTP_ADAPTERS",
942
+ message: "Both Nest Express and Fastify adapters are declared; the active bootstrap adapter must be explicit.",
943
+ severity: "needs-input",
944
+ paths: [candidate.manifest.path],
945
+ });
946
+ }
947
+ const source = await analyzeNestSource(context, candidate.root, diagnostics);
948
+ const { aliases, configs } = await readTsAliases(context, candidate.root, diagnostics);
949
+ const dependencies = candidate.manifest.dependencies;
950
+ const orms = [
951
+ dependencies.typeorm || dependencies["@nestjs/typeorm"] ? "typeorm" : undefined,
952
+ dependencies["@prisma/client"] || dependencies.prisma ? "prisma" : undefined,
953
+ dependencies.mongoose || dependencies["@nestjs/mongoose"] ? "mongoose" : undefined,
954
+ dependencies["@mikro-orm/core"] ? "mikroorm" : undefined,
955
+ dependencies.sequelize || dependencies["@nestjs/sequelize"] ? "sequelize" : undefined,
956
+ ].filter((value) => value !== undefined);
957
+ const variant = candidate.forcedVariant ?? "standard";
958
+ const owner = workspaceOwner(candidate.manifest, manifests);
959
+ const sourceRoots = nearestExistingRoots(context, candidate.root, ["src"]);
960
+ if (candidate.sourceRoot && context.hasDirectory(candidate.sourceRoot))
961
+ sourceRoots.push(candidate.sourceRoot);
962
+ const entryPoints = context.inventory.files.filter((path) => isBelow(path, candidate.root) && /(?:^|\/)main\.[cm]?[jt]s$/.test(path));
963
+ const evidence = await Promise.all([
964
+ evidenceFor(context, candidate.manifest.path, "manifest", "NestJS package manifest"),
965
+ ...(owner.path !== candidate.manifest.path
966
+ ? [evidenceFor(context, owner.path, "workspace", "Owning Node workspace manifest")]
967
+ : []),
968
+ ...(context.hasFile(joinProjectPath(owner.root, "pnpm-workspace.yaml"))
969
+ ? [evidenceFor(context, joinProjectPath(owner.root, "pnpm-workspace.yaml"), "workspace", "pnpm workspace ownership")]
970
+ : []),
971
+ ...lockfiles.map((path) => evidenceFor(context, path, "lockfile", "Owning dependency lockfile")),
972
+ ...configs.map((path) => evidenceFor(context, path, "config", "Static TypeScript alias configuration")),
973
+ ...(candidate.projectConfig
974
+ ? [evidenceFor(context, candidate.projectConfig, "workspace", "Static Nest/Nx project boundary")]
975
+ : []),
976
+ ]);
977
+ for (const item of source.evidence) {
978
+ evidence.push({
979
+ kind: "source",
980
+ path: item.path,
981
+ detail: "Inspected for Nest modules, DI, routes, guards, and bootstrap globals",
982
+ ...(item.hash === undefined ? {} : { contentHash: item.hash }),
983
+ });
984
+ }
985
+ return finalizeWorkspace({
986
+ schemaVersion: PROJECT_PROFILE_SCHEMA_VERSION,
987
+ id: `nestjs:${candidate.root}${candidate.projectName ? `:${candidate.projectName}` : ""}`,
988
+ relativeRoot: candidate.root,
989
+ ecosystem: "nestjs",
990
+ variant,
991
+ support: supportFor("nestjs", variant, primary?.resolvedVersion),
992
+ ownership: {
993
+ root: owner.root,
994
+ ...(owner.name ? { owner: owner.name } : {}),
995
+ members: membersOf(owner, manifests),
996
+ },
997
+ framework: {
998
+ name: "NestJS",
999
+ ...(primary?.declaredVersion ? { declaredVersion: primary.declaredVersion } : {}),
1000
+ ...(primary?.resolvedVersion ? { resolvedVersion: primary.resolvedVersion } : {}),
1001
+ ...(primary?.source ? { versionSource: primary.source } : {}),
1002
+ dependencies: dependencyVersions,
1003
+ },
1004
+ packageManager: { name: manager, ...(lockfiles[0] ? { lockfile: lockfiles[0] } : {}) },
1005
+ runtime: {
1006
+ node: typeof candidate.manifest.data.engines === "object"
1007
+ ? String(asObject(candidate.manifest.data.engines)?.node ?? "unspecified")
1008
+ : "unspecified",
1009
+ typescript: dependencies.typescript ?? "not-declared",
1010
+ },
1011
+ manifests: sortedUnique([
1012
+ candidate.manifest.path,
1013
+ owner.path,
1014
+ ...(context.hasFile(joinProjectPath(owner.root, "pnpm-workspace.yaml"))
1015
+ ? [joinProjectPath(owner.root, "pnpm-workspace.yaml")]
1016
+ : []),
1017
+ ...(candidate.projectConfig ? [candidate.projectConfig] : []),
1018
+ ...configs,
1019
+ ]),
1020
+ lockfiles,
1021
+ sourceRoots,
1022
+ testRoots: nearestExistingRoots(context, candidate.root, ["test", "tests", "e2e"]),
1023
+ generatedRoots: nearestExistingRoots(context, candidate.root, ["dist", "generated"]),
1024
+ migrationRoots: nearestExistingRoots(context, candidate.root, ["migrations", "prisma/migrations"]),
1025
+ protectedRoots: sortedUnique([".git", "node_modules", joinProjectPath(candidate.root, ".env")]),
1026
+ moduleAliases: aliases,
1027
+ workspaceDependencies: Object.entries(dependencies)
1028
+ .filter(([, version]) => version.startsWith("workspace:"))
1029
+ .map(([name]) => name),
1030
+ publicExports: publicExports(candidate.manifest.data),
1031
+ entryPoints,
1032
+ routes: source.routes,
1033
+ signals: {
1034
+ projectName: candidate.projectName ?? candidate.manifest.name ?? "unknown",
1035
+ httpAdapter: fastify ? (express ? "ambiguous" : "fastify") : "express",
1036
+ orms,
1037
+ moduleGraph: source.moduleGraph,
1038
+ dynamicModules: source.dynamicModules,
1039
+ injectionTokens: source.injectionTokens,
1040
+ bootstrapGlobals: source.globals,
1041
+ authFiles: source.authSignals,
1042
+ tenancyFiles: source.tenancySignals,
1043
+ validationLibrary: dependencies["class-validator"] ? "class-validator" : "not-declared",
1044
+ },
1045
+ validationPlan: validationFromScripts(candidate.manifest, candidate.manifest.root, manager),
1046
+ manualAcceptance: [
1047
+ "For protected endpoints, verify unauthenticated, unauthorized, authorized, and foreign-owner/tenant behavior.",
1048
+ "Schema changes require an explicitly reviewed migration; never enable ORM synchronization as a shortcut.",
1049
+ ],
1050
+ diagnostics,
1051
+ evidence,
1052
+ });
1053
+ }
1054
+ /** Static Node adapter covering both React and NestJS project profiles. */
1055
+ export class NodeEcosystemAdapter {
1056
+ // The interface has a single ecosystem discriminator, while this shared
1057
+ // adapter statically emits two closely related Node ecosystem profiles.
1058
+ ecosystem = "react";
1059
+ async analyze(context) {
1060
+ const { candidates, manifests } = await loadNodeCandidates(context);
1061
+ const profiles = [];
1062
+ for (const candidate of candidates.sort((left, right) => `${left.root}\u0000${left.ecosystem}\u0000${left.projectName ?? ""}`.localeCompare(`${right.root}\u0000${right.ecosystem}\u0000${right.projectName ?? ""}`))) {
1063
+ profiles.push(candidate.ecosystem === "react"
1064
+ ? await analyzeReact(context, candidate, manifests)
1065
+ : await analyzeNest(context, candidate, manifests));
1066
+ }
1067
+ return profiles;
1068
+ }
1069
+ }
1070
+ export const nodeEcosystemAdapter = new NodeEcosystemAdapter();
1071
+ //# sourceMappingURL=node.js.map