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/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 import_promises14 = require("fs/promises");
512
- var import_node_path12 = __toESM(require("path"), 1);
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.8.0".length > 0) {
1236
- return "0.8.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/validators/scenario.ts
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, import_promises11.readFile)(entry.scenarioPath, "utf-8");
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 import_promises12 = require("fs/promises");
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, import_promises12.readFile)(entry.specPath, "utf-8");
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 import_promises13 = require("fs/promises");
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, import_promises13.readFile)(file, "utf-8");
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, import_promises13.readFile)(file, "utf-8");
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, import_promises13.readFile)(file, "utf-8");
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 = import_node_path12.default.resolve(root);
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 = import_node_path12.default.join(contractsRoot, "api");
2621
- const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
2622
- const dbRoot = import_node_path12.default.join(contractsRoot, "db");
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
- lines.push("## Summary");
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("## Findings");
2754
- lines.push("");
2755
- lines.push("### Issues (by code)");
2973
+ lines.push("### Next Actions");
2756
2974
  lines.push("");
2757
- const severityOrder = {
2758
- error: 0,
2759
- warning: 1,
2760
- info: 2
2761
- };
2762
- const issueKeyToCount = /* @__PURE__ */ new Map();
2763
- for (const issue7 of data.issues) {
2764
- const key = `${issue7.severity}|${issue7.code}`;
2765
- const current = issueKeyToCount.get(key);
2766
- if (current) {
2767
- current.count += 1;
2768
- continue;
2769
- }
2770
- issueKeyToCount.set(key, {
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
- ...formatMarkdownTable(["Severity", "Code", "Count"], issueSummaryRows)
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("### Issues (list)");
2996
+ lines.push("### Index");
2791
2997
  lines.push("");
2792
- if (data.issues.length === 0) {
2793
- lines.push("- (none)");
2794
- } else {
2795
- const sortedIssues = [...data.issues].sort((a, b) => {
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
- for (const item of sortedIssues) {
2810
- const location = item.file ? ` (${item.file})` : "";
2811
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
2812
- lines.push(
2813
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
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("### IDs");
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("### Traceability");
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, import_promises14.readFile)(file, "utf-8");
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, import_promises14.readFile)(file, "utf-8");
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, import_promises14.readFile)(file, "utf-8");
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, import_promises14.readFile)(file, "utf-8");
3355
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
3082
3356
  if (pattern.test(text)) {
3083
3357
  return true;
3084
3358
  }