lean-spec 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ER23B6KS.js → chunk-7MCDTSVE.js} +1797 -193
- package/dist/chunk-7MCDTSVE.js.map +1 -0
- package/dist/cli.js +36 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands-GRG5UUOF.js +4 -0
- package/dist/{commands-ZNL7ZCHU.js.map → commands-GRG5UUOF.js.map} +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +5 -3
- package/templates/_shared/agents-components/core-rules-base.md +5 -0
- package/templates/_shared/agents-components/core-rules-enterprise.md +5 -0
- package/templates/_shared/agents-components/discovery-commands-enterprise.md +10 -0
- package/templates/_shared/agents-components/discovery-commands-minimal.md +8 -0
- package/templates/_shared/agents-components/discovery-commands-standard.md +9 -0
- package/templates/_shared/agents-components/enterprise-approval.md +10 -0
- package/templates/_shared/agents-components/enterprise-compliance.md +12 -0
- package/templates/_shared/agents-components/enterprise-when-required.md +13 -0
- package/templates/_shared/agents-components/frontmatter-enterprise.md +33 -0
- package/templates/_shared/agents-components/frontmatter-minimal.md +18 -0
- package/templates/_shared/agents-components/frontmatter-standard.md +23 -0
- package/templates/_shared/agents-components/quality-standards-base.md +5 -0
- package/templates/_shared/agents-components/quality-standards-enterprise.md +6 -0
- package/templates/_shared/agents-components/when-to-use-enterprise.md +11 -0
- package/templates/_shared/agents-components/when-to-use-minimal.md +9 -0
- package/templates/_shared/agents-components/when-to-use-standard.md +9 -0
- package/templates/_shared/agents-components/workflow-enterprise.md +8 -0
- package/templates/_shared/agents-components/workflow-standard-detailed.md +7 -0
- package/templates/_shared/agents-components/workflow-standard.md +5 -0
- package/templates/_shared/agents-template.hbs +39 -0
- package/templates/enterprise/agents-config.json +15 -0
- package/templates/enterprise/files/AGENTS.md +1 -0
- package/templates/minimal/agents-config.json +12 -0
- package/templates/minimal/files/AGENTS.md +1 -0
- package/templates/standard/agents-config.json +12 -0
- package/templates/standard/files/AGENTS.md +1 -0
- package/dist/chunk-ER23B6KS.js.map +0 -1
- package/dist/commands-ZNL7ZCHU.js +0 -4
|
@@ -3,18 +3,19 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import * as fs9 from 'fs/promises';
|
|
6
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
6
7
|
import * as path2 from 'path';
|
|
7
8
|
import { dirname, join } from 'path';
|
|
8
9
|
import chalk16 from 'chalk';
|
|
9
|
-
import
|
|
10
|
-
import
|
|
10
|
+
import matter4 from 'gray-matter';
|
|
11
|
+
import yaml3 from 'js-yaml';
|
|
11
12
|
import { spawn, execSync } from 'child_process';
|
|
12
13
|
import ora from 'ora';
|
|
13
14
|
import stripAnsi from 'strip-ansi';
|
|
14
15
|
import { fileURLToPath } from 'url';
|
|
15
16
|
import { select } from '@inquirer/prompts';
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
17
|
+
import { encoding_for_model } from 'tiktoken';
|
|
18
|
+
import dayjs3 from 'dayjs';
|
|
18
19
|
import { marked } from 'marked';
|
|
19
20
|
import { markedTerminal } from 'marked-terminal';
|
|
20
21
|
import { readFileSync } from 'fs';
|
|
@@ -142,13 +143,13 @@ async function loadSubFiles(specDir, options = {}) {
|
|
|
142
143
|
if (entry.name === "README.md") continue;
|
|
143
144
|
if (entry.isDirectory()) continue;
|
|
144
145
|
const filePath = path2.join(specDir, entry.name);
|
|
145
|
-
const
|
|
146
|
+
const stat6 = await fs9.stat(filePath);
|
|
146
147
|
const ext = path2.extname(entry.name).toLowerCase();
|
|
147
148
|
const isDocument = ext === ".md";
|
|
148
149
|
const subFile = {
|
|
149
150
|
name: entry.name,
|
|
150
151
|
path: filePath,
|
|
151
|
-
size:
|
|
152
|
+
size: stat6.size,
|
|
152
153
|
type: isDocument ? "document" : "asset"
|
|
153
154
|
};
|
|
154
155
|
if (isDocument && options.includeContent) {
|
|
@@ -669,9 +670,9 @@ async function createSpec(name, options = {}) {
|
|
|
669
670
|
const title = options.title || name;
|
|
670
671
|
const varContext = await buildVariableContext(config, { name: title, date });
|
|
671
672
|
content = resolveVariables(template, varContext);
|
|
672
|
-
const parsed =
|
|
673
|
+
const parsed = matter4(content, {
|
|
673
674
|
engines: {
|
|
674
|
-
yaml: (str) =>
|
|
675
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
675
676
|
}
|
|
676
677
|
});
|
|
677
678
|
normalizeDateFields(parsed.data);
|
|
@@ -694,9 +695,9 @@ async function createSpec(name, options = {}) {
|
|
|
694
695
|
frontmatter: parsed.data
|
|
695
696
|
};
|
|
696
697
|
parsed.content = resolveVariables(parsed.content, contextWithFrontmatter);
|
|
697
|
-
const { enrichWithTimestamps } = await import('./frontmatter-R2DANL5X.js');
|
|
698
|
-
|
|
699
|
-
content =
|
|
698
|
+
const { enrichWithTimestamps: enrichWithTimestamps2 } = await import('./frontmatter-R2DANL5X.js');
|
|
699
|
+
enrichWithTimestamps2(parsed.data);
|
|
700
|
+
content = matter4.stringify(parsed.content, parsed.data);
|
|
700
701
|
if (options.description) {
|
|
701
702
|
content = content.replace(
|
|
702
703
|
/## Overview\s+<!-- What are we solving\? Why now\? -->/,
|
|
@@ -1330,8 +1331,8 @@ async function listTemplates(cwd = process.cwd()) {
|
|
|
1330
1331
|
console.log(chalk16.cyan("Available files:"));
|
|
1331
1332
|
for (const file of templateFiles) {
|
|
1332
1333
|
const filePath = path2.join(templatesDir, file);
|
|
1333
|
-
const
|
|
1334
|
-
const sizeKB = (
|
|
1334
|
+
const stat6 = await fs9.stat(filePath);
|
|
1335
|
+
const sizeKB = (stat6.size / 1024).toFixed(1);
|
|
1335
1336
|
console.log(` ${file} (${sizeKB} KB)`);
|
|
1336
1337
|
}
|
|
1337
1338
|
console.log("");
|
|
@@ -1840,9 +1841,9 @@ var FrontmatterValidator = class {
|
|
|
1840
1841
|
const warnings = [];
|
|
1841
1842
|
let parsed;
|
|
1842
1843
|
try {
|
|
1843
|
-
parsed =
|
|
1844
|
+
parsed = matter4(content, {
|
|
1844
1845
|
engines: {
|
|
1845
|
-
yaml: (str) =>
|
|
1846
|
+
yaml: (str) => yaml3.load(str, { schema: yaml3.FAILSAFE_SCHEMA })
|
|
1846
1847
|
}
|
|
1847
1848
|
});
|
|
1848
1849
|
} catch (error) {
|
|
@@ -1970,7 +1971,7 @@ var StructureValidator = class {
|
|
|
1970
1971
|
const warnings = [];
|
|
1971
1972
|
let parsed;
|
|
1972
1973
|
try {
|
|
1973
|
-
parsed =
|
|
1974
|
+
parsed = matter4(content);
|
|
1974
1975
|
} catch (error) {
|
|
1975
1976
|
errors.push({
|
|
1976
1977
|
message: "Failed to parse frontmatter",
|
|
@@ -1987,33 +1988,6 @@ var StructureValidator = class {
|
|
|
1987
1988
|
});
|
|
1988
1989
|
}
|
|
1989
1990
|
const headings = this.extractHeadings(body);
|
|
1990
|
-
for (const requiredSection of this.requiredSections) {
|
|
1991
|
-
const found = headings.some(
|
|
1992
|
-
(h) => h.level === 2 && h.text.toLowerCase() === requiredSection.toLowerCase()
|
|
1993
|
-
);
|
|
1994
|
-
if (!found) {
|
|
1995
|
-
if (this.strict) {
|
|
1996
|
-
errors.push({
|
|
1997
|
-
message: `Missing required section: ## ${requiredSection}`,
|
|
1998
|
-
suggestion: `Add ## ${requiredSection} section to the spec`
|
|
1999
|
-
});
|
|
2000
|
-
} else {
|
|
2001
|
-
warnings.push({
|
|
2002
|
-
message: `Recommended section missing: ## ${requiredSection}`,
|
|
2003
|
-
suggestion: `Consider adding ## ${requiredSection} section`
|
|
2004
|
-
});
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
const emptySections = this.findEmptySections(body, headings);
|
|
2009
|
-
for (const section of emptySections) {
|
|
2010
|
-
if (this.requiredSections.some((req) => req.toLowerCase() === section.toLowerCase())) {
|
|
2011
|
-
warnings.push({
|
|
2012
|
-
message: `Empty required section: ## ${section}`,
|
|
2013
|
-
suggestion: "Add content to this section or remove it"
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
1991
|
const duplicates = this.findDuplicateHeaders(headings);
|
|
2018
1992
|
for (const dup of duplicates) {
|
|
2019
1993
|
errors.push({
|
|
@@ -2296,9 +2270,1099 @@ var CorruptionValidator = class {
|
|
|
2296
2270
|
suggestion: "Check for missing closing * in markdown content (not code blocks)"
|
|
2297
2271
|
});
|
|
2298
2272
|
}
|
|
2299
|
-
return errors;
|
|
2273
|
+
return errors;
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
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
|
+
}
|
|
2282
|
+
}
|
|
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];
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
if (previousData && data.status !== previousData.status) {
|
|
2299
|
+
if (!Array.isArray(data.transitions)) {
|
|
2300
|
+
data.transitions = [];
|
|
2301
|
+
}
|
|
2302
|
+
data.transitions.push({
|
|
2303
|
+
status: data.status,
|
|
2304
|
+
at: now
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
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;
|
|
2314
|
+
}
|
|
2315
|
+
} catch {
|
|
2316
|
+
data.tags = data.tags.split(",").map((t) => t.trim());
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
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 })
|
|
2330
|
+
}
|
|
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) ;
|
|
2351
|
+
}
|
|
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;
|
|
2384
|
+
}
|
|
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 })
|
|
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}`;
|
|
2431
|
+
}
|
|
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);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
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;
|
|
2481
|
+
}
|
|
2482
|
+
if (inCodeBlock) {
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
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);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
while (sectionStack.length > 0) {
|
|
2513
|
+
const closedSection = sectionStack.pop();
|
|
2514
|
+
closedSection.endLine = lines.length;
|
|
2515
|
+
closedSection.lineCount = closedSection.endLine - closedSection.startLine + 1;
|
|
2516
|
+
}
|
|
2517
|
+
return sections;
|
|
2518
|
+
}
|
|
2519
|
+
function flattenSections(sections) {
|
|
2520
|
+
const result = [];
|
|
2521
|
+
for (const section of sections) {
|
|
2522
|
+
result.push(section);
|
|
2523
|
+
result.push(...flattenSections(section.subsections));
|
|
2524
|
+
}
|
|
2525
|
+
return result;
|
|
2526
|
+
}
|
|
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}`);
|
|
2531
|
+
}
|
|
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}`);
|
|
2539
|
+
}
|
|
2540
|
+
lines.splice(startLine - 1, endLine - startLine + 1);
|
|
2541
|
+
return lines.join("\n");
|
|
2542
|
+
}
|
|
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
|
+
}
|
|
2300
3350
|
}
|
|
2301
|
-
|
|
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
|
|
2302
3366
|
var SubSpecValidator = class {
|
|
2303
3367
|
name = "sub-specs";
|
|
2304
3368
|
description = "Validate sub-spec files using direct token thresholds (spec 071)";
|
|
@@ -2306,13 +3370,11 @@ var SubSpecValidator = class {
|
|
|
2306
3370
|
goodThreshold;
|
|
2307
3371
|
warningThreshold;
|
|
2308
3372
|
maxLines;
|
|
2309
|
-
checkCrossReferences;
|
|
2310
3373
|
constructor(options = {}) {
|
|
2311
3374
|
this.excellentThreshold = options.excellentThreshold ?? 2e3;
|
|
2312
3375
|
this.goodThreshold = options.goodThreshold ?? 3500;
|
|
2313
3376
|
this.warningThreshold = options.warningThreshold ?? 5e3;
|
|
2314
3377
|
this.maxLines = options.maxLines ?? 500;
|
|
2315
|
-
this.checkCrossReferences = options.checkCrossReferences ?? true;
|
|
2316
3378
|
}
|
|
2317
3379
|
async validate(spec, content) {
|
|
2318
3380
|
const errors = [];
|
|
@@ -2325,9 +3387,6 @@ var SubSpecValidator = class {
|
|
|
2325
3387
|
this.validateNamingConventions(subSpecs, warnings);
|
|
2326
3388
|
await this.validateComplexity(subSpecs, errors, warnings);
|
|
2327
3389
|
this.checkOrphanedSubSpecs(subSpecs, content, warnings);
|
|
2328
|
-
if (this.checkCrossReferences) {
|
|
2329
|
-
await this.validateCrossReferences(subSpecs, spec, warnings);
|
|
2330
|
-
}
|
|
2331
3390
|
return {
|
|
2332
3391
|
passed: errors.length === 0,
|
|
2333
3392
|
errors,
|
|
@@ -2390,12 +3449,6 @@ var SubSpecValidator = class {
|
|
|
2390
3449
|
suggestion: "Break into 15-35 sections for better readability (7\xB12 cognitive chunks)"
|
|
2391
3450
|
});
|
|
2392
3451
|
}
|
|
2393
|
-
if (lineCount > this.maxLines) {
|
|
2394
|
-
warnings.push({
|
|
2395
|
-
message: `Sub-spec ${subSpec.name} is very long (${lineCount} lines)`,
|
|
2396
|
-
suggestion: "Consider splitting even if token count is acceptable"
|
|
2397
|
-
});
|
|
2398
|
-
}
|
|
2399
3452
|
}
|
|
2400
3453
|
}
|
|
2401
3454
|
/**
|
|
@@ -2415,29 +3468,6 @@ var SubSpecValidator = class {
|
|
|
2415
3468
|
}
|
|
2416
3469
|
}
|
|
2417
3470
|
}
|
|
2418
|
-
/**
|
|
2419
|
-
* Validate cross-document references between sub-specs
|
|
2420
|
-
*/
|
|
2421
|
-
async validateCrossReferences(subSpecs, spec, warnings) {
|
|
2422
|
-
const validFileNames = new Set(subSpecs.map((s) => s.name));
|
|
2423
|
-
validFileNames.add("README.md");
|
|
2424
|
-
for (const subSpec of subSpecs) {
|
|
2425
|
-
if (!subSpec.content) {
|
|
2426
|
-
continue;
|
|
2427
|
-
}
|
|
2428
|
-
const linkRegex = /\[([^\]]+)\]\((?:\.\/)?(([^)]+)\.md)\)/g;
|
|
2429
|
-
let match;
|
|
2430
|
-
while ((match = linkRegex.exec(subSpec.content)) !== null) {
|
|
2431
|
-
const referencedFile = match[2];
|
|
2432
|
-
if (!validFileNames.has(referencedFile)) {
|
|
2433
|
-
warnings.push({
|
|
2434
|
-
message: `Broken reference in ${subSpec.name}: ${referencedFile} not found`,
|
|
2435
|
-
suggestion: `Check if ${referencedFile} exists or update the link`
|
|
2436
|
-
});
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
3471
|
};
|
|
2442
3472
|
function groupIssuesByFile(results) {
|
|
2443
3473
|
const fileMap = /* @__PURE__ */ new Map();
|
|
@@ -2911,7 +3941,7 @@ function isCriticalOverdue(spec) {
|
|
|
2911
3941
|
if (!spec.frontmatter.due) {
|
|
2912
3942
|
return false;
|
|
2913
3943
|
}
|
|
2914
|
-
const isOverdue =
|
|
3944
|
+
const isOverdue = dayjs3(spec.frontmatter.due).isBefore(dayjs3(), "day");
|
|
2915
3945
|
const isCritical = spec.frontmatter.priority === "critical" || spec.frontmatter.priority === "high";
|
|
2916
3946
|
return isOverdue && isCritical;
|
|
2917
3947
|
}
|
|
@@ -2923,7 +3953,7 @@ function isLongRunning(spec) {
|
|
|
2923
3953
|
if (!updatedAt) {
|
|
2924
3954
|
return false;
|
|
2925
3955
|
}
|
|
2926
|
-
const daysSinceUpdate =
|
|
3956
|
+
const daysSinceUpdate = dayjs3().diff(dayjs3(updatedAt), "day");
|
|
2927
3957
|
return daysSinceUpdate > 7;
|
|
2928
3958
|
}
|
|
2929
3959
|
function calculateCompletion(specs) {
|
|
@@ -2975,8 +4005,8 @@ function calculateCycleTime(spec) {
|
|
|
2975
4005
|
if (!createdAt || !completedAt) {
|
|
2976
4006
|
return null;
|
|
2977
4007
|
}
|
|
2978
|
-
const created =
|
|
2979
|
-
const completed =
|
|
4008
|
+
const created = dayjs3(createdAt);
|
|
4009
|
+
const completed = dayjs3(completedAt);
|
|
2980
4010
|
return completed.diff(created, "day", true);
|
|
2981
4011
|
}
|
|
2982
4012
|
function calculateLeadTime(spec, fromStatus, toStatus) {
|
|
@@ -2989,12 +4019,12 @@ function calculateLeadTime(spec, fromStatus, toStatus) {
|
|
|
2989
4019
|
if (!fromTransition || !toTransition) {
|
|
2990
4020
|
return null;
|
|
2991
4021
|
}
|
|
2992
|
-
const from =
|
|
2993
|
-
const to =
|
|
4022
|
+
const from = dayjs3(fromTransition.at);
|
|
4023
|
+
const to = dayjs3(toTransition.at);
|
|
2994
4024
|
return to.diff(from, "day", true);
|
|
2995
4025
|
}
|
|
2996
4026
|
function calculateThroughput(specs, days) {
|
|
2997
|
-
const cutoff =
|
|
4027
|
+
const cutoff = dayjs3().subtract(days, "day");
|
|
2998
4028
|
return specs.filter((s) => {
|
|
2999
4029
|
if (s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived") {
|
|
3000
4030
|
return false;
|
|
@@ -3003,19 +4033,19 @@ function calculateThroughput(specs, days) {
|
|
|
3003
4033
|
if (!completedAt) {
|
|
3004
4034
|
return false;
|
|
3005
4035
|
}
|
|
3006
|
-
return
|
|
4036
|
+
return dayjs3(completedAt).isAfter(cutoff);
|
|
3007
4037
|
}).length;
|
|
3008
4038
|
}
|
|
3009
|
-
function calculateWIP(specs, date =
|
|
4039
|
+
function calculateWIP(specs, date = dayjs3()) {
|
|
3010
4040
|
return specs.filter((s) => {
|
|
3011
4041
|
const createdAt = s.frontmatter.created_at || s.frontmatter.created;
|
|
3012
|
-
const created =
|
|
4042
|
+
const created = dayjs3(createdAt);
|
|
3013
4043
|
if (created.isAfter(date)) {
|
|
3014
4044
|
return false;
|
|
3015
4045
|
}
|
|
3016
4046
|
const completedAt = s.frontmatter.completed_at || s.frontmatter.completed;
|
|
3017
4047
|
if (completedAt) {
|
|
3018
|
-
const completed =
|
|
4048
|
+
const completed = dayjs3(completedAt);
|
|
3019
4049
|
return completed.isAfter(date);
|
|
3020
4050
|
}
|
|
3021
4051
|
return s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived";
|
|
@@ -3032,19 +4062,19 @@ function calculateVelocityMetrics(specs) {
|
|
|
3032
4062
|
const avgInProgressToComplete = inProgressToCompleteTimes.length > 0 ? inProgressToCompleteTimes.reduce((sum, t) => sum + t, 0) / inProgressToCompleteTimes.length : 0;
|
|
3033
4063
|
const throughputWeek = calculateThroughput(specs, 7);
|
|
3034
4064
|
const throughputMonth = calculateThroughput(specs, 30);
|
|
3035
|
-
const prevWeekStart =
|
|
3036
|
-
const prevWeekEnd =
|
|
4065
|
+
const prevWeekStart = dayjs3().subtract(14, "day");
|
|
4066
|
+
const prevWeekEnd = dayjs3().subtract(7, "day");
|
|
3037
4067
|
const throughputPrevWeek = specs.filter((s) => {
|
|
3038
4068
|
const completedAt = s.frontmatter.completed_at || s.frontmatter.completed;
|
|
3039
4069
|
if (!completedAt) return false;
|
|
3040
|
-
const completed =
|
|
4070
|
+
const completed = dayjs3(completedAt);
|
|
3041
4071
|
return completed.isAfter(prevWeekStart) && !completed.isAfter(prevWeekEnd);
|
|
3042
4072
|
}).length;
|
|
3043
4073
|
const throughputTrend = throughputWeek > throughputPrevWeek ? "up" : throughputWeek < throughputPrevWeek ? "down" : "stable";
|
|
3044
4074
|
const currentWIP = calculateWIP(specs);
|
|
3045
4075
|
const wipSamples = [];
|
|
3046
4076
|
for (let i = 0; i < 30; i++) {
|
|
3047
|
-
const sampleDate =
|
|
4077
|
+
const sampleDate = dayjs3().subtract(i, "day");
|
|
3048
4078
|
wipSamples.push(calculateWIP(specs, sampleDate));
|
|
3049
4079
|
}
|
|
3050
4080
|
const avgWIP = wipSamples.length > 0 ? wipSamples.reduce((sum, w) => sum + w, 0) / wipSamples.length : 0;
|
|
@@ -3249,7 +4279,7 @@ function countSpecsByStatusAndPriority(specs) {
|
|
|
3249
4279
|
function generateInsights(specs) {
|
|
3250
4280
|
const insights = [];
|
|
3251
4281
|
const criticalOverdue = specs.filter(
|
|
3252
|
-
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due &&
|
|
4282
|
+
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived"
|
|
3253
4283
|
);
|
|
3254
4284
|
if (criticalOverdue.length > 0) {
|
|
3255
4285
|
insights.push({
|
|
@@ -3259,7 +4289,7 @@ function generateInsights(specs) {
|
|
|
3259
4289
|
});
|
|
3260
4290
|
}
|
|
3261
4291
|
const highOverdue = specs.filter(
|
|
3262
|
-
(s) => s.frontmatter.priority === "high" && s.frontmatter.due &&
|
|
4292
|
+
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived"
|
|
3263
4293
|
);
|
|
3264
4294
|
if (highOverdue.length > 0) {
|
|
3265
4295
|
insights.push({
|
|
@@ -3276,7 +4306,7 @@ function generateInsights(specs) {
|
|
|
3276
4306
|
if (!updatedAt) {
|
|
3277
4307
|
return false;
|
|
3278
4308
|
}
|
|
3279
|
-
const daysSinceUpdate =
|
|
4309
|
+
const daysSinceUpdate = dayjs3().diff(dayjs3(updatedAt), "day");
|
|
3280
4310
|
return daysSinceUpdate > 7;
|
|
3281
4311
|
});
|
|
3282
4312
|
if (longRunning.length > 0) {
|
|
@@ -3309,14 +4339,14 @@ function generateInsights(specs) {
|
|
|
3309
4339
|
return insights.slice(0, 5);
|
|
3310
4340
|
}
|
|
3311
4341
|
function getSpecInsightDetails(spec) {
|
|
3312
|
-
if (spec.frontmatter.due &&
|
|
3313
|
-
const daysOverdue =
|
|
4342
|
+
if (spec.frontmatter.due && dayjs3(spec.frontmatter.due).isBefore(dayjs3(), "day") && spec.frontmatter.status !== "complete" && spec.frontmatter.status !== "archived") {
|
|
4343
|
+
const daysOverdue = dayjs3().diff(dayjs3(spec.frontmatter.due), "day");
|
|
3314
4344
|
return `overdue by ${daysOverdue} day${daysOverdue > 1 ? "s" : ""}`;
|
|
3315
4345
|
}
|
|
3316
4346
|
if (spec.frontmatter.status === "in-progress") {
|
|
3317
4347
|
const updatedAt = spec.frontmatter.updated || spec.frontmatter.updated_at || spec.frontmatter.created || spec.frontmatter.created_at;
|
|
3318
4348
|
if (updatedAt) {
|
|
3319
|
-
const daysSinceUpdate =
|
|
4349
|
+
const daysSinceUpdate = dayjs3().diff(dayjs3(updatedAt), "day");
|
|
3320
4350
|
if (daysSinceUpdate > 7) {
|
|
3321
4351
|
return `in-progress for ${daysSinceUpdate} days`;
|
|
3322
4352
|
}
|
|
@@ -3416,7 +4446,7 @@ async function statsCommand(options) {
|
|
|
3416
4446
|
const criticalInProgress = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "in-progress").length;
|
|
3417
4447
|
const criticalComplete = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "complete").length;
|
|
3418
4448
|
const criticalOverdue = specs.filter(
|
|
3419
|
-
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due &&
|
|
4449
|
+
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
3420
4450
|
).length;
|
|
3421
4451
|
const parts = [];
|
|
3422
4452
|
if (criticalPlanned > 0) parts.push(chalk16.dim(`${criticalPlanned} planned`));
|
|
@@ -3430,7 +4460,7 @@ async function statsCommand(options) {
|
|
|
3430
4460
|
const highInProgress = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "in-progress").length;
|
|
3431
4461
|
const highComplete = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "complete").length;
|
|
3432
4462
|
const highOverdue = specs.filter(
|
|
3433
|
-
(s) => s.frontmatter.priority === "high" && s.frontmatter.due &&
|
|
4463
|
+
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
3434
4464
|
).length;
|
|
3435
4465
|
const parts = [];
|
|
3436
4466
|
if (highPlanned > 0) parts.push(chalk16.dim(`${highPlanned} planned`));
|
|
@@ -3592,18 +4622,18 @@ async function statsCommand(options) {
|
|
|
3592
4622
|
}
|
|
3593
4623
|
if (showTimeline) {
|
|
3594
4624
|
const days = 30;
|
|
3595
|
-
const today =
|
|
4625
|
+
const today = dayjs3();
|
|
3596
4626
|
const startDate = today.subtract(days, "day");
|
|
3597
4627
|
const createdByDate = {};
|
|
3598
4628
|
const completedByDate = {};
|
|
3599
4629
|
for (const spec of specs) {
|
|
3600
|
-
const created =
|
|
4630
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
3601
4631
|
if (created.isAfter(startDate)) {
|
|
3602
4632
|
const dateKey = created.format("YYYY-MM-DD");
|
|
3603
4633
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
3604
4634
|
}
|
|
3605
4635
|
if (spec.frontmatter.completed) {
|
|
3606
|
-
const completed =
|
|
4636
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
3607
4637
|
if (completed.isAfter(startDate)) {
|
|
3608
4638
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
3609
4639
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
@@ -3742,27 +4772,21 @@ async function searchCommand(query, options) {
|
|
|
3742
4772
|
console.log("No specs found matching filters.");
|
|
3743
4773
|
return;
|
|
3744
4774
|
}
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
if (matches.length > 0) {
|
|
3763
|
-
results.push({ spec, matches });
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
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;
|
|
3766
4790
|
if (results.length === 0) {
|
|
3767
4791
|
console.log("");
|
|
3768
4792
|
console.log(chalk16.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
|
|
@@ -3779,45 +4803,68 @@ async function searchCommand(query, options) {
|
|
|
3779
4803
|
}
|
|
3780
4804
|
console.log("");
|
|
3781
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`));
|
|
3782
4807
|
if (Object.keys(filter).length > 0) {
|
|
3783
4808
|
const filters = [];
|
|
3784
4809
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
3785
4810
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
3786
4811
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
3787
4812
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
3788
|
-
console.log(chalk16.gray(`With filters: ${filters.join(", ")}`));
|
|
4813
|
+
console.log(chalk16.gray(` With filters: ${filters.join(", ")}`));
|
|
3789
4814
|
}
|
|
3790
4815
|
console.log("");
|
|
3791
4816
|
for (const result of results) {
|
|
3792
|
-
const { spec, matches } = result;
|
|
3793
|
-
|
|
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)`)}`));
|
|
3794
4820
|
const meta = [];
|
|
3795
|
-
if (spec.
|
|
3796
|
-
const priorityEmoji = spec.
|
|
3797
|
-
meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.
|
|
4821
|
+
if (spec.priority) {
|
|
4822
|
+
const priorityEmoji = spec.priority === "critical" ? "\u{1F534}" : spec.priority === "high" ? "\u{1F7E1}" : spec.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
4823
|
+
meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.priority)}`);
|
|
3798
4824
|
}
|
|
3799
|
-
if (spec.
|
|
3800
|
-
meta.push(`[${spec.
|
|
4825
|
+
if (spec.tags && spec.tags.length > 0) {
|
|
4826
|
+
meta.push(`[${spec.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
|
|
3801
4827
|
}
|
|
3802
4828
|
if (meta.length > 0) {
|
|
3803
|
-
console.log(chalk16.gray(`
|
|
4829
|
+
console.log(chalk16.gray(` ${meta.join(" \u2022 ")}`));
|
|
3804
4830
|
}
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
console.log(`
|
|
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
|
+
}
|
|
3808
4850
|
}
|
|
3809
|
-
if (matches.length
|
|
3810
|
-
console.log(chalk16.gray(`
|
|
4851
|
+
if (totalMatches > matches.length) {
|
|
4852
|
+
console.log(chalk16.gray(` ... and ${totalMatches - matches.length} more match${totalMatches - matches.length === 1 ? "" : "es"}`));
|
|
3811
4853
|
}
|
|
3812
4854
|
console.log("");
|
|
3813
4855
|
}
|
|
3814
4856
|
}
|
|
3815
|
-
function
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
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;
|
|
3821
4868
|
}
|
|
3822
4869
|
async function depsCommand(specPath, options) {
|
|
3823
4870
|
await autoCheckIfEnabled();
|
|
@@ -3899,8 +4946,8 @@ function findDependencies(spec, specMap) {
|
|
|
3899
4946
|
if (dep) {
|
|
3900
4947
|
deps.push(dep);
|
|
3901
4948
|
} else {
|
|
3902
|
-
for (const [
|
|
3903
|
-
if (
|
|
4949
|
+
for (const [path26, s] of specMap.entries()) {
|
|
4950
|
+
if (path26.includes(depPath)) {
|
|
3904
4951
|
deps.push(s);
|
|
3905
4952
|
break;
|
|
3906
4953
|
}
|
|
@@ -3932,8 +4979,8 @@ function findRelated(spec, specMap) {
|
|
|
3932
4979
|
if (rel) {
|
|
3933
4980
|
related.push(rel);
|
|
3934
4981
|
} else {
|
|
3935
|
-
for (const [
|
|
3936
|
-
if (
|
|
4982
|
+
for (const [path26, s] of specMap.entries()) {
|
|
4983
|
+
if (path26.includes(relPath)) {
|
|
3937
4984
|
related.push(s);
|
|
3938
4985
|
break;
|
|
3939
4986
|
}
|
|
@@ -4016,13 +5063,13 @@ async function timelineCommand(options) {
|
|
|
4016
5063
|
console.log("No specs found.");
|
|
4017
5064
|
return;
|
|
4018
5065
|
}
|
|
4019
|
-
const today =
|
|
5066
|
+
const today = dayjs3();
|
|
4020
5067
|
const startDate = today.subtract(days, "day");
|
|
4021
5068
|
const createdByDate = {};
|
|
4022
5069
|
const completedByDate = {};
|
|
4023
5070
|
const createdByMonth = {};
|
|
4024
5071
|
for (const spec of specs) {
|
|
4025
|
-
const created =
|
|
5072
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4026
5073
|
if (created.isAfter(startDate)) {
|
|
4027
5074
|
const dateKey = created.format("YYYY-MM-DD");
|
|
4028
5075
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
@@ -4030,7 +5077,7 @@ async function timelineCommand(options) {
|
|
|
4030
5077
|
const monthKey = created.format("MMM YYYY");
|
|
4031
5078
|
createdByMonth[monthKey] = (createdByMonth[monthKey] || 0) + 1;
|
|
4032
5079
|
if (spec.frontmatter.completed) {
|
|
4033
|
-
const completed =
|
|
5080
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4034
5081
|
if (completed.isAfter(startDate)) {
|
|
4035
5082
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
4036
5083
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
@@ -4063,8 +5110,8 @@ async function timelineCommand(options) {
|
|
|
4063
5110
|
console.log("");
|
|
4064
5111
|
}
|
|
4065
5112
|
const sortedMonths = Object.entries(createdByMonth).sort((a, b) => {
|
|
4066
|
-
const dateA =
|
|
4067
|
-
const dateB =
|
|
5113
|
+
const dateA = dayjs3(a[0], "MMM YYYY");
|
|
5114
|
+
const dateB = dayjs3(b[0], "MMM YYYY");
|
|
4068
5115
|
return dateB.diff(dateA);
|
|
4069
5116
|
}).slice(0, 6);
|
|
4070
5117
|
if (sortedMonths.length > 0) {
|
|
@@ -4085,12 +5132,12 @@ async function timelineCommand(options) {
|
|
|
4085
5132
|
}
|
|
4086
5133
|
const last7Days = specs.filter((s) => {
|
|
4087
5134
|
if (!s.frontmatter.completed) return false;
|
|
4088
|
-
const completed =
|
|
5135
|
+
const completed = dayjs3(s.frontmatter.completed);
|
|
4089
5136
|
return completed.isAfter(today.subtract(7, "day"));
|
|
4090
5137
|
}).length;
|
|
4091
5138
|
const last30Days = specs.filter((s) => {
|
|
4092
5139
|
if (!s.frontmatter.completed) return false;
|
|
4093
|
-
const completed =
|
|
5140
|
+
const completed = dayjs3(s.frontmatter.completed);
|
|
4094
5141
|
return completed.isAfter(today.subtract(30, "day"));
|
|
4095
5142
|
}).length;
|
|
4096
5143
|
console.log(chalk16.bold("\u2705 Completion Rate"));
|
|
@@ -4105,14 +5152,14 @@ async function timelineCommand(options) {
|
|
|
4105
5152
|
if (options.byTag) {
|
|
4106
5153
|
const tagStats = {};
|
|
4107
5154
|
for (const spec of specs) {
|
|
4108
|
-
const created =
|
|
5155
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4109
5156
|
const isInRange = created.isAfter(startDate);
|
|
4110
5157
|
if (isInRange && spec.frontmatter.tags) {
|
|
4111
5158
|
for (const tag of spec.frontmatter.tags) {
|
|
4112
5159
|
if (!tagStats[tag]) tagStats[tag] = { created: 0, completed: 0 };
|
|
4113
5160
|
tagStats[tag].created++;
|
|
4114
5161
|
if (spec.frontmatter.completed) {
|
|
4115
|
-
const completed =
|
|
5162
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4116
5163
|
if (completed.isAfter(startDate)) {
|
|
4117
5164
|
tagStats[tag].completed++;
|
|
4118
5165
|
}
|
|
@@ -4133,14 +5180,14 @@ async function timelineCommand(options) {
|
|
|
4133
5180
|
const assigneeStats = {};
|
|
4134
5181
|
for (const spec of specs) {
|
|
4135
5182
|
if (!spec.frontmatter.assignee) continue;
|
|
4136
|
-
const created =
|
|
5183
|
+
const created = dayjs3(spec.frontmatter.created);
|
|
4137
5184
|
const isInRange = created.isAfter(startDate);
|
|
4138
5185
|
if (isInRange) {
|
|
4139
5186
|
const assignee = spec.frontmatter.assignee;
|
|
4140
5187
|
if (!assigneeStats[assignee]) assigneeStats[assignee] = { created: 0, completed: 0 };
|
|
4141
5188
|
assigneeStats[assignee].created++;
|
|
4142
5189
|
if (spec.frontmatter.completed) {
|
|
4143
|
-
const completed =
|
|
5190
|
+
const completed = dayjs3(spec.frontmatter.completed);
|
|
4144
5191
|
if (completed.isAfter(startDate)) {
|
|
4145
5192
|
assigneeStats[assignee].completed++;
|
|
4146
5193
|
}
|
|
@@ -4219,18 +5266,18 @@ async function ganttCommand(options) {
|
|
|
4219
5266
|
if (a.frontmatter.due && !b.frontmatter.due) return -1;
|
|
4220
5267
|
if (!a.frontmatter.due && b.frontmatter.due) return 1;
|
|
4221
5268
|
if (a.frontmatter.due && b.frontmatter.due) {
|
|
4222
|
-
return
|
|
5269
|
+
return dayjs3(a.frontmatter.due).diff(dayjs3(b.frontmatter.due));
|
|
4223
5270
|
}
|
|
4224
5271
|
return 0;
|
|
4225
5272
|
});
|
|
4226
5273
|
};
|
|
4227
|
-
const today =
|
|
5274
|
+
const today = dayjs3();
|
|
4228
5275
|
const startDate = today.startOf("week");
|
|
4229
5276
|
const endDate = startDate.add(weeks, "week");
|
|
4230
5277
|
const inProgress = relevantSpecs.filter((s) => s.frontmatter.status === "in-progress").length;
|
|
4231
5278
|
const planned = relevantSpecs.filter((s) => s.frontmatter.status === "planned").length;
|
|
4232
5279
|
const overdue = relevantSpecs.filter(
|
|
4233
|
-
(s) => s.frontmatter.due &&
|
|
5280
|
+
(s) => s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
|
|
4234
5281
|
).length;
|
|
4235
5282
|
console.log(chalk16.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
|
|
4236
5283
|
console.log("");
|
|
@@ -4296,7 +5343,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
4296
5343
|
function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
4297
5344
|
const charsPerWeek = 8;
|
|
4298
5345
|
const totalChars = weeks * charsPerWeek;
|
|
4299
|
-
const due =
|
|
5346
|
+
const due = dayjs3(spec.frontmatter.due);
|
|
4300
5347
|
const specStart = today;
|
|
4301
5348
|
const startDaysFromStart = specStart.diff(startDate, "day");
|
|
4302
5349
|
const dueDaysFromStart = due.diff(startDate, "day");
|
|
@@ -4471,6 +5518,450 @@ async function tokensAllCommand(options = {}) {
|
|
|
4471
5518
|
console.log(chalk16.dim("Legend: \u2705 excellent (<2K) | \u{1F44D} good (<3.5K) | \u26A0\uFE0F warning (<5K) | \u{1F534} problem (>5K)"));
|
|
4472
5519
|
console.log("");
|
|
4473
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
|
+
}
|
|
4474
5965
|
marked.use(markedTerminal());
|
|
4475
5966
|
async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
4476
5967
|
const config = await loadConfig(cwd);
|
|
@@ -4756,24 +6247,46 @@ async function searchSpecsData(query, options) {
|
|
|
4756
6247
|
includeContent: true,
|
|
4757
6248
|
filter
|
|
4758
6249
|
});
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
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
|
+
};
|
|
4777
6290
|
}
|
|
4778
6291
|
async function readSpecData(specPath) {
|
|
4779
6292
|
const cwd = process.cwd();
|
|
@@ -4909,28 +6422,44 @@ async function createMcpServer() {
|
|
|
4909
6422
|
"search",
|
|
4910
6423
|
{
|
|
4911
6424
|
title: "Search Specs",
|
|
4912
|
-
description: "
|
|
6425
|
+
description: "Intelligent relevance-ranked search across all specification content. Uses field-weighted scoring (title > tags > description > content) to return the most relevant specs. Returns matching specs with relevance scores, highlighted excerpts, and metadata.",
|
|
4913
6426
|
inputSchema: {
|
|
4914
|
-
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."),
|
|
4915
6428
|
status: z.enum(["planned", "in-progress", "complete", "archived"]).optional().describe("Limit search to specs with this status."),
|
|
4916
6429
|
tags: z.array(z.string()).optional().describe("Limit search to specs with these tags."),
|
|
4917
6430
|
priority: z.enum(["low", "medium", "high", "critical"]).optional().describe("Limit search to specs with this priority.")
|
|
4918
6431
|
},
|
|
4919
6432
|
outputSchema: {
|
|
4920
|
-
results: z.array(z.
|
|
6433
|
+
results: z.array(z.object({
|
|
6434
|
+
spec: z.any(),
|
|
6435
|
+
score: z.number(),
|
|
6436
|
+
totalMatches: z.number(),
|
|
6437
|
+
matches: z.array(z.object({
|
|
6438
|
+
field: z.string(),
|
|
6439
|
+
text: z.string(),
|
|
6440
|
+
lineNumber: z.number().optional(),
|
|
6441
|
+
score: z.number(),
|
|
6442
|
+
highlights: z.array(z.tuple([z.number(), z.number()]))
|
|
6443
|
+
}))
|
|
6444
|
+
})),
|
|
6445
|
+
metadata: z.object({
|
|
6446
|
+
totalResults: z.number(),
|
|
6447
|
+
searchTime: z.number(),
|
|
6448
|
+
query: z.string(),
|
|
6449
|
+
specsSearched: z.number()
|
|
6450
|
+
})
|
|
4921
6451
|
}
|
|
4922
6452
|
},
|
|
4923
6453
|
async (input) => {
|
|
4924
6454
|
try {
|
|
4925
|
-
const
|
|
6455
|
+
const searchResult = await searchSpecsData(input.query, {
|
|
4926
6456
|
status: input.status,
|
|
4927
6457
|
tags: input.tags,
|
|
4928
6458
|
priority: input.priority
|
|
4929
6459
|
});
|
|
4930
|
-
const output = { results };
|
|
4931
6460
|
return {
|
|
4932
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
4933
|
-
structuredContent:
|
|
6461
|
+
content: [{ type: "text", text: JSON.stringify(searchResult, null, 2) }],
|
|
6462
|
+
structuredContent: searchResult
|
|
4934
6463
|
};
|
|
4935
6464
|
} catch (error) {
|
|
4936
6465
|
const errorMessage = formatErrorMessage("Error searching specs", error);
|
|
@@ -5442,6 +6971,81 @@ ${result.content}`;
|
|
|
5442
6971
|
}
|
|
5443
6972
|
}
|
|
5444
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
|
+
);
|
|
5445
7049
|
server.registerResource(
|
|
5446
7050
|
"spec",
|
|
5447
7051
|
new ResourceTemplate("spec://{specPath}", { list: void 0 }),
|
|
@@ -5621,6 +7225,6 @@ Please search for this topic and show me the dependencies between related specs.
|
|
|
5621
7225
|
return server;
|
|
5622
7226
|
}
|
|
5623
7227
|
|
|
5624
|
-
export { addTemplate, archiveSpec, backfillTimestamps, boardCommand, checkSpecs, copyTemplate, createMcpServer, createSpec, depsCommand, filesCommand, ganttCommand, initProject, listSpecs, listTemplates, mcpCommand, migrateCommand, openCommand, removeTemplate, searchCommand, showTemplate, statsCommand, timelineCommand, tokensAllCommand, tokensCommand, updateSpec, validateCommand, viewCommand };
|
|
5625
|
-
//# sourceMappingURL=chunk-
|
|
5626
|
-
//# sourceMappingURL=chunk-
|
|
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
|