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