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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/analyzer/blast-radius.d.ts +27 -0
  4. package/dist/analyzer/blast-radius.d.ts.map +1 -0
  5. package/dist/analyzer/blast-radius.js +58 -0
  6. package/dist/analyzer/blast-radius.js.map +1 -0
  7. package/dist/analyzer/churn.d.ts +26 -0
  8. package/dist/analyzer/churn.d.ts.map +1 -0
  9. package/dist/analyzer/churn.js +96 -0
  10. package/dist/analyzer/churn.js.map +1 -0
  11. package/dist/analyzer/co-change.d.ts +60 -0
  12. package/dist/analyzer/co-change.d.ts.map +1 -0
  13. package/dist/analyzer/co-change.js +153 -0
  14. package/dist/analyzer/co-change.js.map +1 -0
  15. package/dist/analyzer/conventions.d.ts +22 -0
  16. package/dist/analyzer/conventions.d.ts.map +1 -0
  17. package/dist/analyzer/conventions.js +81 -0
  18. package/dist/analyzer/conventions.js.map +1 -0
  19. package/dist/analyzer/git-hash.d.ts +11 -0
  20. package/dist/analyzer/git-hash.d.ts.map +1 -0
  21. package/dist/analyzer/git-hash.js +25 -0
  22. package/dist/analyzer/git-hash.js.map +1 -0
  23. package/dist/analyzer/graph-utils.d.ts +46 -0
  24. package/dist/analyzer/graph-utils.d.ts.map +1 -0
  25. package/dist/analyzer/graph-utils.js +145 -0
  26. package/dist/analyzer/graph-utils.js.map +1 -0
  27. package/dist/analyzer/index.d.ts +34 -0
  28. package/dist/analyzer/index.d.ts.map +1 -0
  29. package/dist/analyzer/index.js +63 -0
  30. package/dist/analyzer/index.js.map +1 -0
  31. package/dist/cli/hooks.d.ts +24 -0
  32. package/dist/cli/hooks.d.ts.map +1 -0
  33. package/dist/cli/hooks.js +126 -0
  34. package/dist/cli/hooks.js.map +1 -0
  35. package/dist/cli/index.d.ts +18 -0
  36. package/dist/cli/index.d.ts.map +1 -0
  37. package/dist/cli/index.js +818 -0
  38. package/dist/cli/index.js.map +1 -0
  39. package/dist/cli/plan-parser.d.ts +20 -0
  40. package/dist/cli/plan-parser.d.ts.map +1 -0
  41. package/dist/cli/plan-parser.js +104 -0
  42. package/dist/cli/plan-parser.js.map +1 -0
  43. package/dist/cli/shim.d.ts +3 -0
  44. package/dist/cli/shim.d.ts.map +1 -0
  45. package/dist/cli/shim.js +33 -0
  46. package/dist/cli/shim.js.map +1 -0
  47. package/dist/cli/templates.d.ts +28 -0
  48. package/dist/cli/templates.d.ts.map +1 -0
  49. package/dist/cli/templates.js +91 -0
  50. package/dist/cli/templates.js.map +1 -0
  51. package/dist/encoder/encode.d.ts +17 -0
  52. package/dist/encoder/encode.d.ts.map +1 -0
  53. package/dist/encoder/encode.js +270 -0
  54. package/dist/encoder/encode.js.map +1 -0
  55. package/dist/encoder/layer-infrastructure.d.ts +17 -0
  56. package/dist/encoder/layer-infrastructure.d.ts.map +1 -0
  57. package/dist/encoder/layer-infrastructure.js +232 -0
  58. package/dist/encoder/layer-infrastructure.js.map +1 -0
  59. package/dist/encoder/layer-labels.d.ts +18 -0
  60. package/dist/encoder/layer-labels.d.ts.map +1 -0
  61. package/dist/encoder/layer-labels.js +172 -0
  62. package/dist/encoder/layer-labels.js.map +1 -0
  63. package/dist/encoder/layer-terrain.d.ts +18 -0
  64. package/dist/encoder/layer-terrain.d.ts.map +1 -0
  65. package/dist/encoder/layer-terrain.js +135 -0
  66. package/dist/encoder/layer-terrain.js.map +1 -0
  67. package/dist/encoder/layout.d.ts +53 -0
  68. package/dist/encoder/layout.d.ts.map +1 -0
  69. package/dist/encoder/layout.js +178 -0
  70. package/dist/encoder/layout.js.map +1 -0
  71. package/dist/encoder/parse-strand-header.d.ts +29 -0
  72. package/dist/encoder/parse-strand-header.d.ts.map +1 -0
  73. package/dist/encoder/parse-strand-header.js +59 -0
  74. package/dist/encoder/parse-strand-header.js.map +1 -0
  75. package/dist/encoder/spatial-text-encode.d.ts +22 -0
  76. package/dist/encoder/spatial-text-encode.d.ts.map +1 -0
  77. package/dist/encoder/spatial-text-encode.js +199 -0
  78. package/dist/encoder/spatial-text-encode.js.map +1 -0
  79. package/dist/encoder/strand-format-encode-v1.d.ts +16 -0
  80. package/dist/encoder/strand-format-encode-v1.d.ts.map +1 -0
  81. package/dist/encoder/strand-format-encode-v1.js +296 -0
  82. package/dist/encoder/strand-format-encode-v1.js.map +1 -0
  83. package/dist/encoder/strand-format-encode.d.ts +21 -0
  84. package/dist/encoder/strand-format-encode.d.ts.map +1 -0
  85. package/dist/encoder/strand-format-encode.js +562 -0
  86. package/dist/encoder/strand-format-encode.js.map +1 -0
  87. package/dist/encoder/text-encode.d.ts +13 -0
  88. package/dist/encoder/text-encode.d.ts.map +1 -0
  89. package/dist/encoder/text-encode.js +123 -0
  90. package/dist/encoder/text-encode.js.map +1 -0
  91. package/dist/query/blast-radius.d.ts +14 -0
  92. package/dist/query/blast-radius.d.ts.map +1 -0
  93. package/dist/query/blast-radius.js +81 -0
  94. package/dist/query/blast-radius.js.map +1 -0
  95. package/dist/query/cache.d.ts +29 -0
  96. package/dist/query/cache.d.ts.map +1 -0
  97. package/dist/query/cache.js +138 -0
  98. package/dist/query/cache.js.map +1 -0
  99. package/dist/query/index.d.ts +2 -0
  100. package/dist/query/index.d.ts.map +1 -0
  101. package/dist/query/index.js +46 -0
  102. package/dist/query/index.js.map +1 -0
  103. package/dist/query/resolve.d.ts +7 -0
  104. package/dist/query/resolve.d.ts.map +1 -0
  105. package/dist/query/resolve.js +24 -0
  106. package/dist/query/resolve.js.map +1 -0
  107. package/dist/query/risk-profile.d.ts +30 -0
  108. package/dist/query/risk-profile.d.ts.map +1 -0
  109. package/dist/query/risk-profile.js +94 -0
  110. package/dist/query/risk-profile.js.map +1 -0
  111. package/dist/query/test-map.d.ts +13 -0
  112. package/dist/query/test-map.d.ts.map +1 -0
  113. package/dist/query/test-map.js +43 -0
  114. package/dist/query/test-map.js.map +1 -0
  115. package/dist/scanner/index.d.ts +51 -0
  116. package/dist/scanner/index.d.ts.map +1 -0
  117. package/dist/scanner/index.js +480 -0
  118. package/dist/scanner/index.js.map +1 -0
  119. package/dist/scanner/workspace.d.ts +11 -0
  120. package/dist/scanner/workspace.d.ts.map +1 -0
  121. package/dist/scanner/workspace.js +243 -0
  122. package/dist/scanner/workspace.js.map +1 -0
  123. 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