@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,141 @@
1
+ /**
2
+ * configLint.js — `mapd config lint`. Catches configuration that quietly lies
3
+ * to you: an annotation on a file you also excluded (so it never takes effect),
4
+ * an annotation whose glob matches nothing (a stale path), a glob broad enough
5
+ * to sweep in files you never meant, a dead exclude that matches nothing, and
6
+ * manual assertions with no attribution (who said this, why, when).
7
+ *
8
+ * Every finding carries a concrete suggested patch. Deterministic: it reads the
9
+ * resolved config, the real on-disk file list, and the scored graph — no guesses.
10
+ */
11
+
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { loadConfig } from "../config/index.js";
15
+ import { DEFAULTS } from "../config/schema.js";
16
+ import { buildScoredGraph } from "./intelligence.js";
17
+ import { globToRegex } from "./reachability.js";
18
+ import { bold, dim, red, green, yellow, cyan } from "./theme.js";
19
+
20
+ /** Minimal recursive source walk (posix-relative), skipping the dirs a parser never reads. */
21
+ function listDiskFiles(rootDir) {
22
+ const SKIP = new Set(["node_modules", ".git", ".mapd"]);
23
+ const out = [];
24
+ const walk = (abs, rel) => {
25
+ let entries;
26
+ try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
27
+ for (const e of entries) {
28
+ if (e.name.startsWith(".") && e.name !== ".mapdrc") { if (SKIP.has(e.name)) continue; }
29
+ if (SKIP.has(e.name)) continue;
30
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
31
+ if (e.isDirectory()) walk(path.join(abs, e.name), childRel);
32
+ else out.push(childRel);
33
+ }
34
+ };
35
+ walk(rootDir, "");
36
+ return out;
37
+ }
38
+
39
+ const DEFAULT_EXCLUDES = new Set(DEFAULTS.project.exclude);
40
+
41
+ /** Attribution fields present on an object-form annotation value. */
42
+ function attribution(raw) {
43
+ if (!raw || typeof raw !== "object") return [];
44
+ return ["reason", "source", "date"].filter((k) => raw[k]);
45
+ }
46
+
47
+ export function lintConfig(rootDir) {
48
+ const abs = path.resolve(rootDir);
49
+ const config = loadConfig(abs);
50
+ const graph = buildScoredGraph(abs);
51
+
52
+ const graphFiles = graph.files.map((f) => f.file);
53
+ const graphSet = new Set(graphFiles);
54
+ const diskFiles = listDiskFiles(abs);
55
+ const excludedFiles = diskFiles.filter((f) => !graphSet.has(f)); // on disk but dropped by excludes/ignores
56
+ const wfFiles = new Set(graph.workflows.flatMap((w) => w.files));
57
+
58
+ const annotations = config.project?.annotations ?? {};
59
+ const excludes = config.project?.exclude ?? [];
60
+
61
+ const findings = [];
62
+ const add = (level, code, message, patch) => findings.push({ level, code, message, suggestedPatch: patch });
63
+
64
+ const matchIn = (list, pattern) => { const re = globToRegex(pattern); return list.filter((f) => re.test(f)); };
65
+
66
+ for (const [pattern, raw] of Object.entries(annotations)) {
67
+ const classification = typeof raw === "string" ? raw : raw?.classification;
68
+ const inGraph = matchIn(graphFiles, pattern);
69
+ const inExcluded = matchIn(excludedFiles, pattern);
70
+
71
+ if (inGraph.length === 0 && inExcluded.length > 0) {
72
+ add("error", "excluded-but-annotated",
73
+ `annotation "${pattern}" (${classification}) only matches files that project.exclude removed from the map (${inExcluded.slice(0, 3).join(", ")}${inExcluded.length > 3 ? ", …" : ""}) — the annotation can never take effect.`,
74
+ `remove the annotation, or stop excluding those files (a file cannot be both excluded and, e.g., an entrypoint).`);
75
+ } else if (inGraph.length === 0) {
76
+ add("warn", "stale-annotation",
77
+ `annotation "${pattern}" (${classification}) matches no current file — a stale or mistyped path.`,
78
+ `remove it: \`mapd annotate remove ${pattern}\`, or fix the glob.`);
79
+ }
80
+
81
+ if (inGraph.length > 1 && graphFiles.length >= 5 && inGraph.length / graphFiles.length > 0.4) {
82
+ add("warn", "over-broad-annotation",
83
+ `annotation "${pattern}" (${classification}) matches ${inGraph.length}/${graphFiles.length} files — far broader than a targeted assertion.`,
84
+ `narrow the glob to the specific file(s) or directory you mean.`);
85
+ }
86
+
87
+ if (classification && typeof raw === "string") {
88
+ add("advice", "unattributed-annotation",
89
+ `annotation "${pattern}" (${classification}) has no attribution — who asserted it, why, and when.`,
90
+ `record it as { "classification": "${classification}", "reason": "…", "source": "you/PR", "date": "${new Date().toISOString().slice(0, 10)}" }.`);
91
+ } else if (raw && typeof raw === "object" && attribution(raw).length < 3) {
92
+ const missing = ["reason", "source", "date"].filter((k) => !raw[k]);
93
+ add("advice", "partial-attribution",
94
+ `annotation "${pattern}" is missing attribution field(s): ${missing.join(", ")}.`,
95
+ `add the missing field(s) so the manual assertion is auditable.`);
96
+ }
97
+ }
98
+
99
+ // User-added excludes (beyond the shipped defaults) that match nothing on disk.
100
+ for (const pattern of excludes) {
101
+ if (DEFAULT_EXCLUDES.has(pattern)) continue;
102
+ if (matchIn(diskFiles, pattern).length === 0) {
103
+ add("warn", "dead-exclude",
104
+ `project.exclude "${pattern}" matches no file on disk — a stale or ineffective exclude.`,
105
+ `remove it from project.exclude.`);
106
+ }
107
+ }
108
+
109
+ // An exclude that removes files which ARE reached by a workflow is almost
110
+ // always a mistake (you're hiding live code from the map). Detect via the
111
+ // set difference: excluded files whose basename still appears wired in.
112
+ for (const pattern of excludes) {
113
+ if (DEFAULT_EXCLUDES.has(pattern)) continue;
114
+ const excludedHit = matchIn(excludedFiles, pattern).filter((f) => wfFiles.has(f));
115
+ if (excludedHit.length) {
116
+ add("error", "excludes-live-code", `project.exclude "${pattern}" removes files a workflow still reaches: ${excludedHit.slice(0, 3).join(", ")}.`,
117
+ `stop excluding them, or annotate them intentional-dormant if they really are dead.`);
118
+ }
119
+ }
120
+
121
+ const errors = findings.filter((f) => f.level === "error").length;
122
+ const warnings = findings.filter((f) => f.level === "warn").length;
123
+ return {
124
+ ok: errors === 0,
125
+ counts: { error: errors, warn: warnings, advice: findings.filter((f) => f.level === "advice").length },
126
+ findings,
127
+ };
128
+ }
129
+
130
+ export function renderConfigLint(result) {
131
+ const glyph = { error: red("✗"), warn: yellow("⚠"), advice: cyan("i") };
132
+ const lines = [`\n${bold("config lint")}`];
133
+ if (!result.findings.length) { lines.push(green(" No configuration problems found.")); return lines.join("\n"); }
134
+ for (const f of result.findings) {
135
+ lines.push(` ${glyph[f.level]} ${bold(f.code)} ${f.message}`);
136
+ lines.push(dim(` patch: ${f.suggestedPatch}`));
137
+ }
138
+ const c = result.counts;
139
+ lines.push(`\n ${c.error ? red(`${c.error} error(s)`) : green("0 errors")}, ${c.warn} warning(s), ${c.advice} advisory(ies).`);
140
+ return lines.join("\n");
141
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * diagnose.js — Map'd's self-awareness report.
3
+ *
4
+ * This is deliberately deterministic: it does not ask a provider to judge the
5
+ * repo. It inspects the graph, package scripts, confidence signals, runtime
6
+ * blind spots, and env-variable contract, then says where Map'd's
7
+ * understanding is strong or incomplete.
8
+ */
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { buildScoredGraph } from "./intelligence.js";
13
+ import { loadPkg } from "./graph.js";
14
+ import { loadConfig } from "../config/index.js";
15
+
16
+ const RUNTIME_SCRIPT_NAMES = new Set(["start", "dev", "serve", "preview", "server", "worker", "electron", "desktop"]);
17
+ const NON_RUNTIME_SCRIPT_NAMES = new Set(["test", "lint", "typecheck", "type-check", "build", "coverage", "format", "prettier"]);
18
+ const RUNTIME_SCRIPT_RE = /\b(node|tsx|ts-node|vite|next|nuxt|astro|remix|electron|nodemon|webpack-dev-server)\b/i;
19
+ const ENV_DOT_RE = /\bprocess\.env\.([A-Z_][A-Z0-9_]*)\b/g;
20
+ const ENV_BRACKET_RE = /\bprocess\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g;
21
+ const ENV_DYNAMIC_RE = /\bprocess\.env\[[^\]'"][^\]]*\]/g;
22
+ const ENV_DESTRUCTURE_RE = /\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*process\.env\b/g;
23
+
24
+ function sortedEntries(obj = {}) {
25
+ return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
26
+ }
27
+
28
+ function scriptLooksRuntime(name, command) {
29
+ const lower = String(name ?? "").toLowerCase();
30
+ if (NON_RUNTIME_SCRIPT_NAMES.has(lower) || lower.startsWith("test:") || lower.startsWith("lint:")) return false;
31
+ if (RUNTIME_SCRIPT_NAMES.has(lower) || lower.endsWith(":dev") || lower.endsWith(":start")) return true;
32
+ return RUNTIME_SCRIPT_RE.test(String(command ?? ""));
33
+ }
34
+
35
+ function collectRuntimeScripts(pkg, graph) {
36
+ const mappedNpmScripts = new Set((graph.entryPoints ?? [])
37
+ .filter((e) => e.kind === "npm-script")
38
+ .map((e) => e.detail));
39
+ return sortedEntries(pkg?.scripts ?? {})
40
+ .filter(([name, command]) => scriptLooksRuntime(name, command))
41
+ .map(([name, command]) => ({
42
+ name,
43
+ command,
44
+ mappedAsEntry: mappedNpmScripts.has(name),
45
+ }));
46
+ }
47
+
48
+ function addEnvKey(map, key, file) {
49
+ if (!key) return;
50
+ const item = map.get(key) ?? { key, files: new Set(), count: 0 };
51
+ item.files.add(file);
52
+ item.count++;
53
+ map.set(key, item);
54
+ }
55
+
56
+ function collectEnvReads(rootDir, graph) {
57
+ const abs = path.resolve(rootDir);
58
+ const keys = new Map();
59
+ const dynamic = [];
60
+ for (const f of graph.files ?? []) {
61
+ let source = "";
62
+ try { source = fs.readFileSync(path.join(abs, f.file), "utf8"); } catch { continue; }
63
+
64
+ for (const re of [ENV_DOT_RE, ENV_BRACKET_RE]) {
65
+ re.lastIndex = 0;
66
+ for (const m of source.matchAll(re)) addEnvKey(keys, m[1], f.file);
67
+ }
68
+
69
+ ENV_DESTRUCTURE_RE.lastIndex = 0;
70
+ for (const m of source.matchAll(ENV_DESTRUCTURE_RE)) {
71
+ for (const raw of m[1].split(",")) {
72
+ const key = raw.trim().split(/[:=]/)[0]?.trim();
73
+ if (/^[A-Z_][A-Z0-9_]*$/.test(key)) addEnvKey(keys, key, f.file);
74
+ }
75
+ }
76
+
77
+ ENV_DYNAMIC_RE.lastIndex = 0;
78
+ const dynamicCount = [...source.matchAll(ENV_DYNAMIC_RE)].length;
79
+ if (dynamicCount) dynamic.push({ file: f.file, count: dynamicCount });
80
+ }
81
+
82
+ return {
83
+ keys: [...keys.values()]
84
+ .map((v) => ({ key: v.key, count: v.count, files: [...v.files].sort() }))
85
+ .sort((a, b) => a.key.localeCompare(b.key)),
86
+ dynamic,
87
+ hasExampleFile: fs.existsSync(path.join(abs, ".env.example")),
88
+ };
89
+ }
90
+
91
+ function signalIssues(confidence) {
92
+ return Object.entries(confidence?.signals ?? {})
93
+ .filter(([, s]) => s.unavailable || s.value == null || s.value < 0.7)
94
+ .map(([name, s]) => ({
95
+ signal: name,
96
+ value: s.value ?? null,
97
+ unavailable: !!s.unavailable,
98
+ weight: s.weight,
99
+ }));
100
+ }
101
+
102
+ function collectWeakWorkflows(graph, threshold, top) {
103
+ return [...(graph.workflows ?? [])]
104
+ .filter((wf) => (wf.confidence?.score ?? 1) < threshold || signalIssues(wf.confidence).length)
105
+ .sort((a, b) => (a.confidence?.score ?? 1) - (b.confidence?.score ?? 1) || a.id.localeCompare(b.id))
106
+ .slice(0, top)
107
+ .map((wf) => ({
108
+ id: wf.id,
109
+ entry: wf.entry,
110
+ fileCount: wf.files.length,
111
+ score: wf.confidence?.score ?? null,
112
+ signalCoverage: wf.confidence?.signalCoverage ?? null,
113
+ weakSignals: signalIssues(wf.confidence),
114
+ }));
115
+ }
116
+
117
+ function collectCallExamples(graph, resolution, top) {
118
+ return (graph.callEdges ?? [])
119
+ .filter((e) => e.resolution === resolution)
120
+ .slice(0, top)
121
+ .map((e) => ({
122
+ from: e.from,
123
+ name: e.unresolvedName ?? e.dynamicReceiver ?? null,
124
+ }));
125
+ }
126
+
127
+ function buildRecommendations({ graph, weakWorkflows, runtimeScripts, env, unresolvedImports, unresolvedCalls, dynamicCalls, threshold }) {
128
+ const recs = [];
129
+ if ((graph.repoConfidence ?? 1) < threshold) {
130
+ recs.push("Raise repo confidence by adding tests or entry-point coverage for the weakest workflows first.");
131
+ }
132
+ if (weakWorkflows.some((wf) => wf.weakSignals.some((s) => s.signal === "testPresence"))) {
133
+ recs.push("Add tests near low-testPresence workflow files; Map'd cannot infer behavior that tests never exercise.");
134
+ }
135
+ if ((graph.stats?.callResolutionRate ?? 1) < 0.9 || unresolvedCalls.length || dynamicCalls.length) {
136
+ recs.push("For dynamic call paths, add integration tests or annotations around plugin/runtime dispatch so static findings stay properly caveated.");
137
+ }
138
+ if (unresolvedImports.length) {
139
+ recs.push("Fix unresolved imports or extend resolver configuration; broken aliases lower trust before any AI reasoning begins.");
140
+ }
141
+ if (runtimeScripts.some((s) => !s.mappedAsEntry)) {
142
+ recs.push("Make runtime scripts map-friendly by pointing them at explicit entry files, or add annotations for framework/runtime entry conventions.");
143
+ }
144
+ if (env.keys.length && !env.hasExampleFile) {
145
+ recs.push("Add a .env.example documenting required keys; Map'd reports env names only and never reads secret values.");
146
+ }
147
+ if (graph.unsupported?.length) {
148
+ recs.push("Add parser support or exclude/annotate unsupported languages so Map'd does not overstate its coverage.");
149
+ }
150
+ if (graph.stats?.heuristicFileCount) {
151
+ recs.push("Non-JS/TS files are mapped heuristically (regex-tier imports/functions, half confidence credit). " +
152
+ "Annotate real entry points with `mapd annotate add <pattern> entrypoint` so their workflows form, " +
153
+ "or set .mapdrc mapping.polyglot=false to exclude them from the map entirely.");
154
+ }
155
+ if (!recs.length) recs.push("No major understanding blockers detected by the deterministic diagnosis pass.");
156
+ return recs;
157
+ }
158
+
159
+ export function buildDiagnosis(rootDir, { top = 5 } = {}) {
160
+ const abs = path.resolve(rootDir);
161
+ const graph = buildScoredGraph(abs);
162
+ const pkg = loadPkg(abs);
163
+ const config = loadConfig(abs);
164
+ const threshold = config.mapping?.confidenceThreshold ?? 0.8;
165
+ const limit = Math.max(1, Number.parseInt(top, 10) || 5);
166
+
167
+ const weakWorkflows = collectWeakWorkflows(graph, threshold, limit);
168
+ const runtimeScripts = collectRuntimeScripts(pkg, graph);
169
+ const env = collectEnvReads(abs, graph);
170
+ const unresolvedImports = (graph.importEdges ?? []).filter((e) => e.unresolved).slice(0, limit);
171
+ const unresolvedCalls = collectCallExamples(graph, "unresolved", limit);
172
+ const dynamicCalls = collectCallExamples(graph, "dynamic", limit);
173
+
174
+ return {
175
+ root: abs,
176
+ generatedAt: new Date().toISOString(),
177
+ summary: {
178
+ fileCount: graph.stats.fileCount,
179
+ workflowCount: graph.workflows.length,
180
+ repoConfidence: graph.repoConfidence,
181
+ confidenceThreshold: threshold,
182
+ callResolutionRate: graph.stats.callResolutionRate,
183
+ importResolutionRate: graph.stats.importResolutionRate,
184
+ orphanCount: graph.orphans.length,
185
+ unsupportedLanguageFiles: graph.stats.unsupportedLanguageFiles,
186
+ heuristicFileCount: graph.stats.heuristicFileCount ?? 0,
187
+ unparsed: graph.stats.unparsed,
188
+ },
189
+ confidence: {
190
+ weakWorkflowCount: weakWorkflows.length,
191
+ weakWorkflows,
192
+ },
193
+ runtime: {
194
+ entryPoints: graph.entryPoints,
195
+ runtimeScripts,
196
+ unmappedRuntimeScripts: runtimeScripts.filter((s) => !s.mappedAsEntry),
197
+ dynamicCallCount: graph.stats.dynamicCalls,
198
+ dynamicCalls,
199
+ unresolvedCalls,
200
+ unresolvedImports,
201
+ dynamicallyLoadedFiles: graph.reachability?.dynamicallyLoaded ?? [],
202
+ },
203
+ env,
204
+ coverageGaps: {
205
+ orphans: graph.orphans.slice(0, limit),
206
+ generatedFiles: (graph.generatedFiles ?? []).slice(0, limit),
207
+ unsupported: (graph.unsupported ?? []).slice(0, limit),
208
+ heuristicUnverified: (graph.reachability?.heuristicUnverified ?? []).slice(0, limit),
209
+ },
210
+ recommendations: buildRecommendations({ graph, weakWorkflows, runtimeScripts, env, unresolvedImports, unresolvedCalls, dynamicCalls, threshold }),
211
+ };
212
+ }
213
+
214
+ function signalText(signals) {
215
+ if (!signals.length) return "no weak signals";
216
+ return signals.map((s) => `${s.signal}=${s.unavailable ? "unavailable" : s.value}`).join(", ");
217
+ }
218
+
219
+ export function renderDiagnosis(data) {
220
+ const lines = [];
221
+ lines.push(`Map'd diagnosis — ${data.root}`);
222
+ lines.push(`files: ${data.summary.fileCount} workflows: ${data.summary.workflowCount} confidence: ${data.summary.repoConfidence} call resolution: ${(data.summary.callResolutionRate * 100).toFixed(1)}%`);
223
+ if (data.summary.heuristicFileCount) {
224
+ lines.push(`heuristic-parsed (non-JS/TS) files: ${data.summary.heuristicFileCount} — mapped with regex-tier extraction, half confidence credit; orphan claims about them are never asserted`);
225
+ }
226
+
227
+ lines.push("");
228
+ lines.push("Confidence blockers");
229
+ if (!data.confidence.weakWorkflows.length) {
230
+ lines.push(" none above the configured threshold");
231
+ } else {
232
+ for (const wf of data.confidence.weakWorkflows) {
233
+ lines.push(` ${wf.id} score ${wf.score} (${wf.fileCount} files; ${signalText(wf.weakSignals)})`);
234
+ }
235
+ }
236
+
237
+ lines.push("");
238
+ lines.push("Runtime blind spots");
239
+ if (!data.runtime.unmappedRuntimeScripts.length && !data.runtime.unresolvedCalls.length && !data.runtime.unresolvedImports.length && !data.runtime.dynamicCalls.length) {
240
+ lines.push(" no major runtime blind spots detected");
241
+ } else {
242
+ for (const s of data.runtime.unmappedRuntimeScripts) lines.push(` unmapped script "${s.name}": ${s.command}`);
243
+ for (const e of data.runtime.unresolvedImports) lines.push(` unresolved import from ${e.from}: ${e.unresolved}`);
244
+ for (const c of data.runtime.unresolvedCalls) lines.push(` unresolved call from ${c.from}: ${c.name}`);
245
+ for (const c of data.runtime.dynamicCalls) lines.push(` dynamic receiver from ${c.from}: ${c.name}`);
246
+ }
247
+
248
+ lines.push("");
249
+ lines.push("Env contract");
250
+ if (!data.env.keys.length && !data.env.dynamic.length) {
251
+ lines.push(" no process.env reads detected");
252
+ } else {
253
+ for (const key of data.env.keys) lines.push(` ${key.key} (${key.count} read${key.count === 1 ? "" : "s"} in ${key.files.join(", ")})`);
254
+ for (const item of data.env.dynamic) lines.push(` dynamic process.env[...] read in ${item.file} (${item.count})`);
255
+ lines.push(` .env.example: ${data.env.hasExampleFile ? "present" : "missing"}`);
256
+ }
257
+
258
+ lines.push("");
259
+ lines.push("Next actions");
260
+ for (const rec of data.recommendations) lines.push(` - ${rec}`);
261
+ return lines.join("\n");
262
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * docs.js — Renders MAP.md from the scored graph. Structure and numbers are
3
+ * fully deterministic; LLM narration (if available) is inserted into clearly
4
+ * marked sections so a reader can always tell derived fact from prose.
5
+ */
6
+
7
+ import { narrateWorkflow, llmAvailable } from "../agents/llm.js";
8
+ import { verifyGrounding, describeGrounding } from "./grounding.js";
9
+
10
+ const bar = (score) => {
11
+ const n = Math.round(score * 10);
12
+ return "█".repeat(n) + "░".repeat(10 - n);
13
+ };
14
+
15
+ /* ---- Mermaid rendering: every node/edge below is a graph fact ---- */
16
+
17
+ const mmId = (() => {
18
+ const seen = new Map();
19
+ return (label) => {
20
+ if (!seen.has(label)) seen.set(label, `n${seen.size}`);
21
+ return seen.get(label);
22
+ };
23
+ })();
24
+ const mmLabel = (f) => f.replace(/"/g, "'");
25
+
26
+ const MERMAID_NODE_CAP = 30; // declared cap: beyond this a workflow gets a summary node, never a truncated-silently diagram
27
+
28
+ function mermaidOverview(graph) {
29
+ const lines = ["```mermaid", "graph LR"];
30
+ for (const wf of graph.workflows) {
31
+ const e = mmId(`entry:${wf.id}`);
32
+ const w = mmId(wf.id);
33
+ lines.push(` ${e}(["${mmLabel(wf.entry.kind)}: ${mmLabel(wf.entry.file)}"]) --> ${w}["${wf.files.length} files / ${wf.functionCount} fns<br/>confidence ${wf.confidence?.score ?? "?"}"]`);
34
+ }
35
+ if (graph.orphans.length) lines.push(` ${mmId("orphans")}["orphans: ${graph.orphans.length} file(s)"]`);
36
+ lines.push("```");
37
+ return lines.join("\n");
38
+ }
39
+
40
+ function mermaidWorkflow(wf, graph) {
41
+ const inWf = new Set(wf.files);
42
+ const edges = graph.importEdges.filter((e) => e.to && inWf.has(e.from) && inWf.has(e.to));
43
+ const lines = ["```mermaid", "graph LR"];
44
+ if (wf.files.length > MERMAID_NODE_CAP) {
45
+ lines.push(` ${mmId(wf.id + ":big")}["${wf.files.length} files — exceeds ${MERMAID_NODE_CAP}-node diagram cap; see file list below"]`);
46
+ } else {
47
+ for (const f of wf.files) lines.push(` ${mmId(wf.id + f)}["${mmLabel(f)}"]`);
48
+ for (const e of edges) lines.push(` ${mmId(wf.id + e.from)} --> ${mmId(wf.id + e.to)}`);
49
+ if (!edges.length && wf.files.length === 1) lines.push(` %% single-file workflow, no internal import edges`);
50
+ }
51
+ lines.push("```");
52
+ return lines.join("\n");
53
+ }
54
+
55
+ export async function renderDocs(graph, { withNarration = true } = {}) {
56
+ const s = graph.stats;
57
+ const lines = [];
58
+ lines.push(`# Project Map`);
59
+ lines.push(``);
60
+ lines.push(`> Generated by Map'd on ${graph.generatedAt} — every number below is derived from AST analysis, not estimated.`);
61
+ lines.push(``);
62
+ lines.push(`**Repo confidence: ${graph.repoConfidence}** (size-weighted composite across workflows)`);
63
+ lines.push(``);
64
+ lines.push(`| Metric | Value |`);
65
+ lines.push(`|---|---|`);
66
+ lines.push(`| Files mapped | ${s.fileCount} (${s.totalLoc.toLocaleString()} LOC) |`);
67
+ lines.push(`| Parsed cleanly | ${s.parsedCleanly} |`);
68
+ lines.push(`| Parsed with recovery | ${s.parsedWithRecovery} |`);
69
+ lines.push(`| Unparsed | ${s.unparsed} |`);
70
+ lines.push(`| Unsupported-language files | ${s.unsupportedLanguageFiles} |`);
71
+ lines.push(`| Import resolution rate | ${(s.importResolutionRate * 100).toFixed(1)}% |`);
72
+ lines.push(`| Call resolution rate | ${(s.callResolutionRate * 100).toFixed(1)}% (${s.resolvedCalls}/${s.totalCalls}) |`);
73
+ lines.push(``);
74
+
75
+ lines.push(`## Topology`);
76
+ lines.push(``);
77
+ lines.push(mermaidOverview(graph));
78
+ lines.push(``);
79
+ lines.push(`## Workflows (${graph.workflows.length})`);
80
+ lines.push(``);
81
+
82
+ const narrate = withNarration && llmAvailable();
83
+
84
+ for (const wf of graph.workflows) {
85
+ const c = wf.confidence;
86
+ lines.push(`### \`${wf.id}\``);
87
+ lines.push(``);
88
+ lines.push(`- **Entry:** \`${wf.entry.file}\` (${wf.entry.kind}: ${wf.entry.detail})`);
89
+ lines.push(`- **Scope:** ${wf.files.length} files, ${wf.functionCount} functions`);
90
+ lines.push(`- **Confidence:** \`${bar(c.score)}\` **${c.score}** (signal coverage ${c.signalCoverage})`);
91
+ lines.push(``);
92
+ lines.push(`| Signal | Value | Weight |`);
93
+ lines.push(`|---|---|---|`);
94
+ for (const [k, sig] of Object.entries(c.signals)) {
95
+ lines.push(`| ${k} | ${sig.unavailable ? "unavailable" : sig.value} | ${sig.weight} |`);
96
+ }
97
+ lines.push(``);
98
+ lines.push(mermaidWorkflow(wf, graph));
99
+ lines.push(``);
100
+ lines.push(`**Files:** ${wf.files.map((f) => `\`${f}\``).join(", ")}`);
101
+ if (wf.exportedSurface.length) {
102
+ lines.push(``);
103
+ lines.push(`**Exported surface:** ${wf.exportedSurface.map((e) => `\`${e}\``).join(", ")}`);
104
+ }
105
+ if (narrate) {
106
+ const prose = await narrateWorkflow(wf, s);
107
+ if (prose) {
108
+ lines.push(``);
109
+ lines.push(`<!-- llm-narration: model-generated prose, structure above is ground truth -->`);
110
+ lines.push(prose.trim());
111
+ // same mechanical check chat answers get: unsupported claims are marked in the doc itself
112
+ const check = verifyGrounding(prose, { files: graph.files.map((f) => f.file), workflowIds: graph.workflows.map((w) => w.id), graph });
113
+ const { bad } = describeGrounding(check);
114
+ if (bad.length) {
115
+ lines.push(``);
116
+ lines.push(`> ⚠ Unverified by the map: ${bad.join("; ")}.`);
117
+ }
118
+ }
119
+ }
120
+ lines.push(``);
121
+ }
122
+
123
+ if (graph.orphans.length) {
124
+ lines.push(`## Orphaned files (${graph.orphans.length})`);
125
+ lines.push(``);
126
+ lines.push(`Not reachable from any detected entry point — candidates for dead code or missing entry-point detection:`);
127
+ lines.push(``);
128
+ for (const f of graph.orphans) lines.push(`- \`${f}\``);
129
+ lines.push(``);
130
+ }
131
+
132
+ if (graph.unsupported.length) {
133
+ lines.push(`## Unmapped (no parser adapter yet)`);
134
+ lines.push(``);
135
+ for (const f of graph.unsupported) lines.push(`- \`${f}\``);
136
+ lines.push(``);
137
+ }
138
+
139
+ return lines.join("\n");
140
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * doctor.js — `mapd doctor`: inspects the local environment and project
3
+ * state without mutating anything. Every check is real (actually reads
4
+ * files / runs git / validates config) — nothing here is a placeholder.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { execFileSync } from "node:child_process";
10
+ import { loadConfig, validateConfig } from "../config/index.js";
11
+ import { loadBaseline } from "./regression.js";
12
+ import { loadPkg, detectPackageManager } from "./graph.js";
13
+ import { getProvider } from "../agents/provider.js";
14
+ import { describeModelChoice } from "../agents/modelResolver.js";
15
+ import { checkEnvFiles } from "./envFiles.js";
16
+ import { checkReportFreshness } from "./staleness.js";
17
+ import { platformCommand } from "./proc.js";
18
+
19
+ function hasGitRepo(rootDir) {
20
+ try {
21
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: rootDir, stdio: ["ignore", "pipe", "pipe"] });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /** Real semver-major comparison against the package's stated engines.node — never assumed. */
29
+ function checkNodeVersion(pkg) {
30
+ const required = pkg?.engines?.node ?? ">=20";
31
+ const requiredMajorMatch = /(\d+)/.exec(required);
32
+ const requiredMajor = requiredMajorMatch ? parseInt(requiredMajorMatch[1], 10) : 20;
33
+ const actualMajor = parseInt(process.versions.node.split(".")[0], 10);
34
+ const ok = actualMajor >= requiredMajor;
35
+ return { ok, detail: `${process.version} (project requires ${required})${ok ? "" : " — UPGRADE NODE, this will cause real failures"}` };
36
+ }
37
+
38
+ /** Is the detected (or default) package manager binary actually resolvable, not just assumed present? */
39
+ function packageManagerAvailable(manager) {
40
+ try {
41
+ const pc = platformCommand(manager, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
42
+ execFileSync(pc.file, pc.args, pc.options);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function checkMapdDirWritable(abs) {
50
+ const dir = path.join(abs, ".mapd");
51
+ try {
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ const probe = path.join(dir, ".doctor-write-test");
54
+ fs.writeFileSync(probe, "x");
55
+ fs.rmSync(probe, { force: true });
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export function runDoctor(rootDir) {
63
+ const abs = path.resolve(rootDir);
64
+ const checks = [];
65
+ const add = (name, ok, detail) => checks.push({ name, ok, detail });
66
+
67
+ const pkg = loadPkg(abs);
68
+
69
+ const nodeCheck = checkNodeVersion(pkg);
70
+ add("node-version", nodeCheck.ok, nodeCheck.detail);
71
+
72
+ const { manager, lockfile } = detectPackageManager(abs);
73
+ if (!lockfile) {
74
+ add("package-manager", true, "no lockfile detected (npm/yarn/pnpm/bun) — FIX-G2 will assume npm if you add package.json scripts");
75
+ } else {
76
+ const available = packageManagerAvailable(manager);
77
+ add("package-manager", available,
78
+ available ? `${manager} detected (${lockfile}) and resolvable on PATH` : `${manager} detected (${lockfile}) but NOT found on PATH — FIX-G2 script runs will fail`);
79
+ }
80
+
81
+ const config = loadConfig(abs);
82
+ const { ok: configOk, errors } = validateConfig(config);
83
+ add("config-valid", configOk, configOk ? "resolved configuration is valid" : errors.join("; "));
84
+
85
+ const provider = getProvider(config);
86
+ add("provider-configured", true, provider.available()
87
+ ? `${provider.name} provider available${provider.name === "anthropic" ? ` — model: ${describeModelChoice(config)}` : provider.model ? ` — model: ${provider.model}` : ""}`
88
+ : "none configured — deterministic mode only");
89
+
90
+ const { project: projectEnv, user: userEnv } = checkEnvFiles(abs);
91
+ const envParts = [];
92
+ envParts.push(projectEnv.present
93
+ ? `project .env: ${projectEnv.keys.length ? `defines ${projectEnv.keys.join(", ")}` : "present, no recognized provider keys"}`
94
+ : "no project .env");
95
+ envParts.push(userEnv.present
96
+ ? `~/.env: ${userEnv.keys.length ? `defines ${userEnv.keys.join(", ")}` : "present, no recognized provider keys"}`
97
+ : "no ~/.env");
98
+ add("env-file", true, !projectEnv.present && !userEnv.present
99
+ ? "no .env found (project or user-level) — optional; copy .env.example, or export variables directly"
100
+ : envParts.join(" | "));
101
+
102
+ const mapdWritable = checkMapdDirWritable(abs);
103
+ add("mapd-dir-writable", mapdWritable, mapdWritable ? `${path.join(abs, ".mapd")} is writable` : "not writable — check permissions");
104
+
105
+ const cacheDir = path.join(abs, ".mapd", "cache");
106
+ add("cache-health", true, fs.existsSync(cacheDir) ? "cache directory present" : "not yet created (created on first `mapd watch` run)");
107
+
108
+ const baseline = loadBaseline(abs);
109
+ add(
110
+ "baseline-health",
111
+ !baseline || !baseline.schemaMismatch,
112
+ baseline
113
+ ? (baseline.schemaMismatch ? `schema mismatch — found v${baseline.schemaMismatch.found}, expected v${baseline.schemaMismatch.expected}; re-run \`mapd baseline\`` : "present and schema-current")
114
+ : "none — run `mapd baseline` to create one",
115
+ );
116
+
117
+ add("parser-availability", true, "Babel parser (JavaScript/TypeScript/JSX) available");
118
+
119
+ const freshness = checkReportFreshness(abs);
120
+ add("findings-freshness", !freshness.stale,
121
+ !freshness.checked ? "no check/modernize reports on disk yet"
122
+ : freshness.stale ? `STALE — the following report(s) predate a more recent source change: ${freshness.staleReports.join(", ")}; re-run \`mapd check\`/\`mapd modernize\``
123
+ : "reports are current with the working tree");
124
+
125
+ const scripts = pkg?.scripts ? Object.keys(pkg.scripts) : [];
126
+ add("project-scripts", true, scripts.length ? `detected: ${scripts.join(", ")}` : "no package.json scripts detected");
127
+
128
+ const gitAvailable = hasGitRepo(abs);
129
+ add("git-availability", true, gitAvailable ? "git repository detected — patch isolation will use worktrees" : "no git repository — patch isolation will use tmpdir copies (fully supported)");
130
+
131
+ add("mcp-readiness", true, "`mapd mcp` is available (stdio transport)");
132
+
133
+ return { ok: checks.every((c) => c.ok), checks };
134
+ }