qfai 1.0.1 → 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.
Files changed (29) hide show
  1. package/README.md +13 -3
  2. package/assets/init/.qfai/README.md +2 -2
  3. package/assets/init/.qfai/contracts/README.md +21 -1
  4. package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/assets.yaml +6 -0
  5. package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/palette.png +0 -0
  6. package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/assets.yaml +6 -0
  7. package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/snapshots/login__desktop__light__default.png +0 -0
  8. package/assets/init/.qfai/contracts/ui/thema-001-facebook-like.yml +13 -0
  9. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +9 -0
  10. package/assets/init/.qfai/prompts/makeBusinessFlow.md +1 -1
  11. package/assets/init/.qfai/prompts/makeOverview.md +1 -1
  12. package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +1 -1
  13. package/assets/init/.qfai/prompts/require-to-spec.md +2 -2
  14. package/assets/init/.qfai/samples/analyze/analysis.md +1 -1
  15. package/assets/init/.qfai/specs/README.md +3 -3
  16. package/assets/init/.qfai/specs/spec-0001/delta.md +1 -1
  17. package/assets/init/.qfai/specs/spec-0001/{scenario.md → scenario.feature} +1 -1
  18. package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
  19. package/dist/cli/index.cjs +589 -114
  20. package/dist/cli/index.cjs.map +1 -1
  21. package/dist/cli/index.mjs +591 -116
  22. package/dist/cli/index.mjs.map +1 -1
  23. package/dist/index.cjs +542 -67
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +3 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.mjs +544 -69
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +1 -1
@@ -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";
@@ -627,7 +628,7 @@ async function collectSpecEntries(specsRoot) {
627
628
  dir,
628
629
  specPath: path4.join(dir, "spec.md"),
629
630
  deltaPath: path4.join(dir, "delta.md"),
630
- scenarioPath: path4.join(dir, "scenario.md")
631
+ scenarioPath: path4.join(dir, "scenario.feature")
631
632
  }));
632
633
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
633
634
  }
@@ -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.1".length > 0) {
1145
- return "1.0.1";
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/;
@@ -2606,12 +3045,11 @@ async function validateScenarios(root, config) {
2606
3045
  const specsRoot = resolvePath(root, config, "specsDir");
2607
3046
  const entries = await collectSpecEntries(specsRoot);
2608
3047
  if (entries.length === 0) {
2609
- const expected = "spec-0001/scenario.md";
2610
- const legacy = "spec-001/scenario.md";
3048
+ const expected = "spec-0001/scenario.feature";
2611
3049
  return [
2612
3050
  issue4(
2613
3051
  "QFAI-SC-000",
2614
- `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
3052
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
2615
3053
  "info",
2616
3054
  specsRoot,
2617
3055
  "scenario.files"
@@ -2620,6 +3058,18 @@ async function validateScenarios(root, config) {
2620
3058
  }
2621
3059
  const issues = [];
2622
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
+ }
2623
3073
  let text;
2624
3074
  try {
2625
3075
  text = await readFile10(entry.scenarioPath, "utf-8");
@@ -2628,7 +3078,7 @@ async function validateScenarios(root, config) {
2628
3078
  issues.push(
2629
3079
  issue4(
2630
3080
  "QFAI-SC-001",
2631
- "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
3081
+ "scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2632
3082
  "error",
2633
3083
  entry.scenarioPath,
2634
3084
  "scenario.exists"
@@ -2651,6 +3101,7 @@ function validateScenarioContent(text, file) {
2651
3101
  "UI",
2652
3102
  "API",
2653
3103
  "DB",
3104
+ "THEMA",
2654
3105
  "ADR"
2655
3106
  ]);
2656
3107
  if (invalidIds.length > 0) {
@@ -2794,6 +3245,14 @@ function isMissingFileError3(error2) {
2794
3245
  }
2795
3246
  return error2.code === "ENOENT";
2796
3247
  }
3248
+ async function fileExists(target) {
3249
+ try {
3250
+ await access7(target);
3251
+ return true;
3252
+ } catch {
3253
+ return false;
3254
+ }
3255
+ }
2797
3256
 
2798
3257
  // src/core/validators/spec.ts
2799
3258
  import { readFile as readFile11 } from "fs/promises";
@@ -2853,6 +3312,7 @@ function validateSpecContent(text, file, requiredSections) {
2853
3312
  "UI",
2854
3313
  "API",
2855
3314
  "DB",
3315
+ "THEMA",
2856
3316
  "ADR"
2857
3317
  ]);
2858
3318
  if (invalidIds.length > 0) {
@@ -3032,7 +3492,7 @@ async function validateTraceability(root, config) {
3032
3492
  "QFAI-TRACE-021",
3033
3493
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
3034
3494
  ", "
3035
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3495
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3036
3496
  "error",
3037
3497
  file,
3038
3498
  "traceability.specContractRefFormat",
@@ -3096,7 +3556,7 @@ async function validateTraceability(root, config) {
3096
3556
  "QFAI-TRACE-032",
3097
3557
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
3098
3558
  ", "
3099
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3559
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3100
3560
  "error",
3101
3561
  file,
3102
3562
  "traceability.scenarioContractRefFormat",
@@ -3475,17 +3935,25 @@ function countIssues(issues) {
3475
3935
  }
3476
3936
 
3477
3937
  // src/core/report.ts
3478
- 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
+ ];
3479
3947
  async function createReportData(root, validation, configResult) {
3480
- const resolvedRoot = path19.resolve(root);
3948
+ const resolvedRoot = path21.resolve(root);
3481
3949
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3482
3950
  const config = resolved.config;
3483
3951
  const configPath = resolved.configPath;
3484
3952
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3485
3953
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3486
- const apiRoot = path19.join(contractsRoot, "api");
3487
- const uiRoot = path19.join(contractsRoot, "ui");
3488
- 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");
3489
3957
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3490
3958
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3491
3959
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3493,7 +3961,8 @@ async function createReportData(root, validation, configResult) {
3493
3961
  const {
3494
3962
  api: apiFiles,
3495
3963
  ui: uiFiles,
3496
- db: dbFiles
3964
+ db: dbFiles,
3965
+ thema: themaFiles
3497
3966
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
3498
3967
  const contractIndex = await buildContractIndex(resolvedRoot, config);
3499
3968
  const contractIdList = Array.from(contractIndex.ids);
@@ -3520,7 +3989,8 @@ async function createReportData(root, validation, configResult) {
3520
3989
  ...scenarioFiles,
3521
3990
  ...apiFiles,
3522
3991
  ...uiFiles,
3523
- ...dbFiles
3992
+ ...dbFiles,
3993
+ ...themaFiles
3524
3994
  ]);
3525
3995
  const upstreamIds = await collectUpstreamIds([
3526
3996
  ...specFiles,
@@ -3557,7 +4027,8 @@ async function createReportData(root, validation, configResult) {
3557
4027
  contracts: {
3558
4028
  api: apiFiles.length,
3559
4029
  ui: uiFiles.length,
3560
- db: dbFiles.length
4030
+ db: dbFiles.length,
4031
+ thema: themaFiles.length
3561
4032
  },
3562
4033
  counts: normalizedValidation.counts
3563
4034
  },
@@ -3567,7 +4038,8 @@ async function createReportData(root, validation, configResult) {
3567
4038
  sc: idsByPrefix.SC,
3568
4039
  ui: idsByPrefix.UI,
3569
4040
  api: idsByPrefix.API,
3570
- db: idsByPrefix.DB
4041
+ db: idsByPrefix.DB,
4042
+ thema: idsByPrefix.THEMA
3571
4043
  },
3572
4044
  traceability: {
3573
4045
  upstreamIdsFound: upstreamIds.size,
@@ -3637,7 +4109,7 @@ function formatReportMarkdown(data, options = {}) {
3637
4109
  lines.push(`- specs: ${data.summary.specs}`);
3638
4110
  lines.push(`- scenarios: ${data.summary.scenarios}`);
3639
4111
  lines.push(
3640
- `- 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}`
3641
4113
  );
3642
4114
  lines.push(
3643
4115
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3781,6 +4253,7 @@ function formatReportMarkdown(data, options = {}) {
3781
4253
  lines.push(formatIdLine("UI", data.ids.ui));
3782
4254
  lines.push(formatIdLine("API", data.ids.api));
3783
4255
  lines.push(formatIdLine("DB", data.ids.db));
4256
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3784
4257
  lines.push("");
3785
4258
  lines.push("## Traceability");
3786
4259
  lines.push("");
@@ -4002,7 +4475,8 @@ async function collectIds(files) {
4002
4475
  SC: /* @__PURE__ */ new Set(),
4003
4476
  UI: /* @__PURE__ */ new Set(),
4004
4477
  API: /* @__PURE__ */ new Set(),
4005
- DB: /* @__PURE__ */ new Set()
4478
+ DB: /* @__PURE__ */ new Set(),
4479
+ THEMA: /* @__PURE__ */ new Set()
4006
4480
  };
4007
4481
  for (const file of files) {
4008
4482
  const text = await readFile13(file, "utf-8");
@@ -4017,7 +4491,8 @@ async function collectIds(files) {
4017
4491
  SC: toSortedArray2(result.SC),
4018
4492
  UI: toSortedArray2(result.UI),
4019
4493
  API: toSortedArray2(result.API),
4020
- DB: toSortedArray2(result.DB)
4494
+ DB: toSortedArray2(result.DB),
4495
+ THEMA: toSortedArray2(result.THEMA)
4021
4496
  };
4022
4497
  }
4023
4498
  async function collectUpstreamIds(files) {
@@ -4181,7 +4656,7 @@ function warnIfTruncated(scan, context) {
4181
4656
 
4182
4657
  // src/cli/commands/report.ts
4183
4658
  async function runReport(options) {
4184
- const root = path20.resolve(options.root);
4659
+ const root = path22.resolve(options.root);
4185
4660
  const configResult = await loadConfig(root);
4186
4661
  let validation;
4187
4662
  if (options.runValidate) {
@@ -4198,7 +4673,7 @@ async function runReport(options) {
4198
4673
  validation = normalized;
4199
4674
  } else {
4200
4675
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
4201
- const inputPath = path20.isAbsolute(input) ? input : path20.resolve(root, input);
4676
+ const inputPath = path22.isAbsolute(input) ? input : path22.resolve(root, input);
4202
4677
  try {
4203
4678
  validation = await readValidationResult(inputPath);
4204
4679
  } catch (err) {
@@ -4225,10 +4700,10 @@ async function runReport(options) {
4225
4700
  warnIfTruncated(data.traceability.testFiles, "report");
4226
4701
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4227
4702
  const outRoot = resolvePath(root, configResult.config, "outDir");
4228
- 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");
4229
4704
  const out = options.outPath ?? defaultOut;
4230
- const outPath = path20.isAbsolute(out) ? out : path20.resolve(root, out);
4231
- 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 });
4232
4707
  await writeFile2(outPath, `${output}
4233
4708
  `, "utf-8");
4234
4709
  info(
@@ -4293,15 +4768,15 @@ function isMissingFileError5(error2) {
4293
4768
  return record2.code === "ENOENT";
4294
4769
  }
4295
4770
  async function writeValidationResult(root, outputPath, result) {
4296
- const abs = path20.isAbsolute(outputPath) ? outputPath : path20.resolve(root, outputPath);
4297
- 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 });
4298
4773
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
4299
4774
  `, "utf-8");
4300
4775
  }
4301
4776
 
4302
4777
  // src/cli/commands/validate.ts
4303
4778
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
4304
- import path21 from "path";
4779
+ import path23 from "path";
4305
4780
 
4306
4781
  // src/cli/lib/failOn.ts
4307
4782
  function shouldFail(result, failOn) {
@@ -4316,7 +4791,7 @@ function shouldFail(result, failOn) {
4316
4791
 
4317
4792
  // src/cli/commands/validate.ts
4318
4793
  async function runValidate(options) {
4319
- const root = path21.resolve(options.root);
4794
+ const root = path23.resolve(options.root);
4320
4795
  const configResult = await loadConfig(root);
4321
4796
  const result = await validateProject(root, configResult);
4322
4797
  const normalized = normalizeValidationResult(root, result);
@@ -4441,12 +4916,12 @@ function issueKey(issue7) {
4441
4916
  }
4442
4917
  async function emitJson(result, root, jsonPath) {
4443
4918
  const abs = resolveJsonPath(root, jsonPath);
4444
- await mkdir4(path21.dirname(abs), { recursive: true });
4919
+ await mkdir4(path23.dirname(abs), { recursive: true });
4445
4920
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4446
4921
  `, "utf-8");
4447
4922
  }
4448
4923
  function resolveJsonPath(root, jsonPath) {
4449
- return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
4924
+ return path23.isAbsolute(jsonPath) ? jsonPath : path23.resolve(root, jsonPath);
4450
4925
  }
4451
4926
  var GITHUB_ANNOTATION_LIMIT = 100;
4452
4927