qfai 0.8.0 → 0.9.0
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.md +29 -2
- package/assets/init/.qfai/README.md +8 -1
- package/assets/init/.qfai/prompts/README.md +6 -1
- package/assets/init/.qfai/prompts/analyze/README.md +38 -0
- package/assets/init/.qfai/prompts/analyze/scenario_test_consistency.md +35 -0
- package/assets/init/.qfai/prompts/analyze/spec_contract_consistency.md +36 -0
- package/assets/init/.qfai/prompts/analyze/spec_scenario_consistency.md +35 -0
- package/assets/init/.qfai/prompts.local/README.md +2 -1
- package/assets/init/.qfai/samples/analyze/analysis.md +38 -0
- package/dist/cli/index.cjs +529 -193
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +517 -181
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +346 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +346 -72
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -425,6 +425,7 @@ function configIssue(file, message) {
|
|
|
425
425
|
return {
|
|
426
426
|
code: "QFAI_CONFIG_INVALID",
|
|
427
427
|
severity: "error",
|
|
428
|
+
category: "compatibility",
|
|
428
429
|
message,
|
|
429
430
|
file,
|
|
430
431
|
rule: "config.invalid"
|
|
@@ -508,8 +509,8 @@ function isValidId(value, prefix) {
|
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
// src/core/report.ts
|
|
511
|
-
var
|
|
512
|
-
var
|
|
512
|
+
var import_promises15 = require("fs/promises");
|
|
513
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
513
514
|
|
|
514
515
|
// src/core/contractIndex.ts
|
|
515
516
|
var import_promises5 = require("fs/promises");
|
|
@@ -1232,8 +1233,8 @@ var import_promises7 = require("fs/promises");
|
|
|
1232
1233
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
1233
1234
|
var import_node_url = require("url");
|
|
1234
1235
|
async function resolveToolVersion() {
|
|
1235
|
-
if ("0.
|
|
1236
|
-
return "0.
|
|
1236
|
+
if ("0.9.0".length > 0) {
|
|
1237
|
+
return "0.9.0";
|
|
1237
1238
|
}
|
|
1238
1239
|
try {
|
|
1239
1240
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1544,12 +1545,16 @@ function formatError4(error) {
|
|
|
1544
1545
|
}
|
|
1545
1546
|
return String(error);
|
|
1546
1547
|
}
|
|
1547
|
-
function issue(code, message, severity, file, rule, refs) {
|
|
1548
|
+
function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1548
1549
|
const issue7 = {
|
|
1549
1550
|
code,
|
|
1550
1551
|
severity,
|
|
1552
|
+
category,
|
|
1551
1553
|
message
|
|
1552
1554
|
};
|
|
1555
|
+
if (suggested_action) {
|
|
1556
|
+
issue7.suggested_action = suggested_action;
|
|
1557
|
+
}
|
|
1553
1558
|
if (file) {
|
|
1554
1559
|
issue7.file = file;
|
|
1555
1560
|
}
|
|
@@ -1634,12 +1639,16 @@ function isMissingFileError2(error) {
|
|
|
1634
1639
|
}
|
|
1635
1640
|
return error.code === "ENOENT";
|
|
1636
1641
|
}
|
|
1637
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1642
|
+
function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
|
|
1638
1643
|
const issue7 = {
|
|
1639
1644
|
code,
|
|
1640
1645
|
severity,
|
|
1646
|
+
category,
|
|
1641
1647
|
message
|
|
1642
1648
|
};
|
|
1649
|
+
if (suggested_action) {
|
|
1650
|
+
issue7.suggested_action = suggested_action;
|
|
1651
|
+
}
|
|
1643
1652
|
if (file) {
|
|
1644
1653
|
issue7.file = file;
|
|
1645
1654
|
}
|
|
@@ -1724,12 +1733,16 @@ function formatFileList(files, root) {
|
|
|
1724
1733
|
return relative.length > 0 ? relative : file;
|
|
1725
1734
|
}).join(", ");
|
|
1726
1735
|
}
|
|
1727
|
-
function issue3(code, message, severity, file, rule, refs) {
|
|
1736
|
+
function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1728
1737
|
const issue7 = {
|
|
1729
1738
|
code,
|
|
1730
1739
|
severity,
|
|
1740
|
+
category,
|
|
1731
1741
|
message
|
|
1732
1742
|
};
|
|
1743
|
+
if (suggested_action) {
|
|
1744
|
+
issue7.suggested_action = suggested_action;
|
|
1745
|
+
}
|
|
1733
1746
|
if (file) {
|
|
1734
1747
|
issue7.file = file;
|
|
1735
1748
|
}
|
|
@@ -1742,8 +1755,164 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1742
1755
|
return issue7;
|
|
1743
1756
|
}
|
|
1744
1757
|
|
|
1745
|
-
// src/core/
|
|
1758
|
+
// src/core/promptsIntegrity.ts
|
|
1746
1759
|
var import_promises11 = require("fs/promises");
|
|
1760
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1761
|
+
|
|
1762
|
+
// src/shared/assets.ts
|
|
1763
|
+
var import_node_fs = require("fs");
|
|
1764
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1765
|
+
var import_node_url2 = require("url");
|
|
1766
|
+
function getInitAssetsDir() {
|
|
1767
|
+
const base = __filename;
|
|
1768
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1769
|
+
const baseDir = import_node_path12.default.dirname(basePath);
|
|
1770
|
+
const candidates = [
|
|
1771
|
+
import_node_path12.default.resolve(baseDir, "../../../assets/init"),
|
|
1772
|
+
import_node_path12.default.resolve(baseDir, "../../assets/init")
|
|
1773
|
+
];
|
|
1774
|
+
for (const candidate of candidates) {
|
|
1775
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
1776
|
+
return candidate;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
throw new Error(
|
|
1780
|
+
[
|
|
1781
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1782
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1783
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1784
|
+
].join("\n")
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/core/promptsIntegrity.ts
|
|
1789
|
+
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1790
|
+
const promptsDir = import_node_path13.default.resolve(root, ".qfai", "prompts");
|
|
1791
|
+
let templateDir;
|
|
1792
|
+
try {
|
|
1793
|
+
templateDir = import_node_path13.default.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1794
|
+
} catch {
|
|
1795
|
+
return {
|
|
1796
|
+
status: "skipped_missing_assets",
|
|
1797
|
+
promptsDir,
|
|
1798
|
+
templateDir: "",
|
|
1799
|
+
missing: [],
|
|
1800
|
+
extra: [],
|
|
1801
|
+
changed: []
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const projectFiles = await collectFiles(promptsDir);
|
|
1805
|
+
if (projectFiles.length === 0) {
|
|
1806
|
+
return {
|
|
1807
|
+
status: "skipped_missing_prompts",
|
|
1808
|
+
promptsDir,
|
|
1809
|
+
templateDir,
|
|
1810
|
+
missing: [],
|
|
1811
|
+
extra: [],
|
|
1812
|
+
changed: []
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const templateFiles = await collectFiles(templateDir);
|
|
1816
|
+
const templateByRel = /* @__PURE__ */ new Map();
|
|
1817
|
+
for (const abs of templateFiles) {
|
|
1818
|
+
templateByRel.set(toRel(templateDir, abs), abs);
|
|
1819
|
+
}
|
|
1820
|
+
const projectByRel = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const abs of projectFiles) {
|
|
1822
|
+
projectByRel.set(toRel(promptsDir, abs), abs);
|
|
1823
|
+
}
|
|
1824
|
+
const missing = [];
|
|
1825
|
+
const extra = [];
|
|
1826
|
+
const changed = [];
|
|
1827
|
+
for (const rel of templateByRel.keys()) {
|
|
1828
|
+
if (!projectByRel.has(rel)) {
|
|
1829
|
+
missing.push(rel);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
for (const rel of projectByRel.keys()) {
|
|
1833
|
+
if (!templateByRel.has(rel)) {
|
|
1834
|
+
extra.push(rel);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const common = intersectKeys(templateByRel, projectByRel);
|
|
1838
|
+
for (const rel of common) {
|
|
1839
|
+
const templateAbs = templateByRel.get(rel);
|
|
1840
|
+
const projectAbs = projectByRel.get(rel);
|
|
1841
|
+
if (!templateAbs || !projectAbs) {
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const [a, b] = await Promise.all([
|
|
1846
|
+
(0, import_promises11.readFile)(templateAbs, "utf-8"),
|
|
1847
|
+
(0, import_promises11.readFile)(projectAbs, "utf-8")
|
|
1848
|
+
]);
|
|
1849
|
+
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
1850
|
+
changed.push(rel);
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
changed.push(rel);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
1857
|
+
return {
|
|
1858
|
+
status,
|
|
1859
|
+
promptsDir,
|
|
1860
|
+
templateDir,
|
|
1861
|
+
missing: missing.sort(),
|
|
1862
|
+
extra: extra.sort(),
|
|
1863
|
+
changed: changed.sort()
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
function normalizeNewlines(text) {
|
|
1867
|
+
return text.replace(/\r\n/g, "\n");
|
|
1868
|
+
}
|
|
1869
|
+
function toRel(base, abs) {
|
|
1870
|
+
const rel = import_node_path13.default.relative(base, abs);
|
|
1871
|
+
return rel.replace(/[\\/]+/g, "/");
|
|
1872
|
+
}
|
|
1873
|
+
function intersectKeys(a, b) {
|
|
1874
|
+
const out = [];
|
|
1875
|
+
for (const key of a.keys()) {
|
|
1876
|
+
if (b.has(key)) {
|
|
1877
|
+
out.push(key);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return out;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/core/validators/promptsIntegrity.ts
|
|
1884
|
+
async function validatePromptsIntegrity(root) {
|
|
1885
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
1886
|
+
if (diff.status !== "modified") {
|
|
1887
|
+
return [];
|
|
1888
|
+
}
|
|
1889
|
+
const total = diff.missing.length + diff.extra.length + diff.changed.length;
|
|
1890
|
+
const hints = [
|
|
1891
|
+
diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
|
|
1892
|
+
diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
|
|
1893
|
+
diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
|
|
1894
|
+
].filter(Boolean).join(" / ");
|
|
1895
|
+
const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
|
|
1896
|
+
const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
|
|
1897
|
+
return [
|
|
1898
|
+
{
|
|
1899
|
+
code: "QFAI-PROMPTS-001",
|
|
1900
|
+
severity: "error",
|
|
1901
|
+
category: "change",
|
|
1902
|
+
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
1903
|
+
suggested_action: [
|
|
1904
|
+
"prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
|
|
1905
|
+
"\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
1906
|
+
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
1907
|
+
"- \u6A19\u6E96\u72B6\u614B\u3078\u623B\u3059\u5834\u5408: 'qfai init --force' \u3092\u5B9F\u884C\uFF08prompts \u306E\u307F\u4E0A\u66F8\u304D\u3001prompts.local \u306F\u4FDD\u8B77\uFF09"
|
|
1908
|
+
].join("\n"),
|
|
1909
|
+
rule: "prompts.integrity"
|
|
1910
|
+
}
|
|
1911
|
+
];
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/core/validators/scenario.ts
|
|
1915
|
+
var import_promises12 = require("fs/promises");
|
|
1747
1916
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1748
1917
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1749
1918
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1769,7 +1938,7 @@ async function validateScenarios(root, config) {
|
|
|
1769
1938
|
for (const entry of entries) {
|
|
1770
1939
|
let text;
|
|
1771
1940
|
try {
|
|
1772
|
-
text = await (0,
|
|
1941
|
+
text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
|
|
1773
1942
|
} catch (error) {
|
|
1774
1943
|
if (isMissingFileError3(error)) {
|
|
1775
1944
|
issues.push(
|
|
@@ -1914,12 +2083,16 @@ function validateScenarioContent(text, file) {
|
|
|
1914
2083
|
}
|
|
1915
2084
|
return issues;
|
|
1916
2085
|
}
|
|
1917
|
-
function issue4(code, message, severity, file, rule, refs) {
|
|
2086
|
+
function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1918
2087
|
const issue7 = {
|
|
1919
2088
|
code,
|
|
1920
2089
|
severity,
|
|
2090
|
+
category,
|
|
1921
2091
|
message
|
|
1922
2092
|
};
|
|
2093
|
+
if (suggested_action) {
|
|
2094
|
+
issue7.suggested_action = suggested_action;
|
|
2095
|
+
}
|
|
1923
2096
|
if (file) {
|
|
1924
2097
|
issue7.file = file;
|
|
1925
2098
|
}
|
|
@@ -1939,7 +2112,7 @@ function isMissingFileError3(error) {
|
|
|
1939
2112
|
}
|
|
1940
2113
|
|
|
1941
2114
|
// src/core/validators/spec.ts
|
|
1942
|
-
var
|
|
2115
|
+
var import_promises13 = require("fs/promises");
|
|
1943
2116
|
async function validateSpecs(root, config) {
|
|
1944
2117
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1945
2118
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1960,7 +2133,7 @@ async function validateSpecs(root, config) {
|
|
|
1960
2133
|
for (const entry of entries) {
|
|
1961
2134
|
let text;
|
|
1962
2135
|
try {
|
|
1963
|
-
text = await (0,
|
|
2136
|
+
text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
|
|
1964
2137
|
} catch (error) {
|
|
1965
2138
|
if (isMissingFileError4(error)) {
|
|
1966
2139
|
issues.push(
|
|
@@ -2084,12 +2257,16 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2084
2257
|
}
|
|
2085
2258
|
return issues;
|
|
2086
2259
|
}
|
|
2087
|
-
function issue5(code, message, severity, file, rule, refs) {
|
|
2260
|
+
function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2088
2261
|
const issue7 = {
|
|
2089
2262
|
code,
|
|
2090
2263
|
severity,
|
|
2264
|
+
category,
|
|
2091
2265
|
message
|
|
2092
2266
|
};
|
|
2267
|
+
if (suggested_action) {
|
|
2268
|
+
issue7.suggested_action = suggested_action;
|
|
2269
|
+
}
|
|
2093
2270
|
if (file) {
|
|
2094
2271
|
issue7.file = file;
|
|
2095
2272
|
}
|
|
@@ -2109,7 +2286,7 @@ function isMissingFileError4(error) {
|
|
|
2109
2286
|
}
|
|
2110
2287
|
|
|
2111
2288
|
// src/core/validators/traceability.ts
|
|
2112
|
-
var
|
|
2289
|
+
var import_promises14 = require("fs/promises");
|
|
2113
2290
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2114
2291
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2115
2292
|
async function validateTraceability(root, config) {
|
|
@@ -2129,7 +2306,7 @@ async function validateTraceability(root, config) {
|
|
|
2129
2306
|
const contractIndex = await buildContractIndex(root, config);
|
|
2130
2307
|
const contractIds = contractIndex.ids;
|
|
2131
2308
|
for (const file of specFiles) {
|
|
2132
|
-
const text = await (0,
|
|
2309
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2133
2310
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2134
2311
|
const parsed = parseSpec(text, file);
|
|
2135
2312
|
if (parsed.specId) {
|
|
@@ -2202,7 +2379,7 @@ async function validateTraceability(root, config) {
|
|
|
2202
2379
|
}
|
|
2203
2380
|
}
|
|
2204
2381
|
for (const file of scenarioFiles) {
|
|
2205
|
-
const text = await (0,
|
|
2382
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2206
2383
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2207
2384
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2208
2385
|
allowCommentPrefix: true
|
|
@@ -2524,7 +2701,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2524
2701
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2525
2702
|
let found = false;
|
|
2526
2703
|
for (const file of targetFiles) {
|
|
2527
|
-
const text = await (0,
|
|
2704
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2528
2705
|
if (pattern.test(text)) {
|
|
2529
2706
|
found = true;
|
|
2530
2707
|
break;
|
|
@@ -2547,12 +2724,16 @@ function buildIdPattern(ids) {
|
|
|
2547
2724
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2548
2725
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
2549
2726
|
}
|
|
2550
|
-
function issue6(code, message, severity, file, rule, refs) {
|
|
2727
|
+
function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2551
2728
|
const issue7 = {
|
|
2552
2729
|
code,
|
|
2553
2730
|
severity,
|
|
2731
|
+
category,
|
|
2554
2732
|
message
|
|
2555
2733
|
};
|
|
2734
|
+
if (suggested_action) {
|
|
2735
|
+
issue7.suggested_action = suggested_action;
|
|
2736
|
+
}
|
|
2556
2737
|
if (file) {
|
|
2557
2738
|
issue7.file = file;
|
|
2558
2739
|
}
|
|
@@ -2571,6 +2752,7 @@ async function validateProject(root, configResult) {
|
|
|
2571
2752
|
const { config, issues: configIssues } = resolved;
|
|
2572
2753
|
const issues = [
|
|
2573
2754
|
...configIssues,
|
|
2755
|
+
...await validatePromptsIntegrity(root),
|
|
2574
2756
|
...await validateSpecs(root, config),
|
|
2575
2757
|
...await validateDeltas(root, config),
|
|
2576
2758
|
...await validateScenarios(root, config),
|
|
@@ -2611,15 +2793,15 @@ function countIssues(issues) {
|
|
|
2611
2793
|
// src/core/report.ts
|
|
2612
2794
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2613
2795
|
async function createReportData(root, validation, configResult) {
|
|
2614
|
-
const resolvedRoot =
|
|
2796
|
+
const resolvedRoot = import_node_path14.default.resolve(root);
|
|
2615
2797
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2616
2798
|
const config = resolved.config;
|
|
2617
2799
|
const configPath = resolved.configPath;
|
|
2618
2800
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2619
2801
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2620
|
-
const apiRoot =
|
|
2621
|
-
const uiRoot =
|
|
2622
|
-
const dbRoot =
|
|
2802
|
+
const apiRoot = import_node_path14.default.join(contractsRoot, "api");
|
|
2803
|
+
const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
|
|
2804
|
+
const dbRoot = import_node_path14.default.join(contractsRoot, "db");
|
|
2623
2805
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2624
2806
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2625
2807
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2733,7 +2915,39 @@ function formatReportMarkdown(data) {
|
|
|
2733
2915
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2734
2916
|
lines.push(`- \u7248: ${data.version}`);
|
|
2735
2917
|
lines.push("");
|
|
2736
|
-
|
|
2918
|
+
const severityOrder = {
|
|
2919
|
+
error: 0,
|
|
2920
|
+
warning: 1,
|
|
2921
|
+
info: 2
|
|
2922
|
+
};
|
|
2923
|
+
const categoryOrder = {
|
|
2924
|
+
compatibility: 0,
|
|
2925
|
+
change: 1
|
|
2926
|
+
};
|
|
2927
|
+
const issuesByCategory = {
|
|
2928
|
+
compatibility: [],
|
|
2929
|
+
change: []
|
|
2930
|
+
};
|
|
2931
|
+
for (const issue7 of data.issues) {
|
|
2932
|
+
const cat = issue7.category;
|
|
2933
|
+
if (cat === "change") {
|
|
2934
|
+
issuesByCategory.change.push(issue7);
|
|
2935
|
+
} else {
|
|
2936
|
+
issuesByCategory.compatibility.push(issue7);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const countIssuesBySeverity = (issues) => issues.reduce(
|
|
2940
|
+
(acc, i) => {
|
|
2941
|
+
acc[i.severity] += 1;
|
|
2942
|
+
return acc;
|
|
2943
|
+
},
|
|
2944
|
+
{ info: 0, warning: 0, error: 0 }
|
|
2945
|
+
);
|
|
2946
|
+
const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
|
|
2947
|
+
const changeCounts = countIssuesBySeverity(issuesByCategory.change);
|
|
2948
|
+
lines.push("## Dashboard");
|
|
2949
|
+
lines.push("");
|
|
2950
|
+
lines.push("### Summary");
|
|
2737
2951
|
lines.push("");
|
|
2738
2952
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2739
2953
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
@@ -2741,7 +2955,13 @@ function formatReportMarkdown(data) {
|
|
|
2741
2955
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2742
2956
|
);
|
|
2743
2957
|
lines.push(
|
|
2744
|
-
`- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
2958
|
+
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
2959
|
+
);
|
|
2960
|
+
lines.push(
|
|
2961
|
+
`- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
|
|
2962
|
+
);
|
|
2963
|
+
lines.push(
|
|
2964
|
+
`- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
|
|
2745
2965
|
);
|
|
2746
2966
|
lines.push(
|
|
2747
2967
|
`- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
|
|
@@ -2750,49 +2970,65 @@ function formatReportMarkdown(data) {
|
|
|
2750
2970
|
`- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
|
|
2751
2971
|
);
|
|
2752
2972
|
lines.push("");
|
|
2753
|
-
lines.push("
|
|
2754
|
-
lines.push("");
|
|
2755
|
-
lines.push("### Issues (by code)");
|
|
2973
|
+
lines.push("### Next Actions");
|
|
2756
2974
|
lines.push("");
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
severity: issue7.severity,
|
|
2772
|
-
code: issue7.code,
|
|
2773
|
-
count: 1
|
|
2774
|
-
});
|
|
2775
|
-
}
|
|
2776
|
-
const issueSummaryRows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
2777
|
-
const sa = severityOrder[a.severity] ?? 999;
|
|
2778
|
-
const sb = severityOrder[b.severity] ?? 999;
|
|
2779
|
-
if (sa !== sb) return sa - sb;
|
|
2780
|
-
return a.code.localeCompare(b.code);
|
|
2781
|
-
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
2782
|
-
if (issueSummaryRows.length === 0) {
|
|
2783
|
-
lines.push("- (none)");
|
|
2975
|
+
if (data.summary.counts.error > 0) {
|
|
2976
|
+
lines.push(
|
|
2977
|
+
"- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A `qfai validate --fail-on error` \u3092\u901A\u308B\u307E\u3067\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2978
|
+
);
|
|
2979
|
+
lines.push(
|
|
2980
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
2981
|
+
);
|
|
2982
|
+
} else if (data.summary.counts.warning > 0) {
|
|
2983
|
+
lines.push(
|
|
2984
|
+
"- warning \u306E\u6271\u3044\u306F\u30C1\u30FC\u30E0\u5224\u65AD\u3067\u3059\u3002`--fail-on warning` \u904B\u7528\u306A\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2985
|
+
);
|
|
2986
|
+
lines.push(
|
|
2987
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
2988
|
+
);
|
|
2784
2989
|
} else {
|
|
2990
|
+
lines.push("- issue \u306F\u3042\u308A\u307E\u305B\u3093\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
2785
2991
|
lines.push(
|
|
2786
|
-
|
|
2992
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
|
|
2787
2993
|
);
|
|
2788
2994
|
}
|
|
2789
2995
|
lines.push("");
|
|
2790
|
-
lines.push("###
|
|
2996
|
+
lines.push("### Index");
|
|
2791
2997
|
lines.push("");
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2998
|
+
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
2999
|
+
lines.push("- [Change Issues](#change-issues)");
|
|
3000
|
+
lines.push("- [IDs](#ids)");
|
|
3001
|
+
lines.push("- [Traceability](#traceability)");
|
|
3002
|
+
lines.push("");
|
|
3003
|
+
const formatIssueSummaryTable = (issues) => {
|
|
3004
|
+
const issueKeyToCount = /* @__PURE__ */ new Map();
|
|
3005
|
+
for (const issue7 of issues) {
|
|
3006
|
+
const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
|
|
3007
|
+
const current = issueKeyToCount.get(key);
|
|
3008
|
+
if (current) {
|
|
3009
|
+
current.count += 1;
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
issueKeyToCount.set(key, {
|
|
3013
|
+
category: issue7.category,
|
|
3014
|
+
severity: issue7.severity,
|
|
3015
|
+
code: issue7.code,
|
|
3016
|
+
count: 1
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
3020
|
+
const ca = categoryOrder[a.category] ?? 999;
|
|
3021
|
+
const cb = categoryOrder[b.category] ?? 999;
|
|
3022
|
+
if (ca !== cb) return ca - cb;
|
|
3023
|
+
const sa = severityOrder[a.severity] ?? 999;
|
|
3024
|
+
const sb = severityOrder[b.severity] ?? 999;
|
|
3025
|
+
if (sa !== sb) return sa - sb;
|
|
3026
|
+
return a.code.localeCompare(b.code);
|
|
3027
|
+
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
3028
|
+
return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
|
|
3029
|
+
};
|
|
3030
|
+
const formatIssueCards = (issues) => {
|
|
3031
|
+
const sorted = [...issues].sort((a, b) => {
|
|
2796
3032
|
const sa = severityOrder[a.severity] ?? 999;
|
|
2797
3033
|
const sb = severityOrder[b.severity] ?? 999;
|
|
2798
3034
|
if (sa !== sb) return sa - sb;
|
|
@@ -2806,16 +3042,54 @@ function formatReportMarkdown(data) {
|
|
|
2806
3042
|
const lineB = b.loc?.line ?? 0;
|
|
2807
3043
|
return lineA - lineB;
|
|
2808
3044
|
});
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3045
|
+
if (sorted.length === 0) {
|
|
3046
|
+
return ["- (none)"];
|
|
3047
|
+
}
|
|
3048
|
+
const out = [];
|
|
3049
|
+
for (const item of sorted) {
|
|
3050
|
+
out.push(
|
|
3051
|
+
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
2814
3052
|
);
|
|
3053
|
+
if (item.file) {
|
|
3054
|
+
const loc = item.loc?.line ? `:${item.loc.line}` : "";
|
|
3055
|
+
out.push(`- file: ${item.file}${loc}`);
|
|
3056
|
+
}
|
|
3057
|
+
if (item.rule) {
|
|
3058
|
+
out.push(`- rule: ${item.rule}`);
|
|
3059
|
+
}
|
|
3060
|
+
if (item.refs && item.refs.length > 0) {
|
|
3061
|
+
out.push(`- refs: ${item.refs.join(", ")}`);
|
|
3062
|
+
}
|
|
3063
|
+
if (item.suggested_action) {
|
|
3064
|
+
out.push("- suggested_action:");
|
|
3065
|
+
const actionLines = String(item.suggested_action).split("\n");
|
|
3066
|
+
for (const line of actionLines) {
|
|
3067
|
+
out.push(` ${line}`);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
out.push("");
|
|
2815
3071
|
}
|
|
2816
|
-
|
|
3072
|
+
return out;
|
|
3073
|
+
};
|
|
3074
|
+
lines.push("## Compatibility Issues");
|
|
3075
|
+
lines.push("");
|
|
3076
|
+
lines.push("### Summary");
|
|
3077
|
+
lines.push("");
|
|
3078
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
|
|
3079
|
+
lines.push("");
|
|
3080
|
+
lines.push("### Issues");
|
|
2817
3081
|
lines.push("");
|
|
2818
|
-
lines.push(
|
|
3082
|
+
lines.push(...formatIssueCards(issuesByCategory.compatibility));
|
|
3083
|
+
lines.push("## Change Issues");
|
|
3084
|
+
lines.push("");
|
|
3085
|
+
lines.push("### Summary");
|
|
3086
|
+
lines.push("");
|
|
3087
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.change));
|
|
3088
|
+
lines.push("");
|
|
3089
|
+
lines.push("### Issues");
|
|
3090
|
+
lines.push("");
|
|
3091
|
+
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
3092
|
+
lines.push("## IDs");
|
|
2819
3093
|
lines.push("");
|
|
2820
3094
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2821
3095
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
@@ -2824,7 +3098,7 @@ function formatReportMarkdown(data) {
|
|
|
2824
3098
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2825
3099
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2826
3100
|
lines.push("");
|
|
2827
|
-
lines.push("
|
|
3101
|
+
lines.push("## Traceability");
|
|
2828
3102
|
lines.push("");
|
|
2829
3103
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2830
3104
|
lines.push(
|
|
@@ -2998,7 +3272,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2998
3272
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
2999
3273
|
}
|
|
3000
3274
|
for (const file of specFiles) {
|
|
3001
|
-
const text = await (0,
|
|
3275
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
3002
3276
|
const parsed = parseSpec(text, file);
|
|
3003
3277
|
const specKey = parsed.specId;
|
|
3004
3278
|
if (!specKey) {
|
|
@@ -3039,7 +3313,7 @@ async function collectIds(files) {
|
|
|
3039
3313
|
DB: /* @__PURE__ */ new Set()
|
|
3040
3314
|
};
|
|
3041
3315
|
for (const file of files) {
|
|
3042
|
-
const text = await (0,
|
|
3316
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
3043
3317
|
for (const prefix of ID_PREFIXES2) {
|
|
3044
3318
|
const ids = extractIds(text, prefix);
|
|
3045
3319
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3057,7 +3331,7 @@ async function collectIds(files) {
|
|
|
3057
3331
|
async function collectUpstreamIds(files) {
|
|
3058
3332
|
const ids = /* @__PURE__ */ new Set();
|
|
3059
3333
|
for (const file of files) {
|
|
3060
|
-
const text = await (0,
|
|
3334
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
3061
3335
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3062
3336
|
}
|
|
3063
3337
|
return ids;
|
|
@@ -3078,7 +3352,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3078
3352
|
}
|
|
3079
3353
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3080
3354
|
for (const file of targetFiles) {
|
|
3081
|
-
const text = await (0,
|
|
3355
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
3082
3356
|
if (pattern.test(text)) {
|
|
3083
3357
|
return true;
|
|
3084
3358
|
}
|