qfai 1.0.2 → 1.0.4

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 (39) hide show
  1. package/README.md +13 -12
  2. package/assets/init/.qfai/README.md +2 -7
  3. package/assets/init/.qfai/contracts/README.md +20 -0
  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/promptpack/commands/plan.md +1 -1
  11. package/assets/init/.qfai/promptpack/commands/review.md +1 -2
  12. package/assets/init/.qfai/promptpack/constitution.md +1 -1
  13. package/assets/init/.qfai/prompts/README.md +1 -3
  14. package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +3 -3
  15. package/assets/init/.qfai/prompts/require-to-spec.md +1 -2
  16. package/assets/init/.qfai/specs/README.md +3 -4
  17. package/assets/init/.qfai/specs/spec-0001/delta.md +0 -5
  18. package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
  19. package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
  20. package/assets/init/root/qfai.config.yaml +0 -1
  21. package/dist/cli/index.cjs +596 -162
  22. package/dist/cli/index.cjs.map +1 -1
  23. package/dist/cli/index.mjs +598 -164
  24. package/dist/cli/index.mjs.map +1 -1
  25. package/dist/index.cjs +549 -114
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +3 -2
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.mjs +551 -116
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +1 -1
  32. package/assets/init/.qfai/promptpack/modes/change.md +0 -5
  33. package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
  34. package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
  35. package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
  36. package/assets/init/.qfai/rules/conventions.md +0 -27
  37. package/assets/init/.qfai/rules/pnpm.md +0 -29
  38. package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
  39. package/assets/init/.qfai/samples/analyze/input_bundle.md +0 -54
@@ -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");
@@ -245,7 +245,6 @@ var defaultConfig = {
245
245
  paths: {
246
246
  contractsDir: ".qfai/contracts",
247
247
  specsDir: ".qfai/specs",
248
- rulesDir: ".qfai/rules",
249
248
  outDir: ".qfai/out",
250
249
  promptsDir: ".qfai/prompts",
251
250
  srcDir: "src",
@@ -358,13 +357,6 @@ function normalizePaths(raw, configPath, issues) {
358
357
  configPath,
359
358
  issues
360
359
  ),
361
- rulesDir: readString(
362
- raw.rulesDir,
363
- base.rulesDir,
364
- "paths.rulesDir",
365
- configPath,
366
- issues
367
- ),
368
360
  outDir: readString(
369
361
  raw.outDir,
370
362
  base.outDir,
@@ -639,6 +631,7 @@ function isRecord(value) {
639
631
 
640
632
  // src/core/discovery.ts
641
633
  var import_promises5 = require("fs/promises");
634
+ var import_node_path5 = __toESM(require("path"), 1);
642
635
 
643
636
  // src/core/specLayout.ts
644
637
  var import_promises4 = require("fs/promises");
@@ -686,7 +679,12 @@ async function collectScenarioFiles(specsRoot) {
686
679
  return filterExisting(entries.map((entry) => entry.scenarioPath));
687
680
  }
688
681
  async function collectUiContractFiles(uiRoot) {
689
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
682
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
683
+ return filterByBasenamePrefix(files, "ui-");
684
+ }
685
+ async function collectThemaContractFiles(uiRoot) {
686
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
687
+ return filterByBasenamePrefix(files, "thema-");
690
688
  }
691
689
  async function collectApiContractFiles(apiRoot) {
692
690
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -695,12 +693,13 @@ async function collectDbContractFiles(dbRoot) {
695
693
  return collectFiles(dbRoot, { extensions: [".sql"] });
696
694
  }
697
695
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
698
- const [ui, api, db] = await Promise.all([
696
+ const [ui, thema, api, db] = await Promise.all([
699
697
  collectUiContractFiles(uiRoot),
698
+ collectThemaContractFiles(uiRoot),
700
699
  collectApiContractFiles(apiRoot),
701
700
  collectDbContractFiles(dbRoot)
702
701
  ]);
703
- return { ui, api, db };
702
+ return { ui, thema, api, db };
704
703
  }
705
704
  async function filterExisting(files) {
706
705
  const existing = [];
@@ -719,17 +718,23 @@ async function exists3(target) {
719
718
  return false;
720
719
  }
721
720
  }
721
+ function filterByBasenamePrefix(files, prefix) {
722
+ const lowerPrefix = prefix.toLowerCase();
723
+ return files.filter(
724
+ (file) => import_node_path5.default.basename(file).toLowerCase().startsWith(lowerPrefix)
725
+ );
726
+ }
722
727
 
723
728
  // src/core/paths.ts
724
- var import_node_path5 = __toESM(require("path"), 1);
729
+ var import_node_path6 = __toESM(require("path"), 1);
725
730
  function toRelativePath(root, target) {
726
731
  if (!target) {
727
732
  return target;
728
733
  }
729
- if (!import_node_path5.default.isAbsolute(target)) {
734
+ if (!import_node_path6.default.isAbsolute(target)) {
730
735
  return toPosixPath(target);
731
736
  }
732
- const relative = import_node_path5.default.relative(root, target);
737
+ const relative = import_node_path6.default.relative(root, target);
733
738
  if (!relative) {
734
739
  return ".";
735
740
  }
@@ -741,7 +746,7 @@ function toPosixPath(value) {
741
746
 
742
747
  // src/core/traceability.ts
743
748
  var import_promises6 = require("fs/promises");
744
- var import_node_path6 = __toESM(require("path"), 1);
749
+ var import_node_path7 = __toESM(require("path"), 1);
745
750
 
746
751
  // src/core/gherkin/parse.ts
747
752
  var import_gherkin = require("@cucumber/gherkin");
@@ -969,7 +974,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
969
974
  };
970
975
  }
971
976
  const normalizedFiles = Array.from(
972
- new Set(scanResult.files.map((file) => import_node_path6.default.normalize(file)))
977
+ new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
973
978
  );
974
979
  for (const file of normalizedFiles) {
975
980
  const text = await (0, import_promises6.readFile)(file, "utf-8");
@@ -1032,19 +1037,19 @@ function formatError3(error2) {
1032
1037
 
1033
1038
  // src/core/promptsIntegrity.ts
1034
1039
  var import_promises7 = require("fs/promises");
1035
- var import_node_path8 = __toESM(require("path"), 1);
1040
+ var import_node_path9 = __toESM(require("path"), 1);
1036
1041
 
1037
1042
  // src/shared/assets.ts
1038
1043
  var import_node_fs = require("fs");
1039
- var import_node_path7 = __toESM(require("path"), 1);
1044
+ var import_node_path8 = __toESM(require("path"), 1);
1040
1045
  var import_node_url = require("url");
1041
1046
  function getInitAssetsDir() {
1042
1047
  const base = __filename;
1043
1048
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
1044
- const baseDir = import_node_path7.default.dirname(basePath);
1049
+ const baseDir = import_node_path8.default.dirname(basePath);
1045
1050
  const candidates = [
1046
- import_node_path7.default.resolve(baseDir, "../../../assets/init"),
1047
- import_node_path7.default.resolve(baseDir, "../../assets/init")
1051
+ import_node_path8.default.resolve(baseDir, "../../../assets/init"),
1052
+ import_node_path8.default.resolve(baseDir, "../../assets/init")
1048
1053
  ];
1049
1054
  for (const candidate of candidates) {
1050
1055
  if ((0, import_node_fs.existsSync)(candidate)) {
@@ -1061,11 +1066,12 @@ function getInitAssetsDir() {
1061
1066
  }
1062
1067
 
1063
1068
  // src/core/promptsIntegrity.ts
1069
+ var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
1064
1070
  async function diffProjectPromptsAgainstInitAssets(root) {
1065
- const promptsDir = import_node_path8.default.resolve(root, ".qfai", "prompts");
1071
+ const promptsDir = import_node_path9.default.resolve(root, ".qfai", "prompts");
1066
1072
  let templateDir;
1067
1073
  try {
1068
- templateDir = import_node_path8.default.join(getInitAssetsDir(), ".qfai", "prompts");
1074
+ templateDir = import_node_path9.default.join(getInitAssetsDir(), ".qfai", "prompts");
1069
1075
  } catch {
1070
1076
  return {
1071
1077
  status: "skipped_missing_assets",
@@ -1109,6 +1115,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1109
1115
  extra.push(rel);
1110
1116
  }
1111
1117
  }
1118
+ const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
1112
1119
  const common = intersectKeys(templateByRel, projectByRel);
1113
1120
  for (const rel of common) {
1114
1121
  const templateAbs = templateByRel.get(rel);
@@ -1128,13 +1135,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1128
1135
  changed.push(rel);
1129
1136
  }
1130
1137
  }
1131
- const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
1138
+ const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
1132
1139
  return {
1133
1140
  status,
1134
1141
  promptsDir,
1135
1142
  templateDir,
1136
1143
  missing: missing.sort(),
1137
- extra: extra.sort(),
1144
+ extra: filteredExtra.sort(),
1138
1145
  changed: changed.sort()
1139
1146
  };
1140
1147
  }
@@ -1142,7 +1149,7 @@ function normalizeNewlines(text) {
1142
1149
  return text.replace(/\r\n/g, "\n");
1143
1150
  }
1144
1151
  function toRel(base, abs) {
1145
- const rel = import_node_path8.default.relative(base, abs);
1152
+ const rel = import_node_path9.default.relative(base, abs);
1146
1153
  return rel.replace(/[\\/]+/g, "/");
1147
1154
  }
1148
1155
  function intersectKeys(a, b) {
@@ -1157,11 +1164,11 @@ function intersectKeys(a, b) {
1157
1164
 
1158
1165
  // src/core/version.ts
1159
1166
  var import_promises8 = require("fs/promises");
1160
- var import_node_path9 = __toESM(require("path"), 1);
1167
+ var import_node_path10 = __toESM(require("path"), 1);
1161
1168
  var import_node_url2 = require("url");
1162
1169
  async function resolveToolVersion() {
1163
- if ("1.0.2".length > 0) {
1164
- return "1.0.2";
1170
+ if ("1.0.4".length > 0) {
1171
+ return "1.0.4";
1165
1172
  }
1166
1173
  try {
1167
1174
  const packagePath = resolvePackageJsonPath();
@@ -1176,7 +1183,7 @@ async function resolveToolVersion() {
1176
1183
  function resolvePackageJsonPath() {
1177
1184
  const base = __filename;
1178
1185
  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");
1186
+ return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
1180
1187
  }
1181
1188
 
1182
1189
  // src/core/doctor.ts
@@ -1202,7 +1209,7 @@ function normalizeGlobs2(values) {
1202
1209
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1203
1210
  }
1204
1211
  async function createDoctorData(options) {
1205
- const startDir = import_node_path10.default.resolve(options.startDir);
1212
+ const startDir = import_node_path11.default.resolve(options.startDir);
1206
1213
  const checks = [];
1207
1214
  const configPath = getConfigPath(startDir);
1208
1215
  const search = options.rootExplicit ? {
@@ -1251,7 +1258,6 @@ async function createDoctorData(options) {
1251
1258
  "outDir",
1252
1259
  "srcDir",
1253
1260
  "testsDir",
1254
- "rulesDir",
1255
1261
  "promptsDir"
1256
1262
  ];
1257
1263
  for (const key of pathKeys) {
@@ -1265,9 +1271,9 @@ async function createDoctorData(options) {
1265
1271
  details: { path: toRelativePath(root, resolved) }
1266
1272
  });
1267
1273
  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`
1274
+ const promptsLocalDir = import_node_path11.default.join(
1275
+ import_node_path11.default.dirname(resolved),
1276
+ `${import_node_path11.default.basename(resolved)}.local`
1271
1277
  );
1272
1278
  const found = await exists4(promptsLocalDir);
1273
1279
  addCheck(checks, {
@@ -1340,7 +1346,7 @@ async function createDoctorData(options) {
1340
1346
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1341
1347
  details: { specPacks: entries.length, missingFiles }
1342
1348
  });
1343
- const validateJsonAbs = import_node_path10.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path10.default.resolve(root, config.output.validateJsonPath);
1349
+ const validateJsonAbs = import_node_path11.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path11.default.resolve(root, config.output.validateJsonPath);
1344
1350
  const validateJsonExists = await exists4(validateJsonAbs);
1345
1351
  addCheck(checks, {
1346
1352
  id: "output.validateJson",
@@ -1350,8 +1356,8 @@ async function createDoctorData(options) {
1350
1356
  details: { path: toRelativePath(root, validateJsonAbs) }
1351
1357
  });
1352
1358
  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);
1359
+ const rel = import_node_path11.default.relative(outDirAbs, validateJsonAbs);
1360
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path11.default.isAbsolute(rel);
1355
1361
  addCheck(checks, {
1356
1362
  id: "output.pathAlignment",
1357
1363
  severity: inside ? "ok" : "warning",
@@ -1474,12 +1480,12 @@ async function detectOutDirCollisions(root) {
1474
1480
  });
1475
1481
  const configPaths = configScan.files;
1476
1482
  const configRoots = Array.from(
1477
- new Set(configPaths.map((configPath) => import_node_path10.default.dirname(configPath)))
1483
+ new Set(configPaths.map((configPath) => import_node_path11.default.dirname(configPath)))
1478
1484
  ).sort((a, b) => a.localeCompare(b));
1479
1485
  const outDirToRoots = /* @__PURE__ */ new Map();
1480
1486
  for (const configRoot of configRoots) {
1481
1487
  const { config } = await loadConfig(configRoot);
1482
- const outDir = import_node_path10.default.normalize(resolvePath(configRoot, config, "outDir"));
1488
+ const outDir = import_node_path11.default.normalize(resolvePath(configRoot, config, "outDir"));
1483
1489
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1484
1490
  roots.add(configRoot);
1485
1491
  outDirToRoots.set(outDir, roots);
@@ -1505,20 +1511,20 @@ async function detectOutDirCollisions(root) {
1505
1511
  };
1506
1512
  }
1507
1513
  async function findMonorepoRoot(startDir) {
1508
- let current = import_node_path10.default.resolve(startDir);
1514
+ let current = import_node_path11.default.resolve(startDir);
1509
1515
  while (true) {
1510
- const gitPath = import_node_path10.default.join(current, ".git");
1511
- const workspacePath = import_node_path10.default.join(current, "pnpm-workspace.yaml");
1516
+ const gitPath = import_node_path11.default.join(current, ".git");
1517
+ const workspacePath = import_node_path11.default.join(current, "pnpm-workspace.yaml");
1512
1518
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1513
1519
  return current;
1514
1520
  }
1515
- const parent = import_node_path10.default.dirname(current);
1521
+ const parent = import_node_path11.default.dirname(current);
1516
1522
  if (parent === current) {
1517
1523
  break;
1518
1524
  }
1519
1525
  current = parent;
1520
1526
  }
1521
- return import_node_path10.default.resolve(startDir);
1527
+ return import_node_path11.default.resolve(startDir);
1522
1528
  }
1523
1529
 
1524
1530
  // src/cli/lib/logger.ts
@@ -1560,8 +1566,8 @@ async function runDoctor(options) {
1560
1566
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1561
1567
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1562
1568
  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 });
1569
+ const outAbs = import_node_path12.default.isAbsolute(options.outPath) ? options.outPath : import_node_path12.default.resolve(process.cwd(), options.outPath);
1570
+ await (0, import_promises10.mkdir)(import_node_path12.default.dirname(outAbs), { recursive: true });
1565
1571
  await (0, import_promises10.writeFile)(outAbs, `${output}
1566
1572
  `, "utf-8");
1567
1573
  info(`doctor: wrote ${outAbs}`);
@@ -1581,11 +1587,11 @@ function shouldFailDoctor(summary, failOn) {
1581
1587
  }
1582
1588
 
1583
1589
  // src/cli/commands/init.ts
1584
- var import_node_path13 = __toESM(require("path"), 1);
1590
+ var import_node_path14 = __toESM(require("path"), 1);
1585
1591
 
1586
1592
  // src/cli/lib/fs.ts
1587
1593
  var import_promises11 = require("fs/promises");
1588
- var import_node_path12 = __toESM(require("path"), 1);
1594
+ var import_node_path13 = __toESM(require("path"), 1);
1589
1595
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1590
1596
  const files = await collectTemplateFiles(sourceRoot);
1591
1597
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1593,7 +1599,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1593
1599
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1594
1600
  const allFiles = [];
1595
1601
  for (const relPath of relativePaths) {
1596
- const fullPath = import_node_path12.default.join(sourceRoot, relPath);
1602
+ const fullPath = import_node_path13.default.join(sourceRoot, relPath);
1597
1603
  const files = await collectTemplateFiles(fullPath);
1598
1604
  allFiles.push(...files);
1599
1605
  }
@@ -1603,13 +1609,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1603
1609
  const copied = [];
1604
1610
  const skipped = [];
1605
1611
  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);
1612
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
1613
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
1608
1614
  const isProtectedRelative = (relative) => {
1609
1615
  if (protectPrefixes.length === 0) {
1610
1616
  return false;
1611
1617
  }
1612
- const normalized = relative.replace(/[\\/]+/g, import_node_path12.default.sep);
1618
+ const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
1613
1619
  return protectPrefixes.some(
1614
1620
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1615
1621
  );
@@ -1618,7 +1624,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1618
1624
  if (excludePrefixes.length === 0) {
1619
1625
  return false;
1620
1626
  }
1621
- const normalized = relative.replace(/[\\/]+/g, import_node_path12.default.sep);
1627
+ const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
1622
1628
  return excludePrefixes.some(
1623
1629
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1624
1630
  );
@@ -1626,14 +1632,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1626
1632
  const conflictPolicy = options.conflictPolicy ?? "error";
1627
1633
  if (!options.force && conflictPolicy === "error") {
1628
1634
  for (const file of files) {
1629
- const relative = import_node_path12.default.relative(sourceRoot, file);
1635
+ const relative = import_node_path13.default.relative(sourceRoot, file);
1630
1636
  if (isExcludedRelative(relative)) {
1631
1637
  continue;
1632
1638
  }
1633
1639
  if (isProtectedRelative(relative)) {
1634
1640
  continue;
1635
1641
  }
1636
- const dest = import_node_path12.default.join(destRoot, relative);
1642
+ const dest = import_node_path13.default.join(destRoot, relative);
1637
1643
  if (!await shouldWrite(dest, options.force)) {
1638
1644
  conflicts.push(dest);
1639
1645
  }
@@ -1643,18 +1649,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1643
1649
  }
1644
1650
  }
1645
1651
  for (const file of files) {
1646
- const relative = import_node_path12.default.relative(sourceRoot, file);
1652
+ const relative = import_node_path13.default.relative(sourceRoot, file);
1647
1653
  if (isExcludedRelative(relative)) {
1648
1654
  continue;
1649
1655
  }
1650
- const dest = import_node_path12.default.join(destRoot, relative);
1656
+ const dest = import_node_path13.default.join(destRoot, relative);
1651
1657
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1652
1658
  if (!await shouldWrite(dest, forceForThisFile)) {
1653
1659
  skipped.push(dest);
1654
1660
  continue;
1655
1661
  }
1656
1662
  if (!options.dryRun) {
1657
- await (0, import_promises11.mkdir)(import_node_path12.default.dirname(dest), { recursive: true });
1663
+ await (0, import_promises11.mkdir)(import_node_path13.default.dirname(dest), { recursive: true });
1658
1664
  await (0, import_promises11.copyFile)(file, dest);
1659
1665
  }
1660
1666
  copied.push(dest);
@@ -1678,7 +1684,7 @@ async function collectTemplateFiles(root) {
1678
1684
  }
1679
1685
  const items = await (0, import_promises11.readdir)(root, { withFileTypes: true });
1680
1686
  for (const item of items) {
1681
- const fullPath = import_node_path12.default.join(root, item.name);
1687
+ const fullPath = import_node_path13.default.join(root, item.name);
1682
1688
  if (item.isDirectory()) {
1683
1689
  const nested = await collectTemplateFiles(fullPath);
1684
1690
  entries.push(...nested);
@@ -1708,10 +1714,10 @@ async function exists5(target) {
1708
1714
  // src/cli/commands/init.ts
1709
1715
  async function runInit(options) {
1710
1716
  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");
1717
+ const rootAssets = import_node_path14.default.join(assetsRoot, "root");
1718
+ const qfaiAssets = import_node_path14.default.join(assetsRoot, ".qfai");
1719
+ const destRoot = import_node_path14.default.resolve(options.dir);
1720
+ const destQfai = import_node_path14.default.join(destRoot, ".qfai");
1715
1721
  if (options.force) {
1716
1722
  info(
1717
1723
  "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 +1765,7 @@ function report(copied, skipped, dryRun, label) {
1759
1765
 
1760
1766
  // src/cli/commands/report.ts
1761
1767
  var import_promises20 = require("fs/promises");
1762
- var import_node_path20 = __toESM(require("path"), 1);
1768
+ var import_node_path22 = __toESM(require("path"), 1);
1763
1769
 
1764
1770
  // src/core/normalize.ts
1765
1771
  function normalizeIssuePaths(root, issues) {
@@ -1800,15 +1806,15 @@ function normalizeValidationResult(root, result) {
1800
1806
 
1801
1807
  // src/core/report.ts
1802
1808
  var import_promises19 = require("fs/promises");
1803
- var import_node_path19 = __toESM(require("path"), 1);
1809
+ var import_node_path21 = __toESM(require("path"), 1);
1804
1810
 
1805
1811
  // src/core/contractIndex.ts
1806
1812
  var import_promises12 = require("fs/promises");
1807
- var import_node_path14 = __toESM(require("path"), 1);
1813
+ var import_node_path15 = __toESM(require("path"), 1);
1808
1814
 
1809
1815
  // 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*$/;
1816
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
1817
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
1812
1818
  function extractDeclaredContractIds(text) {
1813
1819
  const ids = [];
1814
1820
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -1826,20 +1832,22 @@ function stripContractDeclarationLines(text) {
1826
1832
  // src/core/contractIndex.ts
1827
1833
  async function buildContractIndex(root, config) {
1828
1834
  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([
1835
+ const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
1836
+ const apiRoot = import_node_path15.default.join(contractsRoot, "api");
1837
+ const dbRoot = import_node_path15.default.join(contractsRoot, "db");
1838
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
1833
1839
  collectUiContractFiles(uiRoot),
1840
+ collectThemaContractFiles(uiRoot),
1834
1841
  collectApiContractFiles(apiRoot),
1835
1842
  collectDbContractFiles(dbRoot)
1836
1843
  ]);
1837
1844
  const index = {
1838
1845
  ids: /* @__PURE__ */ new Set(),
1839
1846
  idToFiles: /* @__PURE__ */ new Map(),
1840
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1847
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
1841
1848
  };
1842
1849
  await indexContractFiles(uiFiles, index);
1850
+ await indexContractFiles(themaFiles, index);
1843
1851
  await indexContractFiles(apiFiles, index);
1844
1852
  await indexContractFiles(dbFiles, index);
1845
1853
  return index;
@@ -1858,7 +1866,15 @@ function record(index, id, file) {
1858
1866
  }
1859
1867
 
1860
1868
  // src/core/ids.ts
1861
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1869
+ var ID_PREFIXES = [
1870
+ "SPEC",
1871
+ "BR",
1872
+ "SC",
1873
+ "UI",
1874
+ "API",
1875
+ "DB",
1876
+ "THEMA"
1877
+ ];
1862
1878
  var STRICT_ID_PATTERNS = {
1863
1879
  SPEC: /\bSPEC-\d{4}\b/g,
1864
1880
  BR: /\bBR-\d{4}\b/g,
@@ -1866,6 +1882,7 @@ var STRICT_ID_PATTERNS = {
1866
1882
  UI: /\bUI-\d{4}\b/g,
1867
1883
  API: /\bAPI-\d{4}\b/g,
1868
1884
  DB: /\bDB-\d{4}\b/g,
1885
+ THEMA: /\bTHEMA-\d{3}\b/g,
1869
1886
  ADR: /\bADR-\d{4}\b/g
1870
1887
  };
1871
1888
  var LOOSE_ID_PATTERNS = {
@@ -1875,6 +1892,7 @@ var LOOSE_ID_PATTERNS = {
1875
1892
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1876
1893
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1877
1894
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1895
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
1878
1896
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1879
1897
  };
1880
1898
  function extractIds(text, prefix) {
@@ -1911,7 +1929,7 @@ function isValidId(value, prefix) {
1911
1929
  }
1912
1930
 
1913
1931
  // src/core/parse/contractRefs.ts
1914
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1932
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
1915
1933
  function parseContractRefs(text, options = {}) {
1916
1934
  const linePattern = buildLinePattern(options);
1917
1935
  const lines = [];
@@ -2082,13 +2100,13 @@ function parseSpec(md, file) {
2082
2100
 
2083
2101
  // src/core/validators/contracts.ts
2084
2102
  var import_promises13 = require("fs/promises");
2085
- var import_node_path16 = __toESM(require("path"), 1);
2103
+ var import_node_path17 = __toESM(require("path"), 1);
2086
2104
 
2087
2105
  // src/core/contracts.ts
2088
- var import_node_path15 = __toESM(require("path"), 1);
2106
+ var import_node_path16 = __toESM(require("path"), 1);
2089
2107
  var import_yaml2 = require("yaml");
2090
2108
  function parseStructuredContract(file, text) {
2091
- const ext = import_node_path15.default.extname(file).toLowerCase();
2109
+ const ext = import_node_path16.default.extname(file).toLowerCase();
2092
2110
  if (ext === ".json") {
2093
2111
  return JSON.parse(text);
2094
2112
  }
@@ -2105,17 +2123,23 @@ var SQL_DANGEROUS_PATTERNS = [
2105
2123
  label: "ALTER TABLE ... DROP"
2106
2124
  }
2107
2125
  ];
2126
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
2108
2127
  async function validateContracts(root, config) {
2109
2128
  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
2129
  const contractIndex = await buildContractIndex(root, config);
2130
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2131
+ const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
2132
+ const themaIds = new Set(
2133
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
2134
+ );
2135
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
2136
+ issues.push(...await validateThemaContracts(uiRoot));
2137
+ issues.push(...await validateApiContracts(import_node_path17.default.join(contractsRoot, "api")));
2138
+ issues.push(...await validateDbContracts(import_node_path17.default.join(contractsRoot, "db")));
2115
2139
  issues.push(...validateDuplicateContractIds(contractIndex));
2116
2140
  return issues;
2117
2141
  }
2118
- async function validateUiContracts(uiRoot) {
2142
+ async function validateUiContracts(uiRoot, themaIds) {
2119
2143
  const files = await collectUiContractFiles(uiRoot);
2120
2144
  if (files.length === 0) {
2121
2145
  return [
@@ -2131,6 +2155,22 @@ async function validateUiContracts(uiRoot) {
2131
2155
  const issues = [];
2132
2156
  for (const file of files) {
2133
2157
  const text = await (0, import_promises13.readFile)(file, "utf-8");
2158
+ const declaredIds = extractDeclaredContractIds(text);
2159
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2160
+ let doc = null;
2161
+ try {
2162
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2163
+ } catch (error2) {
2164
+ issues.push(
2165
+ issue(
2166
+ "QFAI-CONTRACT-001",
2167
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2168
+ "error",
2169
+ file,
2170
+ "contracts.ui.parse"
2171
+ )
2172
+ );
2173
+ }
2134
2174
  const invalidIds = extractInvalidIds(text, [
2135
2175
  "SPEC",
2136
2176
  "BR",
@@ -2138,6 +2178,45 @@ async function validateUiContracts(uiRoot) {
2138
2178
  "UI",
2139
2179
  "API",
2140
2180
  "DB",
2181
+ "THEMA",
2182
+ "ADR"
2183
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
2184
+ if (invalidIds.length > 0) {
2185
+ issues.push(
2186
+ issue(
2187
+ "QFAI-ID-002",
2188
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
2189
+ "error",
2190
+ file,
2191
+ "id.format",
2192
+ invalidIds
2193
+ )
2194
+ );
2195
+ }
2196
+ if (doc) {
2197
+ issues.push(
2198
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
2199
+ );
2200
+ }
2201
+ }
2202
+ return issues;
2203
+ }
2204
+ async function validateThemaContracts(uiRoot) {
2205
+ const files = await collectThemaContractFiles(uiRoot);
2206
+ if (files.length === 0) {
2207
+ return [];
2208
+ }
2209
+ const issues = [];
2210
+ for (const file of files) {
2211
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
2212
+ const invalidIds = extractInvalidIds(text, [
2213
+ "SPEC",
2214
+ "BR",
2215
+ "SC",
2216
+ "UI",
2217
+ "API",
2218
+ "DB",
2219
+ "THEMA",
2141
2220
  "ADR"
2142
2221
  ]);
2143
2222
  if (invalidIds.length > 0) {
@@ -2153,17 +2232,95 @@ async function validateUiContracts(uiRoot) {
2153
2232
  );
2154
2233
  }
2155
2234
  const declaredIds = extractDeclaredContractIds(text);
2156
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2235
+ if (declaredIds.length === 0) {
2236
+ issues.push(
2237
+ issue(
2238
+ "QFAI-THEMA-010",
2239
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
2240
+ "error",
2241
+ file,
2242
+ "contracts.thema.declaration"
2243
+ )
2244
+ );
2245
+ continue;
2246
+ }
2247
+ if (declaredIds.length > 1) {
2248
+ issues.push(
2249
+ issue(
2250
+ "QFAI-THEMA-011",
2251
+ `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(
2252
+ ", "
2253
+ )}`,
2254
+ "error",
2255
+ file,
2256
+ "contracts.thema.declaration",
2257
+ declaredIds
2258
+ )
2259
+ );
2260
+ continue;
2261
+ }
2262
+ const declaredId = declaredIds[0] ?? "";
2263
+ if (!THEMA_ID_RE.test(declaredId)) {
2264
+ issues.push(
2265
+ issue(
2266
+ "QFAI-THEMA-012",
2267
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
2268
+ "error",
2269
+ file,
2270
+ "contracts.thema.idFormat",
2271
+ [declaredId]
2272
+ )
2273
+ );
2274
+ }
2275
+ let doc;
2157
2276
  try {
2158
- parseStructuredContract(file, stripContractDeclarationLines(text));
2277
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2159
2278
  } catch (error2) {
2160
2279
  issues.push(
2161
2280
  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)})`,
2281
+ "QFAI-THEMA-001",
2282
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2164
2283
  "error",
2165
2284
  file,
2166
- "contracts.ui.parse"
2285
+ "contracts.thema.parse"
2286
+ )
2287
+ );
2288
+ continue;
2289
+ }
2290
+ const docId = typeof doc.id === "string" ? doc.id : "";
2291
+ if (!THEMA_ID_RE.test(docId)) {
2292
+ issues.push(
2293
+ issue(
2294
+ "QFAI-THEMA-012",
2295
+ 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",
2296
+ "error",
2297
+ file,
2298
+ "contracts.thema.idFormat",
2299
+ docId.length > 0 ? [docId] : void 0
2300
+ )
2301
+ );
2302
+ }
2303
+ const name = typeof doc.name === "string" ? doc.name : "";
2304
+ if (!name) {
2305
+ issues.push(
2306
+ issue(
2307
+ "QFAI-THEMA-014",
2308
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2309
+ "error",
2310
+ file,
2311
+ "contracts.thema.name"
2312
+ )
2313
+ );
2314
+ }
2315
+ if (declaredId && docId && declaredId !== docId) {
2316
+ issues.push(
2317
+ issue(
2318
+ "QFAI-THEMA-013",
2319
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
2320
+ "error",
2321
+ file,
2322
+ "contracts.thema.idMismatch",
2323
+ [declaredId, docId]
2167
2324
  )
2168
2325
  );
2169
2326
  }
@@ -2193,6 +2350,7 @@ async function validateApiContracts(apiRoot) {
2193
2350
  "UI",
2194
2351
  "API",
2195
2352
  "DB",
2353
+ "THEMA",
2196
2354
  "ADR"
2197
2355
  ]);
2198
2356
  if (invalidIds.length > 0) {
@@ -2261,6 +2419,7 @@ async function validateDbContracts(dbRoot) {
2261
2419
  "UI",
2262
2420
  "API",
2263
2421
  "DB",
2422
+ "THEMA",
2264
2423
  "ADR"
2265
2424
  ]);
2266
2425
  if (invalidIds.length > 0) {
@@ -2367,6 +2526,278 @@ function validateDuplicateContractIds(contractIndex) {
2367
2526
  function hasOpenApi(doc) {
2368
2527
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
2369
2528
  }
2529
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
2530
+ const issues = [];
2531
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
2532
+ const themaRef = doc.themaRef;
2533
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
2534
+ issues.push(
2535
+ issue(
2536
+ "QFAI-UI-020",
2537
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2538
+ "error",
2539
+ file,
2540
+ "contracts.ui.themaRef"
2541
+ )
2542
+ );
2543
+ } else if (!THEMA_ID_RE.test(themaRef)) {
2544
+ issues.push(
2545
+ issue(
2546
+ "QFAI-UI-020",
2547
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
2548
+ "error",
2549
+ file,
2550
+ "contracts.ui.themaRef",
2551
+ [themaRef]
2552
+ )
2553
+ );
2554
+ } else if (!themaIds.has(themaRef)) {
2555
+ issues.push(
2556
+ issue(
2557
+ "QFAI-UI-020",
2558
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
2559
+ "error",
2560
+ file,
2561
+ "contracts.ui.themaRef",
2562
+ [themaRef]
2563
+ )
2564
+ );
2565
+ }
2566
+ }
2567
+ const assets = doc.assets;
2568
+ if (assets && typeof assets === "object") {
2569
+ issues.push(
2570
+ ...await validateUiAssets(
2571
+ assets,
2572
+ file,
2573
+ uiRoot
2574
+ )
2575
+ );
2576
+ }
2577
+ return issues;
2578
+ }
2579
+ async function validateUiAssets(assets, file, uiRoot) {
2580
+ const issues = [];
2581
+ const packValue = assets.pack;
2582
+ const useValue = assets.use;
2583
+ if (packValue === void 0 && useValue === void 0) {
2584
+ return issues;
2585
+ }
2586
+ if (typeof packValue !== "string" || packValue.length === 0) {
2587
+ issues.push(
2588
+ issue(
2589
+ "QFAI-ASSET-001",
2590
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2591
+ "error",
2592
+ file,
2593
+ "assets.pack"
2594
+ )
2595
+ );
2596
+ return issues;
2597
+ }
2598
+ if (!isSafeRelativePath(packValue)) {
2599
+ issues.push(
2600
+ issue(
2601
+ "QFAI-ASSET-001",
2602
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
2603
+ "error",
2604
+ file,
2605
+ "assets.pack",
2606
+ [packValue]
2607
+ )
2608
+ );
2609
+ return issues;
2610
+ }
2611
+ const packDir = import_node_path17.default.resolve(uiRoot, packValue);
2612
+ const packRelative = import_node_path17.default.relative(uiRoot, packDir);
2613
+ if (packRelative.startsWith("..") || import_node_path17.default.isAbsolute(packRelative)) {
2614
+ issues.push(
2615
+ issue(
2616
+ "QFAI-ASSET-001",
2617
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
2618
+ "error",
2619
+ file,
2620
+ "assets.pack",
2621
+ [packValue]
2622
+ )
2623
+ );
2624
+ return issues;
2625
+ }
2626
+ if (!await exists6(packDir)) {
2627
+ issues.push(
2628
+ issue(
2629
+ "QFAI-ASSET-001",
2630
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
2631
+ "error",
2632
+ file,
2633
+ "assets.pack",
2634
+ [packValue]
2635
+ )
2636
+ );
2637
+ return issues;
2638
+ }
2639
+ const assetsYamlPath = import_node_path17.default.join(packDir, "assets.yaml");
2640
+ if (!await exists6(assetsYamlPath)) {
2641
+ issues.push(
2642
+ issue(
2643
+ "QFAI-ASSET-002",
2644
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
2645
+ "error",
2646
+ assetsYamlPath,
2647
+ "assets.yaml"
2648
+ )
2649
+ );
2650
+ return issues;
2651
+ }
2652
+ let manifest;
2653
+ try {
2654
+ const manifestText = await (0, import_promises13.readFile)(assetsYamlPath, "utf-8");
2655
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
2656
+ } catch (error2) {
2657
+ issues.push(
2658
+ issue(
2659
+ "QFAI-ASSET-002",
2660
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
2661
+ "error",
2662
+ assetsYamlPath,
2663
+ "assets.yaml"
2664
+ )
2665
+ );
2666
+ return issues;
2667
+ }
2668
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
2669
+ const itemIds = /* @__PURE__ */ new Set();
2670
+ const itemPaths = [];
2671
+ for (const item of items) {
2672
+ if (!item || typeof item !== "object") {
2673
+ continue;
2674
+ }
2675
+ const record2 = item;
2676
+ const id = typeof record2.id === "string" ? record2.id : void 0;
2677
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
2678
+ if (id) {
2679
+ itemIds.add(id);
2680
+ }
2681
+ itemPaths.push({ id, path: pathValue });
2682
+ }
2683
+ if (useValue !== void 0) {
2684
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
2685
+ issues.push(
2686
+ issue(
2687
+ "QFAI-ASSET-003",
2688
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2689
+ "error",
2690
+ file,
2691
+ "assets.use"
2692
+ )
2693
+ );
2694
+ } else {
2695
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
2696
+ if (missing.length > 0) {
2697
+ issues.push(
2698
+ issue(
2699
+ "QFAI-ASSET-003",
2700
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
2701
+ "error",
2702
+ file,
2703
+ "assets.use",
2704
+ missing
2705
+ )
2706
+ );
2707
+ }
2708
+ }
2709
+ }
2710
+ for (const entry of itemPaths) {
2711
+ if (!entry.path) {
2712
+ continue;
2713
+ }
2714
+ if (!isSafeRelativePath(entry.path)) {
2715
+ issues.push(
2716
+ issue(
2717
+ "QFAI-ASSET-004",
2718
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
2719
+ "error",
2720
+ assetsYamlPath,
2721
+ "assets.path",
2722
+ entry.id ? [entry.id] : [entry.path]
2723
+ )
2724
+ );
2725
+ continue;
2726
+ }
2727
+ const assetPath = import_node_path17.default.resolve(packDir, entry.path);
2728
+ const assetRelative = import_node_path17.default.relative(packDir, assetPath);
2729
+ if (assetRelative.startsWith("..") || import_node_path17.default.isAbsolute(assetRelative)) {
2730
+ issues.push(
2731
+ issue(
2732
+ "QFAI-ASSET-004",
2733
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
2734
+ "error",
2735
+ assetsYamlPath,
2736
+ "assets.path",
2737
+ entry.id ? [entry.id] : [entry.path]
2738
+ )
2739
+ );
2740
+ continue;
2741
+ }
2742
+ if (!await exists6(assetPath)) {
2743
+ issues.push(
2744
+ issue(
2745
+ "QFAI-ASSET-004",
2746
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
2747
+ "error",
2748
+ assetsYamlPath,
2749
+ "assets.path",
2750
+ entry.id ? [entry.id] : [entry.path]
2751
+ )
2752
+ );
2753
+ }
2754
+ }
2755
+ return issues;
2756
+ }
2757
+ function shouldIgnoreInvalidId(value, doc) {
2758
+ if (!doc) {
2759
+ return false;
2760
+ }
2761
+ const assets = doc.assets;
2762
+ if (!assets || typeof assets !== "object") {
2763
+ return false;
2764
+ }
2765
+ const packValue = assets.pack;
2766
+ if (typeof packValue !== "string" || packValue.length === 0) {
2767
+ return false;
2768
+ }
2769
+ const normalized = packValue.replace(/\\/g, "/");
2770
+ const basename = import_node_path17.default.posix.basename(normalized);
2771
+ if (!basename) {
2772
+ return false;
2773
+ }
2774
+ return value.toLowerCase() === basename.toLowerCase();
2775
+ }
2776
+ function isSafeRelativePath(value) {
2777
+ if (!value) {
2778
+ return false;
2779
+ }
2780
+ if (import_node_path17.default.isAbsolute(value)) {
2781
+ return false;
2782
+ }
2783
+ const normalized = value.replace(/\\/g, "/");
2784
+ if (/^[A-Za-z]:/.test(normalized)) {
2785
+ return false;
2786
+ }
2787
+ const segments = normalized.split("/");
2788
+ if (segments.some((segment) => segment === "..")) {
2789
+ return false;
2790
+ }
2791
+ return true;
2792
+ }
2793
+ async function exists6(target) {
2794
+ try {
2795
+ await (0, import_promises13.access)(target);
2796
+ return true;
2797
+ } catch {
2798
+ return false;
2799
+ }
2800
+ }
2370
2801
  function formatError4(error2) {
2371
2802
  if (error2 instanceof Error) {
2372
2803
  return error2.message;
@@ -2397,12 +2828,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2397
2828
 
2398
2829
  // src/core/validators/delta.ts
2399
2830
  var import_promises14 = require("fs/promises");
2400
- var import_node_path17 = __toESM(require("path"), 1);
2401
- var SECTION_RE = /^##\s+変更区分/m;
2402
- var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2403
- var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
2404
- var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
2405
- var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
2831
+ var import_node_path18 = __toESM(require("path"), 1);
2406
2832
  async function validateDeltas(root, config) {
2407
2833
  const specsRoot = resolvePath(root, config, "specsDir");
2408
2834
  const packs = await collectSpecPackDirs(specsRoot);
@@ -2411,10 +2837,9 @@ async function validateDeltas(root, config) {
2411
2837
  }
2412
2838
  const issues = [];
2413
2839
  for (const pack of packs) {
2414
- const deltaPath = import_node_path17.default.join(pack, "delta.md");
2415
- let text;
2840
+ const deltaPath = import_node_path18.default.join(pack, "delta.md");
2416
2841
  try {
2417
- text = await (0, import_promises14.readFile)(deltaPath, "utf-8");
2842
+ await (0, import_promises14.readFile)(deltaPath, "utf-8");
2418
2843
  } catch (error2) {
2419
2844
  if (isMissingFileError2(error2)) {
2420
2845
  issues.push(
@@ -2423,41 +2848,16 @@ async function validateDeltas(root, config) {
2423
2848
  "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2424
2849
  "error",
2425
2850
  deltaPath,
2426
- "delta.exists"
2851
+ "delta.exists",
2852
+ void 0,
2853
+ "change",
2854
+ "spec-xxxx/delta.md \u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30C6\u30F3\u30D7\u30EC\u306F init \u751F\u6210\u7269\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF09\u3002"
2427
2855
  )
2428
2856
  );
2429
2857
  continue;
2430
2858
  }
2431
2859
  throw error2;
2432
2860
  }
2433
- const hasSection = SECTION_RE.test(text);
2434
- const hasCompatibility = COMPAT_LINE_RE.test(text);
2435
- const hasChange = CHANGE_LINE_RE.test(text);
2436
- if (!hasSection || !hasCompatibility || !hasChange) {
2437
- issues.push(
2438
- issue2(
2439
- "QFAI-DELTA-002",
2440
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002`## \u5909\u66F4\u533A\u5206` \u3068\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9\uFF08Compatibility / Change/Improvement\uFF09\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2441
- "error",
2442
- deltaPath,
2443
- "delta.section"
2444
- )
2445
- );
2446
- continue;
2447
- }
2448
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
2449
- const changeChecked = CHANGE_CHECKED_RE.test(text);
2450
- if (compatibilityChecked === changeChecked) {
2451
- issues.push(
2452
- issue2(
2453
- "QFAI-DELTA-003",
2454
- "delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
2455
- "error",
2456
- deltaPath,
2457
- "delta.classification"
2458
- )
2459
- );
2460
- }
2461
2861
  }
2462
2862
  return issues;
2463
2863
  }
@@ -2491,7 +2891,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2491
2891
 
2492
2892
  // src/core/validators/ids.ts
2493
2893
  var import_promises15 = require("fs/promises");
2494
- var import_node_path18 = __toESM(require("path"), 1);
2894
+ var import_node_path19 = __toESM(require("path"), 1);
2495
2895
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2496
2896
  async function validateDefinedIds(root, config) {
2497
2897
  const issues = [];
@@ -2557,7 +2957,7 @@ function recordId(out, id, file) {
2557
2957
  }
2558
2958
  function formatFileList(files, root) {
2559
2959
  return files.map((file) => {
2560
- const relative = import_node_path18.default.relative(root, file);
2960
+ const relative = import_node_path19.default.relative(root, file);
2561
2961
  return relative.length > 0 ? relative : file;
2562
2962
  }).join(", ");
2563
2963
  }
@@ -2616,6 +3016,7 @@ async function validatePromptsIntegrity(root) {
2616
3016
 
2617
3017
  // src/core/validators/scenario.ts
2618
3018
  var import_promises16 = require("fs/promises");
3019
+ var import_node_path20 = __toESM(require("path"), 1);
2619
3020
  var GIVEN_PATTERN = /\bGiven\b/;
2620
3021
  var WHEN_PATTERN = /\bWhen\b/;
2621
3022
  var THEN_PATTERN = /\bThen\b/;
@@ -2638,6 +3039,18 @@ async function validateScenarios(root, config) {
2638
3039
  }
2639
3040
  const issues = [];
2640
3041
  for (const entry of entries) {
3042
+ const legacyScenarioPath = import_node_path20.default.join(entry.dir, "scenario.md");
3043
+ if (await fileExists(legacyScenarioPath)) {
3044
+ issues.push(
3045
+ issue4(
3046
+ "QFAI-SC-004",
3047
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3048
+ "error",
3049
+ legacyScenarioPath,
3050
+ "scenario.legacy"
3051
+ )
3052
+ );
3053
+ }
2641
3054
  let text;
2642
3055
  try {
2643
3056
  text = await (0, import_promises16.readFile)(entry.scenarioPath, "utf-8");
@@ -2669,6 +3082,7 @@ function validateScenarioContent(text, file) {
2669
3082
  "UI",
2670
3083
  "API",
2671
3084
  "DB",
3085
+ "THEMA",
2672
3086
  "ADR"
2673
3087
  ]);
2674
3088
  if (invalidIds.length > 0) {
@@ -2812,6 +3226,14 @@ function isMissingFileError3(error2) {
2812
3226
  }
2813
3227
  return error2.code === "ENOENT";
2814
3228
  }
3229
+ async function fileExists(target) {
3230
+ try {
3231
+ await (0, import_promises16.access)(target);
3232
+ return true;
3233
+ } catch {
3234
+ return false;
3235
+ }
3236
+ }
2815
3237
 
2816
3238
  // src/core/validators/spec.ts
2817
3239
  var import_promises17 = require("fs/promises");
@@ -2871,6 +3293,7 @@ function validateSpecContent(text, file, requiredSections) {
2871
3293
  "UI",
2872
3294
  "API",
2873
3295
  "DB",
3296
+ "THEMA",
2874
3297
  "ADR"
2875
3298
  ]);
2876
3299
  if (invalidIds.length > 0) {
@@ -3050,7 +3473,7 @@ async function validateTraceability(root, config) {
3050
3473
  "QFAI-TRACE-021",
3051
3474
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
3052
3475
  ", "
3053
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3476
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3054
3477
  "error",
3055
3478
  file,
3056
3479
  "traceability.specContractRefFormat",
@@ -3114,7 +3537,7 @@ async function validateTraceability(root, config) {
3114
3537
  "QFAI-TRACE-032",
3115
3538
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
3116
3539
  ", "
3117
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3540
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3118
3541
  "error",
3119
3542
  file,
3120
3543
  "traceability.scenarioContractRefFormat",
@@ -3493,17 +3916,25 @@ function countIssues(issues) {
3493
3916
  }
3494
3917
 
3495
3918
  // src/core/report.ts
3496
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3919
+ var ID_PREFIXES2 = [
3920
+ "SPEC",
3921
+ "BR",
3922
+ "SC",
3923
+ "UI",
3924
+ "API",
3925
+ "DB",
3926
+ "THEMA"
3927
+ ];
3497
3928
  async function createReportData(root, validation, configResult) {
3498
- const resolvedRoot = import_node_path19.default.resolve(root);
3929
+ const resolvedRoot = import_node_path21.default.resolve(root);
3499
3930
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3500
3931
  const config = resolved.config;
3501
3932
  const configPath = resolved.configPath;
3502
3933
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3503
3934
  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");
3935
+ const apiRoot = import_node_path21.default.join(contractsRoot, "api");
3936
+ const uiRoot = import_node_path21.default.join(contractsRoot, "ui");
3937
+ const dbRoot = import_node_path21.default.join(contractsRoot, "db");
3507
3938
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3508
3939
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3509
3940
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3511,7 +3942,8 @@ async function createReportData(root, validation, configResult) {
3511
3942
  const {
3512
3943
  api: apiFiles,
3513
3944
  ui: uiFiles,
3514
- db: dbFiles
3945
+ db: dbFiles,
3946
+ thema: themaFiles
3515
3947
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
3516
3948
  const contractIndex = await buildContractIndex(resolvedRoot, config);
3517
3949
  const contractIdList = Array.from(contractIndex.ids);
@@ -3538,7 +3970,8 @@ async function createReportData(root, validation, configResult) {
3538
3970
  ...scenarioFiles,
3539
3971
  ...apiFiles,
3540
3972
  ...uiFiles,
3541
- ...dbFiles
3973
+ ...dbFiles,
3974
+ ...themaFiles
3542
3975
  ]);
3543
3976
  const upstreamIds = await collectUpstreamIds([
3544
3977
  ...specFiles,
@@ -3575,7 +4008,8 @@ async function createReportData(root, validation, configResult) {
3575
4008
  contracts: {
3576
4009
  api: apiFiles.length,
3577
4010
  ui: uiFiles.length,
3578
- db: dbFiles.length
4011
+ db: dbFiles.length,
4012
+ thema: themaFiles.length
3579
4013
  },
3580
4014
  counts: normalizedValidation.counts
3581
4015
  },
@@ -3585,7 +4019,8 @@ async function createReportData(root, validation, configResult) {
3585
4019
  sc: idsByPrefix.SC,
3586
4020
  ui: idsByPrefix.UI,
3587
4021
  api: idsByPrefix.API,
3588
- db: idsByPrefix.DB
4022
+ db: idsByPrefix.DB,
4023
+ thema: idsByPrefix.THEMA
3589
4024
  },
3590
4025
  traceability: {
3591
4026
  upstreamIdsFound: upstreamIds.size,
@@ -3655,7 +4090,7 @@ function formatReportMarkdown(data, options = {}) {
3655
4090
  lines.push(`- specs: ${data.summary.specs}`);
3656
4091
  lines.push(`- scenarios: ${data.summary.scenarios}`);
3657
4092
  lines.push(
3658
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
4093
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
3659
4094
  );
3660
4095
  lines.push(
3661
4096
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3799,6 +4234,7 @@ function formatReportMarkdown(data, options = {}) {
3799
4234
  lines.push(formatIdLine("UI", data.ids.ui));
3800
4235
  lines.push(formatIdLine("API", data.ids.api));
3801
4236
  lines.push(formatIdLine("DB", data.ids.db));
4237
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3802
4238
  lines.push("");
3803
4239
  lines.push("## Traceability");
3804
4240
  lines.push("");
@@ -3963,12 +4399,8 @@ function formatReportMarkdown(data, options = {}) {
3963
4399
  "- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3964
4400
  );
3965
4401
  }
3966
- lines.push(
3967
- "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3968
- );
3969
- lines.push(
3970
- "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3971
- );
4402
+ lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
4403
+ lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
3972
4404
  return lines.join("\n");
3973
4405
  }
3974
4406
  function formatReportJson(data) {
@@ -4020,7 +4452,8 @@ async function collectIds(files) {
4020
4452
  SC: /* @__PURE__ */ new Set(),
4021
4453
  UI: /* @__PURE__ */ new Set(),
4022
4454
  API: /* @__PURE__ */ new Set(),
4023
- DB: /* @__PURE__ */ new Set()
4455
+ DB: /* @__PURE__ */ new Set(),
4456
+ THEMA: /* @__PURE__ */ new Set()
4024
4457
  };
4025
4458
  for (const file of files) {
4026
4459
  const text = await (0, import_promises19.readFile)(file, "utf-8");
@@ -4035,7 +4468,8 @@ async function collectIds(files) {
4035
4468
  SC: toSortedArray2(result.SC),
4036
4469
  UI: toSortedArray2(result.UI),
4037
4470
  API: toSortedArray2(result.API),
4038
- DB: toSortedArray2(result.DB)
4471
+ DB: toSortedArray2(result.DB),
4472
+ THEMA: toSortedArray2(result.THEMA)
4039
4473
  };
4040
4474
  }
4041
4475
  async function collectUpstreamIds(files) {
@@ -4199,7 +4633,7 @@ function warnIfTruncated(scan, context) {
4199
4633
 
4200
4634
  // src/cli/commands/report.ts
4201
4635
  async function runReport(options) {
4202
- const root = import_node_path20.default.resolve(options.root);
4636
+ const root = import_node_path22.default.resolve(options.root);
4203
4637
  const configResult = await loadConfig(root);
4204
4638
  let validation;
4205
4639
  if (options.runValidate) {
@@ -4216,7 +4650,7 @@ async function runReport(options) {
4216
4650
  validation = normalized;
4217
4651
  } else {
4218
4652
  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);
4653
+ const inputPath = import_node_path22.default.isAbsolute(input) ? input : import_node_path22.default.resolve(root, input);
4220
4654
  try {
4221
4655
  validation = await readValidationResult(inputPath);
4222
4656
  } catch (err) {
@@ -4243,10 +4677,10 @@ async function runReport(options) {
4243
4677
  warnIfTruncated(data.traceability.testFiles, "report");
4244
4678
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4245
4679
  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");
4680
+ const defaultOut = options.format === "json" ? import_node_path22.default.join(outRoot, "report.json") : import_node_path22.default.join(outRoot, "report.md");
4247
4681
  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 });
4682
+ const outPath = import_node_path22.default.isAbsolute(out) ? out : import_node_path22.default.resolve(root, out);
4683
+ await (0, import_promises20.mkdir)(import_node_path22.default.dirname(outPath), { recursive: true });
4250
4684
  await (0, import_promises20.writeFile)(outPath, `${output}
4251
4685
  `, "utf-8");
4252
4686
  info(
@@ -4311,15 +4745,15 @@ function isMissingFileError5(error2) {
4311
4745
  return record2.code === "ENOENT";
4312
4746
  }
4313
4747
  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 });
4748
+ const abs = import_node_path22.default.isAbsolute(outputPath) ? outputPath : import_node_path22.default.resolve(root, outputPath);
4749
+ await (0, import_promises20.mkdir)(import_node_path22.default.dirname(abs), { recursive: true });
4316
4750
  await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4317
4751
  `, "utf-8");
4318
4752
  }
4319
4753
 
4320
4754
  // src/cli/commands/validate.ts
4321
4755
  var import_promises21 = require("fs/promises");
4322
- var import_node_path21 = __toESM(require("path"), 1);
4756
+ var import_node_path23 = __toESM(require("path"), 1);
4323
4757
 
4324
4758
  // src/cli/lib/failOn.ts
4325
4759
  function shouldFail(result, failOn) {
@@ -4334,7 +4768,7 @@ function shouldFail(result, failOn) {
4334
4768
 
4335
4769
  // src/cli/commands/validate.ts
4336
4770
  async function runValidate(options) {
4337
- const root = import_node_path21.default.resolve(options.root);
4771
+ const root = import_node_path23.default.resolve(options.root);
4338
4772
  const configResult = await loadConfig(root);
4339
4773
  const result = await validateProject(root, configResult);
4340
4774
  const normalized = normalizeValidationResult(root, result);
@@ -4459,12 +4893,12 @@ function issueKey(issue7) {
4459
4893
  }
4460
4894
  async function emitJson(result, root, jsonPath) {
4461
4895
  const abs = resolveJsonPath(root, jsonPath);
4462
- await (0, import_promises21.mkdir)(import_node_path21.default.dirname(abs), { recursive: true });
4896
+ await (0, import_promises21.mkdir)(import_node_path23.default.dirname(abs), { recursive: true });
4463
4897
  await (0, import_promises21.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4464
4898
  `, "utf-8");
4465
4899
  }
4466
4900
  function resolveJsonPath(root, jsonPath) {
4467
- return import_node_path21.default.isAbsolute(jsonPath) ? jsonPath : import_node_path21.default.resolve(root, jsonPath);
4901
+ return import_node_path23.default.isAbsolute(jsonPath) ? jsonPath : import_node_path23.default.resolve(root, jsonPath);
4468
4902
  }
4469
4903
  var GITHUB_ANNOTATION_LIMIT = 100;
4470
4904