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.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.7.2".length > 0) {
1183
- return "0.7.2";
1183
+ if ("0.8.1".length > 0) {
1184
+ return "0.8.1";
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
  }
@@ -1551,7 +1556,7 @@ async function validateDeltas(root, config) {
1551
1556
  issues.push(
1552
1557
  issue2(
1553
1558
  "QFAI-DELTA-002",
1554
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
1559
+ "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",
1555
1560
  "error",
1556
1561
  deltaPath,
1557
1562
  "delta.section"
@@ -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) {
@@ -2094,7 +2271,7 @@ async function validateTraceability(root, config) {
2094
2271
  issues.push(
2095
2272
  issue6(
2096
2273
  "QFAI-TRACE-020",
2097
- "Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2274
+ "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`",
2098
2275
  "error",
2099
2276
  file,
2100
2277
  "traceability.specContractRefRequired"
@@ -2105,7 +2282,7 @@ async function validateTraceability(root, config) {
2105
2282
  issues.push(
2106
2283
  issue6(
2107
2284
  "QFAI-TRACE-023",
2108
- "Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2285
+ "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",
2109
2286
  "error",
2110
2287
  file,
2111
2288
  "traceability.specContractRefFormat"
@@ -2118,7 +2295,7 @@ async function validateTraceability(root, config) {
2118
2295
  "QFAI-TRACE-021",
2119
2296
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2120
2297
  ", "
2121
- )}`,
2298
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2122
2299
  "error",
2123
2300
  file,
2124
2301
  "traceability.specContractRefFormat",
@@ -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
@@ -2158,7 +2335,7 @@ async function validateTraceability(root, config) {
2158
2335
  issues.push(
2159
2336
  issue6(
2160
2337
  "QFAI-TRACE-031",
2161
- "Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
2338
+ "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`",
2162
2339
  "error",
2163
2340
  file,
2164
2341
  "traceability.scenarioContractRefRequired"
@@ -2169,7 +2346,7 @@ async function validateTraceability(root, config) {
2169
2346
  issues.push(
2170
2347
  issue6(
2171
2348
  "QFAI-TRACE-033",
2172
- "Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
2349
+ "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",
2173
2350
  "error",
2174
2351
  file,
2175
2352
  "traceability.scenarioContractRefFormat"
@@ -2182,7 +2359,7 @@ async function validateTraceability(root, config) {
2182
2359
  "QFAI-TRACE-032",
2183
2360
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2184
2361
  ", "
2185
- )}`,
2362
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2186
2363
  "error",
2187
2364
  file,
2188
2365
  "traceability.scenarioContractRefFormat",
@@ -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("## \u6982\u8981");
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,10 +2902,141 @@ 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}`
2912
+ );
2913
+ lines.push(
2914
+ `- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
2915
+ );
2916
+ lines.push(
2917
+ `- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
2692
2918
  );
2693
2919
  lines.push("");
2694
- lines.push("## ID\u96C6\u8A08");
2920
+ lines.push("### Next Actions");
2921
+ lines.push("");
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
+ );
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");
2938
+ lines.push(
2939
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
2940
+ );
2941
+ }
2942
+ lines.push("");
2943
+ lines.push("### Index");
2944
+ lines.push("");
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) => {
2979
+ const sa = severityOrder[a.severity] ?? 999;
2980
+ const sb = severityOrder[b.severity] ?? 999;
2981
+ if (sa !== sb) return sa - sb;
2982
+ const code = a.code.localeCompare(b.code);
2983
+ if (code !== 0) return code;
2984
+ const fileA = a.file ?? "";
2985
+ const fileB = b.file ?? "";
2986
+ const file = fileA.localeCompare(fileB);
2987
+ if (file !== 0) return file;
2988
+ const lineA = a.loc?.line ?? 0;
2989
+ const lineB = b.loc?.line ?? 0;
2990
+ return lineA - lineB;
2991
+ });
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}`
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("");
3018
+ }
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");
3028
+ 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");
2695
3040
  lines.push("");
2696
3041
  lines.push(formatIdLine("SPEC", data.ids.spec));
2697
3042
  lines.push(formatIdLine("BR", data.ids.br));
@@ -2700,14 +3045,14 @@ function formatReportMarkdown(data) {
2700
3045
  lines.push(formatIdLine("API", data.ids.api));
2701
3046
  lines.push(formatIdLine("DB", data.ids.db));
2702
3047
  lines.push("");
2703
- lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
3048
+ lines.push("## Traceability");
2704
3049
  lines.push("");
2705
3050
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
2706
3051
  lines.push(
2707
3052
  `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
2708
3053
  );
2709
3054
  lines.push("");
2710
- lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
3055
+ lines.push("### Contract Coverage");
2711
3056
  lines.push("");
2712
3057
  lines.push(`- total: ${data.traceability.contracts.total}`);
2713
3058
  lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
@@ -2716,7 +3061,7 @@ function formatReportMarkdown(data) {
2716
3061
  `- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
2717
3062
  );
2718
3063
  lines.push("");
2719
- lines.push("## \u5951\u7D04\u2192Spec");
3064
+ lines.push("### Contract \u2192 Spec");
2720
3065
  lines.push("");
2721
3066
  const contractToSpecs = data.traceability.contracts.idToSpecs;
2722
3067
  const contractIds = Object.keys(contractToSpecs).sort(
@@ -2735,7 +3080,7 @@ function formatReportMarkdown(data) {
2735
3080
  }
2736
3081
  }
2737
3082
  lines.push("");
2738
- lines.push("## Spec\u2192\u5951\u7D04");
3083
+ lines.push("### Spec \u2192 Contracts");
2739
3084
  lines.push("");
2740
3085
  const specToContracts = data.traceability.specs.specToContracts;
2741
3086
  const specIds = Object.keys(specToContracts).sort(
@@ -2753,7 +3098,7 @@ function formatReportMarkdown(data) {
2753
3098
  lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
2754
3099
  }
2755
3100
  lines.push("");
2756
- lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
3101
+ lines.push("### Specs missing contract-ref");
2757
3102
  lines.push("");
2758
3103
  const missingRefSpecs = data.traceability.specs.missingRefSpecs;
2759
3104
  if (missingRefSpecs.length === 0) {
@@ -2764,7 +3109,7 @@ function formatReportMarkdown(data) {
2764
3109
  }
2765
3110
  }
2766
3111
  lines.push("");
2767
- lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
3112
+ lines.push("### SC coverage");
2768
3113
  lines.push("");
2769
3114
  lines.push(`- total: ${data.traceability.sc.total}`);
2770
3115
  lines.push(`- covered: ${data.traceability.sc.covered}`);
@@ -2794,7 +3139,7 @@ function formatReportMarkdown(data) {
2794
3139
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
2795
3140
  }
2796
3141
  lines.push("");
2797
- lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
3142
+ lines.push("### SC \u2192 referenced tests");
2798
3143
  lines.push("");
2799
3144
  const scRefs = data.traceability.sc.refs;
2800
3145
  const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
@@ -2811,7 +3156,7 @@ function formatReportMarkdown(data) {
2811
3156
  }
2812
3157
  }
2813
3158
  lines.push("");
2814
- lines.push("## Spec:SC=1:1 \u9055\u53CD");
3159
+ lines.push("### Spec:SC=1:1 violations");
2815
3160
  lines.push("");
2816
3161
  const specScIssues = data.issues.filter(
2817
3162
  (item) => item.code === "QFAI-TRACE-012"
@@ -2826,7 +3171,7 @@ function formatReportMarkdown(data) {
2826
3171
  }
2827
3172
  }
2828
3173
  lines.push("");
2829
- lines.push("## Hotspots");
3174
+ lines.push("### Hotspots");
2830
3175
  lines.push("");
2831
3176
  const hotspots = buildHotspots(data.issues);
2832
3177
  if (hotspots.length === 0) {
@@ -2839,35 +3184,28 @@ function formatReportMarkdown(data) {
2839
3184
  }
2840
3185
  }
2841
3186
  lines.push("");
2842
- lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
3187
+ lines.push("## Guidance");
2843
3188
  lines.push("");
2844
- const traceIssues = data.issues.filter(
2845
- (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
3189
+ lines.push(
3190
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
2846
3191
  );
2847
- if (traceIssues.length === 0) {
2848
- lines.push("- (none)");
2849
- } else {
2850
- for (const item of traceIssues) {
2851
- const location = item.file ? ` (${item.file})` : "";
2852
- lines.push(
2853
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}`
2854
- );
2855
- }
2856
- }
2857
- lines.push("");
2858
- lines.push("## \u691C\u8A3C\u7D50\u679C");
2859
- lines.push("");
2860
- if (data.issues.length === 0) {
2861
- lines.push("- (none)");
3192
+ if (data.summary.counts.error > 0) {
3193
+ lines.push("- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A error \u304B\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
3194
+ } else if (data.summary.counts.warning > 0) {
3195
+ lines.push(
3196
+ "- warning \u306E\u6271\u3044\uFF08Hard Gate \u306B\u3059\u308B\u304B\uFF09\u306F\u904B\u7528\u3067\u6C7A\u3081\u3066\u304F\u3060\u3055\u3044\u3002"
3197
+ );
2862
3198
  } else {
2863
- for (const item of data.issues) {
2864
- const location = item.file ? ` (${item.file})` : "";
2865
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
2866
- lines.push(
2867
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
2868
- );
2869
- }
3199
+ lines.push(
3200
+ "- 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"
3201
+ );
2870
3202
  }
3203
+ lines.push(
3204
+ "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3205
+ );
3206
+ lines.push(
3207
+ "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3208
+ );
2871
3209
  return lines.join("\n");
2872
3210
  }
2873
3211
  function formatReportJson(data) {
@@ -2881,7 +3219,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
2881
3219
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
2882
3220
  }
2883
3221
  for (const file of specFiles) {
2884
- const text = await readFile11(file, "utf-8");
3222
+ const text = await readFile12(file, "utf-8");
2885
3223
  const parsed = parseSpec(text, file);
2886
3224
  const specKey = parsed.specId;
2887
3225
  if (!specKey) {
@@ -2922,7 +3260,7 @@ async function collectIds(files) {
2922
3260
  DB: /* @__PURE__ */ new Set()
2923
3261
  };
2924
3262
  for (const file of files) {
2925
- const text = await readFile11(file, "utf-8");
3263
+ const text = await readFile12(file, "utf-8");
2926
3264
  for (const prefix of ID_PREFIXES2) {
2927
3265
  const ids = extractIds(text, prefix);
2928
3266
  ids.forEach((id) => result[prefix].add(id));
@@ -2940,7 +3278,7 @@ async function collectIds(files) {
2940
3278
  async function collectUpstreamIds(files) {
2941
3279
  const ids = /* @__PURE__ */ new Set();
2942
3280
  for (const file of files) {
2943
- const text = await readFile11(file, "utf-8");
3281
+ const text = await readFile12(file, "utf-8");
2944
3282
  extractAllIds(text).forEach((id) => ids.add(id));
2945
3283
  }
2946
3284
  return ids;
@@ -2961,7 +3299,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
2961
3299
  }
2962
3300
  const pattern = buildIdPattern2(Array.from(upstreamIds));
2963
3301
  for (const file of targetFiles) {
2964
- const text = await readFile11(file, "utf-8");
3302
+ const text = await readFile12(file, "utf-8");
2965
3303
  if (pattern.test(text)) {
2966
3304
  return true;
2967
3305
  }