@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,172 @@
1
+ /**
2
+ * fixApply.js — shared selection and post-apply safety for verified fix
3
+ * proposals. `review.transition` still performs the actual approved write;
4
+ * this layer adds the "prove the real tree stayed healthy, otherwise roll
5
+ * back the recorded backup" behavior used by CLI/chat/MCP.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { buildScoredGraph } from "./intelligence.js";
11
+ import { loadPkg } from "./graph.js";
12
+ import { runProjectCorrectnessGate } from "./gates.js";
13
+ import { transition } from "./review.js";
14
+ import { rollbackChange } from "./changes.js";
15
+
16
+ const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
17
+
18
+ function readJson(p) {
19
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
20
+ }
21
+
22
+ function isFixableFinding(item) {
23
+ return item?.status === "awaiting-approval" &&
24
+ (item.source === "check" || item.source?.startsWith("modernize-"));
25
+ }
26
+
27
+ function itemPriority(item) {
28
+ const severity = SEVERITY_RANK[item.severity] ?? 0;
29
+ const priority = Number(item.priority);
30
+ return severity * 1000 + (Number.isFinite(priority) ? priority : 0);
31
+ }
32
+
33
+ /** Pick the strongest open finding when the user intentionally omits an ID. */
34
+ export function chooseFixTarget(items = []) {
35
+ return items
36
+ .filter(isFixableFinding)
37
+ .slice()
38
+ .sort((a, b) =>
39
+ itemPriority(b) - itemPriority(a) ||
40
+ (a.source ?? "").localeCompare(b.source ?? "") ||
41
+ (a.id ?? "").localeCompare(b.id ?? ""),
42
+ )[0] ?? null;
43
+ }
44
+
45
+ export function comparePostApplyHealth(preGraph, postGraph) {
46
+ const preConfidence = Number(preGraph?.repoConfidence ?? 0);
47
+ const postConfidence = Number(postGraph?.repoConfidence ?? 0);
48
+ const preWorkflows = preGraph?.workflows ?? [];
49
+ const postWorkflows = postGraph?.workflows ?? [];
50
+ const postIds = new Set(postWorkflows.map((w) => w.id));
51
+ const missingWorkflowIds = preWorkflows.map((w) => w.id).filter((id) => !postIds.has(id));
52
+ const issues = [];
53
+
54
+ if (postConfidence < preConfidence) {
55
+ issues.push(`repo confidence dropped from ${preConfidence} to ${postConfidence}`);
56
+ }
57
+ if (postWorkflows.length < preWorkflows.length) {
58
+ issues.push(`workflow count dropped from ${preWorkflows.length} to ${postWorkflows.length}`);
59
+ }
60
+ if (missingWorkflowIds.length) {
61
+ issues.push(`workflow(s) disappeared after apply: ${missingWorkflowIds.join(", ")}`);
62
+ }
63
+
64
+ return {
65
+ ok: issues.length === 0,
66
+ issues,
67
+ preConfidence,
68
+ postConfidence,
69
+ preWorkflowCount: preWorkflows.length,
70
+ postWorkflowCount: postWorkflows.length,
71
+ missingWorkflowIds,
72
+ };
73
+ }
74
+
75
+ function firstUsefulLine(...texts) {
76
+ for (const text of texts) {
77
+ const line = String(text ?? "").split(/\r?\n/).find((l) => l.trim());
78
+ if (line) return line.trim();
79
+ }
80
+ return "";
81
+ }
82
+
83
+ function correctnessIssues(gate) {
84
+ if (gate.passed) return [];
85
+ return (gate.checks ?? [])
86
+ .filter((c) => !c.passed)
87
+ .map((c) => {
88
+ const detail = firstUsefulLine(c.stderr, c.stdout);
89
+ return `${c.script} failed${c.exitCode != null ? ` (exit ${c.exitCode})` : ""}${detail ? `: ${detail}` : ""}`;
90
+ });
91
+ }
92
+
93
+ export function runPostApplyVerification(rootDir, preGraph, config = {}) {
94
+ const abs = path.resolve(rootDir);
95
+ const postGraph = buildScoredGraph(abs);
96
+ const health = comparePostApplyHealth(preGraph, postGraph);
97
+ const correctness = runProjectCorrectnessGate({
98
+ cwd: abs,
99
+ pkg: loadPkg(abs),
100
+ runTests: config.fix?.runTests,
101
+ runLint: config.fix?.runLint,
102
+ runTypecheck: config.fix?.runTypecheck,
103
+ });
104
+ const issues = [...health.issues, ...correctnessIssues(correctness)];
105
+ return { ok: issues.length === 0, issues, health, correctness };
106
+ }
107
+
108
+ function recordProposalPostApply(item, status, postApplyVerification, rollbackResults = []) {
109
+ if (!item?.file) return;
110
+ const prop = readJson(item.file);
111
+ if (!prop) return;
112
+ if (status) prop.status = status;
113
+ prop.postApplyVerification = {
114
+ ok: postApplyVerification.ok,
115
+ at: new Date().toISOString(),
116
+ issues: postApplyVerification.issues,
117
+ health: postApplyVerification.health,
118
+ correctness: postApplyVerification.correctness,
119
+ rollbackResults,
120
+ };
121
+ fs.writeFileSync(item.file, JSON.stringify(prop, null, 2));
122
+ }
123
+
124
+ /**
125
+ * Approve a saved fix proposal, then verify the real tree. If tests/lint/
126
+ * typecheck fail, repo confidence drops, or workflows disappear, immediately
127
+ * roll back the recorded change IDs from changes.js.
128
+ */
129
+ export function approveFixWithPostApplyVerification(rootDir, item, config = {}) {
130
+ const abs = path.resolve(rootDir);
131
+ if (item?.source !== "fix") {
132
+ return { ok: false, action: "approve", detail: "post-apply verification only applies to fix proposals" };
133
+ }
134
+
135
+ const preGraph = buildScoredGraph(abs);
136
+ let applied;
137
+ try {
138
+ applied = transition(abs, item, "approve");
139
+ } catch (e) {
140
+ return { ok: false, action: "approve", detail: e.message };
141
+ }
142
+ if (!applied.ok) return applied;
143
+
144
+ const changeIds = applied.changeIds ?? [];
145
+ const postApplyVerification = runPostApplyVerification(abs, preGraph, config);
146
+ if (postApplyVerification.ok) {
147
+ recordProposalPostApply(item, null, postApplyVerification);
148
+ return { ...applied, changeIds, postApplyVerification };
149
+ }
150
+
151
+ const rollbackResults = [];
152
+ for (const changeId of changeIds.slice().reverse()) {
153
+ try {
154
+ rollbackResults.push({ changeId, ...rollbackChange(abs, changeId) });
155
+ } catch (e) {
156
+ rollbackResults.push({ changeId, ok: false, detail: e.message });
157
+ }
158
+ }
159
+ const rolledBack = rollbackResults.every((r) => r.ok);
160
+ const status = rolledBack ? "rolled-back-after-apply" : "post-apply-failed-rollback-incomplete";
161
+ recordProposalPostApply(item, status, postApplyVerification, rollbackResults);
162
+
163
+ return {
164
+ ok: false,
165
+ action: "approve",
166
+ detail: `post-apply verification failed; ${rolledBack ? "rolled back" : "rollback incomplete"} (${postApplyVerification.issues.join("; ")})`,
167
+ originalDetail: applied.detail,
168
+ changeIds,
169
+ rollbackResults,
170
+ postApplyVerification,
171
+ };
172
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * frameworkEntries.js — framework-specific entry-point detection beyond the
3
+ * generic bin/main/npm-script/route-call-site heuristics in graph.js.
4
+ *
5
+ * DESIGN RULE (same as parser.js): every detector here either verifies its
6
+ * finding against real AST structure and confirms the resolved file actually
7
+ * exists in the project, or it detects nothing. No pattern here guesses at a
8
+ * file that might exist — a miss is acceptable, a fabricated entry is not.
9
+ */
10
+
11
+ import { parse } from "@babel/parser";
12
+ import _traverse from "@babel/traverse";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+
16
+ const traverse = _traverse.default ?? _traverse;
17
+
18
+ const BABEL_OPTS = { sourceType: "unambiguous", errorRecovery: true, plugins: ["typescript", "jsx", "decorators-legacy"] };
19
+
20
+ /**
21
+ * Resolves `relSpec` against `dirPosix` and returns the matching project file
22
+ * only if it actually exists in `fileSet` — this existence check, not a
23
+ * leading-dot requirement, is what prevents fabricating an entry. A bare
24
+ * relative filename (e.g. "preload.js", as produced by `path.join(__dirname,
25
+ * "preload.js")`) is just as valid a spec here as "./preload.js"; both are
26
+ * relative to `dirPosix` either way.
27
+ */
28
+ function resolveFromDir(dirPosix, relSpec, fileSet) {
29
+ if (typeof relSpec !== "string" || !relSpec.trim()) return null;
30
+ const base = path.posix.join(dirPosix, relSpec);
31
+ const candidates = [base, `${base}.js`, `${base}.ts`, `${base}.jsx`, `${base}.tsx`, `${base}.mjs`, `${base}.mts`];
32
+ for (const c of candidates) {
33
+ const norm = path.posix.normalize(c);
34
+ if (fileSet.has(norm)) return norm;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Next.js file-based routing: Pages Router (anything under "pages", API
41
+ * routes under "pages/api") and App Router (page/route/layout files nested
42
+ * anywhere under "app"). Pure path-convention matching — no parsing needed,
43
+ * and no ambiguity: a file either matches Next.js's documented convention or
44
+ * it doesn't.
45
+ */
46
+ export function detectNextJsEntries(fileSet) {
47
+ const entries = [];
48
+ for (const file of fileSet) {
49
+ const pagesMatch = /^(?:src\/)?pages\/(.+)\.(jsx?|tsx?)$/.exec(file);
50
+ if (pagesMatch) {
51
+ const routePath = pagesMatch[1];
52
+ if (path.posix.basename(routePath).startsWith("_")) continue; // _app/_document/_error/_middleware are framework hooks, not routes
53
+ const kind = routePath.startsWith("api/") ? "nextjs-api-route" : "nextjs-page";
54
+ entries.push({ file, kind, detail: `Next.js ${kind === "nextjs-api-route" ? "API route" : "page"}: /${routePath}` });
55
+ continue;
56
+ }
57
+ const appMatch = /^(?:src\/)?app\/(.*\/)?(page|route|layout)\.(jsx?|tsx?)$/.exec(file);
58
+ if (appMatch) {
59
+ const kind = appMatch[2] === "route" ? "nextjs-route-handler" : appMatch[2] === "layout" ? "nextjs-layout" : "nextjs-page";
60
+ entries.push({ file, kind, detail: `Next.js App Router ${appMatch[2]}: /${appMatch[1] ?? ""}` });
61
+ }
62
+ }
63
+ return entries;
64
+ }
65
+
66
+ /**
67
+ * HTML entry points: `<script type="module" src="...">` tags in root-level
68
+ * HTML files — Vite's DEFAULT entry convention (used whenever a project does
69
+ * NOT set an explicit `build.rollupOptions.input`, which is the common case:
70
+ * verified against a real project during development, MITRI included). A
71
+ * root-relative src ("/src/main.jsx") resolves against the project root; a
72
+ * relative src ("./main.jsx") resolves against the HTML file's directory. An
73
+ * external URL (http(s)://, a bare specifier) is skipped, never guessed at.
74
+ */
75
+ export function detectHtmlEntries(rootDir, fileSet) {
76
+ const entries = [];
77
+ let rootEntries;
78
+ try { rootEntries = fs.readdirSync(rootDir, { withFileTypes: true }); } catch { return entries; }
79
+
80
+ for (const entry of rootEntries) {
81
+ if (!entry.isFile() || !entry.name.endsWith(".html")) continue;
82
+ const relHtml = entry.name;
83
+ let html;
84
+ try { html = fs.readFileSync(path.join(rootDir, relHtml), "utf8"); } catch { continue; }
85
+
86
+ const scriptTagRe = /<script\b([^>]*)>/gi;
87
+ let tagMatch;
88
+ while ((tagMatch = scriptTagRe.exec(html))) {
89
+ const attrs = tagMatch[1];
90
+ const typeMatch = /\btype\s*=\s*["']([^"']+)["']/i.exec(attrs);
91
+ const srcMatch = /\bsrc\s*=\s*["']([^"']+)["']/i.exec(attrs);
92
+ if (!srcMatch || typeMatch?.[1]?.toLowerCase() !== "module") continue;
93
+ const src = srcMatch[1];
94
+ if (/^([a-z]+:)?\/\//i.test(src)) continue; // external URL, e.g. https://... or //cdn...
95
+
96
+ const dirPosix = src.startsWith("/") ? "." : path.posix.dirname(relHtml);
97
+ const spec = src.startsWith("/") ? `.${src}` : src;
98
+ const resolved = resolveFromDir(dirPosix, spec, fileSet);
99
+ if (resolved) entries.push({ file: resolved, kind: "html-module-entry", detail: `<script type="module"> in ${relHtml}` });
100
+ }
101
+ }
102
+ return entries;
103
+ }
104
+
105
+ /**
106
+ * Vite entry points: `build.rollupOptions.input` and `build.lib.entry` in
107
+ * vite.config.{js,ts,mjs,mts} at the project root. Verified by walking the
108
+ * real AST and anchoring on the literal enclosing key name (`rollupOptions`
109
+ * or `lib`) — never a bare "any object with an `input`/`entry` key."
110
+ */
111
+ export function detectViteEntries(rootDir, fileSet) {
112
+ const entries = [];
113
+ for (const rel of ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.mts"]) {
114
+ const abs = path.join(rootDir, rel);
115
+ if (!fs.existsSync(abs)) continue;
116
+ let ast;
117
+ try { ast = parse(fs.readFileSync(abs, "utf8"), BABEL_OPTS); } catch { continue; }
118
+
119
+ traverse(ast, {
120
+ ObjectProperty(p) {
121
+ const keyName = p.node.key?.name ?? p.node.key?.value;
122
+ if (keyName !== "input" && keyName !== "entry") return;
123
+ const enclosingProp = p.parentPath?.parentPath;
124
+ const enclosingKey = enclosingProp?.isObjectProperty?.() ? (enclosingProp.node.key?.name ?? enclosingProp.node.key?.value) : null;
125
+ const anchorMatches = (keyName === "input" && enclosingKey === "rollupOptions") || (keyName === "entry" && enclosingKey === "lib");
126
+ if (!anchorMatches) return;
127
+
128
+ const value = p.node.value;
129
+ const specs = [];
130
+ if (value.type === "StringLiteral") {
131
+ specs.push(value.value);
132
+ } else if (value.type === "ArrayExpression") {
133
+ for (const el of value.elements) if (el?.type === "StringLiteral") specs.push(el.value);
134
+ } else if (value.type === "ObjectExpression") {
135
+ for (const prop of value.properties) if (prop.value?.type === "StringLiteral") specs.push(prop.value.value);
136
+ }
137
+
138
+ for (const spec of specs) {
139
+ const resolved = resolveFromDir(".", spec, fileSet);
140
+ if (resolved) entries.push({ file: resolved, kind: "vite-entry", detail: `build.${enclosingKey}.${keyName} in ${rel}` });
141
+ }
142
+ },
143
+ });
144
+ }
145
+ return entries;
146
+ }
147
+
148
+ function resolvePreloadValue(valueNode, fileDirPosix, fileSet) {
149
+ if (valueNode.type === "StringLiteral") return resolveFromDir(fileDirPosix, valueNode.value, fileSet);
150
+ if (valueNode.type === "CallExpression") {
151
+ const callee = valueNode.callee;
152
+ const isPathJoinOrResolve = callee?.type === "MemberExpression" && callee.object?.name === "path" &&
153
+ (callee.property?.name === "join" || callee.property?.name === "resolve");
154
+ if (!isPathJoinOrResolve) return null;
155
+ const args = valueNode.arguments ?? [];
156
+ if (!args.some((a) => a.type === "Identifier" && a.name === "__dirname")) return null; // only the well-known, verifiable pattern
157
+ const literalParts = args.filter((a) => a.type === "StringLiteral").map((a) => a.value);
158
+ if (!literalParts.length) return null;
159
+ return resolveFromDir(fileDirPosix, literalParts.join("/"), fileSet);
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Electron preload entries: `new BrowserWindow({ webPreferences: { preload: ... } })`.
166
+ * Only resolves the preload path when it's a plain string or the well-known
167
+ * `path.join(__dirname, "...")`/`path.resolve(__dirname, "...")` pattern —
168
+ * any other expression shape (a variable, a computed path) is left
169
+ * unresolved rather than guessed at.
170
+ */
171
+ export function detectElectronPreloadEntries(rootDir, files, fileSet) {
172
+ const entries = [];
173
+ for (const f of files) {
174
+ const relFile = f.file.split(path.sep).join("/");
175
+ if (!/\.(js|ts|mjs|cjs|mts|cts)$/.test(relFile)) continue;
176
+ let raw;
177
+ try { raw = fs.readFileSync(path.join(rootDir, relFile), "utf8"); } catch { continue; }
178
+ if (!raw.includes("BrowserWindow") || !raw.includes("preload")) continue; // cheap pre-filter, not the actual verification
179
+ let ast;
180
+ try { ast = parse(raw, BABEL_OPTS); } catch { continue; }
181
+ const fileDirPosix = path.posix.dirname(relFile);
182
+
183
+ traverse(ast, {
184
+ NewExpression(p) {
185
+ if (p.node.callee?.name !== "BrowserWindow") return;
186
+ const optsArg = p.node.arguments?.[0];
187
+ if (optsArg?.type !== "ObjectExpression") return;
188
+ const webPrefsProp = optsArg.properties.find((pr) => pr.key?.name === "webPreferences" || pr.key?.value === "webPreferences");
189
+ const webPrefsObj = webPrefsProp?.value;
190
+ if (webPrefsObj?.type !== "ObjectExpression") return;
191
+ const preloadProp = webPrefsObj.properties.find((pr) => pr.key?.name === "preload" || pr.key?.value === "preload");
192
+ if (!preloadProp) return;
193
+ const resolved = resolvePreloadValue(preloadProp.value, fileDirPosix, fileSet);
194
+ if (resolved) entries.push({ file: resolved, kind: "electron-preload", detail: `BrowserWindow webPreferences.preload in ${relFile}` });
195
+ },
196
+ });
197
+ }
198
+ return entries;
199
+ }
200
+
201
+ /**
202
+ * Tooling config files: entry points read directly by an external tool
203
+ * (a test runner, a process manager) rather than imported by application
204
+ * code. Without registering these, everything they alone pull in (e2e specs,
205
+ * fixture helpers, deploy config) reads as unreachable "dead code" when it
206
+ * isn't — it just belongs to a different runtime context than the app's own
207
+ * entry points. Pure filename-convention matching (same verification model
208
+ * as detectHtmlEntries: the file existing at its conventional path/name IS
209
+ * the check) — once registered, the existing import-graph traversal already
210
+ * follows whatever these files themselves import.
211
+ */
212
+ const TOOLING_CONFIG_PATTERNS = [
213
+ { re: /^playwright\.config\.(js|ts|mjs|cjs|mts|cts)$/, kind: "playwright-config", label: "Playwright config" },
214
+ { re: /^jest\.config\.(js|ts|mjs|cjs|json)$/, kind: "jest-config", label: "Jest config" },
215
+ { re: /^vitest\.config\.(js|ts|mjs|cjs|mts|cts)$/, kind: "vitest-config", label: "Vitest config" },
216
+ { re: /^cypress\.config\.(js|ts|mjs|cjs)$/, kind: "cypress-config", label: "Cypress config" },
217
+ { re: /^ecosystem\.config\.(js|cjs)$/, kind: "pm2-ecosystem", label: "PM2 ecosystem config" },
218
+ ];
219
+
220
+ export function detectToolingConfigEntries(fileSet) {
221
+ const entries = [];
222
+ for (const file of fileSet) {
223
+ if (file.includes("/")) continue; // these conventions are always project-root files
224
+ for (const { re, kind, label } of TOOLING_CONFIG_PATTERNS) {
225
+ if (re.test(file)) entries.push({ file, kind, detail: `${label} — read directly by its tool, not imported by app code` });
226
+ }
227
+ }
228
+ return entries;
229
+ }
230
+
231
+ /** Runs every framework detector and returns the combined, deduplicated entry list. */
232
+ export function detectFrameworkEntries(rootDir, files, fileSet) {
233
+ const all = [
234
+ ...detectNextJsEntries(fileSet),
235
+ ...detectHtmlEntries(rootDir, fileSet),
236
+ ...detectViteEntries(rootDir, fileSet),
237
+ ...detectElectronPreloadEntries(rootDir, files, fileSet),
238
+ ...detectToolingConfigEntries(fileSet),
239
+ ];
240
+ const seen = new Set();
241
+ return all.filter((e) => {
242
+ const key = `${e.file}::${e.kind}`;
243
+ if (seen.has(key)) return false;
244
+ seen.add(key);
245
+ return true;
246
+ });
247
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * gates.js — the general-purpose deterministic verification gate runner.
3
+ *
4
+ * G1-G3 originally lived only inside integrate.js's merge-conflict pipeline.
5
+ * This module is the single implementation; integrate.js now calls into it
6
+ * so the merge-resolution and fix-engine pipelines can never drift apart.
7
+ *
8
+ * DESIGN RULE (unchanged from integrate.js): a gate failure never gets
9
+ * silently dropped — every gate result is returned, and the caller decides
10
+ * status ("rejected-by-gate" etc.) from the full list.
11
+ */
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import os from "node:os";
17
+ import { parseJsFile } from "./parser.js";
18
+ import { sanitizeRelPath, isProtectedPath } from "./security.js";
19
+ import { diffWorkspace } from "./workspace.js";
20
+ import { buildScoredGraph } from "./intelligence.js";
21
+ import { diffGraphs } from "./regression.js";
22
+ import { detectPackageManager } from "./graph.js";
23
+ import { platformCommand } from "./proc.js";
24
+
25
+ /** Parse source held in memory by round-tripping through a temp file. */
26
+ export function parseSource(source, label) {
27
+ const tmp = path.join(os.tmpdir(), `mapd-src-${Date.now()}-${Math.random().toString(36).slice(2)}.js`);
28
+ fs.writeFileSync(tmp, source);
29
+ try { return parseJsFile(tmp, label); }
30
+ finally { fs.rmSync(tmp, { force: true }); }
31
+ }
32
+
33
+ /**
34
+ * G1: the source parses cleanly (zero Babel recovery errors).
35
+ * G2: exports are a superset of `requiredExports` (when supplied).
36
+ * G3: top-level function names are a superset of `requiredFunctions` (when supplied),
37
+ * ignoring anonymous functions.
38
+ * G4 (optional, best-effort): a resolvable test command passes in `cwd`. Skipped
39
+ * (never faked) when no test command can be resolved.
40
+ *
41
+ * Returns the same `[{gate, passed, missing?}]` shape integrate.js's
42
+ * verifyProposal has always returned, so existing gate-name strings and
43
+ * consumers (review queue, CLI output) are unaffected by this refactor.
44
+ */
45
+ export function runStandardGates({ source, label, requiredExports, requiredFunctions, cwd, testCommand } = {}) {
46
+ const gates = [];
47
+ const p = parseSource(source, label);
48
+
49
+ gates.push({ gate: "G1-parses-cleanly", passed: p.parsed && p.parseErrors === 0 });
50
+
51
+ if (requiredExports) {
52
+ const got = new Set(p.exports);
53
+ const missing = requiredExports.filter((e) => !got.has(e));
54
+ gates.push({ gate: "G2-export-union-preserved", passed: missing.length === 0, missing });
55
+ }
56
+ if (requiredFunctions) {
57
+ const got = new Set(p.functions.map((f) => f.name));
58
+ const missing = requiredFunctions.filter((f) => !got.has(f) && !f.startsWith("<anon"));
59
+ gates.push({ gate: "G3-function-union-preserved", passed: missing.length === 0, missing });
60
+ }
61
+ if (testCommand && cwd) {
62
+ gates.push(runTestGate(testCommand, cwd));
63
+ }
64
+ return gates;
65
+ }
66
+
67
+ /** G4: best-effort — run a resolvable test command in an isolated cwd. Never fabricates a result. */
68
+ function runTestGate(testCommand, cwd) {
69
+ try {
70
+ const pc = platformCommand(testCommand[0], testCommand.slice(1), { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 120_000 });
71
+ execFileSync(pc.file, pc.args, pc.options);
72
+ return { gate: "G4-tests-pass", passed: true };
73
+ } catch (e) {
74
+ return {
75
+ gate: "G4-tests-pass", passed: false,
76
+ exitCode: e.status ?? null,
77
+ stderr: (e.stderr ?? "").toString().slice(0, 4000),
78
+ stdout: (e.stdout ?? "").toString().slice(0, 4000),
79
+ };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Registry seam: additional gates (e.g. a project-specific policy gate) can be
85
+ * added here without touching integrate.js or fix.js call sites.
86
+ */
87
+ export const GATES = {
88
+ G1: "G1-parses-cleanly",
89
+ G2: "G2-export-union-preserved",
90
+ G3: "G3-function-union-preserved",
91
+ G4: "G4-tests-pass",
92
+ };
93
+
94
+ /* ------------------------------------------------------------------------ *
95
+ * General verification pipeline gates (fix.js / mcp / chat).
96
+ *
97
+ * These are NAMED DIFFERENTLY from integrate.js's merge-specific G1/G2/G3
98
+ * above (which stay untouched for backward compatibility with
99
+ * tests/integrate.test.js). These three implement the spec's general
100
+ * definitions:
101
+ * FIX-G1 patch & structural safety
102
+ * FIX-G2 project correctness (available project scripts: test/lint/typecheck)
103
+ * FIX-G3 Map'd regression verification (remap, compare confidence/orphans)
104
+ * ------------------------------------------------------------------------ */
105
+
106
+ /** FIX-G1: every proposed file is safe to write, parses, and nothing unexpected changed. */
107
+ export function runPatchSafetyGate({ ws, patch, forbiddenPaths = [], maxFileSizeBytes = 1_000_000 }) {
108
+ const issues = [];
109
+ for (const [relFile, newSource] of Object.entries(patch)) {
110
+ try {
111
+ sanitizeRelPath(ws.dir, relFile);
112
+ } catch (e) {
113
+ issues.push(`${relFile}: ${e.message}`);
114
+ continue;
115
+ }
116
+ if (isProtectedPath(relFile, forbiddenPaths)) {
117
+ issues.push(`${relFile}: protected path, refusing to modify`);
118
+ continue;
119
+ }
120
+ if (Buffer.byteLength(newSource, "utf8") > maxFileSizeBytes) {
121
+ issues.push(`${relFile}: exceeds maxFileSizeBytes (${maxFileSizeBytes})`);
122
+ }
123
+ const parsed = parseSource(newSource, relFile);
124
+ if (!parsed.parsed || parsed.parseErrors > 0) {
125
+ issues.push(`${relFile}: does not parse cleanly`);
126
+ }
127
+ }
128
+ const changed = diffWorkspace(ws);
129
+ const allowed = new Set(Object.keys(patch));
130
+ const unexpected = changed.filter((f) => !allowed.has(f));
131
+ if (unexpected.length) issues.push(`unexpected files changed: ${unexpected.join(", ")}`);
132
+
133
+ return { gate: "FIX-G1-patch-safety", passed: issues.length === 0, issues };
134
+ }
135
+
136
+ function runPackageManagerScript(cwd, manager, args, timeoutMs) {
137
+ try {
138
+ const pc = platformCommand(manager, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
139
+ execFileSync(pc.file, pc.args, pc.options);
140
+ return { script: `${manager} ${args.join(" ")}`, passed: true };
141
+ } catch (e) {
142
+ return {
143
+ script: `${manager} ${args.join(" ")}`, passed: false,
144
+ exitCode: e.status ?? null,
145
+ stderr: (e.stderr ?? "").toString().slice(0, 4000),
146
+ stdout: (e.stdout ?? "").toString().slice(0, 4000),
147
+ };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * FIX-G2: run whichever of test/lint/typecheck scripts the project actually
153
+ * defines in package.json. Never assumes a script exists — detects it. Also
154
+ * never assumes npm: detects the actual package manager from the project's
155
+ * lockfile (pnpm-lock.yaml / yarn.lock / package-lock.json / bun.lockb),
156
+ * falling back to npm only when no lockfile is present at all.
157
+ */
158
+ export function runProjectCorrectnessGate({ cwd, pkg, runTests = true, runLint = true, runTypecheck = true, timeoutMs = 120_000 }) {
159
+ const scripts = pkg?.scripts ?? {};
160
+ const { manager } = detectPackageManager(cwd);
161
+ const checks = [];
162
+ if (runTests && scripts.test) checks.push(runPackageManagerScript(cwd, manager, ["test"], timeoutMs));
163
+ if (runLint && scripts.lint) checks.push(runPackageManagerScript(cwd, manager, ["run", "lint"], timeoutMs));
164
+ const typecheckScript = scripts.typecheck ? "typecheck" : scripts["type-check"] ? "type-check" : null;
165
+ if (runTypecheck && typecheckScript) checks.push(runPackageManagerScript(cwd, manager, ["run", typecheckScript], timeoutMs));
166
+
167
+ if (!checks.length) return { gate: "FIX-G2-project-correctness", passed: true, skipped: true, checks: [] };
168
+ return { gate: "FIX-G2-project-correctness", passed: checks.every((c) => c.passed), checks };
169
+ }
170
+
171
+ /**
172
+ * FIX-G3: remap the isolated workspace and compare against the pre-fix graph
173
+ * (and baseline, when available) — confidence must not newly regress, no new
174
+ * high-severity findings, AND — when a target finding and a usable baseline
175
+ * are both available — the finding's exact condition (same kind + detail)
176
+ * must no longer reproduce when diffed against the baseline. A patch that
177
+ * leaves the original problem in place fails this gate; the model's own
178
+ * claim of success is never taken as evidence.
179
+ */
180
+ export function runRegressionGate({ ws, preFixGraph, baseline, targetFinding, confidenceTolerance = 0.02 }) {
181
+ const postGraph = buildScoredGraph(ws.dir);
182
+ const deltaConfidence = Number((postGraph.repoConfidence - preFixGraph.repoConfidence).toFixed(3));
183
+ const newOrphans = postGraph.orphans.filter((o) => !preFixGraph.orphans.includes(o));
184
+ const introduced = diffGraphs(preFixGraph, postGraph).filter((f) => f.kind !== "workflow-added");
185
+ const newHighSeverity = introduced.filter((f) => f.severity === "high");
186
+
187
+ let targetResolved = null;
188
+ let targetStillFailing = false;
189
+ if (baseline && targetFinding?.kind && targetFinding?.detail) {
190
+ const vsBaseline = diffGraphs(baseline, postGraph);
191
+ targetStillFailing = vsBaseline.some((f) => f.kind === targetFinding.kind && f.detail === targetFinding.detail);
192
+ targetResolved = { resolved: !targetStillFailing, remainingFindings: vsBaseline.length };
193
+ }
194
+
195
+ const passed = deltaConfidence >= -confidenceTolerance && newHighSeverity.length === 0 && !targetStillFailing;
196
+ return {
197
+ gate: "FIX-G3-mapd-regression", passed,
198
+ deltaConfidence, newOrphans, newHighSeverity, targetResolved,
199
+ };
200
+ }
201
+
202
+ /** Convenience: run all three general-pipeline gates for one fix attempt. */
203
+ export function runFixGates({ ws, patch, forbiddenPaths, maxFileSizeBytes, pkg, cwd, runTests, runLint, runTypecheck, preFixGraph, baseline, targetFinding }) {
204
+ const g1 = runPatchSafetyGate({ ws, patch, forbiddenPaths, maxFileSizeBytes });
205
+ if (!g1.passed) return [g1]; // no point running tests/regression on an unsafe patch
206
+ const g2 = runProjectCorrectnessGate({ cwd: cwd ?? ws.dir, pkg, runTests, runLint, runTypecheck });
207
+ const g3 = runRegressionGate({ ws, preFixGraph, baseline, targetFinding });
208
+ return [g1, g2, g3];
209
+ }