@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.
@@ -22,10 +22,10 @@
22
22
  * Zero npm dependencies (Node built-in only).
23
23
  */
24
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';
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('--root');
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, 'utf8'));
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('git', [
62
- 'log', '-1', '--format=%aI', '--', filePath
63
- ], { cwd: ROOT, timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] });
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('git', [
77
- 'rev-list', '--count', 'HEAD', '--', filePath
78
- ], { cwd: ROOT, timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] });
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 'current';
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, 'claims.json');
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, 'examples');
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, 'claims.json');
123
+ const claimsPath = join(examplesDir, entry.name, "claims.json");
120
124
  if (existsSync(claimsPath)) {
121
125
  roots.push({
122
126
  claimsPath,
123
- sprintPath: join('examples', entry.name),
127
+ sprintPath: join("examples", entry.name),
124
128
  });
125
129
  }
126
130
  }
127
- } catch { /* skip if unreadable */ }
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 || 'unknown';
146
- const isArchived = phase === 'archived' || phase === 'complete';
147
- const isExample = root.sprintPath.startsWith('examples/') || root.sprintPath.startsWith('examples\\');
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 = 'archived';
159
+ status = "archived";
152
160
  } else if (isExample) {
153
- status = 'example';
161
+ status = "example";
154
162
  } else {
155
- status = 'candidate';
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 === 'active').length,
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 === 'candidate');
187
- const others = sprints.filter(s => s.status !== 'candidate');
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 ? new Date(a.last_git_activity).getTime() : 0;
191
- const dateB = b.last_git_activity ? new Date(b.last_git_activity).getTime() : 0;
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 = 'active';
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 !== 'archived');
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 ? new Date(a.last_git_activity).getTime() : 0;
212
- const dateB = b.last_git_activity ? new Date(b.last_git_activity).getTime() : 0;
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 = 'active';
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 === '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;
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('--help') || args.includes('-h')) {
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('--json')) {
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('--active')) {
278
+ if (args.includes("--active")) {
259
279
  if (result.active) {
260
280
  console.log(result.active.path);
261
281
  } else {
262
- console.error('No active sprint detected.');
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('='.repeat(50));
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 === 'active' ? '>>>' : ' ';
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(` 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'}`);
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(` Question: ${sprint.question.slice(0, 80)}${sprint.question.length > 80 ? '...' : ''}`);
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('No active sprint detected.');
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 { 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';
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('root', process.cwd());
30
- const OUT_PATH = join(ROOT, arg('out', 'wheat-manifest.json'));
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, 'utf8'));
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('.') || entry.name === 'node_modules') continue;
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('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';
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 = ['stated', 'web', 'documented', 'tested', 'production'];
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, 'compilation.json');
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] = { claims: [], files: new Set(), sprint: 'current', evidence_level: 'stated' };
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 = ['research', 'prototypes', 'output', 'evidence', 'templates', 'test', 'docs'];
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('.') || entry === 'node_modules') continue;
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 !== 'other') {
160
+ if (type !== "other") {
142
161
  allFiles[entry] = { topics: [], type };
143
162
  }
144
163
  }
145
- } catch { /* skip */ }
164
+ } catch {
165
+ /* skip */
166
+ }
146
167
  }
147
- } catch { /* skip */ }
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 (claim.source?.artifact && filePath.includes(claim.source.artifact.replace(/^.*[/\\]prototypes[/\\]/, 'prototypes/'))) {
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 (sprintResult.sprints || [])) {
205
+ for (const s of sprintResult.sprints || []) {
175
206
  sprints[s.name] = {
176
- question: s.question || '',
177
- phase: s.phase || 'unknown',
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: '1.0',
227
+ schema_version: "1.0",
197
228
  generated: new Date().toISOString(),
198
- generator: '@grainulation/barn generate-manifest',
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 = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
240
+ const isMain =
241
+ process.argv[1] &&
242
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url);
210
243
  if (isMain) {
211
- if (args.includes('--help') || args.includes('-h')) {
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, 'claims.json'), { root: ROOT });
258
+ const manifest = generateManifest(join(ROOT, "claims.json"), { root: ROOT });
226
259
 
227
- writeFileSync(OUT_PATH, JSON.stringify(manifest, null, 2) + '\n');
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(` Topics: ${topicCount} | Files: ${fileCount} | Sprints: ${sprintCount} | Size: ${(sizeBytes / 1024).toFixed(1)}KB`);
269
+ console.log(
270
+ ` Topics: ${topicCount} | Files: ${fileCount} | Sprints: ${sprintCount} | Size: ${(sizeBytes / 1024).toFixed(1)}KB`,
271
+ );
237
272
  }