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.
Files changed (48) hide show
  1. package/dist/{chunk-J7ZSZ5VJ.js → chunk-7MCDTSVE.js} +2457 -883
  2. package/dist/chunk-7MCDTSVE.js.map +1 -0
  3. package/dist/{chunk-S4YNQ5KE.js → chunk-LVD7ZAVZ.js} +9 -17
  4. package/dist/chunk-LVD7ZAVZ.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +52 -36
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands-GRG5UUOF.js +4 -0
  9. package/dist/commands-GRG5UUOF.js.map +1 -0
  10. package/dist/frontmatter-R2DANL5X.js +3 -0
  11. package/dist/frontmatter-R2DANL5X.js.map +1 -0
  12. package/dist/mcp-server.d.ts +16 -0
  13. package/dist/mcp-server.js +3 -7
  14. package/dist/mcp-server.js.map +1 -1
  15. package/package.json +17 -22
  16. package/templates/_shared/agents-components/core-rules-base.md +5 -0
  17. package/templates/_shared/agents-components/core-rules-enterprise.md +5 -0
  18. package/templates/_shared/agents-components/discovery-commands-enterprise.md +10 -0
  19. package/templates/_shared/agents-components/discovery-commands-minimal.md +8 -0
  20. package/templates/_shared/agents-components/discovery-commands-standard.md +9 -0
  21. package/templates/_shared/agents-components/enterprise-approval.md +10 -0
  22. package/templates/_shared/agents-components/enterprise-compliance.md +12 -0
  23. package/templates/_shared/agents-components/enterprise-when-required.md +13 -0
  24. package/templates/_shared/agents-components/frontmatter-enterprise.md +33 -0
  25. package/templates/_shared/agents-components/frontmatter-minimal.md +18 -0
  26. package/templates/_shared/agents-components/frontmatter-standard.md +23 -0
  27. package/templates/_shared/agents-components/quality-standards-base.md +5 -0
  28. package/templates/_shared/agents-components/quality-standards-enterprise.md +6 -0
  29. package/templates/_shared/agents-components/when-to-use-enterprise.md +11 -0
  30. package/templates/_shared/agents-components/when-to-use-minimal.md +9 -0
  31. package/templates/_shared/agents-components/when-to-use-standard.md +9 -0
  32. package/templates/_shared/agents-components/workflow-enterprise.md +8 -0
  33. package/templates/_shared/agents-components/workflow-standard-detailed.md +7 -0
  34. package/templates/_shared/agents-components/workflow-standard.md +5 -0
  35. package/templates/_shared/agents-template.hbs +39 -0
  36. package/templates/enterprise/agents-config.json +15 -0
  37. package/templates/enterprise/files/AGENTS.md +1 -0
  38. package/templates/minimal/agents-config.json +12 -0
  39. package/templates/minimal/files/AGENTS.md +1 -0
  40. package/templates/standard/agents-config.json +12 -0
  41. package/templates/standard/files/AGENTS.md +1 -0
  42. package/CHANGELOG.md +0 -326
  43. package/LICENSE +0 -21
  44. package/README.md +0 -421
  45. package/dist/chunk-J7ZSZ5VJ.js.map +0 -1
  46. package/dist/chunk-S4YNQ5KE.js.map +0 -1
  47. package/dist/frontmatter-26SOQGYM.js +0 -23
  48. package/dist/frontmatter-26SOQGYM.js.map +0 -1
@@ -1,23 +1,25 @@
1
- import {
2
- getSpecFile,
3
- matchesFilter,
4
- normalizeDateFields,
5
- parseFrontmatter,
6
- updateFrontmatter
7
- } from "./chunk-S4YNQ5KE.js";
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 = path.join(cwd, ".lean-spec", "config.json");
44
+ const configPath = path2.join(cwd, ".lean-spec", "config.json");
43
45
  try {
44
- const content = await fs.readFile(configPath, "utf-8");
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 = path.join(cwd, ".lean-spec");
55
- const configPath = path.join(configDir, "config.json");
56
- await fs.mkdir(configDir, { recursive: true });
57
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
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 fs2.readdir(specDir, { withFileTypes: true });
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 stat5 = await fs2.stat(filePath);
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: stat5.size,
152
+ size: stat6.size,
151
153
  type: isDocument ? "document" : "asset"
152
154
  };
153
155
  if (isDocument && options.includeContent) {
154
- subFile.content = await fs2.readFile(filePath, "utf-8");
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 fs2.access(specsDir);
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 fs2.readdir(dir, { withFileTypes: true });
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 fs2.readFile(specFile, "utf-8");
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 fs2.access(fullPath);
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 fs2.readFile(specFile, "utf-8");
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 fs3.readdir(dir, { withFileTypes: true });
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 = path3.join(dir, entry.name);
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 (path3.isAbsolute(specPath)) {
352
+ if (path2.isAbsolute(specPath)) {
362
353
  try {
363
- await fs3.access(specPath);
354
+ await fs9.access(specPath);
364
355
  return specPath;
365
356
  } catch {
366
357
  return null;
367
358
  }
368
359
  }
369
- const cwdPath = path3.resolve(cwd, specPath);
360
+ const cwdPath = path2.resolve(cwd, specPath);
370
361
  try {
371
- await fs3.access(cwdPath);
362
+ await fs9.access(cwdPath);
372
363
  return cwdPath;
373
364
  } catch {
374
365
  }
375
- const specsPath = path3.join(specsDir, specPath);
366
+ const specsPath = path2.join(specsDir, specPath);
376
367
  try {
377
- await fs3.access(specsPath);
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 fs3.readdir(dir, { withFileTypes: true });
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 path3.join(dir, entry.name);
393
+ return path2.join(dir, entry.name);
403
394
  }
404
395
  }
405
- const subDir = path3.join(dir, entry.name);
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 fs3.readdir(dir, { withFileTypes: true });
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 path3.join(dir, entry.name);
413
+ return path2.join(dir, entry.name);
423
414
  }
424
- const subDir = path3.join(dir, entry.name);
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 = path4.join(cwd, "package.json");
461
- const content = await fs4.readFile(packageJsonPath, "utf-8");
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(options?.failText || `${text} failed`);
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
- const specsDir = path5.join(cwd, config.specsDir);
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 = path5.basename(spec.path);
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(chalk3.green("\u2713 No sequence conflicts detected"));
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(chalk3.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
584
+ console.log(chalk16.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
611
585
  for (const [seq, paths] of conflicts) {
612
- console.log(chalk3.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
586
+ console.log(chalk16.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
613
587
  for (const p of paths) {
614
- console.log(chalk3.gray(` - ${sanitizeUserInput(p)}`));
588
+ console.log(chalk16.gray(` - ${sanitizeUserInput(p)}`));
615
589
  }
616
590
  console.log("");
617
591
  }
618
- console.log(chalk3.cyan("Tip: Use date prefix to prevent conflicts:"));
619
- console.log(chalk3.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
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(chalk3.cyan("Or rename folders manually to resolve."));
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(chalk3.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
626
- console.log(chalk3.gray("Run: lean-spec check"));
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 = path6.join(cwd, config.specsDir);
648
- await fs5.mkdir(specsDir, { recursive: true });
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 = path6.join(specsDir, specRelativePath);
669
- const specFile = path6.join(specDir, config.structure.defaultFile);
642
+ const specDir = path2.join(specsDir, specRelativePath);
643
+ const specFile = path2.join(specDir, config.structure.defaultFile);
670
644
  try {
671
- await fs5.access(specDir);
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 fs5.mkdir(specDir, { recursive: true });
680
- const templatesDir = path6.join(cwd, ".lean-spec", "templates");
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 = path6.join(templatesDir, templateName);
665
+ const templatePath = path2.join(templatesDir, templateName);
693
666
  let content;
694
667
  try {
695
- const template = await fs5.readFile(templatePath, "utf-8");
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 = matter(content, {
673
+ const parsed = matter4(content, {
701
674
  engines: {
702
- yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
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("./frontmatter-26SOQGYM.js");
726
- enrichWithTimestamps(parsed.data);
727
- content = matter.stringify(parsed.content, parsed.data);
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 fs5.writeFile(specFile, content, "utf-8");
740
- console.log(chalk4.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
741
- console.log(chalk4.gray(` Edit: ${sanitizeUserInput(specFile)}`));
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 = path7.join(cwd, config.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 = path7.join(specsDir, "archived");
763
- await fs6.mkdir(archiveDir, { recursive: true });
764
- const specName = path7.basename(resolvedPath);
765
- const archivePath = path7.join(archiveDir, specName);
766
- await fs6.rename(resolvedPath, archivePath);
767
- console.log(chalk5.green(`\u2713 Archived: ${sanitizeUserInput(archivePath)}`));
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: chalk6.blue,
806
- badge: (s = "planned") => chalk6.blue(`[${s}]`)
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: chalk6.yellow,
812
- badge: (s = "in-progress") => chalk6.yellow(`[${s}]`)
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: chalk6.green,
818
- badge: (s = "complete") => chalk6.green(`[${s}]`)
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: chalk6.gray,
824
- badge: (s = "archived") => chalk6.gray(`[${s}]`)
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: chalk6.red.bold,
831
- badge: (s = "critical") => chalk6.red.bold(`[${s}]`)
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: chalk6.hex("#FFA500"),
836
- badge: (s = "high") => chalk6.hex("#FFA500")(`[${s}]`)
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: chalk6.yellow,
841
- badge: (s = "medium") => chalk6.yellow(`[${s}]`)
800
+ colorFn: chalk16.yellow,
801
+ badge: (s = "medium") => chalk16.yellow(`[${s}]`)
842
802
  },
843
803
  low: {
844
804
  emoji: "\u{1F7E2}",
845
- colorFn: chalk6.gray,
846
- badge: (s = "low") => chalk6.gray(`[${s}]`)
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() || chalk6.white(`[${status}]`);
810
+ return STATUS_CONFIG[status]?.badge() || chalk16.white(`[${status}]`);
851
811
  }
852
812
  function formatPriorityBadge(priority) {
853
- return PRIORITY_CONFIG[priority]?.badge() || chalk6.white(`[${priority}]`);
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 chalk6.gray("[unknown]");
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 = path8.join(cwd, config.specsDir);
832
+ const specsDir = path2.join(cwd, config.specsDir);
873
833
  try {
874
- await fs7.access(specsDir);
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(chalk7.dim("No specs found."));
857
+ console.log(chalk16.dim("No specs found."));
898
858
  return;
899
859
  }
900
- console.log(chalk7.bold.cyan("\u{1F4C4} Spec List"));
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(chalk7.dim(`Filtered by: ${filterParts.join(", ")}`));
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(chalk7.bold(`Total: ${chalk7.green(specs.length)} spec${specs.length !== 1 ? "s" : ""}`));
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 = " " + chalk7.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
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 = " " + chalk7.dim(chalk7.magenta(tagStr));
898
+ tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
939
899
  }
940
900
  }
941
901
  const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
942
- console.log(`${priorityPrefix}${statusEmoji} ${chalk7.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
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(`${chalk7.bold.cyan(`${groupEmoji} ${groupName}/`)} ${chalk7.dim(`(${groupSpecs.length})`)}`);
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 = " " + chalk7.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
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 = " " + chalk7.dim(chalk7.magenta(tagStr));
946
+ tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
987
947
  }
988
948
  }
989
949
  const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
990
- console.log(` ${priorityPrefix}${statusEmoji} ${chalk7.cyan(sanitizeUserInput(displayPath))}${assigneeStr}${tagsStr}`);
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 = path9.join(cwd, config.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(chalk8.green(`\u2713 Updated: ${sanitizeUserInput(path9.relative(cwd, resolvedPath))}`));
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(chalk8.gray(` Fields: ${updatedFields.join(", ")}`));
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
- execSync2("git rev-parse --is-inside-work-tree", {
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 = execSync2(
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 = execSync2(
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 = execSync2(
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 = execSync2(
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 = execSync2(
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
- execSync2(
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 = path10.join(cwd, config.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 = path11.join(cwd, ".lean-spec", "templates");
1303
+ const templatesDir = path2.join(cwd, ".lean-spec", "templates");
1359
1304
  console.log("");
1360
- console.log(chalk9.green("=== Project Templates ==="));
1305
+ console.log(chalk16.green("=== Project Templates ==="));
1361
1306
  console.log("");
1362
1307
  try {
1363
- await fs8.access(templatesDir);
1308
+ await fs9.access(templatesDir);
1364
1309
  } catch {
1365
- console.log(chalk9.yellow("No templates directory found."));
1366
- console.log(chalk9.gray("Run: lean-spec init"));
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 fs8.readdir(templatesDir);
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(chalk9.yellow("No templates found."));
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(chalk9.cyan("Registered:"));
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 ? chalk9.green("\u2713 (default)") : "";
1382
- console.log(` ${chalk9.bold(name)}: ${file} ${marker}`);
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(chalk9.cyan("Available files:"));
1331
+ console.log(chalk16.cyan("Available files:"));
1387
1332
  for (const file of templateFiles) {
1388
- const filePath = path11.join(templatesDir, file);
1389
- const stat5 = await fs8.stat(filePath);
1390
- const sizeKB = (stat5.size / 1024).toFixed(1);
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(chalk9.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
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(chalk9.red(`Template not found: ${templateName}`));
1401
- console.error(chalk9.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
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 = path11.join(cwd, ".lean-spec", "templates");
1349
+ const templatesDir = path2.join(cwd, ".lean-spec", "templates");
1405
1350
  const templateFile = config.templates[templateName];
1406
- const templatePath = path11.join(templatesDir, templateFile);
1351
+ const templatePath = path2.join(templatesDir, templateFile);
1407
1352
  try {
1408
- const content = await fs8.readFile(templatePath, "utf-8");
1353
+ const content = await fs9.readFile(templatePath, "utf-8");
1409
1354
  console.log("");
1410
- console.log(chalk9.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
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(chalk9.red(`Error reading template: ${templateFile}`));
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 = path11.join(cwd, ".lean-spec", "templates");
1423
- const templatePath = path11.join(templatesDir, file);
1367
+ const templatesDir = path2.join(cwd, ".lean-spec", "templates");
1368
+ const templatePath = path2.join(templatesDir, file);
1424
1369
  try {
1425
- await fs8.access(templatePath);
1370
+ await fs9.access(templatePath);
1426
1371
  } catch {
1427
- console.error(chalk9.red(`Template file not found: ${file}`));
1428
- console.error(chalk9.gray(`Expected at: ${templatePath}`));
1372
+ console.error(chalk16.red(`Template file not found: ${file}`));
1373
+ console.error(chalk16.gray(`Expected at: ${templatePath}`));
1429
1374
  console.error(
1430
- chalk9.yellow("Create the file first or use: lean-spec templates copy <source> <target>")
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(chalk9.yellow(`Warning: Template '${name}' already exists, updating...`));
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(chalk9.green(`\u2713 Added template: ${name} \u2192 ${file}`));
1443
- console.log(chalk9.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
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(chalk9.red(`Template not found: ${name}`));
1449
- console.error(chalk9.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
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(chalk9.red("Cannot remove default template"));
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(chalk9.green(`\u2713 Removed template: ${name}`));
1460
- console.log(chalk9.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
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 = path11.join(cwd, ".lean-spec", "templates");
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 = path11.join(templatesDir, sourceFile);
1416
+ const sourcePath = path2.join(templatesDir, sourceFile);
1472
1417
  try {
1473
- await fs8.access(sourcePath);
1418
+ await fs9.access(sourcePath);
1474
1419
  } catch {
1475
- console.error(chalk9.red(`Source template not found: ${source}`));
1476
- console.error(chalk9.gray(`Expected at: ${sourcePath}`));
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 = path11.join(templatesDir, targetFile);
1481
- await fs8.copyFile(sourcePath, targetPath);
1482
- console.log(chalk9.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
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(chalk9.green(`\u2713 Registered template: ${templateName}`));
1490
- console.log(chalk9.gray(` Edit: ${targetPath}`));
1491
- console.log(chalk9.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
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(path12.join(cwd, file));
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 = path12.join(cwd, file);
1524
- const templateFilePath = path12.join(templateDir, "files", file);
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 = path12.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
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(path12.dirname(promptPath), { recursive: true });
1501
+ await fs9.mkdir(path2.dirname(promptPath), { recursive: true });
1569
1502
  await fs9.writeFile(promptPath, aiPrompt, "utf-8");
1570
- console.log(chalk10.green(`\u2713 Created AI consolidation prompt`));
1571
- console.log(chalk10.cyan(` \u2192 ${promptPath}`));
1503
+ console.log(chalk16.green(`\u2713 Created AI consolidation prompt`));
1504
+ console.log(chalk16.cyan(` \u2192 ${promptPath}`));
1572
1505
  console.log("");
1573
- console.log(chalk10.yellow("\u{1F4DD} Next steps:"));
1574
- console.log(chalk10.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
1575
- console.log(chalk10.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
1576
- console.log(chalk10.gray(" 3. Let AI create the consolidated AGENTS.md"));
1577
- console.log(chalk10.gray(" 4. Review and commit the result"));
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(chalk10.green(`\u2713 Appended LeanSpec section to ${file}`));
1594
- console.log(chalk10.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
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(chalk10.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
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(chalk10.green(`\u2713 Created new ${file}`));
1605
- console.log(chalk10.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
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 = path12.join(src, entry.name);
1614
- const destPath = path12.join(dest, entry.name);
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 = path12.join(cwd, "package.json");
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 path12.basename(cwd);
1576
+ return path2.basename(cwd);
1644
1577
  }
1645
1578
 
1646
1579
  // src/commands/init.ts
1647
- var __dirname2 = path13.dirname(fileURLToPath(import.meta.url));
1648
- var TEMPLATES_DIR = path13.join(__dirname2, "..", "templates");
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 fs10.access(path13.join(cwd, ".lean-spec", "config.json"));
1653
- console.log(chalk11.yellow("\u26A0 LeanSpec already initialized in this directory."));
1654
- console.log(chalk11.gray("To reinitialize, delete .lean-spec/ directory first."));
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(chalk11.green("Welcome to LeanSpec!"));
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 = path13.join(TEMPLATES_DIR, templateName);
1698
- const templateConfigPath = path13.join(templateDir, "config.json");
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 fs10.readFile(templateConfigPath, "utf-8");
1634
+ const content = await fs9.readFile(templateConfigPath, "utf-8");
1702
1635
  templateConfig = JSON.parse(content).config;
1703
1636
  } catch {
1704
- console.error(chalk11.red(`Error: Template not found: ${templateName}`));
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(chalk11.yellow("\u26A0 Custom pattern input is not yet implemented."));
1748
- console.log(chalk11.gray(" You can manually edit .lean-spec/config.json after initialization."));
1749
- console.log(chalk11.gray(" Using simple pattern for now."));
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 = path13.join(cwd, ".lean-spec", "templates");
1687
+ const templatesDir = path2.join(cwd, ".lean-spec", "templates");
1755
1688
  try {
1756
- await fs10.mkdir(templatesDir, { recursive: true });
1689
+ await fs9.mkdir(templatesDir, { recursive: true });
1757
1690
  } catch (error) {
1758
- console.error(chalk11.red("Error creating templates directory:"), error);
1691
+ console.error(chalk16.red("Error creating templates directory:"), error);
1759
1692
  process.exit(1);
1760
1693
  }
1761
- const templateSpecPath = path13.join(templateDir, "spec-template.md");
1762
- const targetSpecPath = path13.join(templatesDir, "spec-template.md");
1694
+ const templateSpecPath = path2.join(templateDir, "spec-template.md");
1695
+ const targetSpecPath = path2.join(templatesDir, "spec-template.md");
1763
1696
  try {
1764
- await fs10.copyFile(templateSpecPath, targetSpecPath);
1765
- console.log(chalk11.green("\u2713 Created .lean-spec/templates/spec-template.md"));
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(chalk11.red("Error copying template:"), 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(chalk11.green("\u2713 Created .lean-spec/config.json"));
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(chalk11.yellow(`Found existing: ${existingFiles.join(", ")}`));
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 = path13.join(templateDir, "files");
1746
+ const filesDir = path2.join(templateDir, "files");
1814
1747
  try {
1815
1748
  await copyDirectory(filesDir, cwd, skipFiles, { project_name: projectName });
1816
- console.log(chalk11.green("\u2713 Initialized project structure"));
1749
+ console.log(chalk16.green("\u2713 Initialized project structure"));
1817
1750
  } catch (error) {
1818
- console.error(chalk11.red("Error copying template files:"), 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(chalk11.green("\u2713 LeanSpec initialized!"));
1755
+ console.log(chalk16.green("\u2713 LeanSpec initialized!"));
1823
1756
  console.log("");
1824
1757
  console.log("Next steps:");
1825
- console.log(chalk11.gray(" - Review and customize AGENTS.md"));
1826
- console.log(chalk11.gray(" - Check out example spec in specs/"));
1827
- console.log(chalk11.gray(" - Create your first spec: lean-spec create my-feature"));
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 = path14.join(cwd, config.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(chalk12.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
1778
+ console.log(chalk16.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
1851
1779
  console.log("");
1852
- console.log(chalk12.green("Required:"));
1853
- const readmeStat = await fs11.stat(spec.filePath);
1780
+ console.log(chalk16.green("Required:"));
1781
+ const readmeStat = await fs9.stat(spec.filePath);
1854
1782
  const readmeSize = formatSize(readmeStat.size);
1855
- console.log(chalk12.green(` \u2713 README.md (${readmeSize}) Main spec`));
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(chalk12.gray("No additional files"));
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(chalk12.cyan("Documents:"));
1799
+ console.log(chalk16.cyan("Documents:"));
1872
1800
  for (const file of documents) {
1873
1801
  const size = formatSize(file.size);
1874
- console.log(chalk12.cyan(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
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(chalk12.yellow("Assets:"));
1807
+ console.log(chalk16.yellow("Assets:"));
1880
1808
  for (const file of assets) {
1881
1809
  const size = formatSize(file.size);
1882
- console.log(chalk12.yellow(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
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(chalk12.gray(`Total: ${totalFiles} files, ${totalSize}`));
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 = matter2(content, {
1844
+ parsed = matter4(content, {
1966
1845
  engines: {
1967
- yaml: (str) => yaml2.load(str, { schema: yaml2.FAILSAFE_SCHEMA })
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 = matter3(content);
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
- // src/validators/sub-spec.ts
2429
- import * as path15 from "path";
2430
- var SubSpecValidator = class {
2431
- name = "sub-specs";
2432
- description = "Validate sub-spec files per spec 012 conventions";
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
- async validate(spec, content) {
2442
- const errors = [];
2443
- const warnings = [];
2444
- const subFiles = await loadSubFiles(spec.fullPath, { includeContent: true });
2445
- const subSpecs = subFiles.filter((f) => f.type === "document");
2446
- if (subSpecs.length === 0) {
2447
- return { passed: true, errors, warnings };
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
- this.validateNamingConventions(subSpecs, warnings);
2450
- await this.validateLineCounts(subSpecs, errors, warnings);
2451
- this.checkOrphanedSubSpecs(subSpecs, content, warnings);
2452
- if (this.checkCrossReferences) {
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
- return {
2456
- passed: errors.length === 0,
2457
- errors,
2458
- warnings
2459
- };
2302
+ data.transitions.push({
2303
+ status: data.status,
2304
+ at: now
2305
+ });
2460
2306
  }
2461
- /**
2462
- * Validate sub-spec naming conventions
2463
- * Convention: Uppercase filenames (e.g., DESIGN.md, TESTING.md, IMPLEMENTATION.md)
2464
- */
2465
- validateNamingConventions(subSpecs, warnings) {
2466
- for (const subSpec of subSpecs) {
2467
- const baseName = path15.basename(subSpec.name, ".md");
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
- * Validate line counts for each sub-spec file
2478
- */
2479
- async validateLineCounts(subSpecs, errors, warnings) {
2480
- for (const subSpec of subSpecs) {
2481
- if (!subSpec.content) {
2482
- continue;
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
- const lines = subSpec.content.split("\n").length;
2485
- if (lines > this.maxLines) {
2486
- errors.push({
2487
- message: `Sub-spec ${subSpec.name} exceeds ${this.maxLines} lines (${lines} lines)`,
2488
- suggestion: "Consider further splitting or simplification"
2489
- });
2490
- } else if (lines > this.warningThreshold) {
2491
- warnings.push({
2492
- message: `Sub-spec ${subSpec.name} approaching limit (${lines}/${this.maxLines} lines)`,
2493
- suggestion: "Consider simplification"
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
- * Check for orphaned sub-specs not referenced in README.md
2500
- */
2501
- checkOrphanedSubSpecs(subSpecs, readmeContent, warnings) {
2502
- for (const subSpec of subSpecs) {
2503
- const fileName = subSpec.name;
2504
- const escapedFileName = fileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2505
- const linkPattern = new RegExp(`\\[([^\\]]+)\\]\\((?:\\.\\/)?${escapedFileName}\\)`, "gi");
2506
- const isReferenced = linkPattern.test(readmeContent);
2507
- if (!isReferenced) {
2508
- warnings.push({
2509
- message: `Orphaned sub-spec: ${fileName} (not linked from README.md)`,
2510
- suggestion: `Add a link to ${fileName} in README.md to document its purpose`
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
- * Validate cross-document references between sub-specs
2517
- */
2518
- async validateCrossReferences(subSpecs, spec, warnings) {
2519
- const validFileNames = new Set(subSpecs.map((s) => s.name));
2520
- validFileNames.add("README.md");
2521
- for (const subSpec of subSpecs) {
2522
- if (!subSpec.content) {
2523
- continue;
2524
- }
2525
- const linkRegex = /\[([^\]]+)\]\((?:\.\/)?(([^)]+)\.md)\)/g;
2526
- let match;
2527
- while ((match = linkRegex.exec(subSpec.content)) !== null) {
2528
- const referencedFile = match[2];
2529
- if (!validFileNames.has(referencedFile)) {
2530
- warnings.push({
2531
- message: `Broken reference in ${subSpec.name}: ${referencedFile} not found`,
2532
- suggestion: `Check if ${referencedFile} exists or update the link`
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
- // src/utils/validate-formatter.ts
2541
- import chalk13 from "chalk";
2542
- function groupIssuesByFile(results) {
2543
- const fileMap = /* @__PURE__ */ new Map();
2544
- const addIssue = (filePath, issue, spec) => {
2545
- if (!fileMap.has(filePath)) {
2546
- fileMap.set(filePath, { issues: [], spec });
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
- fileMap.get(filePath).issues.push(issue);
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
- for (const warning of result.warnings) {
2562
- addIssue(spec.filePath, {
2563
- severity: "warning",
2564
- message: warning.message,
2565
- suggestion: warning.suggestion,
2566
- ruleName: validatorName,
2567
- filePath: spec.filePath,
2568
- spec
2569
- }, spec);
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
- const fileResults = [];
2573
- for (const [filePath, data] of fileMap.entries()) {
2574
- data.issues.sort((a, b) => {
2575
- if (a.severity === b.severity) return 0;
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
- fileResults.sort((a, b) => {
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 normalizeFilePath(filePath) {
2589
- const cwd = process.cwd();
2590
- if (filePath.startsWith(cwd)) {
2591
- return filePath.substring(cwd.length + 1);
2592
- } else if (filePath.includes("/specs/")) {
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 filePath;
2525
+ return result;
2597
2526
  }
2598
- function formatFileIssues(fileResult, specsDir) {
2599
- const lines = [];
2600
- const relativePath = normalizeFilePath(fileResult.filePath);
2601
- const isMainSpec = relativePath.endsWith("README.md");
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
- for (const issue of fileResult.issues) {
2613
- const severityColor = issue.severity === "error" ? chalk13.red : chalk13.yellow;
2614
- const severityText = severityColor(issue.severity.padEnd(9));
2615
- const ruleText = chalk13.gray(issue.ruleName);
2616
- lines.push(` ${severityText}${issue.message.padEnd(60)} ${ruleText}`);
2617
- if (issue.suggestion) {
2618
- lines.push(chalk13.gray(` \u2192 ${issue.suggestion}`));
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.push("");
2540
+ lines.splice(startLine - 1, endLine - startLine + 1);
2622
2541
  return lines.join("\n");
2623
2542
  }
2624
- function formatSummary(totalSpecs, errorCount, warningCount, cleanCount) {
2625
- if (errorCount > 0) {
2626
- const errorText = errorCount === 1 ? "error" : "errors";
2627
- const warningText = warningCount === 1 ? "warning" : "warnings";
2628
- return chalk13.red.bold(
2629
- `\u2716 ${errorCount} ${errorText}, ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
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 chalk13.yellow.bold(
3563
+ return chalk16.yellow.bold(
2634
3564
  `\u26A0 ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
2635
3565
  );
2636
3566
  } else {
2637
- return chalk13.green.bold(`\u2713 All ${totalSpecs} specs passed`);
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(chalk13.green.bold(`
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(chalk13.gray(` ${relativePath}`));
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(chalk13.bold(`
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(chalk13.gray("\u2500".repeat(80)));
3628
+ lines.push(chalk16.gray("\u2500".repeat(80)));
2699
3629
  lines.push("");
2700
3630
  }
2701
- lines.push(formatFileIssues(fileResult, specsDir));
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, specsDir));
3649
+ lines.push(formatPassingSpecs(passingSpecs));
2720
3650
  }
2721
3651
  if (!options.verbose && cleanCount > 0 && displayResults.length > 0) {
2722
- lines.push(chalk13.gray("\nRun with --verbose to see passing specs."));
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) || path16.basename(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(chalk14.red(`Error: Spec not found: ${specPath}`));
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 LineCountValidator({ maxLines: options.maxLines }),
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 fs12.readFile(spec.filePath, "utf-8");
3697
+ content = await fs9.readFile(spec.filePath, "utf-8");
2767
3698
  } catch (error) {
2768
- console.error(chalk14.red(`Error reading ${spec.filePath}:`), error);
3699
+ console.error(chalk16.red(`Error reading ${spec.filePath}:`), error);
2769
3700
  continue;
2770
3701
  }
2771
3702
  for (const validator of validators) {
2772
- const result = await validator.validate(spec, content);
2773
- results.push({
2774
- spec,
2775
- validatorName: validator.name,
2776
- result,
2777
- content
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 fs13.stat(inputPath);
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 fs13.readdir(currentPath, { withFileTypes: true });
3758
+ const entries = await fs9.readdir(currentPath, { withFileTypes: true });
2828
3759
  for (const entry of entries) {
2829
- const fullPath = path17.join(currentPath, entry.name);
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 fs13.stat(fullPath);
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
- const specsDir = config.specsDir || "specs";
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("child_process");
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 = dayjs(spec.frontmatter.due).isBefore(dayjs(), "day");
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 = dayjs().diff(dayjs(updatedAt), "day");
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 = dayjs2(createdAt);
3087
- const completed = dayjs2(completedAt);
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 = dayjs2(fromTransition.at);
3101
- const to = dayjs2(toTransition.at);
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 = dayjs2().subtract(days, "day");
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 dayjs2(completedAt).isAfter(cutoff);
4036
+ return dayjs3(completedAt).isAfter(cutoff);
3115
4037
  }).length;
3116
4038
  }
3117
- function calculateWIP(specs, date = dayjs2()) {
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 = dayjs2(createdAt);
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 = dayjs2(completedAt);
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 = dayjs2().subtract(14, "day");
3144
- const prevWeekEnd = dayjs2().subtract(7, "day");
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 = dayjs2(completedAt);
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 = dayjs2().subtract(i, "day");
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(chalk15.dim("No specs found."));
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(chalk15.bold.cyan("\u{1F4CB} Spec Kanban Board"));
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(chalk15.dim(`Filtered by: ${filterParts.join(", ")}`));
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
- const completionStatus = getCompletionStatus(completionMetrics.score);
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(chalk15.dim(topBorder));
3235
- const headerLine = chalk15.bold(" Project Overview");
3236
- console.log(chalk15.dim("\u2551") + padLine(headerLine) + chalk15.dim("\u2551"));
3237
- const percentageColor = completionMetrics.score >= 70 ? chalk15.green : completionMetrics.score >= 40 ? chalk15.yellow : chalk15.red;
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(chalk15.dim("\u2551") + padLine(line1) + chalk15.dim("\u2551"));
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 = ` ${chalk15.yellow("\u26A0\uFE0F " + alerts.join(" \xB7 "))}`;
3249
- console.log(chalk15.dim("\u2551") + padLine(alertLine) + chalk15.dim("\u2551"));
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 = ` ${chalk15.cyan("\u{1F680} Velocity:")} ${velocityMetrics.cycleTime.average.toFixed(1)}d avg cycle \xB7 ${(velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1)}/wk throughput`;
3252
- console.log(chalk15.dim("\u2551") + padLine(velocityLine) + chalk15.dim("\u2551"));
3253
- console.log(chalk15.dim(bottomBorder));
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(chalk15.dim("\u2501".repeat(70)));
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(chalk15.dim("\u2501".repeat(70)));
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(chalk15.bold(`${title} (${specs.length})`))}`);
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" ? chalk15.dim : PRIORITY_CONFIG[priority].colorFn;
3294
- console.log(` ${priorityColor(`${priorityEmoji} ${chalk15.bold(priorityLabel)} ${chalk15.dim(`(${groupSpecs.length})`)}`)}`);
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 = " " + chalk15.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
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 = " " + chalk15.dim(chalk15.magenta(tagStr));
4227
+ tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
3307
4228
  }
3308
4229
  }
3309
- console.log(` ${chalk15.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
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(` ${chalk15.dim("(collapsed, use --complete to expand)")}`);
4235
+ console.log(` ${chalk16.dim("(collapsed, use --complete to expand)")}`);
3315
4236
  console.log("");
3316
4237
  } else {
3317
- console.log(` ${chalk15.dim("(empty)")}`);
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
- const showFull = options.full || false;
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 && dayjs4(s.frontmatter.due).isBefore(dayjs4(), "day") && s.frontmatter.status !== "complete"
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 && dayjs4(s.frontmatter.due).isBefore(dayjs4(), "day") && s.frontmatter.status !== "complete"
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 = dayjs4();
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 = dayjs4(spec.frontmatter.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 = dayjs4(spec.frontmatter.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 results = [];
3865
- const queryLower = query.toLowerCase();
3866
- for (const spec of specs) {
3867
- if (!spec.content) continue;
3868
- const matches = [];
3869
- const lines = spec.content.split("\n");
3870
- for (let i = 0; i < lines.length; i++) {
3871
- const line = lines[i];
3872
- if (line.toLowerCase().includes(queryLower)) {
3873
- const contextStart = Math.max(0, i - 1);
3874
- const contextEnd = Math.min(lines.length - 1, i + 1);
3875
- const context = lines.slice(contextStart, contextEnd + 1);
3876
- const matchLine = context[i - contextStart];
3877
- const highlighted = highlightMatch(matchLine, query);
3878
- matches.push(highlighted);
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(chalk17.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
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(chalk17.gray(`With filters: ${filters.join(", ")}`));
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(chalk17.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${sanitizeUserInput(query)}"`));
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(chalk17.gray(`With filters: ${filters.join(", ")}`));
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
- console.log(chalk17.cyan(`${spec.frontmatter.status === "in-progress" ? "\u{1F528}" : spec.frontmatter.status === "complete" ? "\u2705" : "\u{1F4C5}"} ${sanitizeUserInput(spec.path)}`));
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.frontmatter.priority) {
3915
- const priorityEmoji = spec.frontmatter.priority === "critical" ? "\u{1F534}" : spec.frontmatter.priority === "high" ? "\u{1F7E1}" : spec.frontmatter.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
3916
- meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.frontmatter.priority)}`);
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.frontmatter.tags && spec.frontmatter.tags.length > 0) {
3919
- meta.push(`[${spec.frontmatter.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
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(chalk17.gray(` ${meta.join(" \u2022 ")}`));
3923
- }
3924
- const maxMatches = 3;
3925
- for (let i = 0; i < Math.min(matches.length, maxMatches); i++) {
3926
- console.log(` ${chalk17.gray("Match:")} ${matches[i].trim()}`);
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 > maxMatches) {
3929
- console.log(chalk17.gray(` ... and ${matches.length - maxMatches} more match${matches.length - maxMatches === 1 ? "" : "es"}`));
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 highlightMatch(text, query) {
3935
- const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
3936
- return text.replace(regex, chalk17.yellow("$1"));
3937
- }
3938
- function escapeRegex(str) {
3939
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 = path18.join(cwd, config.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(chalk18.green(`\u{1F4E6} Dependencies for ${chalk18.cyan(sanitizeUserInput(spec.path))}`));
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(chalk18.gray(" No dependencies or relationships"));
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(chalk18.bold("Depends On:"));
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(chalk18.bold("Required By:"));
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(chalk18.bold("Related Specs:"));
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(chalk18.bold("Dependency Chain:"));
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 [path20, s] of specMap.entries()) {
4026
- if (path20.includes(depPath)) {
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 [path20, s] of specMap.entries()) {
4059
- if (path20.includes(relPath)) {
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 ? chalk18.cyan(node.spec.path) : node.spec.path;
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 = dayjs5();
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 = dayjs5(spec.frontmatter.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 = dayjs5(spec.frontmatter.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(chalk19.bold.cyan("\u{1F4C8} Spec Timeline"));
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(chalk19.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
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)} ${chalk19.cyan("Created".padEnd(colWidth))} ${chalk19.green("Completed".padEnd(colWidth))}`);
4179
- console.log(` ${chalk19.dim("\u2500".repeat(labelWidth2))} ${chalk19.dim("\u2500".repeat(colWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`);
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(` ${chalk19.dim(date.padEnd(labelWidth2))} ${chalk19.cyan(createdCol)} ${chalk19.green(completedCol)}`);
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 = dayjs5(a[0], "MMM YYYY");
4194
- const dateB = dayjs5(b[0], "MMM YYYY");
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(chalk19.bold("\u{1F4CA} Monthly Overview"));
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)} ${chalk19.magenta("Specs".padEnd(colWidth))}`);
4205
- console.log(` ${chalk19.dim("\u2500".repeat(labelWidth2))} ${chalk19.dim("\u2500".repeat(colWidth))}`);
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)} ${chalk19.magenta(bar.padEnd(barWidth))}${chalk19.magenta(count.toString().padStart(specsWidth))}`);
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 = dayjs5(s.frontmatter.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 = dayjs5(s.frontmatter.completed);
5140
+ const completed = dayjs3(s.frontmatter.completed);
4221
5141
  return completed.isAfter(today.subtract(30, "day"));
4222
5142
  }).length;
4223
- console.log(chalk19.bold("\u2705 Completion Rate"));
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(` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`);
4229
- console.log(` ${"Last 7 days".padEnd(labelWidth)} ${chalk19.green(last7Days.toString().padStart(valueWidth))}`);
4230
- console.log(` ${"Last 30 days".padEnd(labelWidth)} ${chalk19.green(last30Days.toString().padStart(valueWidth))}`);
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 = dayjs5(spec.frontmatter.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 = dayjs5(spec.frontmatter.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(chalk19.bold("\u{1F3F7}\uFE0F By Tag"));
5172
+ console.log(chalk16.bold("\u{1F3F7}\uFE0F By Tag"));
4253
5173
  for (const [tag, stats] of sortedTags) {
4254
- console.log(` ${chalk19.dim("#")}${tag.padEnd(20)} ${chalk19.cyan(stats.created)} created \xB7 ${chalk19.green(stats.completed)} completed`);
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 = dayjs5(spec.frontmatter.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 = dayjs5(spec.frontmatter.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(chalk19.bold("\u{1F464} By Assignee"));
5199
+ console.log(chalk16.bold("\u{1F464} By Assignee"));
4280
5200
  for (const [assignee, stats] of sortedAssignees) {
4281
- console.log(` ${chalk19.dim("@")}${assignee.padEnd(20)} ${chalk19.cyan(stats.created)} created \xB7 ${chalk19.green(stats.completed)} completed`);
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: chalk20.red },
4304
- high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn: chalk20.hex("#FFA500") },
4305
- medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn: chalk20.yellow },
4306
- low: { emoji: "\u{1F7E2}", label: "LOW", colorFn: chalk20.green }
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(chalk20.dim("No active specs found."));
4330
- console.log(chalk20.dim("Tip: Use --show-complete to include completed specs."));
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 dayjs6(a.frontmatter.due).diff(dayjs6(b.frontmatter.due));
5269
+ return dayjs3(a.frontmatter.due).diff(dayjs3(b.frontmatter.due));
4357
5270
  }
4358
5271
  return 0;
4359
5272
  });
4360
5273
  };
4361
- const today = dayjs6();
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 && dayjs6(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
5280
+ (s) => s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
4368
5281
  ).length;
4369
- console.log(chalk20.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
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(chalk20.dim(dateRow));
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(chalk20.dim(specSeparator + COLUMN_SEPARATOR + timelineSeparator));
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(chalk20.dim(todayMarker));
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(chalk20.red(`${overdue} overdue`));
4410
- console.log(chalk20.bold("Summary: ") + summaryParts.join(" \xB7 "));
4411
- console.log(chalk20.dim('\u{1F4A1} Tip: Add "due: YYYY-MM-DD" to frontmatter for timeline planning'));
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 = chalk20.dim("(no due date set)");
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 = dayjs6(spec.frontmatter.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 += chalk20.green(FILLED_BAR_CHAR.repeat(barLength));
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 += chalk20.yellow(FILLED_BAR_CHAR.repeat(halfLength));
4452
- result += chalk20.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
5363
+ result += chalk16.yellow(FILLED_BAR_CHAR.repeat(halfLength));
5364
+ result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
4453
5365
  } else {
4454
- result += chalk20.dim(EMPTY_BAR_CHAR.repeat(barLength));
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
- // src/commands/viewer.ts
4464
- import * as fs14 from "fs/promises";
4465
- import * as path19 from "path";
4466
- import chalk21 from "chalk";
4467
- import { marked } from "marked";
4468
- import { markedTerminal } from "marked-terminal";
4469
- import { spawn } from "child_process";
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 = path19.join(cwd, config.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 = path19.join(resolvedPath, filePart);
5977
+ targetFile = path2.join(resolvedPath, filePart);
4483
5978
  try {
4484
- await fs14.access(targetFile);
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 fs14.readFile(targetFile, "utf-8");
4504
- const fileName = path19.basename(targetFile);
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 = path19.basename(resolvedPath);
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(chalk21.bold(`${statusEmoji} Status: `) + chalk21.cyan(frontmatter.status));
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(chalk21.bold(`${priorityEmoji} Priority: `) + chalk21.yellow(frontmatter.priority));
6058
+ lines.push(chalk16.bold(`${priorityEmoji} Priority: `) + chalk16.yellow(frontmatter.priority));
4564
6059
  }
4565
6060
  if (frontmatter.created) {
4566
- lines.push(chalk21.bold("\u{1F4C6} Created: ") + chalk21.gray(String(frontmatter.created)));
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) => chalk21.blue(`#${tag}`)).join(" ");
4570
- lines.push(chalk21.bold("\u{1F3F7}\uFE0F Tags: ") + tagStr);
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(chalk21.bold("\u{1F464} Assignee: ") + chalk21.green(frontmatter.assignee));
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(chalk21.bold("Custom Fields:"));
6074
+ lines.push(chalk16.bold("Custom Fields:"));
4580
6075
  for (const [key, value] of customFields) {
4581
- lines.push(` ${chalk21.gray(key)}: ${chalk21.white(String(value))}`);
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(chalk21.bold.cyan(`\u2501\u2501\u2501 ${spec.name} \u2501\u2501\u2501`));
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(chalk21.gray("\u2500".repeat(60)));
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 = path19.join(cwd, config.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 = path19.join(resolvedPath, filePart);
6127
+ targetFile = path2.join(resolvedPath, filePart);
4633
6128
  try {
4634
- await fs14.access(targetFile);
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(chalk21.gray(`Opening ${targetFile} with ${editor}...`));
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
- // src/mcp-server.ts
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(join18(__dirname3, "../package.json"), "utf-8")
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 results = [];
4765
- const queryLower = query.toLowerCase();
4766
- for (const spec of specs) {
4767
- if (!spec.content) continue;
4768
- const matches = [];
4769
- const lines = spec.content.split("\n");
4770
- for (let i = 0; i < lines.length; i++) {
4771
- const line = lines[i];
4772
- if (line.toLowerCase().includes(queryLower)) {
4773
- matches.push(line.trim());
4774
- if (matches.length >= 5) break;
4775
- }
4776
- }
4777
- if (matches.length > 0) {
4778
- results.push({ spec: specToData(spec), matches });
4779
- }
4780
- }
4781
- return results;
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: "Full-text search across all specification content. Use this when you need to find specs by keyword, topic, or concept. Returns matching specs with relevant excerpts.",
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.any())
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 results = await searchSpecsData(input.query, {
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(output, null, 2) }],
4938
- structuredContent: output
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
- checkSpecs,
5631
- createSpec,
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