speclore 0.1.8 → 0.1.10
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/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/index.js +379 -227
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +379 -227
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +266 -136
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -148,7 +148,7 @@ var init_cost_tracker = __esm({
|
|
|
148
148
|
totalTokens: promptTokens + completionTokens,
|
|
149
149
|
estimatedCostUsd: cost
|
|
150
150
|
});
|
|
151
|
-
logger.debug(`AI usage: ${model}
|
|
151
|
+
logger.debug(`AI usage: ${model} - ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
|
|
152
152
|
return cost;
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
@@ -818,7 +818,7 @@ function acquireLock(specLoreDir) {
|
|
|
818
818
|
try {
|
|
819
819
|
writeFileSync(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
820
820
|
} catch {
|
|
821
|
-
logger.debug("Lock write failed
|
|
821
|
+
logger.debug("Lock write failed - another process may have acquired it");
|
|
822
822
|
return false;
|
|
823
823
|
}
|
|
824
824
|
logger.debug(`Lock acquired by PID ${process.pid}`);
|
|
@@ -878,8 +878,8 @@ init_logger();
|
|
|
878
878
|
|
|
879
879
|
// src/core/requirement-reader/index.ts
|
|
880
880
|
init_logger();
|
|
881
|
-
import { existsSync as
|
|
882
|
-
import { extname as
|
|
881
|
+
import { existsSync as existsSync7 } from "fs";
|
|
882
|
+
import { extname as extname7 } from "path";
|
|
883
883
|
|
|
884
884
|
// src/plugins/registry.ts
|
|
885
885
|
init_logger();
|
|
@@ -1026,8 +1026,8 @@ var PdfReader = class {
|
|
|
1026
1026
|
} catch {
|
|
1027
1027
|
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1028
1028
|
}
|
|
1029
|
-
const { readFileSync:
|
|
1030
|
-
const buffer =
|
|
1029
|
+
const { readFileSync: readFileSync19 } = await import("fs");
|
|
1030
|
+
const buffer = readFileSync19(source);
|
|
1031
1031
|
const uint8 = new Uint8Array(buffer);
|
|
1032
1032
|
const doc = await pdfjsLib.getDocument({
|
|
1033
1033
|
data: uint8,
|
|
@@ -1060,13 +1060,13 @@ var ImageReader = class {
|
|
|
1060
1060
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
1061
1061
|
}
|
|
1062
1062
|
async read(source) {
|
|
1063
|
-
const { existsSync:
|
|
1064
|
-
if (!
|
|
1063
|
+
const { existsSync: existsSync23, readFileSync: readFileSync19 } = await import("fs");
|
|
1064
|
+
if (!existsSync23(source)) {
|
|
1065
1065
|
throw new Error(`Image file not found: ${source}`);
|
|
1066
1066
|
}
|
|
1067
1067
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
1068
1068
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
1069
|
-
const buffer =
|
|
1069
|
+
const buffer = readFileSync19(source);
|
|
1070
1070
|
let text;
|
|
1071
1071
|
try {
|
|
1072
1072
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -1258,26 +1258,45 @@ var ClaudeWriter = class {
|
|
|
1258
1258
|
};
|
|
1259
1259
|
|
|
1260
1260
|
// src/plugins/builtin/qoder-writer.ts
|
|
1261
|
-
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as
|
|
1262
|
-
import { join as
|
|
1261
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6, rmSync as rmSync3 } from "fs";
|
|
1262
|
+
import { join as join5 } from "path";
|
|
1263
|
+
|
|
1264
|
+
// src/infra/path-utils.ts
|
|
1265
|
+
import { posix, sep, relative, resolve, isAbsolute, join as join4 } from "path";
|
|
1266
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1267
|
+
import { glob, globSync } from "glob";
|
|
1268
|
+
function toPosixPath(p) {
|
|
1269
|
+
return p.split(/[/\\]/).join(posix.sep);
|
|
1270
|
+
}
|
|
1271
|
+
function resolveQoderDir(projectRoot2) {
|
|
1272
|
+
if (existsSync5(join4(projectRoot2, ".qoder-cn"))) return ".qoder-cn";
|
|
1273
|
+
return ".qoder";
|
|
1274
|
+
}
|
|
1275
|
+
function hasQoderDir(projectRoot2) {
|
|
1276
|
+
return existsSync5(join4(projectRoot2, ".qoder")) || existsSync5(join4(projectRoot2, ".qoder-cn"));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/plugins/builtin/qoder-writer.ts
|
|
1263
1280
|
var QoderWriter = class {
|
|
1264
1281
|
toolName = "qoder";
|
|
1265
1282
|
configFile = ".qoder/rules/speclore.md";
|
|
1266
1283
|
projectRoot = "";
|
|
1267
1284
|
detect(projectRoot2) {
|
|
1268
|
-
return
|
|
1285
|
+
return existsSync6(join5(projectRoot2, ".qoder")) || existsSync6(join5(projectRoot2, ".qoder-cn"));
|
|
1269
1286
|
}
|
|
1270
1287
|
write(constraints) {
|
|
1271
1288
|
this.projectRoot = constraints.projectRoot;
|
|
1272
|
-
const
|
|
1289
|
+
const qoderDir = resolveQoderDir(constraints.projectRoot);
|
|
1290
|
+
const rulesDir = join5(constraints.projectRoot, qoderDir, "rules");
|
|
1273
1291
|
mkdirSync3(rulesDir, { recursive: true });
|
|
1274
1292
|
const content = this.buildMarkdown(constraints);
|
|
1275
|
-
writeFileSync4(
|
|
1293
|
+
writeFileSync4(join5(rulesDir, "speclore.md"), content, "utf-8");
|
|
1276
1294
|
return Promise.resolve();
|
|
1277
1295
|
}
|
|
1278
1296
|
remove() {
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1297
|
+
const qoderDir = resolveQoderDir(this.projectRoot);
|
|
1298
|
+
const filePath = join5(this.projectRoot, qoderDir, "rules", "speclore.md");
|
|
1299
|
+
if (existsSync6(filePath)) rmSync3(filePath);
|
|
1281
1300
|
return Promise.resolve();
|
|
1282
1301
|
}
|
|
1283
1302
|
buildMarkdown(c) {
|
|
@@ -1806,9 +1825,30 @@ async function readImageFile(filePath, providerOverride) {
|
|
|
1806
1825
|
};
|
|
1807
1826
|
}
|
|
1808
1827
|
|
|
1828
|
+
// src/core/requirement-reader/text-reader.ts
|
|
1829
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1830
|
+
import { basename as basename6, extname as extname6 } from "path";
|
|
1831
|
+
function readTextFile(filePath) {
|
|
1832
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
1833
|
+
const id = deriveId2(filePath);
|
|
1834
|
+
const lines = content.split("\n").filter((line) => line.trim().length > 0);
|
|
1835
|
+
const title = lines[0]?.slice(0, 100) ?? basename6(filePath, extname6(filePath));
|
|
1836
|
+
return Promise.resolve({
|
|
1837
|
+
id,
|
|
1838
|
+
title,
|
|
1839
|
+
description: content,
|
|
1840
|
+
rawContent: content,
|
|
1841
|
+
confidence: 1
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
function deriveId2(filePath) {
|
|
1845
|
+
const name = basename6(filePath, extname6(filePath));
|
|
1846
|
+
return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1809
1849
|
// src/core/requirement-reader/url-reader.ts
|
|
1810
1850
|
init_logger();
|
|
1811
|
-
import { basename as
|
|
1851
|
+
import { basename as basename7 } from "path";
|
|
1812
1852
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
1813
1853
|
var MAX_CONTENT_LENGTH = 1e5;
|
|
1814
1854
|
async function readUrl(url) {
|
|
@@ -1848,7 +1888,7 @@ async function readUrl(url) {
|
|
|
1848
1888
|
content = content.slice(0, MAX_CONTENT_LENGTH) + "\n... (truncated)";
|
|
1849
1889
|
}
|
|
1850
1890
|
const urlPath = new URL(url).pathname;
|
|
1851
|
-
const id =
|
|
1891
|
+
const id = basename7(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
|
|
1852
1892
|
return {
|
|
1853
1893
|
id,
|
|
1854
1894
|
title,
|
|
@@ -1896,30 +1936,43 @@ var UrlAuthError = class extends Error {
|
|
|
1896
1936
|
};
|
|
1897
1937
|
|
|
1898
1938
|
// src/core/requirement-reader/index.ts
|
|
1939
|
+
function sanitizeSource(source) {
|
|
1940
|
+
return source.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g, "");
|
|
1941
|
+
}
|
|
1899
1942
|
async function readRequirement(source) {
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1943
|
+
const cleaned = sanitizeSource(source);
|
|
1944
|
+
if (cleaned !== source) {
|
|
1945
|
+
logger.debug("Stripped invisible Unicode characters from source input.");
|
|
1946
|
+
}
|
|
1947
|
+
const sourceType = classifySource(cleaned);
|
|
1948
|
+
logger.info(`Reading requirement from ${sourceType}: ${truncate(cleaned, 120)}`);
|
|
1902
1949
|
switch (sourceType) {
|
|
1903
1950
|
case "file":
|
|
1904
|
-
return readFromFile(
|
|
1951
|
+
return readFromFile(cleaned);
|
|
1905
1952
|
case "url":
|
|
1906
|
-
return readFromUrl(
|
|
1953
|
+
return readFromUrl(cleaned);
|
|
1907
1954
|
case "text":
|
|
1908
|
-
return readFromText(
|
|
1955
|
+
return readFromText(cleaned);
|
|
1909
1956
|
}
|
|
1910
1957
|
}
|
|
1911
1958
|
function classifySource(source) {
|
|
1912
1959
|
if (/^https?:\/\//i.test(source)) {
|
|
1913
1960
|
return "url";
|
|
1914
1961
|
}
|
|
1915
|
-
const ext =
|
|
1916
|
-
const supportedExts = [".md", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
|
|
1917
|
-
if (supportedExts.includes(ext) &&
|
|
1962
|
+
const ext = extname7(source).toLowerCase();
|
|
1963
|
+
const supportedExts = [".md", ".txt", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
|
|
1964
|
+
if (supportedExts.includes(ext) && existsSync7(source)) {
|
|
1918
1965
|
return "file";
|
|
1919
1966
|
}
|
|
1920
|
-
if (
|
|
1967
|
+
if (existsSync7(source) && (source.includes("/") || source.includes("\\"))) {
|
|
1921
1968
|
return "file";
|
|
1922
1969
|
}
|
|
1970
|
+
if (supportedExts.includes(ext) && (source.includes("/") || source.includes("\\"))) {
|
|
1971
|
+
throw new Error(
|
|
1972
|
+
`File not found: ${source}
|
|
1973
|
+
The path looks like a file but does not exist. Check the path and try again. If copying from Windows, make sure no invisible characters are included.`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1923
1976
|
return "text";
|
|
1924
1977
|
}
|
|
1925
1978
|
async function readFromFile(filePath) {
|
|
@@ -1933,10 +1986,12 @@ async function readFromFile(filePath) {
|
|
|
1933
1986
|
}
|
|
1934
1987
|
return results[0];
|
|
1935
1988
|
}
|
|
1936
|
-
const ext =
|
|
1989
|
+
const ext = extname7(filePath).toLowerCase();
|
|
1937
1990
|
switch (ext) {
|
|
1938
1991
|
case ".md":
|
|
1939
1992
|
return readMarkdownFile(filePath);
|
|
1993
|
+
case ".txt":
|
|
1994
|
+
return readTextFile(filePath);
|
|
1940
1995
|
case ".docx":
|
|
1941
1996
|
return readDocxFile(filePath);
|
|
1942
1997
|
case ".xlsx":
|
|
@@ -1972,7 +2027,7 @@ function truncate(str, maxLen) {
|
|
|
1972
2027
|
|
|
1973
2028
|
// src/core/feature-generator/generator.ts
|
|
1974
2029
|
init_provider();
|
|
1975
|
-
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as
|
|
2030
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8 } from "fs";
|
|
1976
2031
|
import { join as join6, dirname } from "path";
|
|
1977
2032
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
1978
2033
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
@@ -2184,15 +2239,6 @@ function buildPrompt(requirement, context, config) {
|
|
|
2184
2239
|
|
|
2185
2240
|
// src/core/feature-generator/generator.ts
|
|
2186
2241
|
init_logger();
|
|
2187
|
-
|
|
2188
|
-
// src/infra/path-utils.ts
|
|
2189
|
-
import { posix, sep, relative, resolve, isAbsolute, join as join5 } from "path";
|
|
2190
|
-
import { glob, globSync } from "glob";
|
|
2191
|
-
function toPosixPath(p) {
|
|
2192
|
-
return p.split(/[/\\]/).join(posix.sep);
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
// src/core/feature-generator/generator.ts
|
|
2196
2242
|
var MAX_VALIDATION_RETRIES = 2;
|
|
2197
2243
|
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
2198
2244
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
@@ -2258,7 +2304,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
2258
2304
|
const outputDir = join6(projectRoot2, config.spec.outputDir);
|
|
2259
2305
|
const moduleDir = inferModule(requirement, context);
|
|
2260
2306
|
const filePath = join6(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
2261
|
-
if (!
|
|
2307
|
+
if (!existsSync8(dirname(filePath))) {
|
|
2262
2308
|
mkdirSync4(dirname(filePath), { recursive: true });
|
|
2263
2309
|
}
|
|
2264
2310
|
writeFileSync5(filePath, featureContent, "utf-8");
|
|
@@ -2389,21 +2435,21 @@ function inferModule(requirement, context) {
|
|
|
2389
2435
|
|
|
2390
2436
|
// src/core/constraint-coder/ai-tool-detector.ts
|
|
2391
2437
|
init_logger();
|
|
2392
|
-
import { existsSync as
|
|
2438
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2393
2439
|
import { join as join7 } from "path";
|
|
2394
2440
|
function detectAITools(projectRoot2) {
|
|
2395
2441
|
const tools = [];
|
|
2396
|
-
if (
|
|
2442
|
+
if (existsSync9(join7(projectRoot2, ".cursor"))) {
|
|
2397
2443
|
tools.push("cursor");
|
|
2398
2444
|
logger.debug("Detected Cursor (.cursor/)");
|
|
2399
2445
|
}
|
|
2400
|
-
if (
|
|
2446
|
+
if (existsSync9(join7(projectRoot2, ".claude")) || existsSync9(join7(projectRoot2, "CLAUDE.md"))) {
|
|
2401
2447
|
tools.push("claude");
|
|
2402
2448
|
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
2403
2449
|
}
|
|
2404
|
-
if (
|
|
2450
|
+
if (hasQoderDir(projectRoot2)) {
|
|
2405
2451
|
tools.push("qoder");
|
|
2406
|
-
logger.debug("Detected Qoder (.qoder/)");
|
|
2452
|
+
logger.debug("Detected Qoder (.qoder/ or .qoder-cn/)");
|
|
2407
2453
|
}
|
|
2408
2454
|
logger.debug(`Detected AI tools: ${tools.length > 0 ? tools.join(", ") : "none"}`);
|
|
2409
2455
|
return tools;
|
|
@@ -2411,7 +2457,7 @@ function detectAITools(projectRoot2) {
|
|
|
2411
2457
|
|
|
2412
2458
|
// src/core/constraint-coder/constraint-writer.ts
|
|
2413
2459
|
init_logger();
|
|
2414
|
-
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as
|
|
2460
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
|
|
2415
2461
|
import { join as join8 } from "path";
|
|
2416
2462
|
function writeConstraints(projectRoot2, tools, content) {
|
|
2417
2463
|
const writtenFiles = [];
|
|
@@ -2485,7 +2531,7 @@ function buildConstraintText(content) {
|
|
|
2485
2531
|
}
|
|
2486
2532
|
function writeCursorRule(projectRoot2, text) {
|
|
2487
2533
|
const dir = join8(projectRoot2, ".cursor", "rules");
|
|
2488
|
-
if (!
|
|
2534
|
+
if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
|
|
2489
2535
|
const filePath = join8(dir, "speclore.mdc");
|
|
2490
2536
|
const frontmatter = [
|
|
2491
2537
|
"---",
|
|
@@ -2500,14 +2546,15 @@ function writeCursorRule(projectRoot2, text) {
|
|
|
2500
2546
|
}
|
|
2501
2547
|
function writeClaudeRule(projectRoot2, text) {
|
|
2502
2548
|
const dir = join8(projectRoot2, ".claude", "rules");
|
|
2503
|
-
if (!
|
|
2549
|
+
if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
|
|
2504
2550
|
const filePath = join8(dir, "speclore.md");
|
|
2505
2551
|
writeFileSync6(filePath, text, "utf-8");
|
|
2506
2552
|
return filePath;
|
|
2507
2553
|
}
|
|
2508
2554
|
function writeQoderRule(projectRoot2, text) {
|
|
2509
|
-
const
|
|
2510
|
-
|
|
2555
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
2556
|
+
const dir = join8(projectRoot2, qoderDir, "rules");
|
|
2557
|
+
if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
|
|
2511
2558
|
const filePath = join8(dir, "speclore.md");
|
|
2512
2559
|
writeFileSync6(filePath, text, "utf-8");
|
|
2513
2560
|
return filePath;
|
|
@@ -2518,7 +2565,7 @@ init_logger();
|
|
|
2518
2565
|
function readModuleConfig(config) {
|
|
2519
2566
|
const boundaries = [];
|
|
2520
2567
|
for (const [name, mod] of Object.entries(config.project.modules)) {
|
|
2521
|
-
logger.debug(`Reading module config: ${name}
|
|
2568
|
+
logger.debug(`Reading module config: ${name} -> ${mod.path}`);
|
|
2522
2569
|
boundaries.push({
|
|
2523
2570
|
name,
|
|
2524
2571
|
responsibility: mod.responsibility,
|
|
@@ -2579,7 +2626,7 @@ async function generateConstraints(projectRoot2, features, _context, config) {
|
|
|
2579
2626
|
try {
|
|
2580
2627
|
await writer.write(content);
|
|
2581
2628
|
writtenFiles.push(writer.configFile);
|
|
2582
|
-
logger.info(`Constraint written via plugin: ${writer.toolName}
|
|
2629
|
+
logger.info(`Constraint written via plugin: ${writer.toolName} -> ${writer.configFile}`);
|
|
2583
2630
|
} catch (err) {
|
|
2584
2631
|
logger.warn(`Writer plugin '${tool}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2585
2632
|
}
|
|
@@ -2596,7 +2643,7 @@ import { execFileSync } from "child_process";
|
|
|
2596
2643
|
|
|
2597
2644
|
// src/core/verifier/mapping-resolver.ts
|
|
2598
2645
|
init_logger();
|
|
2599
|
-
import { readFileSync as
|
|
2646
|
+
import { readFileSync as readFileSync7, existsSync as existsSync11, readdirSync } from "fs";
|
|
2600
2647
|
import { join as join9 } from "path";
|
|
2601
2648
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
2602
2649
|
const results = [];
|
|
@@ -2621,17 +2668,17 @@ function resolveMapping(projectRoot2, feature, scenario, _testOutput) {
|
|
|
2621
2668
|
}
|
|
2622
2669
|
function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
2623
2670
|
const mappingsDir = join9(projectRoot2, ".speclore", "mappings");
|
|
2624
|
-
if (!
|
|
2671
|
+
if (!existsSync11(mappingsDir)) return null;
|
|
2625
2672
|
try {
|
|
2626
2673
|
const files = findMappingFiles(mappingsDir);
|
|
2627
2674
|
for (const file of files) {
|
|
2628
2675
|
try {
|
|
2629
|
-
const content =
|
|
2676
|
+
const content = readFileSync7(file, "utf-8");
|
|
2630
2677
|
const mapping = JSON.parse(content);
|
|
2631
2678
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
2632
2679
|
const entry = mapping.scenarios[scenario.name];
|
|
2633
2680
|
if (entry) {
|
|
2634
|
-
logger.debug(`Mapping file hit: ${scenario.name}
|
|
2681
|
+
logger.debug(`Mapping file hit: ${scenario.name} -> ${entry.testMethod}`);
|
|
2635
2682
|
return {
|
|
2636
2683
|
name: scenario.name,
|
|
2637
2684
|
status: "passed",
|
|
@@ -2651,18 +2698,18 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
2651
2698
|
}
|
|
2652
2699
|
function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
2653
2700
|
const testsDir = join9(projectRoot2, "tests");
|
|
2654
|
-
if (!
|
|
2701
|
+
if (!existsSync11(testsDir)) return null;
|
|
2655
2702
|
try {
|
|
2656
2703
|
const files = findTestFiles(testsDir);
|
|
2657
2704
|
for (const file of files) {
|
|
2658
2705
|
try {
|
|
2659
|
-
const content =
|
|
2706
|
+
const content = readFileSync7(file, "utf-8");
|
|
2660
2707
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
2661
2708
|
let match;
|
|
2662
2709
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
2663
2710
|
if (match[1].trim() === scenario.name) {
|
|
2664
2711
|
const methodMatch = findTestMethodNearby(content, match.index);
|
|
2665
|
-
logger.debug(`Tag hit: ${scenario.name}
|
|
2712
|
+
logger.debug(`Tag hit: ${scenario.name} -> ${methodMatch}`);
|
|
2666
2713
|
return {
|
|
2667
2714
|
name: scenario.name,
|
|
2668
2715
|
status: "passed",
|
|
@@ -2819,20 +2866,20 @@ function executeTestCommand(projectRoot2, config) {
|
|
|
2819
2866
|
|
|
2820
2867
|
// src/core/verifier/report-generator.ts
|
|
2821
2868
|
init_logger();
|
|
2822
|
-
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as
|
|
2869
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
2823
2870
|
import { join as join10, dirname as dirname2 } from "path";
|
|
2824
2871
|
import { fileURLToPath } from "url";
|
|
2825
2872
|
|
|
2826
2873
|
// src/core/context-engine/context-writer.ts
|
|
2827
2874
|
init_logger();
|
|
2828
|
-
import { readFileSync as
|
|
2875
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync14, statSync as statSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
2829
2876
|
import { join as join13 } from "path";
|
|
2830
2877
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2831
2878
|
|
|
2832
2879
|
// src/core/context-engine/graph-builder.ts
|
|
2833
2880
|
init_logger();
|
|
2834
|
-
import { existsSync as
|
|
2835
|
-
import { join as join11, basename as
|
|
2881
|
+
import { existsSync as existsSync13, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync9 } from "fs";
|
|
2882
|
+
import { join as join11, basename as basename8, extname as extname8, relative as relative2 } from "path";
|
|
2836
2883
|
function detectProjectInfo(projectRoot2) {
|
|
2837
2884
|
const info = {
|
|
2838
2885
|
language: "unknown",
|
|
@@ -2841,11 +2888,11 @@ function detectProjectInfo(projectRoot2) {
|
|
|
2841
2888
|
testFramework: "unknown",
|
|
2842
2889
|
directoryStructure: ""
|
|
2843
2890
|
};
|
|
2844
|
-
if (
|
|
2891
|
+
if (existsSync13(join11(projectRoot2, "package.json"))) {
|
|
2845
2892
|
info.language = "typescript";
|
|
2846
2893
|
info.buildTool = "npm";
|
|
2847
2894
|
try {
|
|
2848
|
-
const pkg = JSON.parse(
|
|
2895
|
+
const pkg = JSON.parse(readFileSync9(join11(projectRoot2, "package.json"), "utf-8"));
|
|
2849
2896
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2850
2897
|
if ("next" in deps) info.framework = "next.js";
|
|
2851
2898
|
else if ("@nestjs/core" in deps) info.framework = "nestjs";
|
|
@@ -2857,33 +2904,33 @@ function detectProjectInfo(projectRoot2) {
|
|
|
2857
2904
|
} catch {
|
|
2858
2905
|
logger.debug("Failed to parse package.json, using default project info");
|
|
2859
2906
|
}
|
|
2860
|
-
} else if (
|
|
2907
|
+
} else if (existsSync13(join11(projectRoot2, "pom.xml"))) {
|
|
2861
2908
|
info.language = "java";
|
|
2862
2909
|
info.buildTool = "maven";
|
|
2863
2910
|
info.testFramework = "junit";
|
|
2864
2911
|
try {
|
|
2865
|
-
const pom =
|
|
2912
|
+
const pom = readFileSync9(join11(projectRoot2, "pom.xml"), "utf-8");
|
|
2866
2913
|
if (pom.includes("spring-boot")) info.framework = "spring-boot";
|
|
2867
2914
|
} catch {
|
|
2868
2915
|
}
|
|
2869
|
-
} else if (
|
|
2916
|
+
} else if (existsSync13(join11(projectRoot2, "build.gradle")) || existsSync13(join11(projectRoot2, "build.gradle.kts"))) {
|
|
2870
2917
|
info.language = "java";
|
|
2871
2918
|
info.buildTool = "gradle";
|
|
2872
2919
|
info.testFramework = "junit";
|
|
2873
|
-
} else if (
|
|
2920
|
+
} else if (existsSync13(join11(projectRoot2, "go.mod"))) {
|
|
2874
2921
|
info.language = "go";
|
|
2875
2922
|
info.buildTool = "go";
|
|
2876
2923
|
info.testFramework = "go-test";
|
|
2877
|
-
} else if (
|
|
2924
|
+
} else if (existsSync13(join11(projectRoot2, "Cargo.toml"))) {
|
|
2878
2925
|
info.language = "rust";
|
|
2879
2926
|
info.buildTool = "cargo";
|
|
2880
2927
|
info.testFramework = "cargo-test";
|
|
2881
|
-
} else if (
|
|
2928
|
+
} else if (existsSync13(join11(projectRoot2, "requirements.txt")) || existsSync13(join11(projectRoot2, "pyproject.toml"))) {
|
|
2882
2929
|
info.language = "python";
|
|
2883
2930
|
info.buildTool = "pip";
|
|
2884
2931
|
info.testFramework = "pytest";
|
|
2885
2932
|
try {
|
|
2886
|
-
const reqs =
|
|
2933
|
+
const reqs = existsSync13(join11(projectRoot2, "requirements.txt")) ? readFileSync9(join11(projectRoot2, "requirements.txt"), "utf-8") : readFileSync9(join11(projectRoot2, "pyproject.toml"), "utf-8");
|
|
2887
2934
|
if (reqs.includes("django")) info.framework = "django";
|
|
2888
2935
|
else if (reqs.includes("flask")) info.framework = "flask";
|
|
2889
2936
|
else if (reqs.includes("fastapi")) info.framework = "fastapi";
|
|
@@ -2931,7 +2978,7 @@ function buildDependencyGraph(projectRoot2, modules) {
|
|
|
2931
2978
|
}
|
|
2932
2979
|
for (const mod of modules) {
|
|
2933
2980
|
const modPath = join11(projectRoot2, mod.path);
|
|
2934
|
-
if (!
|
|
2981
|
+
if (!existsSync13(modPath)) continue;
|
|
2935
2982
|
try {
|
|
2936
2983
|
const imports = scanImports(modPath);
|
|
2937
2984
|
for (const otherMod of modules) {
|
|
@@ -2967,10 +3014,10 @@ function scanImports(dir) {
|
|
|
2967
3014
|
walk(fullPath);
|
|
2968
3015
|
continue;
|
|
2969
3016
|
}
|
|
2970
|
-
const ext =
|
|
3017
|
+
const ext = extname8(entry);
|
|
2971
3018
|
if (![".ts", ".tsx", ".js", ".jsx", ".java", ".py"].includes(ext)) continue;
|
|
2972
3019
|
try {
|
|
2973
|
-
const content =
|
|
3020
|
+
const content = readFileSync9(fullPath, "utf-8");
|
|
2974
3021
|
const importRegex = /(?:import\s+.*?from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
2975
3022
|
let match;
|
|
2976
3023
|
while ((match = importRegex.exec(content)) !== null) {
|
|
@@ -3003,11 +3050,11 @@ function readDirRecursive(dir) {
|
|
|
3003
3050
|
var ANALYZABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".java", ".py"]);
|
|
3004
3051
|
function analyzeFileForEntities(filePath, modName, modPath, modRelativePath) {
|
|
3005
3052
|
const entities = [];
|
|
3006
|
-
const ext =
|
|
3007
|
-
const name =
|
|
3053
|
+
const ext = extname8(filePath);
|
|
3054
|
+
const name = basename8(filePath, ext);
|
|
3008
3055
|
if (!ANALYZABLE_EXTENSIONS.has(ext)) return entities;
|
|
3009
3056
|
try {
|
|
3010
|
-
const content =
|
|
3057
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
3011
3058
|
const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
|
|
3012
3059
|
if (/(?:Entity|Model|Domain|Schema)$/i.test(name)) {
|
|
3013
3060
|
entities.push({ name, module: modName, file: relFile });
|
|
@@ -3056,11 +3103,11 @@ function analyzeFileForEntities(filePath, modName, modPath, modRelativePath) {
|
|
|
3056
3103
|
}
|
|
3057
3104
|
function analyzeFileForApis(filePath, modName, modPath, modRelativePath) {
|
|
3058
3105
|
const apis = [];
|
|
3059
|
-
const ext =
|
|
3060
|
-
const name =
|
|
3106
|
+
const ext = extname8(filePath);
|
|
3107
|
+
const name = basename8(filePath, ext);
|
|
3061
3108
|
if (!ANALYZABLE_EXTENSIONS.has(ext)) return apis;
|
|
3062
3109
|
try {
|
|
3063
|
-
const content =
|
|
3110
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
3064
3111
|
const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
|
|
3065
3112
|
if (/Controller|Resource|Handler|Route$/i.test(name)) {
|
|
3066
3113
|
apis.push({
|
|
@@ -3134,7 +3181,7 @@ function extractEntities(projectRoot2, modules) {
|
|
|
3134
3181
|
const seen = /* @__PURE__ */ new Set();
|
|
3135
3182
|
for (const mod of modules) {
|
|
3136
3183
|
const modPath = join11(projectRoot2, mod.path);
|
|
3137
|
-
if (!
|
|
3184
|
+
if (!existsSync13(modPath)) continue;
|
|
3138
3185
|
try {
|
|
3139
3186
|
const files = readDirRecursive(modPath);
|
|
3140
3187
|
for (const file of files) {
|
|
@@ -3157,7 +3204,7 @@ function extractApis(projectRoot2, modules) {
|
|
|
3157
3204
|
const seen = /* @__PURE__ */ new Set();
|
|
3158
3205
|
for (const mod of modules) {
|
|
3159
3206
|
const modPath = join11(projectRoot2, mod.path);
|
|
3160
|
-
if (!
|
|
3207
|
+
if (!existsSync13(modPath)) continue;
|
|
3161
3208
|
try {
|
|
3162
3209
|
const files = readDirRecursive(modPath);
|
|
3163
3210
|
for (const file of files) {
|
|
@@ -3177,7 +3224,7 @@ function extractApis(projectRoot2, modules) {
|
|
|
3177
3224
|
}
|
|
3178
3225
|
|
|
3179
3226
|
// src/version.ts
|
|
3180
|
-
import { readFileSync as
|
|
3227
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3181
3228
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3182
3229
|
import { join as join12, dirname as dirname3 } from "path";
|
|
3183
3230
|
function readVersion() {
|
|
@@ -3185,7 +3232,7 @@ function readVersion() {
|
|
|
3185
3232
|
for (let depth = 0; depth < 4; depth++) {
|
|
3186
3233
|
try {
|
|
3187
3234
|
const pkgPath = join12(currentDir, "package.json");
|
|
3188
|
-
const pkg = JSON.parse(
|
|
3235
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
3189
3236
|
if (typeof pkg.version === "string") return pkg.version;
|
|
3190
3237
|
} catch {
|
|
3191
3238
|
}
|
|
@@ -3241,7 +3288,7 @@ function buildContext(projectRoot2, config) {
|
|
|
3241
3288
|
}
|
|
3242
3289
|
function loadContext(specLoreDir) {
|
|
3243
3290
|
const contextPath = join13(specLoreDir, CONTEXT_FILENAME);
|
|
3244
|
-
if (!
|
|
3291
|
+
if (!existsSync14(contextPath)) {
|
|
3245
3292
|
logger.debug("No cached context.json found");
|
|
3246
3293
|
return null;
|
|
3247
3294
|
}
|
|
@@ -3256,7 +3303,7 @@ function loadContext(specLoreDir) {
|
|
|
3256
3303
|
return null;
|
|
3257
3304
|
}
|
|
3258
3305
|
try {
|
|
3259
|
-
const content =
|
|
3306
|
+
const content = readFileSync11(contextPath, "utf-8");
|
|
3260
3307
|
const context = JSON.parse(content);
|
|
3261
3308
|
logger.debug("Loaded cached context.json");
|
|
3262
3309
|
return context;
|
|
@@ -3274,8 +3321,8 @@ function hasGitHeadChanged(specLoreDir) {
|
|
|
3274
3321
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3275
3322
|
}).trim();
|
|
3276
3323
|
const headFile = join13(specLoreDir, ".git-head");
|
|
3277
|
-
if (!
|
|
3278
|
-
const savedHead =
|
|
3324
|
+
if (!existsSync14(headFile)) return true;
|
|
3325
|
+
const savedHead = readFileSync11(headFile, "utf-8").trim();
|
|
3279
3326
|
if (savedHead !== head) {
|
|
3280
3327
|
writeFileSync8(headFile, head, "utf-8");
|
|
3281
3328
|
return true;
|
|
@@ -3294,7 +3341,7 @@ function truncateTo(text, maxLines) {
|
|
|
3294
3341
|
|
|
3295
3342
|
// src/core/analyzer/rdg-builder.ts
|
|
3296
3343
|
init_logger();
|
|
3297
|
-
import { readFileSync as
|
|
3344
|
+
import { readFileSync as readFileSync12, existsSync as existsSync15, readdirSync as readdirSync3 } from "fs";
|
|
3298
3345
|
import { join as join14 } from "path";
|
|
3299
3346
|
|
|
3300
3347
|
// src/core/analyzer/cdg-builder.ts
|
|
@@ -3369,7 +3416,7 @@ function determineAffectedFeatures(affectedModules, projectRoot2, outputDir) {
|
|
|
3369
3416
|
}
|
|
3370
3417
|
|
|
3371
3418
|
// src/core/state-manager/index.ts
|
|
3372
|
-
import { readFileSync as
|
|
3419
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync16, mkdirSync as mkdirSync8 } from "fs";
|
|
3373
3420
|
import { join as join15 } from "path";
|
|
3374
3421
|
import yaml from "js-yaml";
|
|
3375
3422
|
import { globSync as globSync3 } from "glob";
|
|
@@ -3397,11 +3444,11 @@ var StateManager = class {
|
|
|
3397
3444
|
}
|
|
3398
3445
|
/** Load state from disk, or return default if not exists */
|
|
3399
3446
|
load() {
|
|
3400
|
-
if (!
|
|
3447
|
+
if (!existsSync16(this.statePath)) {
|
|
3401
3448
|
return createDefaultState();
|
|
3402
3449
|
}
|
|
3403
3450
|
try {
|
|
3404
|
-
const content =
|
|
3451
|
+
const content = readFileSync13(this.statePath, "utf-8");
|
|
3405
3452
|
const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
3406
3453
|
if (parsed && typeof parsed === "object") {
|
|
3407
3454
|
return parsed;
|
|
@@ -3413,14 +3460,14 @@ var StateManager = class {
|
|
|
3413
3460
|
/** Persist state to disk */
|
|
3414
3461
|
save(state) {
|
|
3415
3462
|
const dir = join15(this.projectRoot, ".speclore");
|
|
3416
|
-
if (!
|
|
3463
|
+
if (!existsSync16(dir)) {
|
|
3417
3464
|
mkdirSync8(dir, { recursive: true });
|
|
3418
3465
|
}
|
|
3419
3466
|
writeFileSync9(this.statePath, yaml.dump(state, { lineWidth: 120 }), "utf-8");
|
|
3420
3467
|
}
|
|
3421
3468
|
/** Ensure state file exists with default values */
|
|
3422
3469
|
ensureInitialized() {
|
|
3423
|
-
if (!
|
|
3470
|
+
if (!existsSync16(this.statePath)) {
|
|
3424
3471
|
const state2 = createDefaultState();
|
|
3425
3472
|
this.save(state2);
|
|
3426
3473
|
return state2;
|
|
@@ -3563,19 +3610,19 @@ var StateManager = class {
|
|
|
3563
3610
|
};
|
|
3564
3611
|
|
|
3565
3612
|
// src/core/test-scaffolder/index.ts
|
|
3566
|
-
import { readFileSync as
|
|
3567
|
-
import { join as join17, relative as relative3, dirname as dirname4, basename as
|
|
3613
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
|
|
3614
|
+
import { join as join17, relative as relative3, dirname as dirname4, basename as basename9 } from "path";
|
|
3568
3615
|
|
|
3569
3616
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3570
|
-
import { readFileSync as
|
|
3617
|
+
import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
|
|
3571
3618
|
import { join as join16 } from "path";
|
|
3572
3619
|
function detectTestFramework(projectRoot2) {
|
|
3573
3620
|
const pkgPath = join16(projectRoot2, "package.json");
|
|
3574
|
-
if (!
|
|
3621
|
+
if (!existsSync17(pkgPath)) {
|
|
3575
3622
|
return "vitest";
|
|
3576
3623
|
}
|
|
3577
3624
|
try {
|
|
3578
|
-
const pkg = JSON.parse(
|
|
3625
|
+
const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3579
3626
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3580
3627
|
if ("vitest" in deps) return "vitest";
|
|
3581
3628
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3598,7 +3645,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3598
3645
|
}
|
|
3599
3646
|
const absTestPath = join17(projectRoot2, testFilePath);
|
|
3600
3647
|
const scenarios = feature.scenarios;
|
|
3601
|
-
if (
|
|
3648
|
+
if (existsSync18(absTestPath)) {
|
|
3602
3649
|
const appended = appendMissingScenarios(absTestPath, scenarios, framework);
|
|
3603
3650
|
if (appended > 0) {
|
|
3604
3651
|
results.push({ testFile: testFilePath, framework, scenarios: appended });
|
|
@@ -3607,7 +3654,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3607
3654
|
} else {
|
|
3608
3655
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3609
3656
|
const dir = dirname4(absTestPath);
|
|
3610
|
-
if (!
|
|
3657
|
+
if (!existsSync18(dir)) {
|
|
3611
3658
|
mkdirSync9(dir, { recursive: true });
|
|
3612
3659
|
}
|
|
3613
3660
|
writeFileSync10(absTestPath, content, "utf-8");
|
|
@@ -3626,7 +3673,7 @@ function resolveTestFilePath(projectRoot2, featurePath, config) {
|
|
|
3626
3673
|
return resolveTestFromPattern(match, pattern);
|
|
3627
3674
|
}
|
|
3628
3675
|
}
|
|
3629
|
-
const featureBase =
|
|
3676
|
+
const featureBase = basename9(featurePath, ".feature");
|
|
3630
3677
|
const featureDir = dirname4(relative3(projectRoot2, featurePath));
|
|
3631
3678
|
return join17(featureDir, `${featureBase}.test.ts`).replace(/\\/g, "/");
|
|
3632
3679
|
}
|
|
@@ -3681,7 +3728,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3681
3728
|
return lines.join("\n");
|
|
3682
3729
|
}
|
|
3683
3730
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3684
|
-
const content =
|
|
3731
|
+
const content = readFileSync15(testFilePath, "utf-8");
|
|
3685
3732
|
const missing = [];
|
|
3686
3733
|
for (const scenario of scenarios) {
|
|
3687
3734
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3715,7 +3762,7 @@ function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
|
3715
3762
|
}
|
|
3716
3763
|
|
|
3717
3764
|
// src/infra/config.ts
|
|
3718
|
-
import { readFileSync as
|
|
3765
|
+
import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
|
|
3719
3766
|
import { join as join18 } from "path";
|
|
3720
3767
|
import { homedir } from "os";
|
|
3721
3768
|
import yaml2 from "js-yaml";
|
|
@@ -3774,11 +3821,11 @@ function loadProjectConfig(projectRoot2) {
|
|
|
3774
3821
|
return readYamlIfExists(projectPath);
|
|
3775
3822
|
}
|
|
3776
3823
|
function readYamlIfExists(filePath) {
|
|
3777
|
-
if (!
|
|
3824
|
+
if (!existsSync19(filePath)) {
|
|
3778
3825
|
return null;
|
|
3779
3826
|
}
|
|
3780
3827
|
try {
|
|
3781
|
-
const content =
|
|
3828
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
3782
3829
|
const parsed = yaml2.load(content, { schema: yaml2.JSON_SCHEMA });
|
|
3783
3830
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3784
3831
|
return parsed;
|
|
@@ -3902,7 +3949,7 @@ verify:
|
|
|
3902
3949
|
// src/mcp/tools.ts
|
|
3903
3950
|
init_logger();
|
|
3904
3951
|
import { join as join19 } from "path";
|
|
3905
|
-
import { readFileSync as
|
|
3952
|
+
import { readFileSync as readFileSync17, existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
3906
3953
|
import { globSync as globSync4 } from "glob";
|
|
3907
3954
|
import { toJSONSchema } from "zod";
|
|
3908
3955
|
|
|
@@ -3925,10 +3972,23 @@ var statusInputSchema = z.object({
|
|
|
3925
3972
|
});
|
|
3926
3973
|
|
|
3927
3974
|
// src/mcp/tools.ts
|
|
3928
|
-
var SPEC_TOOL_DESC =
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3975
|
+
var SPEC_TOOL_DESC = [
|
|
3976
|
+
"Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.",
|
|
3977
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) the created file path, (2) the list of generated scenarios, (3) the recommended next step."
|
|
3978
|
+
].join(" ");
|
|
3979
|
+
var CODE_TOOL_DESC = [
|
|
3980
|
+
"Generate AI coding constraint files for Cursor / Claude Code / Qoder based on .feature specs and project context. Requires .feature files to exist.",
|
|
3981
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) which constraint files were written, (2) test scaffold files generated, (3) the recommended next step."
|
|
3982
|
+
].join(" ");
|
|
3983
|
+
var VERIFY_TOOL_DESC = [
|
|
3984
|
+
"Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.",
|
|
3985
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) pass/fail summary, (2) any failed scenario details, (3) the recommended next step."
|
|
3986
|
+
].join(" ");
|
|
3987
|
+
var STATUS_TOOL_DESC = [
|
|
3988
|
+
"Get current project workflow status, feature states, and recommended next steps.",
|
|
3989
|
+
"BEST PRACTICE: Call this tool FIRST when starting a new conversation about the project to understand where it stands.",
|
|
3990
|
+
"ALWAYS show the user the full status including: project state, feature list, and recommended actions."
|
|
3991
|
+
].join(" ");
|
|
3932
3992
|
var specJsonSchema = toJSONSchema(specInputSchema, { target: "draft-07" });
|
|
3933
3993
|
var codeJsonSchema = toJSONSchema(codeInputSchema, { target: "draft-07" });
|
|
3934
3994
|
var verifyJsonSchema = toJSONSchema(verifyInputSchema, { target: "draft-07" });
|
|
@@ -3936,10 +3996,11 @@ async function executeSpecTool(args, projectRoot2) {
|
|
|
3936
3996
|
if (!args.source || args.source.length > 5e4) {
|
|
3937
3997
|
throw new Error("source must be between 1 and 50000 characters");
|
|
3938
3998
|
}
|
|
3999
|
+
const sanitizedSource = sanitizeSource(args.source);
|
|
3939
4000
|
ensureProjectReady(projectRoot2);
|
|
3940
4001
|
const config = loadConfig(projectRoot2);
|
|
3941
|
-
logger.info(`Reading requirement from: ${
|
|
3942
|
-
const req = await readRequirement(
|
|
4002
|
+
logger.info(`Reading requirement from: ${sanitizedSource.slice(0, 120)}`);
|
|
4003
|
+
const req = await readRequirement(sanitizedSource);
|
|
3943
4004
|
const specLoreDir = join19(projectRoot2, ".speclore");
|
|
3944
4005
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
3945
4006
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
@@ -4090,10 +4151,10 @@ function ensureProjectReady(projectRoot2) {
|
|
|
4090
4151
|
const specLoreDir = join19(projectRoot2, ".speclore");
|
|
4091
4152
|
const configPath = join19(specLoreDir, "config.yaml");
|
|
4092
4153
|
let configCreated = false;
|
|
4093
|
-
if (!
|
|
4154
|
+
if (!existsSync20(specLoreDir)) {
|
|
4094
4155
|
mkdirSync10(specLoreDir, { recursive: true });
|
|
4095
4156
|
}
|
|
4096
|
-
if (!
|
|
4157
|
+
if (!existsSync20(configPath)) {
|
|
4097
4158
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4098
4159
|
writeFileSync11(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4099
4160
|
configCreated = true;
|
|
@@ -4145,8 +4206,8 @@ function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
|
4145
4206
|
for (const pattern of searchPatterns) {
|
|
4146
4207
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
4147
4208
|
for (const filePath of matches) {
|
|
4148
|
-
if (
|
|
4149
|
-
const content =
|
|
4209
|
+
if (existsSync20(filePath)) {
|
|
4210
|
+
const content = readFileSync17(filePath, "utf-8");
|
|
4150
4211
|
files.push(parseFeatureFile(filePath, content));
|
|
4151
4212
|
}
|
|
4152
4213
|
}
|
|
@@ -4230,29 +4291,30 @@ function buildCodingGuidance(_features, context, config) {
|
|
|
4230
4291
|
}
|
|
4231
4292
|
|
|
4232
4293
|
// src/setup/detector.ts
|
|
4233
|
-
import { existsSync as
|
|
4294
|
+
import { existsSync as existsSync21 } from "fs";
|
|
4234
4295
|
import { join as join20 } from "path";
|
|
4235
4296
|
function detectAITools2(projectRoot2) {
|
|
4236
4297
|
const tools = [];
|
|
4237
4298
|
const cursorFiles = [".cursor/mcp.json", ".cursor/rules"];
|
|
4238
|
-
const cursorConfigs = cursorFiles.filter((f) =>
|
|
4299
|
+
const cursorConfigs = cursorFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
|
|
4239
4300
|
tools.push({
|
|
4240
4301
|
tool: "cursor",
|
|
4241
|
-
detected:
|
|
4302
|
+
detected: existsSync21(join20(projectRoot2, ".cursor")),
|
|
4242
4303
|
configFiles: cursorConfigs
|
|
4243
4304
|
});
|
|
4244
4305
|
const claudeFiles = [".claude/", "CLAUDE.md"];
|
|
4245
|
-
const claudeConfigs = claudeFiles.filter((f) =>
|
|
4306
|
+
const claudeConfigs = claudeFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
|
|
4246
4307
|
tools.push({
|
|
4247
4308
|
tool: "claude",
|
|
4248
4309
|
detected: claudeConfigs.length > 0,
|
|
4249
4310
|
configFiles: claudeConfigs
|
|
4250
4311
|
});
|
|
4251
|
-
const
|
|
4252
|
-
const
|
|
4312
|
+
const qoderDir = hasQoderDir(projectRoot2) ? existsSync21(join20(projectRoot2, ".qoder-cn")) ? ".qoder-cn" : ".qoder" : ".qoder";
|
|
4313
|
+
const qoderFiles = [`${qoderDir}/mcp.json`, `${qoderDir}/rules`];
|
|
4314
|
+
const qoderConfigs = qoderFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
|
|
4253
4315
|
tools.push({
|
|
4254
4316
|
tool: "qoder",
|
|
4255
|
-
detected:
|
|
4317
|
+
detected: hasQoderDir(projectRoot2),
|
|
4256
4318
|
configFiles: qoderConfigs
|
|
4257
4319
|
});
|
|
4258
4320
|
return tools.filter((t) => t.detected);
|
|
@@ -4260,7 +4322,7 @@ function detectAITools2(projectRoot2) {
|
|
|
4260
4322
|
|
|
4261
4323
|
// src/mcp/status.ts
|
|
4262
4324
|
init_logger();
|
|
4263
|
-
import { readFileSync as
|
|
4325
|
+
import { readFileSync as readFileSync18, existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
4264
4326
|
import { join as join21 } from "path";
|
|
4265
4327
|
import { globSync as globSync5 } from "glob";
|
|
4266
4328
|
function executeStatusTool(args, projectRoot2) {
|
|
@@ -4277,9 +4339,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4277
4339
|
continue;
|
|
4278
4340
|
}
|
|
4279
4341
|
let scenarioCount = 0;
|
|
4280
|
-
if (
|
|
4342
|
+
if (existsSync22(path)) {
|
|
4281
4343
|
try {
|
|
4282
|
-
const content =
|
|
4344
|
+
const content = readFileSync18(path, "utf-8");
|
|
4283
4345
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4284
4346
|
scenarioCount = matches?.length ?? 0;
|
|
4285
4347
|
} catch {
|
|
@@ -4302,7 +4364,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4302
4364
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4303
4365
|
let scenarioCount = 0;
|
|
4304
4366
|
try {
|
|
4305
|
-
const content =
|
|
4367
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
4306
4368
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4307
4369
|
scenarioCount = matches?.length ?? 0;
|
|
4308
4370
|
} catch {
|
|
@@ -4362,10 +4424,10 @@ function ensureProjectReadyForStatus(projectRoot2) {
|
|
|
4362
4424
|
const specLoreDir = join21(projectRoot2, ".speclore");
|
|
4363
4425
|
const configPath = join21(specLoreDir, "config.yaml");
|
|
4364
4426
|
let configCreated = false;
|
|
4365
|
-
if (!
|
|
4427
|
+
if (!existsSync22(specLoreDir)) {
|
|
4366
4428
|
mkdirSync11(specLoreDir, { recursive: true });
|
|
4367
4429
|
}
|
|
4368
|
-
if (!
|
|
4430
|
+
if (!existsSync22(configPath)) {
|
|
4369
4431
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4370
4432
|
writeFileSync12(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4371
4433
|
configCreated = true;
|
|
@@ -4402,7 +4464,11 @@ server.registerTool(
|
|
|
4402
4464
|
{ source: args.source, module: args.module },
|
|
4403
4465
|
getProjectRoot()
|
|
4404
4466
|
);
|
|
4405
|
-
|
|
4467
|
+
const summary = buildSpecSummary(result);
|
|
4468
|
+
return { content: [{ type: "text", text: `${summary}
|
|
4469
|
+
|
|
4470
|
+
---
|
|
4471
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
4406
4472
|
}
|
|
4407
4473
|
);
|
|
4408
4474
|
server.registerTool(
|
|
@@ -4419,7 +4485,11 @@ server.registerTool(
|
|
|
4419
4485
|
{ features: args.features, tools: args.tools },
|
|
4420
4486
|
getProjectRoot()
|
|
4421
4487
|
);
|
|
4422
|
-
|
|
4488
|
+
const summary = buildCodeSummary(result);
|
|
4489
|
+
return { content: [{ type: "text", text: `${summary}
|
|
4490
|
+
|
|
4491
|
+
---
|
|
4492
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
4423
4493
|
}
|
|
4424
4494
|
);
|
|
4425
4495
|
server.registerTool(
|
|
@@ -4433,7 +4503,11 @@ server.registerTool(
|
|
|
4433
4503
|
{ features: args.features, impact: args.impact },
|
|
4434
4504
|
getProjectRoot()
|
|
4435
4505
|
);
|
|
4436
|
-
|
|
4506
|
+
const summary = buildVerifySummary(result);
|
|
4507
|
+
return { content: [{ type: "text", text: `${summary}
|
|
4508
|
+
|
|
4509
|
+
---
|
|
4510
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
4437
4511
|
}
|
|
4438
4512
|
);
|
|
4439
4513
|
server.registerTool(
|
|
@@ -4447,7 +4521,11 @@ server.registerTool(
|
|
|
4447
4521
|
{ feature: args.feature },
|
|
4448
4522
|
getProjectRoot()
|
|
4449
4523
|
);
|
|
4450
|
-
|
|
4524
|
+
const summary = buildStatusSummary(result);
|
|
4525
|
+
return { content: [{ type: "text", text: `${summary}
|
|
4526
|
+
|
|
4527
|
+
---
|
|
4528
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
4451
4529
|
}
|
|
4452
4530
|
);
|
|
4453
4531
|
var projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
@@ -4461,10 +4539,10 @@ async function startMcpServer() {
|
|
|
4461
4539
|
mkdirSync12(specLoreDir, { recursive: true });
|
|
4462
4540
|
const locked = acquireLock(specLoreDir);
|
|
4463
4541
|
if (!locked) {
|
|
4464
|
-
logger.warn("Could not acquire lock
|
|
4542
|
+
logger.warn("Could not acquire lock - another SpecLore instance may be running.");
|
|
4465
4543
|
}
|
|
4466
4544
|
} catch {
|
|
4467
|
-
logger.warn("Could not acquire lock
|
|
4545
|
+
logger.warn("Could not acquire lock - .speclore/ directory may not exist.");
|
|
4468
4546
|
}
|
|
4469
4547
|
const transport = new StdioServerTransport();
|
|
4470
4548
|
await server.connect(transport);
|
|
@@ -4484,6 +4562,58 @@ async function startMcpServer() {
|
|
|
4484
4562
|
process.exit(0);
|
|
4485
4563
|
});
|
|
4486
4564
|
}
|
|
4565
|
+
function buildSpecSummary(result) {
|
|
4566
|
+
const lines = [];
|
|
4567
|
+
lines.push("[SpecLore] Feature file generated successfully.");
|
|
4568
|
+
lines.push(` File: ${result.createdFiles.join(", ")}`);
|
|
4569
|
+
lines.push(` Scenarios: ${result.scenarios.length}`);
|
|
4570
|
+
for (const sc of result.scenarios) {
|
|
4571
|
+
lines.push(` - ${sc.name}`);
|
|
4572
|
+
}
|
|
4573
|
+
lines.push(` Next step: ${result.nextSteps}`);
|
|
4574
|
+
return lines.join("\n");
|
|
4575
|
+
}
|
|
4576
|
+
function buildCodeSummary(result) {
|
|
4577
|
+
const lines = [];
|
|
4578
|
+
lines.push("[SpecLore] Constraints and test scaffolding generated.");
|
|
4579
|
+
lines.push(` Constraint files: ${result.writtenFiles.length > 0 ? result.writtenFiles.join(", ") : "none"}`);
|
|
4580
|
+
lines.push(` Test scaffold files: ${result.scaffoldFiles.length > 0 ? result.scaffoldFiles.map((s) => s.testFile).join(", ") : "none"}`);
|
|
4581
|
+
lines.push(` Next step: ${result.workflow.nextStep}`);
|
|
4582
|
+
return lines.join("\n");
|
|
4583
|
+
}
|
|
4584
|
+
function buildVerifySummary(result) {
|
|
4585
|
+
const lines = [];
|
|
4586
|
+
const icon = result.failed === 0 ? "[PASS]" : "[FAIL]";
|
|
4587
|
+
lines.push(`[SpecLore] Verification ${icon} ${result.summary}`);
|
|
4588
|
+
if (result.failed > 0) {
|
|
4589
|
+
lines.push(` Failed: ${result.failed} scenario(s)`);
|
|
4590
|
+
for (const d of result.failedDetails) {
|
|
4591
|
+
lines.push(` - ${d.feature} > ${d.scenario}: ${d.error}`);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
if (result.unmapped > 0) {
|
|
4595
|
+
lines.push(` Unmapped: ${result.unmapped} scenario(s) without test mapping`);
|
|
4596
|
+
}
|
|
4597
|
+
lines.push(` Next step: ${result.workflow.nextStep}`);
|
|
4598
|
+
return lines.join("\n");
|
|
4599
|
+
}
|
|
4600
|
+
function buildStatusSummary(result) {
|
|
4601
|
+
const lines = [];
|
|
4602
|
+
lines.push(`[SpecLore] Project status: ${result.project.initialized ? "initialized" : "not initialized"}`);
|
|
4603
|
+
lines.push(` AI tools detected: ${result.project.aiToolsDetected.length > 0 ? result.project.aiToolsDetected.join(", ") : "none"}`);
|
|
4604
|
+
lines.push(` Features: ${result.summary.total} total`);
|
|
4605
|
+
if (result.summary.specified > 0) lines.push(` - ${result.summary.specified} specified`);
|
|
4606
|
+
if (result.summary.constrained > 0) lines.push(` - ${result.summary.constrained} constrained`);
|
|
4607
|
+
if (result.summary.coding > 0) lines.push(` - ${result.summary.coding} coding`);
|
|
4608
|
+
if (result.summary.verified > 0) lines.push(` - ${result.summary.verified} verified`);
|
|
4609
|
+
if (result.recommendedActions.length > 0) {
|
|
4610
|
+
lines.push(" Recommended actions:");
|
|
4611
|
+
for (const action of result.recommendedActions) {
|
|
4612
|
+
lines.push(` -> ${action}`);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
return lines.join("\n");
|
|
4616
|
+
}
|
|
4487
4617
|
|
|
4488
4618
|
// src/bin/mcp.ts
|
|
4489
4619
|
startMcpServer().catch((err) => {
|