lean-spec 0.2.7 → 0.2.9

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.
@@ -0,0 +1,298 @@
1
+ import { getSpecFile, parseFrontmatter, matchesFilter } from './chunk-ENX5NYTE.js';
2
+ import * as fs2 from 'fs/promises';
3
+ import * as path2 from 'path';
4
+
5
+ var DEFAULT_CONFIG = {
6
+ template: "spec-template.md",
7
+ templates: {
8
+ default: "spec-template.md"
9
+ },
10
+ specsDir: "specs",
11
+ structure: {
12
+ pattern: "flat",
13
+ // Default to flat for new projects
14
+ prefix: "",
15
+ // No prefix by default - global sequence numbers only
16
+ dateFormat: "YYYYMMDD",
17
+ sequenceDigits: 3,
18
+ defaultFile: "README.md"
19
+ },
20
+ features: {
21
+ aiAgents: true,
22
+ examples: true
23
+ }
24
+ };
25
+ async function loadConfig(cwd = process.cwd()) {
26
+ const configPath = path2.join(cwd, ".lean-spec", "config.json");
27
+ try {
28
+ const content = await fs2.readFile(configPath, "utf-8");
29
+ const userConfig = JSON.parse(content);
30
+ const merged = { ...DEFAULT_CONFIG, ...userConfig };
31
+ normalizeLegacyPattern(merged);
32
+ return merged;
33
+ } catch {
34
+ return DEFAULT_CONFIG;
35
+ }
36
+ }
37
+ async function saveConfig(config, cwd = process.cwd()) {
38
+ const configDir = path2.join(cwd, ".lean-spec");
39
+ const configPath = path2.join(configDir, "config.json");
40
+ await fs2.mkdir(configDir, { recursive: true });
41
+ await fs2.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
42
+ }
43
+ function getToday(format = "YYYYMMDD") {
44
+ const now = /* @__PURE__ */ new Date();
45
+ const year = now.getFullYear();
46
+ const month = String(now.getMonth() + 1).padStart(2, "0");
47
+ const day = String(now.getDate()).padStart(2, "0");
48
+ switch (format) {
49
+ case "YYYYMMDD":
50
+ return `${year}${month}${day}`;
51
+ case "YYYY-MM-DD":
52
+ return `${year}-${month}-${day}`;
53
+ case "YYYY-MM":
54
+ return `${year}-${month}`;
55
+ case "YYYY/MM":
56
+ return `${year}/${month}`;
57
+ case "YYYY":
58
+ return String(year);
59
+ case "MM":
60
+ return month;
61
+ case "DD":
62
+ return day;
63
+ default:
64
+ return `${year}${month}${day}`;
65
+ }
66
+ }
67
+ function normalizeLegacyPattern(config) {
68
+ const pattern = config.structure.pattern;
69
+ if (pattern && pattern.includes("{date}") && pattern.includes("{seq}") && pattern.includes("{name}")) {
70
+ config.structure.pattern = "custom";
71
+ config.structure.groupExtractor = `{${config.structure.dateFormat}}`;
72
+ }
73
+ }
74
+ function resolvePrefix(prefix, dateFormat = "YYYYMMDD") {
75
+ const dateReplacements = {
76
+ "{YYYYMMDD}": () => getToday("YYYYMMDD"),
77
+ "{YYYY-MM-DD}": () => getToday("YYYY-MM-DD"),
78
+ "{YYYY-MM}": () => getToday("YYYY-MM"),
79
+ "{YYYY}": () => getToday("YYYY"),
80
+ "{MM}": () => getToday("MM"),
81
+ "{DD}": () => getToday("DD")
82
+ };
83
+ let result = prefix;
84
+ for (const [pattern, fn] of Object.entries(dateReplacements)) {
85
+ result = result.replace(pattern, fn());
86
+ }
87
+ return result;
88
+ }
89
+ function extractGroup(extractor, dateFormat = "YYYYMMDD", fields, fallback) {
90
+ const dateReplacements = {
91
+ "{YYYYMMDD}": () => getToday("YYYYMMDD"),
92
+ "{YYYY-MM-DD}": () => getToday("YYYY-MM-DD"),
93
+ "{YYYY-MM}": () => getToday("YYYY-MM"),
94
+ "{YYYY}": () => getToday("YYYY"),
95
+ "{MM}": () => getToday("MM"),
96
+ "{DD}": () => getToday("DD")
97
+ };
98
+ let result = extractor;
99
+ for (const [pattern, fn] of Object.entries(dateReplacements)) {
100
+ result = result.replace(pattern, fn());
101
+ }
102
+ const fieldMatches = result.match(/\{([^}]+)\}/g);
103
+ if (fieldMatches) {
104
+ for (const match of fieldMatches) {
105
+ const fieldName = match.slice(1, -1);
106
+ const fieldValue = fields?.[fieldName];
107
+ if (fieldValue === void 0) {
108
+ if (!fallback) {
109
+ throw new Error(`Custom field '${fieldName}' required but not provided. Set structure.groupFallback in config or provide --field ${fieldName}=<value>`);
110
+ }
111
+ return fallback;
112
+ }
113
+ result = result.replace(match, String(fieldValue));
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ async function loadSubFiles(specDir, options = {}) {
119
+ const subFiles = [];
120
+ try {
121
+ const entries = await fs2.readdir(specDir, { withFileTypes: true });
122
+ for (const entry of entries) {
123
+ if (entry.name === "README.md") continue;
124
+ if (entry.isDirectory()) continue;
125
+ const filePath = path2.join(specDir, entry.name);
126
+ const stat2 = await fs2.stat(filePath);
127
+ const ext = path2.extname(entry.name).toLowerCase();
128
+ const isDocument = ext === ".md";
129
+ const subFile = {
130
+ name: entry.name,
131
+ path: filePath,
132
+ size: stat2.size,
133
+ type: isDocument ? "document" : "asset"
134
+ };
135
+ if (isDocument && options.includeContent) {
136
+ subFile.content = await fs2.readFile(filePath, "utf-8");
137
+ }
138
+ subFiles.push(subFile);
139
+ }
140
+ } catch (error) {
141
+ return [];
142
+ }
143
+ return subFiles.sort((a, b) => {
144
+ if (a.type !== b.type) {
145
+ return a.type === "document" ? -1 : 1;
146
+ }
147
+ return a.name.localeCompare(b.name);
148
+ });
149
+ }
150
+ async function loadAllSpecs(options = {}) {
151
+ const config = await loadConfig();
152
+ const cwd = process.cwd();
153
+ const specsDir = path2.join(cwd, config.specsDir);
154
+ const specs = [];
155
+ try {
156
+ await fs2.access(specsDir);
157
+ } catch {
158
+ return [];
159
+ }
160
+ const specPattern = /^(\d{2,})-/;
161
+ async function loadSpecsFromDir(dir, relativePath = "") {
162
+ try {
163
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
164
+ for (const entry of entries) {
165
+ if (!entry.isDirectory()) continue;
166
+ if (entry.name === "archived" && relativePath === "") continue;
167
+ const entryPath = path2.join(dir, entry.name);
168
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
169
+ if (specPattern.test(entry.name)) {
170
+ const specFile = await getSpecFile(entryPath, config.structure.defaultFile);
171
+ if (specFile) {
172
+ const frontmatter = await parseFrontmatter(specFile, config);
173
+ if (frontmatter) {
174
+ if (options.filter && !matchesFilter(frontmatter, options.filter)) {
175
+ continue;
176
+ }
177
+ const dateMatch = entryRelativePath.match(/(\d{8})/);
178
+ let date;
179
+ if (dateMatch) {
180
+ date = dateMatch[1];
181
+ } else if (typeof frontmatter.created === "string") {
182
+ date = frontmatter.created;
183
+ } else if (frontmatter.created) {
184
+ date = String(frontmatter.created);
185
+ } else {
186
+ date = "";
187
+ }
188
+ const specInfo = {
189
+ path: entryRelativePath,
190
+ fullPath: entryPath,
191
+ filePath: specFile,
192
+ name: entry.name,
193
+ date,
194
+ frontmatter
195
+ };
196
+ if (options.includeContent) {
197
+ specInfo.content = await fs2.readFile(specFile, "utf-8");
198
+ }
199
+ if (options.includeSubFiles) {
200
+ specInfo.subFiles = await loadSubFiles(entryPath, {
201
+ includeContent: options.includeContent
202
+ });
203
+ }
204
+ specs.push(specInfo);
205
+ }
206
+ }
207
+ } else {
208
+ await loadSpecsFromDir(entryPath, entryRelativePath);
209
+ }
210
+ }
211
+ } catch {
212
+ }
213
+ }
214
+ await loadSpecsFromDir(specsDir);
215
+ if (options.includeArchived) {
216
+ const archivedPath = path2.join(specsDir, "archived");
217
+ await loadSpecsFromDir(archivedPath, "archived");
218
+ }
219
+ const sortBy = options.sortBy || "id";
220
+ const sortOrder = options.sortOrder || "desc";
221
+ specs.sort((a, b) => {
222
+ let comparison = 0;
223
+ switch (sortBy) {
224
+ case "id":
225
+ case "number": {
226
+ const aNum = parseInt(a.name.match(/^(\d+)/)?.[1] || "0", 10);
227
+ const bNum = parseInt(b.name.match(/^(\d+)/)?.[1] || "0", 10);
228
+ comparison = aNum - bNum;
229
+ break;
230
+ }
231
+ case "created": {
232
+ const aDate2 = String(a.frontmatter.created || "");
233
+ const bDate2 = String(b.frontmatter.created || "");
234
+ comparison = aDate2.localeCompare(bDate2);
235
+ break;
236
+ }
237
+ case "name": {
238
+ comparison = a.name.localeCompare(b.name);
239
+ break;
240
+ }
241
+ case "status": {
242
+ comparison = a.frontmatter.status.localeCompare(b.frontmatter.status);
243
+ break;
244
+ }
245
+ case "priority": {
246
+ const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
247
+ const aPriority = a.frontmatter.priority ? priorityOrder[a.frontmatter.priority] : 0;
248
+ const bPriority = b.frontmatter.priority ? priorityOrder[b.frontmatter.priority] : 0;
249
+ comparison = aPriority - bPriority;
250
+ break;
251
+ }
252
+ default:
253
+ const aDate = String(a.frontmatter.created || "");
254
+ const bDate = String(b.frontmatter.created || "");
255
+ comparison = aDate.localeCompare(bDate);
256
+ }
257
+ return sortOrder === "desc" ? -comparison : comparison;
258
+ });
259
+ return specs;
260
+ }
261
+ async function getSpec(specPath) {
262
+ const config = await loadConfig();
263
+ const cwd = process.cwd();
264
+ const specsDir = path2.join(cwd, config.specsDir);
265
+ let fullPath;
266
+ if (path2.isAbsolute(specPath)) {
267
+ fullPath = specPath;
268
+ } else {
269
+ fullPath = path2.join(specsDir, specPath);
270
+ }
271
+ try {
272
+ await fs2.access(fullPath);
273
+ } catch {
274
+ return null;
275
+ }
276
+ const specFile = await getSpecFile(fullPath, config.structure.defaultFile);
277
+ if (!specFile) return null;
278
+ const frontmatter = await parseFrontmatter(specFile, config);
279
+ if (!frontmatter) return null;
280
+ const content = await fs2.readFile(specFile, "utf-8");
281
+ const relativePath = path2.relative(specsDir, fullPath);
282
+ const parts = relativePath.split(path2.sep);
283
+ const date = parts[0] === "archived" ? parts[1] : parts[0];
284
+ const name = parts[parts.length - 1];
285
+ return {
286
+ path: relativePath,
287
+ fullPath,
288
+ filePath: specFile,
289
+ name,
290
+ date,
291
+ frontmatter,
292
+ content
293
+ };
294
+ }
295
+
296
+ export { extractGroup, getSpec, loadAllSpecs, loadConfig, loadSubFiles, resolvePrefix, saveConfig };
297
+ //# sourceMappingURL=chunk-K4VTB6BF.js.map
298
+ //# sourceMappingURL=chunk-K4VTB6BF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/spec-loader.ts"],"names":["path","fs","stat","aDate","bDate"],"mappings":"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","file":"chunk-K4VTB6BF.js","sourcesContent":["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\n\nexport interface LeanSpecConfig {\n template: string;\n templates?: Record<string, string>; // Maps template name to filename\n specsDir: string;\n autoCheck?: boolean; // Enable/disable auto-check for sequence conflicts (default: true)\n structure: {\n pattern: 'flat' | 'custom' | string; // 'flat' or 'custom', or legacy pattern string\n dateFormat: string;\n sequenceDigits: number;\n defaultFile: string;\n prefix?: string; // For flat pattern: \"{YYYYMMDD}-\" or \"spec-\" (optional, default: empty for global numbering)\n groupExtractor?: string; // For custom pattern: \"{YYYYMMDD}\" or \"milestone-{milestone}\"\n groupFallback?: string; // Fallback folder if field missing (only for non-date extractors)\n };\n features?: {\n aiAgents?: boolean;\n examples?: boolean;\n collaboration?: boolean;\n compliance?: boolean;\n approvals?: boolean;\n apiDocs?: boolean;\n };\n frontmatter?: {\n required?: string[];\n optional?: string[];\n custom?: Record<string, 'string' | 'number' | 'boolean' | 'array'>;\n };\n variables?: Record<string, string>;\n}\n\nconst DEFAULT_CONFIG: LeanSpecConfig = {\n template: 'spec-template.md',\n templates: {\n default: 'spec-template.md',\n },\n specsDir: 'specs',\n structure: {\n pattern: 'flat', // Default to flat for new projects\n prefix: '', // No prefix by default - global sequence numbers only\n dateFormat: 'YYYYMMDD',\n sequenceDigits: 3,\n defaultFile: 'README.md',\n },\n features: {\n aiAgents: true,\n examples: true,\n },\n};\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<LeanSpecConfig> {\n const configPath = path.join(cwd, '.lean-spec', 'config.json');\n\n try {\n const content = await fs.readFile(configPath, 'utf-8');\n const userConfig = JSON.parse(content);\n const merged = { ...DEFAULT_CONFIG, ...userConfig };\n \n // Normalize legacy pattern format\n normalizeLegacyPattern(merged);\n \n return merged;\n } catch {\n // No config file, use defaults\n return DEFAULT_CONFIG;\n }\n}\n\nexport async function saveConfig(\n config: LeanSpecConfig,\n cwd: string = process.cwd(),\n): Promise<void> {\n const configDir = path.join(cwd, '.lean-spec');\n const configPath = path.join(configDir, 'config.json');\n\n await fs.mkdir(configDir, { recursive: true });\n await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');\n}\n\nexport function getToday(format: string = 'YYYYMMDD'): string {\n const now = new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n\n switch (format) {\n case 'YYYYMMDD':\n return `${year}${month}${day}`;\n case 'YYYY-MM-DD':\n return `${year}-${month}-${day}`;\n case 'YYYY-MM':\n return `${year}-${month}`;\n case 'YYYY/MM':\n return `${year}/${month}`;\n case 'YYYY':\n return String(year);\n case 'MM':\n return month;\n case 'DD':\n return day;\n default:\n return `${year}${month}${day}`;\n }\n}\n\n/**\n * Detect if a config uses legacy pattern format and convert it\n */\nexport function normalizeLegacyPattern(config: LeanSpecConfig): void {\n const pattern = config.structure.pattern;\n \n // If pattern contains {date}/{seq}-{name}/, convert to custom with date grouping\n if (pattern && pattern.includes('{date}') && pattern.includes('{seq}') && pattern.includes('{name}')) {\n config.structure.pattern = 'custom';\n config.structure.groupExtractor = `{${config.structure.dateFormat}}`;\n }\n}\n\n/**\n * Resolve prefix string for flat pattern (e.g., \"{YYYYMMDD}-\" becomes \"20251103-\")\n */\nexport function resolvePrefix(\n prefix: string,\n dateFormat: string = 'YYYYMMDD'\n): string {\n const dateReplacements: Record<string, () => string> = {\n '{YYYYMMDD}': () => getToday('YYYYMMDD'),\n '{YYYY-MM-DD}': () => getToday('YYYY-MM-DD'),\n '{YYYY-MM}': () => getToday('YYYY-MM'),\n '{YYYY}': () => getToday('YYYY'),\n '{MM}': () => getToday('MM'),\n '{DD}': () => getToday('DD'),\n };\n\n let result = prefix;\n for (const [pattern, fn] of Object.entries(dateReplacements)) {\n result = result.replace(pattern, fn());\n }\n\n return result;\n}\n\n/**\n * Extract group folder from extractor pattern\n */\nexport function extractGroup(\n extractor: string,\n dateFormat: string = 'YYYYMMDD',\n fields?: Record<string, unknown>,\n fallback?: string\n): string {\n const dateReplacements: Record<string, () => string> = {\n '{YYYYMMDD}': () => getToday('YYYYMMDD'),\n '{YYYY-MM-DD}': () => getToday('YYYY-MM-DD'),\n '{YYYY-MM}': () => getToday('YYYY-MM'),\n '{YYYY}': () => getToday('YYYY'),\n '{MM}': () => getToday('MM'),\n '{DD}': () => getToday('DD'),\n };\n\n let result = extractor;\n\n // Replace date functions first\n for (const [pattern, fn] of Object.entries(dateReplacements)) {\n result = result.replace(pattern, fn());\n }\n\n // Replace frontmatter fields: {fieldname}\n const fieldMatches = result.match(/\\{([^}]+)\\}/g);\n if (fieldMatches) {\n for (const match of fieldMatches) {\n const fieldName = match.slice(1, -1); // Remove { }\n const fieldValue = fields?.[fieldName];\n\n if (fieldValue === undefined) {\n if (!fallback) {\n throw new Error(`Custom field '${fieldName}' required but not provided. Set structure.groupFallback in config or provide --field ${fieldName}=<value>`);\n }\n return fallback;\n }\n\n result = result.replace(match, String(fieldValue));\n }\n }\n\n return result;\n}\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { loadConfig } from './config.js';\nimport { parseFrontmatter, getSpecFile, matchesFilter, type SpecFrontmatter, type SpecFilterOptions } from './frontmatter.js';\n\nexport interface SpecInfo {\n path: string; // Relative path like \"20251101/003-pm-visualization-tools\"\n fullPath: string; // Absolute path to spec directory\n filePath: string; // Absolute path to spec file (README.md)\n name: string; // Just the spec name like \"003-pm-visualization-tools\"\n date?: string; // Date folder like \"20251101\" (optional for flat/nested patterns)\n frontmatter: SpecFrontmatter;\n content?: string; // Full file content (optional, for search)\n subFiles?: SubFileInfo[]; // Sub-documents and assets\n}\n\nexport interface SubFileInfo {\n name: string; // e.g., \"TESTING.md\" or \"diagram.png\"\n path: string; // Absolute path to the file\n size: number; // File size in bytes\n type: 'document' | 'asset'; // Classification based on file type\n content?: string; // Optional content for documents\n}\n\n// Load sub-files for a spec (all files except README.md)\nexport async function loadSubFiles(\n specDir: string,\n options: { includeContent?: boolean } = {}\n): Promise<SubFileInfo[]> {\n const subFiles: SubFileInfo[] = [];\n\n try {\n const entries = await fs.readdir(specDir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip README.md (main spec file)\n if (entry.name === 'README.md') continue;\n\n // Skip directories for now (could be assets folder)\n if (entry.isDirectory()) continue;\n\n const filePath = path.join(specDir, entry.name);\n const stat = await fs.stat(filePath);\n\n // Determine type based on extension\n const ext = path.extname(entry.name).toLowerCase();\n const isDocument = ext === '.md';\n\n const subFile: SubFileInfo = {\n name: entry.name,\n path: filePath,\n size: stat.size,\n type: isDocument ? 'document' : 'asset',\n };\n\n // Load content for documents if requested\n if (isDocument && options.includeContent) {\n subFile.content = await fs.readFile(filePath, 'utf-8');\n }\n\n subFiles.push(subFile);\n }\n } catch (error) {\n // Directory doesn't exist or can't be read - return empty array\n // This is expected for specs without sub-files\n return [];\n }\n\n // Sort: documents first, then alphabetically\n return subFiles.sort((a, b) => {\n if (a.type !== b.type) {\n return a.type === 'document' ? -1 : 1;\n }\n return a.name.localeCompare(b.name);\n });\n}\n\n// Load all specs from the specs directory\nexport async function loadAllSpecs(options: {\n includeArchived?: boolean;\n includeContent?: boolean;\n includeSubFiles?: boolean;\n filter?: SpecFilterOptions;\n sortBy?: string;\n sortOrder?: 'asc' | 'desc';\n} = {}): Promise<SpecInfo[]> {\n const config = await loadConfig();\n const cwd = process.cwd();\n const specsDir = path.join(cwd, config.specsDir);\n\n const specs: SpecInfo[] = [];\n\n // Check if specs directory exists\n try {\n await fs.access(specsDir);\n } catch {\n return [];\n }\n\n // Pattern to match spec directories (2 or more digits followed by dash)\n const specPattern = /^(\\d{2,})-/;\n\n // Recursively load all specs from the directory structure\n async function loadSpecsFromDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n \n // Skip archived directory in main scan (handle separately)\n if (entry.name === 'archived' && relativePath === '') continue;\n \n const entryPath = path.join(dir, entry.name);\n const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;\n \n // Check if this is a spec directory (NNN-name format)\n if (specPattern.test(entry.name)) {\n const specFile = await getSpecFile(entryPath, config.structure.defaultFile);\n \n if (specFile) {\n const frontmatter = await parseFrontmatter(specFile, config);\n \n if (frontmatter) {\n // Apply filter if provided\n if (options.filter && !matchesFilter(frontmatter, options.filter)) {\n continue;\n }\n \n // Extract date from path or frontmatter\n const dateMatch = entryRelativePath.match(/(\\d{8})/);\n let date: string;\n \n if (dateMatch) {\n date = dateMatch[1];\n } else if (typeof frontmatter.created === 'string') {\n date = frontmatter.created;\n } else if (frontmatter.created) {\n date = String(frontmatter.created);\n } else {\n date = '';\n }\n \n const specInfo: SpecInfo = {\n path: entryRelativePath,\n fullPath: entryPath,\n filePath: specFile,\n name: entry.name,\n date: date,\n frontmatter,\n };\n \n // Load content if requested\n if (options.includeContent) {\n specInfo.content = await fs.readFile(specFile, 'utf-8');\n }\n \n // Load sub-files if requested\n if (options.includeSubFiles) {\n specInfo.subFiles = await loadSubFiles(entryPath, {\n includeContent: options.includeContent,\n });\n }\n \n specs.push(specInfo);\n }\n }\n } else {\n // Not a spec directory, scan recursively for nested structure\n await loadSpecsFromDir(entryPath, entryRelativePath);\n }\n }\n } catch {\n // Directory doesn't exist or can't be read\n }\n }\n \n // Load active specs\n await loadSpecsFromDir(specsDir);\n\n // Load archived specs if requested\n if (options.includeArchived) {\n const archivedPath = path.join(specsDir, 'archived');\n await loadSpecsFromDir(archivedPath, 'archived');\n }\n\n // Sort specs based on options (default: id desc)\n const sortBy = options.sortBy || 'id';\n const sortOrder = options.sortOrder || 'desc';\n \n specs.sort((a, b) => {\n let comparison = 0;\n \n switch (sortBy) {\n case 'id':\n case 'number': { // Keep 'number' for backwards compatibility\n // Extract leading digits from spec name\n const aNum = parseInt(a.name.match(/^(\\d+)/)?.[1] || '0', 10);\n const bNum = parseInt(b.name.match(/^(\\d+)/)?.[1] || '0', 10);\n comparison = aNum - bNum;\n break;\n }\n case 'created': {\n // Sort by created date from frontmatter\n const aDate = String(a.frontmatter.created || '');\n const bDate = String(b.frontmatter.created || '');\n comparison = aDate.localeCompare(bDate);\n break;\n }\n case 'name': {\n comparison = a.name.localeCompare(b.name);\n break;\n }\n case 'status': {\n comparison = a.frontmatter.status.localeCompare(b.frontmatter.status);\n break;\n }\n case 'priority': {\n // Priority order: critical > high > medium > low > (none)\n const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };\n const aPriority = a.frontmatter.priority ? priorityOrder[a.frontmatter.priority] : 0;\n const bPriority = b.frontmatter.priority ? priorityOrder[b.frontmatter.priority] : 0;\n comparison = aPriority - bPriority;\n break;\n }\n default:\n // Default to created date\n const aDate = String(a.frontmatter.created || '');\n const bDate = String(b.frontmatter.created || '');\n comparison = aDate.localeCompare(bDate);\n }\n \n // Apply sort order\n return sortOrder === 'desc' ? -comparison : comparison;\n });\n\n return specs;\n}\n\n// Get a specific spec by path\nexport async function getSpec(specPath: string): Promise<SpecInfo | null> {\n const config = await loadConfig();\n const cwd = process.cwd();\n const specsDir = path.join(cwd, config.specsDir);\n\n // Resolve the full path\n let fullPath: string;\n if (path.isAbsolute(specPath)) {\n fullPath = specPath;\n } else {\n fullPath = path.join(specsDir, specPath);\n }\n\n // Check if directory exists\n try {\n await fs.access(fullPath);\n } catch {\n return null;\n }\n\n const specFile = await getSpecFile(fullPath, config.structure.defaultFile);\n if (!specFile) return null;\n\n const frontmatter = await parseFrontmatter(specFile, config);\n if (!frontmatter) return null;\n\n const content = await fs.readFile(specFile, 'utf-8');\n\n // Parse path components\n const relativePath = path.relative(specsDir, fullPath);\n const parts = relativePath.split(path.sep);\n const date = parts[0] === 'archived' ? parts[1] : parts[0];\n const name = parts[parts.length - 1];\n\n return {\n path: relativePath,\n fullPath,\n filePath: specFile,\n name,\n date,\n frontmatter,\n content,\n };\n}\n"]}