qfai 1.0.2 → 1.0.3

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.
@@ -208,11 +208,11 @@ function emitPromptNotFound(promptName, candidates) {
208
208
 
209
209
  // src/cli/commands/doctor.ts
210
210
  import { mkdir, writeFile } from "fs/promises";
211
- import path11 from "path";
211
+ import path12 from "path";
212
212
 
213
213
  // src/core/doctor.ts
214
214
  import { access as access4 } from "fs/promises";
215
- import path10 from "path";
215
+ import path11 from "path";
216
216
 
217
217
  // src/core/config.ts
218
218
  import { access as access2, readFile as readFile2 } from "fs/promises";
@@ -616,6 +616,7 @@ function isRecord(value) {
616
616
 
617
617
  // src/core/discovery.ts
618
618
  import { access as access3 } from "fs/promises";
619
+ import path5 from "path";
619
620
 
620
621
  // src/core/specLayout.ts
621
622
  import { readdir as readdir2 } from "fs/promises";
@@ -663,7 +664,12 @@ async function collectScenarioFiles(specsRoot) {
663
664
  return filterExisting(entries.map((entry) => entry.scenarioPath));
664
665
  }
665
666
  async function collectUiContractFiles(uiRoot) {
666
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
667
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
668
+ return filterByBasenamePrefix(files, "ui-");
669
+ }
670
+ async function collectThemaContractFiles(uiRoot) {
671
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
672
+ return filterByBasenamePrefix(files, "thema-");
667
673
  }
668
674
  async function collectApiContractFiles(apiRoot) {
669
675
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -672,12 +678,13 @@ async function collectDbContractFiles(dbRoot) {
672
678
  return collectFiles(dbRoot, { extensions: [".sql"] });
673
679
  }
674
680
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
675
- const [ui, api, db] = await Promise.all([
681
+ const [ui, thema, api, db] = await Promise.all([
676
682
  collectUiContractFiles(uiRoot),
683
+ collectThemaContractFiles(uiRoot),
677
684
  collectApiContractFiles(apiRoot),
678
685
  collectDbContractFiles(dbRoot)
679
686
  ]);
680
- return { ui, api, db };
687
+ return { ui, thema, api, db };
681
688
  }
682
689
  async function filterExisting(files) {
683
690
  const existing = [];
@@ -696,17 +703,23 @@ async function exists3(target) {
696
703
  return false;
697
704
  }
698
705
  }
706
+ function filterByBasenamePrefix(files, prefix) {
707
+ const lowerPrefix = prefix.toLowerCase();
708
+ return files.filter(
709
+ (file) => path5.basename(file).toLowerCase().startsWith(lowerPrefix)
710
+ );
711
+ }
699
712
 
700
713
  // src/core/paths.ts
701
- import path5 from "path";
714
+ import path6 from "path";
702
715
  function toRelativePath(root, target) {
703
716
  if (!target) {
704
717
  return target;
705
718
  }
706
- if (!path5.isAbsolute(target)) {
719
+ if (!path6.isAbsolute(target)) {
707
720
  return toPosixPath(target);
708
721
  }
709
- const relative = path5.relative(root, target);
722
+ const relative = path6.relative(root, target);
710
723
  if (!relative) {
711
724
  return ".";
712
725
  }
@@ -718,7 +731,7 @@ function toPosixPath(value) {
718
731
 
719
732
  // src/core/traceability.ts
720
733
  import { readFile as readFile3 } from "fs/promises";
721
- import path6 from "path";
734
+ import path7 from "path";
722
735
 
723
736
  // src/core/gherkin/parse.ts
724
737
  import {
@@ -950,7 +963,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
950
963
  };
951
964
  }
952
965
  const normalizedFiles = Array.from(
953
- new Set(scanResult.files.map((file) => path6.normalize(file)))
966
+ new Set(scanResult.files.map((file) => path7.normalize(file)))
954
967
  );
955
968
  for (const file of normalizedFiles) {
956
969
  const text = await readFile3(file, "utf-8");
@@ -1013,19 +1026,19 @@ function formatError3(error2) {
1013
1026
 
1014
1027
  // src/core/promptsIntegrity.ts
1015
1028
  import { readFile as readFile4 } from "fs/promises";
1016
- import path8 from "path";
1029
+ import path9 from "path";
1017
1030
 
1018
1031
  // src/shared/assets.ts
1019
1032
  import { existsSync } from "fs";
1020
- import path7 from "path";
1033
+ import path8 from "path";
1021
1034
  import { fileURLToPath } from "url";
1022
1035
  function getInitAssetsDir() {
1023
1036
  const base = import.meta.url;
1024
1037
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
1025
- const baseDir = path7.dirname(basePath);
1038
+ const baseDir = path8.dirname(basePath);
1026
1039
  const candidates = [
1027
- path7.resolve(baseDir, "../../../assets/init"),
1028
- path7.resolve(baseDir, "../../assets/init")
1040
+ path8.resolve(baseDir, "../../../assets/init"),
1041
+ path8.resolve(baseDir, "../../assets/init")
1029
1042
  ];
1030
1043
  for (const candidate of candidates) {
1031
1044
  if (existsSync(candidate)) {
@@ -1043,10 +1056,10 @@ function getInitAssetsDir() {
1043
1056
 
1044
1057
  // src/core/promptsIntegrity.ts
1045
1058
  async function diffProjectPromptsAgainstInitAssets(root) {
1046
- const promptsDir = path8.resolve(root, ".qfai", "prompts");
1059
+ const promptsDir = path9.resolve(root, ".qfai", "prompts");
1047
1060
  let templateDir;
1048
1061
  try {
1049
- templateDir = path8.join(getInitAssetsDir(), ".qfai", "prompts");
1062
+ templateDir = path9.join(getInitAssetsDir(), ".qfai", "prompts");
1050
1063
  } catch {
1051
1064
  return {
1052
1065
  status: "skipped_missing_assets",
@@ -1123,7 +1136,7 @@ function normalizeNewlines(text) {
1123
1136
  return text.replace(/\r\n/g, "\n");
1124
1137
  }
1125
1138
  function toRel(base, abs) {
1126
- const rel = path8.relative(base, abs);
1139
+ const rel = path9.relative(base, abs);
1127
1140
  return rel.replace(/[\\/]+/g, "/");
1128
1141
  }
1129
1142
  function intersectKeys(a, b) {
@@ -1138,11 +1151,11 @@ function intersectKeys(a, b) {
1138
1151
 
1139
1152
  // src/core/version.ts
1140
1153
  import { readFile as readFile5 } from "fs/promises";
1141
- import path9 from "path";
1154
+ import path10 from "path";
1142
1155
  import { fileURLToPath as fileURLToPath2 } from "url";
1143
1156
  async function resolveToolVersion() {
1144
- if ("1.0.2".length > 0) {
1145
- return "1.0.2";
1157
+ if ("1.0.3".length > 0) {
1158
+ return "1.0.3";
1146
1159
  }
1147
1160
  try {
1148
1161
  const packagePath = resolvePackageJsonPath();
@@ -1157,7 +1170,7 @@ async function resolveToolVersion() {
1157
1170
  function resolvePackageJsonPath() {
1158
1171
  const base = import.meta.url;
1159
1172
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1160
- return path9.resolve(path9.dirname(basePath), "../../package.json");
1173
+ return path10.resolve(path10.dirname(basePath), "../../package.json");
1161
1174
  }
1162
1175
 
1163
1176
  // src/core/doctor.ts
@@ -1183,7 +1196,7 @@ function normalizeGlobs2(values) {
1183
1196
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1184
1197
  }
1185
1198
  async function createDoctorData(options) {
1186
- const startDir = path10.resolve(options.startDir);
1199
+ const startDir = path11.resolve(options.startDir);
1187
1200
  const checks = [];
1188
1201
  const configPath = getConfigPath(startDir);
1189
1202
  const search = options.rootExplicit ? {
@@ -1246,9 +1259,9 @@ async function createDoctorData(options) {
1246
1259
  details: { path: toRelativePath(root, resolved) }
1247
1260
  });
1248
1261
  if (key === "promptsDir") {
1249
- const promptsLocalDir = path10.join(
1250
- path10.dirname(resolved),
1251
- `${path10.basename(resolved)}.local`
1262
+ const promptsLocalDir = path11.join(
1263
+ path11.dirname(resolved),
1264
+ `${path11.basename(resolved)}.local`
1252
1265
  );
1253
1266
  const found = await exists4(promptsLocalDir);
1254
1267
  addCheck(checks, {
@@ -1321,7 +1334,7 @@ async function createDoctorData(options) {
1321
1334
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1322
1335
  details: { specPacks: entries.length, missingFiles }
1323
1336
  });
1324
- const validateJsonAbs = path10.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path10.resolve(root, config.output.validateJsonPath);
1337
+ const validateJsonAbs = path11.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path11.resolve(root, config.output.validateJsonPath);
1325
1338
  const validateJsonExists = await exists4(validateJsonAbs);
1326
1339
  addCheck(checks, {
1327
1340
  id: "output.validateJson",
@@ -1331,8 +1344,8 @@ async function createDoctorData(options) {
1331
1344
  details: { path: toRelativePath(root, validateJsonAbs) }
1332
1345
  });
1333
1346
  const outDirAbs = resolvePath(root, config, "outDir");
1334
- const rel = path10.relative(outDirAbs, validateJsonAbs);
1335
- const inside = rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
1347
+ const rel = path11.relative(outDirAbs, validateJsonAbs);
1348
+ const inside = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
1336
1349
  addCheck(checks, {
1337
1350
  id: "output.pathAlignment",
1338
1351
  severity: inside ? "ok" : "warning",
@@ -1455,12 +1468,12 @@ async function detectOutDirCollisions(root) {
1455
1468
  });
1456
1469
  const configPaths = configScan.files;
1457
1470
  const configRoots = Array.from(
1458
- new Set(configPaths.map((configPath) => path10.dirname(configPath)))
1471
+ new Set(configPaths.map((configPath) => path11.dirname(configPath)))
1459
1472
  ).sort((a, b) => a.localeCompare(b));
1460
1473
  const outDirToRoots = /* @__PURE__ */ new Map();
1461
1474
  for (const configRoot of configRoots) {
1462
1475
  const { config } = await loadConfig(configRoot);
1463
- const outDir = path10.normalize(resolvePath(configRoot, config, "outDir"));
1476
+ const outDir = path11.normalize(resolvePath(configRoot, config, "outDir"));
1464
1477
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1465
1478
  roots.add(configRoot);
1466
1479
  outDirToRoots.set(outDir, roots);
@@ -1486,20 +1499,20 @@ async function detectOutDirCollisions(root) {
1486
1499
  };
1487
1500
  }
1488
1501
  async function findMonorepoRoot(startDir) {
1489
- let current = path10.resolve(startDir);
1502
+ let current = path11.resolve(startDir);
1490
1503
  while (true) {
1491
- const gitPath = path10.join(current, ".git");
1492
- const workspacePath = path10.join(current, "pnpm-workspace.yaml");
1504
+ const gitPath = path11.join(current, ".git");
1505
+ const workspacePath = path11.join(current, "pnpm-workspace.yaml");
1493
1506
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1494
1507
  return current;
1495
1508
  }
1496
- const parent = path10.dirname(current);
1509
+ const parent = path11.dirname(current);
1497
1510
  if (parent === current) {
1498
1511
  break;
1499
1512
  }
1500
1513
  current = parent;
1501
1514
  }
1502
- return path10.resolve(startDir);
1515
+ return path11.resolve(startDir);
1503
1516
  }
1504
1517
 
1505
1518
  // src/cli/lib/logger.ts
@@ -1541,8 +1554,8 @@ async function runDoctor(options) {
1541
1554
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1542
1555
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1543
1556
  if (options.outPath) {
1544
- const outAbs = path11.isAbsolute(options.outPath) ? options.outPath : path11.resolve(process.cwd(), options.outPath);
1545
- await mkdir(path11.dirname(outAbs), { recursive: true });
1557
+ const outAbs = path12.isAbsolute(options.outPath) ? options.outPath : path12.resolve(process.cwd(), options.outPath);
1558
+ await mkdir(path12.dirname(outAbs), { recursive: true });
1546
1559
  await writeFile(outAbs, `${output}
1547
1560
  `, "utf-8");
1548
1561
  info(`doctor: wrote ${outAbs}`);
@@ -1562,11 +1575,11 @@ function shouldFailDoctor(summary, failOn) {
1562
1575
  }
1563
1576
 
1564
1577
  // src/cli/commands/init.ts
1565
- import path13 from "path";
1578
+ import path14 from "path";
1566
1579
 
1567
1580
  // src/cli/lib/fs.ts
1568
1581
  import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1569
- import path12 from "path";
1582
+ import path13 from "path";
1570
1583
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1571
1584
  const files = await collectTemplateFiles(sourceRoot);
1572
1585
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1574,7 +1587,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1574
1587
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1575
1588
  const allFiles = [];
1576
1589
  for (const relPath of relativePaths) {
1577
- const fullPath = path12.join(sourceRoot, relPath);
1590
+ const fullPath = path13.join(sourceRoot, relPath);
1578
1591
  const files = await collectTemplateFiles(fullPath);
1579
1592
  allFiles.push(...files);
1580
1593
  }
@@ -1584,13 +1597,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1584
1597
  const copied = [];
1585
1598
  const skipped = [];
1586
1599
  const conflicts = [];
1587
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1588
- const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1600
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
1601
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
1589
1602
  const isProtectedRelative = (relative) => {
1590
1603
  if (protectPrefixes.length === 0) {
1591
1604
  return false;
1592
1605
  }
1593
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
1606
+ const normalized = relative.replace(/[\\/]+/g, path13.sep);
1594
1607
  return protectPrefixes.some(
1595
1608
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1596
1609
  );
@@ -1599,7 +1612,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1599
1612
  if (excludePrefixes.length === 0) {
1600
1613
  return false;
1601
1614
  }
1602
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
1615
+ const normalized = relative.replace(/[\\/]+/g, path13.sep);
1603
1616
  return excludePrefixes.some(
1604
1617
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1605
1618
  );
@@ -1607,14 +1620,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1607
1620
  const conflictPolicy = options.conflictPolicy ?? "error";
1608
1621
  if (!options.force && conflictPolicy === "error") {
1609
1622
  for (const file of files) {
1610
- const relative = path12.relative(sourceRoot, file);
1623
+ const relative = path13.relative(sourceRoot, file);
1611
1624
  if (isExcludedRelative(relative)) {
1612
1625
  continue;
1613
1626
  }
1614
1627
  if (isProtectedRelative(relative)) {
1615
1628
  continue;
1616
1629
  }
1617
- const dest = path12.join(destRoot, relative);
1630
+ const dest = path13.join(destRoot, relative);
1618
1631
  if (!await shouldWrite(dest, options.force)) {
1619
1632
  conflicts.push(dest);
1620
1633
  }
@@ -1624,18 +1637,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1624
1637
  }
1625
1638
  }
1626
1639
  for (const file of files) {
1627
- const relative = path12.relative(sourceRoot, file);
1640
+ const relative = path13.relative(sourceRoot, file);
1628
1641
  if (isExcludedRelative(relative)) {
1629
1642
  continue;
1630
1643
  }
1631
- const dest = path12.join(destRoot, relative);
1644
+ const dest = path13.join(destRoot, relative);
1632
1645
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1633
1646
  if (!await shouldWrite(dest, forceForThisFile)) {
1634
1647
  skipped.push(dest);
1635
1648
  continue;
1636
1649
  }
1637
1650
  if (!options.dryRun) {
1638
- await mkdir2(path12.dirname(dest), { recursive: true });
1651
+ await mkdir2(path13.dirname(dest), { recursive: true });
1639
1652
  await copyFile(file, dest);
1640
1653
  }
1641
1654
  copied.push(dest);
@@ -1659,7 +1672,7 @@ async function collectTemplateFiles(root) {
1659
1672
  }
1660
1673
  const items = await readdir3(root, { withFileTypes: true });
1661
1674
  for (const item of items) {
1662
- const fullPath = path12.join(root, item.name);
1675
+ const fullPath = path13.join(root, item.name);
1663
1676
  if (item.isDirectory()) {
1664
1677
  const nested = await collectTemplateFiles(fullPath);
1665
1678
  entries.push(...nested);
@@ -1689,10 +1702,10 @@ async function exists5(target) {
1689
1702
  // src/cli/commands/init.ts
1690
1703
  async function runInit(options) {
1691
1704
  const assetsRoot = getInitAssetsDir();
1692
- const rootAssets = path13.join(assetsRoot, "root");
1693
- const qfaiAssets = path13.join(assetsRoot, ".qfai");
1694
- const destRoot = path13.resolve(options.dir);
1695
- const destQfai = path13.join(destRoot, ".qfai");
1705
+ const rootAssets = path14.join(assetsRoot, "root");
1706
+ const qfaiAssets = path14.join(assetsRoot, ".qfai");
1707
+ const destRoot = path14.resolve(options.dir);
1708
+ const destQfai = path14.join(destRoot, ".qfai");
1696
1709
  if (options.force) {
1697
1710
  info(
1698
1711
  "NOTE: --force \u306F .qfai/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
@@ -1740,7 +1753,7 @@ function report(copied, skipped, dryRun, label) {
1740
1753
 
1741
1754
  // src/cli/commands/report.ts
1742
1755
  import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
1743
- import path20 from "path";
1756
+ import path22 from "path";
1744
1757
 
1745
1758
  // src/core/normalize.ts
1746
1759
  function normalizeIssuePaths(root, issues) {
@@ -1781,15 +1794,15 @@ function normalizeValidationResult(root, result) {
1781
1794
 
1782
1795
  // src/core/report.ts
1783
1796
  import { readFile as readFile13 } from "fs/promises";
1784
- import path19 from "path";
1797
+ import path21 from "path";
1785
1798
 
1786
1799
  // src/core/contractIndex.ts
1787
1800
  import { readFile as readFile6 } from "fs/promises";
1788
- import path14 from "path";
1801
+ import path15 from "path";
1789
1802
 
1790
1803
  // src/core/contractsDecl.ts
1791
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1792
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1804
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
1805
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
1793
1806
  function extractDeclaredContractIds(text) {
1794
1807
  const ids = [];
1795
1808
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -1807,20 +1820,22 @@ function stripContractDeclarationLines(text) {
1807
1820
  // src/core/contractIndex.ts
1808
1821
  async function buildContractIndex(root, config) {
1809
1822
  const contractsRoot = resolvePath(root, config, "contractsDir");
1810
- const uiRoot = path14.join(contractsRoot, "ui");
1811
- const apiRoot = path14.join(contractsRoot, "api");
1812
- const dbRoot = path14.join(contractsRoot, "db");
1813
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1823
+ const uiRoot = path15.join(contractsRoot, "ui");
1824
+ const apiRoot = path15.join(contractsRoot, "api");
1825
+ const dbRoot = path15.join(contractsRoot, "db");
1826
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
1814
1827
  collectUiContractFiles(uiRoot),
1828
+ collectThemaContractFiles(uiRoot),
1815
1829
  collectApiContractFiles(apiRoot),
1816
1830
  collectDbContractFiles(dbRoot)
1817
1831
  ]);
1818
1832
  const index = {
1819
1833
  ids: /* @__PURE__ */ new Set(),
1820
1834
  idToFiles: /* @__PURE__ */ new Map(),
1821
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1835
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
1822
1836
  };
1823
1837
  await indexContractFiles(uiFiles, index);
1838
+ await indexContractFiles(themaFiles, index);
1824
1839
  await indexContractFiles(apiFiles, index);
1825
1840
  await indexContractFiles(dbFiles, index);
1826
1841
  return index;
@@ -1839,7 +1854,15 @@ function record(index, id, file) {
1839
1854
  }
1840
1855
 
1841
1856
  // src/core/ids.ts
1842
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1857
+ var ID_PREFIXES = [
1858
+ "SPEC",
1859
+ "BR",
1860
+ "SC",
1861
+ "UI",
1862
+ "API",
1863
+ "DB",
1864
+ "THEMA"
1865
+ ];
1843
1866
  var STRICT_ID_PATTERNS = {
1844
1867
  SPEC: /\bSPEC-\d{4}\b/g,
1845
1868
  BR: /\bBR-\d{4}\b/g,
@@ -1847,6 +1870,7 @@ var STRICT_ID_PATTERNS = {
1847
1870
  UI: /\bUI-\d{4}\b/g,
1848
1871
  API: /\bAPI-\d{4}\b/g,
1849
1872
  DB: /\bDB-\d{4}\b/g,
1873
+ THEMA: /\bTHEMA-\d{3}\b/g,
1850
1874
  ADR: /\bADR-\d{4}\b/g
1851
1875
  };
1852
1876
  var LOOSE_ID_PATTERNS = {
@@ -1856,6 +1880,7 @@ var LOOSE_ID_PATTERNS = {
1856
1880
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1857
1881
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1858
1882
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1883
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
1859
1884
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1860
1885
  };
1861
1886
  function extractIds(text, prefix) {
@@ -1892,7 +1917,7 @@ function isValidId(value, prefix) {
1892
1917
  }
1893
1918
 
1894
1919
  // src/core/parse/contractRefs.ts
1895
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1920
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
1896
1921
  function parseContractRefs(text, options = {}) {
1897
1922
  const linePattern = buildLinePattern(options);
1898
1923
  const lines = [];
@@ -2062,14 +2087,14 @@ function parseSpec(md, file) {
2062
2087
  }
2063
2088
 
2064
2089
  // src/core/validators/contracts.ts
2065
- import { readFile as readFile7 } from "fs/promises";
2066
- import path16 from "path";
2090
+ import { access as access6, readFile as readFile7 } from "fs/promises";
2091
+ import path17 from "path";
2067
2092
 
2068
2093
  // src/core/contracts.ts
2069
- import path15 from "path";
2094
+ import path16 from "path";
2070
2095
  import { parse as parseYaml2 } from "yaml";
2071
2096
  function parseStructuredContract(file, text) {
2072
- const ext = path15.extname(file).toLowerCase();
2097
+ const ext = path16.extname(file).toLowerCase();
2073
2098
  if (ext === ".json") {
2074
2099
  return JSON.parse(text);
2075
2100
  }
@@ -2086,17 +2111,23 @@ var SQL_DANGEROUS_PATTERNS = [
2086
2111
  label: "ALTER TABLE ... DROP"
2087
2112
  }
2088
2113
  ];
2114
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
2089
2115
  async function validateContracts(root, config) {
2090
2116
  const issues = [];
2091
- const contractsRoot = resolvePath(root, config, "contractsDir");
2092
- issues.push(...await validateUiContracts(path16.join(contractsRoot, "ui")));
2093
- issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
2094
- issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
2095
2117
  const contractIndex = await buildContractIndex(root, config);
2118
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2119
+ const uiRoot = path17.join(contractsRoot, "ui");
2120
+ const themaIds = new Set(
2121
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
2122
+ );
2123
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
2124
+ issues.push(...await validateThemaContracts(uiRoot));
2125
+ issues.push(...await validateApiContracts(path17.join(contractsRoot, "api")));
2126
+ issues.push(...await validateDbContracts(path17.join(contractsRoot, "db")));
2096
2127
  issues.push(...validateDuplicateContractIds(contractIndex));
2097
2128
  return issues;
2098
2129
  }
2099
- async function validateUiContracts(uiRoot) {
2130
+ async function validateUiContracts(uiRoot, themaIds) {
2100
2131
  const files = await collectUiContractFiles(uiRoot);
2101
2132
  if (files.length === 0) {
2102
2133
  return [
@@ -2112,6 +2143,22 @@ async function validateUiContracts(uiRoot) {
2112
2143
  const issues = [];
2113
2144
  for (const file of files) {
2114
2145
  const text = await readFile7(file, "utf-8");
2146
+ const declaredIds = extractDeclaredContractIds(text);
2147
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2148
+ let doc = null;
2149
+ try {
2150
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2151
+ } catch (error2) {
2152
+ issues.push(
2153
+ issue(
2154
+ "QFAI-CONTRACT-001",
2155
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2156
+ "error",
2157
+ file,
2158
+ "contracts.ui.parse"
2159
+ )
2160
+ );
2161
+ }
2115
2162
  const invalidIds = extractInvalidIds(text, [
2116
2163
  "SPEC",
2117
2164
  "BR",
@@ -2119,6 +2166,45 @@ async function validateUiContracts(uiRoot) {
2119
2166
  "UI",
2120
2167
  "API",
2121
2168
  "DB",
2169
+ "THEMA",
2170
+ "ADR"
2171
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
2172
+ if (invalidIds.length > 0) {
2173
+ issues.push(
2174
+ issue(
2175
+ "QFAI-ID-002",
2176
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
2177
+ "error",
2178
+ file,
2179
+ "id.format",
2180
+ invalidIds
2181
+ )
2182
+ );
2183
+ }
2184
+ if (doc) {
2185
+ issues.push(
2186
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
2187
+ );
2188
+ }
2189
+ }
2190
+ return issues;
2191
+ }
2192
+ async function validateThemaContracts(uiRoot) {
2193
+ const files = await collectThemaContractFiles(uiRoot);
2194
+ if (files.length === 0) {
2195
+ return [];
2196
+ }
2197
+ const issues = [];
2198
+ for (const file of files) {
2199
+ const text = await readFile7(file, "utf-8");
2200
+ const invalidIds = extractInvalidIds(text, [
2201
+ "SPEC",
2202
+ "BR",
2203
+ "SC",
2204
+ "UI",
2205
+ "API",
2206
+ "DB",
2207
+ "THEMA",
2122
2208
  "ADR"
2123
2209
  ]);
2124
2210
  if (invalidIds.length > 0) {
@@ -2134,17 +2220,95 @@ async function validateUiContracts(uiRoot) {
2134
2220
  );
2135
2221
  }
2136
2222
  const declaredIds = extractDeclaredContractIds(text);
2137
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2223
+ if (declaredIds.length === 0) {
2224
+ issues.push(
2225
+ issue(
2226
+ "QFAI-THEMA-010",
2227
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
2228
+ "error",
2229
+ file,
2230
+ "contracts.thema.declaration"
2231
+ )
2232
+ );
2233
+ continue;
2234
+ }
2235
+ if (declaredIds.length > 1) {
2236
+ issues.push(
2237
+ issue(
2238
+ "QFAI-THEMA-011",
2239
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${declaredIds.join(
2240
+ ", "
2241
+ )}`,
2242
+ "error",
2243
+ file,
2244
+ "contracts.thema.declaration",
2245
+ declaredIds
2246
+ )
2247
+ );
2248
+ continue;
2249
+ }
2250
+ const declaredId = declaredIds[0] ?? "";
2251
+ if (!THEMA_ID_RE.test(declaredId)) {
2252
+ issues.push(
2253
+ issue(
2254
+ "QFAI-THEMA-012",
2255
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
2256
+ "error",
2257
+ file,
2258
+ "contracts.thema.idFormat",
2259
+ [declaredId]
2260
+ )
2261
+ );
2262
+ }
2263
+ let doc;
2138
2264
  try {
2139
- parseStructuredContract(file, stripContractDeclarationLines(text));
2265
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2140
2266
  } catch (error2) {
2141
2267
  issues.push(
2142
2268
  issue(
2143
- "QFAI-CONTRACT-001",
2144
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2269
+ "QFAI-THEMA-001",
2270
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2145
2271
  "error",
2146
2272
  file,
2147
- "contracts.ui.parse"
2273
+ "contracts.thema.parse"
2274
+ )
2275
+ );
2276
+ continue;
2277
+ }
2278
+ const docId = typeof doc.id === "string" ? doc.id : "";
2279
+ if (!THEMA_ID_RE.test(docId)) {
2280
+ issues.push(
2281
+ issue(
2282
+ "QFAI-THEMA-012",
2283
+ docId.length > 0 ? `thema \u306E id \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${docId}` : "thema \u306E id \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2284
+ "error",
2285
+ file,
2286
+ "contracts.thema.idFormat",
2287
+ docId.length > 0 ? [docId] : void 0
2288
+ )
2289
+ );
2290
+ }
2291
+ const name = typeof doc.name === "string" ? doc.name : "";
2292
+ if (!name) {
2293
+ issues.push(
2294
+ issue(
2295
+ "QFAI-THEMA-014",
2296
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2297
+ "error",
2298
+ file,
2299
+ "contracts.thema.name"
2300
+ )
2301
+ );
2302
+ }
2303
+ if (declaredId && docId && declaredId !== docId) {
2304
+ issues.push(
2305
+ issue(
2306
+ "QFAI-THEMA-013",
2307
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
2308
+ "error",
2309
+ file,
2310
+ "contracts.thema.idMismatch",
2311
+ [declaredId, docId]
2148
2312
  )
2149
2313
  );
2150
2314
  }
@@ -2174,6 +2338,7 @@ async function validateApiContracts(apiRoot) {
2174
2338
  "UI",
2175
2339
  "API",
2176
2340
  "DB",
2341
+ "THEMA",
2177
2342
  "ADR"
2178
2343
  ]);
2179
2344
  if (invalidIds.length > 0) {
@@ -2242,6 +2407,7 @@ async function validateDbContracts(dbRoot) {
2242
2407
  "UI",
2243
2408
  "API",
2244
2409
  "DB",
2410
+ "THEMA",
2245
2411
  "ADR"
2246
2412
  ]);
2247
2413
  if (invalidIds.length > 0) {
@@ -2348,6 +2514,278 @@ function validateDuplicateContractIds(contractIndex) {
2348
2514
  function hasOpenApi(doc) {
2349
2515
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
2350
2516
  }
2517
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
2518
+ const issues = [];
2519
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
2520
+ const themaRef = doc.themaRef;
2521
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
2522
+ issues.push(
2523
+ issue(
2524
+ "QFAI-UI-020",
2525
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2526
+ "error",
2527
+ file,
2528
+ "contracts.ui.themaRef"
2529
+ )
2530
+ );
2531
+ } else if (!THEMA_ID_RE.test(themaRef)) {
2532
+ issues.push(
2533
+ issue(
2534
+ "QFAI-UI-020",
2535
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
2536
+ "error",
2537
+ file,
2538
+ "contracts.ui.themaRef",
2539
+ [themaRef]
2540
+ )
2541
+ );
2542
+ } else if (!themaIds.has(themaRef)) {
2543
+ issues.push(
2544
+ issue(
2545
+ "QFAI-UI-020",
2546
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
2547
+ "error",
2548
+ file,
2549
+ "contracts.ui.themaRef",
2550
+ [themaRef]
2551
+ )
2552
+ );
2553
+ }
2554
+ }
2555
+ const assets = doc.assets;
2556
+ if (assets && typeof assets === "object") {
2557
+ issues.push(
2558
+ ...await validateUiAssets(
2559
+ assets,
2560
+ file,
2561
+ uiRoot
2562
+ )
2563
+ );
2564
+ }
2565
+ return issues;
2566
+ }
2567
+ async function validateUiAssets(assets, file, uiRoot) {
2568
+ const issues = [];
2569
+ const packValue = assets.pack;
2570
+ const useValue = assets.use;
2571
+ if (packValue === void 0 && useValue === void 0) {
2572
+ return issues;
2573
+ }
2574
+ if (typeof packValue !== "string" || packValue.length === 0) {
2575
+ issues.push(
2576
+ issue(
2577
+ "QFAI-ASSET-001",
2578
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2579
+ "error",
2580
+ file,
2581
+ "assets.pack"
2582
+ )
2583
+ );
2584
+ return issues;
2585
+ }
2586
+ if (!isSafeRelativePath(packValue)) {
2587
+ issues.push(
2588
+ issue(
2589
+ "QFAI-ASSET-001",
2590
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
2591
+ "error",
2592
+ file,
2593
+ "assets.pack",
2594
+ [packValue]
2595
+ )
2596
+ );
2597
+ return issues;
2598
+ }
2599
+ const packDir = path17.resolve(uiRoot, packValue);
2600
+ const packRelative = path17.relative(uiRoot, packDir);
2601
+ if (packRelative.startsWith("..") || path17.isAbsolute(packRelative)) {
2602
+ issues.push(
2603
+ issue(
2604
+ "QFAI-ASSET-001",
2605
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
2606
+ "error",
2607
+ file,
2608
+ "assets.pack",
2609
+ [packValue]
2610
+ )
2611
+ );
2612
+ return issues;
2613
+ }
2614
+ if (!await exists6(packDir)) {
2615
+ issues.push(
2616
+ issue(
2617
+ "QFAI-ASSET-001",
2618
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
2619
+ "error",
2620
+ file,
2621
+ "assets.pack",
2622
+ [packValue]
2623
+ )
2624
+ );
2625
+ return issues;
2626
+ }
2627
+ const assetsYamlPath = path17.join(packDir, "assets.yaml");
2628
+ if (!await exists6(assetsYamlPath)) {
2629
+ issues.push(
2630
+ issue(
2631
+ "QFAI-ASSET-002",
2632
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
2633
+ "error",
2634
+ assetsYamlPath,
2635
+ "assets.yaml"
2636
+ )
2637
+ );
2638
+ return issues;
2639
+ }
2640
+ let manifest;
2641
+ try {
2642
+ const manifestText = await readFile7(assetsYamlPath, "utf-8");
2643
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
2644
+ } catch (error2) {
2645
+ issues.push(
2646
+ issue(
2647
+ "QFAI-ASSET-002",
2648
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
2649
+ "error",
2650
+ assetsYamlPath,
2651
+ "assets.yaml"
2652
+ )
2653
+ );
2654
+ return issues;
2655
+ }
2656
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
2657
+ const itemIds = /* @__PURE__ */ new Set();
2658
+ const itemPaths = [];
2659
+ for (const item of items) {
2660
+ if (!item || typeof item !== "object") {
2661
+ continue;
2662
+ }
2663
+ const record2 = item;
2664
+ const id = typeof record2.id === "string" ? record2.id : void 0;
2665
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
2666
+ if (id) {
2667
+ itemIds.add(id);
2668
+ }
2669
+ itemPaths.push({ id, path: pathValue });
2670
+ }
2671
+ if (useValue !== void 0) {
2672
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
2673
+ issues.push(
2674
+ issue(
2675
+ "QFAI-ASSET-003",
2676
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2677
+ "error",
2678
+ file,
2679
+ "assets.use"
2680
+ )
2681
+ );
2682
+ } else {
2683
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
2684
+ if (missing.length > 0) {
2685
+ issues.push(
2686
+ issue(
2687
+ "QFAI-ASSET-003",
2688
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
2689
+ "error",
2690
+ file,
2691
+ "assets.use",
2692
+ missing
2693
+ )
2694
+ );
2695
+ }
2696
+ }
2697
+ }
2698
+ for (const entry of itemPaths) {
2699
+ if (!entry.path) {
2700
+ continue;
2701
+ }
2702
+ if (!isSafeRelativePath(entry.path)) {
2703
+ issues.push(
2704
+ issue(
2705
+ "QFAI-ASSET-004",
2706
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
2707
+ "error",
2708
+ assetsYamlPath,
2709
+ "assets.path",
2710
+ entry.id ? [entry.id] : [entry.path]
2711
+ )
2712
+ );
2713
+ continue;
2714
+ }
2715
+ const assetPath = path17.resolve(packDir, entry.path);
2716
+ const assetRelative = path17.relative(packDir, assetPath);
2717
+ if (assetRelative.startsWith("..") || path17.isAbsolute(assetRelative)) {
2718
+ issues.push(
2719
+ issue(
2720
+ "QFAI-ASSET-004",
2721
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
2722
+ "error",
2723
+ assetsYamlPath,
2724
+ "assets.path",
2725
+ entry.id ? [entry.id] : [entry.path]
2726
+ )
2727
+ );
2728
+ continue;
2729
+ }
2730
+ if (!await exists6(assetPath)) {
2731
+ issues.push(
2732
+ issue(
2733
+ "QFAI-ASSET-004",
2734
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
2735
+ "error",
2736
+ assetsYamlPath,
2737
+ "assets.path",
2738
+ entry.id ? [entry.id] : [entry.path]
2739
+ )
2740
+ );
2741
+ }
2742
+ }
2743
+ return issues;
2744
+ }
2745
+ function shouldIgnoreInvalidId(value, doc) {
2746
+ if (!doc) {
2747
+ return false;
2748
+ }
2749
+ const assets = doc.assets;
2750
+ if (!assets || typeof assets !== "object") {
2751
+ return false;
2752
+ }
2753
+ const packValue = assets.pack;
2754
+ if (typeof packValue !== "string" || packValue.length === 0) {
2755
+ return false;
2756
+ }
2757
+ const normalized = packValue.replace(/\\/g, "/");
2758
+ const basename = path17.posix.basename(normalized);
2759
+ if (!basename) {
2760
+ return false;
2761
+ }
2762
+ return value.toLowerCase() === basename.toLowerCase();
2763
+ }
2764
+ function isSafeRelativePath(value) {
2765
+ if (!value) {
2766
+ return false;
2767
+ }
2768
+ if (path17.isAbsolute(value)) {
2769
+ return false;
2770
+ }
2771
+ const normalized = value.replace(/\\/g, "/");
2772
+ if (/^[A-Za-z]:/.test(normalized)) {
2773
+ return false;
2774
+ }
2775
+ const segments = normalized.split("/");
2776
+ if (segments.some((segment) => segment === "..")) {
2777
+ return false;
2778
+ }
2779
+ return true;
2780
+ }
2781
+ async function exists6(target) {
2782
+ try {
2783
+ await access6(target);
2784
+ return true;
2785
+ } catch {
2786
+ return false;
2787
+ }
2788
+ }
2351
2789
  function formatError4(error2) {
2352
2790
  if (error2 instanceof Error) {
2353
2791
  return error2.message;
@@ -2378,7 +2816,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2378
2816
 
2379
2817
  // src/core/validators/delta.ts
2380
2818
  import { readFile as readFile8 } from "fs/promises";
2381
- import path17 from "path";
2819
+ import path18 from "path";
2382
2820
  var SECTION_RE = /^##\s+変更区分/m;
2383
2821
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2384
2822
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2392,7 +2830,7 @@ async function validateDeltas(root, config) {
2392
2830
  }
2393
2831
  const issues = [];
2394
2832
  for (const pack of packs) {
2395
- const deltaPath = path17.join(pack, "delta.md");
2833
+ const deltaPath = path18.join(pack, "delta.md");
2396
2834
  let text;
2397
2835
  try {
2398
2836
  text = await readFile8(deltaPath, "utf-8");
@@ -2472,7 +2910,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2472
2910
 
2473
2911
  // src/core/validators/ids.ts
2474
2912
  import { readFile as readFile9 } from "fs/promises";
2475
- import path18 from "path";
2913
+ import path19 from "path";
2476
2914
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2477
2915
  async function validateDefinedIds(root, config) {
2478
2916
  const issues = [];
@@ -2538,7 +2976,7 @@ function recordId(out, id, file) {
2538
2976
  }
2539
2977
  function formatFileList(files, root) {
2540
2978
  return files.map((file) => {
2541
- const relative = path18.relative(root, file);
2979
+ const relative = path19.relative(root, file);
2542
2980
  return relative.length > 0 ? relative : file;
2543
2981
  }).join(", ");
2544
2982
  }
@@ -2596,7 +3034,8 @@ async function validatePromptsIntegrity(root) {
2596
3034
  }
2597
3035
 
2598
3036
  // src/core/validators/scenario.ts
2599
- import { readFile as readFile10 } from "fs/promises";
3037
+ import { access as access7, readFile as readFile10 } from "fs/promises";
3038
+ import path20 from "path";
2600
3039
  var GIVEN_PATTERN = /\bGiven\b/;
2601
3040
  var WHEN_PATTERN = /\bWhen\b/;
2602
3041
  var THEN_PATTERN = /\bThen\b/;
@@ -2619,6 +3058,18 @@ async function validateScenarios(root, config) {
2619
3058
  }
2620
3059
  const issues = [];
2621
3060
  for (const entry of entries) {
3061
+ const legacyScenarioPath = path20.join(entry.dir, "scenario.md");
3062
+ if (await fileExists(legacyScenarioPath)) {
3063
+ issues.push(
3064
+ issue4(
3065
+ "QFAI-SC-004",
3066
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3067
+ "error",
3068
+ legacyScenarioPath,
3069
+ "scenario.legacy"
3070
+ )
3071
+ );
3072
+ }
2622
3073
  let text;
2623
3074
  try {
2624
3075
  text = await readFile10(entry.scenarioPath, "utf-8");
@@ -2650,6 +3101,7 @@ function validateScenarioContent(text, file) {
2650
3101
  "UI",
2651
3102
  "API",
2652
3103
  "DB",
3104
+ "THEMA",
2653
3105
  "ADR"
2654
3106
  ]);
2655
3107
  if (invalidIds.length > 0) {
@@ -2793,6 +3245,14 @@ function isMissingFileError3(error2) {
2793
3245
  }
2794
3246
  return error2.code === "ENOENT";
2795
3247
  }
3248
+ async function fileExists(target) {
3249
+ try {
3250
+ await access7(target);
3251
+ return true;
3252
+ } catch {
3253
+ return false;
3254
+ }
3255
+ }
2796
3256
 
2797
3257
  // src/core/validators/spec.ts
2798
3258
  import { readFile as readFile11 } from "fs/promises";
@@ -2852,6 +3312,7 @@ function validateSpecContent(text, file, requiredSections) {
2852
3312
  "UI",
2853
3313
  "API",
2854
3314
  "DB",
3315
+ "THEMA",
2855
3316
  "ADR"
2856
3317
  ]);
2857
3318
  if (invalidIds.length > 0) {
@@ -3031,7 +3492,7 @@ async function validateTraceability(root, config) {
3031
3492
  "QFAI-TRACE-021",
3032
3493
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
3033
3494
  ", "
3034
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3495
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3035
3496
  "error",
3036
3497
  file,
3037
3498
  "traceability.specContractRefFormat",
@@ -3095,7 +3556,7 @@ async function validateTraceability(root, config) {
3095
3556
  "QFAI-TRACE-032",
3096
3557
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
3097
3558
  ", "
3098
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3559
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3099
3560
  "error",
3100
3561
  file,
3101
3562
  "traceability.scenarioContractRefFormat",
@@ -3474,17 +3935,25 @@ function countIssues(issues) {
3474
3935
  }
3475
3936
 
3476
3937
  // src/core/report.ts
3477
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3938
+ var ID_PREFIXES2 = [
3939
+ "SPEC",
3940
+ "BR",
3941
+ "SC",
3942
+ "UI",
3943
+ "API",
3944
+ "DB",
3945
+ "THEMA"
3946
+ ];
3478
3947
  async function createReportData(root, validation, configResult) {
3479
- const resolvedRoot = path19.resolve(root);
3948
+ const resolvedRoot = path21.resolve(root);
3480
3949
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3481
3950
  const config = resolved.config;
3482
3951
  const configPath = resolved.configPath;
3483
3952
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3484
3953
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3485
- const apiRoot = path19.join(contractsRoot, "api");
3486
- const uiRoot = path19.join(contractsRoot, "ui");
3487
- const dbRoot = path19.join(contractsRoot, "db");
3954
+ const apiRoot = path21.join(contractsRoot, "api");
3955
+ const uiRoot = path21.join(contractsRoot, "ui");
3956
+ const dbRoot = path21.join(contractsRoot, "db");
3488
3957
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3489
3958
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3490
3959
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3492,7 +3961,8 @@ async function createReportData(root, validation, configResult) {
3492
3961
  const {
3493
3962
  api: apiFiles,
3494
3963
  ui: uiFiles,
3495
- db: dbFiles
3964
+ db: dbFiles,
3965
+ thema: themaFiles
3496
3966
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
3497
3967
  const contractIndex = await buildContractIndex(resolvedRoot, config);
3498
3968
  const contractIdList = Array.from(contractIndex.ids);
@@ -3519,7 +3989,8 @@ async function createReportData(root, validation, configResult) {
3519
3989
  ...scenarioFiles,
3520
3990
  ...apiFiles,
3521
3991
  ...uiFiles,
3522
- ...dbFiles
3992
+ ...dbFiles,
3993
+ ...themaFiles
3523
3994
  ]);
3524
3995
  const upstreamIds = await collectUpstreamIds([
3525
3996
  ...specFiles,
@@ -3556,7 +4027,8 @@ async function createReportData(root, validation, configResult) {
3556
4027
  contracts: {
3557
4028
  api: apiFiles.length,
3558
4029
  ui: uiFiles.length,
3559
- db: dbFiles.length
4030
+ db: dbFiles.length,
4031
+ thema: themaFiles.length
3560
4032
  },
3561
4033
  counts: normalizedValidation.counts
3562
4034
  },
@@ -3566,7 +4038,8 @@ async function createReportData(root, validation, configResult) {
3566
4038
  sc: idsByPrefix.SC,
3567
4039
  ui: idsByPrefix.UI,
3568
4040
  api: idsByPrefix.API,
3569
- db: idsByPrefix.DB
4041
+ db: idsByPrefix.DB,
4042
+ thema: idsByPrefix.THEMA
3570
4043
  },
3571
4044
  traceability: {
3572
4045
  upstreamIdsFound: upstreamIds.size,
@@ -3636,7 +4109,7 @@ function formatReportMarkdown(data, options = {}) {
3636
4109
  lines.push(`- specs: ${data.summary.specs}`);
3637
4110
  lines.push(`- scenarios: ${data.summary.scenarios}`);
3638
4111
  lines.push(
3639
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
4112
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
3640
4113
  );
3641
4114
  lines.push(
3642
4115
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3780,6 +4253,7 @@ function formatReportMarkdown(data, options = {}) {
3780
4253
  lines.push(formatIdLine("UI", data.ids.ui));
3781
4254
  lines.push(formatIdLine("API", data.ids.api));
3782
4255
  lines.push(formatIdLine("DB", data.ids.db));
4256
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3783
4257
  lines.push("");
3784
4258
  lines.push("## Traceability");
3785
4259
  lines.push("");
@@ -4001,7 +4475,8 @@ async function collectIds(files) {
4001
4475
  SC: /* @__PURE__ */ new Set(),
4002
4476
  UI: /* @__PURE__ */ new Set(),
4003
4477
  API: /* @__PURE__ */ new Set(),
4004
- DB: /* @__PURE__ */ new Set()
4478
+ DB: /* @__PURE__ */ new Set(),
4479
+ THEMA: /* @__PURE__ */ new Set()
4005
4480
  };
4006
4481
  for (const file of files) {
4007
4482
  const text = await readFile13(file, "utf-8");
@@ -4016,7 +4491,8 @@ async function collectIds(files) {
4016
4491
  SC: toSortedArray2(result.SC),
4017
4492
  UI: toSortedArray2(result.UI),
4018
4493
  API: toSortedArray2(result.API),
4019
- DB: toSortedArray2(result.DB)
4494
+ DB: toSortedArray2(result.DB),
4495
+ THEMA: toSortedArray2(result.THEMA)
4020
4496
  };
4021
4497
  }
4022
4498
  async function collectUpstreamIds(files) {
@@ -4180,7 +4656,7 @@ function warnIfTruncated(scan, context) {
4180
4656
 
4181
4657
  // src/cli/commands/report.ts
4182
4658
  async function runReport(options) {
4183
- const root = path20.resolve(options.root);
4659
+ const root = path22.resolve(options.root);
4184
4660
  const configResult = await loadConfig(root);
4185
4661
  let validation;
4186
4662
  if (options.runValidate) {
@@ -4197,7 +4673,7 @@ async function runReport(options) {
4197
4673
  validation = normalized;
4198
4674
  } else {
4199
4675
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
4200
- const inputPath = path20.isAbsolute(input) ? input : path20.resolve(root, input);
4676
+ const inputPath = path22.isAbsolute(input) ? input : path22.resolve(root, input);
4201
4677
  try {
4202
4678
  validation = await readValidationResult(inputPath);
4203
4679
  } catch (err) {
@@ -4224,10 +4700,10 @@ async function runReport(options) {
4224
4700
  warnIfTruncated(data.traceability.testFiles, "report");
4225
4701
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4226
4702
  const outRoot = resolvePath(root, configResult.config, "outDir");
4227
- const defaultOut = options.format === "json" ? path20.join(outRoot, "report.json") : path20.join(outRoot, "report.md");
4703
+ const defaultOut = options.format === "json" ? path22.join(outRoot, "report.json") : path22.join(outRoot, "report.md");
4228
4704
  const out = options.outPath ?? defaultOut;
4229
- const outPath = path20.isAbsolute(out) ? out : path20.resolve(root, out);
4230
- await mkdir3(path20.dirname(outPath), { recursive: true });
4705
+ const outPath = path22.isAbsolute(out) ? out : path22.resolve(root, out);
4706
+ await mkdir3(path22.dirname(outPath), { recursive: true });
4231
4707
  await writeFile2(outPath, `${output}
4232
4708
  `, "utf-8");
4233
4709
  info(
@@ -4292,15 +4768,15 @@ function isMissingFileError5(error2) {
4292
4768
  return record2.code === "ENOENT";
4293
4769
  }
4294
4770
  async function writeValidationResult(root, outputPath, result) {
4295
- const abs = path20.isAbsolute(outputPath) ? outputPath : path20.resolve(root, outputPath);
4296
- await mkdir3(path20.dirname(abs), { recursive: true });
4771
+ const abs = path22.isAbsolute(outputPath) ? outputPath : path22.resolve(root, outputPath);
4772
+ await mkdir3(path22.dirname(abs), { recursive: true });
4297
4773
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
4298
4774
  `, "utf-8");
4299
4775
  }
4300
4776
 
4301
4777
  // src/cli/commands/validate.ts
4302
4778
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
4303
- import path21 from "path";
4779
+ import path23 from "path";
4304
4780
 
4305
4781
  // src/cli/lib/failOn.ts
4306
4782
  function shouldFail(result, failOn) {
@@ -4315,7 +4791,7 @@ function shouldFail(result, failOn) {
4315
4791
 
4316
4792
  // src/cli/commands/validate.ts
4317
4793
  async function runValidate(options) {
4318
- const root = path21.resolve(options.root);
4794
+ const root = path23.resolve(options.root);
4319
4795
  const configResult = await loadConfig(root);
4320
4796
  const result = await validateProject(root, configResult);
4321
4797
  const normalized = normalizeValidationResult(root, result);
@@ -4440,12 +4916,12 @@ function issueKey(issue7) {
4440
4916
  }
4441
4917
  async function emitJson(result, root, jsonPath) {
4442
4918
  const abs = resolveJsonPath(root, jsonPath);
4443
- await mkdir4(path21.dirname(abs), { recursive: true });
4919
+ await mkdir4(path23.dirname(abs), { recursive: true });
4444
4920
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4445
4921
  `, "utf-8");
4446
4922
  }
4447
4923
  function resolveJsonPath(root, jsonPath) {
4448
- return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
4924
+ return path23.isAbsolute(jsonPath) ? jsonPath : path23.resolve(root, jsonPath);
4449
4925
  }
4450
4926
  var GITHUB_ANNOTATION_LIMIT = 100;
4451
4927