speclore 0.1.4 → 0.1.7
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 +116 -65
- package/README.md +113 -62
- package/dist/cli/index.js +416 -201
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +416 -201
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +197 -141
- package/dist/mcp/server.js.map +1 -1
- package/package.json +14 -6
package/dist/mcp/server.js
CHANGED
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __esm = (fn, res
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
-
} catch (e) {
|
|
10
|
-
throw err = [e], e;
|
|
11
|
-
}
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
7
|
};
|
|
13
8
|
var __export = (target, all) => {
|
|
14
9
|
for (var name in all)
|
|
@@ -950,7 +945,7 @@ var DocxReader = class {
|
|
|
950
945
|
};
|
|
951
946
|
|
|
952
947
|
// src/plugins/builtin/xlsx-reader.ts
|
|
953
|
-
import
|
|
948
|
+
import { readFile, read, utils } from "xlsx";
|
|
954
949
|
|
|
955
950
|
// src/infra/excel-utils.ts
|
|
956
951
|
function formatCellValue(value) {
|
|
@@ -958,56 +953,62 @@ function formatCellValue(value) {
|
|
|
958
953
|
if (typeof value === "string") return value;
|
|
959
954
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
960
955
|
if (value instanceof Date) return value.toISOString();
|
|
956
|
+
if (typeof value === "object" && value !== null && "v" in value) {
|
|
957
|
+
const cell = value;
|
|
958
|
+
if (cell.w != null) return cell.w;
|
|
959
|
+
if (cell.v == null) return "";
|
|
960
|
+
if (typeof cell.v === "string" || typeof cell.v === "number" || typeof cell.v === "boolean") return String(cell.v);
|
|
961
|
+
return JSON.stringify(cell.v);
|
|
962
|
+
}
|
|
961
963
|
return "";
|
|
962
964
|
}
|
|
963
965
|
|
|
964
966
|
// src/plugins/builtin/xlsx-reader.ts
|
|
967
|
+
function parseWorkbook(workbook) {
|
|
968
|
+
const requirements = [];
|
|
969
|
+
for (const sheetName of workbook.SheetNames) {
|
|
970
|
+
const sheet = workbook.Sheets[sheetName];
|
|
971
|
+
if (!sheet) continue;
|
|
972
|
+
const allRows = utils.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
973
|
+
if (allRows.length === 0) continue;
|
|
974
|
+
const headers = allRows[0].map((cell) => formatCellValue(cell));
|
|
975
|
+
const dataRows = allRows.slice(1);
|
|
976
|
+
const rows = dataRows.map((row) => {
|
|
977
|
+
const obj = {};
|
|
978
|
+
row.forEach((cell, colNumber) => {
|
|
979
|
+
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
980
|
+
obj[key] = formatCellValue(cell);
|
|
981
|
+
});
|
|
982
|
+
return obj;
|
|
983
|
+
});
|
|
984
|
+
for (let i = 0; i < rows.length; i++) {
|
|
985
|
+
const row = rows[i];
|
|
986
|
+
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
987
|
+
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
988
|
+
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
989
|
+
if (description || title) {
|
|
990
|
+
requirements.push({
|
|
991
|
+
id: `${sheetName}/${i + 1}`,
|
|
992
|
+
title: String(title),
|
|
993
|
+
description: String(description),
|
|
994
|
+
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
995
|
+
rawContent: JSON.stringify(row),
|
|
996
|
+
confidence: 0.7
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return requirements;
|
|
1002
|
+
}
|
|
965
1003
|
var XlsxReader = class {
|
|
966
1004
|
name = "xlsx-reader";
|
|
967
1005
|
supportedFormats = [".xlsx", ".xls"];
|
|
968
1006
|
canRead(source) {
|
|
969
1007
|
return /\.xlsx$/i.test(source) || /\.xls$/i.test(source);
|
|
970
1008
|
}
|
|
971
|
-
|
|
972
|
-
const workbook =
|
|
973
|
-
|
|
974
|
-
const requirements = [];
|
|
975
|
-
for (const sheet of workbook.worksheets) {
|
|
976
|
-
const sheetName = sheet.name;
|
|
977
|
-
const headers = [];
|
|
978
|
-
const rows = [];
|
|
979
|
-
sheet.eachRow((row, rowNumber) => {
|
|
980
|
-
if (rowNumber === 1) {
|
|
981
|
-
row.eachCell((cell, colNumber) => {
|
|
982
|
-
headers[colNumber - 1] = formatCellValue(cell.value);
|
|
983
|
-
});
|
|
984
|
-
} else {
|
|
985
|
-
const obj = {};
|
|
986
|
-
row.eachCell((cell, colNumber) => {
|
|
987
|
-
const key = headers[colNumber - 1] ?? `Col${colNumber}`;
|
|
988
|
-
obj[key] = formatCellValue(cell.value);
|
|
989
|
-
});
|
|
990
|
-
rows.push(obj);
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
for (let i = 0; i < rows.length; i++) {
|
|
994
|
-
const row = rows[i];
|
|
995
|
-
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
996
|
-
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
997
|
-
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
998
|
-
if (description || title) {
|
|
999
|
-
requirements.push({
|
|
1000
|
-
id: `${sheetName}/${i + 1}`,
|
|
1001
|
-
title: String(title),
|
|
1002
|
-
description: String(description),
|
|
1003
|
-
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
1004
|
-
rawContent: JSON.stringify(row),
|
|
1005
|
-
confidence: 0.7
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return requirements;
|
|
1009
|
+
read(source) {
|
|
1010
|
+
const workbook = readFile(source);
|
|
1011
|
+
return Promise.resolve(parseWorkbook(workbook));
|
|
1011
1012
|
}
|
|
1012
1013
|
};
|
|
1013
1014
|
|
|
@@ -1019,21 +1020,35 @@ var PdfReader = class {
|
|
|
1019
1020
|
return /\.pdf$/i.test(source);
|
|
1020
1021
|
}
|
|
1021
1022
|
async read(source) {
|
|
1022
|
-
let
|
|
1023
|
+
let pdfjsLib;
|
|
1023
1024
|
try {
|
|
1024
|
-
|
|
1025
|
-
pdfParse = mod.default ?? mod;
|
|
1025
|
+
pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1026
1026
|
} catch {
|
|
1027
|
-
throw new Error("
|
|
1028
|
-
}
|
|
1029
|
-
const { readFileSync:
|
|
1030
|
-
const buffer =
|
|
1031
|
-
const
|
|
1027
|
+
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1028
|
+
}
|
|
1029
|
+
const { readFileSync: readFileSync18 } = await import("fs");
|
|
1030
|
+
const buffer = readFileSync18(source);
|
|
1031
|
+
const uint8 = new Uint8Array(buffer);
|
|
1032
|
+
const doc = await pdfjsLib.getDocument({
|
|
1033
|
+
data: uint8,
|
|
1034
|
+
useWorkerFetch: false,
|
|
1035
|
+
isEvalSupported: false
|
|
1036
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1037
|
+
}).promise;
|
|
1038
|
+
const textParts = [];
|
|
1039
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
1040
|
+
const page = await doc.getPage(i);
|
|
1041
|
+
const textContent = await page.getTextContent();
|
|
1042
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
1043
|
+
if (pageText) textParts.push(pageText);
|
|
1044
|
+
}
|
|
1045
|
+
doc.destroy();
|
|
1046
|
+
const text = textParts.join("\n");
|
|
1032
1047
|
return [{
|
|
1033
1048
|
id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
|
|
1034
1049
|
title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
|
|
1035
|
-
description:
|
|
1036
|
-
rawContent:
|
|
1050
|
+
description: text,
|
|
1051
|
+
rawContent: text,
|
|
1037
1052
|
confidence: 0.75
|
|
1038
1053
|
}];
|
|
1039
1054
|
}
|
|
@@ -1047,13 +1062,13 @@ var ImageReader = class {
|
|
|
1047
1062
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
1048
1063
|
}
|
|
1049
1064
|
async read(source) {
|
|
1050
|
-
const { existsSync: existsSync22, readFileSync:
|
|
1065
|
+
const { existsSync: existsSync22, readFileSync: readFileSync18 } = await import("fs");
|
|
1051
1066
|
if (!existsSync22(source)) {
|
|
1052
1067
|
throw new Error(`Image file not found: ${source}`);
|
|
1053
1068
|
}
|
|
1054
1069
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
1055
1070
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
1056
|
-
const buffer =
|
|
1071
|
+
const buffer = readFileSync18(source);
|
|
1057
1072
|
let text;
|
|
1058
1073
|
try {
|
|
1059
1074
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -1647,11 +1662,12 @@ function extractDependencies(content) {
|
|
|
1647
1662
|
|
|
1648
1663
|
// src/core/requirement-reader/docx-reader.ts
|
|
1649
1664
|
import { basename as basename2, extname as extname2 } from "path";
|
|
1665
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1650
1666
|
import mammoth from "mammoth";
|
|
1651
|
-
async function
|
|
1652
|
-
const result = await mammoth.extractRawText({
|
|
1667
|
+
async function parseDocxBuffer(buffer, idHint = "document") {
|
|
1668
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
1653
1669
|
const content = result.value;
|
|
1654
|
-
const id =
|
|
1670
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
1655
1671
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
1656
1672
|
const title = lines[0]?.trim() ?? id;
|
|
1657
1673
|
return {
|
|
@@ -1662,86 +1678,123 @@ async function readDocxFile(filePath) {
|
|
|
1662
1678
|
confidence: 0.9
|
|
1663
1679
|
};
|
|
1664
1680
|
}
|
|
1681
|
+
async function readDocxFile(filePath) {
|
|
1682
|
+
const buffer = readFileSync4(filePath);
|
|
1683
|
+
const name = basename2(filePath, extname2(filePath));
|
|
1684
|
+
return parseDocxBuffer(buffer, name);
|
|
1685
|
+
}
|
|
1665
1686
|
|
|
1666
1687
|
// src/core/requirement-reader/xlsx-reader.ts
|
|
1667
1688
|
import { basename as basename3, extname as extname3 } from "path";
|
|
1668
|
-
import
|
|
1669
|
-
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1689
|
+
import { readFile as readFile2, read as read2, utils as utils2 } from "xlsx";
|
|
1690
|
+
function parseWorkbook2(workbook, idHint) {
|
|
1691
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
1692
|
+
const firstSheetName = workbook.SheetNames[0];
|
|
1693
|
+
if (!firstSheetName) {
|
|
1694
|
+
return Promise.reject(new Error("No sheets found in workbook"));
|
|
1695
|
+
}
|
|
1696
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
1674
1697
|
if (!sheet) {
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
const title =
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1698
|
+
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
|
|
1699
|
+
}
|
|
1700
|
+
const title = firstSheetName;
|
|
1701
|
+
const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
1702
|
+
if (rows.length === 0) {
|
|
1703
|
+
return Promise.resolve({
|
|
1704
|
+
id,
|
|
1705
|
+
title,
|
|
1706
|
+
description: "",
|
|
1707
|
+
rawContent: "",
|
|
1708
|
+
confidence: 0.85
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
const headers = rows[0].map((cell) => formatCellValue(cell));
|
|
1712
|
+
const dataRows = rows.slice(1);
|
|
1713
|
+
const objects = dataRows.map((row) => {
|
|
1714
|
+
const obj = {};
|
|
1715
|
+
row.forEach((cell, colNumber) => {
|
|
1716
|
+
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
1717
|
+
obj[key] = formatCellValue(cell);
|
|
1718
|
+
});
|
|
1719
|
+
return obj;
|
|
1693
1720
|
});
|
|
1694
|
-
const textRows =
|
|
1721
|
+
const textRows = objects.map((row) => {
|
|
1695
1722
|
const cells = Object.entries(row).map(([key, value]) => `${key}: ${value}`).join(" | ");
|
|
1696
1723
|
return cells;
|
|
1697
1724
|
});
|
|
1698
1725
|
const content = textRows.join("\n");
|
|
1699
|
-
return {
|
|
1726
|
+
return Promise.resolve({
|
|
1700
1727
|
id,
|
|
1701
1728
|
title,
|
|
1702
1729
|
description: content,
|
|
1703
1730
|
rawContent: content,
|
|
1704
1731
|
confidence: 0.85
|
|
1705
|
-
};
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
function readXlsxFile(filePath) {
|
|
1735
|
+
const workbook = readFile2(filePath);
|
|
1736
|
+
const idHint = basename3(filePath, extname3(filePath));
|
|
1737
|
+
return parseWorkbook2(workbook, idHint);
|
|
1706
1738
|
}
|
|
1707
1739
|
|
|
1708
1740
|
// src/core/requirement-reader/pdf-reader.ts
|
|
1709
1741
|
import { basename as basename4, extname as extname4 } from "path";
|
|
1710
|
-
import { readFile } from "fs/promises";
|
|
1711
|
-
async function
|
|
1712
|
-
const
|
|
1713
|
-
const
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1742
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1743
|
+
async function extractPdfText(dataBuffer) {
|
|
1744
|
+
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1745
|
+
const uint8 = new Uint8Array(dataBuffer);
|
|
1746
|
+
const doc = await pdfjsLib.getDocument({
|
|
1747
|
+
data: uint8,
|
|
1748
|
+
useWorkerFetch: false,
|
|
1749
|
+
isEvalSupported: false
|
|
1750
|
+
}).promise;
|
|
1751
|
+
const textParts = [];
|
|
1752
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
1753
|
+
const page = await doc.getPage(i);
|
|
1754
|
+
const textContent = await page.getTextContent();
|
|
1755
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
1756
|
+
if (pageText) textParts.push(pageText);
|
|
1757
|
+
}
|
|
1758
|
+
doc.destroy();
|
|
1759
|
+
return textParts.join("\n");
|
|
1760
|
+
}
|
|
1761
|
+
async function parsePdfBuffer(dataBuffer, idHint = "document") {
|
|
1762
|
+
const text = await extractPdfText(dataBuffer);
|
|
1763
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
1764
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
1717
1765
|
const title = lines[0]?.trim() ?? id;
|
|
1718
1766
|
return {
|
|
1719
1767
|
id,
|
|
1720
1768
|
title,
|
|
1721
|
-
description:
|
|
1722
|
-
rawContent:
|
|
1769
|
+
description: text,
|
|
1770
|
+
rawContent: text,
|
|
1723
1771
|
confidence: 0.8
|
|
1724
1772
|
};
|
|
1725
1773
|
}
|
|
1774
|
+
async function readPdfFile(filePath) {
|
|
1775
|
+
const dataBuffer = await readFile3(filePath);
|
|
1776
|
+
const name = basename4(filePath, extname4(filePath));
|
|
1777
|
+
return parsePdfBuffer(dataBuffer, name);
|
|
1778
|
+
}
|
|
1726
1779
|
|
|
1727
1780
|
// src/core/requirement-reader/image-reader.ts
|
|
1728
1781
|
init_provider();
|
|
1729
1782
|
init_logger();
|
|
1730
1783
|
import { basename as basename5, extname as extname5 } from "path";
|
|
1731
|
-
import { readFileSync as
|
|
1784
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1732
1785
|
var MIME_MAP = {
|
|
1733
1786
|
".png": "image/png",
|
|
1734
1787
|
".jpg": "image/jpeg",
|
|
1735
1788
|
".jpeg": "image/jpeg",
|
|
1736
1789
|
".webp": "image/webp"
|
|
1737
1790
|
};
|
|
1738
|
-
async function readImageFile(filePath) {
|
|
1791
|
+
async function readImageFile(filePath, providerOverride) {
|
|
1739
1792
|
const id = basename5(filePath, extname5(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
1740
1793
|
logger.info(`Reading image via AI Vision: ${filePath}`);
|
|
1741
1794
|
const ext = extname5(filePath).toLowerCase();
|
|
1742
1795
|
const mimeType = MIME_MAP[ext] ?? "image/png";
|
|
1743
|
-
const buffer =
|
|
1744
|
-
const provider = await createProvider();
|
|
1796
|
+
const buffer = readFileSync5(filePath);
|
|
1797
|
+
const provider = providerOverride ?? await createProvider();
|
|
1745
1798
|
const prompt = `Please extract all text content from this image. Return the text as-is, preserving structure and formatting. If the image contains a table, convert it to a structured text format.`;
|
|
1746
1799
|
if (!provider.generateWithImage) {
|
|
1747
1800
|
throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
|
|
@@ -2145,7 +2198,7 @@ function toPosixPath(p) {
|
|
|
2145
2198
|
|
|
2146
2199
|
// src/core/feature-generator/generator.ts
|
|
2147
2200
|
var MAX_VALIDATION_RETRIES = 2;
|
|
2148
|
-
async function generateFeature(requirement, context, config, projectRoot2) {
|
|
2201
|
+
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
2149
2202
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
2150
2203
|
const registry = getRegistry();
|
|
2151
2204
|
await registry.invokeLifecycle("beforeSpec", requirement);
|
|
@@ -2153,7 +2206,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
|
|
|
2153
2206
|
const aiConfig = config.ai;
|
|
2154
2207
|
const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
|
|
2155
2208
|
const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
|
|
2156
|
-
const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
|
|
2209
|
+
const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
|
|
2157
2210
|
if (!provider.isAvailable()) {
|
|
2158
2211
|
throw new Error("AI provider not available. Set API key in environment or config.yaml.");
|
|
2159
2212
|
}
|
|
@@ -2348,9 +2401,9 @@ function detectAITools(projectRoot2) {
|
|
|
2348
2401
|
tools.push("cursor");
|
|
2349
2402
|
logger.debug("Detected Cursor (.cursor/)");
|
|
2350
2403
|
}
|
|
2351
|
-
if (existsSync8(join7(projectRoot2, ".claude")) || existsSync8(join7(projectRoot2, "CLAUDE.md"))
|
|
2404
|
+
if (existsSync8(join7(projectRoot2, ".claude")) || existsSync8(join7(projectRoot2, "CLAUDE.md"))) {
|
|
2352
2405
|
tools.push("claude");
|
|
2353
|
-
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md
|
|
2406
|
+
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
2354
2407
|
}
|
|
2355
2408
|
if (existsSync8(join7(projectRoot2, ".qoder"))) {
|
|
2356
2409
|
tools.push("qoder");
|
|
@@ -2483,9 +2536,9 @@ function readModuleConfig(config) {
|
|
|
2483
2536
|
|
|
2484
2537
|
// src/core/constraint-coder/index.ts
|
|
2485
2538
|
init_logger();
|
|
2486
|
-
var MAPPING_INSTRUCTIONS =
|
|
2487
|
-
|
|
2488
|
-
|
|
2539
|
+
var MAPPING_INSTRUCTIONS = `Generate a mapping file for each test file at .speclore/mappings/{module}/{feature-name}.json.
|
|
2540
|
+
Format: { "feature": "specs/...", "scenarios": { "Scenario name": { "testFile": "...", "testMethod": "..." } } }
|
|
2541
|
+
Keep mapping files in sync whenever tests are modified.`;
|
|
2489
2542
|
async function generateConstraints(projectRoot2, features, _context, config) {
|
|
2490
2543
|
const tools = detectAITools(projectRoot2);
|
|
2491
2544
|
if (tools.length === 0) {
|
|
@@ -2547,7 +2600,7 @@ import { execFileSync } from "child_process";
|
|
|
2547
2600
|
|
|
2548
2601
|
// src/core/verifier/mapping-resolver.ts
|
|
2549
2602
|
init_logger();
|
|
2550
|
-
import { readFileSync as
|
|
2603
|
+
import { readFileSync as readFileSync6, existsSync as existsSync10, readdirSync } from "fs";
|
|
2551
2604
|
import { join as join9 } from "path";
|
|
2552
2605
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
2553
2606
|
const results = [];
|
|
@@ -2577,7 +2630,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
2577
2630
|
const files = findMappingFiles(mappingsDir);
|
|
2578
2631
|
for (const file of files) {
|
|
2579
2632
|
try {
|
|
2580
|
-
const content =
|
|
2633
|
+
const content = readFileSync6(file, "utf-8");
|
|
2581
2634
|
const mapping = JSON.parse(content);
|
|
2582
2635
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
2583
2636
|
const entry = mapping.scenarios[scenario.name];
|
|
@@ -2607,7 +2660,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
|
2607
2660
|
const files = findTestFiles(testsDir);
|
|
2608
2661
|
for (const file of files) {
|
|
2609
2662
|
try {
|
|
2610
|
-
const content =
|
|
2663
|
+
const content = readFileSync6(file, "utf-8");
|
|
2611
2664
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
2612
2665
|
let match;
|
|
2613
2666
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
@@ -2770,19 +2823,19 @@ function executeTestCommand(projectRoot2, config) {
|
|
|
2770
2823
|
|
|
2771
2824
|
// src/core/verifier/report-generator.ts
|
|
2772
2825
|
init_logger();
|
|
2773
|
-
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, readFileSync as
|
|
2826
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
|
|
2774
2827
|
import { join as join10, dirname as dirname2 } from "path";
|
|
2775
2828
|
import { fileURLToPath } from "url";
|
|
2776
2829
|
|
|
2777
2830
|
// src/core/context-engine/context-writer.ts
|
|
2778
2831
|
init_logger();
|
|
2779
|
-
import { readFileSync as
|
|
2832
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync13, statSync as statSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
2780
2833
|
import { join as join13 } from "path";
|
|
2781
2834
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2782
2835
|
|
|
2783
2836
|
// src/core/context-engine/graph-builder.ts
|
|
2784
2837
|
init_logger();
|
|
2785
|
-
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync2, readFileSync as
|
|
2838
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
2786
2839
|
import { join as join11, basename as basename7, extname as extname7, relative as relative2 } from "path";
|
|
2787
2840
|
function detectProjectInfo(projectRoot2) {
|
|
2788
2841
|
const info = {
|
|
@@ -2796,7 +2849,7 @@ function detectProjectInfo(projectRoot2) {
|
|
|
2796
2849
|
info.language = "typescript";
|
|
2797
2850
|
info.buildTool = "npm";
|
|
2798
2851
|
try {
|
|
2799
|
-
const pkg = JSON.parse(
|
|
2852
|
+
const pkg = JSON.parse(readFileSync8(join11(projectRoot2, "package.json"), "utf-8"));
|
|
2800
2853
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2801
2854
|
if ("next" in deps) info.framework = "next.js";
|
|
2802
2855
|
else if ("@nestjs/core" in deps) info.framework = "nestjs";
|
|
@@ -2813,7 +2866,7 @@ function detectProjectInfo(projectRoot2) {
|
|
|
2813
2866
|
info.buildTool = "maven";
|
|
2814
2867
|
info.testFramework = "junit";
|
|
2815
2868
|
try {
|
|
2816
|
-
const pom =
|
|
2869
|
+
const pom = readFileSync8(join11(projectRoot2, "pom.xml"), "utf-8");
|
|
2817
2870
|
if (pom.includes("spring-boot")) info.framework = "spring-boot";
|
|
2818
2871
|
} catch {
|
|
2819
2872
|
}
|
|
@@ -2834,7 +2887,7 @@ function detectProjectInfo(projectRoot2) {
|
|
|
2834
2887
|
info.buildTool = "pip";
|
|
2835
2888
|
info.testFramework = "pytest";
|
|
2836
2889
|
try {
|
|
2837
|
-
const reqs = existsSync12(join11(projectRoot2, "requirements.txt")) ?
|
|
2890
|
+
const reqs = existsSync12(join11(projectRoot2, "requirements.txt")) ? readFileSync8(join11(projectRoot2, "requirements.txt"), "utf-8") : readFileSync8(join11(projectRoot2, "pyproject.toml"), "utf-8");
|
|
2838
2891
|
if (reqs.includes("django")) info.framework = "django";
|
|
2839
2892
|
else if (reqs.includes("flask")) info.framework = "flask";
|
|
2840
2893
|
else if (reqs.includes("fastapi")) info.framework = "fastapi";
|
|
@@ -2921,7 +2974,7 @@ function scanImports(dir) {
|
|
|
2921
2974
|
const ext = extname7(entry);
|
|
2922
2975
|
if (![".ts", ".tsx", ".js", ".jsx", ".java", ".py"].includes(ext)) continue;
|
|
2923
2976
|
try {
|
|
2924
|
-
const content =
|
|
2977
|
+
const content = readFileSync8(fullPath, "utf-8");
|
|
2925
2978
|
const importRegex = /(?:import\s+.*?from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
2926
2979
|
let match;
|
|
2927
2980
|
while ((match = importRegex.exec(content)) !== null) {
|
|
@@ -2958,7 +3011,7 @@ function analyzeFileForEntities(filePath, modName, modPath, modRelativePath) {
|
|
|
2958
3011
|
const name = basename7(filePath, ext);
|
|
2959
3012
|
if (!ANALYZABLE_EXTENSIONS.has(ext)) return entities;
|
|
2960
3013
|
try {
|
|
2961
|
-
const content =
|
|
3014
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
2962
3015
|
const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
|
|
2963
3016
|
if (/(?:Entity|Model|Domain|Schema)$/i.test(name)) {
|
|
2964
3017
|
entities.push({ name, module: modName, file: relFile });
|
|
@@ -3011,7 +3064,7 @@ function analyzeFileForApis(filePath, modName, modPath, modRelativePath) {
|
|
|
3011
3064
|
const name = basename7(filePath, ext);
|
|
3012
3065
|
if (!ANALYZABLE_EXTENSIONS.has(ext)) return apis;
|
|
3013
3066
|
try {
|
|
3014
|
-
const content =
|
|
3067
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
3015
3068
|
const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
|
|
3016
3069
|
if (/Controller|Resource|Handler|Route$/i.test(name)) {
|
|
3017
3070
|
apis.push({
|
|
@@ -3128,7 +3181,7 @@ function extractApis(projectRoot2, modules) {
|
|
|
3128
3181
|
}
|
|
3129
3182
|
|
|
3130
3183
|
// src/version.ts
|
|
3131
|
-
import { readFileSync as
|
|
3184
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3132
3185
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3133
3186
|
import { join as join12, dirname as dirname3 } from "path";
|
|
3134
3187
|
function readVersion() {
|
|
@@ -3136,7 +3189,7 @@ function readVersion() {
|
|
|
3136
3189
|
for (let depth = 0; depth < 4; depth++) {
|
|
3137
3190
|
try {
|
|
3138
3191
|
const pkgPath = join12(currentDir, "package.json");
|
|
3139
|
-
const pkg = JSON.parse(
|
|
3192
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
3140
3193
|
if (typeof pkg.version === "string") return pkg.version;
|
|
3141
3194
|
} catch {
|
|
3142
3195
|
}
|
|
@@ -3207,7 +3260,7 @@ function loadContext(specLoreDir) {
|
|
|
3207
3260
|
return null;
|
|
3208
3261
|
}
|
|
3209
3262
|
try {
|
|
3210
|
-
const content =
|
|
3263
|
+
const content = readFileSync10(contextPath, "utf-8");
|
|
3211
3264
|
const context = JSON.parse(content);
|
|
3212
3265
|
logger.debug("Loaded cached context.json");
|
|
3213
3266
|
return context;
|
|
@@ -3226,7 +3279,7 @@ function hasGitHeadChanged(specLoreDir) {
|
|
|
3226
3279
|
}).trim();
|
|
3227
3280
|
const headFile = join13(specLoreDir, ".git-head");
|
|
3228
3281
|
if (!existsSync13(headFile)) return true;
|
|
3229
|
-
const savedHead =
|
|
3282
|
+
const savedHead = readFileSync10(headFile, "utf-8").trim();
|
|
3230
3283
|
if (savedHead !== head) {
|
|
3231
3284
|
writeFileSync8(headFile, head, "utf-8");
|
|
3232
3285
|
return true;
|
|
@@ -3245,7 +3298,7 @@ function truncateTo(text, maxLines) {
|
|
|
3245
3298
|
|
|
3246
3299
|
// src/core/analyzer/rdg-builder.ts
|
|
3247
3300
|
init_logger();
|
|
3248
|
-
import { readFileSync as
|
|
3301
|
+
import { readFileSync as readFileSync11, existsSync as existsSync14, readdirSync as readdirSync3 } from "fs";
|
|
3249
3302
|
import { join as join14 } from "path";
|
|
3250
3303
|
|
|
3251
3304
|
// src/core/analyzer/cdg-builder.ts
|
|
@@ -3260,6 +3313,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
3260
3313
|
import { globSync as globSync2 } from "glob";
|
|
3261
3314
|
function analyzeImpact(projectRoot2, context, config) {
|
|
3262
3315
|
const changedFiles = getChangedFiles(projectRoot2);
|
|
3316
|
+
return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
|
|
3317
|
+
}
|
|
3318
|
+
function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
|
|
3263
3319
|
const affectedModules = determineAffectedModules(changedFiles, context);
|
|
3264
3320
|
const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
|
|
3265
3321
|
logger.info(
|
|
@@ -3317,7 +3373,7 @@ function determineAffectedFeatures(affectedModules, projectRoot2, outputDir) {
|
|
|
3317
3373
|
}
|
|
3318
3374
|
|
|
3319
3375
|
// src/core/state-manager/index.ts
|
|
3320
|
-
import { readFileSync as
|
|
3376
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync15, mkdirSync as mkdirSync8 } from "fs";
|
|
3321
3377
|
import { join as join15 } from "path";
|
|
3322
3378
|
import yaml from "js-yaml";
|
|
3323
3379
|
import { globSync as globSync3 } from "glob";
|
|
@@ -3349,7 +3405,7 @@ var StateManager = class {
|
|
|
3349
3405
|
return createDefaultState();
|
|
3350
3406
|
}
|
|
3351
3407
|
try {
|
|
3352
|
-
const content =
|
|
3408
|
+
const content = readFileSync12(this.statePath, "utf-8");
|
|
3353
3409
|
const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
3354
3410
|
if (parsed && typeof parsed === "object") {
|
|
3355
3411
|
return parsed;
|
|
@@ -3511,11 +3567,11 @@ var StateManager = class {
|
|
|
3511
3567
|
};
|
|
3512
3568
|
|
|
3513
3569
|
// src/core/test-scaffolder/index.ts
|
|
3514
|
-
import { readFileSync as
|
|
3570
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
|
|
3515
3571
|
import { join as join17, relative as relative3, dirname as dirname4, basename as basename8 } from "path";
|
|
3516
3572
|
|
|
3517
3573
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3518
|
-
import { readFileSync as
|
|
3574
|
+
import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
|
|
3519
3575
|
import { join as join16 } from "path";
|
|
3520
3576
|
function detectTestFramework(projectRoot2) {
|
|
3521
3577
|
const pkgPath = join16(projectRoot2, "package.json");
|
|
@@ -3523,7 +3579,7 @@ function detectTestFramework(projectRoot2) {
|
|
|
3523
3579
|
return "vitest";
|
|
3524
3580
|
}
|
|
3525
3581
|
try {
|
|
3526
|
-
const pkg = JSON.parse(
|
|
3582
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3527
3583
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3528
3584
|
if ("vitest" in deps) return "vitest";
|
|
3529
3585
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3629,7 +3685,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3629
3685
|
return lines.join("\n");
|
|
3630
3686
|
}
|
|
3631
3687
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3632
|
-
const content =
|
|
3688
|
+
const content = readFileSync14(testFilePath, "utf-8");
|
|
3633
3689
|
const missing = [];
|
|
3634
3690
|
for (const scenario of scenarios) {
|
|
3635
3691
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3663,7 +3719,7 @@ function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
|
3663
3719
|
}
|
|
3664
3720
|
|
|
3665
3721
|
// src/infra/config.ts
|
|
3666
|
-
import { readFileSync as
|
|
3722
|
+
import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
|
|
3667
3723
|
import { join as join18 } from "path";
|
|
3668
3724
|
import { homedir } from "os";
|
|
3669
3725
|
import yaml2 from "js-yaml";
|
|
@@ -3726,7 +3782,7 @@ function readYamlIfExists(filePath) {
|
|
|
3726
3782
|
return null;
|
|
3727
3783
|
}
|
|
3728
3784
|
try {
|
|
3729
|
-
const content =
|
|
3785
|
+
const content = readFileSync15(filePath, "utf-8");
|
|
3730
3786
|
const parsed = yaml2.load(content, { schema: yaml2.JSON_SCHEMA });
|
|
3731
3787
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3732
3788
|
return parsed;
|
|
@@ -3850,7 +3906,7 @@ verify:
|
|
|
3850
3906
|
// src/mcp/tools.ts
|
|
3851
3907
|
init_logger();
|
|
3852
3908
|
import { join as join19 } from "path";
|
|
3853
|
-
import { readFileSync as
|
|
3909
|
+
import { readFileSync as readFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
3854
3910
|
import { globSync as globSync4 } from "glob";
|
|
3855
3911
|
import { toJSONSchema } from "zod";
|
|
3856
3912
|
|
|
@@ -4094,7 +4150,7 @@ function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
|
4094
4150
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
4095
4151
|
for (const filePath of matches) {
|
|
4096
4152
|
if (existsSync19(filePath)) {
|
|
4097
|
-
const content =
|
|
4153
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
4098
4154
|
files.push(parseFeatureFile(filePath, content));
|
|
4099
4155
|
}
|
|
4100
4156
|
}
|
|
@@ -4189,7 +4245,7 @@ function detectAITools2(projectRoot2) {
|
|
|
4189
4245
|
detected: existsSync20(join20(projectRoot2, ".cursor")),
|
|
4190
4246
|
configFiles: cursorConfigs
|
|
4191
4247
|
});
|
|
4192
|
-
const claudeFiles = [".claude/", "
|
|
4248
|
+
const claudeFiles = [".claude/", "CLAUDE.md"];
|
|
4193
4249
|
const claudeConfigs = claudeFiles.filter((f) => existsSync20(join20(projectRoot2, f)));
|
|
4194
4250
|
tools.push({
|
|
4195
4251
|
tool: "claude",
|
|
@@ -4208,7 +4264,7 @@ function detectAITools2(projectRoot2) {
|
|
|
4208
4264
|
|
|
4209
4265
|
// src/mcp/status.ts
|
|
4210
4266
|
init_logger();
|
|
4211
|
-
import { readFileSync as
|
|
4267
|
+
import { readFileSync as readFileSync17, existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
4212
4268
|
import { join as join21 } from "path";
|
|
4213
4269
|
import { globSync as globSync5 } from "glob";
|
|
4214
4270
|
function executeStatusTool(args, projectRoot2) {
|
|
@@ -4227,7 +4283,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4227
4283
|
let scenarioCount = 0;
|
|
4228
4284
|
if (existsSync21(path)) {
|
|
4229
4285
|
try {
|
|
4230
|
-
const content =
|
|
4286
|
+
const content = readFileSync17(path, "utf-8");
|
|
4231
4287
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4232
4288
|
scenarioCount = matches?.length ?? 0;
|
|
4233
4289
|
} catch {
|
|
@@ -4250,7 +4306,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4250
4306
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4251
4307
|
let scenarioCount = 0;
|
|
4252
4308
|
try {
|
|
4253
|
-
const content =
|
|
4309
|
+
const content = readFileSync17(filePath, "utf-8");
|
|
4254
4310
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4255
4311
|
scenarioCount = matches?.length ?? 0;
|
|
4256
4312
|
} catch {
|