qfai 0.7.2 → 0.8.1

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.7.2".length > 0) {
1236
- return "0.7.2";
1236
+ if ("0.8.1".length > 0) {
1237
+ return "0.8.1";
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
  }
@@ -1604,7 +1609,7 @@ async function validateDeltas(root, config) {
1604
1609
  issues.push(
1605
1610
  issue2(
1606
1611
  "QFAI-DELTA-002",
1607
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1612
+ "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002`## \u5909\u66F4\u533A\u5206` \u3068\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9\uFF08Compatibility / Change/Improvement\uFF09\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1608
1613
  "error",
1609
1614
  deltaPath,
1610
1615
  "delta.section"
@@ -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) {
@@ -2147,7 +2324,7 @@ async function validateTraceability(root, config) {
2147
2324
  issues.push(
2148
2325
  issue6(
2149
2326
  "QFAI-TRACE-020",
2150
- "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2327
+ "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002\u4F8B: `QFAI-CONTRACT-REF: none` \u307E\u305F\u306F `QFAI-CONTRACT-REF: UI-0001`",
2151
2328
  "error",
2152
2329
  file,
2153
2330
  "traceability.specContractRefRequired"
@@ -2158,7 +2335,7 @@ async function validateTraceability(root, config) {
2158
2335
  issues.push(
2159
2336
  issue6(
2160
2337
  "QFAI-TRACE-023",
2161
- "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2338
+ "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002none \u304B \u5951\u7D04ID \u306E\u3069\u3061\u3089\u304B\u4E00\u65B9\u3060\u3051\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2162
2339
  "error",
2163
2340
  file,
2164
2341
  "traceability.specContractRefFormat"
@@ -2171,7 +2348,7 @@ async function validateTraceability(root, config) {
2171
2348
  "QFAI-TRACE-021",
2172
2349
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2173
2350
  ", "
2174
- )}`,
2351
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2175
2352
  "error",
2176
2353
  file,
2177
2354
  "traceability.specContractRefFormat",
@@ -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
@@ -2211,7 +2388,7 @@ async function validateTraceability(root, config) {
2211
2388
  issues.push(
2212
2389
  issue6(
2213
2390
  "QFAI-TRACE-031",
2214
- "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2391
+ "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002\u4F8B: `# QFAI-CONTRACT-REF: none` \u307E\u305F\u306F `# QFAI-CONTRACT-REF: UI-0001`",
2215
2392
  "error",
2216
2393
  file,
2217
2394
  "traceability.scenarioContractRefRequired"
@@ -2222,7 +2399,7 @@ async function validateTraceability(root, config) {
2222
2399
  issues.push(
2223
2400
  issue6(
2224
2401
  "QFAI-TRACE-033",
2225
- "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2402
+ "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002none \u304B \u5951\u7D04ID \u306E\u3069\u3061\u3089\u304B\u4E00\u65B9\u3060\u3051\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2226
2403
  "error",
2227
2404
  file,
2228
2405
  "traceability.scenarioContractRefFormat"
@@ -2235,7 +2412,7 @@ async function validateTraceability(root, config) {
2235
2412
  "QFAI-TRACE-032",
2236
2413
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2237
2414
  ", "
2238
- )}`,
2415
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2239
2416
  "error",
2240
2417
  file,
2241
2418
  "traceability.scenarioContractRefFormat",
@@ -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("## \u6982\u8981");
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,10 +2955,141 @@ 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}`
2965
+ );
2966
+ lines.push(
2967
+ `- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
2968
+ );
2969
+ lines.push(
2970
+ `- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
2745
2971
  );
2746
2972
  lines.push("");
2747
- lines.push("## ID\u96C6\u8A08");
2973
+ lines.push("### Next Actions");
2974
+ lines.push("");
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
+ );
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");
2991
+ lines.push(
2992
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
2993
+ );
2994
+ }
2995
+ lines.push("");
2996
+ lines.push("### Index");
2997
+ lines.push("");
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) => {
3032
+ const sa = severityOrder[a.severity] ?? 999;
3033
+ const sb = severityOrder[b.severity] ?? 999;
3034
+ if (sa !== sb) return sa - sb;
3035
+ const code = a.code.localeCompare(b.code);
3036
+ if (code !== 0) return code;
3037
+ const fileA = a.file ?? "";
3038
+ const fileB = b.file ?? "";
3039
+ const file = fileA.localeCompare(fileB);
3040
+ if (file !== 0) return file;
3041
+ const lineA = a.loc?.line ?? 0;
3042
+ const lineB = b.loc?.line ?? 0;
3043
+ return lineA - lineB;
3044
+ });
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}`
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("");
3071
+ }
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");
3081
+ 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");
2748
3093
  lines.push("");
2749
3094
  lines.push(formatIdLine("SPEC", data.ids.spec));
2750
3095
  lines.push(formatIdLine("BR", data.ids.br));
@@ -2753,14 +3098,14 @@ function formatReportMarkdown(data) {
2753
3098
  lines.push(formatIdLine("API", data.ids.api));
2754
3099
  lines.push(formatIdLine("DB", data.ids.db));
2755
3100
  lines.push("");
2756
- lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
3101
+ lines.push("## Traceability");
2757
3102
  lines.push("");
2758
3103
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2759
3104
  lines.push(
2760
3105
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2761
3106
  );
2762
3107
  lines.push("");
2763
- lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
3108
+ lines.push("### Contract Coverage");
2764
3109
  lines.push("");
2765
3110
  lines.push(`- total: ${data.traceability.contracts.total}`);
2766
3111
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
@@ -2769,7 +3114,7 @@ function formatReportMarkdown(data) {
2769
3114
  `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2770
3115
  );
2771
3116
  lines.push("");
2772
- lines.push("## \u5951\u7D04\u2192Spec");
3117
+ lines.push("### Contract \u2192 Spec");
2773
3118
  lines.push("");
2774
3119
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2775
3120
  const contractIds = Object.keys(contractToSpecs).sort(
@@ -2788,7 +3133,7 @@ function formatReportMarkdown(data) {
2788
3133
  }
2789
3134
  }
2790
3135
  lines.push("");
2791
- lines.push("## Spec\u2192\u5951\u7D04");
3136
+ lines.push("### Spec \u2192 Contracts");
2792
3137
  lines.push("");
2793
3138
  const specToContracts = data.traceability.specs.specToContracts;
2794
3139
  const specIds = Object.keys(specToContracts).sort(
@@ -2806,7 +3151,7 @@ function formatReportMarkdown(data) {
2806
3151
  lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2807
3152
  }
2808
3153
  lines.push("");
2809
- lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
3154
+ lines.push("### Specs missing contract-ref");
2810
3155
  lines.push("");
2811
3156
  const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2812
3157
  if (missingRefSpecs.length === 0) {
@@ -2817,7 +3162,7 @@ function formatReportMarkdown(data) {
2817
3162
  }
2818
3163
  }
2819
3164
  lines.push("");
2820
- lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
3165
+ lines.push("### SC coverage");
2821
3166
  lines.push("");
2822
3167
  lines.push(`- total: ${data.traceability.sc.total}`);
2823
3168
  lines.push(`- covered: ${data.traceability.sc.covered}`);
@@ -2847,7 +3192,7 @@ function formatReportMarkdown(data) {
2847
3192
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2848
3193
  }
2849
3194
  lines.push("");
2850
- lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
3195
+ lines.push("### SC \u2192 referenced tests");
2851
3196
  lines.push("");
2852
3197
  const scRefs = data.traceability.sc.refs;
2853
3198
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
@@ -2864,7 +3209,7 @@ function formatReportMarkdown(data) {
2864
3209
  }
2865
3210
  }
2866
3211
  lines.push("");
2867
- lines.push("## Spec:SC=1:1 \u9055\u53CD");
3212
+ lines.push("### Spec:SC=1:1 violations");
2868
3213
  lines.push("");
2869
3214
  const specScIssues = data.issues.filter(
2870
3215
  (item) => item.code === "QFAI-TRACE-012"
@@ -2879,7 +3224,7 @@ function formatReportMarkdown(data) {
2879
3224
  }
2880
3225
  }
2881
3226
  lines.push("");
2882
- lines.push("## Hotspots");
3227
+ lines.push("### Hotspots");
2883
3228
  lines.push("");
2884
3229
  const hotspots = buildHotspots(data.issues);
2885
3230
  if (hotspots.length === 0) {
@@ -2892,35 +3237,28 @@ function formatReportMarkdown(data) {
2892
3237
  }
2893
3238
  }
2894
3239
  lines.push("");
2895
- lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
3240
+ lines.push("## Guidance");
2896
3241
  lines.push("");
2897
- const traceIssues = data.issues.filter(
2898
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
3242
+ lines.push(
3243
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
2899
3244
  );
2900
- if (traceIssues.length === 0) {
2901
- lines.push("- (none)");
2902
- } else {
2903
- for (const item of traceIssues) {
2904
- const location = item.file ? ` (${item.file})` : "";
2905
- lines.push(
2906
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}`
2907
- );
2908
- }
2909
- }
2910
- lines.push("");
2911
- lines.push("## \u691C\u8A3C\u7D50\u679C");
2912
- lines.push("");
2913
- if (data.issues.length === 0) {
2914
- lines.push("- (none)");
3245
+ if (data.summary.counts.error > 0) {
3246
+ lines.push("- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A error \u304B\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
3247
+ } else if (data.summary.counts.warning > 0) {
3248
+ lines.push(
3249
+ "- warning \u306E\u6271\u3044\uFF08Hard Gate \u306B\u3059\u308B\u304B\uFF09\u306F\u904B\u7528\u3067\u6C7A\u3081\u3066\u304F\u3060\u3055\u3044\u3002"
3250
+ );
2915
3251
  } else {
2916
- for (const item of data.issues) {
2917
- const location = item.file ? ` (${item.file})` : "";
2918
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
2919
- lines.push(
2920
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
2921
- );
2922
- }
3252
+ lines.push(
3253
+ "- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3254
+ );
2923
3255
  }
3256
+ lines.push(
3257
+ "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3258
+ );
3259
+ lines.push(
3260
+ "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3261
+ );
2924
3262
  return lines.join("\n");
2925
3263
  }
2926
3264
  function formatReportJson(data) {
@@ -2934,7 +3272,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
2934
3272
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
2935
3273
  }
2936
3274
  for (const file of specFiles) {
2937
- const text = await (0, import_promises14.readFile)(file, "utf-8");
3275
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2938
3276
  const parsed = parseSpec(text, file);
2939
3277
  const specKey = parsed.specId;
2940
3278
  if (!specKey) {
@@ -2975,7 +3313,7 @@ async function collectIds(files) {
2975
3313
  DB: /* @__PURE__ */ new Set()
2976
3314
  };
2977
3315
  for (const file of files) {
2978
- const text = await (0, import_promises14.readFile)(file, "utf-8");
3316
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2979
3317
  for (const prefix of ID_PREFIXES2) {
2980
3318
  const ids = extractIds(text, prefix);
2981
3319
  ids.forEach((id) => result[prefix].add(id));
@@ -2993,7 +3331,7 @@ async function collectIds(files) {
2993
3331
  async function collectUpstreamIds(files) {
2994
3332
  const ids = /* @__PURE__ */ new Set();
2995
3333
  for (const file of files) {
2996
- const text = await (0, import_promises14.readFile)(file, "utf-8");
3334
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
2997
3335
  extractAllIds(text).forEach((id) => ids.add(id));
2998
3336
  }
2999
3337
  return ids;
@@ -3014,7 +3352,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3014
3352
  }
3015
3353
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3016
3354
  for (const file of targetFiles) {
3017
- const text = await (0, import_promises14.readFile)(file, "utf-8");
3355
+ const text = await (0, import_promises15.readFile)(file, "utf-8");
3018
3356
  if (pattern.test(text)) {
3019
3357
  return true;
3020
3358
  }