@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.
- package/LICENSE +21 -0
- package/MASTER_PROMPT.md +134 -0
- package/README.md +494 -0
- package/SETUP.md +108 -0
- package/UAT.md +77 -0
- package/package.json +56 -0
- package/src/adapters/github-app.js +79 -0
- package/src/agents/anthropicClient.js +18 -0
- package/src/agents/llm.js +196 -0
- package/src/agents/modelResolver.js +87 -0
- package/src/agents/provider.js +222 -0
- package/src/chat/commandRunner.js +86 -0
- package/src/chat/commands.js +275 -0
- package/src/chat/intent.js +87 -0
- package/src/chat/llmIntent.js +118 -0
- package/src/chat/repl.js +471 -0
- package/src/cli.js +1408 -0
- package/src/config/index.js +197 -0
- package/src/config/schema.js +119 -0
- package/src/core/assist.js +64 -0
- package/src/core/audit.js +63 -0
- package/src/core/changes.js +110 -0
- package/src/core/confidence.js +0 -0
- package/src/core/configLint.js +141 -0
- package/src/core/diagnose.js +262 -0
- package/src/core/docs.js +140 -0
- package/src/core/doctor.js +134 -0
- package/src/core/envFiles.js +43 -0
- package/src/core/events.js +53 -0
- package/src/core/evidence.js +212 -0
- package/src/core/findingScoring.js +20 -0
- package/src/core/fix.js +192 -0
- package/src/core/fixApply.js +172 -0
- package/src/core/frameworkEntries.js +247 -0
- package/src/core/gates.js +209 -0
- package/src/core/graph.js +467 -0
- package/src/core/grounding.js +235 -0
- package/src/core/handoff.js +157 -0
- package/src/core/importResolver.js +218 -0
- package/src/core/improve.js +226 -0
- package/src/core/integrate.js +169 -0
- package/src/core/intelligence.js +212 -0
- package/src/core/modernize.js +370 -0
- package/src/core/parseCache.js +64 -0
- package/src/core/parser.js +536 -0
- package/src/core/policy.js +65 -0
- package/src/core/polyglot.js +333 -0
- package/src/core/proc.js +25 -0
- package/src/core/reachability.js +543 -0
- package/src/core/regression.js +193 -0
- package/src/core/resolution.js +92 -0
- package/src/core/retry.js +61 -0
- package/src/core/review.js +219 -0
- package/src/core/score.js +338 -0
- package/src/core/security.js +0 -0
- package/src/core/session.js +143 -0
- package/src/core/solutions.js +254 -0
- package/src/core/staleness.js +45 -0
- package/src/core/testGuidance.js +226 -0
- package/src/core/theme.js +50 -0
- package/src/core/trace.js +151 -0
- package/src/core/verify.js +123 -0
- package/src/core/view.js +221 -0
- package/src/core/viewServer.js +88 -0
- package/src/core/watch.js +76 -0
- package/src/core/workspace.js +115 -0
- package/src/mcp/server.js +48 -0
- package/src/mcp/tools.js +423 -0
- 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
|
+
}
|