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.mjs
CHANGED
|
@@ -368,6 +368,7 @@ function configIssue(file, message) {
|
|
|
368
368
|
return {
|
|
369
369
|
code: "QFAI_CONFIG_INVALID",
|
|
370
370
|
severity: "error",
|
|
371
|
+
category: "compatibility",
|
|
371
372
|
message,
|
|
372
373
|
file,
|
|
373
374
|
rule: "config.invalid"
|
|
@@ -451,8 +452,8 @@ function isValidId(value, prefix) {
|
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
// src/core/report.ts
|
|
454
|
-
import { readFile as
|
|
455
|
-
import
|
|
455
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
456
|
+
import path14 from "path";
|
|
456
457
|
|
|
457
458
|
// src/core/contractIndex.ts
|
|
458
459
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -1179,8 +1180,8 @@ import { readFile as readFile4 } from "fs/promises";
|
|
|
1179
1180
|
import path7 from "path";
|
|
1180
1181
|
import { fileURLToPath } from "url";
|
|
1181
1182
|
async function resolveToolVersion() {
|
|
1182
|
-
if ("0.
|
|
1183
|
-
return "0.
|
|
1183
|
+
if ("0.9.0".length > 0) {
|
|
1184
|
+
return "0.9.0";
|
|
1184
1185
|
}
|
|
1185
1186
|
try {
|
|
1186
1187
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1491,12 +1492,16 @@ function formatError4(error) {
|
|
|
1491
1492
|
}
|
|
1492
1493
|
return String(error);
|
|
1493
1494
|
}
|
|
1494
|
-
function issue(code, message, severity, file, rule, refs) {
|
|
1495
|
+
function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1495
1496
|
const issue7 = {
|
|
1496
1497
|
code,
|
|
1497
1498
|
severity,
|
|
1499
|
+
category,
|
|
1498
1500
|
message
|
|
1499
1501
|
};
|
|
1502
|
+
if (suggested_action) {
|
|
1503
|
+
issue7.suggested_action = suggested_action;
|
|
1504
|
+
}
|
|
1500
1505
|
if (file) {
|
|
1501
1506
|
issue7.file = file;
|
|
1502
1507
|
}
|
|
@@ -1581,12 +1586,16 @@ function isMissingFileError2(error) {
|
|
|
1581
1586
|
}
|
|
1582
1587
|
return error.code === "ENOENT";
|
|
1583
1588
|
}
|
|
1584
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
1589
|
+
function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
|
|
1585
1590
|
const issue7 = {
|
|
1586
1591
|
code,
|
|
1587
1592
|
severity,
|
|
1593
|
+
category,
|
|
1588
1594
|
message
|
|
1589
1595
|
};
|
|
1596
|
+
if (suggested_action) {
|
|
1597
|
+
issue7.suggested_action = suggested_action;
|
|
1598
|
+
}
|
|
1590
1599
|
if (file) {
|
|
1591
1600
|
issue7.file = file;
|
|
1592
1601
|
}
|
|
@@ -1671,12 +1680,16 @@ function formatFileList(files, root) {
|
|
|
1671
1680
|
return relative.length > 0 ? relative : file;
|
|
1672
1681
|
}).join(", ");
|
|
1673
1682
|
}
|
|
1674
|
-
function issue3(code, message, severity, file, rule, refs) {
|
|
1683
|
+
function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1675
1684
|
const issue7 = {
|
|
1676
1685
|
code,
|
|
1677
1686
|
severity,
|
|
1687
|
+
category,
|
|
1678
1688
|
message
|
|
1679
1689
|
};
|
|
1690
|
+
if (suggested_action) {
|
|
1691
|
+
issue7.suggested_action = suggested_action;
|
|
1692
|
+
}
|
|
1680
1693
|
if (file) {
|
|
1681
1694
|
issue7.file = file;
|
|
1682
1695
|
}
|
|
@@ -1689,8 +1702,164 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1689
1702
|
return issue7;
|
|
1690
1703
|
}
|
|
1691
1704
|
|
|
1692
|
-
// src/core/
|
|
1705
|
+
// src/core/promptsIntegrity.ts
|
|
1693
1706
|
import { readFile as readFile8 } from "fs/promises";
|
|
1707
|
+
import path13 from "path";
|
|
1708
|
+
|
|
1709
|
+
// src/shared/assets.ts
|
|
1710
|
+
import { existsSync } from "fs";
|
|
1711
|
+
import path12 from "path";
|
|
1712
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1713
|
+
function getInitAssetsDir() {
|
|
1714
|
+
const base = import.meta.url;
|
|
1715
|
+
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1716
|
+
const baseDir = path12.dirname(basePath);
|
|
1717
|
+
const candidates = [
|
|
1718
|
+
path12.resolve(baseDir, "../../../assets/init"),
|
|
1719
|
+
path12.resolve(baseDir, "../../assets/init")
|
|
1720
|
+
];
|
|
1721
|
+
for (const candidate of candidates) {
|
|
1722
|
+
if (existsSync(candidate)) {
|
|
1723
|
+
return candidate;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
[
|
|
1728
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1729
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1730
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1731
|
+
].join("\n")
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/core/promptsIntegrity.ts
|
|
1736
|
+
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1737
|
+
const promptsDir = path13.resolve(root, ".qfai", "prompts");
|
|
1738
|
+
let templateDir;
|
|
1739
|
+
try {
|
|
1740
|
+
templateDir = path13.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1741
|
+
} catch {
|
|
1742
|
+
return {
|
|
1743
|
+
status: "skipped_missing_assets",
|
|
1744
|
+
promptsDir,
|
|
1745
|
+
templateDir: "",
|
|
1746
|
+
missing: [],
|
|
1747
|
+
extra: [],
|
|
1748
|
+
changed: []
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
const projectFiles = await collectFiles(promptsDir);
|
|
1752
|
+
if (projectFiles.length === 0) {
|
|
1753
|
+
return {
|
|
1754
|
+
status: "skipped_missing_prompts",
|
|
1755
|
+
promptsDir,
|
|
1756
|
+
templateDir,
|
|
1757
|
+
missing: [],
|
|
1758
|
+
extra: [],
|
|
1759
|
+
changed: []
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
const templateFiles = await collectFiles(templateDir);
|
|
1763
|
+
const templateByRel = /* @__PURE__ */ new Map();
|
|
1764
|
+
for (const abs of templateFiles) {
|
|
1765
|
+
templateByRel.set(toRel(templateDir, abs), abs);
|
|
1766
|
+
}
|
|
1767
|
+
const projectByRel = /* @__PURE__ */ new Map();
|
|
1768
|
+
for (const abs of projectFiles) {
|
|
1769
|
+
projectByRel.set(toRel(promptsDir, abs), abs);
|
|
1770
|
+
}
|
|
1771
|
+
const missing = [];
|
|
1772
|
+
const extra = [];
|
|
1773
|
+
const changed = [];
|
|
1774
|
+
for (const rel of templateByRel.keys()) {
|
|
1775
|
+
if (!projectByRel.has(rel)) {
|
|
1776
|
+
missing.push(rel);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
for (const rel of projectByRel.keys()) {
|
|
1780
|
+
if (!templateByRel.has(rel)) {
|
|
1781
|
+
extra.push(rel);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const common = intersectKeys(templateByRel, projectByRel);
|
|
1785
|
+
for (const rel of common) {
|
|
1786
|
+
const templateAbs = templateByRel.get(rel);
|
|
1787
|
+
const projectAbs = projectByRel.get(rel);
|
|
1788
|
+
if (!templateAbs || !projectAbs) {
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
try {
|
|
1792
|
+
const [a, b] = await Promise.all([
|
|
1793
|
+
readFile8(templateAbs, "utf-8"),
|
|
1794
|
+
readFile8(projectAbs, "utf-8")
|
|
1795
|
+
]);
|
|
1796
|
+
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
1797
|
+
changed.push(rel);
|
|
1798
|
+
}
|
|
1799
|
+
} catch {
|
|
1800
|
+
changed.push(rel);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
1804
|
+
return {
|
|
1805
|
+
status,
|
|
1806
|
+
promptsDir,
|
|
1807
|
+
templateDir,
|
|
1808
|
+
missing: missing.sort(),
|
|
1809
|
+
extra: extra.sort(),
|
|
1810
|
+
changed: changed.sort()
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
function normalizeNewlines(text) {
|
|
1814
|
+
return text.replace(/\r\n/g, "\n");
|
|
1815
|
+
}
|
|
1816
|
+
function toRel(base, abs) {
|
|
1817
|
+
const rel = path13.relative(base, abs);
|
|
1818
|
+
return rel.replace(/[\\/]+/g, "/");
|
|
1819
|
+
}
|
|
1820
|
+
function intersectKeys(a, b) {
|
|
1821
|
+
const out = [];
|
|
1822
|
+
for (const key of a.keys()) {
|
|
1823
|
+
if (b.has(key)) {
|
|
1824
|
+
out.push(key);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return out;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/core/validators/promptsIntegrity.ts
|
|
1831
|
+
async function validatePromptsIntegrity(root) {
|
|
1832
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
1833
|
+
if (diff.status !== "modified") {
|
|
1834
|
+
return [];
|
|
1835
|
+
}
|
|
1836
|
+
const total = diff.missing.length + diff.extra.length + diff.changed.length;
|
|
1837
|
+
const hints = [
|
|
1838
|
+
diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
|
|
1839
|
+
diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
|
|
1840
|
+
diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
|
|
1841
|
+
].filter(Boolean).join(" / ");
|
|
1842
|
+
const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
|
|
1843
|
+
const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
|
|
1844
|
+
return [
|
|
1845
|
+
{
|
|
1846
|
+
code: "QFAI-PROMPTS-001",
|
|
1847
|
+
severity: "error",
|
|
1848
|
+
category: "change",
|
|
1849
|
+
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
1850
|
+
suggested_action: [
|
|
1851
|
+
"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",
|
|
1852
|
+
"\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
1853
|
+
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
1854
|
+
"- \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"
|
|
1855
|
+
].join("\n"),
|
|
1856
|
+
rule: "prompts.integrity"
|
|
1857
|
+
}
|
|
1858
|
+
];
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/core/validators/scenario.ts
|
|
1862
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1694
1863
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1695
1864
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1696
1865
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1716,7 +1885,7 @@ async function validateScenarios(root, config) {
|
|
|
1716
1885
|
for (const entry of entries) {
|
|
1717
1886
|
let text;
|
|
1718
1887
|
try {
|
|
1719
|
-
text = await
|
|
1888
|
+
text = await readFile9(entry.scenarioPath, "utf-8");
|
|
1720
1889
|
} catch (error) {
|
|
1721
1890
|
if (isMissingFileError3(error)) {
|
|
1722
1891
|
issues.push(
|
|
@@ -1861,12 +2030,16 @@ function validateScenarioContent(text, file) {
|
|
|
1861
2030
|
}
|
|
1862
2031
|
return issues;
|
|
1863
2032
|
}
|
|
1864
|
-
function issue4(code, message, severity, file, rule, refs) {
|
|
2033
|
+
function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1865
2034
|
const issue7 = {
|
|
1866
2035
|
code,
|
|
1867
2036
|
severity,
|
|
2037
|
+
category,
|
|
1868
2038
|
message
|
|
1869
2039
|
};
|
|
2040
|
+
if (suggested_action) {
|
|
2041
|
+
issue7.suggested_action = suggested_action;
|
|
2042
|
+
}
|
|
1870
2043
|
if (file) {
|
|
1871
2044
|
issue7.file = file;
|
|
1872
2045
|
}
|
|
@@ -1886,7 +2059,7 @@ function isMissingFileError3(error) {
|
|
|
1886
2059
|
}
|
|
1887
2060
|
|
|
1888
2061
|
// src/core/validators/spec.ts
|
|
1889
|
-
import { readFile as
|
|
2062
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1890
2063
|
async function validateSpecs(root, config) {
|
|
1891
2064
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1892
2065
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1907,7 +2080,7 @@ async function validateSpecs(root, config) {
|
|
|
1907
2080
|
for (const entry of entries) {
|
|
1908
2081
|
let text;
|
|
1909
2082
|
try {
|
|
1910
|
-
text = await
|
|
2083
|
+
text = await readFile10(entry.specPath, "utf-8");
|
|
1911
2084
|
} catch (error) {
|
|
1912
2085
|
if (isMissingFileError4(error)) {
|
|
1913
2086
|
issues.push(
|
|
@@ -2031,12 +2204,16 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2031
2204
|
}
|
|
2032
2205
|
return issues;
|
|
2033
2206
|
}
|
|
2034
|
-
function issue5(code, message, severity, file, rule, refs) {
|
|
2207
|
+
function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2035
2208
|
const issue7 = {
|
|
2036
2209
|
code,
|
|
2037
2210
|
severity,
|
|
2211
|
+
category,
|
|
2038
2212
|
message
|
|
2039
2213
|
};
|
|
2214
|
+
if (suggested_action) {
|
|
2215
|
+
issue7.suggested_action = suggested_action;
|
|
2216
|
+
}
|
|
2040
2217
|
if (file) {
|
|
2041
2218
|
issue7.file = file;
|
|
2042
2219
|
}
|
|
@@ -2056,7 +2233,7 @@ function isMissingFileError4(error) {
|
|
|
2056
2233
|
}
|
|
2057
2234
|
|
|
2058
2235
|
// src/core/validators/traceability.ts
|
|
2059
|
-
import { readFile as
|
|
2236
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2060
2237
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2061
2238
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2062
2239
|
async function validateTraceability(root, config) {
|
|
@@ -2076,7 +2253,7 @@ async function validateTraceability(root, config) {
|
|
|
2076
2253
|
const contractIndex = await buildContractIndex(root, config);
|
|
2077
2254
|
const contractIds = contractIndex.ids;
|
|
2078
2255
|
for (const file of specFiles) {
|
|
2079
|
-
const text = await
|
|
2256
|
+
const text = await readFile11(file, "utf-8");
|
|
2080
2257
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2081
2258
|
const parsed = parseSpec(text, file);
|
|
2082
2259
|
if (parsed.specId) {
|
|
@@ -2149,7 +2326,7 @@ async function validateTraceability(root, config) {
|
|
|
2149
2326
|
}
|
|
2150
2327
|
}
|
|
2151
2328
|
for (const file of scenarioFiles) {
|
|
2152
|
-
const text = await
|
|
2329
|
+
const text = await readFile11(file, "utf-8");
|
|
2153
2330
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2154
2331
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2155
2332
|
allowCommentPrefix: true
|
|
@@ -2471,7 +2648,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2471
2648
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2472
2649
|
let found = false;
|
|
2473
2650
|
for (const file of targetFiles) {
|
|
2474
|
-
const text = await
|
|
2651
|
+
const text = await readFile11(file, "utf-8");
|
|
2475
2652
|
if (pattern.test(text)) {
|
|
2476
2653
|
found = true;
|
|
2477
2654
|
break;
|
|
@@ -2494,12 +2671,16 @@ function buildIdPattern(ids) {
|
|
|
2494
2671
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2495
2672
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
2496
2673
|
}
|
|
2497
|
-
function issue6(code, message, severity, file, rule, refs) {
|
|
2674
|
+
function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2498
2675
|
const issue7 = {
|
|
2499
2676
|
code,
|
|
2500
2677
|
severity,
|
|
2678
|
+
category,
|
|
2501
2679
|
message
|
|
2502
2680
|
};
|
|
2681
|
+
if (suggested_action) {
|
|
2682
|
+
issue7.suggested_action = suggested_action;
|
|
2683
|
+
}
|
|
2503
2684
|
if (file) {
|
|
2504
2685
|
issue7.file = file;
|
|
2505
2686
|
}
|
|
@@ -2518,6 +2699,7 @@ async function validateProject(root, configResult) {
|
|
|
2518
2699
|
const { config, issues: configIssues } = resolved;
|
|
2519
2700
|
const issues = [
|
|
2520
2701
|
...configIssues,
|
|
2702
|
+
...await validatePromptsIntegrity(root),
|
|
2521
2703
|
...await validateSpecs(root, config),
|
|
2522
2704
|
...await validateDeltas(root, config),
|
|
2523
2705
|
...await validateScenarios(root, config),
|
|
@@ -2558,15 +2740,15 @@ function countIssues(issues) {
|
|
|
2558
2740
|
// src/core/report.ts
|
|
2559
2741
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2560
2742
|
async function createReportData(root, validation, configResult) {
|
|
2561
|
-
const resolvedRoot =
|
|
2743
|
+
const resolvedRoot = path14.resolve(root);
|
|
2562
2744
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2563
2745
|
const config = resolved.config;
|
|
2564
2746
|
const configPath = resolved.configPath;
|
|
2565
2747
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2566
2748
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2567
|
-
const apiRoot =
|
|
2568
|
-
const uiRoot =
|
|
2569
|
-
const dbRoot =
|
|
2749
|
+
const apiRoot = path14.join(contractsRoot, "api");
|
|
2750
|
+
const uiRoot = path14.join(contractsRoot, "ui");
|
|
2751
|
+
const dbRoot = path14.join(contractsRoot, "db");
|
|
2570
2752
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2571
2753
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2572
2754
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2680,7 +2862,39 @@ function formatReportMarkdown(data) {
|
|
|
2680
2862
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2681
2863
|
lines.push(`- \u7248: ${data.version}`);
|
|
2682
2864
|
lines.push("");
|
|
2683
|
-
|
|
2865
|
+
const severityOrder = {
|
|
2866
|
+
error: 0,
|
|
2867
|
+
warning: 1,
|
|
2868
|
+
info: 2
|
|
2869
|
+
};
|
|
2870
|
+
const categoryOrder = {
|
|
2871
|
+
compatibility: 0,
|
|
2872
|
+
change: 1
|
|
2873
|
+
};
|
|
2874
|
+
const issuesByCategory = {
|
|
2875
|
+
compatibility: [],
|
|
2876
|
+
change: []
|
|
2877
|
+
};
|
|
2878
|
+
for (const issue7 of data.issues) {
|
|
2879
|
+
const cat = issue7.category;
|
|
2880
|
+
if (cat === "change") {
|
|
2881
|
+
issuesByCategory.change.push(issue7);
|
|
2882
|
+
} else {
|
|
2883
|
+
issuesByCategory.compatibility.push(issue7);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const countIssuesBySeverity = (issues) => issues.reduce(
|
|
2887
|
+
(acc, i) => {
|
|
2888
|
+
acc[i.severity] += 1;
|
|
2889
|
+
return acc;
|
|
2890
|
+
},
|
|
2891
|
+
{ info: 0, warning: 0, error: 0 }
|
|
2892
|
+
);
|
|
2893
|
+
const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
|
|
2894
|
+
const changeCounts = countIssuesBySeverity(issuesByCategory.change);
|
|
2895
|
+
lines.push("## Dashboard");
|
|
2896
|
+
lines.push("");
|
|
2897
|
+
lines.push("### Summary");
|
|
2684
2898
|
lines.push("");
|
|
2685
2899
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2686
2900
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
@@ -2688,7 +2902,13 @@ function formatReportMarkdown(data) {
|
|
|
2688
2902
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2689
2903
|
);
|
|
2690
2904
|
lines.push(
|
|
2691
|
-
`- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
2905
|
+
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
2906
|
+
);
|
|
2907
|
+
lines.push(
|
|
2908
|
+
`- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
|
|
2909
|
+
);
|
|
2910
|
+
lines.push(
|
|
2911
|
+
`- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
|
|
2692
2912
|
);
|
|
2693
2913
|
lines.push(
|
|
2694
2914
|
`- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
|
|
@@ -2697,49 +2917,65 @@ function formatReportMarkdown(data) {
|
|
|
2697
2917
|
`- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
|
|
2698
2918
|
);
|
|
2699
2919
|
lines.push("");
|
|
2700
|
-
lines.push("
|
|
2701
|
-
lines.push("");
|
|
2702
|
-
lines.push("### Issues (by code)");
|
|
2920
|
+
lines.push("### Next Actions");
|
|
2703
2921
|
lines.push("");
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
severity: issue7.severity,
|
|
2719
|
-
code: issue7.code,
|
|
2720
|
-
count: 1
|
|
2721
|
-
});
|
|
2722
|
-
}
|
|
2723
|
-
const issueSummaryRows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
2724
|
-
const sa = severityOrder[a.severity] ?? 999;
|
|
2725
|
-
const sb = severityOrder[b.severity] ?? 999;
|
|
2726
|
-
if (sa !== sb) return sa - sb;
|
|
2727
|
-
return a.code.localeCompare(b.code);
|
|
2728
|
-
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
2729
|
-
if (issueSummaryRows.length === 0) {
|
|
2730
|
-
lines.push("- (none)");
|
|
2922
|
+
if (data.summary.counts.error > 0) {
|
|
2923
|
+
lines.push(
|
|
2924
|
+
"- 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"
|
|
2925
|
+
);
|
|
2926
|
+
lines.push(
|
|
2927
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
2928
|
+
);
|
|
2929
|
+
} else if (data.summary.counts.warning > 0) {
|
|
2930
|
+
lines.push(
|
|
2931
|
+
"- 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"
|
|
2932
|
+
);
|
|
2933
|
+
lines.push(
|
|
2934
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
2935
|
+
);
|
|
2731
2936
|
} else {
|
|
2937
|
+
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");
|
|
2732
2938
|
lines.push(
|
|
2733
|
-
|
|
2939
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
|
|
2734
2940
|
);
|
|
2735
2941
|
}
|
|
2736
2942
|
lines.push("");
|
|
2737
|
-
lines.push("###
|
|
2943
|
+
lines.push("### Index");
|
|
2738
2944
|
lines.push("");
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2945
|
+
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
2946
|
+
lines.push("- [Change Issues](#change-issues)");
|
|
2947
|
+
lines.push("- [IDs](#ids)");
|
|
2948
|
+
lines.push("- [Traceability](#traceability)");
|
|
2949
|
+
lines.push("");
|
|
2950
|
+
const formatIssueSummaryTable = (issues) => {
|
|
2951
|
+
const issueKeyToCount = /* @__PURE__ */ new Map();
|
|
2952
|
+
for (const issue7 of issues) {
|
|
2953
|
+
const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
|
|
2954
|
+
const current = issueKeyToCount.get(key);
|
|
2955
|
+
if (current) {
|
|
2956
|
+
current.count += 1;
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
issueKeyToCount.set(key, {
|
|
2960
|
+
category: issue7.category,
|
|
2961
|
+
severity: issue7.severity,
|
|
2962
|
+
code: issue7.code,
|
|
2963
|
+
count: 1
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
2967
|
+
const ca = categoryOrder[a.category] ?? 999;
|
|
2968
|
+
const cb = categoryOrder[b.category] ?? 999;
|
|
2969
|
+
if (ca !== cb) return ca - cb;
|
|
2970
|
+
const sa = severityOrder[a.severity] ?? 999;
|
|
2971
|
+
const sb = severityOrder[b.severity] ?? 999;
|
|
2972
|
+
if (sa !== sb) return sa - sb;
|
|
2973
|
+
return a.code.localeCompare(b.code);
|
|
2974
|
+
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
2975
|
+
return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
|
|
2976
|
+
};
|
|
2977
|
+
const formatIssueCards = (issues) => {
|
|
2978
|
+
const sorted = [...issues].sort((a, b) => {
|
|
2743
2979
|
const sa = severityOrder[a.severity] ?? 999;
|
|
2744
2980
|
const sb = severityOrder[b.severity] ?? 999;
|
|
2745
2981
|
if (sa !== sb) return sa - sb;
|
|
@@ -2753,16 +2989,54 @@ function formatReportMarkdown(data) {
|
|
|
2753
2989
|
const lineB = b.loc?.line ?? 0;
|
|
2754
2990
|
return lineA - lineB;
|
|
2755
2991
|
});
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2992
|
+
if (sorted.length === 0) {
|
|
2993
|
+
return ["- (none)"];
|
|
2994
|
+
}
|
|
2995
|
+
const out = [];
|
|
2996
|
+
for (const item of sorted) {
|
|
2997
|
+
out.push(
|
|
2998
|
+
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
2761
2999
|
);
|
|
3000
|
+
if (item.file) {
|
|
3001
|
+
const loc = item.loc?.line ? `:${item.loc.line}` : "";
|
|
3002
|
+
out.push(`- file: ${item.file}${loc}`);
|
|
3003
|
+
}
|
|
3004
|
+
if (item.rule) {
|
|
3005
|
+
out.push(`- rule: ${item.rule}`);
|
|
3006
|
+
}
|
|
3007
|
+
if (item.refs && item.refs.length > 0) {
|
|
3008
|
+
out.push(`- refs: ${item.refs.join(", ")}`);
|
|
3009
|
+
}
|
|
3010
|
+
if (item.suggested_action) {
|
|
3011
|
+
out.push("- suggested_action:");
|
|
3012
|
+
const actionLines = String(item.suggested_action).split("\n");
|
|
3013
|
+
for (const line of actionLines) {
|
|
3014
|
+
out.push(` ${line}`);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
out.push("");
|
|
2762
3018
|
}
|
|
2763
|
-
|
|
3019
|
+
return out;
|
|
3020
|
+
};
|
|
3021
|
+
lines.push("## Compatibility Issues");
|
|
3022
|
+
lines.push("");
|
|
3023
|
+
lines.push("### Summary");
|
|
3024
|
+
lines.push("");
|
|
3025
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
|
|
3026
|
+
lines.push("");
|
|
3027
|
+
lines.push("### Issues");
|
|
2764
3028
|
lines.push("");
|
|
2765
|
-
lines.push(
|
|
3029
|
+
lines.push(...formatIssueCards(issuesByCategory.compatibility));
|
|
3030
|
+
lines.push("## Change Issues");
|
|
3031
|
+
lines.push("");
|
|
3032
|
+
lines.push("### Summary");
|
|
3033
|
+
lines.push("");
|
|
3034
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.change));
|
|
3035
|
+
lines.push("");
|
|
3036
|
+
lines.push("### Issues");
|
|
3037
|
+
lines.push("");
|
|
3038
|
+
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
3039
|
+
lines.push("## IDs");
|
|
2766
3040
|
lines.push("");
|
|
2767
3041
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2768
3042
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
@@ -2771,7 +3045,7 @@ function formatReportMarkdown(data) {
|
|
|
2771
3045
|
lines.push(formatIdLine("API", data.ids.api));
|
|
2772
3046
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2773
3047
|
lines.push("");
|
|
2774
|
-
lines.push("
|
|
3048
|
+
lines.push("## Traceability");
|
|
2775
3049
|
lines.push("");
|
|
2776
3050
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2777
3051
|
lines.push(
|
|
@@ -2945,7 +3219,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2945
3219
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
2946
3220
|
}
|
|
2947
3221
|
for (const file of specFiles) {
|
|
2948
|
-
const text = await
|
|
3222
|
+
const text = await readFile12(file, "utf-8");
|
|
2949
3223
|
const parsed = parseSpec(text, file);
|
|
2950
3224
|
const specKey = parsed.specId;
|
|
2951
3225
|
if (!specKey) {
|
|
@@ -2986,7 +3260,7 @@ async function collectIds(files) {
|
|
|
2986
3260
|
DB: /* @__PURE__ */ new Set()
|
|
2987
3261
|
};
|
|
2988
3262
|
for (const file of files) {
|
|
2989
|
-
const text = await
|
|
3263
|
+
const text = await readFile12(file, "utf-8");
|
|
2990
3264
|
for (const prefix of ID_PREFIXES2) {
|
|
2991
3265
|
const ids = extractIds(text, prefix);
|
|
2992
3266
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3004,7 +3278,7 @@ async function collectIds(files) {
|
|
|
3004
3278
|
async function collectUpstreamIds(files) {
|
|
3005
3279
|
const ids = /* @__PURE__ */ new Set();
|
|
3006
3280
|
for (const file of files) {
|
|
3007
|
-
const text = await
|
|
3281
|
+
const text = await readFile12(file, "utf-8");
|
|
3008
3282
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3009
3283
|
}
|
|
3010
3284
|
return ids;
|
|
@@ -3025,7 +3299,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3025
3299
|
}
|
|
3026
3300
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3027
3301
|
for (const file of targetFiles) {
|
|
3028
|
-
const text = await
|
|
3302
|
+
const text = await readFile12(file, "utf-8");
|
|
3029
3303
|
if (pattern.test(text)) {
|
|
3030
3304
|
return true;
|
|
3031
3305
|
}
|