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
@@ -208,11 +208,11 @@ function emitPromptNotFound(promptName, candidates) {
208
208
 
209
209
  // src/cli/commands/doctor.ts
210
210
  import { mkdir, writeFile } from "fs/promises";
211
- import path11 from "path";
211
+ import path12 from "path";
212
212
 
213
213
  // src/core/doctor.ts
214
214
  import { access as access4 } from "fs/promises";
215
- import path10 from "path";
215
+ import path11 from "path";
216
216
 
217
217
  // src/core/config.ts
218
218
  import { access as access2, readFile as readFile2 } from "fs/promises";
@@ -222,7 +222,6 @@ var defaultConfig = {
222
222
  paths: {
223
223
  contractsDir: ".qfai/contracts",
224
224
  specsDir: ".qfai/specs",
225
- rulesDir: ".qfai/rules",
226
225
  outDir: ".qfai/out",
227
226
  promptsDir: ".qfai/prompts",
228
227
  srcDir: "src",
@@ -335,13 +334,6 @@ function normalizePaths(raw, configPath, issues) {
335
334
  configPath,
336
335
  issues
337
336
  ),
338
- rulesDir: readString(
339
- raw.rulesDir,
340
- base.rulesDir,
341
- "paths.rulesDir",
342
- configPath,
343
- issues
344
- ),
345
337
  outDir: readString(
346
338
  raw.outDir,
347
339
  base.outDir,
@@ -616,6 +608,7 @@ function isRecord(value) {
616
608
 
617
609
  // src/core/discovery.ts
618
610
  import { access as access3 } from "fs/promises";
611
+ import path5 from "path";
619
612
 
620
613
  // src/core/specLayout.ts
621
614
  import { readdir as readdir2 } from "fs/promises";
@@ -663,7 +656,12 @@ async function collectScenarioFiles(specsRoot) {
663
656
  return filterExisting(entries.map((entry) => entry.scenarioPath));
664
657
  }
665
658
  async function collectUiContractFiles(uiRoot) {
666
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
659
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
660
+ return filterByBasenamePrefix(files, "ui-");
661
+ }
662
+ async function collectThemaContractFiles(uiRoot) {
663
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
664
+ return filterByBasenamePrefix(files, "thema-");
667
665
  }
668
666
  async function collectApiContractFiles(apiRoot) {
669
667
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -672,12 +670,13 @@ async function collectDbContractFiles(dbRoot) {
672
670
  return collectFiles(dbRoot, { extensions: [".sql"] });
673
671
  }
674
672
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
675
- const [ui, api, db] = await Promise.all([
673
+ const [ui, thema, api, db] = await Promise.all([
676
674
  collectUiContractFiles(uiRoot),
675
+ collectThemaContractFiles(uiRoot),
677
676
  collectApiContractFiles(apiRoot),
678
677
  collectDbContractFiles(dbRoot)
679
678
  ]);
680
- return { ui, api, db };
679
+ return { ui, thema, api, db };
681
680
  }
682
681
  async function filterExisting(files) {
683
682
  const existing = [];
@@ -696,17 +695,23 @@ async function exists3(target) {
696
695
  return false;
697
696
  }
698
697
  }
698
+ function filterByBasenamePrefix(files, prefix) {
699
+ const lowerPrefix = prefix.toLowerCase();
700
+ return files.filter(
701
+ (file) => path5.basename(file).toLowerCase().startsWith(lowerPrefix)
702
+ );
703
+ }
699
704
 
700
705
  // src/core/paths.ts
701
- import path5 from "path";
706
+ import path6 from "path";
702
707
  function toRelativePath(root, target) {
703
708
  if (!target) {
704
709
  return target;
705
710
  }
706
- if (!path5.isAbsolute(target)) {
711
+ if (!path6.isAbsolute(target)) {
707
712
  return toPosixPath(target);
708
713
  }
709
- const relative = path5.relative(root, target);
714
+ const relative = path6.relative(root, target);
710
715
  if (!relative) {
711
716
  return ".";
712
717
  }
@@ -718,7 +723,7 @@ function toPosixPath(value) {
718
723
 
719
724
  // src/core/traceability.ts
720
725
  import { readFile as readFile3 } from "fs/promises";
721
- import path6 from "path";
726
+ import path7 from "path";
722
727
 
723
728
  // src/core/gherkin/parse.ts
724
729
  import {
@@ -950,7 +955,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
950
955
  };
951
956
  }
952
957
  const normalizedFiles = Array.from(
953
- new Set(scanResult.files.map((file) => path6.normalize(file)))
958
+ new Set(scanResult.files.map((file) => path7.normalize(file)))
954
959
  );
955
960
  for (const file of normalizedFiles) {
956
961
  const text = await readFile3(file, "utf-8");
@@ -1013,19 +1018,19 @@ function formatError3(error2) {
1013
1018
 
1014
1019
  // src/core/promptsIntegrity.ts
1015
1020
  import { readFile as readFile4 } from "fs/promises";
1016
- import path8 from "path";
1021
+ import path9 from "path";
1017
1022
 
1018
1023
  // src/shared/assets.ts
1019
1024
  import { existsSync } from "fs";
1020
- import path7 from "path";
1025
+ import path8 from "path";
1021
1026
  import { fileURLToPath } from "url";
1022
1027
  function getInitAssetsDir() {
1023
1028
  const base = import.meta.url;
1024
1029
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
1025
- const baseDir = path7.dirname(basePath);
1030
+ const baseDir = path8.dirname(basePath);
1026
1031
  const candidates = [
1027
- path7.resolve(baseDir, "../../../assets/init"),
1028
- path7.resolve(baseDir, "../../assets/init")
1032
+ path8.resolve(baseDir, "../../../assets/init"),
1033
+ path8.resolve(baseDir, "../../assets/init")
1029
1034
  ];
1030
1035
  for (const candidate of candidates) {
1031
1036
  if (existsSync(candidate)) {
@@ -1042,11 +1047,12 @@ function getInitAssetsDir() {
1042
1047
  }
1043
1048
 
1044
1049
  // src/core/promptsIntegrity.ts
1050
+ var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
1045
1051
  async function diffProjectPromptsAgainstInitAssets(root) {
1046
- const promptsDir = path8.resolve(root, ".qfai", "prompts");
1052
+ const promptsDir = path9.resolve(root, ".qfai", "prompts");
1047
1053
  let templateDir;
1048
1054
  try {
1049
- templateDir = path8.join(getInitAssetsDir(), ".qfai", "prompts");
1055
+ templateDir = path9.join(getInitAssetsDir(), ".qfai", "prompts");
1050
1056
  } catch {
1051
1057
  return {
1052
1058
  status: "skipped_missing_assets",
@@ -1090,6 +1096,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1090
1096
  extra.push(rel);
1091
1097
  }
1092
1098
  }
1099
+ const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
1093
1100
  const common = intersectKeys(templateByRel, projectByRel);
1094
1101
  for (const rel of common) {
1095
1102
  const templateAbs = templateByRel.get(rel);
@@ -1109,13 +1116,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1109
1116
  changed.push(rel);
1110
1117
  }
1111
1118
  }
1112
- const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
1119
+ const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
1113
1120
  return {
1114
1121
  status,
1115
1122
  promptsDir,
1116
1123
  templateDir,
1117
1124
  missing: missing.sort(),
1118
- extra: extra.sort(),
1125
+ extra: filteredExtra.sort(),
1119
1126
  changed: changed.sort()
1120
1127
  };
1121
1128
  }
@@ -1123,7 +1130,7 @@ function normalizeNewlines(text) {
1123
1130
  return text.replace(/\r\n/g, "\n");
1124
1131
  }
1125
1132
  function toRel(base, abs) {
1126
- const rel = path8.relative(base, abs);
1133
+ const rel = path9.relative(base, abs);
1127
1134
  return rel.replace(/[\\/]+/g, "/");
1128
1135
  }
1129
1136
  function intersectKeys(a, b) {
@@ -1138,11 +1145,11 @@ function intersectKeys(a, b) {
1138
1145
 
1139
1146
  // src/core/version.ts
1140
1147
  import { readFile as readFile5 } from "fs/promises";
1141
- import path9 from "path";
1148
+ import path10 from "path";
1142
1149
  import { fileURLToPath as fileURLToPath2 } from "url";
1143
1150
  async function resolveToolVersion() {
1144
- if ("1.0.2".length > 0) {
1145
- return "1.0.2";
1151
+ if ("1.0.4".length > 0) {
1152
+ return "1.0.4";
1146
1153
  }
1147
1154
  try {
1148
1155
  const packagePath = resolvePackageJsonPath();
@@ -1157,7 +1164,7 @@ async function resolveToolVersion() {
1157
1164
  function resolvePackageJsonPath() {
1158
1165
  const base = import.meta.url;
1159
1166
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1160
- return path9.resolve(path9.dirname(basePath), "../../package.json");
1167
+ return path10.resolve(path10.dirname(basePath), "../../package.json");
1161
1168
  }
1162
1169
 
1163
1170
  // src/core/doctor.ts
@@ -1183,7 +1190,7 @@ function normalizeGlobs2(values) {
1183
1190
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1184
1191
  }
1185
1192
  async function createDoctorData(options) {
1186
- const startDir = path10.resolve(options.startDir);
1193
+ const startDir = path11.resolve(options.startDir);
1187
1194
  const checks = [];
1188
1195
  const configPath = getConfigPath(startDir);
1189
1196
  const search = options.rootExplicit ? {
@@ -1232,7 +1239,6 @@ async function createDoctorData(options) {
1232
1239
  "outDir",
1233
1240
  "srcDir",
1234
1241
  "testsDir",
1235
- "rulesDir",
1236
1242
  "promptsDir"
1237
1243
  ];
1238
1244
  for (const key of pathKeys) {
@@ -1246,9 +1252,9 @@ async function createDoctorData(options) {
1246
1252
  details: { path: toRelativePath(root, resolved) }
1247
1253
  });
1248
1254
  if (key === "promptsDir") {
1249
- const promptsLocalDir = path10.join(
1250
- path10.dirname(resolved),
1251
- `${path10.basename(resolved)}.local`
1255
+ const promptsLocalDir = path11.join(
1256
+ path11.dirname(resolved),
1257
+ `${path11.basename(resolved)}.local`
1252
1258
  );
1253
1259
  const found = await exists4(promptsLocalDir);
1254
1260
  addCheck(checks, {
@@ -1321,7 +1327,7 @@ async function createDoctorData(options) {
1321
1327
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1322
1328
  details: { specPacks: entries.length, missingFiles }
1323
1329
  });
1324
- const validateJsonAbs = path10.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path10.resolve(root, config.output.validateJsonPath);
1330
+ const validateJsonAbs = path11.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path11.resolve(root, config.output.validateJsonPath);
1325
1331
  const validateJsonExists = await exists4(validateJsonAbs);
1326
1332
  addCheck(checks, {
1327
1333
  id: "output.validateJson",
@@ -1331,8 +1337,8 @@ async function createDoctorData(options) {
1331
1337
  details: { path: toRelativePath(root, validateJsonAbs) }
1332
1338
  });
1333
1339
  const outDirAbs = resolvePath(root, config, "outDir");
1334
- const rel = path10.relative(outDirAbs, validateJsonAbs);
1335
- const inside = rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
1340
+ const rel = path11.relative(outDirAbs, validateJsonAbs);
1341
+ const inside = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
1336
1342
  addCheck(checks, {
1337
1343
  id: "output.pathAlignment",
1338
1344
  severity: inside ? "ok" : "warning",
@@ -1455,12 +1461,12 @@ async function detectOutDirCollisions(root) {
1455
1461
  });
1456
1462
  const configPaths = configScan.files;
1457
1463
  const configRoots = Array.from(
1458
- new Set(configPaths.map((configPath) => path10.dirname(configPath)))
1464
+ new Set(configPaths.map((configPath) => path11.dirname(configPath)))
1459
1465
  ).sort((a, b) => a.localeCompare(b));
1460
1466
  const outDirToRoots = /* @__PURE__ */ new Map();
1461
1467
  for (const configRoot of configRoots) {
1462
1468
  const { config } = await loadConfig(configRoot);
1463
- const outDir = path10.normalize(resolvePath(configRoot, config, "outDir"));
1469
+ const outDir = path11.normalize(resolvePath(configRoot, config, "outDir"));
1464
1470
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1465
1471
  roots.add(configRoot);
1466
1472
  outDirToRoots.set(outDir, roots);
@@ -1486,20 +1492,20 @@ async function detectOutDirCollisions(root) {
1486
1492
  };
1487
1493
  }
1488
1494
  async function findMonorepoRoot(startDir) {
1489
- let current = path10.resolve(startDir);
1495
+ let current = path11.resolve(startDir);
1490
1496
  while (true) {
1491
- const gitPath = path10.join(current, ".git");
1492
- const workspacePath = path10.join(current, "pnpm-workspace.yaml");
1497
+ const gitPath = path11.join(current, ".git");
1498
+ const workspacePath = path11.join(current, "pnpm-workspace.yaml");
1493
1499
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1494
1500
  return current;
1495
1501
  }
1496
- const parent = path10.dirname(current);
1502
+ const parent = path11.dirname(current);
1497
1503
  if (parent === current) {
1498
1504
  break;
1499
1505
  }
1500
1506
  current = parent;
1501
1507
  }
1502
- return path10.resolve(startDir);
1508
+ return path11.resolve(startDir);
1503
1509
  }
1504
1510
 
1505
1511
  // src/cli/lib/logger.ts
@@ -1541,8 +1547,8 @@ async function runDoctor(options) {
1541
1547
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1542
1548
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1543
1549
  if (options.outPath) {
1544
- const outAbs = path11.isAbsolute(options.outPath) ? options.outPath : path11.resolve(process.cwd(), options.outPath);
1545
- await mkdir(path11.dirname(outAbs), { recursive: true });
1550
+ const outAbs = path12.isAbsolute(options.outPath) ? options.outPath : path12.resolve(process.cwd(), options.outPath);
1551
+ await mkdir(path12.dirname(outAbs), { recursive: true });
1546
1552
  await writeFile(outAbs, `${output}
1547
1553
  `, "utf-8");
1548
1554
  info(`doctor: wrote ${outAbs}`);
@@ -1562,11 +1568,11 @@ function shouldFailDoctor(summary, failOn) {
1562
1568
  }
1563
1569
 
1564
1570
  // src/cli/commands/init.ts
1565
- import path13 from "path";
1571
+ import path14 from "path";
1566
1572
 
1567
1573
  // src/cli/lib/fs.ts
1568
1574
  import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1569
- import path12 from "path";
1575
+ import path13 from "path";
1570
1576
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1571
1577
  const files = await collectTemplateFiles(sourceRoot);
1572
1578
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1574,7 +1580,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1574
1580
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1575
1581
  const allFiles = [];
1576
1582
  for (const relPath of relativePaths) {
1577
- const fullPath = path12.join(sourceRoot, relPath);
1583
+ const fullPath = path13.join(sourceRoot, relPath);
1578
1584
  const files = await collectTemplateFiles(fullPath);
1579
1585
  allFiles.push(...files);
1580
1586
  }
@@ -1584,13 +1590,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1584
1590
  const copied = [];
1585
1591
  const skipped = [];
1586
1592
  const conflicts = [];
1587
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1588
- const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1593
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
1594
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
1589
1595
  const isProtectedRelative = (relative) => {
1590
1596
  if (protectPrefixes.length === 0) {
1591
1597
  return false;
1592
1598
  }
1593
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
1599
+ const normalized = relative.replace(/[\\/]+/g, path13.sep);
1594
1600
  return protectPrefixes.some(
1595
1601
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1596
1602
  );
@@ -1599,7 +1605,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1599
1605
  if (excludePrefixes.length === 0) {
1600
1606
  return false;
1601
1607
  }
1602
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
1608
+ const normalized = relative.replace(/[\\/]+/g, path13.sep);
1603
1609
  return excludePrefixes.some(
1604
1610
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1605
1611
  );
@@ -1607,14 +1613,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1607
1613
  const conflictPolicy = options.conflictPolicy ?? "error";
1608
1614
  if (!options.force && conflictPolicy === "error") {
1609
1615
  for (const file of files) {
1610
- const relative = path12.relative(sourceRoot, file);
1616
+ const relative = path13.relative(sourceRoot, file);
1611
1617
  if (isExcludedRelative(relative)) {
1612
1618
  continue;
1613
1619
  }
1614
1620
  if (isProtectedRelative(relative)) {
1615
1621
  continue;
1616
1622
  }
1617
- const dest = path12.join(destRoot, relative);
1623
+ const dest = path13.join(destRoot, relative);
1618
1624
  if (!await shouldWrite(dest, options.force)) {
1619
1625
  conflicts.push(dest);
1620
1626
  }
@@ -1624,18 +1630,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1624
1630
  }
1625
1631
  }
1626
1632
  for (const file of files) {
1627
- const relative = path12.relative(sourceRoot, file);
1633
+ const relative = path13.relative(sourceRoot, file);
1628
1634
  if (isExcludedRelative(relative)) {
1629
1635
  continue;
1630
1636
  }
1631
- const dest = path12.join(destRoot, relative);
1637
+ const dest = path13.join(destRoot, relative);
1632
1638
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1633
1639
  if (!await shouldWrite(dest, forceForThisFile)) {
1634
1640
  skipped.push(dest);
1635
1641
  continue;
1636
1642
  }
1637
1643
  if (!options.dryRun) {
1638
- await mkdir2(path12.dirname(dest), { recursive: true });
1644
+ await mkdir2(path13.dirname(dest), { recursive: true });
1639
1645
  await copyFile(file, dest);
1640
1646
  }
1641
1647
  copied.push(dest);
@@ -1659,7 +1665,7 @@ async function collectTemplateFiles(root) {
1659
1665
  }
1660
1666
  const items = await readdir3(root, { withFileTypes: true });
1661
1667
  for (const item of items) {
1662
- const fullPath = path12.join(root, item.name);
1668
+ const fullPath = path13.join(root, item.name);
1663
1669
  if (item.isDirectory()) {
1664
1670
  const nested = await collectTemplateFiles(fullPath);
1665
1671
  entries.push(...nested);
@@ -1689,10 +1695,10 @@ async function exists5(target) {
1689
1695
  // src/cli/commands/init.ts
1690
1696
  async function runInit(options) {
1691
1697
  const assetsRoot = getInitAssetsDir();
1692
- const rootAssets = path13.join(assetsRoot, "root");
1693
- const qfaiAssets = path13.join(assetsRoot, ".qfai");
1694
- const destRoot = path13.resolve(options.dir);
1695
- const destQfai = path13.join(destRoot, ".qfai");
1698
+ const rootAssets = path14.join(assetsRoot, "root");
1699
+ const qfaiAssets = path14.join(assetsRoot, ".qfai");
1700
+ const destRoot = path14.resolve(options.dir);
1701
+ const destQfai = path14.join(destRoot, ".qfai");
1696
1702
  if (options.force) {
1697
1703
  info(
1698
1704
  "NOTE: --force \u306F .qfai/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
@@ -1740,7 +1746,7 @@ function report(copied, skipped, dryRun, label) {
1740
1746
 
1741
1747
  // src/cli/commands/report.ts
1742
1748
  import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
1743
- import path20 from "path";
1749
+ import path22 from "path";
1744
1750
 
1745
1751
  // src/core/normalize.ts
1746
1752
  function normalizeIssuePaths(root, issues) {
@@ -1781,15 +1787,15 @@ function normalizeValidationResult(root, result) {
1781
1787
 
1782
1788
  // src/core/report.ts
1783
1789
  import { readFile as readFile13 } from "fs/promises";
1784
- import path19 from "path";
1790
+ import path21 from "path";
1785
1791
 
1786
1792
  // src/core/contractIndex.ts
1787
1793
  import { readFile as readFile6 } from "fs/promises";
1788
- import path14 from "path";
1794
+ import path15 from "path";
1789
1795
 
1790
1796
  // src/core/contractsDecl.ts
1791
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1792
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1797
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
1798
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
1793
1799
  function extractDeclaredContractIds(text) {
1794
1800
  const ids = [];
1795
1801
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -1807,20 +1813,22 @@ function stripContractDeclarationLines(text) {
1807
1813
  // src/core/contractIndex.ts
1808
1814
  async function buildContractIndex(root, config) {
1809
1815
  const contractsRoot = resolvePath(root, config, "contractsDir");
1810
- const uiRoot = path14.join(contractsRoot, "ui");
1811
- const apiRoot = path14.join(contractsRoot, "api");
1812
- const dbRoot = path14.join(contractsRoot, "db");
1813
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1816
+ const uiRoot = path15.join(contractsRoot, "ui");
1817
+ const apiRoot = path15.join(contractsRoot, "api");
1818
+ const dbRoot = path15.join(contractsRoot, "db");
1819
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
1814
1820
  collectUiContractFiles(uiRoot),
1821
+ collectThemaContractFiles(uiRoot),
1815
1822
  collectApiContractFiles(apiRoot),
1816
1823
  collectDbContractFiles(dbRoot)
1817
1824
  ]);
1818
1825
  const index = {
1819
1826
  ids: /* @__PURE__ */ new Set(),
1820
1827
  idToFiles: /* @__PURE__ */ new Map(),
1821
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1828
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
1822
1829
  };
1823
1830
  await indexContractFiles(uiFiles, index);
1831
+ await indexContractFiles(themaFiles, index);
1824
1832
  await indexContractFiles(apiFiles, index);
1825
1833
  await indexContractFiles(dbFiles, index);
1826
1834
  return index;
@@ -1839,7 +1847,15 @@ function record(index, id, file) {
1839
1847
  }
1840
1848
 
1841
1849
  // src/core/ids.ts
1842
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1850
+ var ID_PREFIXES = [
1851
+ "SPEC",
1852
+ "BR",
1853
+ "SC",
1854
+ "UI",
1855
+ "API",
1856
+ "DB",
1857
+ "THEMA"
1858
+ ];
1843
1859
  var STRICT_ID_PATTERNS = {
1844
1860
  SPEC: /\bSPEC-\d{4}\b/g,
1845
1861
  BR: /\bBR-\d{4}\b/g,
@@ -1847,6 +1863,7 @@ var STRICT_ID_PATTERNS = {
1847
1863
  UI: /\bUI-\d{4}\b/g,
1848
1864
  API: /\bAPI-\d{4}\b/g,
1849
1865
  DB: /\bDB-\d{4}\b/g,
1866
+ THEMA: /\bTHEMA-\d{3}\b/g,
1850
1867
  ADR: /\bADR-\d{4}\b/g
1851
1868
  };
1852
1869
  var LOOSE_ID_PATTERNS = {
@@ -1856,6 +1873,7 @@ var LOOSE_ID_PATTERNS = {
1856
1873
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1857
1874
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1858
1875
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1876
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
1859
1877
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1860
1878
  };
1861
1879
  function extractIds(text, prefix) {
@@ -1892,7 +1910,7 @@ function isValidId(value, prefix) {
1892
1910
  }
1893
1911
 
1894
1912
  // src/core/parse/contractRefs.ts
1895
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1913
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
1896
1914
  function parseContractRefs(text, options = {}) {
1897
1915
  const linePattern = buildLinePattern(options);
1898
1916
  const lines = [];
@@ -2062,14 +2080,14 @@ function parseSpec(md, file) {
2062
2080
  }
2063
2081
 
2064
2082
  // src/core/validators/contracts.ts
2065
- import { readFile as readFile7 } from "fs/promises";
2066
- import path16 from "path";
2083
+ import { access as access6, readFile as readFile7 } from "fs/promises";
2084
+ import path17 from "path";
2067
2085
 
2068
2086
  // src/core/contracts.ts
2069
- import path15 from "path";
2087
+ import path16 from "path";
2070
2088
  import { parse as parseYaml2 } from "yaml";
2071
2089
  function parseStructuredContract(file, text) {
2072
- const ext = path15.extname(file).toLowerCase();
2090
+ const ext = path16.extname(file).toLowerCase();
2073
2091
  if (ext === ".json") {
2074
2092
  return JSON.parse(text);
2075
2093
  }
@@ -2086,17 +2104,23 @@ var SQL_DANGEROUS_PATTERNS = [
2086
2104
  label: "ALTER TABLE ... DROP"
2087
2105
  }
2088
2106
  ];
2107
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
2089
2108
  async function validateContracts(root, config) {
2090
2109
  const issues = [];
2091
- const contractsRoot = resolvePath(root, config, "contractsDir");
2092
- issues.push(...await validateUiContracts(path16.join(contractsRoot, "ui")));
2093
- issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
2094
- issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
2095
2110
  const contractIndex = await buildContractIndex(root, config);
2111
+ const contractsRoot = resolvePath(root, config, "contractsDir");
2112
+ const uiRoot = path17.join(contractsRoot, "ui");
2113
+ const themaIds = new Set(
2114
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
2115
+ );
2116
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
2117
+ issues.push(...await validateThemaContracts(uiRoot));
2118
+ issues.push(...await validateApiContracts(path17.join(contractsRoot, "api")));
2119
+ issues.push(...await validateDbContracts(path17.join(contractsRoot, "db")));
2096
2120
  issues.push(...validateDuplicateContractIds(contractIndex));
2097
2121
  return issues;
2098
2122
  }
2099
- async function validateUiContracts(uiRoot) {
2123
+ async function validateUiContracts(uiRoot, themaIds) {
2100
2124
  const files = await collectUiContractFiles(uiRoot);
2101
2125
  if (files.length === 0) {
2102
2126
  return [
@@ -2112,6 +2136,22 @@ async function validateUiContracts(uiRoot) {
2112
2136
  const issues = [];
2113
2137
  for (const file of files) {
2114
2138
  const text = await readFile7(file, "utf-8");
2139
+ const declaredIds = extractDeclaredContractIds(text);
2140
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2141
+ let doc = null;
2142
+ try {
2143
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2144
+ } catch (error2) {
2145
+ issues.push(
2146
+ issue(
2147
+ "QFAI-CONTRACT-001",
2148
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2149
+ "error",
2150
+ file,
2151
+ "contracts.ui.parse"
2152
+ )
2153
+ );
2154
+ }
2115
2155
  const invalidIds = extractInvalidIds(text, [
2116
2156
  "SPEC",
2117
2157
  "BR",
@@ -2119,6 +2159,45 @@ async function validateUiContracts(uiRoot) {
2119
2159
  "UI",
2120
2160
  "API",
2121
2161
  "DB",
2162
+ "THEMA",
2163
+ "ADR"
2164
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
2165
+ if (invalidIds.length > 0) {
2166
+ issues.push(
2167
+ issue(
2168
+ "QFAI-ID-002",
2169
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
2170
+ "error",
2171
+ file,
2172
+ "id.format",
2173
+ invalidIds
2174
+ )
2175
+ );
2176
+ }
2177
+ if (doc) {
2178
+ issues.push(
2179
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
2180
+ );
2181
+ }
2182
+ }
2183
+ return issues;
2184
+ }
2185
+ async function validateThemaContracts(uiRoot) {
2186
+ const files = await collectThemaContractFiles(uiRoot);
2187
+ if (files.length === 0) {
2188
+ return [];
2189
+ }
2190
+ const issues = [];
2191
+ for (const file of files) {
2192
+ const text = await readFile7(file, "utf-8");
2193
+ const invalidIds = extractInvalidIds(text, [
2194
+ "SPEC",
2195
+ "BR",
2196
+ "SC",
2197
+ "UI",
2198
+ "API",
2199
+ "DB",
2200
+ "THEMA",
2122
2201
  "ADR"
2123
2202
  ]);
2124
2203
  if (invalidIds.length > 0) {
@@ -2134,17 +2213,95 @@ async function validateUiContracts(uiRoot) {
2134
2213
  );
2135
2214
  }
2136
2215
  const declaredIds = extractDeclaredContractIds(text);
2137
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2216
+ if (declaredIds.length === 0) {
2217
+ issues.push(
2218
+ issue(
2219
+ "QFAI-THEMA-010",
2220
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
2221
+ "error",
2222
+ file,
2223
+ "contracts.thema.declaration"
2224
+ )
2225
+ );
2226
+ continue;
2227
+ }
2228
+ if (declaredIds.length > 1) {
2229
+ issues.push(
2230
+ issue(
2231
+ "QFAI-THEMA-011",
2232
+ `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(
2233
+ ", "
2234
+ )}`,
2235
+ "error",
2236
+ file,
2237
+ "contracts.thema.declaration",
2238
+ declaredIds
2239
+ )
2240
+ );
2241
+ continue;
2242
+ }
2243
+ const declaredId = declaredIds[0] ?? "";
2244
+ if (!THEMA_ID_RE.test(declaredId)) {
2245
+ issues.push(
2246
+ issue(
2247
+ "QFAI-THEMA-012",
2248
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
2249
+ "error",
2250
+ file,
2251
+ "contracts.thema.idFormat",
2252
+ [declaredId]
2253
+ )
2254
+ );
2255
+ }
2256
+ let doc;
2138
2257
  try {
2139
- parseStructuredContract(file, stripContractDeclarationLines(text));
2258
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
2140
2259
  } catch (error2) {
2141
2260
  issues.push(
2142
2261
  issue(
2143
- "QFAI-CONTRACT-001",
2144
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2262
+ "QFAI-THEMA-001",
2263
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
2145
2264
  "error",
2146
2265
  file,
2147
- "contracts.ui.parse"
2266
+ "contracts.thema.parse"
2267
+ )
2268
+ );
2269
+ continue;
2270
+ }
2271
+ const docId = typeof doc.id === "string" ? doc.id : "";
2272
+ if (!THEMA_ID_RE.test(docId)) {
2273
+ issues.push(
2274
+ issue(
2275
+ "QFAI-THEMA-012",
2276
+ 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",
2277
+ "error",
2278
+ file,
2279
+ "contracts.thema.idFormat",
2280
+ docId.length > 0 ? [docId] : void 0
2281
+ )
2282
+ );
2283
+ }
2284
+ const name = typeof doc.name === "string" ? doc.name : "";
2285
+ if (!name) {
2286
+ issues.push(
2287
+ issue(
2288
+ "QFAI-THEMA-014",
2289
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2290
+ "error",
2291
+ file,
2292
+ "contracts.thema.name"
2293
+ )
2294
+ );
2295
+ }
2296
+ if (declaredId && docId && declaredId !== docId) {
2297
+ issues.push(
2298
+ issue(
2299
+ "QFAI-THEMA-013",
2300
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
2301
+ "error",
2302
+ file,
2303
+ "contracts.thema.idMismatch",
2304
+ [declaredId, docId]
2148
2305
  )
2149
2306
  );
2150
2307
  }
@@ -2174,6 +2331,7 @@ async function validateApiContracts(apiRoot) {
2174
2331
  "UI",
2175
2332
  "API",
2176
2333
  "DB",
2334
+ "THEMA",
2177
2335
  "ADR"
2178
2336
  ]);
2179
2337
  if (invalidIds.length > 0) {
@@ -2242,6 +2400,7 @@ async function validateDbContracts(dbRoot) {
2242
2400
  "UI",
2243
2401
  "API",
2244
2402
  "DB",
2403
+ "THEMA",
2245
2404
  "ADR"
2246
2405
  ]);
2247
2406
  if (invalidIds.length > 0) {
@@ -2348,6 +2507,278 @@ function validateDuplicateContractIds(contractIndex) {
2348
2507
  function hasOpenApi(doc) {
2349
2508
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
2350
2509
  }
2510
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
2511
+ const issues = [];
2512
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
2513
+ const themaRef = doc.themaRef;
2514
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
2515
+ issues.push(
2516
+ issue(
2517
+ "QFAI-UI-020",
2518
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2519
+ "error",
2520
+ file,
2521
+ "contracts.ui.themaRef"
2522
+ )
2523
+ );
2524
+ } else if (!THEMA_ID_RE.test(themaRef)) {
2525
+ issues.push(
2526
+ issue(
2527
+ "QFAI-UI-020",
2528
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
2529
+ "error",
2530
+ file,
2531
+ "contracts.ui.themaRef",
2532
+ [themaRef]
2533
+ )
2534
+ );
2535
+ } else if (!themaIds.has(themaRef)) {
2536
+ issues.push(
2537
+ issue(
2538
+ "QFAI-UI-020",
2539
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
2540
+ "error",
2541
+ file,
2542
+ "contracts.ui.themaRef",
2543
+ [themaRef]
2544
+ )
2545
+ );
2546
+ }
2547
+ }
2548
+ const assets = doc.assets;
2549
+ if (assets && typeof assets === "object") {
2550
+ issues.push(
2551
+ ...await validateUiAssets(
2552
+ assets,
2553
+ file,
2554
+ uiRoot
2555
+ )
2556
+ );
2557
+ }
2558
+ return issues;
2559
+ }
2560
+ async function validateUiAssets(assets, file, uiRoot) {
2561
+ const issues = [];
2562
+ const packValue = assets.pack;
2563
+ const useValue = assets.use;
2564
+ if (packValue === void 0 && useValue === void 0) {
2565
+ return issues;
2566
+ }
2567
+ if (typeof packValue !== "string" || packValue.length === 0) {
2568
+ issues.push(
2569
+ issue(
2570
+ "QFAI-ASSET-001",
2571
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2572
+ "error",
2573
+ file,
2574
+ "assets.pack"
2575
+ )
2576
+ );
2577
+ return issues;
2578
+ }
2579
+ if (!isSafeRelativePath(packValue)) {
2580
+ issues.push(
2581
+ issue(
2582
+ "QFAI-ASSET-001",
2583
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
2584
+ "error",
2585
+ file,
2586
+ "assets.pack",
2587
+ [packValue]
2588
+ )
2589
+ );
2590
+ return issues;
2591
+ }
2592
+ const packDir = path17.resolve(uiRoot, packValue);
2593
+ const packRelative = path17.relative(uiRoot, packDir);
2594
+ if (packRelative.startsWith("..") || path17.isAbsolute(packRelative)) {
2595
+ issues.push(
2596
+ issue(
2597
+ "QFAI-ASSET-001",
2598
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
2599
+ "error",
2600
+ file,
2601
+ "assets.pack",
2602
+ [packValue]
2603
+ )
2604
+ );
2605
+ return issues;
2606
+ }
2607
+ if (!await exists6(packDir)) {
2608
+ issues.push(
2609
+ issue(
2610
+ "QFAI-ASSET-001",
2611
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
2612
+ "error",
2613
+ file,
2614
+ "assets.pack",
2615
+ [packValue]
2616
+ )
2617
+ );
2618
+ return issues;
2619
+ }
2620
+ const assetsYamlPath = path17.join(packDir, "assets.yaml");
2621
+ if (!await exists6(assetsYamlPath)) {
2622
+ issues.push(
2623
+ issue(
2624
+ "QFAI-ASSET-002",
2625
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
2626
+ "error",
2627
+ assetsYamlPath,
2628
+ "assets.yaml"
2629
+ )
2630
+ );
2631
+ return issues;
2632
+ }
2633
+ let manifest;
2634
+ try {
2635
+ const manifestText = await readFile7(assetsYamlPath, "utf-8");
2636
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
2637
+ } catch (error2) {
2638
+ issues.push(
2639
+ issue(
2640
+ "QFAI-ASSET-002",
2641
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
2642
+ "error",
2643
+ assetsYamlPath,
2644
+ "assets.yaml"
2645
+ )
2646
+ );
2647
+ return issues;
2648
+ }
2649
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
2650
+ const itemIds = /* @__PURE__ */ new Set();
2651
+ const itemPaths = [];
2652
+ for (const item of items) {
2653
+ if (!item || typeof item !== "object") {
2654
+ continue;
2655
+ }
2656
+ const record2 = item;
2657
+ const id = typeof record2.id === "string" ? record2.id : void 0;
2658
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
2659
+ if (id) {
2660
+ itemIds.add(id);
2661
+ }
2662
+ itemPaths.push({ id, path: pathValue });
2663
+ }
2664
+ if (useValue !== void 0) {
2665
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
2666
+ issues.push(
2667
+ issue(
2668
+ "QFAI-ASSET-003",
2669
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2670
+ "error",
2671
+ file,
2672
+ "assets.use"
2673
+ )
2674
+ );
2675
+ } else {
2676
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
2677
+ if (missing.length > 0) {
2678
+ issues.push(
2679
+ issue(
2680
+ "QFAI-ASSET-003",
2681
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
2682
+ "error",
2683
+ file,
2684
+ "assets.use",
2685
+ missing
2686
+ )
2687
+ );
2688
+ }
2689
+ }
2690
+ }
2691
+ for (const entry of itemPaths) {
2692
+ if (!entry.path) {
2693
+ continue;
2694
+ }
2695
+ if (!isSafeRelativePath(entry.path)) {
2696
+ issues.push(
2697
+ issue(
2698
+ "QFAI-ASSET-004",
2699
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
2700
+ "error",
2701
+ assetsYamlPath,
2702
+ "assets.path",
2703
+ entry.id ? [entry.id] : [entry.path]
2704
+ )
2705
+ );
2706
+ continue;
2707
+ }
2708
+ const assetPath = path17.resolve(packDir, entry.path);
2709
+ const assetRelative = path17.relative(packDir, assetPath);
2710
+ if (assetRelative.startsWith("..") || path17.isAbsolute(assetRelative)) {
2711
+ issues.push(
2712
+ issue(
2713
+ "QFAI-ASSET-004",
2714
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
2715
+ "error",
2716
+ assetsYamlPath,
2717
+ "assets.path",
2718
+ entry.id ? [entry.id] : [entry.path]
2719
+ )
2720
+ );
2721
+ continue;
2722
+ }
2723
+ if (!await exists6(assetPath)) {
2724
+ issues.push(
2725
+ issue(
2726
+ "QFAI-ASSET-004",
2727
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
2728
+ "error",
2729
+ assetsYamlPath,
2730
+ "assets.path",
2731
+ entry.id ? [entry.id] : [entry.path]
2732
+ )
2733
+ );
2734
+ }
2735
+ }
2736
+ return issues;
2737
+ }
2738
+ function shouldIgnoreInvalidId(value, doc) {
2739
+ if (!doc) {
2740
+ return false;
2741
+ }
2742
+ const assets = doc.assets;
2743
+ if (!assets || typeof assets !== "object") {
2744
+ return false;
2745
+ }
2746
+ const packValue = assets.pack;
2747
+ if (typeof packValue !== "string" || packValue.length === 0) {
2748
+ return false;
2749
+ }
2750
+ const normalized = packValue.replace(/\\/g, "/");
2751
+ const basename = path17.posix.basename(normalized);
2752
+ if (!basename) {
2753
+ return false;
2754
+ }
2755
+ return value.toLowerCase() === basename.toLowerCase();
2756
+ }
2757
+ function isSafeRelativePath(value) {
2758
+ if (!value) {
2759
+ return false;
2760
+ }
2761
+ if (path17.isAbsolute(value)) {
2762
+ return false;
2763
+ }
2764
+ const normalized = value.replace(/\\/g, "/");
2765
+ if (/^[A-Za-z]:/.test(normalized)) {
2766
+ return false;
2767
+ }
2768
+ const segments = normalized.split("/");
2769
+ if (segments.some((segment) => segment === "..")) {
2770
+ return false;
2771
+ }
2772
+ return true;
2773
+ }
2774
+ async function exists6(target) {
2775
+ try {
2776
+ await access6(target);
2777
+ return true;
2778
+ } catch {
2779
+ return false;
2780
+ }
2781
+ }
2351
2782
  function formatError4(error2) {
2352
2783
  if (error2 instanceof Error) {
2353
2784
  return error2.message;
@@ -2378,12 +2809,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2378
2809
 
2379
2810
  // src/core/validators/delta.ts
2380
2811
  import { readFile as readFile8 } from "fs/promises";
2381
- import path17 from "path";
2382
- var SECTION_RE = /^##\s+変更区分/m;
2383
- var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2384
- var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
2385
- var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
2386
- var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
2812
+ import path18 from "path";
2387
2813
  async function validateDeltas(root, config) {
2388
2814
  const specsRoot = resolvePath(root, config, "specsDir");
2389
2815
  const packs = await collectSpecPackDirs(specsRoot);
@@ -2392,10 +2818,9 @@ async function validateDeltas(root, config) {
2392
2818
  }
2393
2819
  const issues = [];
2394
2820
  for (const pack of packs) {
2395
- const deltaPath = path17.join(pack, "delta.md");
2396
- let text;
2821
+ const deltaPath = path18.join(pack, "delta.md");
2397
2822
  try {
2398
- text = await readFile8(deltaPath, "utf-8");
2823
+ await readFile8(deltaPath, "utf-8");
2399
2824
  } catch (error2) {
2400
2825
  if (isMissingFileError2(error2)) {
2401
2826
  issues.push(
@@ -2404,41 +2829,16 @@ async function validateDeltas(root, config) {
2404
2829
  "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2405
2830
  "error",
2406
2831
  deltaPath,
2407
- "delta.exists"
2832
+ "delta.exists",
2833
+ void 0,
2834
+ "change",
2835
+ "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"
2408
2836
  )
2409
2837
  );
2410
2838
  continue;
2411
2839
  }
2412
2840
  throw error2;
2413
2841
  }
2414
- const hasSection = SECTION_RE.test(text);
2415
- const hasCompatibility = COMPAT_LINE_RE.test(text);
2416
- const hasChange = CHANGE_LINE_RE.test(text);
2417
- if (!hasSection || !hasCompatibility || !hasChange) {
2418
- issues.push(
2419
- issue2(
2420
- "QFAI-DELTA-002",
2421
- "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",
2422
- "error",
2423
- deltaPath,
2424
- "delta.section"
2425
- )
2426
- );
2427
- continue;
2428
- }
2429
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
2430
- const changeChecked = CHANGE_CHECKED_RE.test(text);
2431
- if (compatibilityChecked === changeChecked) {
2432
- issues.push(
2433
- issue2(
2434
- "QFAI-DELTA-003",
2435
- "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",
2436
- "error",
2437
- deltaPath,
2438
- "delta.classification"
2439
- )
2440
- );
2441
- }
2442
2842
  }
2443
2843
  return issues;
2444
2844
  }
@@ -2472,7 +2872,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2472
2872
 
2473
2873
  // src/core/validators/ids.ts
2474
2874
  import { readFile as readFile9 } from "fs/promises";
2475
- import path18 from "path";
2875
+ import path19 from "path";
2476
2876
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2477
2877
  async function validateDefinedIds(root, config) {
2478
2878
  const issues = [];
@@ -2538,7 +2938,7 @@ function recordId(out, id, file) {
2538
2938
  }
2539
2939
  function formatFileList(files, root) {
2540
2940
  return files.map((file) => {
2541
- const relative = path18.relative(root, file);
2941
+ const relative = path19.relative(root, file);
2542
2942
  return relative.length > 0 ? relative : file;
2543
2943
  }).join(", ");
2544
2944
  }
@@ -2596,7 +2996,8 @@ async function validatePromptsIntegrity(root) {
2596
2996
  }
2597
2997
 
2598
2998
  // src/core/validators/scenario.ts
2599
- import { readFile as readFile10 } from "fs/promises";
2999
+ import { access as access7, readFile as readFile10 } from "fs/promises";
3000
+ import path20 from "path";
2600
3001
  var GIVEN_PATTERN = /\bGiven\b/;
2601
3002
  var WHEN_PATTERN = /\bWhen\b/;
2602
3003
  var THEN_PATTERN = /\bThen\b/;
@@ -2619,6 +3020,18 @@ async function validateScenarios(root, config) {
2619
3020
  }
2620
3021
  const issues = [];
2621
3022
  for (const entry of entries) {
3023
+ const legacyScenarioPath = path20.join(entry.dir, "scenario.md");
3024
+ if (await fileExists(legacyScenarioPath)) {
3025
+ issues.push(
3026
+ issue4(
3027
+ "QFAI-SC-004",
3028
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3029
+ "error",
3030
+ legacyScenarioPath,
3031
+ "scenario.legacy"
3032
+ )
3033
+ );
3034
+ }
2622
3035
  let text;
2623
3036
  try {
2624
3037
  text = await readFile10(entry.scenarioPath, "utf-8");
@@ -2650,6 +3063,7 @@ function validateScenarioContent(text, file) {
2650
3063
  "UI",
2651
3064
  "API",
2652
3065
  "DB",
3066
+ "THEMA",
2653
3067
  "ADR"
2654
3068
  ]);
2655
3069
  if (invalidIds.length > 0) {
@@ -2793,6 +3207,14 @@ function isMissingFileError3(error2) {
2793
3207
  }
2794
3208
  return error2.code === "ENOENT";
2795
3209
  }
3210
+ async function fileExists(target) {
3211
+ try {
3212
+ await access7(target);
3213
+ return true;
3214
+ } catch {
3215
+ return false;
3216
+ }
3217
+ }
2796
3218
 
2797
3219
  // src/core/validators/spec.ts
2798
3220
  import { readFile as readFile11 } from "fs/promises";
@@ -2852,6 +3274,7 @@ function validateSpecContent(text, file, requiredSections) {
2852
3274
  "UI",
2853
3275
  "API",
2854
3276
  "DB",
3277
+ "THEMA",
2855
3278
  "ADR"
2856
3279
  ]);
2857
3280
  if (invalidIds.length > 0) {
@@ -3031,7 +3454,7 @@ async function validateTraceability(root, config) {
3031
3454
  "QFAI-TRACE-021",
3032
3455
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
3033
3456
  ", "
3034
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3457
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3035
3458
  "error",
3036
3459
  file,
3037
3460
  "traceability.specContractRefFormat",
@@ -3095,7 +3518,7 @@ async function validateTraceability(root, config) {
3095
3518
  "QFAI-TRACE-032",
3096
3519
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
3097
3520
  ", "
3098
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
3521
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
3099
3522
  "error",
3100
3523
  file,
3101
3524
  "traceability.scenarioContractRefFormat",
@@ -3474,17 +3897,25 @@ function countIssues(issues) {
3474
3897
  }
3475
3898
 
3476
3899
  // src/core/report.ts
3477
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3900
+ var ID_PREFIXES2 = [
3901
+ "SPEC",
3902
+ "BR",
3903
+ "SC",
3904
+ "UI",
3905
+ "API",
3906
+ "DB",
3907
+ "THEMA"
3908
+ ];
3478
3909
  async function createReportData(root, validation, configResult) {
3479
- const resolvedRoot = path19.resolve(root);
3910
+ const resolvedRoot = path21.resolve(root);
3480
3911
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3481
3912
  const config = resolved.config;
3482
3913
  const configPath = resolved.configPath;
3483
3914
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3484
3915
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3485
- const apiRoot = path19.join(contractsRoot, "api");
3486
- const uiRoot = path19.join(contractsRoot, "ui");
3487
- const dbRoot = path19.join(contractsRoot, "db");
3916
+ const apiRoot = path21.join(contractsRoot, "api");
3917
+ const uiRoot = path21.join(contractsRoot, "ui");
3918
+ const dbRoot = path21.join(contractsRoot, "db");
3488
3919
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3489
3920
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3490
3921
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3492,7 +3923,8 @@ async function createReportData(root, validation, configResult) {
3492
3923
  const {
3493
3924
  api: apiFiles,
3494
3925
  ui: uiFiles,
3495
- db: dbFiles
3926
+ db: dbFiles,
3927
+ thema: themaFiles
3496
3928
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
3497
3929
  const contractIndex = await buildContractIndex(resolvedRoot, config);
3498
3930
  const contractIdList = Array.from(contractIndex.ids);
@@ -3519,7 +3951,8 @@ async function createReportData(root, validation, configResult) {
3519
3951
  ...scenarioFiles,
3520
3952
  ...apiFiles,
3521
3953
  ...uiFiles,
3522
- ...dbFiles
3954
+ ...dbFiles,
3955
+ ...themaFiles
3523
3956
  ]);
3524
3957
  const upstreamIds = await collectUpstreamIds([
3525
3958
  ...specFiles,
@@ -3556,7 +3989,8 @@ async function createReportData(root, validation, configResult) {
3556
3989
  contracts: {
3557
3990
  api: apiFiles.length,
3558
3991
  ui: uiFiles.length,
3559
- db: dbFiles.length
3992
+ db: dbFiles.length,
3993
+ thema: themaFiles.length
3560
3994
  },
3561
3995
  counts: normalizedValidation.counts
3562
3996
  },
@@ -3566,7 +4000,8 @@ async function createReportData(root, validation, configResult) {
3566
4000
  sc: idsByPrefix.SC,
3567
4001
  ui: idsByPrefix.UI,
3568
4002
  api: idsByPrefix.API,
3569
- db: idsByPrefix.DB
4003
+ db: idsByPrefix.DB,
4004
+ thema: idsByPrefix.THEMA
3570
4005
  },
3571
4006
  traceability: {
3572
4007
  upstreamIdsFound: upstreamIds.size,
@@ -3636,7 +4071,7 @@ function formatReportMarkdown(data, options = {}) {
3636
4071
  lines.push(`- specs: ${data.summary.specs}`);
3637
4072
  lines.push(`- scenarios: ${data.summary.scenarios}`);
3638
4073
  lines.push(
3639
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
4074
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
3640
4075
  );
3641
4076
  lines.push(
3642
4077
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3780,6 +4215,7 @@ function formatReportMarkdown(data, options = {}) {
3780
4215
  lines.push(formatIdLine("UI", data.ids.ui));
3781
4216
  lines.push(formatIdLine("API", data.ids.api));
3782
4217
  lines.push(formatIdLine("DB", data.ids.db));
4218
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3783
4219
  lines.push("");
3784
4220
  lines.push("## Traceability");
3785
4221
  lines.push("");
@@ -3944,12 +4380,8 @@ function formatReportMarkdown(data, options = {}) {
3944
4380
  "- 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"
3945
4381
  );
3946
4382
  }
3947
- lines.push(
3948
- "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3949
- );
3950
- lines.push(
3951
- "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3952
- );
4383
+ lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
4384
+ lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
3953
4385
  return lines.join("\n");
3954
4386
  }
3955
4387
  function formatReportJson(data) {
@@ -4001,7 +4433,8 @@ async function collectIds(files) {
4001
4433
  SC: /* @__PURE__ */ new Set(),
4002
4434
  UI: /* @__PURE__ */ new Set(),
4003
4435
  API: /* @__PURE__ */ new Set(),
4004
- DB: /* @__PURE__ */ new Set()
4436
+ DB: /* @__PURE__ */ new Set(),
4437
+ THEMA: /* @__PURE__ */ new Set()
4005
4438
  };
4006
4439
  for (const file of files) {
4007
4440
  const text = await readFile13(file, "utf-8");
@@ -4016,7 +4449,8 @@ async function collectIds(files) {
4016
4449
  SC: toSortedArray2(result.SC),
4017
4450
  UI: toSortedArray2(result.UI),
4018
4451
  API: toSortedArray2(result.API),
4019
- DB: toSortedArray2(result.DB)
4452
+ DB: toSortedArray2(result.DB),
4453
+ THEMA: toSortedArray2(result.THEMA)
4020
4454
  };
4021
4455
  }
4022
4456
  async function collectUpstreamIds(files) {
@@ -4180,7 +4614,7 @@ function warnIfTruncated(scan, context) {
4180
4614
 
4181
4615
  // src/cli/commands/report.ts
4182
4616
  async function runReport(options) {
4183
- const root = path20.resolve(options.root);
4617
+ const root = path22.resolve(options.root);
4184
4618
  const configResult = await loadConfig(root);
4185
4619
  let validation;
4186
4620
  if (options.runValidate) {
@@ -4197,7 +4631,7 @@ async function runReport(options) {
4197
4631
  validation = normalized;
4198
4632
  } else {
4199
4633
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
4200
- const inputPath = path20.isAbsolute(input) ? input : path20.resolve(root, input);
4634
+ const inputPath = path22.isAbsolute(input) ? input : path22.resolve(root, input);
4201
4635
  try {
4202
4636
  validation = await readValidationResult(inputPath);
4203
4637
  } catch (err) {
@@ -4224,10 +4658,10 @@ async function runReport(options) {
4224
4658
  warnIfTruncated(data.traceability.testFiles, "report");
4225
4659
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4226
4660
  const outRoot = resolvePath(root, configResult.config, "outDir");
4227
- const defaultOut = options.format === "json" ? path20.join(outRoot, "report.json") : path20.join(outRoot, "report.md");
4661
+ const defaultOut = options.format === "json" ? path22.join(outRoot, "report.json") : path22.join(outRoot, "report.md");
4228
4662
  const out = options.outPath ?? defaultOut;
4229
- const outPath = path20.isAbsolute(out) ? out : path20.resolve(root, out);
4230
- await mkdir3(path20.dirname(outPath), { recursive: true });
4663
+ const outPath = path22.isAbsolute(out) ? out : path22.resolve(root, out);
4664
+ await mkdir3(path22.dirname(outPath), { recursive: true });
4231
4665
  await writeFile2(outPath, `${output}
4232
4666
  `, "utf-8");
4233
4667
  info(
@@ -4292,15 +4726,15 @@ function isMissingFileError5(error2) {
4292
4726
  return record2.code === "ENOENT";
4293
4727
  }
4294
4728
  async function writeValidationResult(root, outputPath, result) {
4295
- const abs = path20.isAbsolute(outputPath) ? outputPath : path20.resolve(root, outputPath);
4296
- await mkdir3(path20.dirname(abs), { recursive: true });
4729
+ const abs = path22.isAbsolute(outputPath) ? outputPath : path22.resolve(root, outputPath);
4730
+ await mkdir3(path22.dirname(abs), { recursive: true });
4297
4731
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
4298
4732
  `, "utf-8");
4299
4733
  }
4300
4734
 
4301
4735
  // src/cli/commands/validate.ts
4302
4736
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
4303
- import path21 from "path";
4737
+ import path23 from "path";
4304
4738
 
4305
4739
  // src/cli/lib/failOn.ts
4306
4740
  function shouldFail(result, failOn) {
@@ -4315,7 +4749,7 @@ function shouldFail(result, failOn) {
4315
4749
 
4316
4750
  // src/cli/commands/validate.ts
4317
4751
  async function runValidate(options) {
4318
- const root = path21.resolve(options.root);
4752
+ const root = path23.resolve(options.root);
4319
4753
  const configResult = await loadConfig(root);
4320
4754
  const result = await validateProject(root, configResult);
4321
4755
  const normalized = normalizeValidationResult(root, result);
@@ -4440,12 +4874,12 @@ function issueKey(issue7) {
4440
4874
  }
4441
4875
  async function emitJson(result, root, jsonPath) {
4442
4876
  const abs = resolveJsonPath(root, jsonPath);
4443
- await mkdir4(path21.dirname(abs), { recursive: true });
4877
+ await mkdir4(path23.dirname(abs), { recursive: true });
4444
4878
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4445
4879
  `, "utf-8");
4446
4880
  }
4447
4881
  function resolveJsonPath(root, jsonPath) {
4448
- return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
4882
+ return path23.isAbsolute(jsonPath) ? jsonPath : path23.resolve(root, jsonPath);
4449
4883
  }
4450
4884
  var GITHUB_ANNOTATION_LIMIT = 100;
4451
4885