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.
@@ -231,11 +231,11 @@ function emitPromptNotFound(promptName, candidates) {
231
231
 
232
232
  // src/cli/commands/doctor.ts
233
233
  var import_promises10 = require("fs/promises");
234
- var import_node_path11 = __toESM(require("path"), 1);
234
+ var import_node_path12 = __toESM(require("path"), 1);
235
235
 
236
236
  // src/core/doctor.ts
237
237
  var import_promises9 = require("fs/promises");
238
- var import_node_path10 = __toESM(require("path"), 1);
238
+ var import_node_path11 = __toESM(require("path"), 1);
239
239
 
240
240
  // src/core/config.ts
241
241
  var import_promises3 = require("fs/promises");
@@ -639,6 +639,7 @@ function isRecord(value) {
639
639
 
640
640
  // src/core/discovery.ts
641
641
  var import_promises5 = require("fs/promises");
642
+ var import_node_path5 = __toESM(require("path"), 1);
642
643
 
643
644
  // src/core/specLayout.ts
644
645
  var import_promises4 = require("fs/promises");
@@ -686,7 +687,12 @@ async function collectScenarioFiles(specsRoot) {
686
687
  return filterExisting(entries.map((entry) => entry.scenarioPath));
687
688
  }
688
689
  async function collectUiContractFiles(uiRoot) {
689
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
690
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
691
+ return filterByBasenamePrefix(files, "ui-");
692
+ }
693
+ async function collectThemaContractFiles(uiRoot) {
694
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
695
+ return filterByBasenamePrefix(files, "thema-");
690
696
  }
691
697
  async function collectApiContractFiles(apiRoot) {
692
698
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -695,12 +701,13 @@ async function collectDbContractFiles(dbRoot) {
695
701
  return collectFiles(dbRoot, { extensions: [".sql"] });
696
702
  }
697
703
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
698
- const [ui, api, db] = await Promise.all([
704
+ const [ui, thema, api, db] = await Promise.all([
699
705
  collectUiContractFiles(uiRoot),
706
+ collectThemaContractFiles(uiRoot),
700
707
  collectApiContractFiles(apiRoot),
701
708
  collectDbContractFiles(dbRoot)
702
709
  ]);
703
- return { ui, api, db };
710
+ return { ui, thema, api, db };
704
711
  }
705
712
  async function filterExisting(files) {
706
713
  const existing = [];
@@ -719,17 +726,23 @@ async function exists3(target) {
719
726
  return false;
720
727
  }
721
728
  }
729
+ function filterByBasenamePrefix(files, prefix) {
730
+ const lowerPrefix = prefix.toLowerCase();
731
+ return files.filter(
732
+ (file) => import_node_path5.default.basename(file).toLowerCase().startsWith(lowerPrefix)
733
+ );
734
+ }
722
735
 
723
736
  // src/core/paths.ts
724
- var import_node_path5 = __toESM(require("path"), 1);
737
+ var import_node_path6 = __toESM(require("path"), 1);
725
738
  function toRelativePath(root, target) {
726
739
  if (!target) {
727
740
  return target;
728
741
  }
729
- if (!import_node_path5.default.isAbsolute(target)) {
742
+ if (!import_node_path6.default.isAbsolute(target)) {
730
743
  return toPosixPath(target);
731
744
  }
732
- const relative = import_node_path5.default.relative(root, target);
745
+ const relative = import_node_path6.default.relative(root, target);
733
746
  if (!relative) {
734
747
  return ".";
735
748
  }
@@ -741,7 +754,7 @@ function toPosixPath(value) {
741
754
 
742
755
  // src/core/traceability.ts
743
756
  var import_promises6 = require("fs/promises");
744
- var import_node_path6 = __toESM(require("path"), 1);
757
+ var import_node_path7 = __toESM(require("path"), 1);
745
758
 
746
759
  // src/core/gherkin/parse.ts
747
760
  var import_gherkin = require("@cucumber/gherkin");
@@ -969,7 +982,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
969
982
  };
970
983
  }
971
984
  const normalizedFiles = Array.from(
972
- new Set(scanResult.files.map((file) => import_node_path6.default.normalize(file)))
985
+ new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
973
986
  );
974
987
  for (const file of normalizedFiles) {
975
988
  const text = await (0, import_promises6.readFile)(file, "utf-8");
@@ -1032,19 +1045,19 @@ function formatError3(error2) {
1032
1045
 
1033
1046
  // src/core/promptsIntegrity.ts
1034
1047
  var import_promises7 = require("fs/promises");
1035
- var import_node_path8 = __toESM(require("path"), 1);
1048
+ var import_node_path9 = __toESM(require("path"), 1);
1036
1049
 
1037
1050
  // src/shared/assets.ts
1038
1051
  var import_node_fs = require("fs");
1039
- var import_node_path7 = __toESM(require("path"), 1);
1052
+ var import_node_path8 = __toESM(require("path"), 1);
1040
1053
  var import_node_url = require("url");
1041
1054
  function getInitAssetsDir() {
1042
1055
  const base = __filename;
1043
1056
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
1044
- const baseDir = import_node_path7.default.dirname(basePath);
1057
+ const baseDir = import_node_path8.default.dirname(basePath);
1045
1058
  const candidates = [
1046
- import_node_path7.default.resolve(baseDir, "../../../assets/init"),
1047
- import_node_path7.default.resolve(baseDir, "../../assets/init")
1059
+ import_node_path8.default.resolve(baseDir, "../../../assets/init"),
1060
+ import_node_path8.default.resolve(baseDir, "../../assets/init")
1048
1061
  ];
1049
1062
  for (const candidate of candidates) {
1050
1063
  if ((0, import_node_fs.existsSync)(candidate)) {
@@ -1062,10 +1075,10 @@ function getInitAssetsDir() {
1062
1075
 
1063
1076
  // src/core/promptsIntegrity.ts
1064
1077
  async function diffProjectPromptsAgainstInitAssets(root) {
1065
- const promptsDir = import_node_path8.default.resolve(root, ".qfai", "prompts");
1078
+ const promptsDir = import_node_path9.default.resolve(root, ".qfai", "prompts");
1066
1079
  let templateDir;
1067
1080
  try {
1068
- templateDir = import_node_path8.default.join(getInitAssetsDir(), ".qfai", "prompts");
1081
+ templateDir = import_node_path9.default.join(getInitAssetsDir(), ".qfai", "prompts");
1069
1082
  } catch {
1070
1083
  return {
1071
1084
  status: "skipped_missing_assets",
@@ -1142,7 +1155,7 @@ function normalizeNewlines(text) {
1142
1155
  return text.replace(/\r\n/g, "\n");
1143
1156
  }
1144
1157
  function toRel(base, abs) {
1145
- const rel = import_node_path8.default.relative(base, abs);
1158
+ const rel = import_node_path9.default.relative(base, abs);
1146
1159
  return rel.replace(/[\\/]+/g, "/");
1147
1160
  }
1148
1161
  function intersectKeys(a, b) {
@@ -1157,11 +1170,11 @@ function intersectKeys(a, b) {
1157
1170
 
1158
1171
  // src/core/version.ts
1159
1172
  var import_promises8 = require("fs/promises");
1160
- var import_node_path9 = __toESM(require("path"), 1);
1173
+ var import_node_path10 = __toESM(require("path"), 1);
1161
1174
  var import_node_url2 = require("url");
1162
1175
  async function resolveToolVersion() {
1163
- if ("1.0.2".length > 0) {
1164
- return "1.0.2";
1176
+ if ("1.0.3".length > 0) {
1177
+ return "1.0.3";
1165
1178
  }
1166
1179
  try {
1167
1180
  const packagePath = resolvePackageJsonPath();
@@ -1176,7 +1189,7 @@ async function resolveToolVersion() {
1176
1189
  function resolvePackageJsonPath() {
1177
1190
  const base = __filename;
1178
1191
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1179
- return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
1192
+ return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
1180
1193
  }
1181
1194
 
1182
1195
  // src/core/doctor.ts
@@ -1202,7 +1215,7 @@ function normalizeGlobs2(values) {
1202
1215
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1203
1216
  }
1204
1217
  async function createDoctorData(options) {
1205
- const startDir = import_node_path10.default.resolve(options.startDir);
1218
+ const startDir = import_node_path11.default.resolve(options.startDir);
1206
1219
  const checks = [];
1207
1220
  const configPath = getConfigPath(startDir);
1208
1221
  const search = options.rootExplicit ? {
@@ -1265,9 +1278,9 @@ async function createDoctorData(options) {
1265
1278
  details: { path: toRelativePath(root, resolved) }
1266
1279
  });
1267
1280
  if (key === "promptsDir") {
1268
- const promptsLocalDir = import_node_path10.default.join(
1269
- import_node_path10.default.dirname(resolved),
1270
- `${import_node_path10.default.basename(resolved)}.local`
1281
+ const promptsLocalDir = import_node_path11.default.join(
1282
+ import_node_path11.default.dirname(resolved),
1283
+ `${import_node_path11.default.basename(resolved)}.local`
1271
1284
  );
1272
1285
  const found = await exists4(promptsLocalDir);
1273
1286
  addCheck(checks, {
@@ -1340,7 +1353,7 @@ async function createDoctorData(options) {
1340
1353
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1341
1354
  details: { specPacks: entries.length, missingFiles }
1342
1355
  });
1343
- const validateJsonAbs = import_node_path10.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path10.default.resolve(root, config.output.validateJsonPath);
1356
+ const validateJsonAbs = import_node_path11.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path11.default.resolve(root, config.output.validateJsonPath);
1344
1357
  const validateJsonExists = await exists4(validateJsonAbs);
1345
1358
  addCheck(checks, {
1346
1359
  id: "output.validateJson",
@@ -1350,8 +1363,8 @@ async function createDoctorData(options) {
1350
1363
  details: { path: toRelativePath(root, validateJsonAbs) }
1351
1364
  });
1352
1365
  const outDirAbs = resolvePath(root, config, "outDir");
1353
- const rel = import_node_path10.default.relative(outDirAbs, validateJsonAbs);
1354
- const inside = rel !== "" && !rel.startsWith("..") && !import_node_path10.default.isAbsolute(rel);
1366
+ const rel = import_node_path11.default.relative(outDirAbs, validateJsonAbs);
1367
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path11.default.isAbsolute(rel);
1355
1368
  addCheck(checks, {
1356
1369
  id: "output.pathAlignment",
1357
1370
  severity: inside ? "ok" : "warning",
@@ -1474,12 +1487,12 @@ async function detectOutDirCollisions(root) {
1474
1487
  });
1475
1488
  const configPaths = configScan.files;
1476
1489
  const configRoots = Array.from(
1477
- new Set(configPaths.map((configPath) => import_node_path10.default.dirname(configPath)))
1490
+ new Set(configPaths.map((configPath) => import_node_path11.default.dirname(configPath)))
1478
1491
  ).sort((a, b) => a.localeCompare(b));
1479
1492
  const outDirToRoots = /* @__PURE__ */ new Map();
1480
1493
  for (const configRoot of configRoots) {
1481
1494
  const { config } = await loadConfig(configRoot);
1482
- const outDir = import_node_path10.default.normalize(resolvePath(configRoot, config, "outDir"));
1495
+ const outDir = import_node_path11.default.normalize(resolvePath(configRoot, config, "outDir"));
1483
1496
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1484
1497
  roots.add(configRoot);
1485
1498
  outDirToRoots.set(outDir, roots);
@@ -1505,20 +1518,20 @@ async function detectOutDirCollisions(root) {
1505
1518
  };
1506
1519
  }
1507
1520
  async function findMonorepoRoot(startDir) {
1508
- let current = import_node_path10.default.resolve(startDir);
1521
+ let current = import_node_path11.default.resolve(startDir);
1509
1522
  while (true) {
1510
- const gitPath = import_node_path10.default.join(current, ".git");
1511
- const workspacePath = import_node_path10.default.join(current, "pnpm-workspace.yaml");
1523
+ const gitPath = import_node_path11.default.join(current, ".git");
1524
+ const workspacePath = import_node_path11.default.join(current, "pnpm-workspace.yaml");
1512
1525
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1513
1526
  return current;
1514
1527
  }
1515
- const parent = import_node_path10.default.dirname(current);
1528
+ const parent = import_node_path11.default.dirname(current);
1516
1529
  if (parent === current) {
1517
1530
  break;
1518
1531
  }
1519
1532
  current = parent;
1520
1533
  }
1521
- return import_node_path10.default.resolve(startDir);
1534
+ return import_node_path11.default.resolve(startDir);
1522
1535
  }
1523
1536
 
1524
1537
  // src/cli/lib/logger.ts
@@ -1560,8 +1573,8 @@ async function runDoctor(options) {
1560
1573
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1561
1574
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1562
1575
  if (options.outPath) {
1563
- const outAbs = import_node_path11.default.isAbsolute(options.outPath) ? options.outPath : import_node_path11.default.resolve(process.cwd(), options.outPath);
1564
- await (0, import_promises10.mkdir)(import_node_path11.default.dirname(outAbs), { recursive: true });
1576
+ const outAbs = import_node_path12.default.isAbsolute(options.outPath) ? options.outPath : import_node_path12.default.resolve(process.cwd(), options.outPath);
1577
+ await (0, import_promises10.mkdir)(import_node_path12.default.dirname(outAbs), { recursive: true });
1565
1578
  await (0, import_promises10.writeFile)(outAbs, `${output}
1566
1579
  `, "utf-8");
1567
1580
  info(`doctor: wrote ${outAbs}`);
@@ -1581,11 +1594,11 @@ function shouldFailDoctor(summary, failOn) {
1581
1594
  }
1582
1595
 
1583
1596
  // src/cli/commands/init.ts
1584
- var import_node_path13 = __toESM(require("path"), 1);
1597
+ var import_node_path14 = __toESM(require("path"), 1);
1585
1598
 
1586
1599
  // src/cli/lib/fs.ts
1587
1600
  var import_promises11 = require("fs/promises");
1588
- var import_node_path12 = __toESM(require("path"), 1);
1601
+ var import_node_path13 = __toESM(require("path"), 1);
1589
1602
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1590
1603
  const files = await collectTemplateFiles(sourceRoot);
1591
1604
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1593,7 +1606,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1593
1606
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1594
1607
  const allFiles = [];
1595
1608
  for (const relPath of relativePaths) {
1596
- const fullPath = import_node_path12.default.join(sourceRoot, relPath);
1609
+ const fullPath = import_node_path13.default.join(sourceRoot, relPath);
1597
1610
  const files = await collectTemplateFiles(fullPath);
1598
1611
  allFiles.push(...files);
1599
1612
  }
@@ -1603,13 +1616,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1603
1616
  const copied = [];
1604
1617
  const skipped = [];
1605
1618
  const conflicts = [];
1606
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path12.default.sep);
1607
- const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path12.default.sep);
1619
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
1620
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
1608
1621
  const isProtectedRelative = (relative) => {
1609
1622
  if (protectPrefixes.length === 0) {
1610
1623
  return false;
1611
1624
  }
1612
- const normalized = relative.replace(/[\\/]+/g, import_node_path12.default.sep);
1625
+ const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
1613
1626
  return protectPrefixes.some(
1614
1627
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1615
1628
  );
@@ -1618,7 +1631,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1618
1631
  if (excludePrefixes.length === 0) {
1619
1632
  return false;
1620
1633
  }
1621
- const normalized = relative.replace(/[\\/]+/g, import_node_path12.default.sep);
1634
+ const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
1622
1635
  return excludePrefixes.some(
1623
1636
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1624
1637
  );
@@ -1626,14 +1639,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1626
1639
  const conflictPolicy = options.conflictPolicy ?? "error";
1627
1640
  if (!options.force && conflictPolicy === "error") {
1628
1641
  for (const file of files) {
1629
- const relative = import_node_path12.default.relative(sourceRoot, file);
1642
+ const relative = import_node_path13.default.relative(sourceRoot, file);
1630
1643
  if (isExcludedRelative(relative)) {
1631
1644
  continue;
1632
1645
  }
1633
1646
  if (isProtectedRelative(relative)) {
1634
1647
  continue;
1635
1648
  }
1636
- const dest = import_node_path12.default.join(destRoot, relative);
1649
+ const dest = import_node_path13.default.join(destRoot, relative);
1637
1650
  if (!await shouldWrite(dest, options.force)) {
1638
1651
  conflicts.push(dest);
1639
1652
  }
@@ -1643,18 +1656,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1643
1656
  }
1644
1657
  }
1645
1658
  for (const file of files) {
1646
- const relative = import_node_path12.default.relative(sourceRoot, file);
1659
+ const relative = import_node_path13.default.relative(sourceRoot, file);
1647
1660
  if (isExcludedRelative(relative)) {
1648
1661
  continue;
1649
1662
  }
1650
- const dest = import_node_path12.default.join(destRoot, relative);
1663
+ const dest = import_node_path13.default.join(destRoot, relative);
1651
1664
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1652
1665
  if (!await shouldWrite(dest, forceForThisFile)) {
1653
1666
  skipped.push(dest);
1654
1667
  continue;
1655
1668
  }
1656
1669
  if (!options.dryRun) {
1657
- await (0, import_promises11.mkdir)(import_node_path12.default.dirname(dest), { recursive: true });
1670
+ await (0, import_promises11.mkdir)(import_node_path13.default.dirname(dest), { recursive: true });
1658
1671
  await (0, import_promises11.copyFile)(file, dest);
1659
1672
  }
1660
1673
  copied.push(dest);
@@ -1678,7 +1691,7 @@ async function collectTemplateFiles(root) {
1678
1691
  }
1679
1692
  const items = await (0, import_promises11.readdir)(root, { withFileTypes: true });
1680
1693
  for (const item of items) {
1681
- const fullPath = import_node_path12.default.join(root, item.name);
1694
+ const fullPath = import_node_path13.default.join(root, item.name);
1682
1695
  if (item.isDirectory()) {
1683
1696
  const nested = await collectTemplateFiles(fullPath);
1684
1697
  entries.push(...nested);
@@ -1708,10 +1721,10 @@ async function exists5(target) {
1708
1721
  // src/cli/commands/init.ts
1709
1722
  async function runInit(options) {
1710
1723
  const assetsRoot = getInitAssetsDir();
1711
- const rootAssets = import_node_path13.default.join(assetsRoot, "root");
1712
- const qfaiAssets = import_node_path13.default.join(assetsRoot, ".qfai");
1713
- const destRoot = import_node_path13.default.resolve(options.dir);
1714
- const destQfai = import_node_path13.default.join(destRoot, ".qfai");
1724
+ const rootAssets = import_node_path14.default.join(assetsRoot, "root");
1725
+ const qfaiAssets = import_node_path14.default.join(assetsRoot, ".qfai");
1726
+ const destRoot = import_node_path14.default.resolve(options.dir);
1727
+ const destQfai = import_node_path14.default.join(destRoot, ".qfai");
1715
1728
  if (options.force) {
1716
1729
  info(
1717
1730
  "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"
@@ -1759,7 +1772,7 @@ function report(copied, skipped, dryRun, label) {
1759
1772
 
1760
1773
  // src/cli/commands/report.ts
1761
1774
  var import_promises20 = require("fs/promises");
1762
- var import_node_path20 = __toESM(require("path"), 1);
1775
+ var import_node_path22 = __toESM(require("path"), 1);
1763
1776
 
1764
1777
  // src/core/normalize.ts
1765
1778
  function normalizeIssuePaths(root, issues) {
@@ -1800,15 +1813,15 @@ function normalizeValidationResult(root, result) {
1800
1813
 
1801
1814
  // src/core/report.ts
1802
1815
  var import_promises19 = require("fs/promises");
1803
- var import_node_path19 = __toESM(require("path"), 1);
1816
+ var import_node_path21 = __toESM(require("path"), 1);
1804
1817
 
1805
1818
  // src/core/contractIndex.ts
1806
1819
  var import_promises12 = require("fs/promises");
1807
- var import_node_path14 = __toESM(require("path"), 1);
1820
+ var import_node_path15 = __toESM(require("path"), 1);
1808
1821
 
1809
1822
  // src/core/contractsDecl.ts
1810
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1811
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1823
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
1824
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
1812
1825
  function extractDeclaredContractIds(text) {
1813
1826
  const ids = [];
1814
1827
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -1826,20 +1839,22 @@ function stripContractDeclarationLines(text) {
1826
1839
  // src/core/contractIndex.ts
1827
1840
  async function buildContractIndex(root, config) {
1828
1841
  const contractsRoot = resolvePath(root, config, "contractsDir");
1829
- const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
1830
- const apiRoot = import_node_path14.default.join(contractsRoot, "api");
1831
- const dbRoot = import_node_path14.default.join(contractsRoot, "db");
1832
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1842
+ const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
1843
+ const apiRoot = import_node_path15.default.join(contractsRoot, "api");
1844
+ const dbRoot = import_node_path15.default.join(contractsRoot, "db");
1845
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
1833
1846
  collectUiContractFiles(uiRoot),
1847
+ collectThemaContractFiles(uiRoot),
1834
1848
  collectApiContractFiles(apiRoot),
1835
1849
  collectDbContractFiles(dbRoot)
1836
1850
  ]);
1837
1851
  const index = {
1838
1852
  ids: /* @__PURE__ */ new Set(),
1839
1853
  idToFiles: /* @__PURE__ */ new Map(),
1840
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1854
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
1841
1855
  };
1842
1856
  await indexContractFiles(uiFiles, index);
1857
+ await indexContractFiles(themaFiles, index);
1843
1858
  await indexContractFiles(apiFiles, index);
1844
1859
  await indexContractFiles(dbFiles, index);
1845
1860
  return index;
@@ -1858,7 +1873,15 @@ function record(index, id, file) {
1858
1873
  }
1859
1874
 
1860
1875
  // src/core/ids.ts
1861
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1876
+ var ID_PREFIXES = [
1877
+ "SPEC",
1878
+ "BR",
1879
+ "SC",
1880
+ "UI",
1881
+ "API",
1882
+ "DB",
1883
+ "THEMA"
1884
+ ];
1862
1885
  var STRICT_ID_PATTERNS = {
1863
1886
  SPEC: /\bSPEC-\d{4}\b/g,
1864
1887
  BR: /\bBR-\d{4}\b/g,
@@ -1866,6 +1889,7 @@ var STRICT_ID_PATTERNS = {
1866
1889
  UI: /\bUI-\d{4}\b/g,
1867
1890
  API: /\bAPI-\d{4}\b/g,
1868
1891
  DB: /\bDB-\d{4}\b/g,
1892
+ THEMA: /\bTHEMA-\d{3}\b/g,
1869
1893
  ADR: /\bADR-\d{4}\b/g
1870
1894
  };
1871
1895
  var LOOSE_ID_PATTERNS = {
@@ -1875,6 +1899,7 @@ var LOOSE_ID_PATTERNS = {
1875
1899
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1876
1900
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1877
1901
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1902
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
1878
1903
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1879
1904
  };
1880
1905
  function extractIds(text, prefix) {
@@ -1911,7 +1936,7 @@ function isValidId(value, prefix) {
1911
1936
  }
1912
1937
 
1913
1938
  // src/core/parse/contractRefs.ts
1914
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1939
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
1915
1940
  function parseContractRefs(text, options = {}) {
1916
1941
  const linePattern = buildLinePattern(options);
1917
1942
  const lines = [];
@@ -2082,13 +2107,13 @@ function parseSpec(md, file) {
2082
2107
 
2083
2108
  // src/core/validators/contracts.ts
2084
2109
  var import_promises13 = require("fs/promises");
2085
- var import_node_path16 = __toESM(require("path"), 1);
2110
+ var import_node_path17 = __toESM(require("path"), 1);
2086
2111
 
2087
2112
  // src/core/contracts.ts
2088
- var import_node_path15 = __toESM(require("path"), 1);
2113
+ var import_node_path16 = __toESM(require("path"), 1);
2089
2114
  var import_yaml2 = require("yaml");
2090
2115
  function parseStructuredContract(file, text) {
2091
- const ext = import_node_path15.default.extname(file).toLowerCase();
2116
+ const ext = import_node_path16.default.extname(file).toLowerCase();
2092
2117
  if (ext === ".json") {
2093
2118
  return JSON.parse(text);
2094
2119
  }
@@ -2105,17 +2130,23 @@ var SQL_DANGEROUS_PATTERNS = [
2105
2130
  label: "ALTER TABLE ... DROP"
2106
2131
  }
2107
2132
  ];
2133
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
2108
2134
  async function validateContracts(root, config) {
2109
2135
  const issues = [];
2110
- const contractsRoot = resolvePath(root, config, "contractsDir");
2111
- issues.push(...await validateUiContracts(import_node_path16.default.join(contractsRoot, "ui")));
2112
- issues.push(...await validateApiContracts(import_node_path16.default.join(contractsRoot, "api")));
2113
- issues.push(...await validateDbContracts(import_node_path16.default.join(contractsRoot, "db")));
2114
2136
  const contractIndex = await buildContractIndex(root, config);
2137
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2138
+ const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
2139
+ const themaIds = new Set(
2140
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
2141
+ );
2142
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
2143
+ issues.push(...await validateThemaContracts(uiRoot));
2144
+ issues.push(...await validateApiContracts(import_node_path17.default.join(contractsRoot, "api")));
2145
+ issues.push(...await validateDbContracts(import_node_path17.default.join(contractsRoot, "db")));
2115
2146
  issues.push(...validateDuplicateContractIds(contractIndex));
2116
2147
  return issues;
2117
2148
  }
2118
- async function validateUiContracts(uiRoot) {
2149
+ async function validateUiContracts(uiRoot, themaIds) {
2119
2150
  const files = await collectUiContractFiles(uiRoot);
2120
2151
  if (files.length === 0) {
2121
2152
  return [
@@ -2131,6 +2162,22 @@ async function validateUiContracts(uiRoot) {
2131
2162
  const issues = [];
2132
2163
  for (const file of files) {
2133
2164
  const text = await (0, import_promises13.readFile)(file, "utf-8");
2165
+ const declaredIds = extractDeclaredContractIds(text);
2166
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2167
+ let doc = null;
2168
+ try {
2169
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2170
+ } catch (error2) {
2171
+ issues.push(
2172
+ issue(
2173
+ "QFAI-CONTRACT-001",
2174
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2175
+ "error",
2176
+ file,
2177
+ "contracts.ui.parse"
2178
+ )
2179
+ );
2180
+ }
2134
2181
  const invalidIds = extractInvalidIds(text, [
2135
2182
  "SPEC",
2136
2183
  "BR",
@@ -2138,6 +2185,45 @@ async function validateUiContracts(uiRoot) {
2138
2185
  "UI",
2139
2186
  "API",
2140
2187
  "DB",
2188
+ "THEMA",
2189
+ "ADR"
2190
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
2191
+ if (invalidIds.length > 0) {
2192
+ issues.push(
2193
+ issue(
2194
+ "QFAI-ID-002",
2195
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
2196
+ "error",
2197
+ file,
2198
+ "id.format",
2199
+ invalidIds
2200
+ )
2201
+ );
2202
+ }
2203
+ if (doc) {
2204
+ issues.push(
2205
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
2206
+ );
2207
+ }
2208
+ }
2209
+ return issues;
2210
+ }
2211
+ async function validateThemaContracts(uiRoot) {
2212
+ const files = await collectThemaContractFiles(uiRoot);
2213
+ if (files.length === 0) {
2214
+ return [];
2215
+ }
2216
+ const issues = [];
2217
+ for (const file of files) {
2218
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2219
+ const invalidIds = extractInvalidIds(text, [
2220
+ "SPEC",
2221
+ "BR",
2222
+ "SC",
2223
+ "UI",
2224
+ "API",
2225
+ "DB",
2226
+ "THEMA",
2141
2227
  "ADR"
2142
2228
  ]);
2143
2229
  if (invalidIds.length > 0) {
@@ -2153,17 +2239,95 @@ async function validateUiContracts(uiRoot) {
2153
2239
  );
2154
2240
  }
2155
2241
  const declaredIds = extractDeclaredContractIds(text);
2156
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2242
+ if (declaredIds.length === 0) {
2243
+ issues.push(
2244
+ issue(
2245
+ "QFAI-THEMA-010",
2246
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
2247
+ "error",
2248
+ file,
2249
+ "contracts.thema.declaration"
2250
+ )
2251
+ );
2252
+ continue;
2253
+ }
2254
+ if (declaredIds.length > 1) {
2255
+ issues.push(
2256
+ issue(
2257
+ "QFAI-THEMA-011",
2258
+ `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(
2259
+ ", "
2260
+ )}`,
2261
+ "error",
2262
+ file,
2263
+ "contracts.thema.declaration",
2264
+ declaredIds
2265
+ )
2266
+ );
2267
+ continue;
2268
+ }
2269
+ const declaredId = declaredIds[0] ?? "";
2270
+ if (!THEMA_ID_RE.test(declaredId)) {
2271
+ issues.push(
2272
+ issue(
2273
+ "QFAI-THEMA-012",
2274
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
2275
+ "error",
2276
+ file,
2277
+ "contracts.thema.idFormat",
2278
+ [declaredId]
2279
+ )
2280
+ );
2281
+ }
2282
+ let doc;
2157
2283
  try {
2158
- parseStructuredContract(file, stripContractDeclarationLines(text));
2284
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2159
2285
  } catch (error2) {
2160
2286
  issues.push(
2161
2287
  issue(
2162
- "QFAI-CONTRACT-001",
2163
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2288
+ "QFAI-THEMA-001",
2289
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2164
2290
  "error",
2165
2291
  file,
2166
- "contracts.ui.parse"
2292
+ "contracts.thema.parse"
2293
+ )
2294
+ );
2295
+ continue;
2296
+ }
2297
+ const docId = typeof doc.id === "string" ? doc.id : "";
2298
+ if (!THEMA_ID_RE.test(docId)) {
2299
+ issues.push(
2300
+ issue(
2301
+ "QFAI-THEMA-012",
2302
+ 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",
2303
+ "error",
2304
+ file,
2305
+ "contracts.thema.idFormat",
2306
+ docId.length > 0 ? [docId] : void 0
2307
+ )
2308
+ );
2309
+ }
2310
+ const name = typeof doc.name === "string" ? doc.name : "";
2311
+ if (!name) {
2312
+ issues.push(
2313
+ issue(
2314
+ "QFAI-THEMA-014",
2315
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2316
+ "error",
2317
+ file,
2318
+ "contracts.thema.name"
2319
+ )
2320
+ );
2321
+ }
2322
+ if (declaredId && docId && declaredId !== docId) {
2323
+ issues.push(
2324
+ issue(
2325
+ "QFAI-THEMA-013",
2326
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
2327
+ "error",
2328
+ file,
2329
+ "contracts.thema.idMismatch",
2330
+ [declaredId, docId]
2167
2331
  )
2168
2332
  );
2169
2333
  }
@@ -2193,6 +2357,7 @@ async function validateApiContracts(apiRoot) {
2193
2357
  "UI",
2194
2358
  "API",
2195
2359
  "DB",
2360
+ "THEMA",
2196
2361
  "ADR"
2197
2362
  ]);
2198
2363
  if (invalidIds.length > 0) {
@@ -2261,6 +2426,7 @@ async function validateDbContracts(dbRoot) {
2261
2426
  "UI",
2262
2427
  "API",
2263
2428
  "DB",
2429
+ "THEMA",
2264
2430
  "ADR"
2265
2431
  ]);
2266
2432
  if (invalidIds.length > 0) {
@@ -2367,6 +2533,278 @@ function validateDuplicateContractIds(contractIndex) {
2367
2533
  function hasOpenApi(doc) {
2368
2534
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
2369
2535
  }
2536
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
2537
+ const issues = [];
2538
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
2539
+ const themaRef = doc.themaRef;
2540
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
2541
+ issues.push(
2542
+ issue(
2543
+ "QFAI-UI-020",
2544
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2545
+ "error",
2546
+ file,
2547
+ "contracts.ui.themaRef"
2548
+ )
2549
+ );
2550
+ } else if (!THEMA_ID_RE.test(themaRef)) {
2551
+ issues.push(
2552
+ issue(
2553
+ "QFAI-UI-020",
2554
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
2555
+ "error",
2556
+ file,
2557
+ "contracts.ui.themaRef",
2558
+ [themaRef]
2559
+ )
2560
+ );
2561
+ } else if (!themaIds.has(themaRef)) {
2562
+ issues.push(
2563
+ issue(
2564
+ "QFAI-UI-020",
2565
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
2566
+ "error",
2567
+ file,
2568
+ "contracts.ui.themaRef",
2569
+ [themaRef]
2570
+ )
2571
+ );
2572
+ }
2573
+ }
2574
+ const assets = doc.assets;
2575
+ if (assets && typeof assets === "object") {
2576
+ issues.push(
2577
+ ...await validateUiAssets(
2578
+ assets,
2579
+ file,
2580
+ uiRoot
2581
+ )
2582
+ );
2583
+ }
2584
+ return issues;
2585
+ }
2586
+ async function validateUiAssets(assets, file, uiRoot) {
2587
+ const issues = [];
2588
+ const packValue = assets.pack;
2589
+ const useValue = assets.use;
2590
+ if (packValue === void 0 && useValue === void 0) {
2591
+ return issues;
2592
+ }
2593
+ if (typeof packValue !== "string" || packValue.length === 0) {
2594
+ issues.push(
2595
+ issue(
2596
+ "QFAI-ASSET-001",
2597
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2598
+ "error",
2599
+ file,
2600
+ "assets.pack"
2601
+ )
2602
+ );
2603
+ return issues;
2604
+ }
2605
+ if (!isSafeRelativePath(packValue)) {
2606
+ issues.push(
2607
+ issue(
2608
+ "QFAI-ASSET-001",
2609
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
2610
+ "error",
2611
+ file,
2612
+ "assets.pack",
2613
+ [packValue]
2614
+ )
2615
+ );
2616
+ return issues;
2617
+ }
2618
+ const packDir = import_node_path17.default.resolve(uiRoot, packValue);
2619
+ const packRelative = import_node_path17.default.relative(uiRoot, packDir);
2620
+ if (packRelative.startsWith("..") || import_node_path17.default.isAbsolute(packRelative)) {
2621
+ issues.push(
2622
+ issue(
2623
+ "QFAI-ASSET-001",
2624
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
2625
+ "error",
2626
+ file,
2627
+ "assets.pack",
2628
+ [packValue]
2629
+ )
2630
+ );
2631
+ return issues;
2632
+ }
2633
+ if (!await exists6(packDir)) {
2634
+ issues.push(
2635
+ issue(
2636
+ "QFAI-ASSET-001",
2637
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
2638
+ "error",
2639
+ file,
2640
+ "assets.pack",
2641
+ [packValue]
2642
+ )
2643
+ );
2644
+ return issues;
2645
+ }
2646
+ const assetsYamlPath = import_node_path17.default.join(packDir, "assets.yaml");
2647
+ if (!await exists6(assetsYamlPath)) {
2648
+ issues.push(
2649
+ issue(
2650
+ "QFAI-ASSET-002",
2651
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
2652
+ "error",
2653
+ assetsYamlPath,
2654
+ "assets.yaml"
2655
+ )
2656
+ );
2657
+ return issues;
2658
+ }
2659
+ let manifest;
2660
+ try {
2661
+ const manifestText = await (0, import_promises13.readFile)(assetsYamlPath, "utf-8");
2662
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
2663
+ } catch (error2) {
2664
+ issues.push(
2665
+ issue(
2666
+ "QFAI-ASSET-002",
2667
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
2668
+ "error",
2669
+ assetsYamlPath,
2670
+ "assets.yaml"
2671
+ )
2672
+ );
2673
+ return issues;
2674
+ }
2675
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
2676
+ const itemIds = /* @__PURE__ */ new Set();
2677
+ const itemPaths = [];
2678
+ for (const item of items) {
2679
+ if (!item || typeof item !== "object") {
2680
+ continue;
2681
+ }
2682
+ const record2 = item;
2683
+ const id = typeof record2.id === "string" ? record2.id : void 0;
2684
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
2685
+ if (id) {
2686
+ itemIds.add(id);
2687
+ }
2688
+ itemPaths.push({ id, path: pathValue });
2689
+ }
2690
+ if (useValue !== void 0) {
2691
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
2692
+ issues.push(
2693
+ issue(
2694
+ "QFAI-ASSET-003",
2695
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2696
+ "error",
2697
+ file,
2698
+ "assets.use"
2699
+ )
2700
+ );
2701
+ } else {
2702
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
2703
+ if (missing.length > 0) {
2704
+ issues.push(
2705
+ issue(
2706
+ "QFAI-ASSET-003",
2707
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
2708
+ "error",
2709
+ file,
2710
+ "assets.use",
2711
+ missing
2712
+ )
2713
+ );
2714
+ }
2715
+ }
2716
+ }
2717
+ for (const entry of itemPaths) {
2718
+ if (!entry.path) {
2719
+ continue;
2720
+ }
2721
+ if (!isSafeRelativePath(entry.path)) {
2722
+ issues.push(
2723
+ issue(
2724
+ "QFAI-ASSET-004",
2725
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
2726
+ "error",
2727
+ assetsYamlPath,
2728
+ "assets.path",
2729
+ entry.id ? [entry.id] : [entry.path]
2730
+ )
2731
+ );
2732
+ continue;
2733
+ }
2734
+ const assetPath = import_node_path17.default.resolve(packDir, entry.path);
2735
+ const assetRelative = import_node_path17.default.relative(packDir, assetPath);
2736
+ if (assetRelative.startsWith("..") || import_node_path17.default.isAbsolute(assetRelative)) {
2737
+ issues.push(
2738
+ issue(
2739
+ "QFAI-ASSET-004",
2740
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
2741
+ "error",
2742
+ assetsYamlPath,
2743
+ "assets.path",
2744
+ entry.id ? [entry.id] : [entry.path]
2745
+ )
2746
+ );
2747
+ continue;
2748
+ }
2749
+ if (!await exists6(assetPath)) {
2750
+ issues.push(
2751
+ issue(
2752
+ "QFAI-ASSET-004",
2753
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
2754
+ "error",
2755
+ assetsYamlPath,
2756
+ "assets.path",
2757
+ entry.id ? [entry.id] : [entry.path]
2758
+ )
2759
+ );
2760
+ }
2761
+ }
2762
+ return issues;
2763
+ }
2764
+ function shouldIgnoreInvalidId(value, doc) {
2765
+ if (!doc) {
2766
+ return false;
2767
+ }
2768
+ const assets = doc.assets;
2769
+ if (!assets || typeof assets !== "object") {
2770
+ return false;
2771
+ }
2772
+ const packValue = assets.pack;
2773
+ if (typeof packValue !== "string" || packValue.length === 0) {
2774
+ return false;
2775
+ }
2776
+ const normalized = packValue.replace(/\\/g, "/");
2777
+ const basename = import_node_path17.default.posix.basename(normalized);
2778
+ if (!basename) {
2779
+ return false;
2780
+ }
2781
+ return value.toLowerCase() === basename.toLowerCase();
2782
+ }
2783
+ function isSafeRelativePath(value) {
2784
+ if (!value) {
2785
+ return false;
2786
+ }
2787
+ if (import_node_path17.default.isAbsolute(value)) {
2788
+ return false;
2789
+ }
2790
+ const normalized = value.replace(/\\/g, "/");
2791
+ if (/^[A-Za-z]:/.test(normalized)) {
2792
+ return false;
2793
+ }
2794
+ const segments = normalized.split("/");
2795
+ if (segments.some((segment) => segment === "..")) {
2796
+ return false;
2797
+ }
2798
+ return true;
2799
+ }
2800
+ async function exists6(target) {
2801
+ try {
2802
+ await (0, import_promises13.access)(target);
2803
+ return true;
2804
+ } catch {
2805
+ return false;
2806
+ }
2807
+ }
2370
2808
  function formatError4(error2) {
2371
2809
  if (error2 instanceof Error) {
2372
2810
  return error2.message;
@@ -2397,7 +2835,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2397
2835
 
2398
2836
  // src/core/validators/delta.ts
2399
2837
  var import_promises14 = require("fs/promises");
2400
- var import_node_path17 = __toESM(require("path"), 1);
2838
+ var import_node_path18 = __toESM(require("path"), 1);
2401
2839
  var SECTION_RE = /^##\s+変更区分/m;
2402
2840
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2403
2841
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2411,7 +2849,7 @@ async function validateDeltas(root, config) {
2411
2849
  }
2412
2850
  const issues = [];
2413
2851
  for (const pack of packs) {
2414
- const deltaPath = import_node_path17.default.join(pack, "delta.md");
2852
+ const deltaPath = import_node_path18.default.join(pack, "delta.md");
2415
2853
  let text;
2416
2854
  try {
2417
2855
  text = await (0, import_promises14.readFile)(deltaPath, "utf-8");
@@ -2491,7 +2929,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2491
2929
 
2492
2930
  // src/core/validators/ids.ts
2493
2931
  var import_promises15 = require("fs/promises");
2494
- var import_node_path18 = __toESM(require("path"), 1);
2932
+ var import_node_path19 = __toESM(require("path"), 1);
2495
2933
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2496
2934
  async function validateDefinedIds(root, config) {
2497
2935
  const issues = [];
@@ -2557,7 +2995,7 @@ function recordId(out, id, file) {
2557
2995
  }
2558
2996
  function formatFileList(files, root) {
2559
2997
  return files.map((file) => {
2560
- const relative = import_node_path18.default.relative(root, file);
2998
+ const relative = import_node_path19.default.relative(root, file);
2561
2999
  return relative.length > 0 ? relative : file;
2562
3000
  }).join(", ");
2563
3001
  }
@@ -2616,6 +3054,7 @@ async function validatePromptsIntegrity(root) {
2616
3054
 
2617
3055
  // src/core/validators/scenario.ts
2618
3056
  var import_promises16 = require("fs/promises");
3057
+ var import_node_path20 = __toESM(require("path"), 1);
2619
3058
  var GIVEN_PATTERN = /\bGiven\b/;
2620
3059
  var WHEN_PATTERN = /\bWhen\b/;
2621
3060
  var THEN_PATTERN = /\bThen\b/;
@@ -2638,6 +3077,18 @@ async function validateScenarios(root, config) {
2638
3077
  }
2639
3078
  const issues = [];
2640
3079
  for (const entry of entries) {
3080
+ const legacyScenarioPath = import_node_path20.default.join(entry.dir, "scenario.md");
3081
+ if (await fileExists(legacyScenarioPath)) {
3082
+ issues.push(
3083
+ issue4(
3084
+ "QFAI-SC-004",
3085
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3086
+ "error",
3087
+ legacyScenarioPath,
3088
+ "scenario.legacy"
3089
+ )
3090
+ );
3091
+ }
2641
3092
  let text;
2642
3093
  try {
2643
3094
  text = await (0, import_promises16.readFile)(entry.scenarioPath, "utf-8");
@@ -2669,6 +3120,7 @@ function validateScenarioContent(text, file) {
2669
3120
  "UI",
2670
3121
  "API",
2671
3122
  "DB",
3123
+ "THEMA",
2672
3124
  "ADR"
2673
3125
  ]);
2674
3126
  if (invalidIds.length > 0) {
@@ -2812,6 +3264,14 @@ function isMissingFileError3(error2) {
2812
3264
  }
2813
3265
  return error2.code === "ENOENT";
2814
3266
  }
3267
+ async function fileExists(target) {
3268
+ try {
3269
+ await (0, import_promises16.access)(target);
3270
+ return true;
3271
+ } catch {
3272
+ return false;
3273
+ }
3274
+ }
2815
3275
 
2816
3276
  // src/core/validators/spec.ts
2817
3277
  var import_promises17 = require("fs/promises");
@@ -2871,6 +3331,7 @@ function validateSpecContent(text, file, requiredSections) {
2871
3331
  "UI",
2872
3332
  "API",
2873
3333
  "DB",
3334
+ "THEMA",
2874
3335
  "ADR"
2875
3336
  ]);
2876
3337
  if (invalidIds.length > 0) {
@@ -3050,7 +3511,7 @@ async function validateTraceability(root, config) {
3050
3511
  "QFAI-TRACE-021",
3051
3512
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
3052
3513
  ", "
3053
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3514
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3054
3515
  "error",
3055
3516
  file,
3056
3517
  "traceability.specContractRefFormat",
@@ -3114,7 +3575,7 @@ async function validateTraceability(root, config) {
3114
3575
  "QFAI-TRACE-032",
3115
3576
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
3116
3577
  ", "
3117
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3578
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3118
3579
  "error",
3119
3580
  file,
3120
3581
  "traceability.scenarioContractRefFormat",
@@ -3493,17 +3954,25 @@ function countIssues(issues) {
3493
3954
  }
3494
3955
 
3495
3956
  // src/core/report.ts
3496
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3957
+ var ID_PREFIXES2 = [
3958
+ "SPEC",
3959
+ "BR",
3960
+ "SC",
3961
+ "UI",
3962
+ "API",
3963
+ "DB",
3964
+ "THEMA"
3965
+ ];
3497
3966
  async function createReportData(root, validation, configResult) {
3498
- const resolvedRoot = import_node_path19.default.resolve(root);
3967
+ const resolvedRoot = import_node_path21.default.resolve(root);
3499
3968
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3500
3969
  const config = resolved.config;
3501
3970
  const configPath = resolved.configPath;
3502
3971
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3503
3972
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3504
- const apiRoot = import_node_path19.default.join(contractsRoot, "api");
3505
- const uiRoot = import_node_path19.default.join(contractsRoot, "ui");
3506
- const dbRoot = import_node_path19.default.join(contractsRoot, "db");
3973
+ const apiRoot = import_node_path21.default.join(contractsRoot, "api");
3974
+ const uiRoot = import_node_path21.default.join(contractsRoot, "ui");
3975
+ const dbRoot = import_node_path21.default.join(contractsRoot, "db");
3507
3976
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3508
3977
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3509
3978
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3511,7 +3980,8 @@ async function createReportData(root, validation, configResult) {
3511
3980
  const {
3512
3981
  api: apiFiles,
3513
3982
  ui: uiFiles,
3514
- db: dbFiles
3983
+ db: dbFiles,
3984
+ thema: themaFiles
3515
3985
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
3516
3986
  const contractIndex = await buildContractIndex(resolvedRoot, config);
3517
3987
  const contractIdList = Array.from(contractIndex.ids);
@@ -3538,7 +4008,8 @@ async function createReportData(root, validation, configResult) {
3538
4008
  ...scenarioFiles,
3539
4009
  ...apiFiles,
3540
4010
  ...uiFiles,
3541
- ...dbFiles
4011
+ ...dbFiles,
4012
+ ...themaFiles
3542
4013
  ]);
3543
4014
  const upstreamIds = await collectUpstreamIds([
3544
4015
  ...specFiles,
@@ -3575,7 +4046,8 @@ async function createReportData(root, validation, configResult) {
3575
4046
  contracts: {
3576
4047
  api: apiFiles.length,
3577
4048
  ui: uiFiles.length,
3578
- db: dbFiles.length
4049
+ db: dbFiles.length,
4050
+ thema: themaFiles.length
3579
4051
  },
3580
4052
  counts: normalizedValidation.counts
3581
4053
  },
@@ -3585,7 +4057,8 @@ async function createReportData(root, validation, configResult) {
3585
4057
  sc: idsByPrefix.SC,
3586
4058
  ui: idsByPrefix.UI,
3587
4059
  api: idsByPrefix.API,
3588
- db: idsByPrefix.DB
4060
+ db: idsByPrefix.DB,
4061
+ thema: idsByPrefix.THEMA
3589
4062
  },
3590
4063
  traceability: {
3591
4064
  upstreamIdsFound: upstreamIds.size,
@@ -3655,7 +4128,7 @@ function formatReportMarkdown(data, options = {}) {
3655
4128
  lines.push(`- specs: ${data.summary.specs}`);
3656
4129
  lines.push(`- scenarios: ${data.summary.scenarios}`);
3657
4130
  lines.push(
3658
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
4131
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
3659
4132
  );
3660
4133
  lines.push(
3661
4134
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3799,6 +4272,7 @@ function formatReportMarkdown(data, options = {}) {
3799
4272
  lines.push(formatIdLine("UI", data.ids.ui));
3800
4273
  lines.push(formatIdLine("API", data.ids.api));
3801
4274
  lines.push(formatIdLine("DB", data.ids.db));
4275
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3802
4276
  lines.push("");
3803
4277
  lines.push("## Traceability");
3804
4278
  lines.push("");
@@ -4020,7 +4494,8 @@ async function collectIds(files) {
4020
4494
  SC: /* @__PURE__ */ new Set(),
4021
4495
  UI: /* @__PURE__ */ new Set(),
4022
4496
  API: /* @__PURE__ */ new Set(),
4023
- DB: /* @__PURE__ */ new Set()
4497
+ DB: /* @__PURE__ */ new Set(),
4498
+ THEMA: /* @__PURE__ */ new Set()
4024
4499
  };
4025
4500
  for (const file of files) {
4026
4501
  const text = await (0, import_promises19.readFile)(file, "utf-8");
@@ -4035,7 +4510,8 @@ async function collectIds(files) {
4035
4510
  SC: toSortedArray2(result.SC),
4036
4511
  UI: toSortedArray2(result.UI),
4037
4512
  API: toSortedArray2(result.API),
4038
- DB: toSortedArray2(result.DB)
4513
+ DB: toSortedArray2(result.DB),
4514
+ THEMA: toSortedArray2(result.THEMA)
4039
4515
  };
4040
4516
  }
4041
4517
  async function collectUpstreamIds(files) {
@@ -4199,7 +4675,7 @@ function warnIfTruncated(scan, context) {
4199
4675
 
4200
4676
  // src/cli/commands/report.ts
4201
4677
  async function runReport(options) {
4202
- const root = import_node_path20.default.resolve(options.root);
4678
+ const root = import_node_path22.default.resolve(options.root);
4203
4679
  const configResult = await loadConfig(root);
4204
4680
  let validation;
4205
4681
  if (options.runValidate) {
@@ -4216,7 +4692,7 @@ async function runReport(options) {
4216
4692
  validation = normalized;
4217
4693
  } else {
4218
4694
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
4219
- const inputPath = import_node_path20.default.isAbsolute(input) ? input : import_node_path20.default.resolve(root, input);
4695
+ const inputPath = import_node_path22.default.isAbsolute(input) ? input : import_node_path22.default.resolve(root, input);
4220
4696
  try {
4221
4697
  validation = await readValidationResult(inputPath);
4222
4698
  } catch (err) {
@@ -4243,10 +4719,10 @@ async function runReport(options) {
4243
4719
  warnIfTruncated(data.traceability.testFiles, "report");
4244
4720
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4245
4721
  const outRoot = resolvePath(root, configResult.config, "outDir");
4246
- const defaultOut = options.format === "json" ? import_node_path20.default.join(outRoot, "report.json") : import_node_path20.default.join(outRoot, "report.md");
4722
+ const defaultOut = options.format === "json" ? import_node_path22.default.join(outRoot, "report.json") : import_node_path22.default.join(outRoot, "report.md");
4247
4723
  const out = options.outPath ?? defaultOut;
4248
- const outPath = import_node_path20.default.isAbsolute(out) ? out : import_node_path20.default.resolve(root, out);
4249
- await (0, import_promises20.mkdir)(import_node_path20.default.dirname(outPath), { recursive: true });
4724
+ const outPath = import_node_path22.default.isAbsolute(out) ? out : import_node_path22.default.resolve(root, out);
4725
+ await (0, import_promises20.mkdir)(import_node_path22.default.dirname(outPath), { recursive: true });
4250
4726
  await (0, import_promises20.writeFile)(outPath, `${output}
4251
4727
  `, "utf-8");
4252
4728
  info(
@@ -4311,15 +4787,15 @@ function isMissingFileError5(error2) {
4311
4787
  return record2.code === "ENOENT";
4312
4788
  }
4313
4789
  async function writeValidationResult(root, outputPath, result) {
4314
- const abs = import_node_path20.default.isAbsolute(outputPath) ? outputPath : import_node_path20.default.resolve(root, outputPath);
4315
- await (0, import_promises20.mkdir)(import_node_path20.default.dirname(abs), { recursive: true });
4790
+ const abs = import_node_path22.default.isAbsolute(outputPath) ? outputPath : import_node_path22.default.resolve(root, outputPath);
4791
+ await (0, import_promises20.mkdir)(import_node_path22.default.dirname(abs), { recursive: true });
4316
4792
  await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4317
4793
  `, "utf-8");
4318
4794
  }
4319
4795
 
4320
4796
  // src/cli/commands/validate.ts
4321
4797
  var import_promises21 = require("fs/promises");
4322
- var import_node_path21 = __toESM(require("path"), 1);
4798
+ var import_node_path23 = __toESM(require("path"), 1);
4323
4799
 
4324
4800
  // src/cli/lib/failOn.ts
4325
4801
  function shouldFail(result, failOn) {
@@ -4334,7 +4810,7 @@ function shouldFail(result, failOn) {
4334
4810
 
4335
4811
  // src/cli/commands/validate.ts
4336
4812
  async function runValidate(options) {
4337
- const root = import_node_path21.default.resolve(options.root);
4813
+ const root = import_node_path23.default.resolve(options.root);
4338
4814
  const configResult = await loadConfig(root);
4339
4815
  const result = await validateProject(root, configResult);
4340
4816
  const normalized = normalizeValidationResult(root, result);
@@ -4459,12 +4935,12 @@ function issueKey(issue7) {
4459
4935
  }
4460
4936
  async function emitJson(result, root, jsonPath) {
4461
4937
  const abs = resolveJsonPath(root, jsonPath);
4462
- await (0, import_promises21.mkdir)(import_node_path21.default.dirname(abs), { recursive: true });
4938
+ await (0, import_promises21.mkdir)(import_node_path23.default.dirname(abs), { recursive: true });
4463
4939
  await (0, import_promises21.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4464
4940
  `, "utf-8");
4465
4941
  }
4466
4942
  function resolveJsonPath(root, jsonPath) {
4467
- return import_node_path21.default.isAbsolute(jsonPath) ? jsonPath : import_node_path21.default.resolve(root, jsonPath);
4943
+ return import_node_path23.default.isAbsolute(jsonPath) ? jsonPath : import_node_path23.default.resolve(root, jsonPath);
4468
4944
  }
4469
4945
  var GITHUB_ANNOTATION_LIMIT = 100;
4470
4946