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.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 readFile11 } from "fs/promises";
455
- import path12 from "path";
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.8.0".length > 0) {
1183
- return "0.8.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/validators/scenario.ts
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 readFile8(entry.scenarioPath, "utf-8");
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 readFile9 } from "fs/promises";
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 readFile9(entry.specPath, "utf-8");
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 readFile10 } from "fs/promises";
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 readFile10(file, "utf-8");
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 readFile10(file, "utf-8");
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 readFile10(file, "utf-8");
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 = path12.resolve(root);
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 = path12.join(contractsRoot, "api");
2568
- const uiRoot = path12.join(contractsRoot, "ui");
2569
- const dbRoot = path12.join(contractsRoot, "db");
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
- lines.push("## Summary");
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("## Findings");
2701
- lines.push("");
2702
- lines.push("### Issues (by code)");
2920
+ lines.push("### Next Actions");
2703
2921
  lines.push("");
2704
- const severityOrder = {
2705
- error: 0,
2706
- warning: 1,
2707
- info: 2
2708
- };
2709
- const issueKeyToCount = /* @__PURE__ */ new Map();
2710
- for (const issue7 of data.issues) {
2711
- const key = `${issue7.severity}|${issue7.code}`;
2712
- const current = issueKeyToCount.get(key);
2713
- if (current) {
2714
- current.count += 1;
2715
- continue;
2716
- }
2717
- issueKeyToCount.set(key, {
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
- ...formatMarkdownTable(["Severity", "Code", "Count"], issueSummaryRows)
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("### Issues (list)");
2943
+ lines.push("### Index");
2738
2944
  lines.push("");
2739
- if (data.issues.length === 0) {
2740
- lines.push("- (none)");
2741
- } else {
2742
- const sortedIssues = [...data.issues].sort((a, b) => {
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
- for (const item of sortedIssues) {
2757
- const location = item.file ? ` (${item.file})` : "";
2758
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
2759
- lines.push(
2760
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
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("### IDs");
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("### Traceability");
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 readFile11(file, "utf-8");
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 readFile11(file, "utf-8");
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 readFile11(file, "utf-8");
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 readFile11(file, "utf-8");
3302
+ const text = await readFile12(file, "utf-8");
3029
3303
  if (pattern.test(text)) {
3030
3304
  return true;
3031
3305
  }