@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,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parser.js — Deterministic AST extraction (adapter #1: JavaScript/TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* DESIGN RULE: Nothing in this file may call an LLM. The map is ground truth,
|
|
5
|
+
* extracted from ASTs. Any file we cannot parse is recorded as unparsed and
|
|
6
|
+
* *lowers* the derived confidence score — we never guess its contents.
|
|
7
|
+
*
|
|
8
|
+
* Adding a language = implementing this same interface (see ParserAdapter shape
|
|
9
|
+
* at bottom) with e.g. tree-sitter-python and registering it in ADAPTERS.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parse } from "@babel/parser";
|
|
13
|
+
import _traverse from "@babel/traverse";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import { parsePolyglotFile, POLYGLOT_EXTENSIONS, KNOWN_UNSUPPORTED_EXTENSIONS } from "./polyglot.js";
|
|
18
|
+
|
|
19
|
+
const traverse = _traverse.default ?? _traverse;
|
|
20
|
+
|
|
21
|
+
const JS_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
22
|
+
|
|
23
|
+
const BABEL_OPTS = {
|
|
24
|
+
sourceType: "unambiguous",
|
|
25
|
+
errorRecovery: true,
|
|
26
|
+
plugins: ["typescript", "jsx", "decorators-legacy"], // Babel 8: class props / top-level await / import attributes are default
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** NestJS-style HTTP method decorators — verified against real Babel AST output, not assumed. */
|
|
30
|
+
const ROUTE_DECORATORS = new Set(["Get", "Post", "Put", "Delete", "Patch", "Options", "Head", "All"]);
|
|
31
|
+
|
|
32
|
+
function decoratorCalleeName(decorator) {
|
|
33
|
+
const expr = decorator.expression;
|
|
34
|
+
if (expr.type === "Identifier") return expr.name;
|
|
35
|
+
if (expr.type === "CallExpression" && expr.callee?.type === "Identifier") return expr.callee.name;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Trivial functions (getters, one-liners) below this size produce meaningless "duplicate" noise across nearly any codebase. */
|
|
40
|
+
const MIN_SHAPE_HASH_LINES = 4;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Structural "shape hash" for near-duplicate (Type-2 clone) detection: a
|
|
44
|
+
* deterministic hash of the function's AST node-type/operator sequence, with
|
|
45
|
+
* locally-declared identifiers (parameters, variables, catch params — incl.
|
|
46
|
+
* nested function scopes within the body) normalized to positional
|
|
47
|
+
* placeholders so a consistent variable rename doesn't change the hash.
|
|
48
|
+
* Literal VALUES are reduced to their type tag, not their content — two
|
|
49
|
+
* functions differing only in a string/number constant still match, which is
|
|
50
|
+
* the standard definition of a Type-2 clone. Property names and called
|
|
51
|
+
* function/method names ARE kept literal: calling a different function is a
|
|
52
|
+
* real behavioral difference, not a rename.
|
|
53
|
+
*
|
|
54
|
+
* Returns null for functions shorter than MIN_SHAPE_HASH_LINES — never
|
|
55
|
+
* computed, never a fabricated hash for a function too small to meaningfully
|
|
56
|
+
* compare.
|
|
57
|
+
*/
|
|
58
|
+
function computeShapeHash(fnPath, locLines) {
|
|
59
|
+
if (locLines < MIN_SHAPE_HASH_LINES) return null;
|
|
60
|
+
|
|
61
|
+
const localNames = new Set();
|
|
62
|
+
for (const name of Object.keys(fnPath.scope.bindings)) localNames.add(name);
|
|
63
|
+
fnPath.traverse({
|
|
64
|
+
Scopable(innerPath) {
|
|
65
|
+
for (const name of Object.keys(innerPath.scope.bindings)) localNames.add(name);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const localOrdinal = new Map();
|
|
70
|
+
const tokens = [];
|
|
71
|
+
const OPERATOR_NODE_TYPES = new Set(["BinaryExpression", "LogicalExpression", "AssignmentExpression", "UnaryExpression", "UpdateExpression"]);
|
|
72
|
+
const LITERAL_NODE_TYPES = new Set(["StringLiteral", "NumericLiteral", "BooleanLiteral"]);
|
|
73
|
+
|
|
74
|
+
const ownNameNode = fnPath.node.id ?? fnPath.node.key ?? null;
|
|
75
|
+
|
|
76
|
+
fnPath.traverse({
|
|
77
|
+
enter(p) {
|
|
78
|
+
const n = p.node;
|
|
79
|
+
if (n === ownNameNode) return; // the function/method's own declared name is not part of its body's shape
|
|
80
|
+
if (n.type === "Identifier") {
|
|
81
|
+
if (localNames.has(n.name)) {
|
|
82
|
+
if (!localOrdinal.has(n.name)) localOrdinal.set(n.name, `VAR${localOrdinal.size + 1}`);
|
|
83
|
+
tokens.push(localOrdinal.get(n.name));
|
|
84
|
+
} else {
|
|
85
|
+
tokens.push(`ref:${n.name}`);
|
|
86
|
+
}
|
|
87
|
+
} else if (LITERAL_NODE_TYPES.has(n.type)) {
|
|
88
|
+
tokens.push(`lit:${n.type}`);
|
|
89
|
+
} else if (OPERATOR_NODE_TYPES.has(n.type)) {
|
|
90
|
+
tokens.push(`${n.type}:${n.operator}`);
|
|
91
|
+
} else {
|
|
92
|
+
tokens.push(n.type);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return crypto.createHash("sha1").update(tokens.join("|")).digest("hex").slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse one file into a FileNode.
|
|
102
|
+
* @returns {{
|
|
103
|
+
* file: string, lang: "js", parsed: boolean, parseErrors: number,
|
|
104
|
+
* functions: Array<{name: string, exported: boolean, async: boolean, loc: number, params: number, calls: string[]}>,
|
|
105
|
+
* imports: Array<{source: string, names: string[]}>,
|
|
106
|
+
* exports: string[],
|
|
107
|
+
* loc: number
|
|
108
|
+
* }}
|
|
109
|
+
*/
|
|
110
|
+
export function parseJsFile(absPath, relPath) {
|
|
111
|
+
const code = fs.readFileSync(absPath, "utf8");
|
|
112
|
+
const loc = code.split("\n").length;
|
|
113
|
+
const node = {
|
|
114
|
+
file: relPath, lang: "js", parsed: false, parseErrors: 0,
|
|
115
|
+
functions: [], imports: [], exports: [], loc,
|
|
116
|
+
moduleType: null, // "module" | "script" — detected, used by modernization scan
|
|
117
|
+
varCount: 0, // `var` declarations — legacy-pattern signal
|
|
118
|
+
decoratorRoutes: [], // NestJS-style @Get()/@Post()/etc. method decorators — see ROUTE_DECORATORS
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let ast;
|
|
122
|
+
try {
|
|
123
|
+
ast = parse(code, BABEL_OPTS);
|
|
124
|
+
node.parseErrors = ast.errors?.length ?? 0;
|
|
125
|
+
node.parsed = true;
|
|
126
|
+
} catch {
|
|
127
|
+
return node; // unparsed file: recorded honestly, degrades confidence
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const exportedNames = new Set();
|
|
131
|
+
/** identifier name -> its ObjectExpression node, for `const x = {...}; module.exports = x;` */
|
|
132
|
+
const objectLiterals = new Map();
|
|
133
|
+
/** name -> { calls:Set, async, loc, params, exported } */
|
|
134
|
+
const fns = new Map();
|
|
135
|
+
|
|
136
|
+
const addFn = (name, fnNode, exported) => {
|
|
137
|
+
if (!name) return;
|
|
138
|
+
const entry = fns.get(name) ?? {
|
|
139
|
+
name, calls: new Set(), async: !!fnNode.async,
|
|
140
|
+
loc: fnNode.loc ? fnNode.loc.end.line - fnNode.loc.start.line + 1 : 0,
|
|
141
|
+
params: fnNode.params?.length ?? 0, exported: false,
|
|
142
|
+
};
|
|
143
|
+
entry.exported = entry.exported || exported;
|
|
144
|
+
fns.set(name, entry);
|
|
145
|
+
return entry;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const calleeName = (callee) => {
|
|
149
|
+
if (!callee) return null;
|
|
150
|
+
if (callee.type === "Identifier") return callee.name;
|
|
151
|
+
if (callee.type === "ThisExpression") return "this"; // this.method() — resolved against the enclosing function's `receiver`, see graph.js
|
|
152
|
+
if (callee.type === "CallExpression") return "()"; // method on a call result, e.g. fetch(x).then
|
|
153
|
+
if (callee.type === "MemberExpression") {
|
|
154
|
+
const obj = calleeName(callee.object);
|
|
155
|
+
const prop = callee.property?.name ?? null;
|
|
156
|
+
return obj && prop ? `${obj}.${prop}` : prop;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** X.prototype.method = <function-like> — l is the AssignmentExpression's left MemberExpression. */
|
|
162
|
+
const prototypeAssignmentTarget = (l) =>
|
|
163
|
+
l?.type === "MemberExpression" && l.object?.type === "MemberExpression" &&
|
|
164
|
+
l.object.property?.name === "prototype" && l.object.object?.type === "Identifier" && l.property?.name
|
|
165
|
+
? { className: l.object.object.name, methodName: l.property.name }
|
|
166
|
+
: null;
|
|
167
|
+
|
|
168
|
+
/** Object.assign(X.prototype, { method(){}, other: function(){} }) — target is the call's first argument. */
|
|
169
|
+
const objectAssignPrototypeTarget = (calleeNode, targetArg) =>
|
|
170
|
+
calleeNode?.type === "MemberExpression" && calleeNode.object?.name === "Object" && calleeNode.property?.name === "assign" &&
|
|
171
|
+
targetArg?.type === "MemberExpression" && targetArg.property?.name === "prototype" && targetArg.object?.type === "Identifier"
|
|
172
|
+
? targetArg.object.name
|
|
173
|
+
: null;
|
|
174
|
+
|
|
175
|
+
// Which function body are we inside? Track a stack.
|
|
176
|
+
const fnStack = [];
|
|
177
|
+
|
|
178
|
+
node.moduleType = ast.program.sourceType;
|
|
179
|
+
|
|
180
|
+
traverse(ast, {
|
|
181
|
+
VariableDeclaration(p) {
|
|
182
|
+
if (p.node.kind === "var") node.varCount++;
|
|
183
|
+
for (const dec of p.node.declarations) {
|
|
184
|
+
if (dec.id?.type === "Identifier" && dec.init?.type === "ObjectExpression") {
|
|
185
|
+
objectLiterals.set(dec.id.name, dec.init);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
ImportDeclaration(p) {
|
|
190
|
+
node.imports.push({
|
|
191
|
+
source: p.node.source.value,
|
|
192
|
+
names: p.node.specifiers.map((s) => s.local.name),
|
|
193
|
+
// `import type { X }` is erased at compile time — it creates no runtime
|
|
194
|
+
// dependency. Flagged so test credit can tell "a test exercises this"
|
|
195
|
+
// from "a test borrows its types", which are not the same claim. Only
|
|
196
|
+
// present when true, so the common record shape is unchanged.
|
|
197
|
+
...(p.node.importKind === "type" ||
|
|
198
|
+
(p.node.specifiers.length > 0 && p.node.specifiers.every((sp) => sp.importKind === "type"))
|
|
199
|
+
? { typeOnly: true }
|
|
200
|
+
: {}),
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
// import("./x.js") with a STATIC string specifier is a real, resolvable
|
|
204
|
+
// module edge (lazy-loaded routes/modals are imported exactly this way) —
|
|
205
|
+
// without it the target looks orphaned. Template-literal dynamic imports
|
|
206
|
+
// stay reachability.js's job. Babel 8 parses import() as ImportExpression.
|
|
207
|
+
ImportExpression(p) {
|
|
208
|
+
if (p.node.source?.type === "StringLiteral") {
|
|
209
|
+
node.imports.push({ source: p.node.source.value, names: [] });
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
CallExpression(p) {
|
|
213
|
+
// require() imports — capture the bound names so CJS resolves like ESM
|
|
214
|
+
if (p.node.callee.name === "require" && p.node.arguments[0]?.type === "StringLiteral") {
|
|
215
|
+
const names = [];
|
|
216
|
+
const parent = p.parent;
|
|
217
|
+
if (parent?.type === "VariableDeclarator") {
|
|
218
|
+
if (parent.id.type === "Identifier") names.push(parent.id.name);
|
|
219
|
+
else if (parent.id.type === "ObjectPattern") {
|
|
220
|
+
for (const prop of parent.id.properties) {
|
|
221
|
+
if (prop.value?.type === "Identifier") names.push(prop.value.name);
|
|
222
|
+
else if (prop.argument?.type === "Identifier") names.push(prop.argument.name); // rest
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (parent?.type === "MemberExpression" && p.parentPath.parent?.type === "VariableDeclarator"
|
|
226
|
+
&& p.parentPath.parent.id?.type === "Identifier") {
|
|
227
|
+
names.push(p.parentPath.parent.id.name); // const x = require('y').z
|
|
228
|
+
}
|
|
229
|
+
node.imports.push({ source: p.node.arguments[0].value, names });
|
|
230
|
+
}
|
|
231
|
+
const name = calleeName(p.node.callee);
|
|
232
|
+
if (name && fnStack.length) fnStack[fnStack.length - 1].calls.add(name);
|
|
233
|
+
},
|
|
234
|
+
AssignmentExpression(p) {
|
|
235
|
+
// CJS export surface: module.exports = ... / module.exports.x = ... / exports.x = ...
|
|
236
|
+
const l = p.node.left;
|
|
237
|
+
if (l?.type !== "MemberExpression") return;
|
|
238
|
+
const objName = l.object?.name ?? (l.object?.object?.name === "module" && l.object?.property?.name === "exports" ? "module.exports" : null);
|
|
239
|
+
if (objName === "exports" || objName === "module.exports") {
|
|
240
|
+
if (l.property?.name) exportedNames.add(l.property.name);
|
|
241
|
+
} else if (l.object?.name === "module" && l.property?.name === "exports") {
|
|
242
|
+
const r = p.node.right;
|
|
243
|
+
if (r.type === "ObjectExpression") {
|
|
244
|
+
for (const prop of r.properties) if (prop.key?.name) exportedNames.add(prop.key.name);
|
|
245
|
+
} else if (r.type === "Identifier") exportedNames.add(r.name);
|
|
246
|
+
else if (r.id?.name) exportedNames.add(r.id.name); // module.exports = function foo(){}
|
|
247
|
+
else if (r.type === "CallExpression" && r.callee?.name) exportedNames.add(r.callee.name); // = createX()
|
|
248
|
+
else exportedNames.add("default");
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
ExportNamedDeclaration(p) {
|
|
252
|
+
for (const s of p.node.specifiers ?? []) exportedNames.add(s.exported.name ?? s.exported.value);
|
|
253
|
+
const d = p.node.declaration;
|
|
254
|
+
if (d?.id?.name) exportedNames.add(d.id.name);
|
|
255
|
+
if (d?.declarations) for (const dec of d.declarations) if (dec.id?.name) exportedNames.add(dec.id.name);
|
|
256
|
+
// `export { x } from "./impl.js"` — a barrel re-export IS an import of
|
|
257
|
+
// ./impl.js; without this edge the implementation file falls out of
|
|
258
|
+
// every workflow and gets misreported as unreachable.
|
|
259
|
+
if (p.node.source?.value) {
|
|
260
|
+
node.imports.push({ source: p.node.source.value, names: p.node.specifiers.map((s) => s.local?.name ?? s.exported?.name).filter(Boolean) });
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
// `export * from "./wide.js"` — same barrel edge. The re-exported NAMES
|
|
264
|
+
// are statically unknowable here, so none are fabricated; only the
|
|
265
|
+
// verified file relationship is recorded.
|
|
266
|
+
ExportAllDeclaration(p) {
|
|
267
|
+
if (p.node.source?.value) node.imports.push({ source: p.node.source.value, names: [] });
|
|
268
|
+
},
|
|
269
|
+
ExportDefaultDeclaration(p) {
|
|
270
|
+
const d = p.node.declaration;
|
|
271
|
+
exportedNames.add(d?.id?.name ?? "default");
|
|
272
|
+
},
|
|
273
|
+
"FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod|ObjectMethod": {
|
|
274
|
+
enter(p) {
|
|
275
|
+
const n = p.node;
|
|
276
|
+
let name = n.id?.name ?? n.key?.name ?? null;
|
|
277
|
+
let receiver = null;
|
|
278
|
+
|
|
279
|
+
// const foo = () => {} / const foo = function () {}
|
|
280
|
+
if (!name && p.parent?.type === "VariableDeclarator" && p.parent.id?.type === "Identifier") {
|
|
281
|
+
name = p.parent.id.name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// X.prototype.method = function(){} / async function(){} / () => {}
|
|
285
|
+
if (p.parent?.type === "AssignmentExpression") {
|
|
286
|
+
const target = prototypeAssignmentTarget(p.parent.left);
|
|
287
|
+
if (target) { receiver = target.className; name = `${receiver}.prototype.${target.methodName}`; }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Object.assign(X.prototype, { method(){}, other: function(){} })
|
|
291
|
+
if (!receiver) {
|
|
292
|
+
let keyName = n.key?.name ?? n.key?.value ?? null; // ObjectMethod carries its own key
|
|
293
|
+
let objExprPath = null;
|
|
294
|
+
if (p.parentPath?.isObjectExpression()) {
|
|
295
|
+
objExprPath = p.parentPath; // ObjectMethod directly inside the object literal
|
|
296
|
+
} else if (p.parentPath?.isObjectProperty() && p.parentPath.parentPath?.isObjectExpression()) {
|
|
297
|
+
keyName = p.parentPath.node.key?.name ?? p.parentPath.node.key?.value ?? keyName;
|
|
298
|
+
objExprPath = p.parentPath.parentPath; // `other: function(){}` — FunctionExpression as a property value
|
|
299
|
+
}
|
|
300
|
+
const callPath = objExprPath?.parentPath?.isCallExpression() ? objExprPath.parentPath : null;
|
|
301
|
+
if (callPath && keyName) {
|
|
302
|
+
const className = objectAssignPrototypeTarget(callPath.node.callee, callPath.node.arguments?.[0]);
|
|
303
|
+
if (className) { receiver = className; name = `${receiver}.prototype.${keyName}`; }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// NestJS-style @Get()/@Post()/etc. method decorators — a real, verified AST
|
|
308
|
+
// shape (ClassMethod.decorators[].expression), not a regex guess.
|
|
309
|
+
if (n.type === "ClassMethod" && Array.isArray(n.decorators)) {
|
|
310
|
+
const classPath = p.findParent((pp) => pp.isClassDeclaration() || pp.isClassExpression());
|
|
311
|
+
const className = classPath?.node.id?.name ?? null;
|
|
312
|
+
for (const decorator of n.decorators) {
|
|
313
|
+
const decoratorName = decoratorCalleeName(decorator);
|
|
314
|
+
if (decoratorName && ROUTE_DECORATORS.has(decoratorName)) {
|
|
315
|
+
node.decoratorRoutes.push({ class: className, method: n.key?.name ?? null, decorator: decoratorName });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// exported inline?
|
|
321
|
+
const exportedInline =
|
|
322
|
+
p.findParent((pp) => pp.isExportNamedDeclaration() || pp.isExportDefaultDeclaration()) != null;
|
|
323
|
+
const entry = addFn(name ?? `<anon:${n.loc?.start.line ?? "?"}>`, n, exportedInline);
|
|
324
|
+
if (entry) {
|
|
325
|
+
if (receiver) entry.receiver = receiver;
|
|
326
|
+
entry.shapeHash = computeShapeHash(p, entry.loc);
|
|
327
|
+
}
|
|
328
|
+
fnStack.push(entry ?? { calls: new Set() });
|
|
329
|
+
},
|
|
330
|
+
exit() { fnStack.pop(); },
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// `const alpaca = { getPortfolioHistory() {...} }; module.exports = alpaca;` — the
|
|
335
|
+
// AssignmentExpression visitor above only saw the identifier "alpaca" being exported;
|
|
336
|
+
// resolve it back to the object literal it was declared with and export its properties
|
|
337
|
+
// too, so CJS's "declare an object, export the identifier" pattern isn't missed.
|
|
338
|
+
for (const name of [...exportedNames]) {
|
|
339
|
+
const obj = objectLiterals.get(name);
|
|
340
|
+
if (!obj) continue;
|
|
341
|
+
for (const prop of obj.properties) {
|
|
342
|
+
const key = prop.key?.name ?? prop.key?.value;
|
|
343
|
+
if (key) exportedNames.add(key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const fn of fns.values()) {
|
|
348
|
+
fn.exported = fn.exported || exportedNames.has(fn.name);
|
|
349
|
+
node.functions.push({ ...fn, calls: [...fn.calls] });
|
|
350
|
+
}
|
|
351
|
+
node.exports = [...exportedNames];
|
|
352
|
+
return node;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Directory names skipped at ANY depth no matter what .mapdrc says: installed
|
|
356
|
+
// dependencies are never project source. (.mapdrc excludes are root-anchored —
|
|
357
|
+
// "node_modules/**" alone let a nested app's packages/x/node_modules flood the map.)
|
|
358
|
+
export const ALWAYS_IGNORE = new Set(["node_modules"]);
|
|
359
|
+
export const DEFAULT_IGNORE = new Set(["node_modules", ".git", "dist", "build", "coverage", ".mapd", ".next", "out"]);
|
|
360
|
+
|
|
361
|
+
function normalizeRelPath(rel) {
|
|
362
|
+
return rel.split(path.sep).join("/").replace(/^\.\//, "");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function escapeRegExp(s) {
|
|
366
|
+
return s.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function globToRegExp(pattern) {
|
|
370
|
+
const p = normalizeRelPath(String(pattern ?? "").trim());
|
|
371
|
+
if (!p) return /^$/;
|
|
372
|
+
let out = "^";
|
|
373
|
+
for (let i = 0; i < p.length; i++) {
|
|
374
|
+
const ch = p[i];
|
|
375
|
+
const next = p[i + 1];
|
|
376
|
+
if (ch === "*" && next === "*") {
|
|
377
|
+
const after = p[i + 2];
|
|
378
|
+
if (after === "/") {
|
|
379
|
+
out += "(?:.*/)?";
|
|
380
|
+
i += 2;
|
|
381
|
+
} else {
|
|
382
|
+
out += ".*";
|
|
383
|
+
i++;
|
|
384
|
+
}
|
|
385
|
+
} else if (ch === "*") {
|
|
386
|
+
out += "[^/]*";
|
|
387
|
+
} else if (ch === "?") {
|
|
388
|
+
out += "[^/]";
|
|
389
|
+
} else {
|
|
390
|
+
out += escapeRegExp(ch);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
out += "$";
|
|
394
|
+
return new RegExp(out);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const globRegexCache = new Map();
|
|
398
|
+
|
|
399
|
+
function matchesGlob(rel, pattern) {
|
|
400
|
+
const normalizedRel = normalizeRelPath(rel).replace(/\/$/, "");
|
|
401
|
+
const normalizedPattern = normalizeRelPath(pattern).replace(/\/$/, "");
|
|
402
|
+
if (!normalizedPattern) return false;
|
|
403
|
+
|
|
404
|
+
// The common directory-subtree pattern should match the directory itself
|
|
405
|
+
// too, so "dist/**" excludes both "dist" and "dist/app.js".
|
|
406
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
407
|
+
const dir = normalizedPattern.slice(0, -3);
|
|
408
|
+
if (normalizedRel === dir || normalizedRel.startsWith(`${dir}/`)) return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let re = globRegexCache.get(normalizedPattern);
|
|
412
|
+
if (!re) {
|
|
413
|
+
re = globToRegExp(normalizedPattern);
|
|
414
|
+
globRegexCache.set(normalizedPattern, re);
|
|
415
|
+
}
|
|
416
|
+
return re.test(normalizedRel);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function matchesAnyGlob(rel, patterns = []) {
|
|
420
|
+
return patterns.some((p) => matchesGlob(rel, p));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function skippedFileNode(relPath, reason) {
|
|
424
|
+
return {
|
|
425
|
+
file: relPath, lang: "js", parsed: false, parseErrors: 0,
|
|
426
|
+
functions: [], imports: [], exports: [], loc: 0, skippedReason: reason,
|
|
427
|
+
moduleType: null, varCount: 0, decoratorRoutes: [],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Walk a project directory and parse every supported file deterministically.
|
|
433
|
+
* Optional `cache` (Map rel → {hash, node}) skips re-parsing unchanged files —
|
|
434
|
+
* correctness identical (keyed on content hash, not mtime), cost proportional
|
|
435
|
+
* to the diff. Used by `mapd watch`.
|
|
436
|
+
*/
|
|
437
|
+
export function parseProject(rootDir, { ignore = DEFAULT_IGNORE, cache = null, include = [], exclude = [], maxFileSizeBytes = Infinity, polyglot = true } = {}) {
|
|
438
|
+
const files = [];
|
|
439
|
+
const skipped = []; // supported-language files we failed to parse
|
|
440
|
+
const unsupported = []; // files in languages with no adapter yet (honestly reported)
|
|
441
|
+
let cacheHits = 0, parsedCount = 0;
|
|
442
|
+
|
|
443
|
+
const parseOne = (abs, rel, parseFn) => {
|
|
444
|
+
const stat = fs.statSync(abs);
|
|
445
|
+
if (Number.isFinite(maxFileSizeBytes) && stat.size > maxFileSizeBytes) {
|
|
446
|
+
const parsed = skippedFileNode(rel, "file-too-large");
|
|
447
|
+
cache?.delete(rel);
|
|
448
|
+
files.push(parsed);
|
|
449
|
+
skipped.push(rel);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
let parsed;
|
|
453
|
+
if (cache) {
|
|
454
|
+
const content = fs.readFileSync(abs);
|
|
455
|
+
const hash = crypto.createHash("sha1").update(content).digest("hex");
|
|
456
|
+
const hit = cache.get(rel);
|
|
457
|
+
if (hit && hit.hash === hash) {
|
|
458
|
+
parsed = hit.node;
|
|
459
|
+
cacheHits++;
|
|
460
|
+
} else {
|
|
461
|
+
parsed = parseFn(abs, rel);
|
|
462
|
+
parsedCount++;
|
|
463
|
+
cache.set(rel, { hash, node: parsed });
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
parsed = parseFn(abs, rel);
|
|
467
|
+
parsedCount++;
|
|
468
|
+
}
|
|
469
|
+
files.push(parsed);
|
|
470
|
+
if (!parsed.parsed) skipped.push(rel);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const walk = (dir) => {
|
|
474
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
475
|
+
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
476
|
+
const abs = path.join(dir, entry.name);
|
|
477
|
+
const rel = normalizeRelPath(path.relative(rootDir, abs));
|
|
478
|
+
if (matchesAnyGlob(rel, exclude)) continue;
|
|
479
|
+
if (entry.isDirectory()) {
|
|
480
|
+
if (!ignore.has(entry.name)) walk(abs);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
484
|
+
if (include.length && !matchesAnyGlob(rel, include)) continue;
|
|
485
|
+
if (JS_EXTENSIONS.has(ext)) {
|
|
486
|
+
parseOne(abs, rel, parseJsFile);
|
|
487
|
+
} else if (POLYGLOT_EXTENSIONS[ext]) {
|
|
488
|
+
// heuristic-tier adapter (see polyglot.js); the `mapping.polyglot: false`
|
|
489
|
+
// kill switch reverts these to the honest "unsupported" bucket
|
|
490
|
+
if (polyglot) parseOne(abs, rel, parsePolyglotFile);
|
|
491
|
+
else unsupported.push(rel);
|
|
492
|
+
} else if (KNOWN_UNSUPPORTED_EXTENSIONS.has(ext)) {
|
|
493
|
+
unsupported.push(rel);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
walk(rootDir);
|
|
498
|
+
if (cache) {
|
|
499
|
+
const live = new Set(files.map((f) => f.file));
|
|
500
|
+
for (const key of cache.keys()) if (!live.has(key)) cache.delete(key);
|
|
501
|
+
}
|
|
502
|
+
return { files, skipped, unsupported, cacheHits, parsedCount };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Most recent mtime among source files parseProject would scan — same ignore
|
|
507
|
+
* rules, no parsing, just a stat walk. Used by staleness.js to tell whether
|
|
508
|
+
* an on-disk report predates a source change, without duplicating or
|
|
509
|
+
* drifting from parseProject's own file-discovery rules.
|
|
510
|
+
*/
|
|
511
|
+
export function latestSourceMtime(rootDir, ignore = DEFAULT_IGNORE) {
|
|
512
|
+
let latest = 0;
|
|
513
|
+
const walk = (dir) => {
|
|
514
|
+
let entries;
|
|
515
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
518
|
+
const abs = path.join(dir, entry.name);
|
|
519
|
+
if (entry.isDirectory()) { if (!ignore.has(entry.name)) walk(abs); continue; }
|
|
520
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
521
|
+
if (!JS_EXTENSIONS.has(ext) && !POLYGLOT_EXTENSIONS[ext]) continue;
|
|
522
|
+
try { const m = fs.statSync(abs).mtimeMs; if (m > latest) latest = m; } catch { /* file vanished mid-walk */ }
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
walk(rootDir);
|
|
526
|
+
return latest;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* ParserAdapter interface (for future languages):
|
|
531
|
+
* { extensions: Set<string>, parseFile(absPath, relPath) -> FileNode }
|
|
532
|
+
* Adapter #2 is polyglot.js: heuristic-tier extraction for Python/Go/Rust/
|
|
533
|
+
* Ruby/Java/PHP, every node marked `parserKind: "heuristic"` so downstream
|
|
534
|
+
* consumers can discount it. A full tree-sitter adapter would slot in the
|
|
535
|
+
* same way and simply drop the heuristic mark.
|
|
536
|
+
*/
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* policy.js — the command-policy classifier shared by chat and mcp. Every
|
|
3
|
+
* dev command chat/mcp might run (npm test, git diff, ...) is classified
|
|
4
|
+
* before it is ever spawned; anything not in the allowlist table below is
|
|
5
|
+
* refused outright, regardless of classification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CLASS = {
|
|
9
|
+
READ_ONLY: "read-only",
|
|
10
|
+
VERIFICATION: "verification",
|
|
11
|
+
PROJECT_MUTATION: "project-mutation",
|
|
12
|
+
DEPENDENCY_MUTATION: "dependency-mutation",
|
|
13
|
+
GIT_MUTATION: "git-mutation",
|
|
14
|
+
DESTRUCTIVE: "destructive",
|
|
15
|
+
NETWORKED: "networked",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Any of these may legitimately be "the project's package manager" — never assume it's npm. */
|
|
19
|
+
const PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
|
|
20
|
+
|
|
21
|
+
/** Ordered rules — first match wins. `test` receives (cmd, args[]). */
|
|
22
|
+
const RULES = [
|
|
23
|
+
{ test: (c, a) => PACKAGE_MANAGERS.has(c) && a[0] === "test", classification: CLASS.VERIFICATION },
|
|
24
|
+
{ test: (c, a) => PACKAGE_MANAGERS.has(c) && a[0] === "run" && ["lint", "typecheck", "type-check", "build"].includes(a[1]), classification: CLASS.VERIFICATION },
|
|
25
|
+
{ test: (c, a) => PACKAGE_MANAGERS.has(c) && a[0] === "run" && ["dev", "start"].includes(a[1]), classification: CLASS.NETWORKED },
|
|
26
|
+
{ test: (c, a) => PACKAGE_MANAGERS.has(c) && a[0] === "start", classification: CLASS.NETWORKED },
|
|
27
|
+
{ test: (c, a) => PACKAGE_MANAGERS.has(c) && ["install", "ci", "update", "uninstall", "add", "remove"].includes(a[0]), classification: CLASS.DEPENDENCY_MUTATION },
|
|
28
|
+
|
|
29
|
+
{ test: (c, a) => c === "git" && ["status", "diff", "log", "show", "branch"].includes(a[0]), classification: CLASS.READ_ONLY },
|
|
30
|
+
{ test: (c, a) => c === "git" && a[0] === "push" && a.includes("--force"), classification: CLASS.DESTRUCTIVE },
|
|
31
|
+
{ test: (c, a) => c === "git" && ["push", "commit", "add", "checkout", "merge", "rebase"].includes(a[0]), classification: CLASS.GIT_MUTATION },
|
|
32
|
+
{ test: (c, a) => c === "git" && ["reset", "clean"].includes(a[0]) && (a.includes("--hard") || a.includes("-f") || a.includes("-fd")), classification: CLASS.DESTRUCTIVE },
|
|
33
|
+
|
|
34
|
+
{ test: (c) => ["grep", "rg", "find", "ls", "cat", "wc"].includes(c), classification: CLASS.READ_ONLY },
|
|
35
|
+
{ test: (c, a) => c === "rm", classification: CLASS.DESTRUCTIVE },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Returns `{ classification, allowed }` — `allowed: false` means "not in the allowlist, refuse outright." */
|
|
39
|
+
export function classifyCommand(cmd, args = []) {
|
|
40
|
+
const rule = RULES.find((r) => r.test(cmd, args));
|
|
41
|
+
if (!rule) return { classification: null, allowed: false, reason: `'${cmd}' is not an allowlisted command` };
|
|
42
|
+
return { classification: rule.classification, allowed: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether a classified command may run given the resolved config and
|
|
47
|
+
* whether the caller has already obtained explicit human approval.
|
|
48
|
+
*/
|
|
49
|
+
export function isPermitted(classification, config, { approved = false } = {}) {
|
|
50
|
+
switch (classification) {
|
|
51
|
+
case CLASS.READ_ONLY:
|
|
52
|
+
case CLASS.VERIFICATION:
|
|
53
|
+
return { permitted: approved || config.chat?.autoRunReadOnly !== false, requiresApproval: false };
|
|
54
|
+
case CLASS.PROJECT_MUTATION:
|
|
55
|
+
case CLASS.DEPENDENCY_MUTATION:
|
|
56
|
+
case CLASS.GIT_MUTATION:
|
|
57
|
+
return { permitted: approved, requiresApproval: true };
|
|
58
|
+
case CLASS.NETWORKED:
|
|
59
|
+
return { permitted: approved && config.security?.allowNetworkCommands === true, requiresApproval: true };
|
|
60
|
+
case CLASS.DESTRUCTIVE:
|
|
61
|
+
return { permitted: approved && config.security?.allowDestructiveCommands === true, requiresApproval: true };
|
|
62
|
+
default:
|
|
63
|
+
return { permitted: false, requiresApproval: true };
|
|
64
|
+
}
|
|
65
|
+
}
|