@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modernize.js — Function 3: modernization / optimization scan.
|
|
3
|
+
*
|
|
4
|
+
* "More deterministic, not hallucinating" is implemented as three detector
|
|
5
|
+
* tiers, all rule-based; the LLM only *elaborates* findings into proposals
|
|
6
|
+
* (heavy mode, optional) and cannot create or score them.
|
|
7
|
+
*
|
|
8
|
+
* MODES are breadth knobs, not intelligence knobs:
|
|
9
|
+
* light — dependency tier only (fast; safe for every push)
|
|
10
|
+
* medium — + code-pattern tier on the largest workflows (top 3 by file count)
|
|
11
|
+
* heavy — + code-pattern tier on ALL workflows + architecture tier
|
|
12
|
+
* (+ --propose drafts LLM migration plans per finding)
|
|
13
|
+
*
|
|
14
|
+
* OPERATIONAL IMPACT is derived per finding:
|
|
15
|
+
* reach occurrences / touched files, normalized against repo size
|
|
16
|
+
* certainty detector-defined: curated table = 1.0 (deprecated by upstream,
|
|
17
|
+
* a fact), npm-outdated = 0.9 (registry-measured), AST pattern
|
|
18
|
+
* = 1.0 for occurrence (the pattern IS there) scaled by whether
|
|
19
|
+
* replacement is behavior-identical (table constant per rule)
|
|
20
|
+
* safety test presence over touched files (how safely can we change it)
|
|
21
|
+
* impact = reach × certainty; priority = impact × (0.5 + 0.5 × safety)
|
|
22
|
+
* Constants are per-rule policy, documented inline — never per-run guesses.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { execSync } from "node:child_process";
|
|
28
|
+
import { performance } from "node:perf_hooks";
|
|
29
|
+
import { detectCircularDependencies, isTestFile } from "./graph.js";
|
|
30
|
+
import { mergeFindingLifecycle } from "./regression.js";
|
|
31
|
+
|
|
32
|
+
/* ---------------- Tier 1: dependency health ---------------- */
|
|
33
|
+
|
|
34
|
+
// Curated table: upstream-deprecated or community-superseded packages.
|
|
35
|
+
// certainty 1.0 = deprecation/supersession is a published fact, not an opinion.
|
|
36
|
+
export const LEGACY_DEPS = {
|
|
37
|
+
request: { replacement: "undici / native fetch", reason: "deprecated by maintainers (Feb 2020)", kind: "deprecated" },
|
|
38
|
+
moment: { replacement: "dayjs / date-fns / Temporal", reason: "in maintenance mode per project docs", kind: "maintenance-mode" },
|
|
39
|
+
"node-sass": { replacement: "sass (dart-sass)", reason: "deprecated in favor of dart-sass", kind: "deprecated" },
|
|
40
|
+
"babel-eslint": { replacement: "@babel/eslint-parser", reason: "renamed/superseded", kind: "superseded" },
|
|
41
|
+
tslint: { replacement: "eslint + typescript-eslint", reason: "deprecated by Palantir", kind: "deprecated" },
|
|
42
|
+
"left-pad": { replacement: "String.prototype.padStart", reason: "native replacement exists", kind: "native-replacement" },
|
|
43
|
+
mkdirp: { replacement: "fs.mkdir({recursive:true})", reason: "native replacement exists", kind: "native-replacement" },
|
|
44
|
+
rimraf: { replacement: "fs.rm({recursive:true,force:true})", reason: "native replacement exists (Node 14.14+)", kind: "native-replacement" },
|
|
45
|
+
"body-parser": { replacement: "express.json()/express.urlencoded()", reason: "bundled into Express 4.16+", kind: "bundled" },
|
|
46
|
+
bluebird: { replacement: "native Promise", reason: "native promises match performance/features", kind: "native-replacement" },
|
|
47
|
+
underscore: { replacement: "native array/object methods or lodash-es", reason: "largely superseded", kind: "superseded" },
|
|
48
|
+
q: { replacement: "native Promise", reason: "native replacement exists", kind: "native-replacement" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function depFindings(rootDir, pkg, { checkRegistry = true, registryTimeoutMs = 2000 } = {}) {
|
|
52
|
+
const findings = [];
|
|
53
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
54
|
+
for (const [name, meta] of Object.entries(LEGACY_DEPS)) {
|
|
55
|
+
if (deps[name]) {
|
|
56
|
+
findings.push({
|
|
57
|
+
tier: "dependency", rule: `legacy-dep:${name}`,
|
|
58
|
+
detail: `\`${name}\` — ${meta.reason}. Suggested replacement: ${meta.replacement}.`,
|
|
59
|
+
certainty: 1.0, occurrences: 1, files: ["package.json"], suggestion: meta.replacement,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// registry-measured staleness (best effort; offline → signal skipped, reported)
|
|
64
|
+
if (!checkRegistry) {
|
|
65
|
+
findings.push({ tier: "dependency", rule: "registry-skipped", informational: true,
|
|
66
|
+
detail: "npm outdated skipped by configuration — registry staleness signal skipped.", certainty: 1.0, occurrences: 0, files: [] });
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const out = execSync("npm outdated --json", { cwd: rootDir, stdio: ["ignore", "pipe", "ignore"], timeout: registryTimeoutMs }).toString();
|
|
71
|
+
const outdated = out.trim() ? JSON.parse(out) : {};
|
|
72
|
+
const majors = Object.entries(outdated).filter(([, v]) =>
|
|
73
|
+
v.current && v.latest && v.current.split(".")[0] !== v.latest.split(".")[0]);
|
|
74
|
+
if (majors.length) {
|
|
75
|
+
findings.push({
|
|
76
|
+
tier: "dependency", rule: "major-versions-behind",
|
|
77
|
+
detail: `${majors.length} dependencies a major version behind: ${majors.map(([k, v]) => `${k} (${v.current}→${v.latest})`).join(", ")}.`,
|
|
78
|
+
certainty: 0.9, occurrences: majors.length, files: ["package.json"],
|
|
79
|
+
suggestion: "staged major upgrades, largest-risk first",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
findings.push({ tier: "dependency", rule: "registry-unavailable", informational: true,
|
|
84
|
+
detail: "npm outdated unavailable (offline or no registry access) — staleness signal skipped.", certainty: 1.0, occurrences: 0, files: [] });
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------------- Tier 2: code patterns (from the map — zero extra parsing) ---------------- */
|
|
90
|
+
|
|
91
|
+
// Each rule reads FileNode data the parser already extracted.
|
|
92
|
+
// behaviorSafe: replacement is semantically identical (raises certainty of benefit).
|
|
93
|
+
const PATTERN_RULES = [
|
|
94
|
+
{
|
|
95
|
+
rule: "var-declarations", behaviorSafe: true,
|
|
96
|
+
detect: (f) => f.varCount,
|
|
97
|
+
detail: (n) => `${n} \`var\` declaration(s) — migrate to let/const for block scoping.`,
|
|
98
|
+
suggestion: "let/const migration (codemod-able)",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
rule: "promise-then-chains", behaviorSafe: false,
|
|
102
|
+
detect: (f) => f.functions.reduce((a, fn) => a + fn.calls.filter((c) => c.endsWith(".then") || c.endsWith(".catch")).length, 0),
|
|
103
|
+
detail: (n) => `${n} .then/.catch chain site(s) — candidates for async/await.`,
|
|
104
|
+
suggestion: "async/await refactor",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
rule: "deprecated-node-api", behaviorSafe: false,
|
|
108
|
+
detect: (f) => f.functions.reduce((a, fn) => a + fn.calls.filter((c) =>
|
|
109
|
+
["url.parse", "fs.exists", "util.isArray", "querystring.parse"].includes(c)).length, 0),
|
|
110
|
+
detail: (n) => `${n} call(s) to deprecated Node APIs (url.parse / fs.exists / util.isArray / querystring.parse).`,
|
|
111
|
+
suggestion: "WHATWG URL, fs.access/existsSync, Array.isArray, URLSearchParams",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
// A .cjs extension is Node's explicit, unconditional CommonJS opt-out —
|
|
115
|
+
// Node treats it as CommonJS regardless of package.json's "type" field,
|
|
116
|
+
// so there is no real format ambiguity or interop risk there (unlike a
|
|
117
|
+
// plain .js/.mjs file, where Node has to guess from "type" and a mismatch
|
|
118
|
+
// is a genuine bug). Only flag the genuinely ambiguous case.
|
|
119
|
+
rule: "cjs-in-esm-project", behaviorSafe: false, projectLevel: true,
|
|
120
|
+
detect: (f, pkg) => (pkg?.type === "module" && f.moduleType === "script" && !f.file.endsWith(".cjs") ? 1 : 0),
|
|
121
|
+
detail: (n) => `${n} ambiguous CommonJS file(s) (.js/.mjs using require()/module.exports, not .cjs) inside an ` +
|
|
122
|
+
`ESM ("type":"module") package — Node must guess the format here, real interop risk. ` +
|
|
123
|
+
`(.cjs files are Node's explicit CommonJS opt-out and are excluded — no ambiguity there.)`,
|
|
124
|
+
suggestion: "convert to ESM imports/exports, or rename to .cjs to make the CommonJS choice explicit",
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
function patternFindings(graph, pkg, scopeFiles, generatedSet) {
|
|
129
|
+
const findings = [];
|
|
130
|
+
// Generated files (bundler output, etc.) are excluded from every rule here
|
|
131
|
+
// — a var-declarations/promise-chain/CJS finding pointing at a build
|
|
132
|
+
// artifact tells you to hand-edit something that gets silently overwritten
|
|
133
|
+
// on the next build. The real fix target is the authored source the
|
|
134
|
+
// bundler reads from, which — being hand-written — gets scanned normally.
|
|
135
|
+
const inScope = graph.files.filter((f) => scopeFiles.has(f.file) && !generatedSet.has(f.file));
|
|
136
|
+
for (const rule of PATTERN_RULES) {
|
|
137
|
+
let occurrences = 0;
|
|
138
|
+
const files = [];
|
|
139
|
+
for (const f of inScope) {
|
|
140
|
+
const n = rule.detect(f, pkg) || 0;
|
|
141
|
+
if (n > 0) { occurrences += n; files.push(f.file); }
|
|
142
|
+
}
|
|
143
|
+
if (occurrences > 0) {
|
|
144
|
+
findings.push({
|
|
145
|
+
tier: "code-pattern", rule: rule.rule,
|
|
146
|
+
detail: rule.detail(occurrences),
|
|
147
|
+
certainty: 1.0, // occurrence is a fact; benefit certainty encoded in behaviorSafe
|
|
148
|
+
behaviorSafe: rule.behaviorSafe,
|
|
149
|
+
occurrences, files, suggestion: rule.suggestion,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---------------- Tier 3: architecture (heavy only) ---------------- */
|
|
157
|
+
|
|
158
|
+
function architectureFindings(graph) {
|
|
159
|
+
const findings = [];
|
|
160
|
+
const totalFiles = graph.files.length;
|
|
161
|
+
|
|
162
|
+
// monolith workflow: one workflow covering >70% of a repo with 12+ files
|
|
163
|
+
for (const wf of graph.workflows) {
|
|
164
|
+
if (totalFiles >= 12 && wf.files.length / totalFiles > 0.7) {
|
|
165
|
+
findings.push({
|
|
166
|
+
tier: "architecture", rule: "monolithic-workflow",
|
|
167
|
+
detail: `Workflow ${wf.id} spans ${wf.files.length}/${totalFiles} files (${Math.round(100 * wf.files.length / totalFiles)}%) — module-boundary or service-split candidate.`,
|
|
168
|
+
certainty: 1.0, occurrences: 1, files: wf.files,
|
|
169
|
+
suggestion: "extract sub-workflows along import-cluster boundaries",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// unclassified reachability: orphans beyond noise. graph.orphans already
|
|
174
|
+
// excludes files classified as generated artifacts or dynamically-loaded
|
|
175
|
+
// (see reachability.js) — what's left needs human classification, it is
|
|
176
|
+
// NOT asserted to be dead code. Leading with "dead code" overclaims
|
|
177
|
+
// certainty the static graph doesn't have; leading with "needs
|
|
178
|
+
// classification" matches what's actually known.
|
|
179
|
+
if (graph.orphans.length >= 3) {
|
|
180
|
+
// Orphan status is only as trustworthy as the call graph it's derived from:
|
|
181
|
+
// a low call-resolution rate (CJS require() indirection, dynamic dispatch,
|
|
182
|
+
// etc.) means real callers can be missed, producing false "orphan" claims.
|
|
183
|
+
// Disclose that on the finding itself rather than only as a top-level stat.
|
|
184
|
+
const resolutionRate = graph.stats.callResolutionRate;
|
|
185
|
+
const lowConfidence = resolutionRate < 0.9;
|
|
186
|
+
const excludedCount = (graph.reachability?.generatedArtifacts.length ?? 0) + (graph.reachability?.dynamicallyLoaded.length ?? 0);
|
|
187
|
+
findings.push({
|
|
188
|
+
tier: "architecture", rule: "orphan-cluster",
|
|
189
|
+
detail: `${graph.orphans.length} file(s) need classification — unreachable from any statically-traced entry ` +
|
|
190
|
+
"point. This is NOT a dead-code claim: verify each one (audit queue, not a deletion queue) before deleting or wiring in." +
|
|
191
|
+
(excludedCount > 0
|
|
192
|
+
? ` (${excludedCount} other file(s) were already excluded from this count as generated artifacts or ` +
|
|
193
|
+
"dynamically-loaded — real evidence for each is in the project's reachability data, not asserted blindly.)"
|
|
194
|
+
: "") +
|
|
195
|
+
(lowConfidence
|
|
196
|
+
? ` CAVEAT: call resolution is only ${(resolutionRate * 100).toFixed(1)}% — some of these may have real ` +
|
|
197
|
+
"callers the static analysis couldn't trace (e.g. dynamic require()/CJS indirection); verify before deleting."
|
|
198
|
+
: ""),
|
|
199
|
+
certainty: lowConfidence ? Number((0.5 + 0.5 * resolutionRate).toFixed(2)) : 1.0,
|
|
200
|
+
occurrences: graph.orphans.length, files: graph.orphans,
|
|
201
|
+
suggestion: "delete, or register the missing entry point",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// low-confidence workflow: derived score under 0.4 flags fragile territory
|
|
205
|
+
for (const wf of graph.workflows) {
|
|
206
|
+
if (wf.confidence && wf.confidence.score < 0.4) {
|
|
207
|
+
findings.push({
|
|
208
|
+
tier: "architecture", rule: "fragile-workflow",
|
|
209
|
+
detail: `Workflow ${wf.id} derived confidence ${wf.confidence.score} — weakest signals: ${
|
|
210
|
+
Object.entries(wf.confidence.signals).filter(([, s]) => s.value !== null && s.value < 0.5).map(([k]) => k).join(", ") || "n/a"}.`,
|
|
211
|
+
certainty: 1.0, occurrences: 1, files: wf.files,
|
|
212
|
+
suggestion: "raise the named signals before modernizing on top of this workflow",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// circular import dependencies: purely graph-derived (Tarjan SCCs over the
|
|
217
|
+
// resolved import graph) — a structural fact, not a heuristic guess.
|
|
218
|
+
for (const cycle of detectCircularDependencies(graph)) {
|
|
219
|
+
findings.push({
|
|
220
|
+
tier: "architecture", rule: "circular-dependency",
|
|
221
|
+
detail: `${cycle.length} file(s) mutually depend on each other in an import cycle: ${cycle.join(" -> ")}.`,
|
|
222
|
+
certainty: 1.0, occurrences: cycle.length, files: cycle,
|
|
223
|
+
suggestion: "extract the shared interface into a separate module both sides can import without importing each other",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// duplicate/near-duplicate functions: grouped by AST shape-hash (parser.js),
|
|
227
|
+
// which normalizes local variable names and literal values but keeps
|
|
228
|
+
// control-flow shape and called-function/property names literal — an exact
|
|
229
|
+
// structural match under that normalization, not a fuzzy guess. Generated
|
|
230
|
+
// files are excluded — repeated shapes inside a single bundler output are
|
|
231
|
+
// an artifact of the build, not hand-duplicated code worth consolidating.
|
|
232
|
+
const generatedSet = new Set(graph.generatedFiles.map((g) => g.file));
|
|
233
|
+
const shapeGroups = new Map();
|
|
234
|
+
for (const f of graph.files) {
|
|
235
|
+
if (generatedSet.has(f.file)) continue;
|
|
236
|
+
for (const fn of f.functions) {
|
|
237
|
+
if (!fn.shapeHash) continue;
|
|
238
|
+
if (!shapeGroups.has(fn.shapeHash)) shapeGroups.set(fn.shapeHash, []);
|
|
239
|
+
shapeGroups.get(fn.shapeHash).push({ file: f.file, name: fn.name });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const members of shapeGroups.values()) {
|
|
243
|
+
if (members.length < 2) continue;
|
|
244
|
+
const files = [...new Set(members.map((m) => m.file))];
|
|
245
|
+
findings.push({
|
|
246
|
+
tier: "architecture", rule: "duplicate-functions",
|
|
247
|
+
detail: `${members.length} structurally identical function(s) (same logic; variable names and literal values ignored): ${
|
|
248
|
+
members.map((m) => `${m.file}#${m.name}`).join(", ")}.`,
|
|
249
|
+
certainty: 1.0, occurrences: members.length, files,
|
|
250
|
+
suggestion: "extract the shared logic into one function every call site imports",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return findings;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ---------------- Impact scoring + orchestration ---------------- */
|
|
257
|
+
|
|
258
|
+
// Test-only files never ship — a finding confined entirely to test scaffolding
|
|
259
|
+
// (e.g. a fixture helper duplicated across 11 *.test.js files) is real
|
|
260
|
+
// maintenance debt, but it is categorically lower-stakes than the same
|
|
261
|
+
// pattern in production code, and a rule like duplicate-functions scans all
|
|
262
|
+
// files without distinguishing them. Without this, raw file-count/occurrence
|
|
263
|
+
// breadth in test files can outweigh a smaller finding in real, connected
|
|
264
|
+
// production code purely because it touches more files — verified against
|
|
265
|
+
// mapd's own self-scan, where an 11-file test-fixture duplicate outranked a
|
|
266
|
+
// 2-file production duplicate with real workflow blast radius. Weighting
|
|
267
|
+
// (not excluding) keeps large-scale test-suite disrepair visible, just far
|
|
268
|
+
// less urgent than anything touching shipped code.
|
|
269
|
+
const TEST_FILE_OPERATIONAL_WEIGHT = 0.15;
|
|
270
|
+
|
|
271
|
+
function operationalWeight(file) { return isTestFile(file) ? TEST_FILE_OPERATIONAL_WEIGHT : 1; }
|
|
272
|
+
|
|
273
|
+
function hasTestFor(allFiles, file) {
|
|
274
|
+
const base = path.posix.basename(file).replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, "");
|
|
275
|
+
return allFiles.some((f) => /(\.test\.|\.spec\.|__tests__\/|tests?\/)/.test(f) && f.includes(base));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scoreFinding(finding, graph) {
|
|
279
|
+
if (finding.informational) return finding;
|
|
280
|
+
const repoFiles = Math.max(1, graph.files.length);
|
|
281
|
+
const weightedFileCount = finding.files.reduce((a, f) => a + operationalWeight(f), 0);
|
|
282
|
+
const avgWeight = finding.files.length ? weightedFileCount / finding.files.length : 1;
|
|
283
|
+
const reach = Math.min(1, weightedFileCount / repoFiles + (finding.occurrences * avgWeight) / (repoFiles * 5));
|
|
284
|
+
const allFiles = graph.files.map((f) => f.file);
|
|
285
|
+
const tested = finding.files.filter((f) => f === "package.json" || hasTestFor(allFiles, f)).length;
|
|
286
|
+
const safety = finding.files.length ? tested / finding.files.length : 1;
|
|
287
|
+
const impact = reach * finding.certainty;
|
|
288
|
+
finding.operationalImpact = {
|
|
289
|
+
reach: Number(reach.toFixed(3)),
|
|
290
|
+
certainty: finding.certainty,
|
|
291
|
+
safety: Number(safety.toFixed(3)),
|
|
292
|
+
impact: Number(impact.toFixed(3)),
|
|
293
|
+
priority: Number((impact * (0.5 + 0.5 * safety)).toFixed(3)),
|
|
294
|
+
method: "impact-composite-v2 (reach×certainty on operationally-weighted files/occurrences — test-only files count for less, they don't ship — priority damped by test safety)",
|
|
295
|
+
};
|
|
296
|
+
return finding;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function timed(profile, name, fn) {
|
|
300
|
+
if (!profile) return fn();
|
|
301
|
+
const startedAt = performance.now();
|
|
302
|
+
try {
|
|
303
|
+
return fn();
|
|
304
|
+
} finally {
|
|
305
|
+
profile.steps.push({ name, durationMs: Number((performance.now() - startedAt).toFixed(2)) });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function runModernizationScan(rootDir, graph, pkg, mode = "medium", {
|
|
310
|
+
profile: withProfile = false,
|
|
311
|
+
checkRegistry = true,
|
|
312
|
+
registryTimeoutMs = 2000,
|
|
313
|
+
} = {}) {
|
|
314
|
+
const findings = [];
|
|
315
|
+
const startedAt = performance.now();
|
|
316
|
+
const profile = withProfile ? { steps: [] } : null;
|
|
317
|
+
|
|
318
|
+
findings.push(...timed(profile, "dependency-health", () => depFindings(rootDir, pkg, { checkRegistry, registryTimeoutMs }))); // all modes
|
|
319
|
+
|
|
320
|
+
if (mode === "medium" || mode === "heavy") {
|
|
321
|
+
findings.push(...timed(profile, "code-patterns", () => {
|
|
322
|
+
const wfs = [...graph.workflows].sort((a, b) => b.files.length - a.files.length);
|
|
323
|
+
const scoped = mode === "heavy" ? wfs : wfs.slice(0, 3);
|
|
324
|
+
const scopeFiles = new Set(scoped.flatMap((w) => w.files));
|
|
325
|
+
if (mode === "heavy") for (const o of graph.orphans) scopeFiles.add(o);
|
|
326
|
+
const generatedSet = new Set(graph.generatedFiles.map((g) => g.file));
|
|
327
|
+
return patternFindings(graph, pkg, scopeFiles, generatedSet);
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
if (mode === "heavy") findings.push(...timed(profile, "architecture", () => architectureFindings(graph)));
|
|
331
|
+
|
|
332
|
+
timed(profile, "score-and-sort", () => {
|
|
333
|
+
for (const f of findings) scoreFinding(f, graph);
|
|
334
|
+
findings.sort((a, b) => (b.operationalImpact?.priority ?? -1) - (a.operationalImpact?.priority ?? -1));
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const report = {
|
|
338
|
+
generatedAt: new Date().toISOString(), mode,
|
|
339
|
+
findings: findings.map((f) => ({ ...f, status: f.informational ? "info" : "awaiting-approval" })),
|
|
340
|
+
};
|
|
341
|
+
if (profile) {
|
|
342
|
+
profile.totalMs = Number((performance.now() - startedAt).toFixed(2));
|
|
343
|
+
report.profile = profile;
|
|
344
|
+
}
|
|
345
|
+
return report;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Persist a modernization report, merging its findings against the previous
|
|
350
|
+
* report for this mode through the same lifecycle rules as `mapd check`
|
|
351
|
+
* (regression.js mergeFindingLifecycle): unreproduced open findings become
|
|
352
|
+
* "resolved" with evidence, human dismissals are preserved, terminal entries
|
|
353
|
+
* stay as audit trail. Mutates report.findings to the merged set so every
|
|
354
|
+
* consumer (cli/chat/mcp) sees the same lifecycle-aware report.
|
|
355
|
+
* Returns { path, resolvedNow }.
|
|
356
|
+
*/
|
|
357
|
+
export function saveModernizationReport(rootDir, report) {
|
|
358
|
+
const p = path.join(rootDir, ".mapd", `modernize-${report.mode}.json`);
|
|
359
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
360
|
+
|
|
361
|
+
let prev = null;
|
|
362
|
+
try { prev = JSON.parse(fs.readFileSync(p, "utf8")); } catch { /* first run for this mode */ }
|
|
363
|
+
|
|
364
|
+
const { findings, resolvedNow } = mergeFindingLifecycle(
|
|
365
|
+
report.findings, prev?.findings, (f) => `${f.rule}:${f.detail}`, `mapd modernize ${report.mode}`,
|
|
366
|
+
);
|
|
367
|
+
report.findings = findings;
|
|
368
|
+
fs.writeFileSync(p, JSON.stringify(report, null, 2));
|
|
369
|
+
return { path: p, resolvedNow };
|
|
370
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parseCache.js - persistent, content-hash keyed parser cache.
|
|
3
|
+
*
|
|
4
|
+
* parseProject already knows how to reuse a Map of rel -> {hash,node}; this
|
|
5
|
+
* module only persists that Map between CLI/chat/MCP processes. The cache is
|
|
6
|
+
* intentionally a performance artifact: unreadable, stale, or schema-mismatched
|
|
7
|
+
* cache files are ignored and rebuilt from source.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
|
|
14
|
+
const CACHE_SCHEMA = 1;
|
|
15
|
+
const PARSER_SEMANTICS_VERSION = 2;
|
|
16
|
+
|
|
17
|
+
function cacheDir(rootDir) {
|
|
18
|
+
return path.join(path.resolve(rootDir), ".mapd", "cache");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCachePath(rootDir) {
|
|
22
|
+
return path.join(cacheDir(rootDir), "parse-v1.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parserConfigSignature(config = {}) {
|
|
26
|
+
const payload = {
|
|
27
|
+
parser: PARSER_SEMANTICS_VERSION,
|
|
28
|
+
include: config.project?.include ?? [],
|
|
29
|
+
exclude: config.project?.exclude ?? [],
|
|
30
|
+
maxFileSizeBytes: config.mapping?.maxFileSizeBytes ?? null,
|
|
31
|
+
};
|
|
32
|
+
return crypto.createHash("sha1").update(JSON.stringify(payload)).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadPersistentParseCache(rootDir, config = {}) {
|
|
36
|
+
const p = parseCachePath(rootDir);
|
|
37
|
+
let raw;
|
|
38
|
+
try { raw = JSON.parse(fs.readFileSync(p, "utf8")); } catch { return new Map(); }
|
|
39
|
+
if (raw?.mapdSchema !== CACHE_SCHEMA) return new Map();
|
|
40
|
+
if (raw?.configSignature !== parserConfigSignature(config)) return new Map();
|
|
41
|
+
const entries = raw.entries && typeof raw.entries === "object" ? raw.entries : {};
|
|
42
|
+
return new Map(Object.entries(entries));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function savePersistentParseCache(rootDir, config = {}, cache = new Map()) {
|
|
46
|
+
const dir = cacheDir(rootDir);
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
const p = parseCachePath(rootDir);
|
|
49
|
+
const tmp = path.join(dir, `parse-v1.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
|
|
50
|
+
const body = {
|
|
51
|
+
mapdSchema: CACHE_SCHEMA,
|
|
52
|
+
configSignature: parserConfigSignature(config),
|
|
53
|
+
savedAt: new Date().toISOString(),
|
|
54
|
+
entries: Object.fromEntries(cache),
|
|
55
|
+
};
|
|
56
|
+
fs.writeFileSync(tmp, JSON.stringify(body));
|
|
57
|
+
try {
|
|
58
|
+
fs.renameSync(tmp, p);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
try { fs.unlinkSync(tmp); } catch { /* best-effort cleanup */ }
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
return p;
|
|
64
|
+
}
|