An agent harness for spec-driven development.
1import fs from "node:fs/promises";
2import path from "node:path";
3import fg from "fast-glob";
4import { parseSpecMarkdown } from "./markdown.js";
5import type { SpecDocumentRow, SpecImportPlan, SpecMarkdownDocument } from "./types.js";
6
7export interface ScanSpecOptions {
8 projectRoot?: string;
9 specDir?: string;
10}
11
12export async function scanSpecDocuments(options: ScanSpecOptions = {}): Promise<SpecMarkdownDocument[]> {
13 const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
14 const specDir = options.specDir ?? "spec";
15 const specRoot = path.resolve(projectRoot, specDir);
16
17 const entries = await fg("**/*.md", {
18 cwd: specRoot,
19 dot: false,
20 onlyFiles: true,
21 unique: true,
22 });
23
24 const documents = await Promise.all(
25 entries.sort().map(async (relativePath) => {
26 const absolutePath = path.join(specRoot, relativePath);
27 const [raw, stat] = await Promise.all([fs.readFile(absolutePath, "utf8"), fs.stat(absolutePath)]);
28 return parseSpecMarkdown({
29 absolutePath,
30 relativePath,
31 raw,
32 mtimeMs: stat.mtimeMs,
33 });
34 }),
35 );
36
37 return documents.sort((left, right) => left.path.localeCompare(right.path));
38}
39
40export async function buildImportPlan(options: ScanSpecOptions = {}): Promise<SpecImportPlan> {
41 const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
42 const specRoot = path.resolve(projectRoot, options.specDir ?? "spec");
43 const generatedAt = new Date().toISOString();
44 const documents = await scanSpecDocuments({
45 projectRoot,
46 ...(options.specDir === undefined ? {} : { specDir: options.specDir }),
47 });
48 const rows = documents.map((doc) => documentToRow(doc, generatedAt));
49
50 return {
51 projectRoot,
52 specRoot,
53 generatedAt,
54 documents,
55 rows,
56 events: [],
57 };
58}
59
60export function documentToRow(document: SpecMarkdownDocument, importedAt = new Date().toISOString()): SpecDocumentRow {
61 return {
62 id: document.id,
63 path: document.path,
64 title: document.title,
65 kind: document.kind,
66 status: document.status ?? "draft",
67 body: document.body,
68 frontmatterJson: JSON.stringify(document.frontmatter),
69 headingsJson: JSON.stringify(document.headings),
70 sha256: document.sha256,
71 sizeBytes: document.sizeBytes,
72 mtimeMs: document.mtimeMs,
73 importedAt,
74 };
75}