lean-spec 0.2.0 → 0.2.2
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/dist/{chunk-J7ZSZ5VJ.js → chunk-7MCDTSVE.js} +2457 -883
- package/dist/chunk-7MCDTSVE.js.map +1 -0
- package/dist/{chunk-S4YNQ5KE.js → chunk-LVD7ZAVZ.js} +9 -17
- package/dist/chunk-LVD7ZAVZ.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +52 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands-GRG5UUOF.js +4 -0
- package/dist/commands-GRG5UUOF.js.map +1 -0
- package/dist/frontmatter-R2DANL5X.js +3 -0
- package/dist/frontmatter-R2DANL5X.js.map +1 -0
- package/dist/mcp-server.d.ts +16 -0
- package/dist/mcp-server.js +3 -7
- package/dist/mcp-server.js.map +1 -1
- package/package.json +17 -22
- package/templates/_shared/agents-components/core-rules-base.md +5 -0
- package/templates/_shared/agents-components/core-rules-enterprise.md +5 -0
- package/templates/_shared/agents-components/discovery-commands-enterprise.md +10 -0
- package/templates/_shared/agents-components/discovery-commands-minimal.md +8 -0
- package/templates/_shared/agents-components/discovery-commands-standard.md +9 -0
- package/templates/_shared/agents-components/enterprise-approval.md +10 -0
- package/templates/_shared/agents-components/enterprise-compliance.md +12 -0
- package/templates/_shared/agents-components/enterprise-when-required.md +13 -0
- package/templates/_shared/agents-components/frontmatter-enterprise.md +33 -0
- package/templates/_shared/agents-components/frontmatter-minimal.md +18 -0
- package/templates/_shared/agents-components/frontmatter-standard.md +23 -0
- package/templates/_shared/agents-components/quality-standards-base.md +5 -0
- package/templates/_shared/agents-components/quality-standards-enterprise.md +6 -0
- package/templates/_shared/agents-components/when-to-use-enterprise.md +11 -0
- package/templates/_shared/agents-components/when-to-use-minimal.md +9 -0
- package/templates/_shared/agents-components/when-to-use-standard.md +9 -0
- package/templates/_shared/agents-components/workflow-enterprise.md +8 -0
- package/templates/_shared/agents-components/workflow-standard-detailed.md +7 -0
- package/templates/_shared/agents-components/workflow-standard.md +5 -0
- package/templates/_shared/agents-template.hbs +39 -0
- package/templates/enterprise/agents-config.json +15 -0
- package/templates/enterprise/files/AGENTS.md +1 -0
- package/templates/minimal/agents-config.json +12 -0
- package/templates/minimal/files/AGENTS.md +1 -0
- package/templates/standard/agents-config.json +12 -0
- package/templates/standard/files/AGENTS.md +1 -0
- package/CHANGELOG.md +0 -326
- package/LICENSE +0 -21
- package/README.md +0 -421
- package/dist/chunk-J7ZSZ5VJ.js.map +0 -1
- package/dist/chunk-S4YNQ5KE.js.map +0 -1
- package/dist/frontmatter-26SOQGYM.js +0 -23
- package/dist/frontmatter-26SOQGYM.js.map +0 -1
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { normalizeDateFields, getSpecFile, updateFrontmatter, parseFrontmatter, matchesFilter } from './chunk-LVD7ZAVZ.js';
|
|
2
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as fs9 from 'fs/promises';
|
|
6
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
7
|
+
import * as path2 from 'path';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import chalk16 from 'chalk';
|
|
10
|
+
import matter4 from 'gray-matter';
|
|
11
|
+
import yaml3 from 'js-yaml';
|
|
12
|
+
import { spawn, execSync } from 'child_process';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import stripAnsi from 'strip-ansi';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { select } from '@inquirer/prompts';
|
|
17
|
+
import { encoding_for_model } from 'tiktoken';
|
|
18
|
+
import dayjs3 from 'dayjs';
|
|
19
|
+
import { marked } from 'marked';
|
|
20
|
+
import { markedTerminal } from 'marked-terminal';
|
|
21
|
+
import { readFileSync } from 'fs';
|
|
8
22
|
|
|
9
|
-
// src/mcp-server.ts
|
|
10
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
-
import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
|
|
14
|
-
// src/spec-loader.ts
|
|
15
|
-
import * as fs2 from "fs/promises";
|
|
16
|
-
import * as path2 from "path";
|
|
17
|
-
|
|
18
|
-
// src/config.ts
|
|
19
|
-
import * as fs from "fs/promises";
|
|
20
|
-
import * as path from "path";
|
|
21
23
|
var DEFAULT_CONFIG = {
|
|
22
24
|
template: "spec-template.md",
|
|
23
25
|
templates: {
|
|
@@ -39,9 +41,9 @@ var DEFAULT_CONFIG = {
|
|
|
39
41
|
}
|
|
40
42
|
};
|
|
41
43
|
async function loadConfig(cwd = process.cwd()) {
|
|
42
|
-
const configPath =
|
|
44
|
+
const configPath = path2.join(cwd, ".lean-spec", "config.json");
|
|
43
45
|
try {
|
|
44
|
-
const content = await
|
|
46
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
45
47
|
const userConfig = JSON.parse(content);
|
|
46
48
|
const merged = { ...DEFAULT_CONFIG, ...userConfig };
|
|
47
49
|
normalizeLegacyPattern(merged);
|
|
@@ -51,10 +53,10 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
async function saveConfig(config, cwd = process.cwd()) {
|
|
54
|
-
const configDir =
|
|
55
|
-
const configPath =
|
|
56
|
-
await
|
|
57
|
-
await
|
|
56
|
+
const configDir = path2.join(cwd, ".lean-spec");
|
|
57
|
+
const configPath = path2.join(configDir, "config.json");
|
|
58
|
+
await fs9.mkdir(configDir, { recursive: true });
|
|
59
|
+
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
58
60
|
}
|
|
59
61
|
function getToday(format = "YYYYMMDD") {
|
|
60
62
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -136,22 +138,22 @@ function extractGroup(extractor, dateFormat = "YYYYMMDD", fields, fallback) {
|
|
|
136
138
|
async function loadSubFiles(specDir, options = {}) {
|
|
137
139
|
const subFiles = [];
|
|
138
140
|
try {
|
|
139
|
-
const entries = await
|
|
141
|
+
const entries = await fs9.readdir(specDir, { withFileTypes: true });
|
|
140
142
|
for (const entry of entries) {
|
|
141
143
|
if (entry.name === "README.md") continue;
|
|
142
144
|
if (entry.isDirectory()) continue;
|
|
143
145
|
const filePath = path2.join(specDir, entry.name);
|
|
144
|
-
const
|
|
146
|
+
const stat6 = await fs9.stat(filePath);
|
|
145
147
|
const ext = path2.extname(entry.name).toLowerCase();
|
|
146
148
|
const isDocument = ext === ".md";
|
|
147
149
|
const subFile = {
|
|
148
150
|
name: entry.name,
|
|
149
151
|
path: filePath,
|
|
150
|
-
size:
|
|
152
|
+
size: stat6.size,
|
|
151
153
|
type: isDocument ? "document" : "asset"
|
|
152
154
|
};
|
|
153
155
|
if (isDocument && options.includeContent) {
|
|
154
|
-
subFile.content = await
|
|
156
|
+
subFile.content = await fs9.readFile(filePath, "utf-8");
|
|
155
157
|
}
|
|
156
158
|
subFiles.push(subFile);
|
|
157
159
|
}
|
|
@@ -171,14 +173,14 @@ async function loadAllSpecs(options = {}) {
|
|
|
171
173
|
const specsDir = path2.join(cwd, config.specsDir);
|
|
172
174
|
const specs = [];
|
|
173
175
|
try {
|
|
174
|
-
await
|
|
176
|
+
await fs9.access(specsDir);
|
|
175
177
|
} catch {
|
|
176
178
|
return [];
|
|
177
179
|
}
|
|
178
180
|
const specPattern = /^(\d{2,})-/;
|
|
179
181
|
async function loadSpecsFromDir(dir, relativePath = "") {
|
|
180
182
|
try {
|
|
181
|
-
const entries = await
|
|
183
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
182
184
|
for (const entry of entries) {
|
|
183
185
|
if (!entry.isDirectory()) continue;
|
|
184
186
|
if (entry.name === "archived" && relativePath === "") continue;
|
|
@@ -212,7 +214,7 @@ async function loadAllSpecs(options = {}) {
|
|
|
212
214
|
frontmatter
|
|
213
215
|
};
|
|
214
216
|
if (options.includeContent) {
|
|
215
|
-
specInfo.content = await
|
|
217
|
+
specInfo.content = await fs9.readFile(specFile, "utf-8");
|
|
216
218
|
}
|
|
217
219
|
if (options.includeSubFiles) {
|
|
218
220
|
specInfo.subFiles = await loadSubFiles(entryPath, {
|
|
@@ -287,7 +289,7 @@ async function getSpec(specPath) {
|
|
|
287
289
|
fullPath = path2.join(specsDir, specPath);
|
|
288
290
|
}
|
|
289
291
|
try {
|
|
290
|
-
await
|
|
292
|
+
await fs9.access(fullPath);
|
|
291
293
|
} catch {
|
|
292
294
|
return null;
|
|
293
295
|
}
|
|
@@ -295,7 +297,7 @@ async function getSpec(specPath) {
|
|
|
295
297
|
if (!specFile) return null;
|
|
296
298
|
const frontmatter = await parseFrontmatter(specFile, config);
|
|
297
299
|
if (!frontmatter) return null;
|
|
298
|
-
const content = await
|
|
300
|
+
const content = await fs9.readFile(specFile, "utf-8");
|
|
299
301
|
const relativePath = path2.relative(specsDir, fullPath);
|
|
300
302
|
const parts = relativePath.split(path2.sep);
|
|
301
303
|
const date = parts[0] === "archived" ? parts[1] : parts[0];
|
|
@@ -310,17 +312,6 @@ async function getSpec(specPath) {
|
|
|
310
312
|
content
|
|
311
313
|
};
|
|
312
314
|
}
|
|
313
|
-
|
|
314
|
-
// src/commands/create.ts
|
|
315
|
-
import * as fs5 from "fs/promises";
|
|
316
|
-
import * as path6 from "path";
|
|
317
|
-
import chalk4 from "chalk";
|
|
318
|
-
import matter from "gray-matter";
|
|
319
|
-
import yaml from "js-yaml";
|
|
320
|
-
|
|
321
|
-
// src/utils/path-helpers.ts
|
|
322
|
-
import * as fs3 from "fs/promises";
|
|
323
|
-
import * as path3 from "path";
|
|
324
315
|
function createSpecDirPattern() {
|
|
325
316
|
return /(?:^|\D)(\d{2,4})-[a-z]/i;
|
|
326
317
|
}
|
|
@@ -330,7 +321,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
330
321
|
const specPattern = createSpecDirPattern();
|
|
331
322
|
async function scanDirectory(dir) {
|
|
332
323
|
try {
|
|
333
|
-
const entries = await
|
|
324
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
334
325
|
for (const entry of entries) {
|
|
335
326
|
if (!entry.isDirectory()) continue;
|
|
336
327
|
const match = entry.name.match(specPattern);
|
|
@@ -341,7 +332,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
341
332
|
}
|
|
342
333
|
}
|
|
343
334
|
if (entry.name === "archived") continue;
|
|
344
|
-
const subDir =
|
|
335
|
+
const subDir = path2.join(dir, entry.name);
|
|
345
336
|
await scanDirectory(subDir);
|
|
346
337
|
}
|
|
347
338
|
} catch {
|
|
@@ -358,23 +349,23 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
358
349
|
}
|
|
359
350
|
}
|
|
360
351
|
async function resolveSpecPath(specPath, cwd, specsDir) {
|
|
361
|
-
if (
|
|
352
|
+
if (path2.isAbsolute(specPath)) {
|
|
362
353
|
try {
|
|
363
|
-
await
|
|
354
|
+
await fs9.access(specPath);
|
|
364
355
|
return specPath;
|
|
365
356
|
} catch {
|
|
366
357
|
return null;
|
|
367
358
|
}
|
|
368
359
|
}
|
|
369
|
-
const cwdPath =
|
|
360
|
+
const cwdPath = path2.resolve(cwd, specPath);
|
|
370
361
|
try {
|
|
371
|
-
await
|
|
362
|
+
await fs9.access(cwdPath);
|
|
372
363
|
return cwdPath;
|
|
373
364
|
} catch {
|
|
374
365
|
}
|
|
375
|
-
const specsPath =
|
|
366
|
+
const specsPath = path2.join(specsDir, specPath);
|
|
376
367
|
try {
|
|
377
|
-
await
|
|
368
|
+
await fs9.access(specsPath);
|
|
378
369
|
return specsPath;
|
|
379
370
|
} catch {
|
|
380
371
|
}
|
|
@@ -392,17 +383,17 @@ async function searchBySequence(specsDir, seqNum) {
|
|
|
392
383
|
const specPattern = createSpecDirPattern();
|
|
393
384
|
async function scanDirectory(dir) {
|
|
394
385
|
try {
|
|
395
|
-
const entries = await
|
|
386
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
396
387
|
for (const entry of entries) {
|
|
397
388
|
if (!entry.isDirectory()) continue;
|
|
398
389
|
const match = entry.name.match(specPattern);
|
|
399
390
|
if (match) {
|
|
400
391
|
const entrySeq = parseInt(match[1], 10);
|
|
401
392
|
if (entrySeq === seqNum) {
|
|
402
|
-
return
|
|
393
|
+
return path2.join(dir, entry.name);
|
|
403
394
|
}
|
|
404
395
|
}
|
|
405
|
-
const subDir =
|
|
396
|
+
const subDir = path2.join(dir, entry.name);
|
|
406
397
|
const result = await scanDirectory(subDir);
|
|
407
398
|
if (result) return result;
|
|
408
399
|
}
|
|
@@ -415,13 +406,13 @@ async function searchBySequence(specsDir, seqNum) {
|
|
|
415
406
|
async function searchInAllDirectories(specsDir, specName) {
|
|
416
407
|
async function scanDirectory(dir) {
|
|
417
408
|
try {
|
|
418
|
-
const entries = await
|
|
409
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
419
410
|
for (const entry of entries) {
|
|
420
411
|
if (!entry.isDirectory()) continue;
|
|
421
412
|
if (entry.name === specName) {
|
|
422
|
-
return
|
|
413
|
+
return path2.join(dir, entry.name);
|
|
423
414
|
}
|
|
424
|
-
const subDir =
|
|
415
|
+
const subDir = path2.join(dir, entry.name);
|
|
425
416
|
const result = await scanDirectory(subDir);
|
|
426
417
|
if (result) return result;
|
|
427
418
|
}
|
|
@@ -431,11 +422,6 @@ async function searchInAllDirectories(specsDir, specName) {
|
|
|
431
422
|
}
|
|
432
423
|
return scanDirectory(specsDir);
|
|
433
424
|
}
|
|
434
|
-
|
|
435
|
-
// src/utils/variable-resolver.ts
|
|
436
|
-
import * as fs4 from "fs/promises";
|
|
437
|
-
import * as path4 from "path";
|
|
438
|
-
import { execSync } from "child_process";
|
|
439
425
|
async function getGitInfo() {
|
|
440
426
|
try {
|
|
441
427
|
const user = execSync("git config user.name", { encoding: "utf-8" }).trim();
|
|
@@ -457,8 +443,8 @@ async function getGitInfo() {
|
|
|
457
443
|
}
|
|
458
444
|
async function getProjectName(cwd = process.cwd()) {
|
|
459
445
|
try {
|
|
460
|
-
const packageJsonPath =
|
|
461
|
-
const content = await
|
|
446
|
+
const packageJsonPath = path2.join(cwd, "package.json");
|
|
447
|
+
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
462
448
|
const packageJson2 = JSON.parse(content);
|
|
463
449
|
return packageJson2.name || null;
|
|
464
450
|
} catch {
|
|
@@ -539,18 +525,6 @@ async function buildVariableContext(config, options = {}) {
|
|
|
539
525
|
context.gitInfo = await getGitInfo() ?? void 0;
|
|
540
526
|
return context;
|
|
541
527
|
}
|
|
542
|
-
|
|
543
|
-
// src/commands/check.ts
|
|
544
|
-
import * as path5 from "path";
|
|
545
|
-
import chalk3 from "chalk";
|
|
546
|
-
|
|
547
|
-
// src/utils/ui.ts
|
|
548
|
-
import ora from "ora";
|
|
549
|
-
import chalk2 from "chalk";
|
|
550
|
-
|
|
551
|
-
// src/utils/safe-output.ts
|
|
552
|
-
import chalk from "chalk";
|
|
553
|
-
import stripAnsi from "strip-ansi";
|
|
554
528
|
function sanitizeUserInput(input) {
|
|
555
529
|
if (typeof input !== "string") {
|
|
556
530
|
return "";
|
|
@@ -571,7 +545,7 @@ async function withSpinner(text, fn, options) {
|
|
|
571
545
|
spinner.succeed(options?.successText || text);
|
|
572
546
|
return result;
|
|
573
547
|
} catch (error) {
|
|
574
|
-
spinner.fail(
|
|
548
|
+
spinner.fail(`${text} failed`);
|
|
575
549
|
throw error;
|
|
576
550
|
}
|
|
577
551
|
}
|
|
@@ -580,12 +554,12 @@ async function withSpinner(text, fn, options) {
|
|
|
580
554
|
async function checkSpecs(options = {}) {
|
|
581
555
|
const config = await loadConfig();
|
|
582
556
|
const cwd = process.cwd();
|
|
583
|
-
|
|
557
|
+
path2.join(cwd, config.specsDir);
|
|
584
558
|
const specs = await loadAllSpecs();
|
|
585
559
|
const sequenceMap = /* @__PURE__ */ new Map();
|
|
586
560
|
const specPattern = createSpecDirPattern();
|
|
587
561
|
for (const spec of specs) {
|
|
588
|
-
const specName =
|
|
562
|
+
const specName = path2.basename(spec.path);
|
|
589
563
|
const match = specName.match(specPattern);
|
|
590
564
|
if (match) {
|
|
591
565
|
const seq = parseInt(match[1], 10);
|
|
@@ -600,30 +574,30 @@ async function checkSpecs(options = {}) {
|
|
|
600
574
|
const conflicts = Array.from(sequenceMap.entries()).filter(([_, paths]) => paths.length > 1).sort(([a], [b]) => a - b);
|
|
601
575
|
if (conflicts.length === 0) {
|
|
602
576
|
if (!options.quiet && !options.silent) {
|
|
603
|
-
console.log(
|
|
577
|
+
console.log(chalk16.green("\u2713 No sequence conflicts detected"));
|
|
604
578
|
}
|
|
605
579
|
return true;
|
|
606
580
|
}
|
|
607
581
|
if (!options.silent) {
|
|
608
582
|
if (!options.quiet) {
|
|
609
583
|
console.log("");
|
|
610
|
-
console.log(
|
|
584
|
+
console.log(chalk16.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
|
|
611
585
|
for (const [seq, paths] of conflicts) {
|
|
612
|
-
console.log(
|
|
586
|
+
console.log(chalk16.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
|
|
613
587
|
for (const p of paths) {
|
|
614
|
-
console.log(
|
|
588
|
+
console.log(chalk16.gray(` - ${sanitizeUserInput(p)}`));
|
|
615
589
|
}
|
|
616
590
|
console.log("");
|
|
617
591
|
}
|
|
618
|
-
console.log(
|
|
619
|
-
console.log(
|
|
592
|
+
console.log(chalk16.cyan("Tip: Use date prefix to prevent conflicts:"));
|
|
593
|
+
console.log(chalk16.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
|
|
620
594
|
console.log("");
|
|
621
|
-
console.log(
|
|
595
|
+
console.log(chalk16.cyan("Or rename folders manually to resolve."));
|
|
622
596
|
console.log("");
|
|
623
597
|
} else {
|
|
624
598
|
console.log("");
|
|
625
|
-
console.log(
|
|
626
|
-
console.log(
|
|
599
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
|
|
600
|
+
console.log(chalk16.gray("Run: lean-spec check"));
|
|
627
601
|
console.log("");
|
|
628
602
|
}
|
|
629
603
|
}
|
|
@@ -644,8 +618,8 @@ async function autoCheckIfEnabled() {
|
|
|
644
618
|
async function createSpec(name, options = {}) {
|
|
645
619
|
const config = await loadConfig();
|
|
646
620
|
const cwd = process.cwd();
|
|
647
|
-
const specsDir =
|
|
648
|
-
await
|
|
621
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
622
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
649
623
|
const seq = await getGlobalNextSeq(specsDir, config.structure.sequenceDigits);
|
|
650
624
|
let specRelativePath;
|
|
651
625
|
if (config.structure.pattern === "flat") {
|
|
@@ -665,19 +639,18 @@ async function createSpec(name, options = {}) {
|
|
|
665
639
|
} else {
|
|
666
640
|
throw new Error(`Unknown pattern: ${config.structure.pattern}`);
|
|
667
641
|
}
|
|
668
|
-
const specDir =
|
|
669
|
-
const specFile =
|
|
642
|
+
const specDir = path2.join(specsDir, specRelativePath);
|
|
643
|
+
const specFile = path2.join(specDir, config.structure.defaultFile);
|
|
670
644
|
try {
|
|
671
|
-
await
|
|
645
|
+
await fs9.access(specDir);
|
|
672
646
|
throw new Error(`Spec already exists: ${sanitizeUserInput(specDir)}`);
|
|
673
647
|
} catch (error) {
|
|
674
|
-
if (error.code === "ENOENT") {
|
|
675
|
-
} else {
|
|
648
|
+
if (error.code === "ENOENT") ; else {
|
|
676
649
|
throw error;
|
|
677
650
|
}
|
|
678
651
|
}
|
|
679
|
-
await
|
|
680
|
-
const templatesDir =
|
|
652
|
+
await fs9.mkdir(specDir, { recursive: true });
|
|
653
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
681
654
|
let templateName;
|
|
682
655
|
if (options.template) {
|
|
683
656
|
if (config.templates?.[options.template]) {
|
|
@@ -689,17 +662,17 @@ async function createSpec(name, options = {}) {
|
|
|
689
662
|
} else {
|
|
690
663
|
templateName = config.template || "spec-template.md";
|
|
691
664
|
}
|
|
692
|
-
const templatePath =
|
|
665
|
+
const templatePath = path2.join(templatesDir, templateName);
|
|
693
666
|
let content;
|
|
694
667
|
try {
|
|
695
|
-
const template = await
|
|
668
|
+
const template = await fs9.readFile(templatePath, "utf-8");
|
|
696
669
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
697
670
|
const title = options.title || name;
|
|
698
671
|
const varContext = await buildVariableContext(config, { name: title, date });
|
|
699
672
|
content = resolveVariables(template, varContext);
|
|
700
|
-
const parsed =
|
|
673
|
+
const parsed = matter4(content, {
|
|
701
674
|
engines: {
|
|
702
|
-
yaml: (str) =>
|
|
675
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
703
676
|
}
|
|
704
677
|
});
|
|
705
678
|
normalizeDateFields(parsed.data);
|
|
@@ -722,9 +695,9 @@ async function createSpec(name, options = {}) {
|
|
|
722
695
|
frontmatter: parsed.data
|
|
723
696
|
};
|
|
724
697
|
parsed.content = resolveVariables(parsed.content, contextWithFrontmatter);
|
|
725
|
-
const { enrichWithTimestamps } = await import(
|
|
726
|
-
|
|
727
|
-
content =
|
|
698
|
+
const { enrichWithTimestamps: enrichWithTimestamps2 } = await import('./frontmatter-R2DANL5X.js');
|
|
699
|
+
enrichWithTimestamps2(parsed.data);
|
|
700
|
+
content = matter4.stringify(parsed.content, parsed.data);
|
|
728
701
|
if (options.description) {
|
|
729
702
|
content = content.replace(
|
|
730
703
|
/## Overview\s+<!-- What are we solving\? Why now\? -->/,
|
|
@@ -736,21 +709,16 @@ ${options.description}`
|
|
|
736
709
|
} catch (error) {
|
|
737
710
|
throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
|
|
738
711
|
}
|
|
739
|
-
await
|
|
740
|
-
console.log(
|
|
741
|
-
console.log(
|
|
712
|
+
await fs9.writeFile(specFile, content, "utf-8");
|
|
713
|
+
console.log(chalk16.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
714
|
+
console.log(chalk16.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
742
715
|
await autoCheckIfEnabled();
|
|
743
716
|
}
|
|
744
|
-
|
|
745
|
-
// src/commands/archive.ts
|
|
746
|
-
import * as fs6 from "fs/promises";
|
|
747
|
-
import * as path7 from "path";
|
|
748
|
-
import chalk5 from "chalk";
|
|
749
717
|
async function archiveSpec(specPath) {
|
|
750
718
|
await autoCheckIfEnabled();
|
|
751
719
|
const config = await loadConfig();
|
|
752
720
|
const cwd = process.cwd();
|
|
753
|
-
const specsDir =
|
|
721
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
754
722
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
755
723
|
if (!resolvedPath) {
|
|
756
724
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -759,19 +727,14 @@ async function archiveSpec(specPath) {
|
|
|
759
727
|
if (specFile) {
|
|
760
728
|
await updateFrontmatter(specFile, { status: "archived" });
|
|
761
729
|
}
|
|
762
|
-
const archiveDir =
|
|
763
|
-
await
|
|
764
|
-
const specName =
|
|
765
|
-
const archivePath =
|
|
766
|
-
await
|
|
767
|
-
console.log(
|
|
730
|
+
const archiveDir = path2.join(specsDir, "archived");
|
|
731
|
+
await fs9.mkdir(archiveDir, { recursive: true });
|
|
732
|
+
const specName = path2.basename(resolvedPath);
|
|
733
|
+
const archivePath = path2.join(archiveDir, specName);
|
|
734
|
+
await fs9.rename(resolvedPath, archivePath);
|
|
735
|
+
console.log(chalk16.green(`\u2713 Archived: ${sanitizeUserInput(archivePath)}`));
|
|
768
736
|
}
|
|
769
737
|
|
|
770
|
-
// src/commands/list.ts
|
|
771
|
-
import chalk7 from "chalk";
|
|
772
|
-
import * as fs7 from "fs/promises";
|
|
773
|
-
import * as path8 from "path";
|
|
774
|
-
|
|
775
738
|
// src/utils/pattern-detection.ts
|
|
776
739
|
function detectPatternType(config) {
|
|
777
740
|
const { pattern, groupExtractor } = config.structure;
|
|
@@ -795,66 +758,63 @@ function detectPatternType(config) {
|
|
|
795
758
|
shouldGroup: false
|
|
796
759
|
};
|
|
797
760
|
}
|
|
798
|
-
|
|
799
|
-
// src/utils/colors.ts
|
|
800
|
-
import chalk6 from "chalk";
|
|
801
761
|
var STATUS_CONFIG = {
|
|
802
762
|
planned: {
|
|
803
763
|
emoji: "\u{1F4C5}",
|
|
804
764
|
label: "Planned",
|
|
805
|
-
colorFn:
|
|
806
|
-
badge: (s = "planned") =>
|
|
765
|
+
colorFn: chalk16.blue,
|
|
766
|
+
badge: (s = "planned") => chalk16.blue(`[${s}]`)
|
|
807
767
|
},
|
|
808
768
|
"in-progress": {
|
|
809
769
|
emoji: "\u23F3",
|
|
810
770
|
label: "In Progress",
|
|
811
|
-
colorFn:
|
|
812
|
-
badge: (s = "in-progress") =>
|
|
771
|
+
colorFn: chalk16.yellow,
|
|
772
|
+
badge: (s = "in-progress") => chalk16.yellow(`[${s}]`)
|
|
813
773
|
},
|
|
814
774
|
complete: {
|
|
815
775
|
emoji: "\u2705",
|
|
816
776
|
label: "Complete",
|
|
817
|
-
colorFn:
|
|
818
|
-
badge: (s = "complete") =>
|
|
777
|
+
colorFn: chalk16.green,
|
|
778
|
+
badge: (s = "complete") => chalk16.green(`[${s}]`)
|
|
819
779
|
},
|
|
820
780
|
archived: {
|
|
821
781
|
emoji: "\u{1F4E6}",
|
|
822
782
|
label: "Archived",
|
|
823
|
-
colorFn:
|
|
824
|
-
badge: (s = "archived") =>
|
|
783
|
+
colorFn: chalk16.gray,
|
|
784
|
+
badge: (s = "archived") => chalk16.gray(`[${s}]`)
|
|
825
785
|
}
|
|
826
786
|
};
|
|
827
787
|
var PRIORITY_CONFIG = {
|
|
828
788
|
critical: {
|
|
829
789
|
emoji: "\u{1F534}",
|
|
830
|
-
colorFn:
|
|
831
|
-
badge: (s = "critical") =>
|
|
790
|
+
colorFn: chalk16.red.bold,
|
|
791
|
+
badge: (s = "critical") => chalk16.red.bold(`[${s}]`)
|
|
832
792
|
},
|
|
833
793
|
high: {
|
|
834
794
|
emoji: "\u{1F7E0}",
|
|
835
|
-
colorFn:
|
|
836
|
-
badge: (s = "high") =>
|
|
795
|
+
colorFn: chalk16.hex("#FFA500"),
|
|
796
|
+
badge: (s = "high") => chalk16.hex("#FFA500")(`[${s}]`)
|
|
837
797
|
},
|
|
838
798
|
medium: {
|
|
839
799
|
emoji: "\u{1F7E1}",
|
|
840
|
-
colorFn:
|
|
841
|
-
badge: (s = "medium") =>
|
|
800
|
+
colorFn: chalk16.yellow,
|
|
801
|
+
badge: (s = "medium") => chalk16.yellow(`[${s}]`)
|
|
842
802
|
},
|
|
843
803
|
low: {
|
|
844
804
|
emoji: "\u{1F7E2}",
|
|
845
|
-
colorFn:
|
|
846
|
-
badge: (s = "low") =>
|
|
805
|
+
colorFn: chalk16.gray,
|
|
806
|
+
badge: (s = "low") => chalk16.gray(`[${s}]`)
|
|
847
807
|
}
|
|
848
808
|
};
|
|
849
809
|
function formatStatusBadge(status) {
|
|
850
|
-
return STATUS_CONFIG[status]?.badge() ||
|
|
810
|
+
return STATUS_CONFIG[status]?.badge() || chalk16.white(`[${status}]`);
|
|
851
811
|
}
|
|
852
812
|
function formatPriorityBadge(priority) {
|
|
853
|
-
return PRIORITY_CONFIG[priority]?.badge() ||
|
|
813
|
+
return PRIORITY_CONFIG[priority]?.badge() || chalk16.white(`[${priority}]`);
|
|
854
814
|
}
|
|
855
815
|
function getStatusIndicator(status) {
|
|
856
816
|
const config = STATUS_CONFIG[status];
|
|
857
|
-
if (!config) return
|
|
817
|
+
if (!config) return chalk16.gray("[unknown]");
|
|
858
818
|
return config.colorFn(`[${status}]`);
|
|
859
819
|
}
|
|
860
820
|
function getStatusEmoji(status) {
|
|
@@ -869,9 +829,9 @@ async function listSpecs(options = {}) {
|
|
|
869
829
|
await autoCheckIfEnabled();
|
|
870
830
|
const config = await loadConfig();
|
|
871
831
|
const cwd = process.cwd();
|
|
872
|
-
const specsDir =
|
|
832
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
873
833
|
try {
|
|
874
|
-
await
|
|
834
|
+
await fs9.access(specsDir);
|
|
875
835
|
} catch {
|
|
876
836
|
console.log("");
|
|
877
837
|
console.log("No specs directory found. Initialize with: lean-spec init");
|
|
@@ -894,10 +854,10 @@ async function listSpecs(options = {}) {
|
|
|
894
854
|
})
|
|
895
855
|
);
|
|
896
856
|
if (specs.length === 0) {
|
|
897
|
-
console.log(
|
|
857
|
+
console.log(chalk16.dim("No specs found."));
|
|
898
858
|
return;
|
|
899
859
|
}
|
|
900
|
-
console.log(
|
|
860
|
+
console.log(chalk16.bold.cyan("\u{1F4C4} Spec List"));
|
|
901
861
|
const filterParts = [];
|
|
902
862
|
if (options.status) {
|
|
903
863
|
const statusStr = Array.isArray(options.status) ? options.status.join(",") : options.status;
|
|
@@ -910,7 +870,7 @@ async function listSpecs(options = {}) {
|
|
|
910
870
|
}
|
|
911
871
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
912
872
|
if (filterParts.length > 0) {
|
|
913
|
-
console.log(
|
|
873
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
914
874
|
}
|
|
915
875
|
console.log("");
|
|
916
876
|
const patternInfo = detectPatternType(config);
|
|
@@ -920,7 +880,7 @@ async function listSpecs(options = {}) {
|
|
|
920
880
|
renderFlatList(specs);
|
|
921
881
|
}
|
|
922
882
|
console.log("");
|
|
923
|
-
console.log(
|
|
883
|
+
console.log(chalk16.bold(`Total: ${chalk16.green(specs.length)} spec${specs.length !== 1 ? "s" : ""}`));
|
|
924
884
|
}
|
|
925
885
|
function renderFlatList(specs) {
|
|
926
886
|
for (const spec of specs) {
|
|
@@ -928,18 +888,18 @@ function renderFlatList(specs) {
|
|
|
928
888
|
const priorityEmoji = getPriorityEmoji(spec.frontmatter.priority);
|
|
929
889
|
let assigneeStr = "";
|
|
930
890
|
if (spec.frontmatter.assignee) {
|
|
931
|
-
assigneeStr = " " +
|
|
891
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
932
892
|
}
|
|
933
893
|
let tagsStr = "";
|
|
934
894
|
if (spec.frontmatter.tags?.length) {
|
|
935
895
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
936
896
|
if (tags.length > 0) {
|
|
937
897
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
938
|
-
tagsStr = " " +
|
|
898
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
939
899
|
}
|
|
940
900
|
}
|
|
941
901
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
942
|
-
console.log(`${priorityPrefix}${statusEmoji} ${
|
|
902
|
+
console.log(`${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
943
903
|
}
|
|
944
904
|
}
|
|
945
905
|
function renderGroupedList(specs, groupExtractor) {
|
|
@@ -968,7 +928,7 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
968
928
|
const groupName = sortedGroups[i];
|
|
969
929
|
const groupSpecs = groups.get(groupName);
|
|
970
930
|
const groupEmoji = /^\d{8}$/.test(groupName) ? "\u{1F4C5}" : groupName.startsWith("milestone") ? "\u{1F3AF}" : "\u{1F4C1}";
|
|
971
|
-
console.log(`${
|
|
931
|
+
console.log(`${chalk16.bold.cyan(`${groupEmoji} ${groupName}/`)} ${chalk16.dim(`(${groupSpecs.length})`)}`);
|
|
972
932
|
console.log("");
|
|
973
933
|
for (const spec of groupSpecs) {
|
|
974
934
|
const statusEmoji = getStatusEmoji(spec.frontmatter.status);
|
|
@@ -976,33 +936,29 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
976
936
|
const displayPath = spec.path.includes("/") ? spec.path.split("/").slice(1).join("/") : spec.path;
|
|
977
937
|
let assigneeStr = "";
|
|
978
938
|
if (spec.frontmatter.assignee) {
|
|
979
|
-
assigneeStr = " " +
|
|
939
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
980
940
|
}
|
|
981
941
|
let tagsStr = "";
|
|
982
942
|
if (spec.frontmatter.tags?.length) {
|
|
983
943
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
984
944
|
if (tags.length > 0) {
|
|
985
945
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
986
|
-
tagsStr = " " +
|
|
946
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
987
947
|
}
|
|
988
948
|
}
|
|
989
949
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
990
|
-
console.log(` ${priorityPrefix}${statusEmoji} ${
|
|
950
|
+
console.log(` ${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(displayPath))}${assigneeStr}${tagsStr}`);
|
|
991
951
|
}
|
|
992
952
|
if (i < sortedGroups.length - 1) {
|
|
993
953
|
console.log("");
|
|
994
954
|
}
|
|
995
955
|
}
|
|
996
956
|
}
|
|
997
|
-
|
|
998
|
-
// src/commands/update.ts
|
|
999
|
-
import * as path9 from "path";
|
|
1000
|
-
import chalk8 from "chalk";
|
|
1001
957
|
async function updateSpec(specPath, updates) {
|
|
1002
958
|
await autoCheckIfEnabled();
|
|
1003
959
|
const config = await loadConfig();
|
|
1004
960
|
const cwd = process.cwd();
|
|
1005
|
-
const specsDir =
|
|
961
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1006
962
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1007
963
|
if (!resolvedPath) {
|
|
1008
964
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Tried: ${sanitizeUserInput(specPath)}, specs/${sanitizeUserInput(specPath)}, and searching in date directories`);
|
|
@@ -1024,22 +980,16 @@ async function updateSpec(specPath, updates) {
|
|
|
1024
980
|
});
|
|
1025
981
|
}
|
|
1026
982
|
await updateFrontmatter(specFile, allUpdates);
|
|
1027
|
-
console.log(
|
|
983
|
+
console.log(chalk16.green(`\u2713 Updated: ${sanitizeUserInput(path2.relative(cwd, resolvedPath))}`));
|
|
1028
984
|
const updatedFields = Object.keys(updates).filter((k) => k !== "customFields");
|
|
1029
985
|
if (updates.customFields) {
|
|
1030
986
|
updatedFields.push(...Object.keys(updates.customFields));
|
|
1031
987
|
}
|
|
1032
|
-
console.log(
|
|
988
|
+
console.log(chalk16.gray(` Fields: ${updatedFields.join(", ")}`));
|
|
1033
989
|
}
|
|
1034
|
-
|
|
1035
|
-
// src/commands/backfill.ts
|
|
1036
|
-
import * as path10 from "path";
|
|
1037
|
-
|
|
1038
|
-
// src/utils/git-timestamps.ts
|
|
1039
|
-
import { execSync as execSync2 } from "child_process";
|
|
1040
990
|
function isGitRepository() {
|
|
1041
991
|
try {
|
|
1042
|
-
|
|
992
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
1043
993
|
stdio: "ignore",
|
|
1044
994
|
encoding: "utf-8"
|
|
1045
995
|
});
|
|
@@ -1050,7 +1000,7 @@ function isGitRepository() {
|
|
|
1050
1000
|
}
|
|
1051
1001
|
function getFirstCommitTimestamp(filePath) {
|
|
1052
1002
|
try {
|
|
1053
|
-
const timestamp =
|
|
1003
|
+
const timestamp = execSync(
|
|
1054
1004
|
`git log --follow --format="%aI" --diff-filter=A -- "${filePath}" | tail -1`,
|
|
1055
1005
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1056
1006
|
).trim();
|
|
@@ -1061,7 +1011,7 @@ function getFirstCommitTimestamp(filePath) {
|
|
|
1061
1011
|
}
|
|
1062
1012
|
function getLastCommitTimestamp(filePath) {
|
|
1063
1013
|
try {
|
|
1064
|
-
const timestamp =
|
|
1014
|
+
const timestamp = execSync(
|
|
1065
1015
|
`git log --format="%aI" -n 1 -- "${filePath}"`,
|
|
1066
1016
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1067
1017
|
).trim();
|
|
@@ -1072,7 +1022,7 @@ function getLastCommitTimestamp(filePath) {
|
|
|
1072
1022
|
}
|
|
1073
1023
|
function getCompletionTimestamp(filePath) {
|
|
1074
1024
|
try {
|
|
1075
|
-
const gitLog =
|
|
1025
|
+
const gitLog = execSync(
|
|
1076
1026
|
`git log --format="%H|%aI" -p -- "${filePath}"`,
|
|
1077
1027
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1078
1028
|
);
|
|
@@ -1093,7 +1043,7 @@ function getCompletionTimestamp(filePath) {
|
|
|
1093
1043
|
}
|
|
1094
1044
|
function getFirstCommitAuthor(filePath) {
|
|
1095
1045
|
try {
|
|
1096
|
-
const author =
|
|
1046
|
+
const author = execSync(
|
|
1097
1047
|
`git log --follow --format="%an" --diff-filter=A -- "${filePath}" | tail -1`,
|
|
1098
1048
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1099
1049
|
).trim();
|
|
@@ -1105,7 +1055,7 @@ function getFirstCommitAuthor(filePath) {
|
|
|
1105
1055
|
function parseStatusTransitions(filePath) {
|
|
1106
1056
|
const transitions = [];
|
|
1107
1057
|
try {
|
|
1108
|
-
const gitLog =
|
|
1058
|
+
const gitLog = execSync(
|
|
1109
1059
|
`git log --format="%H|%aI" -p --reverse -- "${filePath}"`,
|
|
1110
1060
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1111
1061
|
);
|
|
@@ -1153,7 +1103,7 @@ function extractGitTimestamps(filePath, options = {}) {
|
|
|
1153
1103
|
}
|
|
1154
1104
|
function fileExistsInGit(filePath) {
|
|
1155
1105
|
try {
|
|
1156
|
-
|
|
1106
|
+
execSync(
|
|
1157
1107
|
`git log -n 1 -- "${filePath}"`,
|
|
1158
1108
|
{ stdio: "ignore", encoding: "utf-8" }
|
|
1159
1109
|
);
|
|
@@ -1176,7 +1126,7 @@ async function backfillTimestamps(options = {}) {
|
|
|
1176
1126
|
specs = [];
|
|
1177
1127
|
const config = await loadConfig();
|
|
1178
1128
|
const cwd = process.cwd();
|
|
1179
|
-
const specsDir =
|
|
1129
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1180
1130
|
for (const specPath of options.specs) {
|
|
1181
1131
|
const resolved = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1182
1132
|
if (!resolved) {
|
|
@@ -1348,86 +1298,81 @@ function printSummary(results, options) {
|
|
|
1348
1298
|
console.log(" Run \x1B[36mlspec stats\x1B[0m to see velocity metrics");
|
|
1349
1299
|
}
|
|
1350
1300
|
}
|
|
1351
|
-
|
|
1352
|
-
// src/commands/templates.ts
|
|
1353
|
-
import * as fs8 from "fs/promises";
|
|
1354
|
-
import * as path11 from "path";
|
|
1355
|
-
import chalk9 from "chalk";
|
|
1356
1301
|
async function listTemplates(cwd = process.cwd()) {
|
|
1357
1302
|
const config = await loadConfig(cwd);
|
|
1358
|
-
const templatesDir =
|
|
1303
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1359
1304
|
console.log("");
|
|
1360
|
-
console.log(
|
|
1305
|
+
console.log(chalk16.green("=== Project Templates ==="));
|
|
1361
1306
|
console.log("");
|
|
1362
1307
|
try {
|
|
1363
|
-
await
|
|
1308
|
+
await fs9.access(templatesDir);
|
|
1364
1309
|
} catch {
|
|
1365
|
-
console.log(
|
|
1366
|
-
console.log(
|
|
1310
|
+
console.log(chalk16.yellow("No templates directory found."));
|
|
1311
|
+
console.log(chalk16.gray("Run: lean-spec init"));
|
|
1367
1312
|
console.log("");
|
|
1368
1313
|
return;
|
|
1369
1314
|
}
|
|
1370
|
-
const files = await
|
|
1315
|
+
const files = await fs9.readdir(templatesDir);
|
|
1371
1316
|
const templateFiles = files.filter((f) => f.endsWith(".md"));
|
|
1372
1317
|
if (templateFiles.length === 0) {
|
|
1373
|
-
console.log(
|
|
1318
|
+
console.log(chalk16.yellow("No templates found."));
|
|
1374
1319
|
console.log("");
|
|
1375
1320
|
return;
|
|
1376
1321
|
}
|
|
1377
1322
|
if (config.templates && Object.keys(config.templates).length > 0) {
|
|
1378
|
-
console.log(
|
|
1323
|
+
console.log(chalk16.cyan("Registered:"));
|
|
1379
1324
|
for (const [name, file] of Object.entries(config.templates)) {
|
|
1380
1325
|
const isDefault = config.template === file;
|
|
1381
|
-
const marker = isDefault ?
|
|
1382
|
-
console.log(` ${
|
|
1326
|
+
const marker = isDefault ? chalk16.green("\u2713 (default)") : "";
|
|
1327
|
+
console.log(` ${chalk16.bold(name)}: ${file} ${marker}`);
|
|
1383
1328
|
}
|
|
1384
1329
|
console.log("");
|
|
1385
1330
|
}
|
|
1386
|
-
console.log(
|
|
1331
|
+
console.log(chalk16.cyan("Available files:"));
|
|
1387
1332
|
for (const file of templateFiles) {
|
|
1388
|
-
const filePath =
|
|
1389
|
-
const
|
|
1390
|
-
const sizeKB = (
|
|
1333
|
+
const filePath = path2.join(templatesDir, file);
|
|
1334
|
+
const stat6 = await fs9.stat(filePath);
|
|
1335
|
+
const sizeKB = (stat6.size / 1024).toFixed(1);
|
|
1391
1336
|
console.log(` ${file} (${sizeKB} KB)`);
|
|
1392
1337
|
}
|
|
1393
1338
|
console.log("");
|
|
1394
|
-
console.log(
|
|
1339
|
+
console.log(chalk16.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
|
|
1395
1340
|
console.log("");
|
|
1396
1341
|
}
|
|
1397
1342
|
async function showTemplate(templateName, cwd = process.cwd()) {
|
|
1398
1343
|
const config = await loadConfig(cwd);
|
|
1399
1344
|
if (!config.templates?.[templateName]) {
|
|
1400
|
-
console.error(
|
|
1401
|
-
console.error(
|
|
1345
|
+
console.error(chalk16.red(`Template not found: ${templateName}`));
|
|
1346
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1402
1347
|
process.exit(1);
|
|
1403
1348
|
}
|
|
1404
|
-
const templatesDir =
|
|
1349
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1405
1350
|
const templateFile = config.templates[templateName];
|
|
1406
|
-
const templatePath =
|
|
1351
|
+
const templatePath = path2.join(templatesDir, templateFile);
|
|
1407
1352
|
try {
|
|
1408
|
-
const content = await
|
|
1353
|
+
const content = await fs9.readFile(templatePath, "utf-8");
|
|
1409
1354
|
console.log("");
|
|
1410
|
-
console.log(
|
|
1355
|
+
console.log(chalk16.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
|
|
1411
1356
|
console.log("");
|
|
1412
1357
|
console.log(content);
|
|
1413
1358
|
console.log("");
|
|
1414
1359
|
} catch (error) {
|
|
1415
|
-
console.error(
|
|
1360
|
+
console.error(chalk16.red(`Error reading template: ${templateFile}`));
|
|
1416
1361
|
console.error(error);
|
|
1417
1362
|
process.exit(1);
|
|
1418
1363
|
}
|
|
1419
1364
|
}
|
|
1420
1365
|
async function addTemplate(name, file, cwd = process.cwd()) {
|
|
1421
1366
|
const config = await loadConfig(cwd);
|
|
1422
|
-
const templatesDir =
|
|
1423
|
-
const templatePath =
|
|
1367
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1368
|
+
const templatePath = path2.join(templatesDir, file);
|
|
1424
1369
|
try {
|
|
1425
|
-
await
|
|
1370
|
+
await fs9.access(templatePath);
|
|
1426
1371
|
} catch {
|
|
1427
|
-
console.error(
|
|
1428
|
-
console.error(
|
|
1372
|
+
console.error(chalk16.red(`Template file not found: ${file}`));
|
|
1373
|
+
console.error(chalk16.gray(`Expected at: ${templatePath}`));
|
|
1429
1374
|
console.error(
|
|
1430
|
-
|
|
1375
|
+
chalk16.yellow("Create the file first or use: lean-spec templates copy <source> <target>")
|
|
1431
1376
|
);
|
|
1432
1377
|
process.exit(1);
|
|
1433
1378
|
}
|
|
@@ -1435,73 +1380,61 @@ async function addTemplate(name, file, cwd = process.cwd()) {
|
|
|
1435
1380
|
config.templates = {};
|
|
1436
1381
|
}
|
|
1437
1382
|
if (config.templates[name]) {
|
|
1438
|
-
console.log(
|
|
1383
|
+
console.log(chalk16.yellow(`Warning: Template '${name}' already exists, updating...`));
|
|
1439
1384
|
}
|
|
1440
1385
|
config.templates[name] = file;
|
|
1441
1386
|
await saveConfig(config, cwd);
|
|
1442
|
-
console.log(
|
|
1443
|
-
console.log(
|
|
1387
|
+
console.log(chalk16.green(`\u2713 Added template: ${name} \u2192 ${file}`));
|
|
1388
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
|
|
1444
1389
|
}
|
|
1445
1390
|
async function removeTemplate(name, cwd = process.cwd()) {
|
|
1446
1391
|
const config = await loadConfig(cwd);
|
|
1447
1392
|
if (!config.templates?.[name]) {
|
|
1448
|
-
console.error(
|
|
1449
|
-
console.error(
|
|
1393
|
+
console.error(chalk16.red(`Template not found: ${name}`));
|
|
1394
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1450
1395
|
process.exit(1);
|
|
1451
1396
|
}
|
|
1452
1397
|
if (name === "default") {
|
|
1453
|
-
console.error(
|
|
1398
|
+
console.error(chalk16.red("Cannot remove default template"));
|
|
1454
1399
|
process.exit(1);
|
|
1455
1400
|
}
|
|
1456
1401
|
const file = config.templates[name];
|
|
1457
1402
|
delete config.templates[name];
|
|
1458
1403
|
await saveConfig(config, cwd);
|
|
1459
|
-
console.log(
|
|
1460
|
-
console.log(
|
|
1404
|
+
console.log(chalk16.green(`\u2713 Removed template: ${name}`));
|
|
1405
|
+
console.log(chalk16.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
|
|
1461
1406
|
}
|
|
1462
1407
|
async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
1463
1408
|
const config = await loadConfig(cwd);
|
|
1464
|
-
const templatesDir =
|
|
1409
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1465
1410
|
let sourceFile;
|
|
1466
1411
|
if (config.templates?.[source]) {
|
|
1467
1412
|
sourceFile = config.templates[source];
|
|
1468
1413
|
} else {
|
|
1469
1414
|
sourceFile = source;
|
|
1470
1415
|
}
|
|
1471
|
-
const sourcePath =
|
|
1416
|
+
const sourcePath = path2.join(templatesDir, sourceFile);
|
|
1472
1417
|
try {
|
|
1473
|
-
await
|
|
1418
|
+
await fs9.access(sourcePath);
|
|
1474
1419
|
} catch {
|
|
1475
|
-
console.error(
|
|
1476
|
-
console.error(
|
|
1420
|
+
console.error(chalk16.red(`Source template not found: ${source}`));
|
|
1421
|
+
console.error(chalk16.gray(`Expected at: ${sourcePath}`));
|
|
1477
1422
|
process.exit(1);
|
|
1478
1423
|
}
|
|
1479
1424
|
const targetFile = target.endsWith(".md") ? target : `${target}.md`;
|
|
1480
|
-
const targetPath =
|
|
1481
|
-
await
|
|
1482
|
-
console.log(
|
|
1425
|
+
const targetPath = path2.join(templatesDir, targetFile);
|
|
1426
|
+
await fs9.copyFile(sourcePath, targetPath);
|
|
1427
|
+
console.log(chalk16.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
|
|
1483
1428
|
if (!config.templates) {
|
|
1484
1429
|
config.templates = {};
|
|
1485
1430
|
}
|
|
1486
1431
|
const templateName = target.replace(/\.md$/, "");
|
|
1487
1432
|
config.templates[templateName] = targetFile;
|
|
1488
1433
|
await saveConfig(config, cwd);
|
|
1489
|
-
console.log(
|
|
1490
|
-
console.log(
|
|
1491
|
-
console.log(
|
|
1434
|
+
console.log(chalk16.green(`\u2713 Registered template: ${templateName}`));
|
|
1435
|
+
console.log(chalk16.gray(` Edit: ${targetPath}`));
|
|
1436
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
|
|
1492
1437
|
}
|
|
1493
|
-
|
|
1494
|
-
// src/commands/init.ts
|
|
1495
|
-
import * as fs10 from "fs/promises";
|
|
1496
|
-
import * as path13 from "path";
|
|
1497
|
-
import { fileURLToPath } from "url";
|
|
1498
|
-
import chalk11 from "chalk";
|
|
1499
|
-
import { select } from "@inquirer/prompts";
|
|
1500
|
-
|
|
1501
|
-
// src/utils/template-helpers.ts
|
|
1502
|
-
import * as fs9 from "fs/promises";
|
|
1503
|
-
import * as path12 from "path";
|
|
1504
|
-
import chalk10 from "chalk";
|
|
1505
1438
|
async function detectExistingSystemPrompts(cwd) {
|
|
1506
1439
|
const commonFiles = [
|
|
1507
1440
|
"AGENTS.md",
|
|
@@ -1511,7 +1444,7 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1511
1444
|
const found = [];
|
|
1512
1445
|
for (const file of commonFiles) {
|
|
1513
1446
|
try {
|
|
1514
|
-
await fs9.access(
|
|
1447
|
+
await fs9.access(path2.join(cwd, file));
|
|
1515
1448
|
found.push(file);
|
|
1516
1449
|
} catch {
|
|
1517
1450
|
}
|
|
@@ -1520,8 +1453,8 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1520
1453
|
}
|
|
1521
1454
|
async function handleExistingFiles(action, existingFiles, templateDir, cwd, variables = {}) {
|
|
1522
1455
|
for (const file of existingFiles) {
|
|
1523
|
-
const filePath =
|
|
1524
|
-
const templateFilePath =
|
|
1456
|
+
const filePath = path2.join(cwd, file);
|
|
1457
|
+
const templateFilePath = path2.join(templateDir, "files", file);
|
|
1525
1458
|
try {
|
|
1526
1459
|
await fs9.access(templateFilePath);
|
|
1527
1460
|
} catch {
|
|
@@ -1533,7 +1466,7 @@ async function handleExistingFiles(action, existingFiles, templateDir, cwd, vari
|
|
|
1533
1466
|
for (const [key, value] of Object.entries(variables)) {
|
|
1534
1467
|
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1535
1468
|
}
|
|
1536
|
-
const promptPath =
|
|
1469
|
+
const promptPath = path2.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
1537
1470
|
const aiPrompt = `# AI Prompt: Consolidate AGENTS.md
|
|
1538
1471
|
|
|
1539
1472
|
## Task
|
|
@@ -1565,16 +1498,16 @@ Create a single consolidated AGENTS.md that:
|
|
|
1565
1498
|
- Maintains clear structure and readability
|
|
1566
1499
|
- Removes any duplicate or conflicting guidance
|
|
1567
1500
|
`;
|
|
1568
|
-
await fs9.mkdir(
|
|
1501
|
+
await fs9.mkdir(path2.dirname(promptPath), { recursive: true });
|
|
1569
1502
|
await fs9.writeFile(promptPath, aiPrompt, "utf-8");
|
|
1570
|
-
console.log(
|
|
1571
|
-
console.log(
|
|
1503
|
+
console.log(chalk16.green(`\u2713 Created AI consolidation prompt`));
|
|
1504
|
+
console.log(chalk16.cyan(` \u2192 ${promptPath}`));
|
|
1572
1505
|
console.log("");
|
|
1573
|
-
console.log(
|
|
1574
|
-
console.log(
|
|
1575
|
-
console.log(
|
|
1576
|
-
console.log(
|
|
1577
|
-
console.log(
|
|
1506
|
+
console.log(chalk16.yellow("\u{1F4DD} Next steps:"));
|
|
1507
|
+
console.log(chalk16.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
|
|
1508
|
+
console.log(chalk16.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
|
|
1509
|
+
console.log(chalk16.gray(" 3. Let AI create the consolidated AGENTS.md"));
|
|
1510
|
+
console.log(chalk16.gray(" 4. Review and commit the result"));
|
|
1578
1511
|
console.log("");
|
|
1579
1512
|
} else if (action === "merge-append" && file === "AGENTS.md") {
|
|
1580
1513
|
const existing = await fs9.readFile(filePath, "utf-8");
|
|
@@ -1590,19 +1523,19 @@ Create a single consolidated AGENTS.md that:
|
|
|
1590
1523
|
|
|
1591
1524
|
${template.split("\n").slice(1).join("\n")}`;
|
|
1592
1525
|
await fs9.writeFile(filePath, merged, "utf-8");
|
|
1593
|
-
console.log(
|
|
1594
|
-
console.log(
|
|
1526
|
+
console.log(chalk16.green(`\u2713 Appended LeanSpec section to ${file}`));
|
|
1527
|
+
console.log(chalk16.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
|
|
1595
1528
|
} else if (action === "overwrite") {
|
|
1596
1529
|
const backupPath = `${filePath}.backup`;
|
|
1597
1530
|
await fs9.rename(filePath, backupPath);
|
|
1598
|
-
console.log(
|
|
1531
|
+
console.log(chalk16.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
|
|
1599
1532
|
let content = await fs9.readFile(templateFilePath, "utf-8");
|
|
1600
1533
|
for (const [key, value] of Object.entries(variables)) {
|
|
1601
1534
|
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1602
1535
|
}
|
|
1603
1536
|
await fs9.writeFile(filePath, content, "utf-8");
|
|
1604
|
-
console.log(
|
|
1605
|
-
console.log(
|
|
1537
|
+
console.log(chalk16.green(`\u2713 Created new ${file}`));
|
|
1538
|
+
console.log(chalk16.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
|
|
1606
1539
|
}
|
|
1607
1540
|
}
|
|
1608
1541
|
}
|
|
@@ -1610,8 +1543,8 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1610
1543
|
await fs9.mkdir(dest, { recursive: true });
|
|
1611
1544
|
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
1612
1545
|
for (const entry of entries) {
|
|
1613
|
-
const srcPath =
|
|
1614
|
-
const destPath =
|
|
1546
|
+
const srcPath = path2.join(src, entry.name);
|
|
1547
|
+
const destPath = path2.join(dest, entry.name);
|
|
1615
1548
|
if (skipFiles.includes(entry.name)) {
|
|
1616
1549
|
continue;
|
|
1617
1550
|
}
|
|
@@ -1632,7 +1565,7 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1632
1565
|
}
|
|
1633
1566
|
async function getProjectName2(cwd) {
|
|
1634
1567
|
try {
|
|
1635
|
-
const packageJsonPath =
|
|
1568
|
+
const packageJsonPath = path2.join(cwd, "package.json");
|
|
1636
1569
|
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
1637
1570
|
const pkg = JSON.parse(content);
|
|
1638
1571
|
if (pkg.name) {
|
|
@@ -1640,23 +1573,23 @@ async function getProjectName2(cwd) {
|
|
|
1640
1573
|
}
|
|
1641
1574
|
} catch {
|
|
1642
1575
|
}
|
|
1643
|
-
return
|
|
1576
|
+
return path2.basename(cwd);
|
|
1644
1577
|
}
|
|
1645
1578
|
|
|
1646
1579
|
// src/commands/init.ts
|
|
1647
|
-
var
|
|
1648
|
-
var TEMPLATES_DIR =
|
|
1580
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
1581
|
+
var TEMPLATES_DIR = path2.join(__dirname, "..", "templates");
|
|
1649
1582
|
async function initProject() {
|
|
1650
1583
|
const cwd = process.cwd();
|
|
1651
1584
|
try {
|
|
1652
|
-
await
|
|
1653
|
-
console.log(
|
|
1654
|
-
console.log(
|
|
1585
|
+
await fs9.access(path2.join(cwd, ".lean-spec", "config.json"));
|
|
1586
|
+
console.log(chalk16.yellow("\u26A0 LeanSpec already initialized in this directory."));
|
|
1587
|
+
console.log(chalk16.gray("To reinitialize, delete .lean-spec/ directory first."));
|
|
1655
1588
|
return;
|
|
1656
1589
|
} catch {
|
|
1657
1590
|
}
|
|
1658
1591
|
console.log("");
|
|
1659
|
-
console.log(
|
|
1592
|
+
console.log(chalk16.green("Welcome to LeanSpec!"));
|
|
1660
1593
|
console.log("");
|
|
1661
1594
|
const setupMode = await select({
|
|
1662
1595
|
message: "How would you like to set up?",
|
|
@@ -1694,14 +1627,14 @@ async function initProject() {
|
|
|
1694
1627
|
]
|
|
1695
1628
|
});
|
|
1696
1629
|
}
|
|
1697
|
-
const templateDir =
|
|
1698
|
-
const templateConfigPath =
|
|
1630
|
+
const templateDir = path2.join(TEMPLATES_DIR, templateName);
|
|
1631
|
+
const templateConfigPath = path2.join(templateDir, "config.json");
|
|
1699
1632
|
let templateConfig;
|
|
1700
1633
|
try {
|
|
1701
|
-
const content = await
|
|
1634
|
+
const content = await fs9.readFile(templateConfigPath, "utf-8");
|
|
1702
1635
|
templateConfig = JSON.parse(content).config;
|
|
1703
1636
|
} catch {
|
|
1704
|
-
console.error(
|
|
1637
|
+
console.error(chalk16.red(`Error: Template not found: ${templateName}`));
|
|
1705
1638
|
process.exit(1);
|
|
1706
1639
|
}
|
|
1707
1640
|
let patternChoice = "simple";
|
|
@@ -1744,27 +1677,27 @@ async function initProject() {
|
|
|
1744
1677
|
templateConfig.structure.prefix = "{YYYYMMDD}-";
|
|
1745
1678
|
} else if (patternChoice === "custom") {
|
|
1746
1679
|
console.log("");
|
|
1747
|
-
console.log(
|
|
1748
|
-
console.log(
|
|
1749
|
-
console.log(
|
|
1680
|
+
console.log(chalk16.yellow("\u26A0 Custom pattern input is not yet implemented."));
|
|
1681
|
+
console.log(chalk16.gray(" You can manually edit .lean-spec/config.json after initialization."));
|
|
1682
|
+
console.log(chalk16.gray(" Using simple pattern for now."));
|
|
1750
1683
|
console.log("");
|
|
1751
1684
|
templateConfig.structure.pattern = "flat";
|
|
1752
1685
|
templateConfig.structure.prefix = "";
|
|
1753
1686
|
}
|
|
1754
|
-
const templatesDir =
|
|
1687
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1755
1688
|
try {
|
|
1756
|
-
await
|
|
1689
|
+
await fs9.mkdir(templatesDir, { recursive: true });
|
|
1757
1690
|
} catch (error) {
|
|
1758
|
-
console.error(
|
|
1691
|
+
console.error(chalk16.red("Error creating templates directory:"), error);
|
|
1759
1692
|
process.exit(1);
|
|
1760
1693
|
}
|
|
1761
|
-
const templateSpecPath =
|
|
1762
|
-
const targetSpecPath =
|
|
1694
|
+
const templateSpecPath = path2.join(templateDir, "spec-template.md");
|
|
1695
|
+
const targetSpecPath = path2.join(templatesDir, "spec-template.md");
|
|
1763
1696
|
try {
|
|
1764
|
-
await
|
|
1765
|
-
console.log(
|
|
1697
|
+
await fs9.copyFile(templateSpecPath, targetSpecPath);
|
|
1698
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/templates/spec-template.md"));
|
|
1766
1699
|
} catch (error) {
|
|
1767
|
-
console.error(
|
|
1700
|
+
console.error(chalk16.red("Error copying template:"), error);
|
|
1768
1701
|
process.exit(1);
|
|
1769
1702
|
}
|
|
1770
1703
|
templateConfig.template = "spec-template.md";
|
|
@@ -1772,12 +1705,12 @@ async function initProject() {
|
|
|
1772
1705
|
default: "spec-template.md"
|
|
1773
1706
|
};
|
|
1774
1707
|
await saveConfig(templateConfig, cwd);
|
|
1775
|
-
console.log(
|
|
1708
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/config.json"));
|
|
1776
1709
|
const existingFiles = await detectExistingSystemPrompts(cwd);
|
|
1777
1710
|
let skipFiles = [];
|
|
1778
1711
|
if (existingFiles.length > 0) {
|
|
1779
1712
|
console.log("");
|
|
1780
|
-
console.log(
|
|
1713
|
+
console.log(chalk16.yellow(`Found existing: ${existingFiles.join(", ")}`));
|
|
1781
1714
|
const action = await select({
|
|
1782
1715
|
message: "How would you like to handle existing AGENTS.md?",
|
|
1783
1716
|
choices: [
|
|
@@ -1810,33 +1743,28 @@ async function initProject() {
|
|
|
1810
1743
|
}
|
|
1811
1744
|
}
|
|
1812
1745
|
const projectName = await getProjectName2(cwd);
|
|
1813
|
-
const filesDir =
|
|
1746
|
+
const filesDir = path2.join(templateDir, "files");
|
|
1814
1747
|
try {
|
|
1815
1748
|
await copyDirectory(filesDir, cwd, skipFiles, { project_name: projectName });
|
|
1816
|
-
console.log(
|
|
1749
|
+
console.log(chalk16.green("\u2713 Initialized project structure"));
|
|
1817
1750
|
} catch (error) {
|
|
1818
|
-
console.error(
|
|
1751
|
+
console.error(chalk16.red("Error copying template files:"), error);
|
|
1819
1752
|
process.exit(1);
|
|
1820
1753
|
}
|
|
1821
1754
|
console.log("");
|
|
1822
|
-
console.log(
|
|
1755
|
+
console.log(chalk16.green("\u2713 LeanSpec initialized!"));
|
|
1823
1756
|
console.log("");
|
|
1824
1757
|
console.log("Next steps:");
|
|
1825
|
-
console.log(
|
|
1826
|
-
console.log(
|
|
1827
|
-
console.log(
|
|
1758
|
+
console.log(chalk16.gray(" - Review and customize AGENTS.md"));
|
|
1759
|
+
console.log(chalk16.gray(" - Check out example spec in specs/"));
|
|
1760
|
+
console.log(chalk16.gray(" - Create your first spec: lean-spec create my-feature"));
|
|
1828
1761
|
console.log("");
|
|
1829
1762
|
}
|
|
1830
|
-
|
|
1831
|
-
// src/commands/files.ts
|
|
1832
|
-
import * as fs11 from "fs/promises";
|
|
1833
|
-
import * as path14 from "path";
|
|
1834
|
-
import chalk12 from "chalk";
|
|
1835
1763
|
async function filesCommand(specPath, options = {}) {
|
|
1836
1764
|
await autoCheckIfEnabled();
|
|
1837
1765
|
const config = await loadConfig();
|
|
1838
1766
|
const cwd = process.cwd();
|
|
1839
|
-
const specsDir =
|
|
1767
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1840
1768
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1841
1769
|
if (!resolvedPath) {
|
|
1842
1770
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Try using the full path or spec name (e.g., 001-my-spec)`);
|
|
@@ -1847,12 +1775,12 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1847
1775
|
}
|
|
1848
1776
|
const subFiles = await loadSubFiles(spec.fullPath);
|
|
1849
1777
|
console.log("");
|
|
1850
|
-
console.log(
|
|
1778
|
+
console.log(chalk16.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
|
|
1851
1779
|
console.log("");
|
|
1852
|
-
console.log(
|
|
1853
|
-
const readmeStat = await
|
|
1780
|
+
console.log(chalk16.green("Required:"));
|
|
1781
|
+
const readmeStat = await fs9.stat(spec.filePath);
|
|
1854
1782
|
const readmeSize = formatSize(readmeStat.size);
|
|
1855
|
-
console.log(
|
|
1783
|
+
console.log(chalk16.green(` \u2713 README.md (${readmeSize}) Main spec`));
|
|
1856
1784
|
console.log("");
|
|
1857
1785
|
let filteredFiles = subFiles;
|
|
1858
1786
|
if (options.type === "docs") {
|
|
@@ -1861,25 +1789,25 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1861
1789
|
filteredFiles = subFiles.filter((f) => f.type === "asset");
|
|
1862
1790
|
}
|
|
1863
1791
|
if (filteredFiles.length === 0) {
|
|
1864
|
-
console.log(
|
|
1792
|
+
console.log(chalk16.gray("No additional files"));
|
|
1865
1793
|
console.log("");
|
|
1866
1794
|
return;
|
|
1867
1795
|
}
|
|
1868
1796
|
const documents = filteredFiles.filter((f) => f.type === "document");
|
|
1869
1797
|
const assets = filteredFiles.filter((f) => f.type === "asset");
|
|
1870
1798
|
if (documents.length > 0 && (!options.type || options.type === "docs")) {
|
|
1871
|
-
console.log(
|
|
1799
|
+
console.log(chalk16.cyan("Documents:"));
|
|
1872
1800
|
for (const file of documents) {
|
|
1873
1801
|
const size = formatSize(file.size);
|
|
1874
|
-
console.log(
|
|
1802
|
+
console.log(chalk16.cyan(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
1875
1803
|
}
|
|
1876
1804
|
console.log("");
|
|
1877
1805
|
}
|
|
1878
1806
|
if (assets.length > 0 && (!options.type || options.type === "assets")) {
|
|
1879
|
-
console.log(
|
|
1807
|
+
console.log(chalk16.yellow("Assets:"));
|
|
1880
1808
|
for (const file of assets) {
|
|
1881
1809
|
const size = formatSize(file.size);
|
|
1882
|
-
console.log(
|
|
1810
|
+
console.log(chalk16.yellow(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
1883
1811
|
}
|
|
1884
1812
|
console.log("");
|
|
1885
1813
|
}
|
|
@@ -1887,7 +1815,7 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1887
1815
|
const totalSize = formatSize(
|
|
1888
1816
|
readmeStat.size + filteredFiles.reduce((sum, f) => sum + f.size, 0)
|
|
1889
1817
|
);
|
|
1890
|
-
console.log(
|
|
1818
|
+
console.log(chalk16.gray(`Total: ${totalFiles} files, ${totalSize}`));
|
|
1891
1819
|
console.log("");
|
|
1892
1820
|
}
|
|
1893
1821
|
function formatSize(bytes) {
|
|
@@ -1899,55 +1827,6 @@ function formatSize(bytes) {
|
|
|
1899
1827
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1900
1828
|
}
|
|
1901
1829
|
}
|
|
1902
|
-
|
|
1903
|
-
// src/commands/validate.ts
|
|
1904
|
-
import * as fs12 from "fs/promises";
|
|
1905
|
-
import * as path16 from "path";
|
|
1906
|
-
import chalk14 from "chalk";
|
|
1907
|
-
|
|
1908
|
-
// src/validators/line-count.ts
|
|
1909
|
-
var LineCountValidator = class {
|
|
1910
|
-
name = "max-lines";
|
|
1911
|
-
description = "Enforce Context Economy: specs must be <400 lines";
|
|
1912
|
-
maxLines;
|
|
1913
|
-
warningThreshold;
|
|
1914
|
-
constructor(options = {}) {
|
|
1915
|
-
this.maxLines = options.maxLines ?? 400;
|
|
1916
|
-
this.warningThreshold = options.warningThreshold ?? 300;
|
|
1917
|
-
}
|
|
1918
|
-
validate(_spec, content) {
|
|
1919
|
-
const lines = content.split("\n").length;
|
|
1920
|
-
if (lines > this.maxLines) {
|
|
1921
|
-
return {
|
|
1922
|
-
passed: false,
|
|
1923
|
-
errors: [{
|
|
1924
|
-
message: `Spec exceeds ${this.maxLines} lines (${lines} lines)`,
|
|
1925
|
-
suggestion: "Consider splitting into sub-specs using spec 012 pattern"
|
|
1926
|
-
}],
|
|
1927
|
-
warnings: []
|
|
1928
|
-
};
|
|
1929
|
-
}
|
|
1930
|
-
if (lines > this.warningThreshold) {
|
|
1931
|
-
return {
|
|
1932
|
-
passed: true,
|
|
1933
|
-
errors: [],
|
|
1934
|
-
warnings: [{
|
|
1935
|
-
message: `Spec approaching limit (${lines}/${this.maxLines} lines)`,
|
|
1936
|
-
suggestion: "Consider simplification or splitting"
|
|
1937
|
-
}]
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
return {
|
|
1941
|
-
passed: true,
|
|
1942
|
-
errors: [],
|
|
1943
|
-
warnings: []
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
};
|
|
1947
|
-
|
|
1948
|
-
// src/validators/frontmatter.ts
|
|
1949
|
-
import matter2 from "gray-matter";
|
|
1950
|
-
import yaml2 from "js-yaml";
|
|
1951
1830
|
var FrontmatterValidator = class {
|
|
1952
1831
|
name = "frontmatter";
|
|
1953
1832
|
description = "Validate spec frontmatter for required fields and valid values";
|
|
@@ -1962,9 +1841,9 @@ var FrontmatterValidator = class {
|
|
|
1962
1841
|
const warnings = [];
|
|
1963
1842
|
let parsed;
|
|
1964
1843
|
try {
|
|
1965
|
-
parsed =
|
|
1844
|
+
parsed = matter4(content, {
|
|
1966
1845
|
engines: {
|
|
1967
|
-
yaml: (str) =>
|
|
1846
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
1968
1847
|
}
|
|
1969
1848
|
});
|
|
1970
1849
|
} catch (error) {
|
|
@@ -2078,9 +1957,6 @@ var FrontmatterValidator = class {
|
|
|
2078
1957
|
return { valid: true };
|
|
2079
1958
|
}
|
|
2080
1959
|
};
|
|
2081
|
-
|
|
2082
|
-
// src/validators/structure.ts
|
|
2083
|
-
import matter3 from "gray-matter";
|
|
2084
1960
|
var StructureValidator = class {
|
|
2085
1961
|
name = "structure";
|
|
2086
1962
|
description = "Validate spec structure and required sections";
|
|
@@ -2095,7 +1971,7 @@ var StructureValidator = class {
|
|
|
2095
1971
|
const warnings = [];
|
|
2096
1972
|
let parsed;
|
|
2097
1973
|
try {
|
|
2098
|
-
parsed =
|
|
1974
|
+
parsed = matter4(content);
|
|
2099
1975
|
} catch (error) {
|
|
2100
1976
|
errors.push({
|
|
2101
1977
|
message: "Failed to parse frontmatter",
|
|
@@ -2112,33 +1988,6 @@ var StructureValidator = class {
|
|
|
2112
1988
|
});
|
|
2113
1989
|
}
|
|
2114
1990
|
const headings = this.extractHeadings(body);
|
|
2115
|
-
for (const requiredSection of this.requiredSections) {
|
|
2116
|
-
const found = headings.some(
|
|
2117
|
-
(h) => h.level === 2 && h.text.toLowerCase() === requiredSection.toLowerCase()
|
|
2118
|
-
);
|
|
2119
|
-
if (!found) {
|
|
2120
|
-
if (this.strict) {
|
|
2121
|
-
errors.push({
|
|
2122
|
-
message: `Missing required section: ## ${requiredSection}`,
|
|
2123
|
-
suggestion: `Add ## ${requiredSection} section to the spec`
|
|
2124
|
-
});
|
|
2125
|
-
} else {
|
|
2126
|
-
warnings.push({
|
|
2127
|
-
message: `Recommended section missing: ## ${requiredSection}`,
|
|
2128
|
-
suggestion: `Consider adding ## ${requiredSection} section`
|
|
2129
|
-
});
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
const emptySections = this.findEmptySections(body, headings);
|
|
2134
|
-
for (const section of emptySections) {
|
|
2135
|
-
if (this.requiredSections.some((req) => req.toLowerCase() === section.toLowerCase())) {
|
|
2136
|
-
warnings.push({
|
|
2137
|
-
message: `Empty required section: ## ${section}`,
|
|
2138
|
-
suggestion: "Add content to this section or remove it"
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
1991
|
const duplicates = this.findDuplicateHeaders(headings);
|
|
2143
1992
|
for (const dup of duplicates) {
|
|
2144
1993
|
errors.push({
|
|
@@ -2424,226 +2273,1307 @@ var CorruptionValidator = class {
|
|
|
2424
2273
|
return errors;
|
|
2425
2274
|
}
|
|
2426
2275
|
};
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
maxLines;
|
|
2434
|
-
warningThreshold;
|
|
2435
|
-
checkCrossReferences;
|
|
2436
|
-
constructor(options = {}) {
|
|
2437
|
-
this.maxLines = options.maxLines ?? 400;
|
|
2438
|
-
this.warningThreshold = options.warningThreshold ?? 300;
|
|
2439
|
-
this.checkCrossReferences = options.checkCrossReferences ?? true;
|
|
2276
|
+
function normalizeDateFields2(data) {
|
|
2277
|
+
const dateFields = ["created", "completed", "updated", "due"];
|
|
2278
|
+
for (const field of dateFields) {
|
|
2279
|
+
if (data[field] instanceof Date) {
|
|
2280
|
+
data[field] = data[field].toISOString().split("T")[0];
|
|
2281
|
+
}
|
|
2440
2282
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2283
|
+
}
|
|
2284
|
+
function enrichWithTimestamps(data, previousData) {
|
|
2285
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2286
|
+
if (!data.created_at) {
|
|
2287
|
+
data.created_at = now;
|
|
2288
|
+
}
|
|
2289
|
+
if (previousData) {
|
|
2290
|
+
data.updated_at = now;
|
|
2291
|
+
}
|
|
2292
|
+
if (data.status === "complete" && previousData?.status !== "complete" && !data.completed_at) {
|
|
2293
|
+
data.completed_at = now;
|
|
2294
|
+
if (!data.completed) {
|
|
2295
|
+
data.completed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2448
2296
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
await this.validateCrossReferences(subSpecs, spec, warnings);
|
|
2297
|
+
}
|
|
2298
|
+
if (previousData && data.status !== previousData.status) {
|
|
2299
|
+
if (!Array.isArray(data.transitions)) {
|
|
2300
|
+
data.transitions = [];
|
|
2454
2301
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
};
|
|
2302
|
+
data.transitions.push({
|
|
2303
|
+
status: data.status,
|
|
2304
|
+
at: now
|
|
2305
|
+
});
|
|
2460
2306
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
if (baseName !== baseName.toUpperCase()) {
|
|
2469
|
-
warnings.push({
|
|
2470
|
-
message: `Sub-spec filename should be uppercase: ${subSpec.name}`,
|
|
2471
|
-
suggestion: `Consider renaming to ${baseName.toUpperCase()}.md`
|
|
2472
|
-
});
|
|
2307
|
+
}
|
|
2308
|
+
function normalizeTagsField(data) {
|
|
2309
|
+
if (data.tags && typeof data.tags === "string") {
|
|
2310
|
+
try {
|
|
2311
|
+
const parsed = JSON.parse(data.tags);
|
|
2312
|
+
if (Array.isArray(parsed)) {
|
|
2313
|
+
data.tags = parsed;
|
|
2473
2314
|
}
|
|
2315
|
+
} catch {
|
|
2316
|
+
data.tags = data.tags.split(",").map((t) => t.trim());
|
|
2474
2317
|
}
|
|
2475
2318
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2319
|
+
}
|
|
2320
|
+
function validateCustomFields(frontmatter, config) {
|
|
2321
|
+
{
|
|
2322
|
+
return frontmatter;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function parseFrontmatterFromString(content, filePath, config) {
|
|
2326
|
+
try {
|
|
2327
|
+
const parsed = matter4(content, {
|
|
2328
|
+
engines: {
|
|
2329
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
2483
2330
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2331
|
+
});
|
|
2332
|
+
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
2333
|
+
return parseFallbackFields(content);
|
|
2334
|
+
}
|
|
2335
|
+
if (!parsed.data.status) {
|
|
2336
|
+
if (filePath) ;
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
if (!parsed.data.created) {
|
|
2340
|
+
if (filePath) ;
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
const validStatuses = ["planned", "in-progress", "complete", "archived"];
|
|
2344
|
+
if (!validStatuses.includes(parsed.data.status)) {
|
|
2345
|
+
if (filePath) ;
|
|
2346
|
+
}
|
|
2347
|
+
if (parsed.data.priority) {
|
|
2348
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
2349
|
+
if (!validPriorities.includes(parsed.data.priority)) {
|
|
2350
|
+
if (filePath) ;
|
|
2495
2351
|
}
|
|
2496
2352
|
}
|
|
2353
|
+
normalizeTagsField(parsed.data);
|
|
2354
|
+
const knownFields = [
|
|
2355
|
+
"status",
|
|
2356
|
+
"created",
|
|
2357
|
+
"tags",
|
|
2358
|
+
"priority",
|
|
2359
|
+
"related",
|
|
2360
|
+
"depends_on",
|
|
2361
|
+
"updated",
|
|
2362
|
+
"completed",
|
|
2363
|
+
"assignee",
|
|
2364
|
+
"reviewer",
|
|
2365
|
+
"issue",
|
|
2366
|
+
"pr",
|
|
2367
|
+
"epic",
|
|
2368
|
+
"breaking",
|
|
2369
|
+
"due",
|
|
2370
|
+
"created_at",
|
|
2371
|
+
"updated_at",
|
|
2372
|
+
"completed_at",
|
|
2373
|
+
"transitions"
|
|
2374
|
+
];
|
|
2375
|
+
const customFields = config?.frontmatter?.custom ? Object.keys(config.frontmatter.custom) : [];
|
|
2376
|
+
const allKnownFields = [...knownFields, ...customFields];
|
|
2377
|
+
const unknownFields = Object.keys(parsed.data).filter((k) => !allKnownFields.includes(k));
|
|
2378
|
+
if (unknownFields.length > 0 && filePath) ;
|
|
2379
|
+
const validatedData = validateCustomFields(parsed.data, config);
|
|
2380
|
+
return validatedData;
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
console.error(`Error parsing frontmatter${""}:`, error);
|
|
2383
|
+
return null;
|
|
2497
2384
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2385
|
+
}
|
|
2386
|
+
function parseFallbackFields(content) {
|
|
2387
|
+
const statusMatch = content.match(/\*\*Status\*\*:\s*(?:📅\s*)?(\w+(?:-\w+)?)/i);
|
|
2388
|
+
const createdMatch = content.match(/\*\*Created\*\*:\s*(\d{4}-\d{2}-\d{2})/);
|
|
2389
|
+
if (statusMatch && createdMatch) {
|
|
2390
|
+
const status = statusMatch[1].toLowerCase().replace(/\s+/g, "-");
|
|
2391
|
+
const created = createdMatch[1];
|
|
2392
|
+
return {
|
|
2393
|
+
status,
|
|
2394
|
+
created
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
function createUpdatedFrontmatter(existingContent, updates) {
|
|
2400
|
+
const parsed = matter4(existingContent, {
|
|
2401
|
+
engines: {
|
|
2402
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
2513
2403
|
}
|
|
2404
|
+
});
|
|
2405
|
+
const previousData = { ...parsed.data };
|
|
2406
|
+
const newData = { ...parsed.data, ...updates };
|
|
2407
|
+
normalizeDateFields2(newData);
|
|
2408
|
+
enrichWithTimestamps(newData, previousData);
|
|
2409
|
+
if (updates.status === "complete" && !newData.completed) {
|
|
2410
|
+
newData.completed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2411
|
+
}
|
|
2412
|
+
if ("updated" in parsed.data) {
|
|
2413
|
+
newData.updated = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2414
|
+
}
|
|
2415
|
+
let updatedContent = parsed.content;
|
|
2416
|
+
updatedContent = updateVisualMetadata(updatedContent, newData);
|
|
2417
|
+
const newContent = matter4.stringify(updatedContent, newData);
|
|
2418
|
+
return {
|
|
2419
|
+
content: newContent,
|
|
2420
|
+
frontmatter: newData
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
function updateVisualMetadata(content, frontmatter) {
|
|
2424
|
+
const statusEmoji = getStatusEmojiPlain(frontmatter.status);
|
|
2425
|
+
const statusLabel = frontmatter.status.charAt(0).toUpperCase() + frontmatter.status.slice(1).replace("-", " ");
|
|
2426
|
+
const created = frontmatter.created;
|
|
2427
|
+
let metadataLine = `> **Status**: ${statusEmoji} ${statusLabel}`;
|
|
2428
|
+
if (frontmatter.priority) {
|
|
2429
|
+
const priorityLabel = frontmatter.priority.charAt(0).toUpperCase() + frontmatter.priority.slice(1);
|
|
2430
|
+
metadataLine += ` \xB7 **Priority**: ${priorityLabel}`;
|
|
2514
2431
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2432
|
+
metadataLine += ` \xB7 **Created**: ${created}`;
|
|
2433
|
+
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
2434
|
+
metadataLine += ` \xB7 **Tags**: ${frontmatter.tags.join(", ")}`;
|
|
2435
|
+
}
|
|
2436
|
+
let secondLine = "";
|
|
2437
|
+
if (frontmatter.assignee || frontmatter.reviewer) {
|
|
2438
|
+
const assignee = frontmatter.assignee || "TBD";
|
|
2439
|
+
const reviewer = frontmatter.reviewer || "TBD";
|
|
2440
|
+
secondLine = `
|
|
2441
|
+
> **Assignee**: ${assignee} \xB7 **Reviewer**: ${reviewer}`;
|
|
2442
|
+
}
|
|
2443
|
+
const metadataPattern = /^>\s+\*\*Status\*\*:.*(?:\n>\s+\*\*Assignee\*\*:.*)?/m;
|
|
2444
|
+
if (metadataPattern.test(content)) {
|
|
2445
|
+
return content.replace(metadataPattern, metadataLine + secondLine);
|
|
2446
|
+
} else {
|
|
2447
|
+
const titleMatch = content.match(/^#\s+.+$/m);
|
|
2448
|
+
if (titleMatch) {
|
|
2449
|
+
const insertPos = titleMatch.index + titleMatch[0].length;
|
|
2450
|
+
return content.slice(0, insertPos) + "\n\n" + metadataLine + secondLine + "\n" + content.slice(insertPos);
|
|
2536
2451
|
}
|
|
2537
2452
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2453
|
+
return content;
|
|
2454
|
+
}
|
|
2455
|
+
function getStatusEmojiPlain(status) {
|
|
2456
|
+
switch (status) {
|
|
2457
|
+
case "planned":
|
|
2458
|
+
return "\u{1F4C5}";
|
|
2459
|
+
case "in-progress":
|
|
2460
|
+
return "\u23F3";
|
|
2461
|
+
case "complete":
|
|
2462
|
+
return "\u2705";
|
|
2463
|
+
case "archived":
|
|
2464
|
+
return "\u{1F4E6}";
|
|
2465
|
+
default:
|
|
2466
|
+
return "\u{1F4C4}";
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
function parseMarkdownSections(content) {
|
|
2470
|
+
const lines = content.split("\n");
|
|
2471
|
+
const sections = [];
|
|
2472
|
+
const sectionStack = [];
|
|
2473
|
+
let inCodeBlock = false;
|
|
2474
|
+
let currentLineNum = 1;
|
|
2475
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2476
|
+
const line = lines[i];
|
|
2477
|
+
currentLineNum = i + 1;
|
|
2478
|
+
if (line.trimStart().startsWith("```")) {
|
|
2479
|
+
inCodeBlock = !inCodeBlock;
|
|
2480
|
+
continue;
|
|
2547
2481
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
for (const { spec, validatorName, result } of results) {
|
|
2551
|
-
for (const error of result.errors) {
|
|
2552
|
-
addIssue(spec.filePath, {
|
|
2553
|
-
severity: "error",
|
|
2554
|
-
message: error.message,
|
|
2555
|
-
suggestion: error.suggestion,
|
|
2556
|
-
ruleName: validatorName,
|
|
2557
|
-
filePath: spec.filePath,
|
|
2558
|
-
spec
|
|
2559
|
-
}, spec);
|
|
2482
|
+
if (inCodeBlock) {
|
|
2483
|
+
continue;
|
|
2560
2484
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2485
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
2486
|
+
if (headingMatch) {
|
|
2487
|
+
const level = headingMatch[1].length;
|
|
2488
|
+
const title = headingMatch[2].trim();
|
|
2489
|
+
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
|
|
2490
|
+
const closedSection = sectionStack.pop();
|
|
2491
|
+
closedSection.endLine = currentLineNum - 1;
|
|
2492
|
+
closedSection.lineCount = closedSection.endLine - closedSection.startLine + 1;
|
|
2493
|
+
}
|
|
2494
|
+
const newSection = {
|
|
2495
|
+
title,
|
|
2496
|
+
level,
|
|
2497
|
+
startLine: currentLineNum,
|
|
2498
|
+
endLine: lines.length,
|
|
2499
|
+
// Will be updated when section closes
|
|
2500
|
+
lineCount: 0,
|
|
2501
|
+
// Will be calculated when section closes
|
|
2502
|
+
subsections: []
|
|
2503
|
+
};
|
|
2504
|
+
if (sectionStack.length > 0) {
|
|
2505
|
+
sectionStack[sectionStack.length - 1].subsections.push(newSection);
|
|
2506
|
+
} else {
|
|
2507
|
+
sections.push(newSection);
|
|
2508
|
+
}
|
|
2509
|
+
sectionStack.push(newSection);
|
|
2570
2510
|
}
|
|
2571
2511
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
return a.severity === "error" ? -1 : 1;
|
|
2577
|
-
});
|
|
2578
|
-
fileResults.push({ filePath, issues: data.issues, spec: data.spec });
|
|
2512
|
+
while (sectionStack.length > 0) {
|
|
2513
|
+
const closedSection = sectionStack.pop();
|
|
2514
|
+
closedSection.endLine = lines.length;
|
|
2515
|
+
closedSection.lineCount = closedSection.endLine - closedSection.startLine + 1;
|
|
2579
2516
|
}
|
|
2580
|
-
|
|
2581
|
-
if (a.spec?.name && b.spec?.name) {
|
|
2582
|
-
return a.spec.name.localeCompare(b.spec.name);
|
|
2583
|
-
}
|
|
2584
|
-
return a.filePath.localeCompare(b.filePath);
|
|
2585
|
-
});
|
|
2586
|
-
return fileResults;
|
|
2517
|
+
return sections;
|
|
2587
2518
|
}
|
|
2588
|
-
function
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
const specsIndex = filePath.indexOf("/specs/");
|
|
2594
|
-
return filePath.substring(specsIndex + 1);
|
|
2519
|
+
function flattenSections(sections) {
|
|
2520
|
+
const result = [];
|
|
2521
|
+
for (const section of sections) {
|
|
2522
|
+
result.push(section);
|
|
2523
|
+
result.push(...flattenSections(section.subsections));
|
|
2595
2524
|
}
|
|
2596
|
-
return
|
|
2525
|
+
return result;
|
|
2597
2526
|
}
|
|
2598
|
-
function
|
|
2599
|
-
const lines =
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
if (isMainSpec && fileResult.spec) {
|
|
2603
|
-
const specName = fileResult.spec.name;
|
|
2604
|
-
const status = fileResult.spec.frontmatter.status;
|
|
2605
|
-
const priority = fileResult.spec.frontmatter.priority || "medium";
|
|
2606
|
-
const statusBadge = formatStatusBadge(status);
|
|
2607
|
-
const priorityBadge = formatPriorityBadge(priority);
|
|
2608
|
-
lines.push(chalk13.bold.cyan(`${specName} ${statusBadge} ${priorityBadge}`));
|
|
2609
|
-
} else {
|
|
2610
|
-
lines.push(chalk13.cyan.underline(relativePath));
|
|
2527
|
+
function extractLines(content, startLine, endLine) {
|
|
2528
|
+
const lines = content.split("\n");
|
|
2529
|
+
if (startLine < 1 || endLine < startLine || startLine > lines.length || endLine > lines.length) {
|
|
2530
|
+
throw new Error(`Invalid line range: ${startLine}-${endLine}`);
|
|
2611
2531
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
}
|
|
2532
|
+
const extracted = lines.slice(startLine - 1, endLine);
|
|
2533
|
+
return extracted.join("\n");
|
|
2534
|
+
}
|
|
2535
|
+
function removeLines(content, startLine, endLine) {
|
|
2536
|
+
const lines = content.split("\n");
|
|
2537
|
+
if (startLine < 1 || endLine < startLine || startLine > lines.length) {
|
|
2538
|
+
throw new Error(`Invalid line range: ${startLine}-${endLine}`);
|
|
2620
2539
|
}
|
|
2621
|
-
lines.
|
|
2540
|
+
lines.splice(startLine - 1, endLine - startLine + 1);
|
|
2622
2541
|
return lines.join("\n");
|
|
2623
2542
|
}
|
|
2624
|
-
function
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2543
|
+
function countLines(content) {
|
|
2544
|
+
return content.split("\n").length;
|
|
2545
|
+
}
|
|
2546
|
+
function analyzeMarkdownStructure(content) {
|
|
2547
|
+
const lines = content.split("\n");
|
|
2548
|
+
const sections = parseMarkdownSections(content);
|
|
2549
|
+
const allSections = flattenSections(sections);
|
|
2550
|
+
const levelCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, total: 0 };
|
|
2551
|
+
for (const section of allSections) {
|
|
2552
|
+
levelCounts[`h${section.level}`]++;
|
|
2553
|
+
levelCounts.total++;
|
|
2554
|
+
}
|
|
2555
|
+
let codeBlocks = 0;
|
|
2556
|
+
let inCodeBlock = false;
|
|
2557
|
+
for (const line of lines) {
|
|
2558
|
+
if (line.trimStart().startsWith("```")) {
|
|
2559
|
+
if (!inCodeBlock) {
|
|
2560
|
+
codeBlocks++;
|
|
2561
|
+
}
|
|
2562
|
+
inCodeBlock = !inCodeBlock;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
let maxNesting = 0;
|
|
2566
|
+
function calculateNesting(secs, depth) {
|
|
2567
|
+
for (const section of secs) {
|
|
2568
|
+
maxNesting = Math.max(maxNesting, depth);
|
|
2569
|
+
calculateNesting(section.subsections, depth + 1);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
calculateNesting(sections, 1);
|
|
2573
|
+
return {
|
|
2574
|
+
lines: lines.length,
|
|
2575
|
+
sections,
|
|
2576
|
+
allSections,
|
|
2577
|
+
sectionsByLevel: levelCounts,
|
|
2578
|
+
codeBlocks,
|
|
2579
|
+
maxNesting
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
var TokenCounter = class {
|
|
2583
|
+
encoding;
|
|
2584
|
+
constructor() {
|
|
2585
|
+
this.encoding = encoding_for_model("gpt-4");
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Clean up resources (important to prevent memory leaks)
|
|
2589
|
+
*/
|
|
2590
|
+
dispose() {
|
|
2591
|
+
this.encoding.free();
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Count tokens in a string
|
|
2595
|
+
*/
|
|
2596
|
+
countString(text) {
|
|
2597
|
+
const tokens = this.encoding.encode(text);
|
|
2598
|
+
return tokens.length;
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Count tokens in content (convenience method for analyze command)
|
|
2602
|
+
* Alias for countString - provided for clarity in command usage
|
|
2603
|
+
*/
|
|
2604
|
+
async countTokensInContent(content) {
|
|
2605
|
+
return this.countString(content);
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Count tokens in a single file
|
|
2609
|
+
*/
|
|
2610
|
+
async countFile(filePath, options = {}) {
|
|
2611
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
2612
|
+
const tokens = this.countString(content);
|
|
2613
|
+
const lines = content.split("\n").length;
|
|
2614
|
+
const result = {
|
|
2615
|
+
total: tokens,
|
|
2616
|
+
files: [{
|
|
2617
|
+
path: filePath,
|
|
2618
|
+
tokens,
|
|
2619
|
+
lines
|
|
2620
|
+
}]
|
|
2621
|
+
};
|
|
2622
|
+
if (options.detailed) {
|
|
2623
|
+
result.breakdown = await this.analyzeBreakdown(content);
|
|
2624
|
+
}
|
|
2625
|
+
return result;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Count tokens in a spec (including sub-specs if requested)
|
|
2629
|
+
*/
|
|
2630
|
+
async countSpec(specPath, options = {}) {
|
|
2631
|
+
const stats = await fs9.stat(specPath);
|
|
2632
|
+
if (stats.isFile()) {
|
|
2633
|
+
return this.countFile(specPath, options);
|
|
2634
|
+
}
|
|
2635
|
+
const files = await fs9.readdir(specPath);
|
|
2636
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
2637
|
+
const filesToCount = [];
|
|
2638
|
+
if (mdFiles.includes("README.md")) {
|
|
2639
|
+
filesToCount.push("README.md");
|
|
2640
|
+
}
|
|
2641
|
+
if (options.includeSubSpecs) {
|
|
2642
|
+
mdFiles.forEach((f) => {
|
|
2643
|
+
if (f !== "README.md") {
|
|
2644
|
+
filesToCount.push(f);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
const fileCounts = [];
|
|
2649
|
+
let totalTokens = 0;
|
|
2650
|
+
let totalBreakdown;
|
|
2651
|
+
if (options.detailed) {
|
|
2652
|
+
totalBreakdown = {
|
|
2653
|
+
code: 0,
|
|
2654
|
+
prose: 0,
|
|
2655
|
+
tables: 0,
|
|
2656
|
+
frontmatter: 0
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
for (const file of filesToCount) {
|
|
2660
|
+
const filePath = path2.join(specPath, file);
|
|
2661
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
2662
|
+
const tokens = this.countString(content);
|
|
2663
|
+
const lines = content.split("\n").length;
|
|
2664
|
+
fileCounts.push({
|
|
2665
|
+
path: file,
|
|
2666
|
+
tokens,
|
|
2667
|
+
lines
|
|
2668
|
+
});
|
|
2669
|
+
totalTokens += tokens;
|
|
2670
|
+
if (options.detailed && totalBreakdown) {
|
|
2671
|
+
const breakdown = await this.analyzeBreakdown(content);
|
|
2672
|
+
totalBreakdown.code += breakdown.code;
|
|
2673
|
+
totalBreakdown.prose += breakdown.prose;
|
|
2674
|
+
totalBreakdown.tables += breakdown.tables;
|
|
2675
|
+
totalBreakdown.frontmatter += breakdown.frontmatter;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return {
|
|
2679
|
+
total: totalTokens,
|
|
2680
|
+
files: fileCounts,
|
|
2681
|
+
breakdown: totalBreakdown
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Analyze token breakdown by content type
|
|
2686
|
+
*/
|
|
2687
|
+
async analyzeBreakdown(content) {
|
|
2688
|
+
const breakdown = {
|
|
2689
|
+
code: 0,
|
|
2690
|
+
prose: 0,
|
|
2691
|
+
tables: 0,
|
|
2692
|
+
frontmatter: 0
|
|
2693
|
+
};
|
|
2694
|
+
let body = content;
|
|
2695
|
+
let frontmatterContent = "";
|
|
2696
|
+
try {
|
|
2697
|
+
const parsed = matter4(content);
|
|
2698
|
+
body = parsed.content;
|
|
2699
|
+
frontmatterContent = parsed.matter;
|
|
2700
|
+
breakdown.frontmatter = this.countString(frontmatterContent);
|
|
2701
|
+
} catch {
|
|
2702
|
+
}
|
|
2703
|
+
let inCodeBlock = false;
|
|
2704
|
+
let inTable = false;
|
|
2705
|
+
const lines = body.split("\n");
|
|
2706
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2707
|
+
const line = lines[i];
|
|
2708
|
+
const trimmed = line.trim();
|
|
2709
|
+
if (trimmed.startsWith("```")) {
|
|
2710
|
+
inCodeBlock = !inCodeBlock;
|
|
2711
|
+
breakdown.code += this.countString(line + "\n");
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
if (inCodeBlock) {
|
|
2715
|
+
breakdown.code += this.countString(line + "\n");
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
const isTableSeparator = trimmed.includes("|") && /[-:]{3,}/.test(trimmed);
|
|
2719
|
+
const isTableRow = trimmed.includes("|") && trimmed.startsWith("|");
|
|
2720
|
+
if (isTableSeparator || inTable && isTableRow) {
|
|
2721
|
+
inTable = true;
|
|
2722
|
+
breakdown.tables += this.countString(line + "\n");
|
|
2723
|
+
continue;
|
|
2724
|
+
} else if (inTable && !isTableRow) {
|
|
2725
|
+
inTable = false;
|
|
2726
|
+
}
|
|
2727
|
+
breakdown.prose += this.countString(line + "\n");
|
|
2728
|
+
}
|
|
2729
|
+
return breakdown;
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Check if content fits within token limit
|
|
2733
|
+
*/
|
|
2734
|
+
isWithinLimit(count, limit) {
|
|
2735
|
+
return count.total <= limit;
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Format token count for display
|
|
2739
|
+
*/
|
|
2740
|
+
formatCount(count, verbose = false) {
|
|
2741
|
+
if (!verbose) {
|
|
2742
|
+
return `${count.total.toLocaleString()} tokens`;
|
|
2743
|
+
}
|
|
2744
|
+
const lines = [
|
|
2745
|
+
`Total: ${count.total.toLocaleString()} tokens`,
|
|
2746
|
+
"",
|
|
2747
|
+
"Files:"
|
|
2748
|
+
];
|
|
2749
|
+
for (const file of count.files) {
|
|
2750
|
+
const lineInfo = file.lines ? ` (${file.lines} lines)` : "";
|
|
2751
|
+
lines.push(` ${file.path}: ${file.tokens.toLocaleString()} tokens${lineInfo}`);
|
|
2752
|
+
}
|
|
2753
|
+
if (count.breakdown) {
|
|
2754
|
+
const b = count.breakdown;
|
|
2755
|
+
const total = b.code + b.prose + b.tables + b.frontmatter;
|
|
2756
|
+
lines.push("");
|
|
2757
|
+
lines.push("Content Breakdown:");
|
|
2758
|
+
lines.push(` Prose: ${b.prose.toLocaleString()} tokens (${Math.round(b.prose / total * 100)}%)`);
|
|
2759
|
+
lines.push(` Code: ${b.code.toLocaleString()} tokens (${Math.round(b.code / total * 100)}%)`);
|
|
2760
|
+
lines.push(` Tables: ${b.tables.toLocaleString()} tokens (${Math.round(b.tables / total * 100)}%)`);
|
|
2761
|
+
lines.push(` Frontmatter: ${b.frontmatter.toLocaleString()} tokens (${Math.round(b.frontmatter / total * 100)}%)`);
|
|
2762
|
+
}
|
|
2763
|
+
return lines.join("\n");
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Get performance indicators based on token count
|
|
2767
|
+
* Based on research from spec 066
|
|
2768
|
+
*/
|
|
2769
|
+
getPerformanceIndicators(tokenCount) {
|
|
2770
|
+
const baselineTokens = 1200;
|
|
2771
|
+
const costMultiplier = Math.round(tokenCount / baselineTokens * 10) / 10;
|
|
2772
|
+
if (tokenCount < 2e3) {
|
|
2773
|
+
return {
|
|
2774
|
+
level: "excellent",
|
|
2775
|
+
costMultiplier,
|
|
2776
|
+
effectiveness: 100,
|
|
2777
|
+
recommendation: "Optimal size for Context Economy"
|
|
2778
|
+
};
|
|
2779
|
+
} else if (tokenCount < 3500) {
|
|
2780
|
+
return {
|
|
2781
|
+
level: "good",
|
|
2782
|
+
costMultiplier,
|
|
2783
|
+
effectiveness: 95,
|
|
2784
|
+
recommendation: "Good size, no action needed"
|
|
2785
|
+
};
|
|
2786
|
+
} else if (tokenCount < 5e3) {
|
|
2787
|
+
return {
|
|
2788
|
+
level: "warning",
|
|
2789
|
+
costMultiplier,
|
|
2790
|
+
effectiveness: 85,
|
|
2791
|
+
recommendation: "Consider simplification or sub-specs"
|
|
2792
|
+
};
|
|
2793
|
+
} else {
|
|
2794
|
+
return {
|
|
2795
|
+
level: "problem",
|
|
2796
|
+
costMultiplier,
|
|
2797
|
+
effectiveness: 70,
|
|
2798
|
+
recommendation: "Should split - elevated token count"
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
async function countTokens(input, options) {
|
|
2804
|
+
const counter = new TokenCounter();
|
|
2805
|
+
try {
|
|
2806
|
+
if (typeof input === "string") {
|
|
2807
|
+
return {
|
|
2808
|
+
total: counter.countString(input),
|
|
2809
|
+
files: []
|
|
2810
|
+
};
|
|
2811
|
+
} else if ("content" in input) {
|
|
2812
|
+
return {
|
|
2813
|
+
total: counter.countString(input.content),
|
|
2814
|
+
files: []
|
|
2815
|
+
};
|
|
2816
|
+
} else if ("filePath" in input) {
|
|
2817
|
+
return await counter.countFile(input.filePath, options);
|
|
2818
|
+
} else if ("specPath" in input) {
|
|
2819
|
+
return await counter.countSpec(input.specPath, options);
|
|
2820
|
+
}
|
|
2821
|
+
throw new Error("Invalid input type");
|
|
2822
|
+
} finally {
|
|
2823
|
+
counter.dispose();
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
var ComplexityValidator = class {
|
|
2827
|
+
name = "complexity";
|
|
2828
|
+
description = "Direct token threshold validation with independent structure checks";
|
|
2829
|
+
excellentThreshold;
|
|
2830
|
+
goodThreshold;
|
|
2831
|
+
warningThreshold;
|
|
2832
|
+
maxLines;
|
|
2833
|
+
warningLines;
|
|
2834
|
+
constructor(options = {}) {
|
|
2835
|
+
this.excellentThreshold = options.excellentThreshold ?? 2e3;
|
|
2836
|
+
this.goodThreshold = options.goodThreshold ?? 3500;
|
|
2837
|
+
this.warningThreshold = options.warningThreshold ?? 5e3;
|
|
2838
|
+
this.maxLines = options.maxLines ?? 500;
|
|
2839
|
+
this.warningLines = options.warningLines ?? 400;
|
|
2840
|
+
}
|
|
2841
|
+
async validate(spec, content) {
|
|
2842
|
+
const errors = [];
|
|
2843
|
+
const warnings = [];
|
|
2844
|
+
const metrics = await this.analyzeComplexity(content, spec);
|
|
2845
|
+
const tokenValidation = this.validateTokens(metrics.tokenCount);
|
|
2846
|
+
if (tokenValidation.level === "error") {
|
|
2847
|
+
errors.push({
|
|
2848
|
+
message: tokenValidation.message,
|
|
2849
|
+
suggestion: "Consider splitting for Context Economy (attention and cognitive load)"
|
|
2850
|
+
});
|
|
2851
|
+
} else if (tokenValidation.level === "warning") {
|
|
2852
|
+
warnings.push({
|
|
2853
|
+
message: tokenValidation.message,
|
|
2854
|
+
suggestion: "Consider simplification or splitting into sub-specs"
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
const structureChecks = this.checkStructure(metrics);
|
|
2858
|
+
for (const check of structureChecks) {
|
|
2859
|
+
if (!check.passed && check.message) {
|
|
2860
|
+
warnings.push({
|
|
2861
|
+
message: check.message,
|
|
2862
|
+
suggestion: check.suggestion
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return {
|
|
2867
|
+
passed: errors.length === 0,
|
|
2868
|
+
errors,
|
|
2869
|
+
warnings
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Validate token count with direct thresholds
|
|
2874
|
+
*/
|
|
2875
|
+
validateTokens(tokens) {
|
|
2876
|
+
if (tokens > this.warningThreshold) {
|
|
2877
|
+
return {
|
|
2878
|
+
level: "error",
|
|
2879
|
+
message: `Spec has ${tokens.toLocaleString()} tokens (threshold: ${this.warningThreshold.toLocaleString()}) - should split`
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
if (tokens > this.goodThreshold) {
|
|
2883
|
+
return {
|
|
2884
|
+
level: "warning",
|
|
2885
|
+
message: `Spec has ${tokens.toLocaleString()} tokens (threshold: ${this.goodThreshold.toLocaleString()})`
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
if (tokens > this.excellentThreshold) {
|
|
2889
|
+
return {
|
|
2890
|
+
level: "info",
|
|
2891
|
+
message: `Spec has ${tokens.toLocaleString()} tokens - acceptable, watch for growth`
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
level: "excellent",
|
|
2896
|
+
message: `Spec has ${tokens.toLocaleString()} tokens - excellent`
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Check structure quality independently
|
|
2901
|
+
*/
|
|
2902
|
+
checkStructure(metrics) {
|
|
2903
|
+
const checks = [];
|
|
2904
|
+
if (metrics.hasSubSpecs) {
|
|
2905
|
+
if (metrics.tokenCount > this.excellentThreshold) {
|
|
2906
|
+
checks.push({
|
|
2907
|
+
passed: true,
|
|
2908
|
+
message: `Uses ${metrics.subSpecCount} sub-spec file${metrics.subSpecCount > 1 ? "s" : ""} for progressive disclosure`
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
} else if (metrics.tokenCount > this.goodThreshold) {
|
|
2912
|
+
checks.push({
|
|
2913
|
+
passed: false,
|
|
2914
|
+
message: "Consider using sub-spec files (DESIGN.md, IMPLEMENTATION.md, etc.)",
|
|
2915
|
+
suggestion: "Progressive disclosure reduces cognitive load for large specs"
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
if (metrics.sectionCount >= 15 && metrics.sectionCount <= 35) {
|
|
2919
|
+
if (metrics.tokenCount > this.excellentThreshold) {
|
|
2920
|
+
checks.push({
|
|
2921
|
+
passed: true,
|
|
2922
|
+
message: `Good sectioning (${metrics.sectionCount} sections) enables cognitive chunking`
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
} else if (metrics.sectionCount < 8 && metrics.lineCount > 200) {
|
|
2926
|
+
checks.push({
|
|
2927
|
+
passed: false,
|
|
2928
|
+
message: `Only ${metrics.sectionCount} sections - too monolithic`,
|
|
2929
|
+
suggestion: "Break into 15-35 sections for better readability (7\xB12 cognitive chunks)"
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
if (metrics.codeBlockCount > 20) {
|
|
2933
|
+
checks.push({
|
|
2934
|
+
passed: false,
|
|
2935
|
+
message: `High code block density (${metrics.codeBlockCount} blocks)`,
|
|
2936
|
+
suggestion: "Consider moving examples to separate files or sub-specs"
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
return checks;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Analyze complexity metrics from spec content
|
|
2943
|
+
*/
|
|
2944
|
+
async analyzeComplexity(content, spec) {
|
|
2945
|
+
let body;
|
|
2946
|
+
try {
|
|
2947
|
+
const parsed = matter4(content);
|
|
2948
|
+
body = parsed.content;
|
|
2949
|
+
} catch {
|
|
2950
|
+
body = content;
|
|
2951
|
+
}
|
|
2952
|
+
const lines = content.split("\n");
|
|
2953
|
+
const lineCount = lines.length;
|
|
2954
|
+
let sectionCount = 0;
|
|
2955
|
+
let inCodeBlock = false;
|
|
2956
|
+
for (const line of lines) {
|
|
2957
|
+
if (line.trim().startsWith("```")) {
|
|
2958
|
+
inCodeBlock = !inCodeBlock;
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
if (!inCodeBlock && line.match(/^#{2,4}\s/)) {
|
|
2962
|
+
sectionCount++;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
const codeBlockCount = Math.floor((content.match(/```/g) || []).length / 2);
|
|
2966
|
+
const listItemCount = lines.filter((line) => line.match(/^[\s]*[-*]\s/) || line.match(/^[\s]*\d+\.\s/)).length;
|
|
2967
|
+
const tableCount = lines.filter((line) => line.includes("|") && line.match(/[-:]{3,}/)).length;
|
|
2968
|
+
const counter = new TokenCounter();
|
|
2969
|
+
const tokenCount = counter.countString(content);
|
|
2970
|
+
counter.dispose();
|
|
2971
|
+
let hasSubSpecs = false;
|
|
2972
|
+
let subSpecCount = 0;
|
|
2973
|
+
try {
|
|
2974
|
+
const specDir = path2.dirname(spec.filePath);
|
|
2975
|
+
const files = await fs9.readdir(specDir);
|
|
2976
|
+
const mdFiles = files.filter(
|
|
2977
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
2978
|
+
);
|
|
2979
|
+
hasSubSpecs = mdFiles.length > 0;
|
|
2980
|
+
subSpecCount = mdFiles.length;
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
hasSubSpecs = /\b(DESIGN|IMPLEMENTATION|TESTING|CONFIGURATION|API|MIGRATION)\.md\b/.test(content);
|
|
2983
|
+
const subSpecMatches = content.match(/\b[A-Z-]+\.md\b/g) || [];
|
|
2984
|
+
const uniqueSubSpecs = new Set(subSpecMatches.filter((m) => m !== "README.md"));
|
|
2985
|
+
subSpecCount = uniqueSubSpecs.size;
|
|
2986
|
+
}
|
|
2987
|
+
const averageSectionLength = sectionCount > 0 ? Math.round(lineCount / sectionCount) : 0;
|
|
2988
|
+
return {
|
|
2989
|
+
lineCount,
|
|
2990
|
+
tokenCount,
|
|
2991
|
+
sectionCount,
|
|
2992
|
+
codeBlockCount,
|
|
2993
|
+
listItemCount,
|
|
2994
|
+
tableCount,
|
|
2995
|
+
hasSubSpecs,
|
|
2996
|
+
subSpecCount,
|
|
2997
|
+
averageSectionLength
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
};
|
|
3001
|
+
var FIELD_WEIGHTS = {
|
|
3002
|
+
title: 100,
|
|
3003
|
+
name: 70,
|
|
3004
|
+
tags: 70,
|
|
3005
|
+
description: 50,
|
|
3006
|
+
content: 10
|
|
3007
|
+
};
|
|
3008
|
+
function calculateMatchScore(match, queryTerms, totalMatches, matchPosition) {
|
|
3009
|
+
let score = FIELD_WEIGHTS[match.field];
|
|
3010
|
+
match.text.toLowerCase();
|
|
3011
|
+
const hasExactMatch = queryTerms.some((term) => {
|
|
3012
|
+
const regex = new RegExp(`\\b${escapeRegex(term)}\\b`, "i");
|
|
3013
|
+
return regex.test(match.text);
|
|
3014
|
+
});
|
|
3015
|
+
if (hasExactMatch) {
|
|
3016
|
+
score *= 2;
|
|
3017
|
+
}
|
|
3018
|
+
const positionBonus = Math.max(1, 1.5 - matchPosition * 0.1);
|
|
3019
|
+
score *= positionBonus;
|
|
3020
|
+
const frequencyFactor = Math.min(1, 3 / totalMatches);
|
|
3021
|
+
score *= frequencyFactor;
|
|
3022
|
+
return Math.min(100, score * 10);
|
|
3023
|
+
}
|
|
3024
|
+
function calculateSpecScore(matches) {
|
|
3025
|
+
if (matches.length === 0) return 0;
|
|
3026
|
+
const fieldScores = {};
|
|
3027
|
+
for (const match of matches) {
|
|
3028
|
+
const field = match.field;
|
|
3029
|
+
const currentScore = fieldScores[field] || 0;
|
|
3030
|
+
fieldScores[field] = Math.max(currentScore, match.score);
|
|
3031
|
+
}
|
|
3032
|
+
let totalScore = 0;
|
|
3033
|
+
let totalWeight = 0;
|
|
3034
|
+
for (const [field, score] of Object.entries(fieldScores)) {
|
|
3035
|
+
const weight = FIELD_WEIGHTS[field] || 1;
|
|
3036
|
+
totalScore += score * weight;
|
|
3037
|
+
totalWeight += weight;
|
|
3038
|
+
}
|
|
3039
|
+
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0;
|
|
3040
|
+
}
|
|
3041
|
+
function containsAllTerms(text, queryTerms) {
|
|
3042
|
+
const textLower = text.toLowerCase();
|
|
3043
|
+
return queryTerms.every((term) => textLower.includes(term));
|
|
3044
|
+
}
|
|
3045
|
+
function countOccurrences(text, queryTerms) {
|
|
3046
|
+
const textLower = text.toLowerCase();
|
|
3047
|
+
let count = 0;
|
|
3048
|
+
for (const term of queryTerms) {
|
|
3049
|
+
const regex = new RegExp(escapeRegex(term), "gi");
|
|
3050
|
+
const matches = textLower.match(regex);
|
|
3051
|
+
count += matches ? matches.length : 0;
|
|
3052
|
+
}
|
|
3053
|
+
return count;
|
|
3054
|
+
}
|
|
3055
|
+
function escapeRegex(str) {
|
|
3056
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3057
|
+
}
|
|
3058
|
+
function findMatchPositions(text, queryTerms) {
|
|
3059
|
+
const positions = [];
|
|
3060
|
+
const textLower = text.toLowerCase();
|
|
3061
|
+
for (const term of queryTerms) {
|
|
3062
|
+
const termLower = term.toLowerCase();
|
|
3063
|
+
let index = 0;
|
|
3064
|
+
while ((index = textLower.indexOf(termLower, index)) !== -1) {
|
|
3065
|
+
positions.push([index, index + term.length]);
|
|
3066
|
+
index += term.length;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
positions.sort((a, b) => a[0] - b[0]);
|
|
3070
|
+
const merged = [];
|
|
3071
|
+
for (const pos of positions) {
|
|
3072
|
+
if (merged.length === 0) {
|
|
3073
|
+
merged.push(pos);
|
|
3074
|
+
} else {
|
|
3075
|
+
const last = merged[merged.length - 1];
|
|
3076
|
+
if (pos[0] <= last[1]) {
|
|
3077
|
+
last[1] = Math.max(last[1], pos[1]);
|
|
3078
|
+
} else {
|
|
3079
|
+
merged.push(pos);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return merged;
|
|
3084
|
+
}
|
|
3085
|
+
function extractContext(text, matchIndex, queryTerms, contextLength = 80) {
|
|
3086
|
+
const lines = text.split("\n");
|
|
3087
|
+
const matchLine = lines[matchIndex] || "";
|
|
3088
|
+
if (matchLine.length <= contextLength * 2) {
|
|
3089
|
+
const highlights2 = findMatchPositions(matchLine, queryTerms);
|
|
3090
|
+
return { text: matchLine, highlights: highlights2 };
|
|
3091
|
+
}
|
|
3092
|
+
const matchLineLower = matchLine.toLowerCase();
|
|
3093
|
+
let firstMatchPos = matchLine.length;
|
|
3094
|
+
for (const term of queryTerms) {
|
|
3095
|
+
const pos = matchLineLower.indexOf(term.toLowerCase());
|
|
3096
|
+
if (pos !== -1 && pos < firstMatchPos) {
|
|
3097
|
+
firstMatchPos = pos;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
const start = Math.max(0, firstMatchPos - contextLength);
|
|
3101
|
+
const end = Math.min(matchLine.length, firstMatchPos + contextLength);
|
|
3102
|
+
let contextText = matchLine.substring(start, end);
|
|
3103
|
+
if (start > 0) contextText = "..." + contextText;
|
|
3104
|
+
if (end < matchLine.length) contextText = contextText + "...";
|
|
3105
|
+
const highlights = findMatchPositions(contextText, queryTerms);
|
|
3106
|
+
return { text: contextText, highlights };
|
|
3107
|
+
}
|
|
3108
|
+
function extractSmartContext(text, matchIndex, queryTerms, contextLength = 80) {
|
|
3109
|
+
const lines = text.split("\n");
|
|
3110
|
+
const matchLine = lines[matchIndex] || "";
|
|
3111
|
+
if (matchLine.length <= contextLength * 2) {
|
|
3112
|
+
return extractContext(text, matchIndex, queryTerms, contextLength);
|
|
3113
|
+
}
|
|
3114
|
+
const matchLineLower = matchLine.toLowerCase();
|
|
3115
|
+
let firstMatchPos = matchLine.length;
|
|
3116
|
+
for (const term of queryTerms) {
|
|
3117
|
+
const pos = matchLineLower.indexOf(term.toLowerCase());
|
|
3118
|
+
if (pos !== -1 && pos < firstMatchPos) {
|
|
3119
|
+
firstMatchPos = pos;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
let start = Math.max(0, firstMatchPos - contextLength);
|
|
3123
|
+
let end = Math.min(matchLine.length, firstMatchPos + contextLength);
|
|
3124
|
+
const beforeText = matchLine.substring(0, start);
|
|
3125
|
+
const lastSentence = beforeText.lastIndexOf(". ");
|
|
3126
|
+
if (lastSentence !== -1 && start - lastSentence < 20) {
|
|
3127
|
+
start = lastSentence + 2;
|
|
3128
|
+
}
|
|
3129
|
+
const afterText = matchLine.substring(end);
|
|
3130
|
+
const nextSentence = afterText.indexOf(". ");
|
|
3131
|
+
if (nextSentence !== -1 && nextSentence < 20) {
|
|
3132
|
+
end = end + nextSentence + 1;
|
|
3133
|
+
}
|
|
3134
|
+
let contextText = matchLine.substring(start, end);
|
|
3135
|
+
if (start > 0) contextText = "..." + contextText;
|
|
3136
|
+
if (end < matchLine.length) contextText = contextText + "...";
|
|
3137
|
+
const highlights = findMatchPositions(contextText, queryTerms);
|
|
3138
|
+
return { text: contextText, highlights };
|
|
3139
|
+
}
|
|
3140
|
+
function deduplicateMatches(matches, minDistance = 3) {
|
|
3141
|
+
if (matches.length === 0) return matches;
|
|
3142
|
+
const sorted = [...matches].sort((a, b) => {
|
|
3143
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
3144
|
+
return (a.lineNumber || 0) - (b.lineNumber || 0);
|
|
3145
|
+
});
|
|
3146
|
+
const deduplicated = [];
|
|
3147
|
+
const usedLines = /* @__PURE__ */ new Set();
|
|
3148
|
+
for (const match of sorted) {
|
|
3149
|
+
if (match.field !== "content") {
|
|
3150
|
+
deduplicated.push(match);
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
const lineNum = match.lineNumber || 0;
|
|
3154
|
+
let tooClose = false;
|
|
3155
|
+
for (let i = lineNum - minDistance; i <= lineNum + minDistance; i++) {
|
|
3156
|
+
if (usedLines.has(i)) {
|
|
3157
|
+
tooClose = true;
|
|
3158
|
+
break;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
if (!tooClose) {
|
|
3162
|
+
deduplicated.push(match);
|
|
3163
|
+
usedLines.add(lineNum);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return deduplicated.sort((a, b) => {
|
|
3167
|
+
const fieldOrder = { title: 0, name: 1, tags: 2, description: 3, content: 4 };
|
|
3168
|
+
const orderA = fieldOrder[a.field];
|
|
3169
|
+
const orderB = fieldOrder[b.field];
|
|
3170
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
3171
|
+
return b.score - a.score;
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
function limitMatches(matches, maxMatches = 5) {
|
|
3175
|
+
if (matches.length <= maxMatches) return matches;
|
|
3176
|
+
const fieldMatches = {
|
|
3177
|
+
title: [],
|
|
3178
|
+
name: [],
|
|
3179
|
+
tags: [],
|
|
3180
|
+
description: [],
|
|
3181
|
+
content: []
|
|
3182
|
+
};
|
|
3183
|
+
for (const match of matches) {
|
|
3184
|
+
fieldMatches[match.field].push(match);
|
|
3185
|
+
}
|
|
3186
|
+
const nonContent = [
|
|
3187
|
+
...fieldMatches.title,
|
|
3188
|
+
...fieldMatches.name,
|
|
3189
|
+
...fieldMatches.tags,
|
|
3190
|
+
...fieldMatches.description
|
|
3191
|
+
];
|
|
3192
|
+
const contentMatches = fieldMatches.content.sort((a, b) => b.score - a.score).slice(0, Math.max(0, maxMatches - nonContent.length));
|
|
3193
|
+
return [...nonContent, ...contentMatches];
|
|
3194
|
+
}
|
|
3195
|
+
function searchSpecs(query, specs, options = {}) {
|
|
3196
|
+
const startTime = Date.now();
|
|
3197
|
+
const queryTerms = query.trim().toLowerCase().split(/\s+/).filter((term) => term.length > 0);
|
|
3198
|
+
if (queryTerms.length === 0) {
|
|
3199
|
+
return {
|
|
3200
|
+
results: [],
|
|
3201
|
+
metadata: {
|
|
3202
|
+
totalResults: 0,
|
|
3203
|
+
searchTime: Date.now() - startTime,
|
|
3204
|
+
query,
|
|
3205
|
+
specsSearched: specs.length
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
const maxMatchesPerSpec = options.maxMatchesPerSpec || 5;
|
|
3210
|
+
const contextLength = options.contextLength || 80;
|
|
3211
|
+
const results = [];
|
|
3212
|
+
for (const spec of specs) {
|
|
3213
|
+
const matches = searchSpec(spec, queryTerms, contextLength);
|
|
3214
|
+
if (matches.length > 0) {
|
|
3215
|
+
let processedMatches = deduplicateMatches(matches, 3);
|
|
3216
|
+
processedMatches = limitMatches(processedMatches, maxMatchesPerSpec);
|
|
3217
|
+
const score = calculateSpecScore(processedMatches);
|
|
3218
|
+
results.push({
|
|
3219
|
+
spec: specToSearchResult(spec),
|
|
3220
|
+
score,
|
|
3221
|
+
totalMatches: matches.length,
|
|
3222
|
+
matches: processedMatches
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
results.sort((a, b) => b.score - a.score);
|
|
3227
|
+
return {
|
|
3228
|
+
results,
|
|
3229
|
+
metadata: {
|
|
3230
|
+
totalResults: results.length,
|
|
3231
|
+
searchTime: Date.now() - startTime,
|
|
3232
|
+
query,
|
|
3233
|
+
specsSearched: specs.length
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
function searchSpec(spec, queryTerms, contextLength) {
|
|
3238
|
+
const matches = [];
|
|
3239
|
+
if (spec.title && containsAllTerms(spec.title, queryTerms)) {
|
|
3240
|
+
const occurrences = countOccurrences(spec.title, queryTerms);
|
|
3241
|
+
const highlights = findMatchPositions(spec.title, queryTerms);
|
|
3242
|
+
const score = calculateMatchScore(
|
|
3243
|
+
{ field: "title", text: spec.title },
|
|
3244
|
+
queryTerms,
|
|
3245
|
+
1,
|
|
3246
|
+
0
|
|
3247
|
+
);
|
|
3248
|
+
matches.push({
|
|
3249
|
+
field: "title",
|
|
3250
|
+
text: spec.title,
|
|
3251
|
+
score,
|
|
3252
|
+
highlights,
|
|
3253
|
+
occurrences
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
if (spec.name && containsAllTerms(spec.name, queryTerms)) {
|
|
3257
|
+
const occurrences = countOccurrences(spec.name, queryTerms);
|
|
3258
|
+
const highlights = findMatchPositions(spec.name, queryTerms);
|
|
3259
|
+
const score = calculateMatchScore(
|
|
3260
|
+
{ field: "name", text: spec.name },
|
|
3261
|
+
queryTerms,
|
|
3262
|
+
1,
|
|
3263
|
+
0
|
|
3264
|
+
);
|
|
3265
|
+
matches.push({
|
|
3266
|
+
field: "name",
|
|
3267
|
+
text: spec.name,
|
|
3268
|
+
score,
|
|
3269
|
+
highlights,
|
|
3270
|
+
occurrences
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
if (spec.tags && spec.tags.length > 0) {
|
|
3274
|
+
for (const tag of spec.tags) {
|
|
3275
|
+
if (containsAllTerms(tag, queryTerms)) {
|
|
3276
|
+
const occurrences = countOccurrences(tag, queryTerms);
|
|
3277
|
+
const highlights = findMatchPositions(tag, queryTerms);
|
|
3278
|
+
const score = calculateMatchScore(
|
|
3279
|
+
{ field: "tags", text: tag },
|
|
3280
|
+
queryTerms,
|
|
3281
|
+
spec.tags.length,
|
|
3282
|
+
spec.tags.indexOf(tag)
|
|
3283
|
+
);
|
|
3284
|
+
matches.push({
|
|
3285
|
+
field: "tags",
|
|
3286
|
+
text: tag,
|
|
3287
|
+
score,
|
|
3288
|
+
highlights,
|
|
3289
|
+
occurrences
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
if (spec.description && containsAllTerms(spec.description, queryTerms)) {
|
|
3295
|
+
const occurrences = countOccurrences(spec.description, queryTerms);
|
|
3296
|
+
const highlights = findMatchPositions(spec.description, queryTerms);
|
|
3297
|
+
const score = calculateMatchScore(
|
|
3298
|
+
{ field: "description", text: spec.description },
|
|
3299
|
+
queryTerms,
|
|
3300
|
+
1,
|
|
3301
|
+
0
|
|
3302
|
+
);
|
|
3303
|
+
matches.push({
|
|
3304
|
+
field: "description",
|
|
3305
|
+
text: spec.description,
|
|
3306
|
+
score,
|
|
3307
|
+
highlights,
|
|
3308
|
+
occurrences
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
if (spec.content) {
|
|
3312
|
+
const contentMatches = searchContent(
|
|
3313
|
+
spec.content,
|
|
3314
|
+
queryTerms,
|
|
3315
|
+
contextLength
|
|
3316
|
+
);
|
|
3317
|
+
matches.push(...contentMatches);
|
|
3318
|
+
}
|
|
3319
|
+
return matches;
|
|
3320
|
+
}
|
|
3321
|
+
function searchContent(content, queryTerms, contextLength) {
|
|
3322
|
+
const matches = [];
|
|
3323
|
+
const lines = content.split("\n");
|
|
3324
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3325
|
+
const line = lines[i];
|
|
3326
|
+
if (containsAllTerms(line, queryTerms)) {
|
|
3327
|
+
const occurrences = countOccurrences(line, queryTerms);
|
|
3328
|
+
const { text, highlights } = extractSmartContext(
|
|
3329
|
+
content,
|
|
3330
|
+
i,
|
|
3331
|
+
queryTerms,
|
|
3332
|
+
contextLength
|
|
3333
|
+
);
|
|
3334
|
+
const score = calculateMatchScore(
|
|
3335
|
+
{ field: "content", text: line },
|
|
3336
|
+
queryTerms,
|
|
3337
|
+
lines.length,
|
|
3338
|
+
i
|
|
3339
|
+
);
|
|
3340
|
+
matches.push({
|
|
3341
|
+
field: "content",
|
|
3342
|
+
text,
|
|
3343
|
+
lineNumber: i + 1,
|
|
3344
|
+
// 1-based line numbers
|
|
3345
|
+
score,
|
|
3346
|
+
highlights,
|
|
3347
|
+
occurrences
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return matches;
|
|
3352
|
+
}
|
|
3353
|
+
function specToSearchResult(spec) {
|
|
3354
|
+
return {
|
|
3355
|
+
name: spec.name,
|
|
3356
|
+
path: spec.path,
|
|
3357
|
+
status: spec.status,
|
|
3358
|
+
priority: spec.priority,
|
|
3359
|
+
tags: spec.tags,
|
|
3360
|
+
title: spec.title,
|
|
3361
|
+
description: spec.description
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// src/validators/sub-spec.ts
|
|
3366
|
+
var SubSpecValidator = class {
|
|
3367
|
+
name = "sub-specs";
|
|
3368
|
+
description = "Validate sub-spec files using direct token thresholds (spec 071)";
|
|
3369
|
+
excellentThreshold;
|
|
3370
|
+
goodThreshold;
|
|
3371
|
+
warningThreshold;
|
|
3372
|
+
maxLines;
|
|
3373
|
+
constructor(options = {}) {
|
|
3374
|
+
this.excellentThreshold = options.excellentThreshold ?? 2e3;
|
|
3375
|
+
this.goodThreshold = options.goodThreshold ?? 3500;
|
|
3376
|
+
this.warningThreshold = options.warningThreshold ?? 5e3;
|
|
3377
|
+
this.maxLines = options.maxLines ?? 500;
|
|
3378
|
+
}
|
|
3379
|
+
async validate(spec, content) {
|
|
3380
|
+
const errors = [];
|
|
3381
|
+
const warnings = [];
|
|
3382
|
+
const subFiles = await loadSubFiles(spec.fullPath, { includeContent: true });
|
|
3383
|
+
const subSpecs = subFiles.filter((f) => f.type === "document");
|
|
3384
|
+
if (subSpecs.length === 0) {
|
|
3385
|
+
return { passed: true, errors, warnings };
|
|
3386
|
+
}
|
|
3387
|
+
this.validateNamingConventions(subSpecs, warnings);
|
|
3388
|
+
await this.validateComplexity(subSpecs, errors, warnings);
|
|
3389
|
+
this.checkOrphanedSubSpecs(subSpecs, content, warnings);
|
|
3390
|
+
return {
|
|
3391
|
+
passed: errors.length === 0,
|
|
3392
|
+
errors,
|
|
3393
|
+
warnings
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* Validate sub-spec naming conventions
|
|
3398
|
+
* Convention: Uppercase filenames (e.g., DESIGN.md, TESTING.md, IMPLEMENTATION.md)
|
|
3399
|
+
*/
|
|
3400
|
+
validateNamingConventions(subSpecs, warnings) {
|
|
3401
|
+
for (const subSpec of subSpecs) {
|
|
3402
|
+
const baseName = path2.basename(subSpec.name, ".md");
|
|
3403
|
+
if (baseName !== baseName.toUpperCase()) {
|
|
3404
|
+
warnings.push({
|
|
3405
|
+
message: `Sub-spec filename should be uppercase: ${subSpec.name}`,
|
|
3406
|
+
suggestion: `Consider renaming to ${baseName.toUpperCase()}.md`
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Validate complexity for each sub-spec file using direct token thresholds
|
|
3413
|
+
* Same approach as ComplexityValidator (spec 071)
|
|
3414
|
+
*/
|
|
3415
|
+
async validateComplexity(subSpecs, errors, warnings) {
|
|
3416
|
+
for (const subSpec of subSpecs) {
|
|
3417
|
+
if (!subSpec.content) {
|
|
3418
|
+
continue;
|
|
3419
|
+
}
|
|
3420
|
+
const lines = subSpec.content.split("\n");
|
|
3421
|
+
const lineCount = lines.length;
|
|
3422
|
+
let sectionCount = 0;
|
|
3423
|
+
let inCodeBlock = false;
|
|
3424
|
+
for (const line of lines) {
|
|
3425
|
+
if (line.trim().startsWith("```")) {
|
|
3426
|
+
inCodeBlock = !inCodeBlock;
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
if (!inCodeBlock && line.match(/^#{2,4}\s/)) {
|
|
3430
|
+
sectionCount++;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
const tokenResult = await countTokens(subSpec.content);
|
|
3434
|
+
const tokenCount = tokenResult.total;
|
|
3435
|
+
if (tokenCount > this.warningThreshold) {
|
|
3436
|
+
errors.push({
|
|
3437
|
+
message: `Sub-spec ${subSpec.name} has ${tokenCount.toLocaleString()} tokens (threshold: ${this.warningThreshold.toLocaleString()}) - should split`,
|
|
3438
|
+
suggestion: "Consider splitting for Context Economy (attention and cognitive load)"
|
|
3439
|
+
});
|
|
3440
|
+
} else if (tokenCount > this.goodThreshold) {
|
|
3441
|
+
warnings.push({
|
|
3442
|
+
message: `Sub-spec ${subSpec.name} has ${tokenCount.toLocaleString()} tokens (threshold: ${this.goodThreshold.toLocaleString()})`,
|
|
3443
|
+
suggestion: "Consider simplification or further splitting"
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
if (sectionCount < 8 && lineCount > 200) {
|
|
3447
|
+
warnings.push({
|
|
3448
|
+
message: `Sub-spec ${subSpec.name} has only ${sectionCount} sections - too monolithic`,
|
|
3449
|
+
suggestion: "Break into 15-35 sections for better readability (7\xB12 cognitive chunks)"
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Check for orphaned sub-specs not referenced in README.md
|
|
3456
|
+
*/
|
|
3457
|
+
checkOrphanedSubSpecs(subSpecs, readmeContent, warnings) {
|
|
3458
|
+
for (const subSpec of subSpecs) {
|
|
3459
|
+
const fileName = subSpec.name;
|
|
3460
|
+
const escapedFileName = fileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3461
|
+
const linkPattern = new RegExp(`\\[([^\\]]+)\\]\\((?:\\.\\/)?${escapedFileName}\\)`, "gi");
|
|
3462
|
+
const isReferenced = linkPattern.test(readmeContent);
|
|
3463
|
+
if (!isReferenced) {
|
|
3464
|
+
warnings.push({
|
|
3465
|
+
message: `Orphaned sub-spec: ${fileName} (not linked from README.md)`,
|
|
3466
|
+
suggestion: `Add a link to ${fileName} in README.md to document its purpose`
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
};
|
|
3472
|
+
function groupIssuesByFile(results) {
|
|
3473
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
3474
|
+
const addIssue = (filePath, issue, spec) => {
|
|
3475
|
+
if (!fileMap.has(filePath)) {
|
|
3476
|
+
fileMap.set(filePath, { issues: [], spec });
|
|
3477
|
+
}
|
|
3478
|
+
fileMap.get(filePath).issues.push(issue);
|
|
3479
|
+
};
|
|
3480
|
+
for (const { spec, validatorName, result } of results) {
|
|
3481
|
+
for (const error of result.errors) {
|
|
3482
|
+
addIssue(spec.filePath, {
|
|
3483
|
+
severity: "error",
|
|
3484
|
+
message: error.message,
|
|
3485
|
+
suggestion: error.suggestion,
|
|
3486
|
+
ruleName: validatorName,
|
|
3487
|
+
filePath: spec.filePath,
|
|
3488
|
+
spec
|
|
3489
|
+
}, spec);
|
|
3490
|
+
}
|
|
3491
|
+
for (const warning of result.warnings) {
|
|
3492
|
+
addIssue(spec.filePath, {
|
|
3493
|
+
severity: "warning",
|
|
3494
|
+
message: warning.message,
|
|
3495
|
+
suggestion: warning.suggestion,
|
|
3496
|
+
ruleName: validatorName,
|
|
3497
|
+
filePath: spec.filePath,
|
|
3498
|
+
spec
|
|
3499
|
+
}, spec);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
const fileResults = [];
|
|
3503
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
3504
|
+
data.issues.sort((a, b) => {
|
|
3505
|
+
if (a.severity === b.severity) return 0;
|
|
3506
|
+
return a.severity === "error" ? -1 : 1;
|
|
3507
|
+
});
|
|
3508
|
+
fileResults.push({ filePath, issues: data.issues, spec: data.spec });
|
|
3509
|
+
}
|
|
3510
|
+
fileResults.sort((a, b) => {
|
|
3511
|
+
if (a.spec?.name && b.spec?.name) {
|
|
3512
|
+
return a.spec.name.localeCompare(b.spec.name);
|
|
3513
|
+
}
|
|
3514
|
+
return a.filePath.localeCompare(b.filePath);
|
|
3515
|
+
});
|
|
3516
|
+
return fileResults;
|
|
3517
|
+
}
|
|
3518
|
+
function normalizeFilePath(filePath) {
|
|
3519
|
+
const cwd = process.cwd();
|
|
3520
|
+
if (filePath.startsWith(cwd)) {
|
|
3521
|
+
return filePath.substring(cwd.length + 1);
|
|
3522
|
+
} else if (filePath.includes("/specs/")) {
|
|
3523
|
+
const specsIndex = filePath.indexOf("/specs/");
|
|
3524
|
+
return filePath.substring(specsIndex + 1);
|
|
3525
|
+
}
|
|
3526
|
+
return filePath;
|
|
3527
|
+
}
|
|
3528
|
+
function formatFileIssues(fileResult, specsDir) {
|
|
3529
|
+
const lines = [];
|
|
3530
|
+
const relativePath = normalizeFilePath(fileResult.filePath);
|
|
3531
|
+
const isMainSpec = relativePath.endsWith("README.md");
|
|
3532
|
+
if (isMainSpec && fileResult.spec) {
|
|
3533
|
+
const specName = fileResult.spec.name;
|
|
3534
|
+
const status = fileResult.spec.frontmatter.status;
|
|
3535
|
+
const priority = fileResult.spec.frontmatter.priority || "medium";
|
|
3536
|
+
const statusBadge = formatStatusBadge(status);
|
|
3537
|
+
const priorityBadge = formatPriorityBadge(priority);
|
|
3538
|
+
lines.push(chalk16.bold.cyan(`${specName} ${statusBadge} ${priorityBadge}`));
|
|
3539
|
+
} else {
|
|
3540
|
+
lines.push(chalk16.cyan.underline(relativePath));
|
|
3541
|
+
}
|
|
3542
|
+
for (const issue of fileResult.issues) {
|
|
3543
|
+
const severityColor = issue.severity === "error" ? chalk16.red : chalk16.yellow;
|
|
3544
|
+
const severityText = severityColor(issue.severity.padEnd(9));
|
|
3545
|
+
const ruleText = chalk16.gray(issue.ruleName);
|
|
3546
|
+
lines.push(` ${severityText}${issue.message.padEnd(60)} ${ruleText}`);
|
|
3547
|
+
if (issue.suggestion) {
|
|
3548
|
+
lines.push(chalk16.gray(` \u2192 ${issue.suggestion}`));
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
lines.push("");
|
|
3552
|
+
return lines.join("\n");
|
|
3553
|
+
}
|
|
3554
|
+
function formatSummary(totalSpecs, errorCount, warningCount, cleanCount) {
|
|
3555
|
+
if (errorCount > 0) {
|
|
3556
|
+
const errorText = errorCount === 1 ? "error" : "errors";
|
|
3557
|
+
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
3558
|
+
return chalk16.red.bold(
|
|
3559
|
+
`\u2716 ${errorCount} ${errorText}, ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
2630
3560
|
);
|
|
2631
3561
|
} else if (warningCount > 0) {
|
|
2632
3562
|
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
2633
|
-
return
|
|
3563
|
+
return chalk16.yellow.bold(
|
|
2634
3564
|
`\u26A0 ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
2635
3565
|
);
|
|
2636
3566
|
} else {
|
|
2637
|
-
return
|
|
3567
|
+
return chalk16.green.bold(`\u2713 All ${totalSpecs} specs passed`);
|
|
2638
3568
|
}
|
|
2639
3569
|
}
|
|
2640
3570
|
function formatPassingSpecs(specs, specsDir) {
|
|
2641
3571
|
const lines = [];
|
|
2642
|
-
lines.push(
|
|
3572
|
+
lines.push(chalk16.green.bold(`
|
|
2643
3573
|
\u2713 ${specs.length} specs passed:`));
|
|
2644
3574
|
for (const spec of specs) {
|
|
2645
3575
|
const relativePath = normalizeFilePath(spec.filePath);
|
|
2646
|
-
lines.push(
|
|
3576
|
+
lines.push(chalk16.gray(` ${relativePath}`));
|
|
2647
3577
|
}
|
|
2648
3578
|
return lines.join("\n");
|
|
2649
3579
|
}
|
|
@@ -2689,16 +3619,16 @@ function formatValidationResults(results, specs, specsDir, options = {}) {
|
|
|
2689
3619
|
return formatJson(displayResults, specs.length, errorCount2, warningCount2);
|
|
2690
3620
|
}
|
|
2691
3621
|
const lines = [];
|
|
2692
|
-
lines.push(
|
|
3622
|
+
lines.push(chalk16.bold(`
|
|
2693
3623
|
Validating ${specs.length} specs...
|
|
2694
3624
|
`));
|
|
2695
3625
|
let previousSpecName;
|
|
2696
3626
|
for (const fileResult of displayResults) {
|
|
2697
3627
|
if (fileResult.spec && previousSpecName && fileResult.spec.name !== previousSpecName) {
|
|
2698
|
-
lines.push(
|
|
3628
|
+
lines.push(chalk16.gray("\u2500".repeat(80)));
|
|
2699
3629
|
lines.push("");
|
|
2700
3630
|
}
|
|
2701
|
-
lines.push(formatFileIssues(fileResult
|
|
3631
|
+
lines.push(formatFileIssues(fileResult));
|
|
2702
3632
|
if (fileResult.spec) {
|
|
2703
3633
|
previousSpecName = fileResult.spec.name;
|
|
2704
3634
|
}
|
|
@@ -2716,10 +3646,10 @@ Validating ${specs.length} specs...
|
|
|
2716
3646
|
if (options.verbose && cleanCount > 0) {
|
|
2717
3647
|
const specsWithIssues = new Set(fileResults.map((fr) => fr.filePath));
|
|
2718
3648
|
const passingSpecs = specs.filter((spec) => !specsWithIssues.has(spec.filePath));
|
|
2719
|
-
lines.push(formatPassingSpecs(passingSpecs
|
|
3649
|
+
lines.push(formatPassingSpecs(passingSpecs));
|
|
2720
3650
|
}
|
|
2721
3651
|
if (!options.verbose && cleanCount > 0 && displayResults.length > 0) {
|
|
2722
|
-
lines.push(
|
|
3652
|
+
lines.push(chalk16.gray("\nRun with --verbose to see passing specs."));
|
|
2723
3653
|
}
|
|
2724
3654
|
return lines.join("\n");
|
|
2725
3655
|
}
|
|
@@ -2733,12 +3663,12 @@ async function validateCommand(options = {}) {
|
|
|
2733
3663
|
specs = [];
|
|
2734
3664
|
for (const specPath of options.specs) {
|
|
2735
3665
|
const spec = allSpecs.find(
|
|
2736
|
-
(s) => s.path.includes(specPath) ||
|
|
3666
|
+
(s) => s.path.includes(specPath) || path2.basename(s.path).includes(specPath)
|
|
2737
3667
|
);
|
|
2738
3668
|
if (spec) {
|
|
2739
3669
|
specs.push(spec);
|
|
2740
3670
|
} else {
|
|
2741
|
-
console.error(
|
|
3671
|
+
console.error(chalk16.red(`Error: Spec not found: ${specPath}`));
|
|
2742
3672
|
return false;
|
|
2743
3673
|
}
|
|
2744
3674
|
}
|
|
@@ -2753,7 +3683,8 @@ async function validateCommand(options = {}) {
|
|
|
2753
3683
|
return true;
|
|
2754
3684
|
}
|
|
2755
3685
|
const validators = [
|
|
2756
|
-
new
|
|
3686
|
+
new ComplexityValidator({ maxLines: options.maxLines }),
|
|
3687
|
+
// Token-based complexity (primary), line count (backstop)
|
|
2757
3688
|
new FrontmatterValidator(),
|
|
2758
3689
|
new StructureValidator(),
|
|
2759
3690
|
new CorruptionValidator(),
|
|
@@ -2763,19 +3694,23 @@ async function validateCommand(options = {}) {
|
|
|
2763
3694
|
for (const spec of specs) {
|
|
2764
3695
|
let content;
|
|
2765
3696
|
try {
|
|
2766
|
-
content = await
|
|
3697
|
+
content = await fs9.readFile(spec.filePath, "utf-8");
|
|
2767
3698
|
} catch (error) {
|
|
2768
|
-
console.error(
|
|
3699
|
+
console.error(chalk16.red(`Error reading ${spec.filePath}:`), error);
|
|
2769
3700
|
continue;
|
|
2770
3701
|
}
|
|
2771
3702
|
for (const validator of validators) {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3703
|
+
try {
|
|
3704
|
+
const result = await validator.validate(spec, content);
|
|
3705
|
+
results.push({
|
|
3706
|
+
spec,
|
|
3707
|
+
validatorName: validator.name,
|
|
3708
|
+
result,
|
|
3709
|
+
content
|
|
3710
|
+
});
|
|
3711
|
+
} catch (error) {
|
|
3712
|
+
console.error(chalk16.yellow(`Warning: Validator ${validator.name} failed:`), error instanceof Error ? error.message : error);
|
|
3713
|
+
}
|
|
2779
3714
|
}
|
|
2780
3715
|
}
|
|
2781
3716
|
const formatOptions = {
|
|
@@ -2789,14 +3724,10 @@ async function validateCommand(options = {}) {
|
|
|
2789
3724
|
const hasErrors = results.some((r) => !r.result.passed);
|
|
2790
3725
|
return !hasErrors;
|
|
2791
3726
|
}
|
|
2792
|
-
|
|
2793
|
-
// src/commands/migrate.ts
|
|
2794
|
-
import * as fs13 from "fs/promises";
|
|
2795
|
-
import * as path17 from "path";
|
|
2796
3727
|
async function migrateCommand(inputPath, options = {}) {
|
|
2797
3728
|
const config = await loadConfig();
|
|
2798
3729
|
try {
|
|
2799
|
-
const stats = await
|
|
3730
|
+
const stats = await fs9.stat(inputPath);
|
|
2800
3731
|
if (!stats.isDirectory()) {
|
|
2801
3732
|
console.error("\x1B[31m\u274C Error:\x1B[0m Input path must be a directory");
|
|
2802
3733
|
process.exit(1);
|
|
@@ -2824,16 +3755,16 @@ async function migrateCommand(inputPath, options = {}) {
|
|
|
2824
3755
|
async function scanDocuments(dirPath) {
|
|
2825
3756
|
const documents = [];
|
|
2826
3757
|
async function scanRecursive(currentPath) {
|
|
2827
|
-
const entries = await
|
|
3758
|
+
const entries = await fs9.readdir(currentPath, { withFileTypes: true });
|
|
2828
3759
|
for (const entry of entries) {
|
|
2829
|
-
const fullPath =
|
|
3760
|
+
const fullPath = path2.join(currentPath, entry.name);
|
|
2830
3761
|
if (entry.isDirectory()) {
|
|
2831
3762
|
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2832
3763
|
await scanRecursive(fullPath);
|
|
2833
3764
|
}
|
|
2834
3765
|
} else if (entry.isFile()) {
|
|
2835
3766
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".markdown")) {
|
|
2836
|
-
const stats = await
|
|
3767
|
+
const stats = await fs9.stat(fullPath);
|
|
2837
3768
|
documents.push({
|
|
2838
3769
|
path: fullPath,
|
|
2839
3770
|
name: entry.name,
|
|
@@ -2847,7 +3778,7 @@ async function scanDocuments(dirPath) {
|
|
|
2847
3778
|
return documents;
|
|
2848
3779
|
}
|
|
2849
3780
|
async function outputManualInstructions(inputPath, documents, config) {
|
|
2850
|
-
|
|
3781
|
+
config.specsDir || "specs";
|
|
2851
3782
|
console.log("\u2550".repeat(70));
|
|
2852
3783
|
console.log("\x1B[1m\x1B[36m\u{1F4CB} LeanSpec Migration Instructions\x1B[0m");
|
|
2853
3784
|
console.log("\u2550".repeat(70));
|
|
@@ -2966,7 +3897,7 @@ async function verifyAITool(provider) {
|
|
|
2966
3897
|
let installed = false;
|
|
2967
3898
|
let version;
|
|
2968
3899
|
try {
|
|
2969
|
-
const { execSync: execSync3 } = await import(
|
|
3900
|
+
const { execSync: execSync3 } = await import('child_process');
|
|
2970
3901
|
execSync3(`which ${toolDef.cliCommand}`, { stdio: "ignore" });
|
|
2971
3902
|
installed = true;
|
|
2972
3903
|
try {
|
|
@@ -3003,12 +3934,6 @@ function satisfiesVersion(version, minVersion) {
|
|
|
3003
3934
|
}
|
|
3004
3935
|
return true;
|
|
3005
3936
|
}
|
|
3006
|
-
|
|
3007
|
-
// src/commands/board.ts
|
|
3008
|
-
import chalk15 from "chalk";
|
|
3009
|
-
|
|
3010
|
-
// src/utils/completion.ts
|
|
3011
|
-
import dayjs from "dayjs";
|
|
3012
3937
|
function isCriticalOverdue(spec) {
|
|
3013
3938
|
if (spec.frontmatter.status === "complete" || spec.frontmatter.status === "archived") {
|
|
3014
3939
|
return false;
|
|
@@ -3016,7 +3941,7 @@ function isCriticalOverdue(spec) {
|
|
|
3016
3941
|
if (!spec.frontmatter.due) {
|
|
3017
3942
|
return false;
|
|
3018
3943
|
}
|
|
3019
|
-
const isOverdue =
|
|
3944
|
+
const isOverdue = dayjs3(spec.frontmatter.due).isBefore(dayjs3(), "day");
|
|
3020
3945
|
const isCritical = spec.frontmatter.priority === "critical" || spec.frontmatter.priority === "high";
|
|
3021
3946
|
return isOverdue && isCritical;
|
|
3022
3947
|
}
|
|
@@ -3028,7 +3953,7 @@ function isLongRunning(spec) {
|
|
|
3028
3953
|
if (!updatedAt) {
|
|
3029
3954
|
return false;
|
|
3030
3955
|
}
|
|
3031
|
-
const daysSinceUpdate =
|
|
3956
|
+
const daysSinceUpdate = dayjs3().diff(dayjs3(updatedAt), "day");
|
|
3032
3957
|
return daysSinceUpdate > 7;
|
|
3033
3958
|
}
|
|
3034
3959
|
function calculateCompletion(specs) {
|
|
@@ -3071,9 +3996,6 @@ function getCompletionStatus(score) {
|
|
|
3071
3996
|
return { emoji: "\u2717", label: "Needs Attention", color: "red" };
|
|
3072
3997
|
}
|
|
3073
3998
|
}
|
|
3074
|
-
|
|
3075
|
-
// src/utils/velocity.ts
|
|
3076
|
-
import dayjs2 from "dayjs";
|
|
3077
3999
|
function calculateCycleTime(spec) {
|
|
3078
4000
|
if (spec.frontmatter.status !== "complete" && spec.frontmatter.status !== "archived") {
|
|
3079
4001
|
return null;
|
|
@@ -3083,8 +4005,8 @@ function calculateCycleTime(spec) {
|
|
|
3083
4005
|
if (!createdAt || !completedAt) {
|
|
3084
4006
|
return null;
|
|
3085
4007
|
}
|
|
3086
|
-
const created =
|
|
3087
|
-
const completed =
|
|
4008
|
+
const created = dayjs3(createdAt);
|
|
4009
|
+
const completed = dayjs3(completedAt);
|
|
3088
4010
|
return completed.diff(created, "day", true);
|
|
3089
4011
|
}
|
|
3090
4012
|
function calculateLeadTime(spec, fromStatus, toStatus) {
|
|
@@ -3097,12 +4019,12 @@ function calculateLeadTime(spec, fromStatus, toStatus) {
|
|
|
3097
4019
|
if (!fromTransition || !toTransition) {
|
|
3098
4020
|
return null;
|
|
3099
4021
|
}
|
|
3100
|
-
const from =
|
|
3101
|
-
const to =
|
|
4022
|
+
const from = dayjs3(fromTransition.at);
|
|
4023
|
+
const to = dayjs3(toTransition.at);
|
|
3102
4024
|
return to.diff(from, "day", true);
|
|
3103
4025
|
}
|
|
3104
4026
|
function calculateThroughput(specs, days) {
|
|
3105
|
-
const cutoff =
|
|
4027
|
+
const cutoff = dayjs3().subtract(days, "day");
|
|
3106
4028
|
return specs.filter((s) => {
|
|
3107
4029
|
if (s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived") {
|
|
3108
4030
|
return false;
|
|
@@ -3111,19 +4033,19 @@ function calculateThroughput(specs, days) {
|
|
|
3111
4033
|
if (!completedAt) {
|
|
3112
4034
|
return false;
|
|
3113
4035
|
}
|
|
3114
|
-
return
|
|
4036
|
+
return dayjs3(completedAt).isAfter(cutoff);
|
|
3115
4037
|
}).length;
|
|
3116
4038
|
}
|
|
3117
|
-
function calculateWIP(specs, date =
|
|
4039
|
+
function calculateWIP(specs, date = dayjs3()) {
|
|
3118
4040
|
return specs.filter((s) => {
|
|
3119
4041
|
const createdAt = s.frontmatter.created_at || s.frontmatter.created;
|
|
3120
|
-
const created =
|
|
4042
|
+
const created = dayjs3(createdAt);
|
|
3121
4043
|
if (created.isAfter(date)) {
|
|
3122
4044
|
return false;
|
|
3123
4045
|
}
|
|
3124
4046
|
const completedAt = s.frontmatter.completed_at || s.frontmatter.completed;
|
|
3125
4047
|
if (completedAt) {
|
|
3126
|
-
const completed =
|
|
4048
|
+
const completed = dayjs3(completedAt);
|
|
3127
4049
|
return completed.isAfter(date);
|
|
3128
4050
|
}
|
|
3129
4051
|
return s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived";
|
|
@@ -3140,19 +4062,19 @@ function calculateVelocityMetrics(specs) {
|
|
|
3140
4062
|
const avgInProgressToComplete = inProgressToCompleteTimes.length > 0 ? inProgressToCompleteTimes.reduce((sum, t) => sum + t, 0) / inProgressToCompleteTimes.length : 0;
|
|
3141
4063
|
const throughputWeek = calculateThroughput(specs, 7);
|
|
3142
4064
|
const throughputMonth = calculateThroughput(specs, 30);
|
|
3143
|
-
const prevWeekStart =
|
|
3144
|
-
const prevWeekEnd =
|
|
4065
|
+
const prevWeekStart = dayjs3().subtract(14, "day");
|
|
4066
|
+
const prevWeekEnd = dayjs3().subtract(7, "day");
|
|
3145
4067
|
const throughputPrevWeek = specs.filter((s) => {
|
|
3146
4068
|
const completedAt = s.frontmatter.completed_at || s.frontmatter.completed;
|
|
3147
4069
|
if (!completedAt) return false;
|
|
3148
|
-
const completed =
|
|
4070
|
+
const completed = dayjs3(completedAt);
|
|
3149
4071
|
return completed.isAfter(prevWeekStart) && !completed.isAfter(prevWeekEnd);
|
|
3150
4072
|
}).length;
|
|
3151
4073
|
const throughputTrend = throughputWeek > throughputPrevWeek ? "up" : throughputWeek < throughputPrevWeek ? "down" : "stable";
|
|
3152
4074
|
const currentWIP = calculateWIP(specs);
|
|
3153
4075
|
const wipSamples = [];
|
|
3154
4076
|
for (let i = 0; i < 30; i++) {
|
|
3155
|
-
const sampleDate =
|
|
4077
|
+
const sampleDate = dayjs3().subtract(i, "day");
|
|
3156
4078
|
wipSamples.push(calculateWIP(specs, sampleDate));
|
|
3157
4079
|
}
|
|
3158
4080
|
const avgWIP = wipSamples.length > 0 ? wipSamples.reduce((sum, w) => sum + w, 0) / wipSamples.length : 0;
|
|
@@ -3196,7 +4118,7 @@ async function boardCommand(options) {
|
|
|
3196
4118
|
})
|
|
3197
4119
|
);
|
|
3198
4120
|
if (specs.length === 0) {
|
|
3199
|
-
console.log(
|
|
4121
|
+
console.log(chalk16.dim("No specs found."));
|
|
3200
4122
|
return;
|
|
3201
4123
|
}
|
|
3202
4124
|
const columns = {
|
|
@@ -3211,18 +4133,18 @@ async function boardCommand(options) {
|
|
|
3211
4133
|
columns[status].push(spec);
|
|
3212
4134
|
}
|
|
3213
4135
|
}
|
|
3214
|
-
console.log(
|
|
4136
|
+
console.log(chalk16.bold.cyan("\u{1F4CB} Spec Kanban Board"));
|
|
3215
4137
|
if (options.tag || options.assignee) {
|
|
3216
4138
|
const filterParts = [];
|
|
3217
4139
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
3218
4140
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
3219
|
-
console.log(
|
|
4141
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
3220
4142
|
}
|
|
3221
4143
|
console.log("");
|
|
3222
4144
|
if (!options.simple) {
|
|
3223
4145
|
const completionMetrics = calculateCompletion(specs);
|
|
3224
4146
|
const velocityMetrics = calculateVelocityMetrics(specs);
|
|
3225
|
-
|
|
4147
|
+
getCompletionStatus(completionMetrics.score);
|
|
3226
4148
|
const boxWidth = 62;
|
|
3227
4149
|
const topBorder = "\u2554" + "\u2550".repeat(boxWidth - 2) + "\u2557";
|
|
3228
4150
|
const bottomBorder = "\u255A" + "\u2550".repeat(boxWidth - 2) + "\u255D";
|
|
@@ -3231,12 +4153,12 @@ async function boardCommand(options) {
|
|
|
3231
4153
|
const padding = boxWidth - 2 - visibleLength;
|
|
3232
4154
|
return content + " ".repeat(Math.max(0, padding));
|
|
3233
4155
|
};
|
|
3234
|
-
console.log(
|
|
3235
|
-
const headerLine =
|
|
3236
|
-
console.log(
|
|
3237
|
-
const percentageColor = completionMetrics.score >= 70 ?
|
|
4156
|
+
console.log(chalk16.dim(topBorder));
|
|
4157
|
+
const headerLine = chalk16.bold(" Project Overview");
|
|
4158
|
+
console.log(chalk16.dim("\u2551") + padLine(headerLine) + chalk16.dim("\u2551"));
|
|
4159
|
+
const percentageColor = completionMetrics.score >= 70 ? chalk16.green : completionMetrics.score >= 40 ? chalk16.yellow : chalk16.red;
|
|
3238
4160
|
const line1 = ` ${completionMetrics.totalSpecs} total \xB7 ${completionMetrics.activeSpecs} active \xB7 ${completionMetrics.completeSpecs} complete ${percentageColor("(" + completionMetrics.score + "%)")}`;
|
|
3239
|
-
console.log(
|
|
4161
|
+
console.log(chalk16.dim("\u2551") + padLine(line1) + chalk16.dim("\u2551"));
|
|
3240
4162
|
if (completionMetrics.criticalIssues.length > 0 || completionMetrics.warnings.length > 0) {
|
|
3241
4163
|
const alerts = [];
|
|
3242
4164
|
if (completionMetrics.criticalIssues.length > 0) {
|
|
@@ -3245,27 +4167,27 @@ async function boardCommand(options) {
|
|
|
3245
4167
|
if (completionMetrics.warnings.length > 0) {
|
|
3246
4168
|
alerts.push(`${completionMetrics.warnings.length} specs WIP > 7 days`);
|
|
3247
4169
|
}
|
|
3248
|
-
const alertLine = ` ${
|
|
3249
|
-
console.log(
|
|
4170
|
+
const alertLine = ` ${chalk16.yellow("\u26A0\uFE0F " + alerts.join(" \xB7 "))}`;
|
|
4171
|
+
console.log(chalk16.dim("\u2551") + padLine(alertLine) + chalk16.dim("\u2551"));
|
|
3250
4172
|
}
|
|
3251
|
-
const velocityLine = ` ${
|
|
3252
|
-
console.log(
|
|
3253
|
-
console.log(
|
|
4173
|
+
const velocityLine = ` ${chalk16.cyan("\u{1F680} Velocity:")} ${velocityMetrics.cycleTime.average.toFixed(1)}d avg cycle \xB7 ${(velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1)}/wk throughput`;
|
|
4174
|
+
console.log(chalk16.dim("\u2551") + padLine(velocityLine) + chalk16.dim("\u2551"));
|
|
4175
|
+
console.log(chalk16.dim(bottomBorder));
|
|
3254
4176
|
console.log("");
|
|
3255
4177
|
if (options.completionOnly) {
|
|
3256
4178
|
return;
|
|
3257
4179
|
}
|
|
3258
4180
|
}
|
|
3259
4181
|
renderColumn(STATUS_CONFIG.planned.label, STATUS_CONFIG.planned.emoji, columns.planned, true, STATUS_CONFIG.planned.colorFn);
|
|
3260
|
-
console.log(
|
|
4182
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3261
4183
|
console.log("");
|
|
3262
4184
|
renderColumn(STATUS_CONFIG["in-progress"].label, STATUS_CONFIG["in-progress"].emoji, columns["in-progress"], true, STATUS_CONFIG["in-progress"].colorFn);
|
|
3263
|
-
console.log(
|
|
4185
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3264
4186
|
console.log("");
|
|
3265
4187
|
renderColumn(STATUS_CONFIG.complete.label, STATUS_CONFIG.complete.emoji, columns.complete, options.showComplete || false, STATUS_CONFIG.complete.colorFn);
|
|
3266
4188
|
}
|
|
3267
4189
|
function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
3268
|
-
console.log(`${emoji} ${colorFn(
|
|
4190
|
+
console.log(`${emoji} ${colorFn(chalk16.bold(`${title} (${specs.length})`))}`);
|
|
3269
4191
|
console.log("");
|
|
3270
4192
|
if (expanded && specs.length > 0) {
|
|
3271
4193
|
const priorityGroups = {
|
|
@@ -3290,31 +4212,30 @@ function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
|
3290
4212
|
firstGroup = false;
|
|
3291
4213
|
const priorityLabel = priority === "none" ? "No Priority" : priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
3292
4214
|
const priorityEmoji = priority === "none" ? "\u26AA" : PRIORITY_CONFIG[priority].emoji;
|
|
3293
|
-
const priorityColor = priority === "none" ?
|
|
3294
|
-
console.log(` ${priorityColor(`${priorityEmoji} ${
|
|
3295
|
-
;
|
|
4215
|
+
const priorityColor = priority === "none" ? chalk16.dim : PRIORITY_CONFIG[priority].colorFn;
|
|
4216
|
+
console.log(` ${priorityColor(`${priorityEmoji} ${chalk16.bold(priorityLabel)} ${chalk16.dim(`(${groupSpecs.length})`)}`)}`);
|
|
3296
4217
|
for (const spec of groupSpecs) {
|
|
3297
4218
|
let assigneeStr = "";
|
|
3298
4219
|
if (spec.frontmatter.assignee) {
|
|
3299
|
-
assigneeStr = " " +
|
|
4220
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
3300
4221
|
}
|
|
3301
4222
|
let tagsStr = "";
|
|
3302
4223
|
if (spec.frontmatter.tags?.length) {
|
|
3303
4224
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
3304
4225
|
if (tags.length > 0) {
|
|
3305
4226
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
3306
|
-
tagsStr = " " +
|
|
4227
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
3307
4228
|
}
|
|
3308
4229
|
}
|
|
3309
|
-
console.log(` ${
|
|
4230
|
+
console.log(` ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
3310
4231
|
}
|
|
3311
4232
|
}
|
|
3312
4233
|
console.log("");
|
|
3313
4234
|
} else if (!expanded && specs.length > 0) {
|
|
3314
|
-
console.log(` ${
|
|
4235
|
+
console.log(` ${chalk16.dim("(collapsed, use --complete to expand)")}`);
|
|
3315
4236
|
console.log("");
|
|
3316
4237
|
} else {
|
|
3317
|
-
console.log(` ${
|
|
4238
|
+
console.log(` ${chalk16.dim("(empty)")}`);
|
|
3318
4239
|
console.log("");
|
|
3319
4240
|
}
|
|
3320
4241
|
}
|
|
@@ -3322,10 +4243,6 @@ function stripAnsi2(str) {
|
|
|
3322
4243
|
return str.replace(/\u001b\[\d+m/g, "");
|
|
3323
4244
|
}
|
|
3324
4245
|
|
|
3325
|
-
// src/commands/stats.ts
|
|
3326
|
-
import chalk16 from "chalk";
|
|
3327
|
-
import dayjs4 from "dayjs";
|
|
3328
|
-
|
|
3329
4246
|
// src/utils/spec-stats.ts
|
|
3330
4247
|
function countSpecsByStatusAndPriority(specs) {
|
|
3331
4248
|
const statusCounts = {
|
|
@@ -3359,9 +4276,6 @@ function countSpecsByStatusAndPriority(specs) {
|
|
|
3359
4276
|
}
|
|
3360
4277
|
return { statusCounts, priorityCounts, tagCounts };
|
|
3361
4278
|
}
|
|
3362
|
-
|
|
3363
|
-
// src/utils/insights.ts
|
|
3364
|
-
import dayjs3 from "dayjs";
|
|
3365
4279
|
function generateInsights(specs) {
|
|
3366
4280
|
const insights = [];
|
|
3367
4281
|
const criticalOverdue = specs.filter(
|
|
@@ -3462,7 +4376,7 @@ async function statsCommand(options) {
|
|
|
3462
4376
|
console.log("No specs found.");
|
|
3463
4377
|
return;
|
|
3464
4378
|
}
|
|
3465
|
-
|
|
4379
|
+
options.full || false;
|
|
3466
4380
|
const showStats = options.full || !options.timeline && !options.velocity;
|
|
3467
4381
|
const showTimeline = options.timeline || options.full;
|
|
3468
4382
|
const showVelocity = options.velocity || options.full;
|
|
@@ -3532,7 +4446,7 @@ async function statsCommand(options) {
|
|
|
3532
4446
|
const criticalInProgress = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "in-progress").length;
|
|
3533
4447
|
const criticalComplete = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "complete").length;
|
|
3534
4448
|
const criticalOverdue = specs.filter(
|
|
3535
|
-
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due &&
|
|
4449
|
+
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
3536
4450
|
).length;
|
|
3537
4451
|
const parts = [];
|
|
3538
4452
|
if (criticalPlanned > 0) parts.push(chalk16.dim(`${criticalPlanned} planned`));
|
|
@@ -3546,7 +4460,7 @@ async function statsCommand(options) {
|
|
|
3546
4460
|
const highInProgress = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "in-progress").length;
|
|
3547
4461
|
const highComplete = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "complete").length;
|
|
3548
4462
|
const highOverdue = specs.filter(
|
|
3549
|
-
(s) => s.frontmatter.priority === "high" && s.frontmatter.due &&
|
|
4463
|
+
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
3550
4464
|
).length;
|
|
3551
4465
|
const parts = [];
|
|
3552
4466
|
if (highPlanned > 0) parts.push(chalk16.dim(`${highPlanned} planned`));
|
|
@@ -3708,18 +4622,18 @@ async function statsCommand(options) {
|
|
|
3708
4622
|
}
|
|
3709
4623
|
if (showTimeline) {
|
|
3710
4624
|
const days = 30;
|
|
3711
|
-
const today =
|
|
4625
|
+
const today = dayjs3();
|
|
3712
4626
|
const startDate = today.subtract(days, "day");
|
|
3713
4627
|
const createdByDate = {};
|
|
3714
4628
|
const completedByDate = {};
|
|
3715
4629
|
for (const spec of specs) {
|
|
3716
|
-
const created =
|
|
4630
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
3717
4631
|
if (created.isAfter(startDate)) {
|
|
3718
4632
|
const dateKey = created.format("YYYY-MM-DD");
|
|
3719
4633
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
3720
4634
|
}
|
|
3721
4635
|
if (spec.frontmatter.completed) {
|
|
3722
|
-
const completed =
|
|
4636
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
3723
4637
|
if (completed.isAfter(startDate)) {
|
|
3724
4638
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
3725
4639
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
@@ -3838,9 +4752,6 @@ async function statsCommand(options) {
|
|
|
3838
4752
|
}
|
|
3839
4753
|
}
|
|
3840
4754
|
}
|
|
3841
|
-
|
|
3842
|
-
// src/commands/search.ts
|
|
3843
|
-
import chalk17 from "chalk";
|
|
3844
4755
|
async function searchCommand(query, options) {
|
|
3845
4756
|
await autoCheckIfEnabled();
|
|
3846
4757
|
const filter = {};
|
|
@@ -3861,92 +4772,105 @@ async function searchCommand(query, options) {
|
|
|
3861
4772
|
console.log("No specs found matching filters.");
|
|
3862
4773
|
return;
|
|
3863
4774
|
}
|
|
3864
|
-
const
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
if (matches.length > 0) {
|
|
3882
|
-
results.push({ spec, matches });
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
4775
|
+
const searchableSpecs = specs.map((spec) => ({
|
|
4776
|
+
path: spec.path,
|
|
4777
|
+
name: spec.path,
|
|
4778
|
+
status: spec.frontmatter.status,
|
|
4779
|
+
priority: spec.frontmatter.priority,
|
|
4780
|
+
tags: spec.frontmatter.tags,
|
|
4781
|
+
title: spec.frontmatter.title,
|
|
4782
|
+
description: spec.frontmatter.description,
|
|
4783
|
+
content: spec.content
|
|
4784
|
+
}));
|
|
4785
|
+
const searchResult = searchSpecs(query, searchableSpecs, {
|
|
4786
|
+
maxMatchesPerSpec: 5,
|
|
4787
|
+
contextLength: 80
|
|
4788
|
+
});
|
|
4789
|
+
const { results, metadata } = searchResult;
|
|
3885
4790
|
if (results.length === 0) {
|
|
3886
4791
|
console.log("");
|
|
3887
|
-
console.log(
|
|
4792
|
+
console.log(chalk16.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
|
|
3888
4793
|
if (Object.keys(filter).length > 0) {
|
|
3889
4794
|
const filters = [];
|
|
3890
4795
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
3891
4796
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
3892
4797
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
3893
4798
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
3894
|
-
console.log(
|
|
4799
|
+
console.log(chalk16.gray(`With filters: ${filters.join(", ")}`));
|
|
3895
4800
|
}
|
|
3896
4801
|
console.log("");
|
|
3897
4802
|
return;
|
|
3898
4803
|
}
|
|
3899
4804
|
console.log("");
|
|
3900
|
-
console.log(
|
|
4805
|
+
console.log(chalk16.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${sanitizeUserInput(query)}"`));
|
|
4806
|
+
console.log(chalk16.gray(` Searched ${metadata.specsSearched} specs in ${metadata.searchTime}ms`));
|
|
3901
4807
|
if (Object.keys(filter).length > 0) {
|
|
3902
4808
|
const filters = [];
|
|
3903
4809
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
3904
4810
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
3905
4811
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
3906
4812
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
3907
|
-
console.log(
|
|
4813
|
+
console.log(chalk16.gray(` With filters: ${filters.join(", ")}`));
|
|
3908
4814
|
}
|
|
3909
4815
|
console.log("");
|
|
3910
4816
|
for (const result of results) {
|
|
3911
|
-
const { spec, matches } = result;
|
|
3912
|
-
|
|
4817
|
+
const { spec, matches, score, totalMatches } = result;
|
|
4818
|
+
const statusEmoji = spec.status === "in-progress" ? "\u{1F528}" : spec.status === "complete" ? "\u2705" : "\u{1F4C5}";
|
|
4819
|
+
console.log(chalk16.cyan(`${statusEmoji} ${sanitizeUserInput(spec.path)} ${chalk16.gray(`(${score}% match)`)}`));
|
|
3913
4820
|
const meta = [];
|
|
3914
|
-
if (spec.
|
|
3915
|
-
const priorityEmoji = spec.
|
|
3916
|
-
meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.
|
|
4821
|
+
if (spec.priority) {
|
|
4822
|
+
const priorityEmoji = spec.priority === "critical" ? "\u{1F534}" : spec.priority === "high" ? "\u{1F7E1}" : spec.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
4823
|
+
meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.priority)}`);
|
|
3917
4824
|
}
|
|
3918
|
-
if (spec.
|
|
3919
|
-
meta.push(`[${spec.
|
|
4825
|
+
if (spec.tags && spec.tags.length > 0) {
|
|
4826
|
+
meta.push(`[${spec.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
|
|
3920
4827
|
}
|
|
3921
4828
|
if (meta.length > 0) {
|
|
3922
|
-
console.log(
|
|
3923
|
-
}
|
|
3924
|
-
const
|
|
3925
|
-
|
|
3926
|
-
console.log(`
|
|
4829
|
+
console.log(chalk16.gray(` ${meta.join(" \u2022 ")}`));
|
|
4830
|
+
}
|
|
4831
|
+
const titleMatch = matches.find((m) => m.field === "title");
|
|
4832
|
+
if (titleMatch) {
|
|
4833
|
+
console.log(` ${chalk16.bold("Title:")} ${highlightMatches(titleMatch.text, titleMatch.highlights)}`);
|
|
4834
|
+
}
|
|
4835
|
+
const descMatch = matches.find((m) => m.field === "description");
|
|
4836
|
+
if (descMatch) {
|
|
4837
|
+
console.log(` ${chalk16.bold("Description:")} ${highlightMatches(descMatch.text, descMatch.highlights)}`);
|
|
4838
|
+
}
|
|
4839
|
+
const tagMatches = matches.filter((m) => m.field === "tags");
|
|
4840
|
+
if (tagMatches.length > 0) {
|
|
4841
|
+
console.log(` ${chalk16.bold("Tags:")} ${tagMatches.map((m) => highlightMatches(m.text, m.highlights)).join(", ")}`);
|
|
4842
|
+
}
|
|
4843
|
+
const contentMatches = matches.filter((m) => m.field === "content");
|
|
4844
|
+
if (contentMatches.length > 0) {
|
|
4845
|
+
console.log(` ${chalk16.bold("Content matches:")}`);
|
|
4846
|
+
for (const match of contentMatches) {
|
|
4847
|
+
const lineInfo = match.lineNumber ? chalk16.gray(`[L${match.lineNumber}]`) : "";
|
|
4848
|
+
console.log(` ${lineInfo} ${highlightMatches(match.text, match.highlights)}`);
|
|
4849
|
+
}
|
|
3927
4850
|
}
|
|
3928
|
-
if (matches.length
|
|
3929
|
-
console.log(
|
|
4851
|
+
if (totalMatches > matches.length) {
|
|
4852
|
+
console.log(chalk16.gray(` ... and ${totalMatches - matches.length} more match${totalMatches - matches.length === 1 ? "" : "es"}`));
|
|
3930
4853
|
}
|
|
3931
4854
|
console.log("");
|
|
3932
4855
|
}
|
|
3933
4856
|
}
|
|
3934
|
-
function
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4857
|
+
function highlightMatches(text, highlights) {
|
|
4858
|
+
if (highlights.length === 0) return text;
|
|
4859
|
+
let result = "";
|
|
4860
|
+
let lastEnd = 0;
|
|
4861
|
+
for (const [start, end] of highlights) {
|
|
4862
|
+
result += text.substring(lastEnd, start);
|
|
4863
|
+
result += chalk16.yellow(text.substring(start, end));
|
|
4864
|
+
lastEnd = end;
|
|
4865
|
+
}
|
|
4866
|
+
result += text.substring(lastEnd);
|
|
4867
|
+
return result;
|
|
3940
4868
|
}
|
|
3941
|
-
|
|
3942
|
-
// src/commands/deps.ts
|
|
3943
|
-
import chalk18 from "chalk";
|
|
3944
|
-
import * as path18 from "path";
|
|
3945
4869
|
async function depsCommand(specPath, options) {
|
|
3946
4870
|
await autoCheckIfEnabled();
|
|
3947
4871
|
const config = await loadConfig();
|
|
3948
4872
|
const cwd = process.cwd();
|
|
3949
|
-
const specsDir =
|
|
4873
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
3950
4874
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
3951
4875
|
if (!resolvedPath) {
|
|
3952
4876
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -3975,16 +4899,16 @@ async function depsCommand(specPath, options) {
|
|
|
3975
4899
|
return;
|
|
3976
4900
|
}
|
|
3977
4901
|
console.log("");
|
|
3978
|
-
console.log(
|
|
4902
|
+
console.log(chalk16.green(`\u{1F4E6} Dependencies for ${chalk16.cyan(sanitizeUserInput(spec.path))}`));
|
|
3979
4903
|
console.log("");
|
|
3980
4904
|
const hasAnyRelationships = dependsOn.length > 0 || blocks.length > 0 || relatedSpecs.length > 0;
|
|
3981
4905
|
if (!hasAnyRelationships) {
|
|
3982
|
-
console.log(
|
|
4906
|
+
console.log(chalk16.gray(" No dependencies or relationships"));
|
|
3983
4907
|
console.log("");
|
|
3984
4908
|
return;
|
|
3985
4909
|
}
|
|
3986
4910
|
if (dependsOn.length > 0) {
|
|
3987
|
-
console.log(
|
|
4911
|
+
console.log(chalk16.bold("Depends On:"));
|
|
3988
4912
|
for (const dep of dependsOn) {
|
|
3989
4913
|
const status = getStatusIndicator(dep.frontmatter.status);
|
|
3990
4914
|
console.log(` \u2192 ${sanitizeUserInput(dep.path)} ${status}`);
|
|
@@ -3992,7 +4916,7 @@ async function depsCommand(specPath, options) {
|
|
|
3992
4916
|
console.log("");
|
|
3993
4917
|
}
|
|
3994
4918
|
if (blocks.length > 0) {
|
|
3995
|
-
console.log(
|
|
4919
|
+
console.log(chalk16.bold("Required By:"));
|
|
3996
4920
|
for (const blocked of blocks) {
|
|
3997
4921
|
const status = getStatusIndicator(blocked.frontmatter.status);
|
|
3998
4922
|
console.log(` \u2190 ${sanitizeUserInput(blocked.path)} ${status}`);
|
|
@@ -4000,7 +4924,7 @@ async function depsCommand(specPath, options) {
|
|
|
4000
4924
|
console.log("");
|
|
4001
4925
|
}
|
|
4002
4926
|
if (relatedSpecs.length > 0) {
|
|
4003
|
-
console.log(
|
|
4927
|
+
console.log(chalk16.bold("Related Specs:"));
|
|
4004
4928
|
for (const rel of relatedSpecs) {
|
|
4005
4929
|
const status = getStatusIndicator(rel.frontmatter.status);
|
|
4006
4930
|
console.log(` \u27F7 ${sanitizeUserInput(rel.path)} ${status}`);
|
|
@@ -4008,7 +4932,7 @@ async function depsCommand(specPath, options) {
|
|
|
4008
4932
|
console.log("");
|
|
4009
4933
|
}
|
|
4010
4934
|
if (options.graph || dependsOn.length > 0) {
|
|
4011
|
-
console.log(
|
|
4935
|
+
console.log(chalk16.bold("Dependency Chain:"));
|
|
4012
4936
|
const chain = buildDependencyChain(spec, specMap, options.depth || 3);
|
|
4013
4937
|
displayChain(chain, 0);
|
|
4014
4938
|
console.log("");
|
|
@@ -4022,8 +4946,8 @@ function findDependencies(spec, specMap) {
|
|
|
4022
4946
|
if (dep) {
|
|
4023
4947
|
deps.push(dep);
|
|
4024
4948
|
} else {
|
|
4025
|
-
for (const [
|
|
4026
|
-
if (
|
|
4949
|
+
for (const [path26, s] of specMap.entries()) {
|
|
4950
|
+
if (path26.includes(depPath)) {
|
|
4027
4951
|
deps.push(s);
|
|
4028
4952
|
break;
|
|
4029
4953
|
}
|
|
@@ -4055,8 +4979,8 @@ function findRelated(spec, specMap) {
|
|
|
4055
4979
|
if (rel) {
|
|
4056
4980
|
related.push(rel);
|
|
4057
4981
|
} else {
|
|
4058
|
-
for (const [
|
|
4059
|
-
if (
|
|
4982
|
+
for (const [path26, s] of specMap.entries()) {
|
|
4983
|
+
if (path26.includes(relPath)) {
|
|
4060
4984
|
related.push(s);
|
|
4061
4985
|
break;
|
|
4062
4986
|
}
|
|
@@ -4114,7 +5038,7 @@ function buildDependencyChain(spec, specMap, maxDepth, currentDepth = 0, visited
|
|
|
4114
5038
|
function displayChain(node, level) {
|
|
4115
5039
|
const indent = " ".repeat(level);
|
|
4116
5040
|
const status = getStatusIndicator(node.spec.frontmatter.status);
|
|
4117
|
-
const name = level === 0 ?
|
|
5041
|
+
const name = level === 0 ? chalk16.cyan(node.spec.path) : node.spec.path;
|
|
4118
5042
|
console.log(`${indent}${name} ${status}`);
|
|
4119
5043
|
for (const dep of node.dependencies) {
|
|
4120
5044
|
const prefix = " ".repeat(level) + "\u2514\u2500 ";
|
|
@@ -4125,10 +5049,6 @@ function displayChain(node, level) {
|
|
|
4125
5049
|
}
|
|
4126
5050
|
}
|
|
4127
5051
|
}
|
|
4128
|
-
|
|
4129
|
-
// src/commands/timeline.ts
|
|
4130
|
-
import chalk19 from "chalk";
|
|
4131
|
-
import dayjs5 from "dayjs";
|
|
4132
5052
|
async function timelineCommand(options) {
|
|
4133
5053
|
await autoCheckIfEnabled();
|
|
4134
5054
|
const createBar = (count, maxCount, width, char = "\u2501") => {
|
|
@@ -4143,13 +5063,13 @@ async function timelineCommand(options) {
|
|
|
4143
5063
|
console.log("No specs found.");
|
|
4144
5064
|
return;
|
|
4145
5065
|
}
|
|
4146
|
-
const today =
|
|
5066
|
+
const today = dayjs3();
|
|
4147
5067
|
const startDate = today.subtract(days, "day");
|
|
4148
5068
|
const createdByDate = {};
|
|
4149
5069
|
const completedByDate = {};
|
|
4150
5070
|
const createdByMonth = {};
|
|
4151
5071
|
for (const spec of specs) {
|
|
4152
|
-
const created =
|
|
5072
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4153
5073
|
if (created.isAfter(startDate)) {
|
|
4154
5074
|
const dateKey = created.format("YYYY-MM-DD");
|
|
4155
5075
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
@@ -4157,26 +5077,26 @@ async function timelineCommand(options) {
|
|
|
4157
5077
|
const monthKey = created.format("MMM YYYY");
|
|
4158
5078
|
createdByMonth[monthKey] = (createdByMonth[monthKey] || 0) + 1;
|
|
4159
5079
|
if (spec.frontmatter.completed) {
|
|
4160
|
-
const completed =
|
|
5080
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4161
5081
|
if (completed.isAfter(startDate)) {
|
|
4162
5082
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
4163
5083
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
4164
5084
|
}
|
|
4165
5085
|
}
|
|
4166
5086
|
}
|
|
4167
|
-
console.log(
|
|
5087
|
+
console.log(chalk16.bold.cyan("\u{1F4C8} Spec Timeline"));
|
|
4168
5088
|
console.log("");
|
|
4169
5089
|
const allDates = /* @__PURE__ */ new Set([...Object.keys(createdByDate), ...Object.keys(completedByDate)]);
|
|
4170
5090
|
const sortedDates = Array.from(allDates).sort();
|
|
4171
5091
|
if (sortedDates.length > 0) {
|
|
4172
|
-
console.log(
|
|
5092
|
+
console.log(chalk16.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
4173
5093
|
console.log("");
|
|
4174
5094
|
const labelWidth2 = 15;
|
|
4175
5095
|
const barWidth = 20;
|
|
4176
5096
|
const specsWidth = 3;
|
|
4177
5097
|
const colWidth = barWidth + specsWidth;
|
|
4178
|
-
console.log(` ${"Date".padEnd(labelWidth2)} ${
|
|
4179
|
-
console.log(` ${
|
|
5098
|
+
console.log(` ${"Date".padEnd(labelWidth2)} ${chalk16.cyan("Created".padEnd(colWidth))} ${chalk16.green("Completed".padEnd(colWidth))}`);
|
|
5099
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4180
5100
|
const maxCount = Math.max(...Object.values(createdByDate), ...Object.values(completedByDate));
|
|
4181
5101
|
for (const date of sortedDates) {
|
|
4182
5102
|
const created = createdByDate[date] || 0;
|
|
@@ -4185,61 +5105,61 @@ async function timelineCommand(options) {
|
|
|
4185
5105
|
const completedBar = createBar(completed, maxCount, barWidth);
|
|
4186
5106
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(specsWidth)}`;
|
|
4187
5107
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(specsWidth)}`;
|
|
4188
|
-
console.log(` ${
|
|
5108
|
+
console.log(` ${chalk16.dim(date.padEnd(labelWidth2))} ${chalk16.cyan(createdCol)} ${chalk16.green(completedCol)}`);
|
|
4189
5109
|
}
|
|
4190
5110
|
console.log("");
|
|
4191
5111
|
}
|
|
4192
5112
|
const sortedMonths = Object.entries(createdByMonth).sort((a, b) => {
|
|
4193
|
-
const dateA =
|
|
4194
|
-
const dateB =
|
|
5113
|
+
const dateA = dayjs3(a[0], "MMM YYYY");
|
|
5114
|
+
const dateB = dayjs3(b[0], "MMM YYYY");
|
|
4195
5115
|
return dateB.diff(dateA);
|
|
4196
5116
|
}).slice(0, 6);
|
|
4197
5117
|
if (sortedMonths.length > 0) {
|
|
4198
|
-
console.log(
|
|
5118
|
+
console.log(chalk16.bold("\u{1F4CA} Monthly Overview"));
|
|
4199
5119
|
console.log("");
|
|
4200
5120
|
const labelWidth2 = 15;
|
|
4201
5121
|
const barWidth = 20;
|
|
4202
5122
|
const specsWidth = 3;
|
|
4203
5123
|
const colWidth = barWidth + specsWidth;
|
|
4204
|
-
console.log(` ${"Month".padEnd(labelWidth2)} ${
|
|
4205
|
-
console.log(` ${
|
|
5124
|
+
console.log(` ${"Month".padEnd(labelWidth2)} ${chalk16.magenta("Specs".padEnd(colWidth))}`);
|
|
5125
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4206
5126
|
const maxCount = Math.max(...sortedMonths.map(([, count]) => count));
|
|
4207
5127
|
for (const [month, count] of sortedMonths) {
|
|
4208
5128
|
const bar = createBar(count, maxCount, barWidth);
|
|
4209
|
-
console.log(` ${month.padEnd(labelWidth2)} ${
|
|
5129
|
+
console.log(` ${month.padEnd(labelWidth2)} ${chalk16.magenta(bar.padEnd(barWidth))}${chalk16.magenta(count.toString().padStart(specsWidth))}`);
|
|
4210
5130
|
}
|
|
4211
5131
|
console.log("");
|
|
4212
5132
|
}
|
|
4213
5133
|
const last7Days = specs.filter((s) => {
|
|
4214
5134
|
if (!s.frontmatter.completed) return false;
|
|
4215
|
-
const completed =
|
|
5135
|
+
const completed = dayjs3(s.frontmatter.completed);
|
|
4216
5136
|
return completed.isAfter(today.subtract(7, "day"));
|
|
4217
5137
|
}).length;
|
|
4218
5138
|
const last30Days = specs.filter((s) => {
|
|
4219
5139
|
if (!s.frontmatter.completed) return false;
|
|
4220
|
-
const completed =
|
|
5140
|
+
const completed = dayjs3(s.frontmatter.completed);
|
|
4221
5141
|
return completed.isAfter(today.subtract(30, "day"));
|
|
4222
5142
|
}).length;
|
|
4223
|
-
console.log(
|
|
5143
|
+
console.log(chalk16.bold("\u2705 Completion Rate"));
|
|
4224
5144
|
console.log("");
|
|
4225
5145
|
const labelWidth = 15;
|
|
4226
5146
|
const valueWidth = 5;
|
|
4227
5147
|
console.log(` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`);
|
|
4228
|
-
console.log(` ${
|
|
4229
|
-
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
4230
|
-
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
5148
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`);
|
|
5149
|
+
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${chalk16.green(last7Days.toString().padStart(valueWidth))}`);
|
|
5150
|
+
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${chalk16.green(last30Days.toString().padStart(valueWidth))}`);
|
|
4231
5151
|
console.log("");
|
|
4232
5152
|
if (options.byTag) {
|
|
4233
5153
|
const tagStats = {};
|
|
4234
5154
|
for (const spec of specs) {
|
|
4235
|
-
const created =
|
|
5155
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4236
5156
|
const isInRange = created.isAfter(startDate);
|
|
4237
5157
|
if (isInRange && spec.frontmatter.tags) {
|
|
4238
5158
|
for (const tag of spec.frontmatter.tags) {
|
|
4239
5159
|
if (!tagStats[tag]) tagStats[tag] = { created: 0, completed: 0 };
|
|
4240
5160
|
tagStats[tag].created++;
|
|
4241
5161
|
if (spec.frontmatter.completed) {
|
|
4242
|
-
const completed =
|
|
5162
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4243
5163
|
if (completed.isAfter(startDate)) {
|
|
4244
5164
|
tagStats[tag].completed++;
|
|
4245
5165
|
}
|
|
@@ -4249,9 +5169,9 @@ async function timelineCommand(options) {
|
|
|
4249
5169
|
}
|
|
4250
5170
|
const sortedTags = Object.entries(tagStats).sort((a, b) => b[1].created - a[1].created).slice(0, 10);
|
|
4251
5171
|
if (sortedTags.length > 0) {
|
|
4252
|
-
console.log(
|
|
5172
|
+
console.log(chalk16.bold("\u{1F3F7}\uFE0F By Tag"));
|
|
4253
5173
|
for (const [tag, stats] of sortedTags) {
|
|
4254
|
-
console.log(` ${
|
|
5174
|
+
console.log(` ${chalk16.dim("#")}${tag.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4255
5175
|
}
|
|
4256
5176
|
console.log("");
|
|
4257
5177
|
}
|
|
@@ -4260,14 +5180,14 @@ async function timelineCommand(options) {
|
|
|
4260
5180
|
const assigneeStats = {};
|
|
4261
5181
|
for (const spec of specs) {
|
|
4262
5182
|
if (!spec.frontmatter.assignee) continue;
|
|
4263
|
-
const created =
|
|
5183
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4264
5184
|
const isInRange = created.isAfter(startDate);
|
|
4265
5185
|
if (isInRange) {
|
|
4266
5186
|
const assignee = spec.frontmatter.assignee;
|
|
4267
5187
|
if (!assigneeStats[assignee]) assigneeStats[assignee] = { created: 0, completed: 0 };
|
|
4268
5188
|
assigneeStats[assignee].created++;
|
|
4269
5189
|
if (spec.frontmatter.completed) {
|
|
4270
|
-
const completed =
|
|
5190
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4271
5191
|
if (completed.isAfter(startDate)) {
|
|
4272
5192
|
assigneeStats[assignee].completed++;
|
|
4273
5193
|
}
|
|
@@ -4276,18 +5196,14 @@ async function timelineCommand(options) {
|
|
|
4276
5196
|
}
|
|
4277
5197
|
const sortedAssignees = Object.entries(assigneeStats).sort((a, b) => b[1].created - a[1].created);
|
|
4278
5198
|
if (sortedAssignees.length > 0) {
|
|
4279
|
-
console.log(
|
|
5199
|
+
console.log(chalk16.bold("\u{1F464} By Assignee"));
|
|
4280
5200
|
for (const [assignee, stats] of sortedAssignees) {
|
|
4281
|
-
console.log(` ${
|
|
5201
|
+
console.log(` ${chalk16.dim("@")}${assignee.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4282
5202
|
}
|
|
4283
5203
|
console.log("");
|
|
4284
5204
|
}
|
|
4285
5205
|
}
|
|
4286
5206
|
}
|
|
4287
|
-
|
|
4288
|
-
// src/commands/gantt.ts
|
|
4289
|
-
import chalk20 from "chalk";
|
|
4290
|
-
import dayjs6 from "dayjs";
|
|
4291
5207
|
var SPEC_COLUMN_WIDTH = 43;
|
|
4292
5208
|
var COLUMN_SEPARATOR = " ";
|
|
4293
5209
|
var SPEC_INDENT = " ";
|
|
@@ -4300,10 +5216,10 @@ var STATUS_CONFIG2 = {
|
|
|
4300
5216
|
archived: { emoji: "\u{1F4E6}", color: "gray" }
|
|
4301
5217
|
};
|
|
4302
5218
|
var PRIORITY_CONFIG3 = {
|
|
4303
|
-
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn:
|
|
4304
|
-
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn:
|
|
4305
|
-
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn:
|
|
4306
|
-
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn:
|
|
5219
|
+
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn: chalk16.red },
|
|
5220
|
+
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn: chalk16.hex("#FFA500") },
|
|
5221
|
+
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn: chalk16.yellow },
|
|
5222
|
+
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn: chalk16.green }
|
|
4307
5223
|
};
|
|
4308
5224
|
async function ganttCommand(options) {
|
|
4309
5225
|
await autoCheckIfEnabled();
|
|
@@ -4326,8 +5242,8 @@ async function ganttCommand(options) {
|
|
|
4326
5242
|
return spec.frontmatter.status !== "archived";
|
|
4327
5243
|
});
|
|
4328
5244
|
if (relevantSpecs.length === 0) {
|
|
4329
|
-
console.log(
|
|
4330
|
-
console.log(
|
|
5245
|
+
console.log(chalk16.dim("No active specs found."));
|
|
5246
|
+
console.log(chalk16.dim("Tip: Use --show-complete to include completed specs."));
|
|
4331
5247
|
return;
|
|
4332
5248
|
}
|
|
4333
5249
|
const groupedSpecs = {
|
|
@@ -4336,12 +5252,9 @@ async function ganttCommand(options) {
|
|
|
4336
5252
|
medium: [],
|
|
4337
5253
|
low: []
|
|
4338
5254
|
};
|
|
4339
|
-
const noPrioritySpecs = [];
|
|
4340
5255
|
for (const spec of relevantSpecs) {
|
|
4341
5256
|
if (spec.frontmatter.priority && spec.frontmatter.priority in groupedSpecs) {
|
|
4342
5257
|
groupedSpecs[spec.frontmatter.priority].push(spec);
|
|
4343
|
-
} else {
|
|
4344
|
-
noPrioritySpecs.push(spec);
|
|
4345
5258
|
}
|
|
4346
5259
|
}
|
|
4347
5260
|
const sortSpecs = (specs2) => {
|
|
@@ -4353,20 +5266,20 @@ async function ganttCommand(options) {
|
|
|
4353
5266
|
if (a.frontmatter.due && !b.frontmatter.due) return -1;
|
|
4354
5267
|
if (!a.frontmatter.due && b.frontmatter.due) return 1;
|
|
4355
5268
|
if (a.frontmatter.due && b.frontmatter.due) {
|
|
4356
|
-
return
|
|
5269
|
+
return dayjs3(a.frontmatter.due).diff(dayjs3(b.frontmatter.due));
|
|
4357
5270
|
}
|
|
4358
5271
|
return 0;
|
|
4359
5272
|
});
|
|
4360
5273
|
};
|
|
4361
|
-
const today =
|
|
5274
|
+
const today = dayjs3();
|
|
4362
5275
|
const startDate = today.startOf("week");
|
|
4363
5276
|
const endDate = startDate.add(weeks, "week");
|
|
4364
5277
|
const inProgress = relevantSpecs.filter((s) => s.frontmatter.status === "in-progress").length;
|
|
4365
5278
|
const planned = relevantSpecs.filter((s) => s.frontmatter.status === "planned").length;
|
|
4366
5279
|
const overdue = relevantSpecs.filter(
|
|
4367
|
-
(s) => s.frontmatter.due &&
|
|
5280
|
+
(s) => s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
|
|
4368
5281
|
).length;
|
|
4369
|
-
console.log(
|
|
5282
|
+
console.log(chalk16.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
|
|
4370
5283
|
console.log("");
|
|
4371
5284
|
const specHeader = "Spec".padEnd(SPEC_COLUMN_WIDTH);
|
|
4372
5285
|
const timelineHeader = "Timeline";
|
|
@@ -4378,17 +5291,17 @@ async function ganttCommand(options) {
|
|
|
4378
5291
|
calendarDates.push(dateStr);
|
|
4379
5292
|
}
|
|
4380
5293
|
const dateRow = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR + calendarDates.join("");
|
|
4381
|
-
console.log(
|
|
5294
|
+
console.log(chalk16.dim(dateRow));
|
|
4382
5295
|
const specSeparator = "\u2500".repeat(SPEC_COLUMN_WIDTH);
|
|
4383
5296
|
const timelineSeparator = "\u2500".repeat(timelineColumnWidth);
|
|
4384
|
-
console.log(
|
|
5297
|
+
console.log(chalk16.dim(specSeparator + COLUMN_SEPARATOR + timelineSeparator));
|
|
4385
5298
|
const todayWeekOffset = today.diff(startDate, "week");
|
|
4386
5299
|
const todayMarkerPos = todayWeekOffset * 8;
|
|
4387
5300
|
let todayMarker = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR;
|
|
4388
5301
|
if (todayMarkerPos >= 0 && todayMarkerPos < timelineColumnWidth) {
|
|
4389
5302
|
todayMarker += " ".repeat(todayMarkerPos) + "\u2502 Today";
|
|
4390
5303
|
}
|
|
4391
|
-
console.log(
|
|
5304
|
+
console.log(chalk16.dim(todayMarker));
|
|
4392
5305
|
console.log("");
|
|
4393
5306
|
const priorities = ["critical", "high", "medium", "low"];
|
|
4394
5307
|
for (const priority of priorities) {
|
|
@@ -4406,13 +5319,12 @@ async function ganttCommand(options) {
|
|
|
4406
5319
|
const summaryParts = [];
|
|
4407
5320
|
if (inProgress > 0) summaryParts.push(`${inProgress} in-progress`);
|
|
4408
5321
|
if (planned > 0) summaryParts.push(`${planned} planned`);
|
|
4409
|
-
if (overdue > 0) summaryParts.push(
|
|
4410
|
-
console.log(
|
|
4411
|
-
console.log(
|
|
5322
|
+
if (overdue > 0) summaryParts.push(chalk16.red(`${overdue} overdue`));
|
|
5323
|
+
console.log(chalk16.bold("Summary: ") + summaryParts.join(" \xB7 "));
|
|
5324
|
+
console.log(chalk16.dim('\u{1F4A1} Tip: Add "due: YYYY-MM-DD" to frontmatter for timeline planning'));
|
|
4412
5325
|
}
|
|
4413
5326
|
function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
4414
5327
|
const statusConfig = STATUS_CONFIG2[spec.frontmatter.status];
|
|
4415
|
-
const timelineColumnWidth = weeks * 8;
|
|
4416
5328
|
const emoji = statusConfig.emoji;
|
|
4417
5329
|
const maxNameLength = SPEC_COLUMN_WIDTH - 2;
|
|
4418
5330
|
let specName = spec.name;
|
|
@@ -4422,7 +5334,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
4422
5334
|
const specColumn = `${SPEC_INDENT}${emoji} ${specName}`.padEnd(SPEC_COLUMN_WIDTH);
|
|
4423
5335
|
let timelineColumn;
|
|
4424
5336
|
if (!spec.frontmatter.due) {
|
|
4425
|
-
timelineColumn =
|
|
5337
|
+
timelineColumn = chalk16.dim("(no due date set)");
|
|
4426
5338
|
} else {
|
|
4427
5339
|
timelineColumn = renderTimelineBar(spec, startDate, endDate, weeks, today);
|
|
4428
5340
|
}
|
|
@@ -4431,7 +5343,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
4431
5343
|
function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
4432
5344
|
const charsPerWeek = 8;
|
|
4433
5345
|
const totalChars = weeks * charsPerWeek;
|
|
4434
|
-
const due =
|
|
5346
|
+
const due = dayjs3(spec.frontmatter.due);
|
|
4435
5347
|
const specStart = today;
|
|
4436
5348
|
const startDaysFromStart = specStart.diff(startDate, "day");
|
|
4437
5349
|
const dueDaysFromStart = due.diff(startDate, "day");
|
|
@@ -4445,13 +5357,13 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
4445
5357
|
result += " ".repeat(barStart);
|
|
4446
5358
|
}
|
|
4447
5359
|
if (spec.frontmatter.status === "complete") {
|
|
4448
|
-
result +=
|
|
5360
|
+
result += chalk16.green(FILLED_BAR_CHAR.repeat(barLength));
|
|
4449
5361
|
} else if (spec.frontmatter.status === "in-progress") {
|
|
4450
5362
|
const halfLength = Math.floor(barLength / 2);
|
|
4451
|
-
result +=
|
|
4452
|
-
result +=
|
|
5363
|
+
result += chalk16.yellow(FILLED_BAR_CHAR.repeat(halfLength));
|
|
5364
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
|
|
4453
5365
|
} else {
|
|
4454
|
-
result +=
|
|
5366
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength));
|
|
4455
5367
|
}
|
|
4456
5368
|
const trailingSpace = totalChars - barEnd;
|
|
4457
5369
|
if (trailingSpace > 0) {
|
|
@@ -4459,18 +5371,601 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
4459
5371
|
}
|
|
4460
5372
|
return result;
|
|
4461
5373
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
5374
|
+
async function tokensCommand(specPath, options = {}) {
|
|
5375
|
+
await autoCheckIfEnabled();
|
|
5376
|
+
const counter = new TokenCounter();
|
|
5377
|
+
try {
|
|
5378
|
+
const config = await loadConfig();
|
|
5379
|
+
const cwd = process.cwd();
|
|
5380
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
5381
|
+
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5382
|
+
if (!resolvedPath) {
|
|
5383
|
+
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5384
|
+
}
|
|
5385
|
+
const specName = path2.basename(resolvedPath);
|
|
5386
|
+
const result = await counter.countSpec(resolvedPath, {
|
|
5387
|
+
detailed: options.detailed,
|
|
5388
|
+
includeSubSpecs: options.includeSubSpecs
|
|
5389
|
+
});
|
|
5390
|
+
if (options.json) {
|
|
5391
|
+
console.log(JSON.stringify({
|
|
5392
|
+
spec: specName,
|
|
5393
|
+
path: resolvedPath,
|
|
5394
|
+
...result
|
|
5395
|
+
}, null, 2));
|
|
5396
|
+
return;
|
|
5397
|
+
}
|
|
5398
|
+
console.log(chalk16.bold.cyan(`\u{1F4CA} Token Count: ${specName}`));
|
|
5399
|
+
console.log("");
|
|
5400
|
+
const indicators = counter.getPerformanceIndicators(result.total);
|
|
5401
|
+
const levelEmoji = indicators.level === "excellent" ? "\u2705" : indicators.level === "good" ? "\u{1F44D}" : indicators.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5402
|
+
console.log(` Total: ${chalk16.cyan(result.total.toLocaleString())} tokens ${levelEmoji}`);
|
|
5403
|
+
console.log("");
|
|
5404
|
+
if (result.files.length > 1 || options.detailed) {
|
|
5405
|
+
console.log(chalk16.bold("Files:"));
|
|
5406
|
+
console.log("");
|
|
5407
|
+
for (const file of result.files) {
|
|
5408
|
+
const lineInfo = file.lines ? chalk16.dim(` (${file.lines} lines)`) : "";
|
|
5409
|
+
console.log(` ${file.path.padEnd(25)} ${chalk16.cyan(file.tokens.toLocaleString().padStart(6))} tokens${lineInfo}`);
|
|
5410
|
+
}
|
|
5411
|
+
console.log("");
|
|
5412
|
+
}
|
|
5413
|
+
if (options.detailed && result.breakdown) {
|
|
5414
|
+
const b = result.breakdown;
|
|
5415
|
+
const total = b.code + b.prose + b.tables + b.frontmatter;
|
|
5416
|
+
console.log(chalk16.bold("Content Breakdown:"));
|
|
5417
|
+
console.log("");
|
|
5418
|
+
console.log(` Prose ${chalk16.cyan(b.prose.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.prose / total * 100)}%)`)}`);
|
|
5419
|
+
console.log(` Code ${chalk16.cyan(b.code.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.code / total * 100)}%)`)}`);
|
|
5420
|
+
console.log(` Tables ${chalk16.cyan(b.tables.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.tables / total * 100)}%)`)}`);
|
|
5421
|
+
console.log(` Frontmatter ${chalk16.cyan(b.frontmatter.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.frontmatter / total * 100)}%)`)}`);
|
|
5422
|
+
console.log("");
|
|
5423
|
+
}
|
|
5424
|
+
console.log(chalk16.bold("Performance Indicators:"));
|
|
5425
|
+
console.log("");
|
|
5426
|
+
const costColor = indicators.costMultiplier < 2 ? chalk16.green : indicators.costMultiplier < 4 ? chalk16.yellow : chalk16.red;
|
|
5427
|
+
const effectivenessColor = indicators.effectiveness >= 95 ? chalk16.green : indicators.effectiveness >= 85 ? chalk16.yellow : chalk16.red;
|
|
5428
|
+
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${chalk16.dim("vs 1,200 token baseline")}`);
|
|
5429
|
+
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${chalk16.dim("(hypothesis)")}`);
|
|
5430
|
+
console.log(` Context Economy: ${levelEmoji} ${indicators.recommendation}`);
|
|
5431
|
+
console.log("");
|
|
5432
|
+
if (!options.includeSubSpecs && result.files.length === 1) {
|
|
5433
|
+
console.log(chalk16.dim("\u{1F4A1} Use `--include-sub-specs` to count all sub-spec files"));
|
|
5434
|
+
}
|
|
5435
|
+
} finally {
|
|
5436
|
+
counter.dispose();
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5439
|
+
async function tokensAllCommand(options = {}) {
|
|
5440
|
+
await autoCheckIfEnabled();
|
|
5441
|
+
const specs = await withSpinner(
|
|
5442
|
+
"Loading specs...",
|
|
5443
|
+
() => loadAllSpecs({ includeArchived: false })
|
|
5444
|
+
);
|
|
5445
|
+
if (specs.length === 0) {
|
|
5446
|
+
console.log("No specs found.");
|
|
5447
|
+
return;
|
|
5448
|
+
}
|
|
5449
|
+
const counter = new TokenCounter();
|
|
5450
|
+
const results = [];
|
|
5451
|
+
try {
|
|
5452
|
+
for (const spec of specs) {
|
|
5453
|
+
const result = await counter.countSpec(spec.fullPath, {
|
|
5454
|
+
includeSubSpecs: options.includeSubSpecs
|
|
5455
|
+
});
|
|
5456
|
+
const indicators = counter.getPerformanceIndicators(result.total);
|
|
5457
|
+
const totalLines = result.files.reduce((sum, f) => sum + (f.lines || 0), 0);
|
|
5458
|
+
results.push({
|
|
5459
|
+
name: spec.name,
|
|
5460
|
+
path: spec.fullPath,
|
|
5461
|
+
tokens: result.total,
|
|
5462
|
+
lines: totalLines,
|
|
5463
|
+
level: indicators.level
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
} finally {
|
|
5467
|
+
counter.dispose();
|
|
5468
|
+
}
|
|
5469
|
+
const sortBy = options.sortBy || "tokens";
|
|
5470
|
+
results.sort((a, b) => {
|
|
5471
|
+
if (sortBy === "tokens") return b.tokens - a.tokens;
|
|
5472
|
+
if (sortBy === "lines") return b.lines - a.lines;
|
|
5473
|
+
return a.name.localeCompare(b.name);
|
|
5474
|
+
});
|
|
5475
|
+
if (options.json) {
|
|
5476
|
+
console.log(JSON.stringify(results, null, 2));
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
console.log(chalk16.bold.cyan("\u{1F4CA} Token Counts"));
|
|
5480
|
+
console.log("");
|
|
5481
|
+
console.log(chalk16.dim(`Sorted by: ${sortBy}`));
|
|
5482
|
+
console.log("");
|
|
5483
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
5484
|
+
const avgTokens = Math.round(totalTokens / results.length);
|
|
5485
|
+
const warningCount = results.filter((r) => r.level === "warning" || r.level === "problem").length;
|
|
5486
|
+
console.log(chalk16.bold("Summary:"));
|
|
5487
|
+
console.log("");
|
|
5488
|
+
console.log(` Total specs: ${chalk16.cyan(results.length)}`);
|
|
5489
|
+
console.log(` Total tokens: ${chalk16.cyan(totalTokens.toLocaleString())}`);
|
|
5490
|
+
console.log(` Average tokens: ${chalk16.cyan(avgTokens.toLocaleString())}`);
|
|
5491
|
+
if (warningCount > 0) {
|
|
5492
|
+
console.log(` Needs review: ${chalk16.yellow(warningCount)} specs ${chalk16.dim("(\u26A0\uFE0F or \u{1F534})")}`);
|
|
5493
|
+
}
|
|
5494
|
+
console.log("");
|
|
5495
|
+
const nameCol = 35;
|
|
5496
|
+
const tokensCol = 10;
|
|
5497
|
+
const linesCol = 8;
|
|
5498
|
+
console.log(chalk16.bold(
|
|
5499
|
+
"Spec".padEnd(nameCol) + "Tokens".padStart(tokensCol) + "Lines".padStart(linesCol) + " Status"
|
|
5500
|
+
));
|
|
5501
|
+
console.log(chalk16.dim("\u2500".repeat(nameCol + tokensCol + linesCol + 10)));
|
|
5502
|
+
const displayCount = options.all ? results.length : Math.min(20, results.length);
|
|
5503
|
+
for (let i = 0; i < displayCount; i++) {
|
|
5504
|
+
const r = results[i];
|
|
5505
|
+
const emoji = r.level === "excellent" ? "\u2705" : r.level === "good" ? "\u{1F44D}" : r.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5506
|
+
const tokensColor = r.level === "excellent" || r.level === "good" ? chalk16.cyan : r.level === "warning" ? chalk16.yellow : chalk16.red;
|
|
5507
|
+
const name = r.name.length > nameCol - 2 ? r.name.substring(0, nameCol - 3) + "\u2026" : r.name;
|
|
5508
|
+
console.log(
|
|
5509
|
+
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) + chalk16.dim(r.lines.toString().padStart(linesCol)) + ` ${emoji}`
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
if (results.length > displayCount) {
|
|
5513
|
+
console.log("");
|
|
5514
|
+
console.log(chalk16.dim(`... and ${results.length - displayCount} more specs`));
|
|
5515
|
+
console.log(chalk16.dim(`Use --all to show all specs`));
|
|
5516
|
+
}
|
|
5517
|
+
console.log("");
|
|
5518
|
+
console.log(chalk16.dim("Legend: \u2705 excellent (<2K) | \u{1F44D} good (<3.5K) | \u26A0\uFE0F warning (<5K) | \u{1F534} problem (>5K)"));
|
|
5519
|
+
console.log("");
|
|
5520
|
+
}
|
|
5521
|
+
async function analyzeCommand(specPath, options = {}) {
|
|
5522
|
+
await autoCheckIfEnabled();
|
|
5523
|
+
const counter = new TokenCounter();
|
|
5524
|
+
try {
|
|
5525
|
+
const config = await loadConfig();
|
|
5526
|
+
const cwd = process.cwd();
|
|
5527
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
5528
|
+
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5529
|
+
if (!resolvedPath) {
|
|
5530
|
+
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5531
|
+
}
|
|
5532
|
+
const specName = path2.basename(resolvedPath);
|
|
5533
|
+
const readmePath = path2.join(resolvedPath, "README.md");
|
|
5534
|
+
const content = await readFile(readmePath, "utf-8");
|
|
5535
|
+
const structure = analyzeMarkdownStructure(content);
|
|
5536
|
+
const tokenResult = await counter.countSpec(resolvedPath, {
|
|
5537
|
+
detailed: true,
|
|
5538
|
+
includeSubSpecs: false
|
|
5539
|
+
// Only analyze README.md for structure
|
|
5540
|
+
});
|
|
5541
|
+
const indicators = counter.getPerformanceIndicators(tokenResult.total);
|
|
5542
|
+
const sectionsWithTokens = await Promise.all(
|
|
5543
|
+
structure.allSections.map(async (section) => {
|
|
5544
|
+
const sectionContent = content.split("\n").slice(section.startLine - 1, section.endLine).join("\n");
|
|
5545
|
+
const sectionTokens = await counter.countTokensInContent(sectionContent);
|
|
5546
|
+
return {
|
|
5547
|
+
section: section.title,
|
|
5548
|
+
level: section.level,
|
|
5549
|
+
lineRange: [section.startLine, section.endLine],
|
|
5550
|
+
tokens: sectionTokens,
|
|
5551
|
+
subsections: section.subsections.map((s) => s.title)
|
|
5552
|
+
};
|
|
5553
|
+
})
|
|
5554
|
+
);
|
|
5555
|
+
const recommendation = generateRecommendation(tokenResult.total, structure, indicators.level);
|
|
5556
|
+
const result = {
|
|
5557
|
+
spec: specName,
|
|
5558
|
+
path: resolvedPath,
|
|
5559
|
+
metrics: {
|
|
5560
|
+
tokens: tokenResult.total,
|
|
5561
|
+
lines: structure.lines,
|
|
5562
|
+
characters: content.length,
|
|
5563
|
+
sections: structure.sectionsByLevel,
|
|
5564
|
+
codeBlocks: structure.codeBlocks,
|
|
5565
|
+
maxNesting: structure.maxNesting
|
|
5566
|
+
},
|
|
5567
|
+
threshold: {
|
|
5568
|
+
status: indicators.level,
|
|
5569
|
+
limit: getThresholdLimit(indicators.level),
|
|
5570
|
+
message: indicators.recommendation
|
|
5571
|
+
},
|
|
5572
|
+
structure: sectionsWithTokens,
|
|
5573
|
+
recommendation
|
|
5574
|
+
};
|
|
5575
|
+
if (options.json) {
|
|
5576
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
displayAnalysis(result, options.verbose);
|
|
5580
|
+
} finally {
|
|
5581
|
+
counter.dispose();
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
function generateRecommendation(tokens, structure, level) {
|
|
5585
|
+
if (tokens < 2e3) {
|
|
5586
|
+
return {
|
|
5587
|
+
action: "none",
|
|
5588
|
+
reason: "Spec is under 2,000 tokens (optimal)",
|
|
5589
|
+
confidence: "high"
|
|
5590
|
+
};
|
|
5591
|
+
}
|
|
5592
|
+
if (tokens < 3500) {
|
|
5593
|
+
return {
|
|
5594
|
+
action: "compact",
|
|
5595
|
+
reason: "Spec could benefit from removing redundancy",
|
|
5596
|
+
confidence: "medium"
|
|
5597
|
+
};
|
|
5598
|
+
}
|
|
5599
|
+
if (tokens < 5e3) {
|
|
5600
|
+
const h2Count = structure.sectionsByLevel.h2;
|
|
5601
|
+
if (h2Count >= 3) {
|
|
5602
|
+
return {
|
|
5603
|
+
action: "split",
|
|
5604
|
+
reason: `Exceeds 3,500 token threshold with ${h2Count} concerns`,
|
|
5605
|
+
confidence: "high"
|
|
5606
|
+
};
|
|
5607
|
+
} else {
|
|
5608
|
+
return {
|
|
5609
|
+
action: "split",
|
|
5610
|
+
reason: "Exceeds 3,500 token threshold",
|
|
5611
|
+
confidence: "medium"
|
|
5612
|
+
};
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
return {
|
|
5616
|
+
action: "split",
|
|
5617
|
+
reason: "Critically oversized - must split immediately",
|
|
5618
|
+
confidence: "high"
|
|
5619
|
+
};
|
|
5620
|
+
}
|
|
5621
|
+
function getThresholdLimit(level) {
|
|
5622
|
+
switch (level) {
|
|
5623
|
+
case "excellent":
|
|
5624
|
+
return 2e3;
|
|
5625
|
+
case "good":
|
|
5626
|
+
return 3500;
|
|
5627
|
+
case "warning":
|
|
5628
|
+
return 5e3;
|
|
5629
|
+
case "problem":
|
|
5630
|
+
return 5e3;
|
|
5631
|
+
default:
|
|
5632
|
+
return 2e3;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
function displayAnalysis(result, verbose) {
|
|
5636
|
+
console.log(chalk16.bold.cyan(`\u{1F4CA} Spec Analysis: ${result.spec}`));
|
|
5637
|
+
console.log("");
|
|
5638
|
+
const statusEmoji = result.threshold.status === "excellent" ? "\u2705" : result.threshold.status === "good" ? "\u{1F44D}" : result.threshold.status === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5639
|
+
const tokenColor = result.threshold.status === "excellent" || result.threshold.status === "good" ? chalk16.cyan : result.threshold.status === "warning" ? chalk16.yellow : chalk16.red;
|
|
5640
|
+
console.log(chalk16.bold("Token Count:"), tokenColor(result.metrics.tokens.toLocaleString()), "tokens", statusEmoji);
|
|
5641
|
+
console.log(chalk16.dim(` Threshold: ${result.threshold.limit.toLocaleString()} tokens`));
|
|
5642
|
+
console.log(chalk16.dim(` Status: ${result.threshold.message}`));
|
|
5643
|
+
console.log("");
|
|
5644
|
+
console.log(chalk16.bold("Structure:"));
|
|
5645
|
+
console.log(` Lines: ${chalk16.cyan(result.metrics.lines.toLocaleString())}`);
|
|
5646
|
+
console.log(` Sections: ${chalk16.cyan(result.metrics.sections.total)} (H1:${result.metrics.sections.h1}, H2:${result.metrics.sections.h2}, H3:${result.metrics.sections.h3}, H4:${result.metrics.sections.h4})`);
|
|
5647
|
+
console.log(` Code blocks: ${chalk16.cyan(result.metrics.codeBlocks)}`);
|
|
5648
|
+
console.log(` Max nesting: ${chalk16.cyan(result.metrics.maxNesting)} levels`);
|
|
5649
|
+
console.log("");
|
|
5650
|
+
if (verbose && result.structure.length > 0) {
|
|
5651
|
+
const topSections = result.structure.filter((s) => s.level <= 2).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
5652
|
+
console.log(chalk16.bold("Top Sections by Size:"));
|
|
5653
|
+
console.log("");
|
|
5654
|
+
for (let i = 0; i < topSections.length; i++) {
|
|
5655
|
+
const s = topSections[i];
|
|
5656
|
+
const percentage = Math.round(s.tokens / result.metrics.tokens * 100);
|
|
5657
|
+
const indent = " ".repeat(s.level - 1);
|
|
5658
|
+
console.log(` ${i + 1}. ${indent}${s.section}`);
|
|
5659
|
+
console.log(` ${chalk16.cyan(s.tokens.toLocaleString())} tokens / ${s.lineRange[1] - s.lineRange[0] + 1} lines ${chalk16.dim(`(${percentage}%)`)}`);
|
|
5660
|
+
console.log(chalk16.dim(` Lines ${s.lineRange[0]}-${s.lineRange[1]}`));
|
|
5661
|
+
}
|
|
5662
|
+
console.log("");
|
|
5663
|
+
}
|
|
5664
|
+
const actionColor = result.recommendation.action === "none" ? chalk16.green : result.recommendation.action === "compact" ? chalk16.yellow : result.recommendation.action === "split" ? chalk16.red : chalk16.blue;
|
|
5665
|
+
console.log(chalk16.bold("Recommendation:"), actionColor(result.recommendation.action.toUpperCase()));
|
|
5666
|
+
console.log(chalk16.dim(` ${result.recommendation.reason}`));
|
|
5667
|
+
console.log(chalk16.dim(` Confidence: ${result.recommendation.confidence}`));
|
|
5668
|
+
console.log("");
|
|
5669
|
+
if (result.recommendation.action === "split") {
|
|
5670
|
+
console.log(chalk16.dim("\u{1F4A1} Use `lean-spec split` to partition into sub-specs"));
|
|
5671
|
+
console.log(chalk16.dim("\u{1F4A1} Consider splitting by H2 sections (concerns)"));
|
|
5672
|
+
} else if (result.recommendation.action === "compact") {
|
|
5673
|
+
console.log(chalk16.dim("\u{1F4A1} Use `lean-spec compact` to remove redundancy"));
|
|
5674
|
+
}
|
|
5675
|
+
console.log("");
|
|
5676
|
+
}
|
|
5677
|
+
async function splitCommand(specPath, options) {
|
|
5678
|
+
await autoCheckIfEnabled();
|
|
5679
|
+
try {
|
|
5680
|
+
if (!options.outputs || options.outputs.length === 0) {
|
|
5681
|
+
throw new Error("At least one --output option is required");
|
|
5682
|
+
}
|
|
5683
|
+
const config = await loadConfig();
|
|
5684
|
+
const cwd = process.cwd();
|
|
5685
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
5686
|
+
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5687
|
+
if (!resolvedPath) {
|
|
5688
|
+
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5689
|
+
}
|
|
5690
|
+
const specName = path2.basename(resolvedPath);
|
|
5691
|
+
const readmePath = path2.join(resolvedPath, "README.md");
|
|
5692
|
+
const content = await readFile(readmePath, "utf-8");
|
|
5693
|
+
const parsedOutputs = parseOutputSpecs(options.outputs);
|
|
5694
|
+
validateNoOverlaps(parsedOutputs);
|
|
5695
|
+
const extractions = [];
|
|
5696
|
+
for (const output of parsedOutputs) {
|
|
5697
|
+
const extracted = extractLines(content, output.startLine, output.endLine);
|
|
5698
|
+
const lineCount = countLines(extracted);
|
|
5699
|
+
extractions.push({
|
|
5700
|
+
file: output.file,
|
|
5701
|
+
content: extracted,
|
|
5702
|
+
tokens: 0,
|
|
5703
|
+
// Will be calculated in dry-run or actual execution
|
|
5704
|
+
lines: lineCount
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
if (options.dryRun) {
|
|
5708
|
+
await displayDryRun(specName, extractions);
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
await executeSplit(resolvedPath, specName, content, extractions, options);
|
|
5712
|
+
} catch (error) {
|
|
5713
|
+
if (error instanceof Error) {
|
|
5714
|
+
console.error(chalk16.red(`Error: ${error.message}`));
|
|
5715
|
+
}
|
|
5716
|
+
throw error;
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
function parseOutputSpecs(outputs) {
|
|
5720
|
+
const parsed = [];
|
|
5721
|
+
for (const output of outputs) {
|
|
5722
|
+
const match = output.lines.match(/^(\d+)-(\d+)$/);
|
|
5723
|
+
if (!match) {
|
|
5724
|
+
throw new Error(`Invalid line range format: ${output.lines}. Expected format: "1-150"`);
|
|
5725
|
+
}
|
|
5726
|
+
const startLine = parseInt(match[1], 10);
|
|
5727
|
+
const endLine = parseInt(match[2], 10);
|
|
5728
|
+
if (startLine < 1 || endLine < startLine) {
|
|
5729
|
+
throw new Error(`Invalid line range: ${output.lines}`);
|
|
5730
|
+
}
|
|
5731
|
+
parsed.push({
|
|
5732
|
+
file: output.file,
|
|
5733
|
+
startLine,
|
|
5734
|
+
endLine
|
|
5735
|
+
});
|
|
5736
|
+
}
|
|
5737
|
+
return parsed;
|
|
5738
|
+
}
|
|
5739
|
+
function validateNoOverlaps(outputs) {
|
|
5740
|
+
const sorted = [...outputs].sort((a, b) => a.startLine - b.startLine);
|
|
5741
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
5742
|
+
const current = sorted[i];
|
|
5743
|
+
const next = sorted[i + 1];
|
|
5744
|
+
if (current.endLine >= next.startLine) {
|
|
5745
|
+
throw new Error(
|
|
5746
|
+
`Overlapping line ranges: ${current.file} (${current.startLine}-${current.endLine}) overlaps with ${next.file} (${next.startLine}-${next.endLine})`
|
|
5747
|
+
);
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
async function displayDryRun(specName, extractions) {
|
|
5752
|
+
console.log(chalk16.bold.cyan(`\u{1F4CB} Split Preview: ${specName}`));
|
|
5753
|
+
console.log("");
|
|
5754
|
+
console.log(chalk16.bold("Would create:"));
|
|
5755
|
+
console.log("");
|
|
5756
|
+
for (const ext of extractions) {
|
|
5757
|
+
console.log(` ${chalk16.cyan(ext.file)}`);
|
|
5758
|
+
console.log(` Lines: ${ext.lines}`);
|
|
5759
|
+
const previewLines = ext.content.split("\n").slice(0, 3);
|
|
5760
|
+
console.log(chalk16.dim(" Preview:"));
|
|
5761
|
+
for (const line of previewLines) {
|
|
5762
|
+
console.log(chalk16.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
5763
|
+
}
|
|
5764
|
+
console.log("");
|
|
5765
|
+
}
|
|
5766
|
+
console.log(chalk16.dim("No files modified (dry run)"));
|
|
5767
|
+
console.log(chalk16.dim("Run without --dry-run to apply changes"));
|
|
5768
|
+
console.log("");
|
|
5769
|
+
}
|
|
5770
|
+
async function executeSplit(specPath, specName, originalContent, extractions, options) {
|
|
5771
|
+
console.log(chalk16.bold.cyan(`\u2702\uFE0F Splitting: ${specName}`));
|
|
5772
|
+
console.log("");
|
|
5773
|
+
const frontmatter = parseFrontmatterFromString(originalContent);
|
|
5774
|
+
for (const ext of extractions) {
|
|
5775
|
+
const outputPath = path2.join(specPath, ext.file);
|
|
5776
|
+
let finalContent = ext.content;
|
|
5777
|
+
if (ext.file === "README.md" && frontmatter) {
|
|
5778
|
+
const { content: contentWithFrontmatter } = createUpdatedFrontmatter(
|
|
5779
|
+
ext.content,
|
|
5780
|
+
frontmatter
|
|
5781
|
+
);
|
|
5782
|
+
finalContent = contentWithFrontmatter;
|
|
5783
|
+
}
|
|
5784
|
+
await writeFile(outputPath, finalContent, "utf-8");
|
|
5785
|
+
console.log(chalk16.green(`\u2713 Created ${ext.file} (${ext.lines} lines)`));
|
|
5786
|
+
}
|
|
5787
|
+
if (options.updateRefs) {
|
|
5788
|
+
const readmePath = path2.join(specPath, "README.md");
|
|
5789
|
+
const readmeContent = await readFile(readmePath, "utf-8");
|
|
5790
|
+
const updatedReadme = await addSubSpecLinks(
|
|
5791
|
+
readmeContent,
|
|
5792
|
+
extractions.map((e) => e.file).filter((f) => f !== "README.md")
|
|
5793
|
+
);
|
|
5794
|
+
await writeFile(readmePath, updatedReadme, "utf-8");
|
|
5795
|
+
console.log(chalk16.green(`\u2713 Updated README.md with sub-spec links`));
|
|
5796
|
+
}
|
|
5797
|
+
console.log("");
|
|
5798
|
+
console.log(chalk16.bold.green("Split complete!"));
|
|
5799
|
+
console.log(chalk16.dim(`Created ${extractions.length} files in ${specName}`));
|
|
5800
|
+
console.log("");
|
|
5801
|
+
}
|
|
5802
|
+
async function addSubSpecLinks(content, subSpecs) {
|
|
5803
|
+
if (subSpecs.length === 0) {
|
|
5804
|
+
return content;
|
|
5805
|
+
}
|
|
5806
|
+
if (content.includes("## Sub-Specs") || content.includes("## Sub-specs")) {
|
|
5807
|
+
return content;
|
|
5808
|
+
}
|
|
5809
|
+
const lines = content.split("\n");
|
|
5810
|
+
let insertIndex = -1;
|
|
5811
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5812
|
+
const line = lines[i].toLowerCase();
|
|
5813
|
+
if (line.includes("## implementation") || line.includes("## plan") || line.includes("## test")) {
|
|
5814
|
+
insertIndex = i;
|
|
5815
|
+
break;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
if (insertIndex === -1) {
|
|
5819
|
+
insertIndex = lines.length;
|
|
5820
|
+
}
|
|
5821
|
+
const subSpecsSection = [
|
|
5822
|
+
"",
|
|
5823
|
+
"## Sub-Specs",
|
|
5824
|
+
"",
|
|
5825
|
+
"This spec is organized using sub-spec files:",
|
|
5826
|
+
"",
|
|
5827
|
+
...subSpecs.map((file) => {
|
|
5828
|
+
const name = file.replace(".md", "");
|
|
5829
|
+
return `- **[${name}](./${file})** - ${getFileDescription(file)}`;
|
|
5830
|
+
}),
|
|
5831
|
+
""
|
|
5832
|
+
];
|
|
5833
|
+
lines.splice(insertIndex, 0, ...subSpecsSection);
|
|
5834
|
+
return lines.join("\n");
|
|
5835
|
+
}
|
|
5836
|
+
function getFileDescription(file) {
|
|
5837
|
+
const lower = file.toLowerCase();
|
|
5838
|
+
if (lower.includes("design")) return "Architecture and design details";
|
|
5839
|
+
if (lower.includes("implementation")) return "Implementation plan and phases";
|
|
5840
|
+
if (lower.includes("testing") || lower.includes("test")) return "Test strategy and cases";
|
|
5841
|
+
if (lower.includes("rationale")) return "Design rationale and decisions";
|
|
5842
|
+
if (lower.includes("api")) return "API specification";
|
|
5843
|
+
if (lower.includes("migration")) return "Migration plan and strategy";
|
|
5844
|
+
if (lower.includes("context")) return "Context and research";
|
|
5845
|
+
return "Additional documentation";
|
|
5846
|
+
}
|
|
5847
|
+
async function compactCommand(specPath, options) {
|
|
5848
|
+
await autoCheckIfEnabled();
|
|
5849
|
+
try {
|
|
5850
|
+
if (!options.removes || options.removes.length === 0) {
|
|
5851
|
+
throw new Error("At least one --remove option is required");
|
|
5852
|
+
}
|
|
5853
|
+
const config = await loadConfig();
|
|
5854
|
+
const cwd = process.cwd();
|
|
5855
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
5856
|
+
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5857
|
+
if (!resolvedPath) {
|
|
5858
|
+
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5859
|
+
}
|
|
5860
|
+
const specName = path2.basename(resolvedPath);
|
|
5861
|
+
const readmePath = path2.join(resolvedPath, "README.md");
|
|
5862
|
+
const content = await readFile(readmePath, "utf-8");
|
|
5863
|
+
const parsedRemoves = parseRemoveSpecs(options.removes);
|
|
5864
|
+
validateNoOverlaps2(parsedRemoves);
|
|
5865
|
+
if (options.dryRun) {
|
|
5866
|
+
await displayDryRun2(specName, content, parsedRemoves);
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5869
|
+
await executeCompact(readmePath, specName, content, parsedRemoves);
|
|
5870
|
+
} catch (error) {
|
|
5871
|
+
if (error instanceof Error) {
|
|
5872
|
+
console.error(chalk16.red(`Error: ${error.message}`));
|
|
5873
|
+
}
|
|
5874
|
+
throw error;
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
function parseRemoveSpecs(removes) {
|
|
5878
|
+
const parsed = [];
|
|
5879
|
+
for (let i = 0; i < removes.length; i++) {
|
|
5880
|
+
const spec = removes[i];
|
|
5881
|
+
const match = spec.match(/^(\d+)-(\d+)$/);
|
|
5882
|
+
if (!match) {
|
|
5883
|
+
throw new Error(`Invalid line range format: ${spec}. Expected format: "145-153"`);
|
|
5884
|
+
}
|
|
5885
|
+
const startLine = parseInt(match[1], 10);
|
|
5886
|
+
const endLine = parseInt(match[2], 10);
|
|
5887
|
+
if (startLine < 1 || endLine < startLine) {
|
|
5888
|
+
throw new Error(`Invalid line range: ${spec}`);
|
|
5889
|
+
}
|
|
5890
|
+
parsed.push({
|
|
5891
|
+
startLine,
|
|
5892
|
+
endLine,
|
|
5893
|
+
originalIndex: i
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5896
|
+
return parsed;
|
|
5897
|
+
}
|
|
5898
|
+
function validateNoOverlaps2(removes) {
|
|
5899
|
+
const sorted = [...removes].sort((a, b) => a.startLine - b.startLine);
|
|
5900
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
5901
|
+
const current = sorted[i];
|
|
5902
|
+
const next = sorted[i + 1];
|
|
5903
|
+
if (current.endLine >= next.startLine) {
|
|
5904
|
+
throw new Error(
|
|
5905
|
+
`Overlapping line ranges: ${current.startLine}-${current.endLine} overlaps with ${next.startLine}-${next.endLine}`
|
|
5906
|
+
);
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
async function displayDryRun2(specName, content, removes) {
|
|
5911
|
+
console.log(chalk16.bold.cyan(`\u{1F4CB} Compact Preview: ${specName}`));
|
|
5912
|
+
console.log("");
|
|
5913
|
+
console.log(chalk16.bold("Would remove:"));
|
|
5914
|
+
console.log("");
|
|
5915
|
+
let totalLines = 0;
|
|
5916
|
+
for (const remove of removes) {
|
|
5917
|
+
const lineCount = remove.endLine - remove.startLine + 1;
|
|
5918
|
+
totalLines += lineCount;
|
|
5919
|
+
const removedContent = extractLines(content, remove.startLine, remove.endLine);
|
|
5920
|
+
const previewLines = removedContent.split("\n").slice(0, 3);
|
|
5921
|
+
console.log(` Lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`);
|
|
5922
|
+
console.log(chalk16.dim(" Preview:"));
|
|
5923
|
+
for (const line of previewLines) {
|
|
5924
|
+
console.log(chalk16.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
5925
|
+
}
|
|
5926
|
+
if (removedContent.split("\n").length > 3) {
|
|
5927
|
+
console.log(chalk16.dim(` ... (${removedContent.split("\n").length - 3} more lines)`));
|
|
5928
|
+
}
|
|
5929
|
+
console.log("");
|
|
5930
|
+
}
|
|
5931
|
+
const originalLines = countLines(content);
|
|
5932
|
+
const remainingLines = originalLines - totalLines;
|
|
5933
|
+
const percentage = Math.round(totalLines / originalLines * 100);
|
|
5934
|
+
console.log(chalk16.bold("Summary:"));
|
|
5935
|
+
console.log(` Original lines: ${chalk16.cyan(originalLines)}`);
|
|
5936
|
+
console.log(` Removing: ${chalk16.yellow(totalLines)} lines (${percentage}%)`);
|
|
5937
|
+
console.log(` Remaining lines: ${chalk16.cyan(remainingLines)}`);
|
|
5938
|
+
console.log("");
|
|
5939
|
+
console.log(chalk16.dim("No files modified (dry run)"));
|
|
5940
|
+
console.log(chalk16.dim("Run without --dry-run to apply changes"));
|
|
5941
|
+
console.log("");
|
|
5942
|
+
}
|
|
5943
|
+
async function executeCompact(readmePath, specName, content, removes) {
|
|
5944
|
+
console.log(chalk16.bold.cyan(`\u{1F5DC}\uFE0F Compacting: ${specName}`));
|
|
5945
|
+
console.log("");
|
|
5946
|
+
const sorted = [...removes].sort((a, b) => b.startLine - a.startLine);
|
|
5947
|
+
let updatedContent = content;
|
|
5948
|
+
let totalRemoved = 0;
|
|
5949
|
+
for (const remove of sorted) {
|
|
5950
|
+
const lineCount = remove.endLine - remove.startLine + 1;
|
|
5951
|
+
updatedContent = removeLines(updatedContent, remove.startLine, remove.endLine);
|
|
5952
|
+
totalRemoved += lineCount;
|
|
5953
|
+
console.log(chalk16.green(`\u2713 Removed lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`));
|
|
5954
|
+
}
|
|
5955
|
+
await writeFile(readmePath, updatedContent, "utf-8");
|
|
5956
|
+
const originalLines = countLines(content);
|
|
5957
|
+
const finalLines = countLines(updatedContent);
|
|
5958
|
+
const percentage = Math.round(totalRemoved / originalLines * 100);
|
|
5959
|
+
console.log("");
|
|
5960
|
+
console.log(chalk16.bold.green("Compaction complete!"));
|
|
5961
|
+
console.log(chalk16.dim(`Removed ${totalRemoved} lines (${percentage}%)`));
|
|
5962
|
+
console.log(chalk16.dim(`${originalLines} \u2192 ${finalLines} lines`));
|
|
5963
|
+
console.log("");
|
|
5964
|
+
}
|
|
4470
5965
|
marked.use(markedTerminal());
|
|
4471
5966
|
async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
4472
5967
|
const config = await loadConfig(cwd);
|
|
4473
|
-
const specsDir =
|
|
5968
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
4474
5969
|
let resolvedPath = null;
|
|
4475
5970
|
let targetFile = null;
|
|
4476
5971
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -4479,9 +5974,9 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4479
5974
|
const filePart = pathParts[pathParts.length - 1];
|
|
4480
5975
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
4481
5976
|
if (resolvedPath) {
|
|
4482
|
-
targetFile =
|
|
5977
|
+
targetFile = path2.join(resolvedPath, filePart);
|
|
4483
5978
|
try {
|
|
4484
|
-
await
|
|
5979
|
+
await fs9.access(targetFile);
|
|
4485
5980
|
} catch {
|
|
4486
5981
|
return null;
|
|
4487
5982
|
}
|
|
@@ -4500,8 +5995,8 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4500
5995
|
if (!targetFile) {
|
|
4501
5996
|
return null;
|
|
4502
5997
|
}
|
|
4503
|
-
const rawContent = await
|
|
4504
|
-
const fileName =
|
|
5998
|
+
const rawContent = await fs9.readFile(targetFile, "utf-8");
|
|
5999
|
+
const fileName = path2.basename(targetFile);
|
|
4505
6000
|
const isSubSpec = fileName !== config.structure.defaultFile;
|
|
4506
6001
|
let frontmatter = null;
|
|
4507
6002
|
if (!isSubSpec) {
|
|
@@ -4530,7 +6025,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4530
6025
|
}
|
|
4531
6026
|
}
|
|
4532
6027
|
const content = lines.slice(contentStartIndex).join("\n").trim();
|
|
4533
|
-
const specName =
|
|
6028
|
+
const specName = path2.basename(resolvedPath);
|
|
4534
6029
|
const displayName = isSubSpec ? `${specName}/${fileName}` : specName;
|
|
4535
6030
|
return {
|
|
4536
6031
|
frontmatter,
|
|
@@ -4551,7 +6046,7 @@ function formatFrontmatter(frontmatter) {
|
|
|
4551
6046
|
archived: "\u{1F4E6}"
|
|
4552
6047
|
};
|
|
4553
6048
|
const statusEmoji = statusEmojis[frontmatter.status] || "\u{1F4C4}";
|
|
4554
|
-
lines.push(
|
|
6049
|
+
lines.push(chalk16.bold(`${statusEmoji} Status: `) + chalk16.cyan(frontmatter.status));
|
|
4555
6050
|
if (frontmatter.priority) {
|
|
4556
6051
|
const priorityEmojis = {
|
|
4557
6052
|
low: "\u{1F7E2}",
|
|
@@ -4560,25 +6055,25 @@ function formatFrontmatter(frontmatter) {
|
|
|
4560
6055
|
critical: "\u{1F534}"
|
|
4561
6056
|
};
|
|
4562
6057
|
const priorityEmoji = priorityEmojis[frontmatter.priority] || "";
|
|
4563
|
-
lines.push(
|
|
6058
|
+
lines.push(chalk16.bold(`${priorityEmoji} Priority: `) + chalk16.yellow(frontmatter.priority));
|
|
4564
6059
|
}
|
|
4565
6060
|
if (frontmatter.created) {
|
|
4566
|
-
lines.push(
|
|
6061
|
+
lines.push(chalk16.bold("\u{1F4C6} Created: ") + chalk16.gray(String(frontmatter.created)));
|
|
4567
6062
|
}
|
|
4568
6063
|
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
4569
|
-
const tagStr = frontmatter.tags.map((tag) =>
|
|
4570
|
-
lines.push(
|
|
6064
|
+
const tagStr = frontmatter.tags.map((tag) => chalk16.blue(`#${tag}`)).join(" ");
|
|
6065
|
+
lines.push(chalk16.bold("\u{1F3F7}\uFE0F Tags: ") + tagStr);
|
|
4571
6066
|
}
|
|
4572
6067
|
if (frontmatter.assignee) {
|
|
4573
|
-
lines.push(
|
|
6068
|
+
lines.push(chalk16.bold("\u{1F464} Assignee: ") + chalk16.green(frontmatter.assignee));
|
|
4574
6069
|
}
|
|
4575
6070
|
const standardFields = ["status", "priority", "created", "tags", "assignee"];
|
|
4576
6071
|
const customFields = Object.entries(frontmatter).filter(([key]) => !standardFields.includes(key)).filter(([_, value]) => value !== void 0 && value !== null);
|
|
4577
6072
|
if (customFields.length > 0) {
|
|
4578
6073
|
lines.push("");
|
|
4579
|
-
lines.push(
|
|
6074
|
+
lines.push(chalk16.bold("Custom Fields:"));
|
|
4580
6075
|
for (const [key, value] of customFields) {
|
|
4581
|
-
lines.push(` ${
|
|
6076
|
+
lines.push(` ${chalk16.gray(key)}: ${chalk16.white(String(value))}`);
|
|
4582
6077
|
}
|
|
4583
6078
|
}
|
|
4584
6079
|
return lines.join("\n");
|
|
@@ -4586,11 +6081,11 @@ function formatFrontmatter(frontmatter) {
|
|
|
4586
6081
|
function displayFormattedSpec(spec) {
|
|
4587
6082
|
const output = [];
|
|
4588
6083
|
output.push("");
|
|
4589
|
-
output.push(
|
|
6084
|
+
output.push(chalk16.bold.cyan(`\u2501\u2501\u2501 ${spec.name} \u2501\u2501\u2501`));
|
|
4590
6085
|
output.push("");
|
|
4591
6086
|
output.push(formatFrontmatter(spec.frontmatter));
|
|
4592
6087
|
output.push("");
|
|
4593
|
-
output.push(
|
|
6088
|
+
output.push(chalk16.gray("\u2500".repeat(60)));
|
|
4594
6089
|
output.push("");
|
|
4595
6090
|
return output.join("\n");
|
|
4596
6091
|
}
|
|
@@ -4620,7 +6115,7 @@ async function viewCommand(specPath, options = {}) {
|
|
|
4620
6115
|
async function openCommand(specPath, options = {}) {
|
|
4621
6116
|
const cwd = process.cwd();
|
|
4622
6117
|
const config = await loadConfig(cwd);
|
|
4623
|
-
const specsDir =
|
|
6118
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
4624
6119
|
let resolvedPath = null;
|
|
4625
6120
|
let targetFile = null;
|
|
4626
6121
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -4629,9 +6124,9 @@ async function openCommand(specPath, options = {}) {
|
|
|
4629
6124
|
const filePart = pathParts[pathParts.length - 1];
|
|
4630
6125
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
4631
6126
|
if (resolvedPath) {
|
|
4632
|
-
targetFile =
|
|
6127
|
+
targetFile = path2.join(resolvedPath, filePart);
|
|
4633
6128
|
try {
|
|
4634
|
-
await
|
|
6129
|
+
await fs9.access(targetFile);
|
|
4635
6130
|
} catch {
|
|
4636
6131
|
targetFile = null;
|
|
4637
6132
|
}
|
|
@@ -4649,7 +6144,6 @@ async function openCommand(specPath, options = {}) {
|
|
|
4649
6144
|
} else if (!targetFile) {
|
|
4650
6145
|
throw new Error(`Sub-spec file not found: ${specPath}`);
|
|
4651
6146
|
}
|
|
4652
|
-
const specFile = targetFile;
|
|
4653
6147
|
let editor = options.editor;
|
|
4654
6148
|
if (!editor) {
|
|
4655
6149
|
editor = process.env.VISUAL || process.env.EDITOR;
|
|
@@ -4664,7 +6158,7 @@ async function openCommand(specPath, options = {}) {
|
|
|
4664
6158
|
editor = "xdg-open";
|
|
4665
6159
|
}
|
|
4666
6160
|
}
|
|
4667
|
-
console.log(
|
|
6161
|
+
console.log(chalk16.gray(`Opening ${targetFile} with ${editor}...`));
|
|
4668
6162
|
const child = spawn(editor, [targetFile], {
|
|
4669
6163
|
stdio: "inherit",
|
|
4670
6164
|
shell: true
|
|
@@ -4694,9 +6188,6 @@ async function openCommand(specPath, options = {}) {
|
|
|
4694
6188
|
});
|
|
4695
6189
|
}
|
|
4696
6190
|
}
|
|
4697
|
-
|
|
4698
|
-
// src/commands/mcp.ts
|
|
4699
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4700
6191
|
async function mcpCommand() {
|
|
4701
6192
|
try {
|
|
4702
6193
|
const server = await createMcpServer();
|
|
@@ -4708,15 +6199,10 @@ async function mcpCommand() {
|
|
|
4708
6199
|
process.exit(1);
|
|
4709
6200
|
}
|
|
4710
6201
|
}
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
import { readFileSync } from "fs";
|
|
4714
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4715
|
-
import { dirname as dirname3, join as join18 } from "path";
|
|
4716
|
-
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4717
|
-
var __dirname3 = dirname3(__filename2);
|
|
6202
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
6203
|
+
var __dirname2 = dirname(__filename);
|
|
4718
6204
|
var packageJson = JSON.parse(
|
|
4719
|
-
readFileSync(
|
|
6205
|
+
readFileSync(join(__dirname2, "../package.json"), "utf-8")
|
|
4720
6206
|
);
|
|
4721
6207
|
function formatErrorMessage(prefix, error) {
|
|
4722
6208
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -4761,24 +6247,46 @@ async function searchSpecsData(query, options) {
|
|
|
4761
6247
|
includeContent: true,
|
|
4762
6248
|
filter
|
|
4763
6249
|
});
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
6250
|
+
const searchableSpecs = specs.map((spec) => ({
|
|
6251
|
+
path: spec.path,
|
|
6252
|
+
name: spec.path,
|
|
6253
|
+
status: spec.frontmatter.status,
|
|
6254
|
+
priority: spec.frontmatter.priority,
|
|
6255
|
+
tags: spec.frontmatter.tags,
|
|
6256
|
+
title: spec.frontmatter.title,
|
|
6257
|
+
description: spec.frontmatter.description,
|
|
6258
|
+
content: spec.content
|
|
6259
|
+
}));
|
|
6260
|
+
const searchResult = searchSpecs(query, searchableSpecs, {
|
|
6261
|
+
maxMatchesPerSpec: 5,
|
|
6262
|
+
contextLength: 80
|
|
6263
|
+
});
|
|
6264
|
+
return {
|
|
6265
|
+
results: searchResult.results.map((result) => ({
|
|
6266
|
+
spec: {
|
|
6267
|
+
name: result.spec.name,
|
|
6268
|
+
path: result.spec.path,
|
|
6269
|
+
status: result.spec.status,
|
|
6270
|
+
created: specs.find((s) => s.path === result.spec.path)?.frontmatter.created || "",
|
|
6271
|
+
title: result.spec.title,
|
|
6272
|
+
tags: result.spec.tags,
|
|
6273
|
+
priority: result.spec.priority,
|
|
6274
|
+
assignee: specs.find((s) => s.path === result.spec.path)?.frontmatter.assignee,
|
|
6275
|
+
description: result.spec.description,
|
|
6276
|
+
customFields: specs.find((s) => s.path === result.spec.path)?.frontmatter.custom
|
|
6277
|
+
},
|
|
6278
|
+
score: result.score,
|
|
6279
|
+
totalMatches: result.totalMatches,
|
|
6280
|
+
matches: result.matches.map((match) => ({
|
|
6281
|
+
field: match.field,
|
|
6282
|
+
text: match.text,
|
|
6283
|
+
lineNumber: match.lineNumber,
|
|
6284
|
+
score: match.score,
|
|
6285
|
+
highlights: match.highlights
|
|
6286
|
+
}))
|
|
6287
|
+
})),
|
|
6288
|
+
metadata: searchResult.metadata
|
|
6289
|
+
};
|
|
4782
6290
|
}
|
|
4783
6291
|
async function readSpecData(specPath) {
|
|
4784
6292
|
const cwd = process.cwd();
|
|
@@ -4914,28 +6422,44 @@ async function createMcpServer() {
|
|
|
4914
6422
|
"search",
|
|
4915
6423
|
{
|
|
4916
6424
|
title: "Search Specs",
|
|
4917
|
-
description: "
|
|
6425
|
+
description: "Intelligent relevance-ranked search across all specification content. Uses field-weighted scoring (title > tags > description > content) to return the most relevant specs. Returns matching specs with relevance scores, highlighted excerpts, and metadata.",
|
|
4918
6426
|
inputSchema: {
|
|
4919
|
-
query: z.string().describe("Search term or phrase to find in spec content. Searches across titles, descriptions, and body text."),
|
|
6427
|
+
query: z.string().describe("Search term or phrase to find in spec content. Multiple terms are combined with AND logic. Searches across titles, tags, descriptions, and body text with intelligent relevance ranking."),
|
|
4920
6428
|
status: z.enum(["planned", "in-progress", "complete", "archived"]).optional().describe("Limit search to specs with this status."),
|
|
4921
6429
|
tags: z.array(z.string()).optional().describe("Limit search to specs with these tags."),
|
|
4922
6430
|
priority: z.enum(["low", "medium", "high", "critical"]).optional().describe("Limit search to specs with this priority.")
|
|
4923
6431
|
},
|
|
4924
6432
|
outputSchema: {
|
|
4925
|
-
results: z.array(z.
|
|
6433
|
+
results: z.array(z.object({
|
|
6434
|
+
spec: z.any(),
|
|
6435
|
+
score: z.number(),
|
|
6436
|
+
totalMatches: z.number(),
|
|
6437
|
+
matches: z.array(z.object({
|
|
6438
|
+
field: z.string(),
|
|
6439
|
+
text: z.string(),
|
|
6440
|
+
lineNumber: z.number().optional(),
|
|
6441
|
+
score: z.number(),
|
|
6442
|
+
highlights: z.array(z.tuple([z.number(), z.number()]))
|
|
6443
|
+
}))
|
|
6444
|
+
})),
|
|
6445
|
+
metadata: z.object({
|
|
6446
|
+
totalResults: z.number(),
|
|
6447
|
+
searchTime: z.number(),
|
|
6448
|
+
query: z.string(),
|
|
6449
|
+
specsSearched: z.number()
|
|
6450
|
+
})
|
|
4926
6451
|
}
|
|
4927
6452
|
},
|
|
4928
6453
|
async (input) => {
|
|
4929
6454
|
try {
|
|
4930
|
-
const
|
|
6455
|
+
const searchResult = await searchSpecsData(input.query, {
|
|
4931
6456
|
status: input.status,
|
|
4932
6457
|
tags: input.tags,
|
|
4933
6458
|
priority: input.priority
|
|
4934
6459
|
});
|
|
4935
|
-
const output = { results };
|
|
4936
6460
|
return {
|
|
4937
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
4938
|
-
structuredContent:
|
|
6461
|
+
content: [{ type: "text", text: JSON.stringify(searchResult, null, 2) }],
|
|
6462
|
+
structuredContent: searchResult
|
|
4939
6463
|
};
|
|
4940
6464
|
} catch (error) {
|
|
4941
6465
|
const errorMessage = formatErrorMessage("Error searching specs", error);
|
|
@@ -5447,6 +6971,81 @@ ${result.content}`;
|
|
|
5447
6971
|
}
|
|
5448
6972
|
}
|
|
5449
6973
|
);
|
|
6974
|
+
server.registerTool(
|
|
6975
|
+
"tokens",
|
|
6976
|
+
{
|
|
6977
|
+
title: "Count Tokens",
|
|
6978
|
+
description: "Count tokens in spec or sub-spec for LLM context management. Use this before loading specs to check if they fit in context budget.",
|
|
6979
|
+
inputSchema: {
|
|
6980
|
+
specPath: z.string().describe('Spec name, number, or file path (e.g., "059", "unified-dashboard", "059/DESIGN.md")'),
|
|
6981
|
+
includeSubSpecs: z.boolean().optional().describe("Include all sub-spec files in count (default: false)"),
|
|
6982
|
+
detailed: z.boolean().optional().describe("Return breakdown by content type (code, prose, tables, frontmatter)")
|
|
6983
|
+
},
|
|
6984
|
+
outputSchema: {
|
|
6985
|
+
spec: z.string(),
|
|
6986
|
+
total: z.number(),
|
|
6987
|
+
files: z.array(z.any()),
|
|
6988
|
+
breakdown: z.any().optional(),
|
|
6989
|
+
performance: z.any().optional(),
|
|
6990
|
+
recommendation: z.string().optional()
|
|
6991
|
+
}
|
|
6992
|
+
},
|
|
6993
|
+
async (input) => {
|
|
6994
|
+
const counter = new TokenCounter();
|
|
6995
|
+
try {
|
|
6996
|
+
const config = await loadConfig();
|
|
6997
|
+
const cwd = process.cwd();
|
|
6998
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
6999
|
+
const resolvedPath = await resolveSpecPath(input.specPath, cwd, specsDir);
|
|
7000
|
+
if (!resolvedPath) {
|
|
7001
|
+
return {
|
|
7002
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
7003
|
+
error: `Spec not found: ${input.specPath}`,
|
|
7004
|
+
code: "SPEC_NOT_FOUND"
|
|
7005
|
+
}, null, 2) }],
|
|
7006
|
+
isError: true
|
|
7007
|
+
};
|
|
7008
|
+
}
|
|
7009
|
+
const specName = path2.basename(resolvedPath);
|
|
7010
|
+
const result = await counter.countSpec(resolvedPath, {
|
|
7011
|
+
detailed: input.detailed,
|
|
7012
|
+
includeSubSpecs: input.includeSubSpecs
|
|
7013
|
+
});
|
|
7014
|
+
const output = {
|
|
7015
|
+
spec: specName,
|
|
7016
|
+
total: result.total,
|
|
7017
|
+
files: result.files
|
|
7018
|
+
};
|
|
7019
|
+
if (input.detailed && result.breakdown) {
|
|
7020
|
+
output.breakdown = result.breakdown;
|
|
7021
|
+
const indicators = counter.getPerformanceIndicators(result.total);
|
|
7022
|
+
output.performance = {
|
|
7023
|
+
level: indicators.level,
|
|
7024
|
+
costMultiplier: indicators.costMultiplier,
|
|
7025
|
+
effectiveness: indicators.effectiveness,
|
|
7026
|
+
recommendation: indicators.recommendation
|
|
7027
|
+
};
|
|
7028
|
+
}
|
|
7029
|
+
if (result.total > 5e3) {
|
|
7030
|
+
output.recommendation = "\u26A0\uFE0F Total >5K tokens - consider loading README.md only";
|
|
7031
|
+
} else if (result.total > 3500) {
|
|
7032
|
+
output.recommendation = "\u26A0\uFE0F Total >3.5K tokens - consider loading in sections";
|
|
7033
|
+
}
|
|
7034
|
+
return {
|
|
7035
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
7036
|
+
structuredContent: output
|
|
7037
|
+
};
|
|
7038
|
+
} catch (error) {
|
|
7039
|
+
const errorMessage = formatErrorMessage("Error counting tokens", error);
|
|
7040
|
+
return {
|
|
7041
|
+
content: [{ type: "text", text: errorMessage }],
|
|
7042
|
+
isError: true
|
|
7043
|
+
};
|
|
7044
|
+
} finally {
|
|
7045
|
+
counter.dispose();
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
);
|
|
5450
7049
|
server.registerResource(
|
|
5451
7050
|
"spec",
|
|
5452
7051
|
new ResourceTemplate("spec://{specPath}", { list: void 0 }),
|
|
@@ -5626,31 +7225,6 @@ Please search for this topic and show me the dependencies between related specs.
|
|
|
5626
7225
|
return server;
|
|
5627
7226
|
}
|
|
5628
7227
|
|
|
5629
|
-
export {
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
archiveSpec,
|
|
5633
|
-
listSpecs,
|
|
5634
|
-
updateSpec,
|
|
5635
|
-
backfillTimestamps,
|
|
5636
|
-
listTemplates,
|
|
5637
|
-
showTemplate,
|
|
5638
|
-
addTemplate,
|
|
5639
|
-
removeTemplate,
|
|
5640
|
-
copyTemplate,
|
|
5641
|
-
initProject,
|
|
5642
|
-
filesCommand,
|
|
5643
|
-
validateCommand,
|
|
5644
|
-
migrateCommand,
|
|
5645
|
-
boardCommand,
|
|
5646
|
-
statsCommand,
|
|
5647
|
-
searchCommand,
|
|
5648
|
-
depsCommand,
|
|
5649
|
-
timelineCommand,
|
|
5650
|
-
ganttCommand,
|
|
5651
|
-
viewCommand,
|
|
5652
|
-
openCommand,
|
|
5653
|
-
createMcpServer,
|
|
5654
|
-
mcpCommand
|
|
5655
|
-
};
|
|
5656
|
-
//# sourceMappingURL=chunk-J7ZSZ5VJ.js.map
|
|
7228
|
+
export { addTemplate, analyzeCommand, archiveSpec, backfillTimestamps, boardCommand, checkSpecs, compactCommand, copyTemplate, createMcpServer, createSpec, depsCommand, filesCommand, ganttCommand, initProject, listSpecs, listTemplates, mcpCommand, migrateCommand, openCommand, removeTemplate, searchCommand, showTemplate, splitCommand, statsCommand, timelineCommand, tokensAllCommand, tokensCommand, updateSpec, validateCommand, viewCommand };
|
|
7229
|
+
//# sourceMappingURL=chunk-7MCDTSVE.js.map
|
|
7230
|
+
//# sourceMappingURL=chunk-7MCDTSVE.js.map
|