-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodemap.mjs
More file actions
93 lines (85 loc) · 3.23 KB
/
Copy pathcodemap.mjs
File metadata and controls
93 lines (85 loc) · 3.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env node
// projectstore — codemap.mjs
// Derives code-map.md — the epic↔code overview — from epic/story frontmatter
// `code_refs` (ADR-004). Same one-way pattern as kanban.mjs: frontmatter is
// the source of truth, this file is a regenerated view.
//
// Output: JSON { path, content, stats } — caller writes after approval.
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { readConfig, loadLayout, folderByKind, parseFrontmatter, today } from "./lib.mjs";
function die(msg) {
process.stderr.write(`projectstore/codemap: ${msg}\n`);
process.exit(1);
}
function parseRefs(raw) {
if (!raw || raw === "[]") return [];
try {
const v = JSON.parse(raw);
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
} catch {
return [];
}
}
function main() {
const cfg = readConfig();
if (!cfg) die("No projectstore config. Run /projectstore:bind first.");
const layout = loadLayout(cfg.layout);
const folder = folderByKind(layout, "epic");
if (!folder) die("Layout has no epic folder — nothing to map.");
const root = join(cfg.vault_path, folder.path);
const epics = [];
const storyRows = [];
if (existsSync(root)) {
for (const id of readdirSync(root).sort()) {
const epicMd = join(root, id, "epic.md");
if (!existsSync(epicMd)) continue;
const fm = parseFrontmatter(readFileSync(epicMd, "utf8")).data;
epics.push({ id, title: fm.title || id, status: fm.status || "planned", refs: parseRefs(fm.code_refs) });
const storiesDir = join(root, id, "stories");
if (!existsSync(storiesDir)) continue;
for (const f of readdirSync(storiesDir).sort()) {
if (!f.endsWith(".md")) continue;
const sfm = parseFrontmatter(readFileSync(join(storiesDir, f), "utf8")).data;
const refs = parseRefs(sfm.code_refs);
if (refs.length) {
storyRows.push({ epic: id, story: f.replace(/\.md$/, ""), title: sfm.title || f, refs });
}
}
}
}
const lines = [
"---",
"",
"projectstore: derived",
`generated_at: ${today()}`,
"",
"---",
"",
"# Code map",
"",
"Epic ↔ code mapping, derived from frontmatter `code_refs` (source of truth).",
"Regenerate via `/projectstore:codemap`; edit refs via `/projectstore:codemap set`.",
"",
"| Epic | Title | Status | code_refs |",
"|------|-------|--------|-----------|",
];
for (const e of epics) {
const refs = e.refs.length ? e.refs.map((r) => `\`${r}\``).join(", ") : "—";
lines.push(`| [[${folder.path}/${e.id}/epic\\|${e.id}]] | ${e.title} | ${e.status} | ${refs} |`);
}
if (storyRows.length) {
lines.push("", "## Story-level refs (files each story touched)", "",
"| Epic | Story | code_refs |", "|------|-------|-----------|");
for (const s of storyRows) {
lines.push(`| ${s.epic} | ${s.title} | ${s.refs.map((r) => `\`${r}\``).join(", ")} |`);
}
}
lines.push("");
process.stdout.write(JSON.stringify({
path: join(cfg.vault_path, "code-map.md"),
content: lines.join("\n"),
stats: { epics: epics.length, epics_with_refs: epics.filter((e) => e.refs.length).length, story_rows: storyRows.length },
}, null, 2) + "\n");
}
main();