@grainulation/barn 1.0.0 → 1.0.1
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/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +97 -0
- package/README.md +46 -29
- package/bin/barn.js +30 -23
- package/lib/index.js +41 -25
- package/lib/server.js +211 -114
- package/package.json +8 -4
- package/public/grainulation-icons.svg +47 -0
- package/public/grainulation-tokens.css +78 -18
- package/public/status-icons.svg +40 -0
- package/templates/README.md +4 -0
- package/templates/adr.json +1 -3
- package/templates/brief.json +1 -4
- package/templates/certificate.json +7 -1
- package/templates/comparison.json +1 -3
- package/templates/dashboard.json +1 -4
- package/templates/email-digest.html +1 -1
- package/templates/email-digest.json +1 -3
- package/templates/evidence-matrix.json +1 -3
- package/templates/explainer.json +1 -4
- package/templates/handoff.json +1 -3
- package/templates/rfc.json +1 -4
- package/templates/risk-register.json +1 -4
- package/templates/slide-deck.json +1 -3
- package/templates/template.schema.json +20 -4
- package/tools/README.md +5 -2
- package/tools/build-pdf.js +6 -6
- package/tools/detect-sprints.js +78 -54
- package/tools/generate-manifest.js +78 -43
package/tools/detect-sprints.js
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
* Zero npm dependencies (Node built-in only).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { readFileSync, existsSync, readdirSync } from
|
|
26
|
-
import { join, basename } from
|
|
27
|
-
import { execFileSync } from
|
|
28
|
-
import { fileURLToPath } from
|
|
25
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
26
|
+
import { join, basename } from "node:path";
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
29
|
|
|
30
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
31
|
|
|
@@ -36,7 +36,7 @@ let ROOT = process.cwd();
|
|
|
36
36
|
/** Parse CLI args — only called when this file is the entry point. */
|
|
37
37
|
function parseCLIArgs() {
|
|
38
38
|
const args = process.argv.slice(2);
|
|
39
|
-
const i = args.indexOf(
|
|
39
|
+
const i = args.indexOf("--root");
|
|
40
40
|
if (i !== -1 && args[i + 1]) ROOT = args[i + 1];
|
|
41
41
|
return args;
|
|
42
42
|
}
|
|
@@ -46,7 +46,7 @@ function parseCLIArgs() {
|
|
|
46
46
|
/** Safely parse JSON from a file path; returns null on failure. */
|
|
47
47
|
function loadJSON(filePath) {
|
|
48
48
|
try {
|
|
49
|
-
return JSON.parse(readFileSync(filePath,
|
|
49
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
50
50
|
} catch {
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
@@ -58,9 +58,11 @@ function loadJSON(filePath) {
|
|
|
58
58
|
*/
|
|
59
59
|
function lastGitCommitDate(filePath) {
|
|
60
60
|
try {
|
|
61
|
-
const result = execFileSync(
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const result = execFileSync(
|
|
62
|
+
"git",
|
|
63
|
+
["log", "-1", "--format=%aI", "--", filePath],
|
|
64
|
+
{ cwd: ROOT, timeout: 5000, stdio: ["ignore", "pipe", "pipe"] },
|
|
65
|
+
);
|
|
64
66
|
const dateStr = result.toString().trim();
|
|
65
67
|
return dateStr || null;
|
|
66
68
|
} catch {
|
|
@@ -73,9 +75,11 @@ function lastGitCommitDate(filePath) {
|
|
|
73
75
|
*/
|
|
74
76
|
function gitCommitCount(filePath) {
|
|
75
77
|
try {
|
|
76
|
-
const result = execFileSync(
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const result = execFileSync(
|
|
79
|
+
"git",
|
|
80
|
+
["rev-list", "--count", "HEAD", "--", filePath],
|
|
81
|
+
{ cwd: ROOT, timeout: 5000, stdio: ["ignore", "pipe", "pipe"] },
|
|
82
|
+
);
|
|
79
83
|
return parseInt(result.toString().trim(), 10) || 0;
|
|
80
84
|
} catch {
|
|
81
85
|
return 0;
|
|
@@ -86,18 +90,18 @@ function gitCommitCount(filePath) {
|
|
|
86
90
|
* Derive a slug from the sprint's path or question.
|
|
87
91
|
*/
|
|
88
92
|
function deriveName(sprintPath, meta) {
|
|
89
|
-
if (sprintPath !==
|
|
93
|
+
if (sprintPath !== ".") {
|
|
90
94
|
return basename(sprintPath);
|
|
91
95
|
}
|
|
92
96
|
if (meta?.question) {
|
|
93
97
|
return meta.question
|
|
94
98
|
.toLowerCase()
|
|
95
|
-
.replace(/[^a-z0-9\s]/g,
|
|
99
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
96
100
|
.split(/\s+/)
|
|
97
101
|
.slice(0, 4)
|
|
98
|
-
.join(
|
|
102
|
+
.join("-");
|
|
99
103
|
}
|
|
100
|
-
return
|
|
104
|
+
return "current";
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
// ─── Scanner ─────────────────────────────────────────────────────────────────
|
|
@@ -106,25 +110,27 @@ function deriveName(sprintPath, meta) {
|
|
|
106
110
|
function findSprintRoots() {
|
|
107
111
|
const roots = [];
|
|
108
112
|
|
|
109
|
-
const rootClaims = join(ROOT,
|
|
113
|
+
const rootClaims = join(ROOT, "claims.json");
|
|
110
114
|
if (existsSync(rootClaims)) {
|
|
111
|
-
roots.push({ claimsPath: rootClaims, sprintPath:
|
|
115
|
+
roots.push({ claimsPath: rootClaims, sprintPath: "." });
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
const examplesDir = join(ROOT,
|
|
118
|
+
const examplesDir = join(ROOT, "examples");
|
|
115
119
|
if (existsSync(examplesDir)) {
|
|
116
120
|
try {
|
|
117
121
|
for (const entry of readdirSync(examplesDir, { withFileTypes: true })) {
|
|
118
122
|
if (!entry.isDirectory()) continue;
|
|
119
|
-
const claimsPath = join(examplesDir, entry.name,
|
|
123
|
+
const claimsPath = join(examplesDir, entry.name, "claims.json");
|
|
120
124
|
if (existsSync(claimsPath)) {
|
|
121
125
|
roots.push({
|
|
122
126
|
claimsPath,
|
|
123
|
-
sprintPath: join(
|
|
127
|
+
sprintPath: join("examples", entry.name),
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
|
-
} catch {
|
|
131
|
+
} catch {
|
|
132
|
+
/* skip if unreadable */
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
return roots;
|
|
@@ -142,29 +148,31 @@ function analyzeSprint(root) {
|
|
|
142
148
|
const lastCommit = lastGitCommitDate(root.claimsPath);
|
|
143
149
|
const commitCount = gitCommitCount(root.claimsPath);
|
|
144
150
|
|
|
145
|
-
const phase = meta.phase ||
|
|
146
|
-
const isArchived = phase ===
|
|
147
|
-
const isExample =
|
|
151
|
+
const phase = meta.phase || "unknown";
|
|
152
|
+
const isArchived = phase === "archived" || phase === "complete";
|
|
153
|
+
const isExample =
|
|
154
|
+
root.sprintPath.startsWith("examples/") ||
|
|
155
|
+
root.sprintPath.startsWith("examples\\");
|
|
148
156
|
|
|
149
157
|
let status;
|
|
150
158
|
if (isArchived) {
|
|
151
|
-
status =
|
|
159
|
+
status = "archived";
|
|
152
160
|
} else if (isExample) {
|
|
153
|
-
status =
|
|
161
|
+
status = "example";
|
|
154
162
|
} else {
|
|
155
|
-
status =
|
|
163
|
+
status = "candidate";
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
return {
|
|
159
167
|
name: deriveName(root.sprintPath, meta),
|
|
160
168
|
path: root.sprintPath,
|
|
161
|
-
question: meta.question ||
|
|
169
|
+
question: meta.question || "",
|
|
162
170
|
phase,
|
|
163
171
|
initiated: meta.initiated || null,
|
|
164
172
|
last_git_activity: lastCommit,
|
|
165
173
|
git_commit_count: commitCount,
|
|
166
174
|
claims_count: claimsList.length,
|
|
167
|
-
active_claims: claimsList.filter(c => c.status ===
|
|
175
|
+
active_claims: claimsList.filter((c) => c.status === "active").length,
|
|
168
176
|
status,
|
|
169
177
|
};
|
|
170
178
|
}
|
|
@@ -183,12 +191,16 @@ export function detectSprints(rootDir) {
|
|
|
183
191
|
const roots = findSprintRoots();
|
|
184
192
|
const sprints = roots.map(analyzeSprint).filter(Boolean);
|
|
185
193
|
|
|
186
|
-
const candidates = sprints.filter(s => s.status ===
|
|
187
|
-
const others = sprints.filter(s => s.status !==
|
|
194
|
+
const candidates = sprints.filter((s) => s.status === "candidate");
|
|
195
|
+
const others = sprints.filter((s) => s.status !== "candidate");
|
|
188
196
|
|
|
189
197
|
candidates.sort((a, b) => {
|
|
190
|
-
const dateA = a.last_git_activity
|
|
191
|
-
|
|
198
|
+
const dateA = a.last_git_activity
|
|
199
|
+
? new Date(a.last_git_activity).getTime()
|
|
200
|
+
: 0;
|
|
201
|
+
const dateB = b.last_git_activity
|
|
202
|
+
? new Date(b.last_git_activity).getTime()
|
|
203
|
+
: 0;
|
|
192
204
|
if (dateB !== dateA) return dateB - dateA;
|
|
193
205
|
|
|
194
206
|
const initA = a.initiated ? new Date(a.initiated).getTime() : 0;
|
|
@@ -200,28 +212,36 @@ export function detectSprints(rootDir) {
|
|
|
200
212
|
|
|
201
213
|
let active = null;
|
|
202
214
|
if (candidates.length > 0) {
|
|
203
|
-
candidates[0].status =
|
|
215
|
+
candidates[0].status = "active";
|
|
204
216
|
active = candidates[0];
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
if (!active && others.length > 0) {
|
|
208
|
-
const nonArchived = others.filter(s => s.status !==
|
|
220
|
+
const nonArchived = others.filter((s) => s.status !== "archived");
|
|
209
221
|
if (nonArchived.length > 0) {
|
|
210
222
|
nonArchived.sort((a, b) => {
|
|
211
|
-
const dateA = a.last_git_activity
|
|
212
|
-
|
|
223
|
+
const dateA = a.last_git_activity
|
|
224
|
+
? new Date(a.last_git_activity).getTime()
|
|
225
|
+
: 0;
|
|
226
|
+
const dateB = b.last_git_activity
|
|
227
|
+
? new Date(b.last_git_activity).getTime()
|
|
228
|
+
: 0;
|
|
213
229
|
return dateB - dateA;
|
|
214
230
|
});
|
|
215
|
-
nonArchived[0].status =
|
|
231
|
+
nonArchived[0].status = "active";
|
|
216
232
|
active = nonArchived[0];
|
|
217
233
|
}
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
const allSprints = [...candidates, ...others].sort((a, b) => {
|
|
221
|
-
if (a.status ===
|
|
222
|
-
if (b.status ===
|
|
223
|
-
const dateA = a.last_git_activity
|
|
224
|
-
|
|
237
|
+
if (a.status === "active" && b.status !== "active") return -1;
|
|
238
|
+
if (b.status === "active" && a.status !== "active") return 1;
|
|
239
|
+
const dateA = a.last_git_activity
|
|
240
|
+
? new Date(a.last_git_activity).getTime()
|
|
241
|
+
: 0;
|
|
242
|
+
const dateB = b.last_git_activity
|
|
243
|
+
? new Date(b.last_git_activity).getTime()
|
|
244
|
+
: 0;
|
|
225
245
|
return dateB - dateA;
|
|
226
246
|
});
|
|
227
247
|
|
|
@@ -232,7 +252,7 @@ export function detectSprints(rootDir) {
|
|
|
232
252
|
|
|
233
253
|
if (process.argv[1] === __filename) {
|
|
234
254
|
const args = parseCLIArgs();
|
|
235
|
-
if (args.includes(
|
|
255
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
236
256
|
console.log(`detect-sprints — git-based sprint detection (no config required)
|
|
237
257
|
|
|
238
258
|
Usage:
|
|
@@ -250,16 +270,16 @@ sprint using git commit history and metadata — no config pointer needed.`);
|
|
|
250
270
|
const result = detectSprints();
|
|
251
271
|
const elapsed = (performance.now() - t0).toFixed(1);
|
|
252
272
|
|
|
253
|
-
if (args.includes(
|
|
273
|
+
if (args.includes("--json")) {
|
|
254
274
|
console.log(JSON.stringify(result, null, 2));
|
|
255
275
|
process.exit(0);
|
|
256
276
|
}
|
|
257
277
|
|
|
258
|
-
if (args.includes(
|
|
278
|
+
if (args.includes("--active")) {
|
|
259
279
|
if (result.active) {
|
|
260
280
|
console.log(result.active.path);
|
|
261
281
|
} else {
|
|
262
|
-
console.error(
|
|
282
|
+
console.error("No active sprint detected.");
|
|
263
283
|
process.exit(1);
|
|
264
284
|
}
|
|
265
285
|
process.exit(0);
|
|
@@ -267,26 +287,30 @@ sprint using git commit history and metadata — no config pointer needed.`);
|
|
|
267
287
|
|
|
268
288
|
// Human-readable output
|
|
269
289
|
console.log(`Sprint Detection (${elapsed}ms)`);
|
|
270
|
-
console.log(
|
|
290
|
+
console.log("=".repeat(50));
|
|
271
291
|
console.log(`Found ${result.sprints.length} sprint(s)\n`);
|
|
272
292
|
|
|
273
293
|
for (const sprint of result.sprints) {
|
|
274
|
-
const icon = sprint.status ===
|
|
294
|
+
const icon = sprint.status === "active" ? ">>>" : " ";
|
|
275
295
|
const statusTag = sprint.status.toUpperCase().padEnd(8);
|
|
276
296
|
console.log(`${icon} [${statusTag}] ${sprint.name}`);
|
|
277
297
|
console.log(` Path: ${sprint.path}`);
|
|
278
298
|
console.log(` Phase: ${sprint.phase}`);
|
|
279
|
-
console.log(
|
|
280
|
-
|
|
281
|
-
|
|
299
|
+
console.log(
|
|
300
|
+
` Claims: ${sprint.claims_count} total, ${sprint.active_claims} active`,
|
|
301
|
+
);
|
|
302
|
+
console.log(` Initiated: ${sprint.initiated || "unknown"}`);
|
|
303
|
+
console.log(` Last git: ${sprint.last_git_activity || "untracked"}`);
|
|
282
304
|
console.log(` Commits: ${sprint.git_commit_count}`);
|
|
283
|
-
console.log(
|
|
305
|
+
console.log(
|
|
306
|
+
` Question: ${sprint.question.slice(0, 80)}${sprint.question.length > 80 ? "..." : ""}`,
|
|
307
|
+
);
|
|
284
308
|
console.log();
|
|
285
309
|
}
|
|
286
310
|
|
|
287
311
|
if (result.active) {
|
|
288
312
|
console.log(`Active sprint: ${result.active.path} (${result.active.name})`);
|
|
289
313
|
} else {
|
|
290
|
-
console.log(
|
|
314
|
+
console.log("No active sprint detected.");
|
|
291
315
|
}
|
|
292
316
|
}
|
|
@@ -11,11 +11,17 @@
|
|
|
11
11
|
* barn generate-manifest --out custom-name.json # Custom output path
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
import {
|
|
15
|
+
readFileSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
statSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join, relative, resolve, basename, sep } from "node:path";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { detectSprints } from "./detect-sprints.js";
|
|
19
25
|
|
|
20
26
|
// ─── CLI args ────────────────────────────────────────────────────────────────
|
|
21
27
|
|
|
@@ -26,14 +32,14 @@ function arg(name, fallback) {
|
|
|
26
32
|
return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
const ROOT = arg(
|
|
30
|
-
const OUT_PATH = join(ROOT, arg(
|
|
35
|
+
const ROOT = arg("root", process.cwd());
|
|
36
|
+
const OUT_PATH = join(ROOT, arg("out", "wheat-manifest.json"));
|
|
31
37
|
|
|
32
38
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
33
39
|
|
|
34
40
|
function loadJSON(path) {
|
|
35
41
|
try {
|
|
36
|
-
return JSON.parse(readFileSync(path,
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
37
43
|
} catch {
|
|
38
44
|
return null;
|
|
39
45
|
}
|
|
@@ -47,10 +53,10 @@ function walk(dir, filter, rootDir) {
|
|
|
47
53
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
48
54
|
const full = join(dir, entry.name);
|
|
49
55
|
if (entry.isDirectory()) {
|
|
50
|
-
if (entry.name.startsWith(
|
|
56
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
51
57
|
results.push(...walk(full, filter, base));
|
|
52
58
|
} else {
|
|
53
|
-
const rel = relative(base, full).split(sep).join(
|
|
59
|
+
const rel = relative(base, full).split(sep).join("/");
|
|
54
60
|
if (!filter || filter(rel, entry.name)) results.push(rel);
|
|
55
61
|
}
|
|
56
62
|
}
|
|
@@ -59,23 +65,23 @@ function walk(dir, filter, rootDir) {
|
|
|
59
65
|
|
|
60
66
|
/** Determine file type from its path. */
|
|
61
67
|
function classifyFile(relPath) {
|
|
62
|
-
if (relPath.startsWith(
|
|
63
|
-
if (relPath.startsWith(
|
|
64
|
-
if (relPath.startsWith(
|
|
65
|
-
if (relPath.startsWith(
|
|
66
|
-
if (relPath.startsWith(
|
|
67
|
-
if (relPath.startsWith(
|
|
68
|
-
if (relPath.startsWith(
|
|
69
|
-
if (relPath.startsWith(
|
|
70
|
-
if (relPath.endsWith(
|
|
71
|
-
if (relPath.endsWith(
|
|
72
|
-
if (relPath.endsWith(
|
|
73
|
-
return
|
|
68
|
+
if (relPath.startsWith("prototypes/")) return "prototype";
|
|
69
|
+
if (relPath.startsWith("research/")) return "research";
|
|
70
|
+
if (relPath.startsWith("output/")) return "output";
|
|
71
|
+
if (relPath.startsWith("evidence/")) return "evidence";
|
|
72
|
+
if (relPath.startsWith("templates/")) return "template";
|
|
73
|
+
if (relPath.startsWith("examples/")) return "example";
|
|
74
|
+
if (relPath.startsWith("test/")) return "test";
|
|
75
|
+
if (relPath.startsWith("docs/")) return "docs";
|
|
76
|
+
if (relPath.endsWith(".json")) return "config";
|
|
77
|
+
if (relPath.endsWith(".js") || relPath.endsWith(".mjs")) return "script";
|
|
78
|
+
if (relPath.endsWith(".md")) return "docs";
|
|
79
|
+
return "other";
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/** Compute highest evidence tier from a list of claims. */
|
|
77
83
|
function highestEvidence(claims) {
|
|
78
|
-
const tiers = [
|
|
84
|
+
const tiers = ["stated", "web", "documented", "tested", "production"];
|
|
79
85
|
let max = 0;
|
|
80
86
|
for (const c of claims) {
|
|
81
87
|
const idx = tiers.indexOf(c.evidence);
|
|
@@ -95,13 +101,13 @@ function highestEvidence(claims) {
|
|
|
95
101
|
* @returns {object} — the manifest object (not written to disk)
|
|
96
102
|
*/
|
|
97
103
|
export function generateManifest(claimsPath, opts = {}) {
|
|
98
|
-
const root = opts.root || join(claimsPath,
|
|
104
|
+
const root = opts.root || join(claimsPath, "..");
|
|
99
105
|
const claims = loadJSON(claimsPath);
|
|
100
106
|
if (!claims) {
|
|
101
107
|
throw new Error(`claims.json not found or invalid at ${claimsPath}`);
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
const compilationPath = join(root,
|
|
110
|
+
const compilationPath = join(root, "compilation.json");
|
|
105
111
|
const compilation = loadJSON(compilationPath);
|
|
106
112
|
|
|
107
113
|
// 1. Build topic map from claims
|
|
@@ -109,18 +115,31 @@ export function generateManifest(claimsPath, opts = {}) {
|
|
|
109
115
|
for (const claim of claims.claims) {
|
|
110
116
|
const topic = claim.topic;
|
|
111
117
|
if (!topicMap[topic]) {
|
|
112
|
-
topicMap[topic] = {
|
|
118
|
+
topicMap[topic] = {
|
|
119
|
+
claims: [],
|
|
120
|
+
files: new Set(),
|
|
121
|
+
sprint: "current",
|
|
122
|
+
evidence_level: "stated",
|
|
123
|
+
};
|
|
113
124
|
}
|
|
114
125
|
topicMap[topic].claims.push(claim.id);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
for (const topic of Object.keys(topicMap)) {
|
|
118
|
-
const topicClaims = claims.claims.filter(c => c.topic === topic);
|
|
129
|
+
const topicClaims = claims.claims.filter((c) => c.topic === topic);
|
|
119
130
|
topicMap[topic].evidence_level = highestEvidence(topicClaims);
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
// 2. Scan directories for files
|
|
123
|
-
const scanDirs = [
|
|
134
|
+
const scanDirs = [
|
|
135
|
+
"research",
|
|
136
|
+
"prototypes",
|
|
137
|
+
"output",
|
|
138
|
+
"evidence",
|
|
139
|
+
"templates",
|
|
140
|
+
"test",
|
|
141
|
+
"docs",
|
|
142
|
+
];
|
|
124
143
|
const allFiles = {};
|
|
125
144
|
|
|
126
145
|
for (const dir of scanDirs) {
|
|
@@ -133,23 +152,35 @@ export function generateManifest(claimsPath, opts = {}) {
|
|
|
133
152
|
// Root-level scripts/configs
|
|
134
153
|
try {
|
|
135
154
|
for (const entry of readdirSync(root)) {
|
|
136
|
-
if (entry.startsWith(
|
|
155
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
137
156
|
const full = join(root, entry);
|
|
138
157
|
try {
|
|
139
158
|
if (statSync(full).isFile()) {
|
|
140
159
|
const type = classifyFile(entry);
|
|
141
|
-
if (type !==
|
|
160
|
+
if (type !== "other") {
|
|
142
161
|
allFiles[entry] = { topics: [], type };
|
|
143
162
|
}
|
|
144
163
|
}
|
|
145
|
-
} catch {
|
|
164
|
+
} catch {
|
|
165
|
+
/* skip */
|
|
166
|
+
}
|
|
146
167
|
}
|
|
147
|
-
} catch {
|
|
168
|
+
} catch {
|
|
169
|
+
/* skip */
|
|
170
|
+
}
|
|
148
171
|
|
|
149
172
|
// 3. Map files to topics via claim artifacts
|
|
150
173
|
for (const [filePath, fileInfo] of Object.entries(allFiles)) {
|
|
151
174
|
for (const claim of claims.claims) {
|
|
152
|
-
if (
|
|
175
|
+
if (
|
|
176
|
+
claim.source?.artifact &&
|
|
177
|
+
filePath.includes(
|
|
178
|
+
claim.source.artifact.replace(
|
|
179
|
+
/^.*[/\\]prototypes[/\\]/,
|
|
180
|
+
"prototypes/",
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
) {
|
|
153
184
|
if (!fileInfo.topics.includes(claim.topic)) {
|
|
154
185
|
fileInfo.topics.push(claim.topic);
|
|
155
186
|
}
|
|
@@ -171,10 +202,10 @@ export function generateManifest(claimsPath, opts = {}) {
|
|
|
171
202
|
// 5. Detect sprints
|
|
172
203
|
const sprintResult = detectSprints(root);
|
|
173
204
|
const sprints = {};
|
|
174
|
-
for (const s of
|
|
205
|
+
for (const s of sprintResult.sprints || []) {
|
|
175
206
|
sprints[s.name] = {
|
|
176
|
-
question: s.question ||
|
|
177
|
-
phase: s.phase ||
|
|
207
|
+
question: s.question || "",
|
|
208
|
+
phase: s.phase || "unknown",
|
|
178
209
|
claims_count: s.claims_count || 0,
|
|
179
210
|
active_claims: s.active_claims || 0,
|
|
180
211
|
path: s.path,
|
|
@@ -193,9 +224,9 @@ export function generateManifest(claimsPath, opts = {}) {
|
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
return {
|
|
196
|
-
schema_version:
|
|
227
|
+
schema_version: "1.0",
|
|
197
228
|
generated: new Date().toISOString(),
|
|
198
|
-
generator:
|
|
229
|
+
generator: "@grainulation/barn generate-manifest",
|
|
199
230
|
claims_hash: compilation?.claims_hash || null,
|
|
200
231
|
topics: topicMap,
|
|
201
232
|
sprints,
|
|
@@ -206,9 +237,11 @@ export function generateManifest(claimsPath, opts = {}) {
|
|
|
206
237
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
207
238
|
|
|
208
239
|
// Only run CLI logic when executed directly (not imported)
|
|
209
|
-
const isMain =
|
|
240
|
+
const isMain =
|
|
241
|
+
process.argv[1] &&
|
|
242
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
210
243
|
if (isMain) {
|
|
211
|
-
if (args.includes(
|
|
244
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
212
245
|
console.log(`generate-manifest — build wheat-manifest.json topic map
|
|
213
246
|
|
|
214
247
|
Usage:
|
|
@@ -222,9 +255,9 @@ that gives AI tools a single file describing the sprint state.`);
|
|
|
222
255
|
}
|
|
223
256
|
|
|
224
257
|
const t0 = performance.now();
|
|
225
|
-
const manifest = generateManifest(join(ROOT,
|
|
258
|
+
const manifest = generateManifest(join(ROOT, "claims.json"), { root: ROOT });
|
|
226
259
|
|
|
227
|
-
writeFileSync(OUT_PATH, JSON.stringify(manifest, null, 2) +
|
|
260
|
+
writeFileSync(OUT_PATH, JSON.stringify(manifest, null, 2) + "\n");
|
|
228
261
|
const elapsed = (performance.now() - t0).toFixed(1);
|
|
229
262
|
|
|
230
263
|
const topicCount = Object.keys(manifest.topics).length;
|
|
@@ -233,5 +266,7 @@ that gives AI tools a single file describing the sprint state.`);
|
|
|
233
266
|
const sizeBytes = Buffer.byteLength(JSON.stringify(manifest, null, 2));
|
|
234
267
|
|
|
235
268
|
console.log(`wheat-manifest.json generated in ${elapsed}ms`);
|
|
236
|
-
console.log(
|
|
269
|
+
console.log(
|
|
270
|
+
` Topics: ${topicCount} | Files: ${fileCount} | Sprints: ${sprintCount} | Size: ${(sizeBytes / 1024).toFixed(1)}KB`,
|
|
271
|
+
);
|
|
237
272
|
}
|