@dev-tren/mapd 0.21.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/MASTER_PROMPT.md +134 -0
  3. package/README.md +494 -0
  4. package/SETUP.md +108 -0
  5. package/UAT.md +77 -0
  6. package/package.json +56 -0
  7. package/src/adapters/github-app.js +79 -0
  8. package/src/agents/anthropicClient.js +18 -0
  9. package/src/agents/llm.js +196 -0
  10. package/src/agents/modelResolver.js +87 -0
  11. package/src/agents/provider.js +222 -0
  12. package/src/chat/commandRunner.js +86 -0
  13. package/src/chat/commands.js +275 -0
  14. package/src/chat/intent.js +87 -0
  15. package/src/chat/llmIntent.js +118 -0
  16. package/src/chat/repl.js +471 -0
  17. package/src/cli.js +1408 -0
  18. package/src/config/index.js +197 -0
  19. package/src/config/schema.js +119 -0
  20. package/src/core/assist.js +64 -0
  21. package/src/core/audit.js +63 -0
  22. package/src/core/changes.js +110 -0
  23. package/src/core/confidence.js +0 -0
  24. package/src/core/configLint.js +141 -0
  25. package/src/core/diagnose.js +262 -0
  26. package/src/core/docs.js +140 -0
  27. package/src/core/doctor.js +134 -0
  28. package/src/core/envFiles.js +43 -0
  29. package/src/core/events.js +53 -0
  30. package/src/core/evidence.js +212 -0
  31. package/src/core/findingScoring.js +20 -0
  32. package/src/core/fix.js +192 -0
  33. package/src/core/fixApply.js +172 -0
  34. package/src/core/frameworkEntries.js +247 -0
  35. package/src/core/gates.js +209 -0
  36. package/src/core/graph.js +467 -0
  37. package/src/core/grounding.js +235 -0
  38. package/src/core/handoff.js +157 -0
  39. package/src/core/importResolver.js +218 -0
  40. package/src/core/improve.js +226 -0
  41. package/src/core/integrate.js +169 -0
  42. package/src/core/intelligence.js +212 -0
  43. package/src/core/modernize.js +370 -0
  44. package/src/core/parseCache.js +64 -0
  45. package/src/core/parser.js +536 -0
  46. package/src/core/policy.js +65 -0
  47. package/src/core/polyglot.js +333 -0
  48. package/src/core/proc.js +25 -0
  49. package/src/core/reachability.js +543 -0
  50. package/src/core/regression.js +193 -0
  51. package/src/core/resolution.js +92 -0
  52. package/src/core/retry.js +61 -0
  53. package/src/core/review.js +219 -0
  54. package/src/core/score.js +338 -0
  55. package/src/core/security.js +0 -0
  56. package/src/core/session.js +143 -0
  57. package/src/core/solutions.js +254 -0
  58. package/src/core/staleness.js +45 -0
  59. package/src/core/testGuidance.js +226 -0
  60. package/src/core/theme.js +50 -0
  61. package/src/core/trace.js +151 -0
  62. package/src/core/verify.js +123 -0
  63. package/src/core/view.js +221 -0
  64. package/src/core/viewServer.js +88 -0
  65. package/src/core/watch.js +76 -0
  66. package/src/core/workspace.js +115 -0
  67. package/src/mcp/server.js +48 -0
  68. package/src/mcp/tools.js +423 -0
  69. package/src/server.js +84 -0
@@ -0,0 +1,543 @@
1
+ /**
2
+ * reachability.js — replaces the old binary "covered vs orphan" model with a
3
+ * real taxonomy. Static import/call tracing genuinely cannot see everything:
4
+ * runtime directory-scan plugin loaders, and test runners that discover
5
+ * specs by globbing a config-declared directory rather than importing them,
6
+ * are both legitimate, common patterns that static analysis cannot resolve.
7
+ *
8
+ * The design rule (same as frameworkEntries.js): every detector here either
9
+ * verifies its finding against real AST structure and a real file/directory
10
+ * that exists in the project, or it detects nothing. We never claim a file
11
+ * IS used just because it sits under a dynamically-loaded directory — only
12
+ * that its reachability is legitimately unverifiable by import tracing, so
13
+ * it must not be asserted dead either. That's a distinct bucket from "truly
14
+ * orphaned," not a guess at aliveness.
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { parse } from "@babel/parser";
20
+ import _traverse from "@babel/traverse";
21
+
22
+ const traverse = _traverse.default ?? _traverse;
23
+ const BABEL_OPTS = { sourceType: "unambiguous", errorRecovery: true, plugins: ["typescript", "jsx", "decorators-legacy"] };
24
+
25
+ const GENERATED_HEADER_RE = /@generated\b|auto-?generated|generated by|do not edit(?:\s+by hand)?/i;
26
+ const GENERATED_NAME_SEGMENTS = new Set(["bundle", "bundled", "min", "generated"]);
27
+
28
+ /**
29
+ * Filename convention, checked as a whole path segment (split on -._), not a
30
+ * bare substring — so "plan-mode-bundle.cjs" (real-world hyphenated bundler
31
+ * output) matches just as well as "app.bundle.js" (dot-separated), while a
32
+ * genuinely hand-written file like "bundler.js" (a file *about* bundling,
33
+ * not *produced by* one) does not.
34
+ */
35
+ function isGeneratedFilename(relFile) {
36
+ const base = path.posix.basename(relFile).replace(/\.[cm]?[jt]sx?$/i, "");
37
+ return base.split(/[-._]/).some((seg) => GENERATED_NAME_SEGMENTS.has(seg.toLowerCase()));
38
+ }
39
+
40
+ const BUNDLER_TOOLS_RE = /\b(esbuild|webpack|rollup|parcel|vite|tsc|swc|rspack)\b/;
41
+ const OUTPUT_FLAG_RES = [
42
+ /--outfile[= ]+["']?([^\s"']+)/,
43
+ /--outdir(?:ir)?[= ]+["']?([^\s"']+)/i,
44
+ /--output(?:-path)?[= ]+["']?([^\s"']+)/,
45
+ /--file[= ]+["']?([^\s"']+)/,
46
+ /(?:^|\s)-o\s+["']?([^\s"']+)/,
47
+ ];
48
+
49
+ /**
50
+ * Recognizes a package.json script that invokes a known bundler/compiler CLI
51
+ * and declares its output via a real, literal flag (`--outfile=X`, `-o X`,
52
+ * etc.) — the strongest possible signal a file is generated, since the
53
+ * project's OWN build configuration says so, not a naming guess. This is
54
+ * what catches a real bundler output whose filename doesn't happen to match
55
+ * any naming convention at all.
56
+ */
57
+ export function detectBundlerOutputs(pkg) {
58
+ const outputs = [];
59
+ for (const [scriptName, cmd] of Object.entries(pkg?.scripts ?? {})) {
60
+ if (typeof cmd !== "string" || !BUNDLER_TOOLS_RE.test(cmd)) continue;
61
+ for (const re of OUTPUT_FLAG_RES) {
62
+ const m = re.exec(cmd);
63
+ if (m) outputs.push({ path: path.posix.normalize(m[1].replace(/^["']|["']$/g, "")), script: scriptName });
64
+ }
65
+ }
66
+ return outputs;
67
+ }
68
+
69
+ const BUNDLER_CONFIG_FILES = [
70
+ "webpack.config.js", "webpack.config.cjs", "webpack.config.mjs", "webpack.config.ts",
71
+ "rollup.config.js", "rollup.config.cjs", "rollup.config.mjs", "rollup.config.ts",
72
+ ];
73
+
74
+ /** Resolves `path.join/resolve(__dirname, "lit", ...)` to its literal parts, or a plain StringLiteral to its value. */
75
+ function resolveLiteralOrDirnamePath(valueNode) {
76
+ if (valueNode?.type === "StringLiteral") return valueNode.value;
77
+ if (valueNode?.type === "CallExpression") {
78
+ const callee = valueNode.callee;
79
+ const isPathJoinOrResolve = callee?.type === "MemberExpression" && callee.object?.name === "path" &&
80
+ (callee.property?.name === "join" || callee.property?.name === "resolve");
81
+ if (!isPathJoinOrResolve) return null;
82
+ const args = valueNode.arguments ?? [];
83
+ if (!args.some((a) => a.type === "Identifier" && a.name === "__dirname")) return null;
84
+ const literalParts = args.filter((a) => a.type === "StringLiteral").map((a) => a.value);
85
+ return literalParts.length ? literalParts.join("/") : null;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * webpack/rollup declare their output in a config FILE, not an npm-script
92
+ * flag — a project building through `webpack --config webpack.config.js`
93
+ * gives detectBundlerOutputs (script-string parsing) nothing to find, so its
94
+ * bundle would be scanned as source, the exact bug just fixed for esbuild.
95
+ * Same verification model as detectViteEntries: anchor on the literal
96
+ * `output` key, only trust plain string literals or the well-known
97
+ * `path.join/resolve(__dirname, ...)` pattern — anything dynamic is left
98
+ * undetected rather than guessed at.
99
+ * webpack: output: { path: path.resolve(__dirname, "electron"), filename: "main.js" }
100
+ * rollup: output: { file: "electron/bundle.cjs" } or output: { dir: "out" } (arrays too)
101
+ */
102
+ export function detectConfigBundlerOutputs(rootDir) {
103
+ const outputs = [];
104
+ for (const rel of BUNDLER_CONFIG_FILES) {
105
+ const abs = path.join(rootDir, rel);
106
+ if (!fs.existsSync(abs)) continue;
107
+ let ast;
108
+ try { ast = parse(fs.readFileSync(abs, "utf8"), BABEL_OPTS); } catch { continue; }
109
+
110
+ traverse(ast, {
111
+ ObjectProperty(p) {
112
+ const keyName = p.node.key?.name ?? p.node.key?.value;
113
+ if (keyName !== "output") return;
114
+ const outputObjs = p.node.value.type === "ArrayExpression"
115
+ ? p.node.value.elements.filter((el) => el?.type === "ObjectExpression")
116
+ : p.node.value.type === "ObjectExpression" ? [p.node.value] : [];
117
+
118
+ for (const obj of outputObjs) {
119
+ const prop = (name) => obj.properties.find((pr) => (pr.key?.name ?? pr.key?.value) === name);
120
+ // rollup: `file` (single output) / `dir` (whole directory)
121
+ const file = resolveLiteralOrDirnamePath(prop("file")?.value);
122
+ if (file) { outputs.push({ path: path.posix.normalize(file), script: rel }); continue; }
123
+ const dir = resolveLiteralOrDirnamePath(prop("dir")?.value);
124
+ if (dir) { outputs.push({ path: path.posix.normalize(dir), script: rel }); continue; }
125
+ // webpack: `path` (directory) + optional `filename`
126
+ const outPath = resolveLiteralOrDirnamePath(prop("path")?.value);
127
+ if (!outPath) continue;
128
+ const filename = prop("filename")?.value?.type === "StringLiteral" ? prop("filename").value.value : null;
129
+ // a webpack filename with substitution tokens ([name].js etc.) means
130
+ // we can't name the exact file — fall back to the output directory,
131
+ // which is still fully declared, not guessed.
132
+ const exact = filename && !filename.includes("[") ? path.posix.join(outPath, filename) : outPath;
133
+ outputs.push({ path: path.posix.normalize(exact), script: rel });
134
+ }
135
+ },
136
+ });
137
+ }
138
+ return outputs;
139
+ }
140
+
141
+ /**
142
+ * Filename convention, a declared bundler output (npm-script flag OR
143
+ * webpack/rollup config file), or an explicit header marker — never a
144
+ * size/minification guess. `bundlerOutputs` (from detectBundlerOutputs +
145
+ * detectConfigBundlerOutputs) is optional; passing it in lets a file be
146
+ * recognized as generated even with no naming convention and no header
147
+ * comment at all (common for esbuild/webpack output, which often has neither).
148
+ */
149
+ export function isGeneratedArtifact(rootDir, relFile, bundlerOutputs = []) {
150
+ if (isGeneratedFilename(relFile)) return { generated: true, reason: `filename convention (${path.posix.basename(relFile)})` };
151
+ const declared = bundlerOutputs.find((o) => o.path === relFile || relFile.startsWith(`${o.path}/`));
152
+ if (declared) return { generated: true, reason: `declared build output of "${declared.script}"` };
153
+ let head;
154
+ try { head = fs.readFileSync(path.join(rootDir, relFile), "utf8").slice(0, 300); } catch { return { generated: false }; }
155
+ const m = GENERATED_HEADER_RE.exec(head);
156
+ if (m) return { generated: true, reason: `header comment ("${m[0]}")` };
157
+ return { generated: false };
158
+ }
159
+
160
+ function allAncestorDirs(fileSet) {
161
+ const dirs = new Set();
162
+ for (const f of fileSet) {
163
+ let dir = path.posix.dirname(f);
164
+ while (dir && dir !== ".") { dirs.add(dir); dir = path.posix.dirname(dir); }
165
+ }
166
+ return dirs;
167
+ }
168
+
169
+ /**
170
+ * `<something>.load(path.join(__dirname, "tools"))`-style runtime directory
171
+ * loaders: any call whose argument resolves — via the same verified
172
+ * `path.join(__dirname, ...)`/`path.resolve(__dirname, ...)` pattern already
173
+ * used for Electron preload resolution — to a real DIRECTORY (not a single
174
+ * file) in the project. We record the exact call site as evidence; we do
175
+ * not attempt to guess which files inside actually get loaded.
176
+ */
177
+ export function detectDynamicDirectoryReferences(rootDir, files, fileSet) {
178
+ const dirSet = allAncestorDirs(fileSet);
179
+ const results = [];
180
+ for (const f of files) {
181
+ const relFile = f.file.split(path.sep).join("/");
182
+ if (!/\.(js|ts|jsx|tsx|mjs|cjs|mts|cts)$/.test(relFile)) continue;
183
+ let raw;
184
+ try { raw = fs.readFileSync(path.join(rootDir, relFile), "utf8"); } catch { continue; }
185
+ if (!raw.includes("__dirname")) continue; // cheap pre-filter, not the verification itself
186
+ let ast;
187
+ try { ast = parse(raw, BABEL_OPTS); } catch { continue; }
188
+ const fileDirPosix = path.posix.dirname(relFile);
189
+
190
+ traverse(ast, {
191
+ CallExpression(p) {
192
+ const callee = p.node.callee;
193
+ const isPathJoinOrResolve = callee?.type === "MemberExpression" && callee.object?.name === "path" &&
194
+ (callee.property?.name === "join" || callee.property?.name === "resolve");
195
+ if (!isPathJoinOrResolve) return;
196
+ const args = p.node.arguments ?? [];
197
+ if (!args.some((a) => a.type === "Identifier" && a.name === "__dirname")) return;
198
+ const literalParts = args.filter((a) => a.type === "StringLiteral").map((a) => a.value);
199
+ if (!literalParts.length) return;
200
+ const resolved = path.posix.normalize(path.posix.join(fileDirPosix, literalParts.join("/")));
201
+ if (dirSet.has(resolved) && !fileSet.has(resolved)) {
202
+ results.push({ dir: resolved, referencedFrom: relFile, line: p.node.loc?.start.line ?? null });
203
+ }
204
+ },
205
+ });
206
+ }
207
+ return results;
208
+ }
209
+
210
+ /**
211
+ * Template-literal dynamic loads: `import(\`./plugins/${name}.js\`)` and
212
+ * `require(\`./commands/${cmd}.js\`)` — the classic plugin/command-registry
213
+ * pattern. Static analysis cannot know which file gets loaded, but the
214
+ * literal PREFIX before the first interpolation names the directory being
215
+ * loaded from, and that part is fully verifiable: we resolve it relative to
216
+ * the importing file and only report it when it is a real directory that
217
+ * contains project files. Same design rule as every other detector here —
218
+ * verified against real AST structure and a real directory, or nothing.
219
+ */
220
+ export function detectDynamicImportPrefixes(rootDir, files, fileSet) {
221
+ const dirSet = allAncestorDirs(fileSet);
222
+ const results = [];
223
+ for (const f of files) {
224
+ const relFile = f.file.split(path.sep).join("/");
225
+ if (!/\.(js|ts|jsx|tsx|mjs|cjs|mts|cts)$/.test(relFile)) continue;
226
+ let raw;
227
+ try { raw = fs.readFileSync(path.join(rootDir, relFile), "utf8"); } catch { continue; }
228
+ if (!raw.includes("import(") && !raw.includes("require(")) continue; // cheap pre-filter, not the verification itself
229
+ let ast;
230
+ try { ast = parse(raw, BABEL_OPTS); } catch { continue; }
231
+ const fileDirPosix = path.posix.dirname(relFile);
232
+
233
+ // only template literals WITH interpolation — a fully-static specifier
234
+ // is ordinary import resolution's job, not a dynamic edge
235
+ const checkSpecifier = (arg, node, via) => {
236
+ if (arg?.type !== "TemplateLiteral" || !arg.expressions?.length) return;
237
+ const prefix = arg.quasis?.[0]?.value?.cooked ?? "";
238
+ if (!prefix.startsWith("./") && !prefix.startsWith("../")) return; // bare/absolute specifiers can't be verified against the project tree
239
+ const dirPart = prefix.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : ".";
240
+ const resolved = path.posix.normalize(path.posix.join(fileDirPosix, dirPart));
241
+ if (dirSet.has(resolved)) {
242
+ results.push({ dir: resolved, referencedFrom: relFile, line: node.loc?.start.line ?? null, via });
243
+ }
244
+ };
245
+
246
+ traverse(ast, {
247
+ // Babel 8 parses `import(x)` as its own ImportExpression node
248
+ ImportExpression(p) {
249
+ checkSpecifier(p.node.source, p.node, "dynamic-import-template");
250
+ },
251
+ CallExpression(p) {
252
+ const callee = p.node.callee;
253
+ // Babel 7 shape for `import(x)` was CallExpression with an Import callee
254
+ if (callee?.type === "Import") return checkSpecifier(p.node.arguments?.[0], p.node, "dynamic-import-template");
255
+ if (callee?.type === "Identifier" && callee.name === "require") return checkSpecifier(p.node.arguments?.[0], p.node, "require-template");
256
+ },
257
+ });
258
+ }
259
+ return results;
260
+ }
261
+
262
+ /**
263
+ * Vite's `import.meta.glob("./modules/*.js")` — the files matching the glob
264
+ * really are bundled and loaded (lazily) at runtime, but no ImportDeclaration
265
+ * ever names them. AST-verified shape: a call whose callee is
266
+ * `import.meta.glob` (MetaProperty + MemberExpression, not a regex guess)
267
+ * with literal string glob argument(s). Matched against the real file set
268
+ * with the same deterministic globToRegex used for annotations — a glob that
269
+ * matches nothing detects nothing.
270
+ */
271
+ export function detectImportMetaGlobs(rootDir, files, fileSet) {
272
+ const results = [];
273
+ for (const f of files) {
274
+ const relFile = f.file.split(path.sep).join("/");
275
+ if (!/\.(js|ts|jsx|tsx|mjs|cjs|mts|cts)$/.test(relFile)) continue;
276
+ let raw;
277
+ try { raw = fs.readFileSync(path.join(rootDir, relFile), "utf8"); } catch { continue; }
278
+ if (!raw.includes("import.meta.glob")) continue; // cheap pre-filter, not the verification itself
279
+ let ast;
280
+ try { ast = parse(raw, BABEL_OPTS); } catch { continue; }
281
+ const fileDirPosix = path.posix.dirname(relFile);
282
+
283
+ traverse(ast, {
284
+ CallExpression(p) {
285
+ const callee = p.node.callee;
286
+ const isImportMetaGlob = callee?.type === "MemberExpression" &&
287
+ callee.object?.type === "MetaProperty" &&
288
+ callee.object.meta?.name === "import" && callee.object.property?.name === "meta" &&
289
+ callee.property?.name === "glob";
290
+ if (!isImportMetaGlob) return;
291
+ const arg = p.node.arguments?.[0];
292
+ const patterns = arg?.type === "StringLiteral" ? [arg.value]
293
+ : arg?.type === "ArrayExpression" ? arg.elements.filter((el) => el?.type === "StringLiteral").map((el) => el.value)
294
+ : [];
295
+ for (const pattern of patterns) {
296
+ if (!pattern.startsWith("./") && !pattern.startsWith("../") && !pattern.startsWith("/")) continue;
297
+ const resolved = pattern.startsWith("/")
298
+ ? pattern.slice(1) // Vite: leading / = project root
299
+ : path.posix.normalize(path.posix.join(fileDirPosix, pattern));
300
+ const re = globToRegex(resolved);
301
+ for (const file of fileSet) {
302
+ if (re.test(file)) results.push({ file, referencedFrom: relFile, line: p.node.loc?.start.line ?? null, via: `import.meta.glob("${pattern}")` });
303
+ }
304
+ }
305
+ },
306
+ });
307
+ }
308
+ return results;
309
+ }
310
+
311
+ /**
312
+ * `new Worker(new URL("./worker.js", import.meta.url))` — the standard
313
+ * bundler-recognized worker pattern. The worker file is genuinely executed
314
+ * at runtime but never appears in an import statement, so it otherwise reads
315
+ * as orphaned. AST-verified: NewExpression Worker/SharedWorker whose first
316
+ * argument is `new URL(<string literal>, import.meta.url)`, resolved relative
317
+ * to the referencing file and only reported when the target file exists.
318
+ */
319
+ export function detectWorkerUrls(rootDir, files, fileSet) {
320
+ const results = [];
321
+ for (const f of files) {
322
+ const relFile = f.file.split(path.sep).join("/");
323
+ if (!/\.(js|ts|jsx|tsx|mjs|cjs|mts|cts)$/.test(relFile)) continue;
324
+ let raw;
325
+ try { raw = fs.readFileSync(path.join(rootDir, relFile), "utf8"); } catch { continue; }
326
+ if (!raw.includes("Worker")) continue; // cheap pre-filter, not the verification itself
327
+ let ast;
328
+ try { ast = parse(raw, BABEL_OPTS); } catch { continue; }
329
+ const fileDirPosix = path.posix.dirname(relFile);
330
+
331
+ traverse(ast, {
332
+ NewExpression(p) {
333
+ const calleeName = p.node.callee?.type === "Identifier" ? p.node.callee.name : null;
334
+ if (calleeName !== "Worker" && calleeName !== "SharedWorker") return;
335
+ const arg = p.node.arguments?.[0];
336
+ const isNewUrl = arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && arg.callee.name === "URL";
337
+ if (!isNewUrl) return;
338
+ const spec = arg.arguments?.[0];
339
+ const base = arg.arguments?.[1];
340
+ const isImportMetaUrl = base?.type === "MemberExpression" &&
341
+ base.object?.type === "MetaProperty" && base.property?.name === "url";
342
+ if (spec?.type !== "StringLiteral" || !isImportMetaUrl) return;
343
+ const resolved = path.posix.normalize(path.posix.join(fileDirPosix, spec.value));
344
+ if (fileSet.has(resolved)) {
345
+ results.push({ file: resolved, referencedFrom: relFile, line: p.node.loc?.start.line ?? null, via: `new ${calleeName}(new URL(...))` });
346
+ }
347
+ },
348
+ });
349
+ }
350
+ return results;
351
+ }
352
+
353
+ const TEST_GLOB_CONFIGS = [
354
+ "playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs",
355
+ "vitest.config.ts", "vitest.config.js", "vitest.config.mjs", "vitest.config.cjs",
356
+ "jest.config.js", "jest.config.ts", "jest.config.cjs", "jest.config.mjs",
357
+ "cypress.config.ts", "cypress.config.js",
358
+ ];
359
+ const TEST_GLOB_KEYS = new Set(["testDir", "testMatch", "include", "roots", "specPattern", "testRegex"]);
360
+
361
+ /** Everything up to the first glob metacharacter, with any trailing partial segment trimmed. */
362
+ function staticPrefixOfGlob(globStr) {
363
+ const idx = globStr.search(/[*{]/);
364
+ if (idx === -1) return globStr.replace(/\/$/, ""); // no glob metachar at all — the whole string is a literal path
365
+ const prefix = globStr.slice(0, idx);
366
+ return prefix.replace(/\/[^/]*$/, ""); // trim the partial segment before the wildcard, e.g. "tests/**" -> "tests"
367
+ }
368
+
369
+ /**
370
+ * Playwright/Vitest/Jest/Cypress configs declare WHERE their specs live as a
371
+ * string or glob value (`testDir`, `include`, ...) — the runner globs that
372
+ * directory at runtime, it never imports the spec files, so the existing
373
+ * tooling-config entry-point registration (frameworkEntries.js) doesn't help
374
+ * here. We only trust literal string values under a fixed, known key set.
375
+ */
376
+ export function detectTestGlobDirectories(rootDir, fileSet) {
377
+ const results = [];
378
+ for (const rel of TEST_GLOB_CONFIGS) {
379
+ const abs = path.join(rootDir, rel);
380
+ if (!fs.existsSync(abs)) continue;
381
+ let ast;
382
+ try { ast = parse(fs.readFileSync(abs, "utf8"), BABEL_OPTS); } catch { continue; }
383
+
384
+ traverse(ast, {
385
+ ObjectProperty(p) {
386
+ const keyName = p.node.key?.name ?? p.node.key?.value;
387
+ if (!TEST_GLOB_KEYS.has(keyName)) return;
388
+ const value = p.node.value;
389
+ const literals = [];
390
+ if (value.type === "StringLiteral") literals.push(value.value);
391
+ else if (value.type === "ArrayExpression") for (const el of value.elements) if (el?.type === "StringLiteral") literals.push(el.value);
392
+
393
+ for (const lit of literals) {
394
+ const prefix = staticPrefixOfGlob(lit);
395
+ if (!prefix) continue;
396
+ const resolved = path.posix.normalize(path.posix.join(".", prefix));
397
+ if ([...fileSet].some((f) => f === resolved || f.startsWith(`${resolved}/`))) {
398
+ results.push({ dir: resolved, referencedFrom: rel, key: keyName, line: p.node.loc?.start.line ?? null });
399
+ }
400
+ }
401
+ },
402
+ });
403
+ }
404
+ return results;
405
+ }
406
+
407
+ /** Tiny deterministic glob: `**` crosses directories, `*` stays within a segment, everything else literal. */
408
+ export function globToRegex(pattern) {
409
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
410
+ const body = escaped
411
+ .replace(/\*\*\//g, "ANYDIR")
412
+ .replace(/\*\*/g, "ANY")
413
+ .replace(/\*/g, "[^/]*")
414
+ .replace(/ANYDIR/g, "(?:.*/)?")
415
+ .replace(/ANY/g, ".*");
416
+ return new RegExp(`^${body}$`);
417
+ }
418
+
419
+ /**
420
+ * User-asserted classifications from .mapdrc `project.annotations` — the
421
+ * honest answer for things static analysis structurally cannot know (a
422
+ * timestamped results directory, a plugin folder loaded by a pattern mapd
423
+ * doesn't recognize). Mapd never guesses these; the user states them, and
424
+ * every downstream mention is labeled as user-asserted, never as detected.
425
+ * Returns [{ file, classification, pattern }] for files matching any pattern.
426
+ */
427
+ export function applyAnnotations(fileSet, annotations = {}) {
428
+ // a value is either a bare classification string or { classification, reason?, source?, date? }
429
+ const compiled = Object.entries(annotations).map(([pattern, raw]) => ({
430
+ pattern,
431
+ classification: typeof raw === "string" ? raw : raw?.classification,
432
+ re: globToRegex(pattern),
433
+ }));
434
+ if (!compiled.length) return [];
435
+ const matches = [];
436
+ for (const file of fileSet) {
437
+ const hit = compiled.find((c) => c.re.test(file));
438
+ if (hit) matches.push({ file, classification: hit.classification, pattern: hit.pattern });
439
+ }
440
+ return matches;
441
+ }
442
+
443
+ /**
444
+ * Classifies generated-file status for EVERY file in the project, not just
445
+ * unreachable ones — a bundler output is generated regardless of whether
446
+ * something else in the project happens to import/require it. This is what
447
+ * lets modernize.js exclude a real, reachable bundle (e.g. one loaded by an
448
+ * Electron main process) from its scan, not just orphan-cluster.
449
+ * `annotations` adds user-asserted "generated" classifications on top of the
450
+ * detected ones, labeled as such.
451
+ */
452
+ export function detectGeneratedFiles(rootDir, fileSet, pkg, annotations = {}) {
453
+ const bundlerOutputs = [...detectBundlerOutputs(pkg), ...detectConfigBundlerOutputs(rootDir)];
454
+ const annotated = new Map(
455
+ applyAnnotations(fileSet, annotations)
456
+ .filter((a) => a.classification === "generated")
457
+ .map((a) => [a.file, a.pattern]),
458
+ );
459
+ const generated = [];
460
+ for (const file of fileSet) {
461
+ if (annotated.has(file)) {
462
+ generated.push({ file, reason: `user annotation in .mapdrc ("${annotated.get(file)}": "generated")` });
463
+ continue;
464
+ }
465
+ const result = isGeneratedArtifact(rootDir, file, bundlerOutputs);
466
+ if (result.generated) generated.push({ file, reason: result.reason });
467
+ }
468
+ return generated;
469
+ }
470
+
471
+ /**
472
+ * The single entry point graph.js calls: classifies every file that ISN'T
473
+ * already statically reachable (not in `covered`) and isn't a test file (the
474
+ * two categories graph.js already computes) into: generated artifact,
475
+ * dynamically-loaded/unverifiable, user-asserted intentional-dormant,
476
+ * heuristic-unverified (parsed by a heuristic language adapter, so orphan
477
+ * status structurally cannot be asserted), or truly orphaned. Every
478
+ * non-orphan classification carries its evidence (the exact file/line or
479
+ * annotation that justified excluding it) — never a bare, unexplained
480
+ * exclusion. `generatedFiles` is detectGeneratedFiles' project-wide result,
481
+ * passed in so it's computed once.
482
+ */
483
+ export function classifyUncoveredFiles(rootDir, files, fileSet, uncovered, generatedFiles, annotations = {}) {
484
+ const generatedSet = new Set(generatedFiles.map((g) => g.file));
485
+ const generatedReasons = new Map(generatedFiles.map((g) => [g.file, g.reason]));
486
+ const parserKindByFile = new Map(files.map((f) => [f.file.split(path.sep).join("/"), f.parserKind ?? "ast"]));
487
+
488
+ const dynamicRefs = [
489
+ ...detectDynamicDirectoryReferences(rootDir, files, fileSet),
490
+ ...detectDynamicImportPrefixes(rootDir, files, fileSet),
491
+ ...detectTestGlobDirectories(rootDir, fileSet),
492
+ ];
493
+ const dynamicDirs = new Map(); // dir -> evidence[]
494
+ for (const ref of dynamicRefs) {
495
+ if (!dynamicDirs.has(ref.dir)) dynamicDirs.set(ref.dir, []);
496
+ dynamicDirs.get(ref.dir).push(ref);
497
+ }
498
+ // file-level dynamic references: import.meta.glob matches and Worker URLs
499
+ // name EXACT files (verified to exist), not just a directory
500
+ const dynamicFiles = new Map(); // file -> evidence[]
501
+ for (const ref of [...detectImportMetaGlobs(rootDir, files, fileSet), ...detectWorkerUrls(rootDir, files, fileSet)]) {
502
+ if (!dynamicFiles.has(ref.file)) dynamicFiles.set(ref.file, []);
503
+ dynamicFiles.get(ref.file).push(ref);
504
+ }
505
+ const isUnderDynamicDir = (file) => {
506
+ if (dynamicFiles.has(file)) return dynamicFiles.get(file);
507
+ for (const [dir, evidence] of dynamicDirs) if (file === dir || file.startsWith(`${dir}/`)) return evidence;
508
+ return null;
509
+ };
510
+ const annotated = applyAnnotations(fileSet, annotations);
511
+ const annotatedDynamic = new Map(annotated.filter((a) => a.classification === "dynamically-loaded").map((a) => [a.file, a.pattern]));
512
+ const annotatedDormant = new Map(annotated.filter((a) => a.classification === "intentional-dormant").map((a) => [a.file, a.pattern]));
513
+
514
+ const generatedArtifacts = [];
515
+ const dynamicallyLoaded = [];
516
+ const intentionalDormant = [];
517
+ const heuristicUnverified = [];
518
+ const trulyOrphaned = [];
519
+
520
+ for (const file of uncovered) {
521
+ if (generatedSet.has(file)) { generatedArtifacts.push({ file, reason: generatedReasons.get(file) }); continue; }
522
+ if (annotatedDormant.has(file)) {
523
+ intentionalDormant.push({ file, evidence: [{ userAnnotation: `.mapdrc "${annotatedDormant.get(file)}": "intentional-dormant"` }] });
524
+ continue;
525
+ }
526
+ if (annotatedDynamic.has(file)) {
527
+ dynamicallyLoaded.push({ file, evidence: [{ userAnnotation: `.mapdrc "${annotatedDynamic.get(file)}": "dynamically-loaded"` }] });
528
+ continue;
529
+ }
530
+ const evidence = isUnderDynamicDir(file);
531
+ if (evidence) { dynamicallyLoaded.push({ file, evidence }); continue; }
532
+ if (parserKindByFile.get(file) === "heuristic") {
533
+ // heuristic import tracing is partial by construction — "no edge found"
534
+ // is not evidence of death, so these are disclosed as unverifiable,
535
+ // never asserted orphaned
536
+ heuristicUnverified.push({ file, reason: "parsed by heuristic language adapter — import tracing is partial, orphan status cannot be asserted" });
537
+ continue;
538
+ }
539
+ trulyOrphaned.push(file);
540
+ }
541
+
542
+ return { generatedArtifacts, dynamicallyLoaded, intentionalDormant, heuristicUnverified, trulyOrphaned: trulyOrphaned.sort() };
543
+ }