diff --git a/.jules/bolt.md b/.jules/bolt.md index 5513142..ef0d883 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,8 @@ ## 2026-06-09 - Optimize File System Traversal **Learning:** Combining `fs.readdirSync` with `fs.statSync` for every file is a major performance bottleneck for local-first CLI tools that traverse many files. **Action:** Always prefer `fs.readdirSync(..., { withFileTypes: true })` to avoid redundant syscalls, which significantly speeds up directory traversal. + + +## 2025-02-12 - Concurrent File Reading for CLI Performance +**Learning:** Using synchronous `readFileSync` inside loops during filesystem traversal (like in `listUseCases`) creates a significant performance bottleneck for the CLI as the number of spec files grows. +**Action:** When a CLI command needs to read multiple files from disk to aggregate data, use `fs.promises.readFile` with `Promise.all()` instead of synchronous loops to read files concurrently, and ensure the command wrapper supports asynchronous execution. \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index a6bb95a..32d1a69 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -307,9 +307,9 @@ function suggestDoctorActions(findings: { rule: string; message: string }[]) { return actions; } -function runCommand( - fn: () => T, - payload: (data: T) => { +async function runCommand( + fn: () => T | Promise, + payload: (data: Awaited) => { data: unknown; affectedFiles?: { path: string }[]; warnings?: { message: string }[]; @@ -317,7 +317,8 @@ function runCommand( }, ) { try { - outputSuccess(payload(fn())); + const result = (await fn()) as Awaited; + outputSuccess(payload(result)); } catch (error) { const info = errorInfo(error); outputError({ code: info.code, message: info.message, details: info.details, actions: info.actions }); diff --git a/src/usecase-commands.ts b/src/usecase-commands.ts index 72e0367..c99b3b4 100644 --- a/src/usecase-commands.ts +++ b/src/usecase-commands.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync, promises as fsPromises } from "node:fs"; import { join } from "node:path"; import { orderActorFrontmatter, @@ -84,11 +84,21 @@ export function createUseCase(args: { return { key, path: relativePath(path, root), format: "BRIEF" as const, affectedFiles }; } -export function listUseCases(args: { cwd?: string; status?: string; actor?: string; level?: string; q?: string }) { +export async function listUseCases(args: { cwd?: string; status?: string; actor?: string; level?: string; q?: string }) { const config = readConfig(args.cwd ?? process.cwd()); if (!config) throw new Error("NOT_INITIALIZED"); - return walkFiles(join(config.root, "specs/usecases"), (path) => path.endsWith(".md")) - .map((path) => ({ path, parsed: parseUseCaseMarkdown(readFileSync(path, "utf8")) })) + + const files = walkFiles(join(config.root, "specs/usecases"), (path) => path.endsWith(".md")); + + // ⚡ Bolt: Read files concurrently using Promise.all + const readPromises = files.map(async (path) => { + const text = await fsPromises.readFile(path, "utf8"); + return { path, parsed: parseUseCaseMarkdown(text) }; + }); + + const parsedFiles = await Promise.all(readPromises); + + return parsedFiles .filter(({ parsed }) => !args.status || parsed.frontmatter.status === args.status.toUpperCase()) .filter(({ parsed }) => !args.actor || parsed.frontmatter.primary_actor === slugify(args.actor!)) .filter(({ parsed }) => !args.level || parsed.frontmatter.level === parseLevel(args.level!)) diff --git a/tests/authoring.test.ts b/tests/authoring.test.ts index 8e83335..fcedeb6 100644 --- a/tests/authoring.test.ts +++ b/tests/authoring.test.ts @@ -11,7 +11,7 @@ import { normalizeUseCaseMarkdown } from "../src/format/normalize.js"; import { runDoctor } from "../src/validate/doctor.js"; describe("use-case authoring loop", () => { - it("runs init -> usecase create -> round-trip -> doctor with no errors", () => { + it("runs init -> usecase create -> round-trip -> doctor with no errors", async () => { const root = join(tmpdir(), `vspec-authoring-${crypto.randomUUID()}`); mkdirSync(root, { recursive: true }); initProject({ root, key: "VSPEC" }); @@ -19,7 +19,7 @@ describe("use-case authoring loop", () => { const file = readFileSync(join(root, created.path), "utf8"); expect(serializeUseCase(parseUseCaseMarkdown(file))).toBe(normalizeUseCaseMarkdown(file)); expect(runDoctor({ root, target: created.key }).findings.filter((finding) => finding.level === "error")).toEqual([]); - expect(listUseCases({ cwd: root })).toHaveLength(1); + expect(await listUseCases({ cwd: root })).toHaveLength(1); expect(showUseCase({ cwd: root, key: created.key }).useCase.frontmatter.title).toBe("Author a use case"); rmSync(root, { recursive: true, force: true }); });