mapra 0.1.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/README.md +144 -0
- package/dist/analyzer/blast-radius.d.ts +27 -0
- package/dist/analyzer/blast-radius.d.ts.map +1 -0
- package/dist/analyzer/blast-radius.js +58 -0
- package/dist/analyzer/blast-radius.js.map +1 -0
- package/dist/analyzer/churn.d.ts +26 -0
- package/dist/analyzer/churn.d.ts.map +1 -0
- package/dist/analyzer/churn.js +96 -0
- package/dist/analyzer/churn.js.map +1 -0
- package/dist/analyzer/co-change.d.ts +60 -0
- package/dist/analyzer/co-change.d.ts.map +1 -0
- package/dist/analyzer/co-change.js +153 -0
- package/dist/analyzer/co-change.js.map +1 -0
- package/dist/analyzer/conventions.d.ts +22 -0
- package/dist/analyzer/conventions.d.ts.map +1 -0
- package/dist/analyzer/conventions.js +81 -0
- package/dist/analyzer/conventions.js.map +1 -0
- package/dist/analyzer/git-hash.d.ts +11 -0
- package/dist/analyzer/git-hash.d.ts.map +1 -0
- package/dist/analyzer/git-hash.js +25 -0
- package/dist/analyzer/git-hash.js.map +1 -0
- package/dist/analyzer/graph-utils.d.ts +46 -0
- package/dist/analyzer/graph-utils.d.ts.map +1 -0
- package/dist/analyzer/graph-utils.js +145 -0
- package/dist/analyzer/graph-utils.js.map +1 -0
- package/dist/analyzer/index.d.ts +34 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +63 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/cli/hooks.d.ts +24 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +126 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +818 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/plan-parser.d.ts +20 -0
- package/dist/cli/plan-parser.d.ts.map +1 -0
- package/dist/cli/plan-parser.js +104 -0
- package/dist/cli/plan-parser.js.map +1 -0
- package/dist/cli/shim.d.ts +3 -0
- package/dist/cli/shim.d.ts.map +1 -0
- package/dist/cli/shim.js +33 -0
- package/dist/cli/shim.js.map +1 -0
- package/dist/cli/templates.d.ts +28 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +91 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/encoder/encode.d.ts +17 -0
- package/dist/encoder/encode.d.ts.map +1 -0
- package/dist/encoder/encode.js +270 -0
- package/dist/encoder/encode.js.map +1 -0
- package/dist/encoder/layer-infrastructure.d.ts +17 -0
- package/dist/encoder/layer-infrastructure.d.ts.map +1 -0
- package/dist/encoder/layer-infrastructure.js +232 -0
- package/dist/encoder/layer-infrastructure.js.map +1 -0
- package/dist/encoder/layer-labels.d.ts +18 -0
- package/dist/encoder/layer-labels.d.ts.map +1 -0
- package/dist/encoder/layer-labels.js +172 -0
- package/dist/encoder/layer-labels.js.map +1 -0
- package/dist/encoder/layer-terrain.d.ts +18 -0
- package/dist/encoder/layer-terrain.d.ts.map +1 -0
- package/dist/encoder/layer-terrain.js +135 -0
- package/dist/encoder/layer-terrain.js.map +1 -0
- package/dist/encoder/layout.d.ts +53 -0
- package/dist/encoder/layout.d.ts.map +1 -0
- package/dist/encoder/layout.js +178 -0
- package/dist/encoder/layout.js.map +1 -0
- package/dist/encoder/parse-strand-header.d.ts +29 -0
- package/dist/encoder/parse-strand-header.d.ts.map +1 -0
- package/dist/encoder/parse-strand-header.js +59 -0
- package/dist/encoder/parse-strand-header.js.map +1 -0
- package/dist/encoder/spatial-text-encode.d.ts +22 -0
- package/dist/encoder/spatial-text-encode.d.ts.map +1 -0
- package/dist/encoder/spatial-text-encode.js +199 -0
- package/dist/encoder/spatial-text-encode.js.map +1 -0
- package/dist/encoder/strand-format-encode-v1.d.ts +16 -0
- package/dist/encoder/strand-format-encode-v1.d.ts.map +1 -0
- package/dist/encoder/strand-format-encode-v1.js +296 -0
- package/dist/encoder/strand-format-encode-v1.js.map +1 -0
- package/dist/encoder/strand-format-encode.d.ts +21 -0
- package/dist/encoder/strand-format-encode.d.ts.map +1 -0
- package/dist/encoder/strand-format-encode.js +562 -0
- package/dist/encoder/strand-format-encode.js.map +1 -0
- package/dist/encoder/text-encode.d.ts +13 -0
- package/dist/encoder/text-encode.d.ts.map +1 -0
- package/dist/encoder/text-encode.js +123 -0
- package/dist/encoder/text-encode.js.map +1 -0
- package/dist/query/blast-radius.d.ts +14 -0
- package/dist/query/blast-radius.d.ts.map +1 -0
- package/dist/query/blast-radius.js +81 -0
- package/dist/query/blast-radius.js.map +1 -0
- package/dist/query/cache.d.ts +29 -0
- package/dist/query/cache.d.ts.map +1 -0
- package/dist/query/cache.js +138 -0
- package/dist/query/cache.js.map +1 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +46 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/resolve.d.ts +7 -0
- package/dist/query/resolve.d.ts.map +1 -0
- package/dist/query/resolve.js +24 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/risk-profile.d.ts +30 -0
- package/dist/query/risk-profile.d.ts.map +1 -0
- package/dist/query/risk-profile.js +94 -0
- package/dist/query/risk-profile.js.map +1 -0
- package/dist/query/test-map.d.ts +13 -0
- package/dist/query/test-map.d.ts.map +1 -0
- package/dist/query/test-map.js +43 -0
- package/dist/query/test-map.js.map +1 -0
- package/dist/scanner/index.d.ts +51 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +480 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/workspace.d.ts +11 -0
- package/dist/scanner/workspace.d.ts.map +1 -0
- package/dist/scanner/workspace.js +243 -0
- package/dist/scanner/workspace.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mapra CLI
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* mapra setup [path] Generate .mapra and wire CLAUDE.md (first-time setup)
|
|
7
|
+
* mapra generate [path] Scan codebase and write .mapra file
|
|
8
|
+
* mapra update [path] Regenerate .mapra in place (alias for generate in cwd)
|
|
9
|
+
* mapra init [path] Wire .mapra into project's CLAUDE.md
|
|
10
|
+
* mapra status [path] Show current mapra setup state
|
|
11
|
+
* mapra check [path] Check if .mapra is current or stale (git hash comparison)
|
|
12
|
+
* mapra validate-plan <plan.md> [--since YYYY-MM-DD] [--checkpoints] Cross-reference plan against .mapra
|
|
13
|
+
* mapra batch <config.json> [--resume] Run batch experiment from config
|
|
14
|
+
* mapra analyze <results.json> [--advise] [--judge-check] Analyze experiment results
|
|
15
|
+
* mapra query <type> <file> [--json] Query structural data (blast_radius, risk_profile, test_map)
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { applyMapraSection, SUPERSESSION_MESSAGE } from "./templates.js";
|
|
20
|
+
import { installAllHooks, uninstallAllHooks, getHooksDir, MAPRA_HOOK_START } from "./hooks.js";
|
|
21
|
+
import { generateHookShim } from "./shim.js";
|
|
22
|
+
const [, , command, ...args] = process.argv;
|
|
23
|
+
if (command === "--help" || command === "-h") {
|
|
24
|
+
printHelp();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
if (!command) {
|
|
28
|
+
console.log("No command given — running setup (generate + init) in current directory.");
|
|
29
|
+
console.log("Use 'mapra --help' to see all commands.\n");
|
|
30
|
+
await runSetup(undefined);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
switch (command) {
|
|
34
|
+
case "setup":
|
|
35
|
+
await runSetup(args[0]);
|
|
36
|
+
break;
|
|
37
|
+
case "generate": {
|
|
38
|
+
const silent = args.includes("--silent");
|
|
39
|
+
const targetArg = args.find((a) => !a.startsWith("--"));
|
|
40
|
+
await runGenerate(targetArg, false, silent);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "update":
|
|
44
|
+
try {
|
|
45
|
+
const silent = args.includes("--silent");
|
|
46
|
+
const targetArg = args.find((a) => !a.startsWith("--"));
|
|
47
|
+
await runGenerate(targetArg ?? process.cwd(), true, silent);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(`mapra update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
console.error("Continuing with stale .mapra. Complete your refactor and retry.");
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "init":
|
|
55
|
+
await runInit(args[0]);
|
|
56
|
+
break;
|
|
57
|
+
case "status":
|
|
58
|
+
await runStatus(args[0]);
|
|
59
|
+
break;
|
|
60
|
+
case "check": {
|
|
61
|
+
const failIfStale = args.includes("--fail-if-stale");
|
|
62
|
+
const targetArg = args.find((a) => !a.startsWith("--"));
|
|
63
|
+
await runCheck(targetArg, failIfStale);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "validate-plan": {
|
|
67
|
+
const sinceIdx = args.indexOf("--since");
|
|
68
|
+
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
|
|
69
|
+
const checkpoints = args.includes("--checkpoints");
|
|
70
|
+
const planFile = args.find((a) => !a.startsWith("--") && a !== since);
|
|
71
|
+
await runValidatePlan(planFile, since, checkpoints);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "batch": {
|
|
75
|
+
const configFile = args.find((a) => !a.startsWith("--"));
|
|
76
|
+
const resume = args.includes("--resume");
|
|
77
|
+
const smart = args.includes("--smart");
|
|
78
|
+
await runBatchCommand(configFile, resume, smart);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "analyze": {
|
|
82
|
+
const files = args.filter((a) => !a.startsWith("--"));
|
|
83
|
+
const advise = args.includes("--advise");
|
|
84
|
+
const apply = args.includes("--apply");
|
|
85
|
+
const judgeCheck = args.includes("--judge-check");
|
|
86
|
+
await runAnalyzeCommand(files, { advise, apply, judgeCheck });
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "install-hooks": {
|
|
90
|
+
const targetPath = resolveTarget(args[0]);
|
|
91
|
+
const { installed, skipped } = installAllHooks(targetPath);
|
|
92
|
+
if (skipped) {
|
|
93
|
+
console.warn(`\u26A0 ${skipped} \u2014 skipping hook installation`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const h of installed)
|
|
97
|
+
console.log(`\u2713 Installed ${h} hook`);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "uninstall-hooks": {
|
|
102
|
+
const targetPath = resolveTarget(args[0]);
|
|
103
|
+
uninstallAllHooks(targetPath);
|
|
104
|
+
console.log("Removed mapra git hooks");
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "query": {
|
|
108
|
+
try {
|
|
109
|
+
const { runQueryCommand } = await import("../query/index.js");
|
|
110
|
+
await runQueryCommand(args);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
handleError("query", err);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
console.error(`Unknown command: ${command}`);
|
|
119
|
+
printHelp();
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
function printHelp() {
|
|
123
|
+
console.log(`
|
|
124
|
+
mapra — stop exploring. start building.
|
|
125
|
+
|
|
126
|
+
Quick start:
|
|
127
|
+
mapra Run setup in current directory (first-time setup)
|
|
128
|
+
mapra update Regenerate .mapra after codebase changes
|
|
129
|
+
|
|
130
|
+
Commands:
|
|
131
|
+
setup [path] Generate .mapra, wire CLAUDE.md, install auto-update hooks
|
|
132
|
+
generate [path] Scan codebase and write .mapra to project root
|
|
133
|
+
update [path] Regenerate .mapra in place (alias for generate in cwd)
|
|
134
|
+
init [path] Wire @.mapra reference into project's CLAUDE.md
|
|
135
|
+
status [path] Show whether .mapra is present, wired, and fresh
|
|
136
|
+
check [path] Check if .mapra is current or stale (compares git hash)
|
|
137
|
+
--fail-if-stale: exit code 1 if stale (for CI)
|
|
138
|
+
install-hooks [path] Install git hooks for auto-update
|
|
139
|
+
uninstall-hooks [path] Remove mapra git hooks
|
|
140
|
+
validate-plan <plan.md> [--since YYYY-MM-DD] [--checkpoints]
|
|
141
|
+
Cross-reference plan file paths against .mapra data
|
|
142
|
+
batch <config.json> [--resume] [--smart]
|
|
143
|
+
Run batch experiment comparing encoding conditions
|
|
144
|
+
--smart: score inline and stop early when verdicts are unanimous
|
|
145
|
+
analyze <results.json> [--advise] [--judge-check]
|
|
146
|
+
Analyze experiment results: stats, diagnostics, recommendations
|
|
147
|
+
analyze <old.json> <new.json>
|
|
148
|
+
Compare two experiment iterations
|
|
149
|
+
query <type> <file> [--json]
|
|
150
|
+
Query structural data for a specific file
|
|
151
|
+
Types: blast_radius, risk_profile, test_map
|
|
152
|
+
|
|
153
|
+
Flags:
|
|
154
|
+
--silent Suppress output (used by git hooks)
|
|
155
|
+
|
|
156
|
+
Default path: current working directory
|
|
157
|
+
|
|
158
|
+
Auto-update:
|
|
159
|
+
After setup, .mapra regenerates automatically on commit, merge, and
|
|
160
|
+
branch switch via git hooks. Teammates get hooks via npm install
|
|
161
|
+
(prepare script). Run 'mapra status' to check hook state.
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
mapra setup # first-time setup in cwd
|
|
165
|
+
mapra setup /path/to/project # first-time setup for a specific project
|
|
166
|
+
mapra update # refresh after code changes
|
|
167
|
+
mapra status # check current state
|
|
168
|
+
mapra batch experiments/configs/strand-v3-effectiveness.json
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
async function runSetup(targetArg) {
|
|
172
|
+
console.log("Setting up mapra...\n");
|
|
173
|
+
const targetPath = resolveTarget(targetArg ?? process.cwd());
|
|
174
|
+
// Step 1: Generate .mapra
|
|
175
|
+
await runGenerate(targetPath);
|
|
176
|
+
console.log();
|
|
177
|
+
// Step 2: Wire CLAUDE.md
|
|
178
|
+
await runInit(targetPath);
|
|
179
|
+
// Step 3: Write .mapra/hook.mjs
|
|
180
|
+
const mapraDir = path.join(targetPath, ".mapra");
|
|
181
|
+
fs.mkdirSync(mapraDir, { recursive: true });
|
|
182
|
+
const version = getVersion();
|
|
183
|
+
const shimContent = generateHookShim(version);
|
|
184
|
+
fs.writeFileSync(path.join(mapraDir, "hook.mjs"), shimContent.replace(/\r\n/g, "\n"));
|
|
185
|
+
console.log("Wrote .mapra/hook.mjs (auto-update shim)");
|
|
186
|
+
// Step 4: Write .mapra/.gitignore (ignore lockfile)
|
|
187
|
+
fs.writeFileSync(path.join(mapraDir, ".gitignore"), ".lock\n");
|
|
188
|
+
// Step 5: Install git hooks
|
|
189
|
+
const { installed, skipped } = installAllHooks(targetPath);
|
|
190
|
+
if (skipped) {
|
|
191
|
+
console.warn(`\u26A0 ${skipped} \u2014 skipping hook installation`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(`Installed git hooks (${installed.join(", ")})`);
|
|
195
|
+
}
|
|
196
|
+
// Step 6: Add .gitattributes entry for linguist-generated
|
|
197
|
+
const gitattrsPath = path.join(targetPath, ".gitattributes");
|
|
198
|
+
const gitattrsEntry = ".mapra/hook.mjs linguist-generated=true";
|
|
199
|
+
if (fs.existsSync(gitattrsPath)) {
|
|
200
|
+
const existing = fs.readFileSync(gitattrsPath, "utf-8");
|
|
201
|
+
if (!existing.includes(gitattrsEntry)) {
|
|
202
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
203
|
+
fs.writeFileSync(gitattrsPath, existing + separator + gitattrsEntry + "\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
fs.writeFileSync(gitattrsPath, gitattrsEntry + "\n");
|
|
208
|
+
}
|
|
209
|
+
// Step 7: Add prepare script and devDependency to package.json
|
|
210
|
+
const pkgJsonPath = path.join(targetPath, "package.json");
|
|
211
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
212
|
+
try {
|
|
213
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
214
|
+
let modified = false;
|
|
215
|
+
// Add prepare script
|
|
216
|
+
if (!pkgJson.scripts)
|
|
217
|
+
pkgJson.scripts = {};
|
|
218
|
+
if (!pkgJson.scripts.prepare || !pkgJson.scripts.prepare.includes("mapra")) {
|
|
219
|
+
if (pkgJson.scripts.prepare) {
|
|
220
|
+
pkgJson.scripts.prepare += " && mapra install-hooks";
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
pkgJson.scripts.prepare = "mapra install-hooks";
|
|
224
|
+
}
|
|
225
|
+
modified = true;
|
|
226
|
+
}
|
|
227
|
+
// Add devDependency
|
|
228
|
+
if (!pkgJson.devDependencies)
|
|
229
|
+
pkgJson.devDependencies = {};
|
|
230
|
+
if (!pkgJson.devDependencies.mapra) {
|
|
231
|
+
pkgJson.devDependencies.mapra = `^${version}`;
|
|
232
|
+
modified = true;
|
|
233
|
+
}
|
|
234
|
+
if (modified) {
|
|
235
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
236
|
+
console.log("Updated package.json (added mapra devDependency + prepare script)");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
console.warn("\u26A0 Could not update package.json \u2014 add mapra manually");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.log("\nDone. Your codebase map will stay fresh automatically.");
|
|
244
|
+
}
|
|
245
|
+
function getVersion() {
|
|
246
|
+
try {
|
|
247
|
+
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
248
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
249
|
+
return pkg.version ?? "0.0.0";
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return "0.0.0";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function runGenerate(targetArg, softFail = false, silent = false) {
|
|
256
|
+
const targetPath = resolveTarget(targetArg);
|
|
257
|
+
try {
|
|
258
|
+
const { scanCodebase } = await import("../scanner/index.js");
|
|
259
|
+
const { analyzeGraph } = await import("../analyzer/index.js");
|
|
260
|
+
const { encodeToStrandFormat } = await import("../encoder/strand-format-encode.js");
|
|
261
|
+
const { getGitHash } = await import("../analyzer/git-hash.js");
|
|
262
|
+
const outputPath = path.join(targetPath, ".mapra");
|
|
263
|
+
if (!silent)
|
|
264
|
+
console.log(`Scanning ${targetPath}`);
|
|
265
|
+
const graph = await Promise.resolve(scanCodebase(targetPath));
|
|
266
|
+
const riskCount = graph.nodes.filter((n) => n.type !== "test" &&
|
|
267
|
+
n.type !== "config" &&
|
|
268
|
+
graph.edges.filter((e) => e.to === n.id).length > 3).length;
|
|
269
|
+
if (!silent) {
|
|
270
|
+
console.log(` ${graph.totalFiles} files ${graph.totalLines.toLocaleString()} lines ${graph.modules.length} modules ${riskCount} high-import files`);
|
|
271
|
+
}
|
|
272
|
+
const analysis = analyzeGraph(graph, targetPath);
|
|
273
|
+
const gitHash = getGitHash(targetPath);
|
|
274
|
+
const encoded = encodeToStrandFormat(graph, analysis, { gitHash });
|
|
275
|
+
const tokens = Math.round(encoded.length / 4);
|
|
276
|
+
const tmpPath = outputPath + ".tmp";
|
|
277
|
+
fs.writeFileSync(tmpPath, encoded, "utf-8");
|
|
278
|
+
try {
|
|
279
|
+
fs.renameSync(tmpPath, outputPath);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Windows: rename can fail if another process holds a read handle.
|
|
283
|
+
// Fall back to direct write.
|
|
284
|
+
fs.writeFileSync(outputPath, encoded, "utf-8");
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
try {
|
|
288
|
+
fs.unlinkSync(tmpPath);
|
|
289
|
+
}
|
|
290
|
+
catch { /* .tmp already renamed or gone */ }
|
|
291
|
+
}
|
|
292
|
+
// Write query cache alongside .mapra — failure is non-fatal
|
|
293
|
+
try {
|
|
294
|
+
const { writeCache, ensureCacheInGitignore } = await import("../query/cache.js");
|
|
295
|
+
const { execSync } = await import("child_process");
|
|
296
|
+
let fullHash;
|
|
297
|
+
try {
|
|
298
|
+
fullHash = execSync("git rev-parse HEAD", {
|
|
299
|
+
cwd: targetPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
|
|
300
|
+
}).trim() || undefined;
|
|
301
|
+
}
|
|
302
|
+
catch { /* not a git repo */ }
|
|
303
|
+
writeCache(targetPath, graph, analysis, fullHash);
|
|
304
|
+
ensureCacheInGitignore(targetPath);
|
|
305
|
+
}
|
|
306
|
+
catch (cacheErr) {
|
|
307
|
+
if (!silent) {
|
|
308
|
+
console.warn(`\u26A0 Failed to write .mapra-cache.json: ${cacheErr instanceof Error ? cacheErr.message : String(cacheErr)}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!silent) {
|
|
312
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "");
|
|
313
|
+
console.log(`\nWrote .mapra (${encoded.length.toLocaleString()} chars ~${tokens} tokens)`);
|
|
314
|
+
console.log(SUPERSESSION_MESSAGE(timestamp));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (softFail)
|
|
319
|
+
throw err;
|
|
320
|
+
handleError("generate", err);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function runInit(targetArg) {
|
|
324
|
+
const targetPath = resolveTarget(targetArg);
|
|
325
|
+
try {
|
|
326
|
+
const strandPath = path.join(targetPath, ".mapra");
|
|
327
|
+
const claudePath = path.join(targetPath, "CLAUDE.md");
|
|
328
|
+
// Guard: .mapra must exist and be non-empty
|
|
329
|
+
if (!fs.existsSync(strandPath)) {
|
|
330
|
+
console.error(`Error: .mapra not found at ${strandPath}`);
|
|
331
|
+
console.error(`Run 'mapra generate' or 'mapra setup' first.`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const strandSize = fs.statSync(strandPath).size;
|
|
335
|
+
if (strandSize < 100) {
|
|
336
|
+
console.error(`Warning: .mapra appears malformed (${strandSize} bytes). Re-run 'mapra generate'.`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const existingContent = fs.existsSync(claudePath)
|
|
340
|
+
? fs.readFileSync(claudePath, "utf-8")
|
|
341
|
+
: null;
|
|
342
|
+
const { content, action } = applyMapraSection(existingContent);
|
|
343
|
+
if (action === "up-to-date") {
|
|
344
|
+
console.log(`Already up to date — CLAUDE.md has current mapra section`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
fs.writeFileSync(claudePath, content, "utf-8");
|
|
348
|
+
const messages = {
|
|
349
|
+
created: `Created CLAUDE.md and wired @.mapra`,
|
|
350
|
+
upgraded: `Upgraded mapra section in CLAUDE.md`,
|
|
351
|
+
"legacy-upgraded": `Upgraded CLAUDE.md — added section markers for future updates`,
|
|
352
|
+
appended: `Wired — added @.mapra reference to ${claudePath}`,
|
|
353
|
+
};
|
|
354
|
+
console.log(messages[action]);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
handleError("init", err);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function runStatus(targetArg) {
|
|
361
|
+
const targetPath = resolveTarget(targetArg);
|
|
362
|
+
const strandPath = path.join(targetPath, ".mapra");
|
|
363
|
+
const claudePath = path.join(targetPath, "CLAUDE.md");
|
|
364
|
+
const gitignorePath = path.join(targetPath, ".gitignore");
|
|
365
|
+
console.log(`Status for: ${targetPath}\n`);
|
|
366
|
+
// .mapra presence and staleness
|
|
367
|
+
if (!fs.existsSync(strandPath)) {
|
|
368
|
+
console.log(` .mapra ✗ not found (run 'mapra setup')`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const { parseStrandHeader } = await import("../encoder/parse-strand-header.js");
|
|
372
|
+
const { getGitHash } = await import("../analyzer/git-hash.js");
|
|
373
|
+
const strandContent = fs.readFileSync(strandPath, "utf-8");
|
|
374
|
+
const header = parseStrandHeader(strandContent);
|
|
375
|
+
const strandMtime = fs.statSync(strandPath).mtimeMs;
|
|
376
|
+
const sourceMtime = newestSourceFileMtime(targetPath);
|
|
377
|
+
const ageMs = Date.now() - strandMtime;
|
|
378
|
+
const ageDays = Math.floor(ageMs / 86_400_000);
|
|
379
|
+
const ageStr = ageDays === 0 ? "today" : `${ageDays} day${ageDays !== 1 ? "s" : ""} ago`;
|
|
380
|
+
// Use git hash comparison if available, fall back to mtime
|
|
381
|
+
const currentHash = getGitHash(targetPath);
|
|
382
|
+
let staleStr = "";
|
|
383
|
+
if (header?.gitHash && currentHash) {
|
|
384
|
+
if (header.gitHash !== currentHash) {
|
|
385
|
+
staleStr = ` \u26A0 stale (generated at git:${header.gitHash}, HEAD is ${currentHash})`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const stale = sourceMtime > strandMtime;
|
|
390
|
+
staleStr = stale ? " \u26A0 may be stale (run 'mapra update')" : "";
|
|
391
|
+
}
|
|
392
|
+
console.log(` .mapra \u2713 present (updated ${ageStr})${staleStr}`);
|
|
393
|
+
}
|
|
394
|
+
// CLAUDE.md wiring
|
|
395
|
+
if (!fs.existsSync(claudePath)) {
|
|
396
|
+
console.log(` CLAUDE.md ✗ not found (run 'mapra init')`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const content = fs.readFileSync(claudePath, "utf-8");
|
|
400
|
+
const wired = /^@\.mapra$/m.test(content);
|
|
401
|
+
console.log(` CLAUDE.md ${wired ? "✓ wired" : "✗ not wired (run 'mapra init')"}`);
|
|
402
|
+
}
|
|
403
|
+
// .gitignore check
|
|
404
|
+
if (fs.existsSync(gitignorePath)) {
|
|
405
|
+
const gitignore = fs.readFileSync(gitignorePath, "utf-8");
|
|
406
|
+
if (/^\.?mapra$/m.test(gitignore) || /^\*\.mapra$/m.test(gitignore)) {
|
|
407
|
+
console.log(` .gitignore ⚠ .mapra appears to be ignored — collaborators won't have the map`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Hook installation check
|
|
411
|
+
const hooksDir = getHooksDir(targetPath);
|
|
412
|
+
if (hooksDir) {
|
|
413
|
+
const postCommit = path.join(hooksDir, "post-commit");
|
|
414
|
+
if (fs.existsSync(postCommit)) {
|
|
415
|
+
const content = fs.readFileSync(postCommit, "utf-8");
|
|
416
|
+
const hasStrnd = content.includes(MAPRA_HOOK_START);
|
|
417
|
+
console.log(` git hooks ${hasStrnd ? "\u2713 installed" : "\u2717 not installed (run 'mapra setup')"}`);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
console.log(` git hooks \u2717 not installed (run 'mapra setup')`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
console.log(` git hooks \u2717 no .git directory`);
|
|
425
|
+
}
|
|
426
|
+
// .mapra/hook.mjs check
|
|
427
|
+
const shimPath = path.join(targetPath, ".mapra", "hook.mjs");
|
|
428
|
+
if (fs.existsSync(shimPath)) {
|
|
429
|
+
console.log(` hook shim \u2713 present`);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
console.log(` hook shim \u2717 not found (run 'mapra setup')`);
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
}
|
|
436
|
+
async function runCheck(targetArg, failIfStale = false) {
|
|
437
|
+
const targetPath = resolveTarget(targetArg);
|
|
438
|
+
const strandPath = path.join(targetPath, ".mapra");
|
|
439
|
+
if (!fs.existsSync(strandPath)) {
|
|
440
|
+
console.error(".mapra not found. Run 'mapra setup' or 'mapra generate' first.");
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const { parseStrandHeader } = await import("../encoder/parse-strand-header.js");
|
|
444
|
+
const { getGitHash } = await import("../analyzer/git-hash.js");
|
|
445
|
+
const { execSync } = await import("child_process");
|
|
446
|
+
const strandContent = fs.readFileSync(strandPath, "utf-8");
|
|
447
|
+
const header = parseStrandHeader(strandContent);
|
|
448
|
+
if (!header) {
|
|
449
|
+
console.error(".mapra header is malformed. Run 'mapra generate' to regenerate.");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const currentHash = getGitHash(targetPath);
|
|
453
|
+
// Case 1: No git available
|
|
454
|
+
if (!currentHash) {
|
|
455
|
+
console.log(`.mapra generated ${header.timestamp} (not a git repo — cannot compare)`);
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
458
|
+
// Case 2: Legacy .mapra without git hash
|
|
459
|
+
if (!header.gitHash) {
|
|
460
|
+
console.log(`.mapra generated ${header.timestamp} (no git hash in header — run 'mapra update' to add)`);
|
|
461
|
+
if (failIfStale) {
|
|
462
|
+
console.log("Cannot determine staleness without git hash in .mapra header.");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
// Case 3: Current — hashes match
|
|
468
|
+
if (header.gitHash === currentHash) {
|
|
469
|
+
console.log(`.mapra is current (generated at commit ${header.gitHash}, HEAD is ${currentHash})`);
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
// Case 4: Stale — hashes differ
|
|
473
|
+
let commitsAhead = "unknown";
|
|
474
|
+
let changedFiles = "unknown";
|
|
475
|
+
try {
|
|
476
|
+
commitsAhead = execSync(`git rev-list --count ${header.gitHash}..HEAD`, {
|
|
477
|
+
cwd: targetPath,
|
|
478
|
+
encoding: "utf-8",
|
|
479
|
+
timeout: 5000,
|
|
480
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
481
|
+
}).trim();
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// git rev-list may fail if the generation commit is not in history (e.g., rebase)
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const diffOutput = execSync(`git diff --name-only ${header.gitHash}..HEAD`, {
|
|
488
|
+
cwd: targetPath,
|
|
489
|
+
encoding: "utf-8",
|
|
490
|
+
timeout: 5000,
|
|
491
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
492
|
+
}).trim();
|
|
493
|
+
changedFiles = diffOutput ? String(diffOutput.split("\n").length) : "0";
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// git diff may fail if the generation commit is not in history
|
|
497
|
+
}
|
|
498
|
+
console.log(`.mapra may be stale:`);
|
|
499
|
+
console.log(` Generated: ${header.timestamp} (commit ${header.gitHash})`);
|
|
500
|
+
console.log(` Current HEAD: ${currentHash} (${commitsAhead} commits ahead)`);
|
|
501
|
+
console.log(` Changed files since generation: ${changedFiles}`);
|
|
502
|
+
console.log(` Run 'mapra update' to refresh.`);
|
|
503
|
+
if (failIfStale) {
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
async function runValidatePlan(planArg, sinceDate, checkpoints = false) {
|
|
509
|
+
if (!planArg) {
|
|
510
|
+
console.error("Usage: mapra validate-plan <plan.md> [--since YYYY-MM-DD] [--checkpoints]");
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const planPath = path.resolve(planArg);
|
|
514
|
+
if (!fs.existsSync(planPath)) {
|
|
515
|
+
console.error(`Error: plan file not found: ${planPath}`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
// Find project root (walk up to find .mapra)
|
|
519
|
+
let projectRoot = path.dirname(planPath);
|
|
520
|
+
while (projectRoot !== path.dirname(projectRoot)) {
|
|
521
|
+
if (fs.existsSync(path.join(projectRoot, ".mapra")))
|
|
522
|
+
break;
|
|
523
|
+
projectRoot = path.dirname(projectRoot);
|
|
524
|
+
}
|
|
525
|
+
const strandPath = path.join(projectRoot, ".mapra");
|
|
526
|
+
if (!fs.existsSync(strandPath)) {
|
|
527
|
+
console.error("Error: no .mapra file found. Run 'mapra generate' first.");
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
// Staleness check: warn if .mapra is older than newest source file
|
|
531
|
+
const strandMtime = fs.statSync(strandPath).mtimeMs;
|
|
532
|
+
const sourceMtime = newestSourceFileMtime(projectRoot);
|
|
533
|
+
if (sourceMtime > strandMtime) {
|
|
534
|
+
const ageDays = Math.floor((Date.now() - strandMtime) / 86_400_000);
|
|
535
|
+
console.warn(`Warning: .mapra is ${ageDays > 0 ? `${ageDays}d` : "<1d"} old and source files have changed since.`);
|
|
536
|
+
console.warn(`Run 'mapra generate' first for accurate churn and risk data.\n`);
|
|
537
|
+
}
|
|
538
|
+
const { extractFilePaths, detectMissingCheckpoints } = await import("./plan-parser.js");
|
|
539
|
+
const { scanCodebase } = await import("../scanner/index.js");
|
|
540
|
+
const { analyzeGraph } = await import("../analyzer/index.js");
|
|
541
|
+
const planContent = fs.readFileSync(planPath, "utf-8");
|
|
542
|
+
const planPaths = extractFilePaths(planContent);
|
|
543
|
+
console.log(`Plan references ${planPaths.length} files. Validating against current codebase...\n`);
|
|
544
|
+
if (planPaths.length === 0) {
|
|
545
|
+
console.log("No file paths found in plan. Nothing to validate.");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Scan and analyze
|
|
549
|
+
const graph = scanCodebase(projectRoot);
|
|
550
|
+
const analysis = analyzeGraph(graph, projectRoot);
|
|
551
|
+
// Build lookup maps
|
|
552
|
+
const riskMap = new Map(analysis.risk.map((r) => [r.nodeId, r]));
|
|
553
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
554
|
+
const testCounts = new Map();
|
|
555
|
+
for (const edge of graph.edges) {
|
|
556
|
+
if (edge.type === "tests") {
|
|
557
|
+
testCounts.set(edge.to, (testCounts.get(edge.to) ?? 0) + 1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Parse --since date
|
|
561
|
+
const since = sinceDate ? new Date(sinceDate) : undefined;
|
|
562
|
+
// Categorize plan files
|
|
563
|
+
const stale = [];
|
|
564
|
+
const highCascade = [];
|
|
565
|
+
const notFound = [];
|
|
566
|
+
for (const filePath of planPaths) {
|
|
567
|
+
const node = nodeMap.get(filePath);
|
|
568
|
+
const risk = riskMap.get(filePath);
|
|
569
|
+
const churn = analysis.churn.get(filePath);
|
|
570
|
+
if (!node) {
|
|
571
|
+
notFound.push(filePath);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
// Stale: has churn data (modified recently)
|
|
575
|
+
if (churn && churn.commits30d > 0) {
|
|
576
|
+
if (!since || new Date(churn.lastCommitDate) >= since) {
|
|
577
|
+
stale.push({ path: filePath, churn, risk });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// High cascade: amplification >= 2.0
|
|
581
|
+
if (risk && risk.amplificationRatio >= 2.0) {
|
|
582
|
+
highCascade.push({
|
|
583
|
+
path: filePath,
|
|
584
|
+
risk,
|
|
585
|
+
node,
|
|
586
|
+
tests: testCounts.get(filePath) ?? 0,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Report: STALE
|
|
591
|
+
if (stale.length > 0) {
|
|
592
|
+
console.log(`STALE (modified${since ? ` since ${since.toISOString().slice(0, 10)}` : " in last 30 days"}):`);
|
|
593
|
+
for (const s of stale) {
|
|
594
|
+
console.log(` ${s.path}`);
|
|
595
|
+
if (s.churn) {
|
|
596
|
+
console.log(` ${s.churn.commits30d} commits, +${s.churn.linesAdded30d} -${s.churn.linesRemoved30d} lines`);
|
|
597
|
+
console.log(` Last: "${s.churn.lastCommitMsg}" (${s.churn.lastCommitDate.slice(0, 10)})`);
|
|
598
|
+
}
|
|
599
|
+
if (s.risk) {
|
|
600
|
+
const amp = s.risk.amplificationRatio >= 2.0 ? "[AMP] " : "";
|
|
601
|
+
console.log(` RISK: ${amp}amp${s.risk.amplificationRatio.toFixed(1)} ×${s.risk.directImporters}→${s.risk.affectedCount} d${s.risk.maxDepth}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
console.log();
|
|
605
|
+
}
|
|
606
|
+
// Report: HIGH CASCADE
|
|
607
|
+
if (highCascade.length > 0) {
|
|
608
|
+
console.log("HIGH CASCADE (amplification >= 2.0):");
|
|
609
|
+
for (const h of highCascade) {
|
|
610
|
+
console.log(` ${h.path}`);
|
|
611
|
+
console.log(` RISK: [AMP] amp${h.risk.amplificationRatio.toFixed(1)} ×${h.risk.directImporters}→${h.risk.affectedCount} d${h.risk.maxDepth}`);
|
|
612
|
+
if (h.node?.exports && h.node.exports.length > 0) {
|
|
613
|
+
const shown = h.node.exports.filter((e) => e !== "default").slice(0, 5);
|
|
614
|
+
if (shown.length > 0)
|
|
615
|
+
console.log(` exports: ${shown.join(", ")}`);
|
|
616
|
+
}
|
|
617
|
+
console.log(` Tests: ${h.tests} file${h.tests !== 1 ? "s" : ""}`);
|
|
618
|
+
}
|
|
619
|
+
console.log();
|
|
620
|
+
}
|
|
621
|
+
// Report: MISSING CONVENTIONS
|
|
622
|
+
if (analysis.conventions.length > 0) {
|
|
623
|
+
const missing = [];
|
|
624
|
+
for (const conv of analysis.conventions) {
|
|
625
|
+
// Check if plan adds new files of this consumer type
|
|
626
|
+
const newFilesOfType = notFound.filter((p) => {
|
|
627
|
+
// Rough type detection from path
|
|
628
|
+
if (conv.consumerType === "api-route" &&
|
|
629
|
+
/\/api\/.*route\.(ts|js)$/.test(p))
|
|
630
|
+
return true;
|
|
631
|
+
if (conv.consumerType === "route" && /\/page\.(tsx|jsx)$/.test(p))
|
|
632
|
+
return true;
|
|
633
|
+
return false;
|
|
634
|
+
});
|
|
635
|
+
if (newFilesOfType.length > 0) {
|
|
636
|
+
const label = conv.anchorExports.slice(0, 2).join(", ") ||
|
|
637
|
+
conv.anchorFile
|
|
638
|
+
.split("/")
|
|
639
|
+
.pop()
|
|
640
|
+
?.replace(/\.\w+$/, "") ||
|
|
641
|
+
"?";
|
|
642
|
+
missing.push(`Plan adds ${conv.consumerType} but may not import ${label} from ${conv.anchorFile} (${conv.adoption}/${conv.total} ${conv.consumerType}s use it)`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (missing.length > 0) {
|
|
646
|
+
console.log("MISSING CONVENTIONS:");
|
|
647
|
+
for (const m of missing) {
|
|
648
|
+
console.log(` ${m}`);
|
|
649
|
+
}
|
|
650
|
+
console.log();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// Report: DEAD CODE REFERENCED
|
|
654
|
+
const deadCodeSet = new Set(analysis.deadCode);
|
|
655
|
+
const deadRefs = planPaths.filter((p) => deadCodeSet.has(p));
|
|
656
|
+
if (deadRefs.length > 0) {
|
|
657
|
+
console.log("DEAD CODE REFERENCED (plan modifies unreachable files):");
|
|
658
|
+
for (const d of deadRefs) {
|
|
659
|
+
console.log(` ${d}`);
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
}
|
|
663
|
+
// Report: NOT FOUND (new files the plan will create)
|
|
664
|
+
if (notFound.length > 0) {
|
|
665
|
+
console.log(`NEW FILES (${notFound.length} paths not in current codebase):`);
|
|
666
|
+
for (const p of notFound) {
|
|
667
|
+
console.log(` ${p}`);
|
|
668
|
+
}
|
|
669
|
+
console.log();
|
|
670
|
+
}
|
|
671
|
+
// Summary
|
|
672
|
+
console.log(`SUMMARY: ${stale.length} stale, ${highCascade.length} high-cascade, ${deadRefs.length} dead-code, ${notFound.length} new files`);
|
|
673
|
+
// Checkpoint validation
|
|
674
|
+
if (checkpoints) {
|
|
675
|
+
const cpWarnings = detectMissingCheckpoints(planContent);
|
|
676
|
+
if (cpWarnings.length > 0) {
|
|
677
|
+
console.log("\nMISSING CHECKPOINTS:");
|
|
678
|
+
for (const w of cpWarnings) {
|
|
679
|
+
console.log(` \u26A0 ${w}`);
|
|
680
|
+
}
|
|
681
|
+
console.log(`\n Add [CHECKPOINT] steps after architectural changes: run \`mapra update\`,`);
|
|
682
|
+
console.log(` then use the Read tool or \`cat .mapra\` to load fresh data into context.`);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
console.log("\nCHECKPOINTS: all architectural steps have checkpoints.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function runBatchCommand(configArg, resume, smart) {
|
|
690
|
+
if (!configArg) {
|
|
691
|
+
console.error("Usage: mapra batch <config.json> [--resume] [--smart]");
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
const configPath = path.resolve(configArg);
|
|
695
|
+
if (!fs.existsSync(configPath)) {
|
|
696
|
+
console.error(`Error: config file not found: ${configPath}`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
if (!process.env["ANTHROPIC_API_KEY"]) {
|
|
700
|
+
console.error("Error: ANTHROPIC_API_KEY environment variable is required");
|
|
701
|
+
console.error(" Set it: ANTHROPIC_API_KEY=sk-... mapra batch <config>");
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const { runBatch } = await import("../batch/runner.js");
|
|
706
|
+
await runBatch(configPath, { resume, smart });
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
handleError("batch", err);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function runAnalyzeCommand(files, options) {
|
|
713
|
+
if (files.length === 0) {
|
|
714
|
+
console.error("Usage: mapra analyze <results.json> [results2.json] [--advise] [--apply] [--judge-check]");
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
const { analyzeResults, formatReport, compareIterations, formatComparison } = await import("../batch/analyzer.js");
|
|
718
|
+
const resultsPath = path.resolve(files[0]);
|
|
719
|
+
if (!fs.existsSync(resultsPath)) {
|
|
720
|
+
console.error(`Error: results file not found: ${resultsPath}`);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
const batch = JSON.parse(fs.readFileSync(resultsPath, "utf-8"));
|
|
724
|
+
if (files.length === 1) {
|
|
725
|
+
const report = analyzeResults(batch);
|
|
726
|
+
console.log(formatReport(report));
|
|
727
|
+
if (options.judgeCheck) {
|
|
728
|
+
if (!process.env["ANTHROPIC_API_KEY"]) {
|
|
729
|
+
console.error("Error: --judge-check requires ANTHROPIC_API_KEY");
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
const { runJudgeCheck, formatJudgeCheck } = await import("../batch/judge-check.js");
|
|
733
|
+
const check = await runJudgeCheck(batch);
|
|
734
|
+
console.log(formatJudgeCheck(check));
|
|
735
|
+
}
|
|
736
|
+
if (options.advise) {
|
|
737
|
+
if (!process.env["ANTHROPIC_API_KEY"]) {
|
|
738
|
+
console.error("Error: --advise requires ANTHROPIC_API_KEY");
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const { generateAdvice, formatAdvice } = await import("../batch/advisor.js");
|
|
742
|
+
const advice = await generateAdvice(report, batch);
|
|
743
|
+
console.log(formatAdvice(advice));
|
|
744
|
+
if (options.apply) {
|
|
745
|
+
console.log("\n--apply: config generation not yet implemented");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const afterPath = path.resolve(files[1]);
|
|
751
|
+
if (!fs.existsSync(afterPath)) {
|
|
752
|
+
console.error(`Error: results file not found: ${afterPath}`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
const after = JSON.parse(fs.readFileSync(afterPath, "utf-8"));
|
|
756
|
+
const comp = compareIterations(batch, after);
|
|
757
|
+
console.log(formatComparison(comp));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
761
|
+
function resolveTarget(targetArg) {
|
|
762
|
+
const targetPath = path.resolve(targetArg ?? process.cwd());
|
|
763
|
+
if (!fs.existsSync(targetPath)) {
|
|
764
|
+
console.error(`Error: path does not exist: ${targetPath}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const stat = fs.statSync(targetPath);
|
|
768
|
+
if (!stat.isDirectory()) {
|
|
769
|
+
console.error(`Error: expected a directory, got a file: ${targetPath}`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
if (!fs.existsSync(path.join(targetPath, "package.json"))) {
|
|
773
|
+
console.warn(`Warning: no package.json found at ${targetPath} — are you in the right directory?`);
|
|
774
|
+
}
|
|
775
|
+
return targetPath;
|
|
776
|
+
}
|
|
777
|
+
function handleError(command, err) {
|
|
778
|
+
if (err instanceof Error &&
|
|
779
|
+
err.code === "EACCES") {
|
|
780
|
+
console.error(`Error: permission denied`);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
console.error(`Error: ${command} failed unexpectedly`);
|
|
784
|
+
if (err instanceof Error)
|
|
785
|
+
console.error(err.message);
|
|
786
|
+
console.error(`\nPlease report this at https://github.com/joellopezjl96/mapra/issues`);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
function newestSourceFileMtime(targetPath) {
|
|
790
|
+
// Only check top-level src/ to avoid scanning everything
|
|
791
|
+
const srcPath = path.join(targetPath, "src");
|
|
792
|
+
if (!fs.existsSync(srcPath))
|
|
793
|
+
return 0;
|
|
794
|
+
let newest = 0;
|
|
795
|
+
function scan(dir) {
|
|
796
|
+
try {
|
|
797
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
798
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
799
|
+
continue;
|
|
800
|
+
const full = path.join(dir, entry.name);
|
|
801
|
+
if (entry.isDirectory()) {
|
|
802
|
+
scan(full);
|
|
803
|
+
}
|
|
804
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
805
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
806
|
+
if (mtime > newest)
|
|
807
|
+
newest = mtime;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
// skip unreadable dirs
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
scan(srcPath);
|
|
816
|
+
return newest;
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=index.js.map
|