qfai 0.8.0 → 0.9.0

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.
@@ -24,12 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli/commands/doctor.ts
27
- var import_promises8 = require("fs/promises");
28
- var import_node_path8 = __toESM(require("path"), 1);
27
+ var import_promises9 = require("fs/promises");
28
+ var import_node_path10 = __toESM(require("path"), 1);
29
29
 
30
30
  // src/core/doctor.ts
31
- var import_promises7 = require("fs/promises");
32
- var import_node_path7 = __toESM(require("path"), 1);
31
+ var import_promises8 = require("fs/promises");
32
+ var import_node_path9 = __toESM(require("path"), 1);
33
33
 
34
34
  // src/core/config.ts
35
35
  var import_promises = require("fs/promises");
@@ -401,6 +401,7 @@ function configIssue(file, message) {
401
401
  return {
402
402
  code: "QFAI_CONFIG_INVALID",
403
403
  severity: "error",
404
+ category: "compatibility",
404
405
  message,
405
406
  file,
406
407
  rule: "config.invalid"
@@ -884,17 +885,142 @@ function formatError3(error2) {
884
885
  return String(error2);
885
886
  }
886
887
 
887
- // src/core/version.ts
888
+ // src/core/promptsIntegrity.ts
888
889
  var import_promises6 = require("fs/promises");
890
+ var import_node_path7 = __toESM(require("path"), 1);
891
+
892
+ // src/shared/assets.ts
893
+ var import_node_fs = require("fs");
889
894
  var import_node_path6 = __toESM(require("path"), 1);
890
895
  var import_node_url = require("url");
896
+ function getInitAssetsDir() {
897
+ const base = __filename;
898
+ const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
899
+ const baseDir = import_node_path6.default.dirname(basePath);
900
+ const candidates = [
901
+ import_node_path6.default.resolve(baseDir, "../../../assets/init"),
902
+ import_node_path6.default.resolve(baseDir, "../../assets/init")
903
+ ];
904
+ for (const candidate of candidates) {
905
+ if ((0, import_node_fs.existsSync)(candidate)) {
906
+ return candidate;
907
+ }
908
+ }
909
+ throw new Error(
910
+ [
911
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
912
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
913
+ ...candidates.map((candidate) => `- ${candidate}`)
914
+ ].join("\n")
915
+ );
916
+ }
917
+
918
+ // src/core/promptsIntegrity.ts
919
+ async function diffProjectPromptsAgainstInitAssets(root) {
920
+ const promptsDir = import_node_path7.default.resolve(root, ".qfai", "prompts");
921
+ let templateDir;
922
+ try {
923
+ templateDir = import_node_path7.default.join(getInitAssetsDir(), ".qfai", "prompts");
924
+ } catch {
925
+ return {
926
+ status: "skipped_missing_assets",
927
+ promptsDir,
928
+ templateDir: "",
929
+ missing: [],
930
+ extra: [],
931
+ changed: []
932
+ };
933
+ }
934
+ const projectFiles = await collectFiles(promptsDir);
935
+ if (projectFiles.length === 0) {
936
+ return {
937
+ status: "skipped_missing_prompts",
938
+ promptsDir,
939
+ templateDir,
940
+ missing: [],
941
+ extra: [],
942
+ changed: []
943
+ };
944
+ }
945
+ const templateFiles = await collectFiles(templateDir);
946
+ const templateByRel = /* @__PURE__ */ new Map();
947
+ for (const abs of templateFiles) {
948
+ templateByRel.set(toRel(templateDir, abs), abs);
949
+ }
950
+ const projectByRel = /* @__PURE__ */ new Map();
951
+ for (const abs of projectFiles) {
952
+ projectByRel.set(toRel(promptsDir, abs), abs);
953
+ }
954
+ const missing = [];
955
+ const extra = [];
956
+ const changed = [];
957
+ for (const rel of templateByRel.keys()) {
958
+ if (!projectByRel.has(rel)) {
959
+ missing.push(rel);
960
+ }
961
+ }
962
+ for (const rel of projectByRel.keys()) {
963
+ if (!templateByRel.has(rel)) {
964
+ extra.push(rel);
965
+ }
966
+ }
967
+ const common = intersectKeys(templateByRel, projectByRel);
968
+ for (const rel of common) {
969
+ const templateAbs = templateByRel.get(rel);
970
+ const projectAbs = projectByRel.get(rel);
971
+ if (!templateAbs || !projectAbs) {
972
+ continue;
973
+ }
974
+ try {
975
+ const [a, b] = await Promise.all([
976
+ (0, import_promises6.readFile)(templateAbs, "utf-8"),
977
+ (0, import_promises6.readFile)(projectAbs, "utf-8")
978
+ ]);
979
+ if (normalizeNewlines(a) !== normalizeNewlines(b)) {
980
+ changed.push(rel);
981
+ }
982
+ } catch {
983
+ changed.push(rel);
984
+ }
985
+ }
986
+ const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
987
+ return {
988
+ status,
989
+ promptsDir,
990
+ templateDir,
991
+ missing: missing.sort(),
992
+ extra: extra.sort(),
993
+ changed: changed.sort()
994
+ };
995
+ }
996
+ function normalizeNewlines(text) {
997
+ return text.replace(/\r\n/g, "\n");
998
+ }
999
+ function toRel(base, abs) {
1000
+ const rel = import_node_path7.default.relative(base, abs);
1001
+ return rel.replace(/[\\/]+/g, "/");
1002
+ }
1003
+ function intersectKeys(a, b) {
1004
+ const out = [];
1005
+ for (const key of a.keys()) {
1006
+ if (b.has(key)) {
1007
+ out.push(key);
1008
+ }
1009
+ }
1010
+ return out;
1011
+ }
1012
+
1013
+ // src/core/version.ts
1014
+ var import_promises7 = require("fs/promises");
1015
+ var import_node_path8 = __toESM(require("path"), 1);
1016
+ var import_node_url2 = require("url");
891
1017
  async function resolveToolVersion() {
892
- if ("0.8.0".length > 0) {
893
- return "0.8.0";
1018
+ if ("0.9.0".length > 0) {
1019
+ return "0.9.0";
894
1020
  }
895
1021
  try {
896
1022
  const packagePath = resolvePackageJsonPath();
897
- const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
1023
+ const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
898
1024
  const parsed = JSON.parse(raw);
899
1025
  const version = typeof parsed.version === "string" ? parsed.version : "";
900
1026
  return version.length > 0 ? version : "unknown";
@@ -904,14 +1030,14 @@ async function resolveToolVersion() {
904
1030
  }
905
1031
  function resolvePackageJsonPath() {
906
1032
  const base = __filename;
907
- const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
908
- return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
1033
+ const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1034
+ return import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
909
1035
  }
910
1036
 
911
1037
  // src/core/doctor.ts
912
1038
  async function exists4(target) {
913
1039
  try {
914
- await (0, import_promises7.access)(target);
1040
+ await (0, import_promises8.access)(target);
915
1041
  return true;
916
1042
  } catch {
917
1043
  return false;
@@ -931,7 +1057,7 @@ function normalizeGlobs2(values) {
931
1057
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
932
1058
  }
933
1059
  async function createDoctorData(options) {
934
- const startDir = import_node_path7.default.resolve(options.startDir);
1060
+ const startDir = import_node_path9.default.resolve(options.startDir);
935
1061
  const checks = [];
936
1062
  const configPath = getConfigPath(startDir);
937
1063
  const search = options.rootExplicit ? {
@@ -994,9 +1120,9 @@ async function createDoctorData(options) {
994
1120
  details: { path: toRelativePath(root, resolved) }
995
1121
  });
996
1122
  if (key === "promptsDir") {
997
- const promptsLocalDir = import_node_path7.default.join(
998
- import_node_path7.default.dirname(resolved),
999
- `${import_node_path7.default.basename(resolved)}.local`
1123
+ const promptsLocalDir = import_node_path9.default.join(
1124
+ import_node_path9.default.dirname(resolved),
1125
+ `${import_node_path9.default.basename(resolved)}.local`
1000
1126
  );
1001
1127
  const found = await exists4(promptsLocalDir);
1002
1128
  addCheck(checks, {
@@ -1006,6 +1132,49 @@ async function createDoctorData(options) {
1006
1132
  message: found ? "prompts.local exists (overlay can be used)" : "prompts.local is optional (create it to override prompts)",
1007
1133
  details: { path: toRelativePath(root, promptsLocalDir) }
1008
1134
  });
1135
+ const diff = await diffProjectPromptsAgainstInitAssets(root);
1136
+ if (diff.status === "skipped_missing_prompts") {
1137
+ addCheck(checks, {
1138
+ id: "prompts.integrity",
1139
+ severity: "info",
1140
+ title: "Prompts integrity (.qfai/prompts)",
1141
+ message: "prompts \u304C\u672A\u4F5C\u6210\u306E\u305F\u3081\u691C\u67FB\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F\uFF08'qfai init' \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
1142
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1143
+ });
1144
+ } else if (diff.status === "skipped_missing_assets") {
1145
+ addCheck(checks, {
1146
+ id: "prompts.integrity",
1147
+ severity: "info",
1148
+ title: "Prompts integrity (.qfai/prompts)",
1149
+ message: "init assets \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u691C\u67FB\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F\uFF08\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
1150
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1151
+ });
1152
+ } else if (diff.status === "ok") {
1153
+ addCheck(checks, {
1154
+ id: "prompts.integrity",
1155
+ severity: "ok",
1156
+ title: "Prompts integrity (.qfai/prompts)",
1157
+ message: "\u6A19\u6E96 assets \u3068\u4E00\u81F4\u3057\u3066\u3044\u307E\u3059",
1158
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1159
+ });
1160
+ } else {
1161
+ addCheck(checks, {
1162
+ id: "prompts.integrity",
1163
+ severity: "error",
1164
+ title: "Prompts integrity (.qfai/prompts)",
1165
+ message: "\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\u3002prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
1166
+ details: {
1167
+ promptsDir: toRelativePath(root, diff.promptsDir),
1168
+ missing: diff.missing,
1169
+ extra: diff.extra,
1170
+ changed: diff.changed,
1171
+ nextActions: [
1172
+ "\u5909\u66F4\u5185\u5BB9\u3092 .qfai/prompts.local/** \u306B\u79FB\u3059\uFF08\u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067\u914D\u7F6E\uFF09",
1173
+ "\u5FC5\u8981\u306A\u3089 qfai init --force \u3067 prompts \u3092\u6A19\u6E96\u72B6\u614B\u3078\u623B\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u307E\u3059\uFF09"
1174
+ ]
1175
+ }
1176
+ });
1177
+ }
1009
1178
  }
1010
1179
  }
1011
1180
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1026,7 +1195,7 @@ async function createDoctorData(options) {
1026
1195
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1027
1196
  details: { specPacks: entries.length, missingFiles }
1028
1197
  });
1029
- const validateJsonAbs = import_node_path7.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path7.default.resolve(root, config.output.validateJsonPath);
1198
+ const validateJsonAbs = import_node_path9.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path9.default.resolve(root, config.output.validateJsonPath);
1030
1199
  const validateJsonExists = await exists4(validateJsonAbs);
1031
1200
  addCheck(checks, {
1032
1201
  id: "output.validateJson",
@@ -1036,8 +1205,8 @@ async function createDoctorData(options) {
1036
1205
  details: { path: toRelativePath(root, validateJsonAbs) }
1037
1206
  });
1038
1207
  const outDirAbs = resolvePath(root, config, "outDir");
1039
- const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
1040
- const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
1208
+ const rel = import_node_path9.default.relative(outDirAbs, validateJsonAbs);
1209
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path9.default.isAbsolute(rel);
1041
1210
  addCheck(checks, {
1042
1211
  id: "output.pathAlignment",
1043
1212
  severity: inside ? "ok" : "warning",
@@ -1143,12 +1312,12 @@ async function detectOutDirCollisions(root) {
1143
1312
  ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1144
1313
  });
1145
1314
  const configRoots = Array.from(
1146
- new Set(configPaths.map((configPath) => import_node_path7.default.dirname(configPath)))
1315
+ new Set(configPaths.map((configPath) => import_node_path9.default.dirname(configPath)))
1147
1316
  ).sort((a, b) => a.localeCompare(b));
1148
1317
  const outDirToRoots = /* @__PURE__ */ new Map();
1149
1318
  for (const configRoot of configRoots) {
1150
1319
  const { config } = await loadConfig(configRoot);
1151
- const outDir = import_node_path7.default.normalize(resolvePath(configRoot, config, "outDir"));
1320
+ const outDir = import_node_path9.default.normalize(resolvePath(configRoot, config, "outDir"));
1152
1321
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1153
1322
  roots.add(configRoot);
1154
1323
  outDirToRoots.set(outDir, roots);
@@ -1165,20 +1334,20 @@ async function detectOutDirCollisions(root) {
1165
1334
  return { monorepoRoot, configRoots, collisions };
1166
1335
  }
1167
1336
  async function findMonorepoRoot(startDir) {
1168
- let current = import_node_path7.default.resolve(startDir);
1337
+ let current = import_node_path9.default.resolve(startDir);
1169
1338
  while (true) {
1170
- const gitPath = import_node_path7.default.join(current, ".git");
1171
- const workspacePath = import_node_path7.default.join(current, "pnpm-workspace.yaml");
1339
+ const gitPath = import_node_path9.default.join(current, ".git");
1340
+ const workspacePath = import_node_path9.default.join(current, "pnpm-workspace.yaml");
1172
1341
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1173
1342
  return current;
1174
1343
  }
1175
- const parent = import_node_path7.default.dirname(current);
1344
+ const parent = import_node_path9.default.dirname(current);
1176
1345
  if (parent === current) {
1177
1346
  break;
1178
1347
  }
1179
1348
  current = parent;
1180
1349
  }
1181
- return import_node_path7.default.resolve(startDir);
1350
+ return import_node_path9.default.resolve(startDir);
1182
1351
  }
1183
1352
 
1184
1353
  // src/cli/lib/logger.ts
@@ -1220,9 +1389,9 @@ async function runDoctor(options) {
1220
1389
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1221
1390
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1222
1391
  if (options.outPath) {
1223
- const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
1224
- await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
1225
- await (0, import_promises8.writeFile)(outAbs, `${output}
1392
+ const outAbs = import_node_path10.default.isAbsolute(options.outPath) ? options.outPath : import_node_path10.default.resolve(process.cwd(), options.outPath);
1393
+ await (0, import_promises9.mkdir)(import_node_path10.default.dirname(outAbs), { recursive: true });
1394
+ await (0, import_promises9.writeFile)(outAbs, `${output}
1226
1395
  `, "utf-8");
1227
1396
  info(`doctor: wrote ${outAbs}`);
1228
1397
  return exitCode;
@@ -1241,36 +1410,59 @@ function shouldFailDoctor(summary, failOn) {
1241
1410
  }
1242
1411
 
1243
1412
  // src/cli/commands/init.ts
1244
- var import_node_path11 = __toESM(require("path"), 1);
1413
+ var import_node_path12 = __toESM(require("path"), 1);
1245
1414
 
1246
1415
  // src/cli/lib/fs.ts
1247
- var import_promises9 = require("fs/promises");
1248
- var import_node_path9 = __toESM(require("path"), 1);
1416
+ var import_promises10 = require("fs/promises");
1417
+ var import_node_path11 = __toESM(require("path"), 1);
1249
1418
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1250
1419
  const files = await collectTemplateFiles(sourceRoot);
1251
1420
  return copyFiles(files, sourceRoot, destRoot, options);
1252
1421
  }
1422
+ async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1423
+ const allFiles = [];
1424
+ for (const relPath of relativePaths) {
1425
+ const fullPath = import_node_path11.default.join(sourceRoot, relPath);
1426
+ const files = await collectTemplateFiles(fullPath);
1427
+ allFiles.push(...files);
1428
+ }
1429
+ return copyFiles(allFiles, sourceRoot, destRoot, options);
1430
+ }
1253
1431
  async function copyFiles(files, sourceRoot, destRoot, options) {
1254
1432
  const copied = [];
1255
1433
  const skipped = [];
1256
1434
  const conflicts = [];
1257
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path9.default.sep);
1435
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path11.default.sep);
1436
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path11.default.sep);
1258
1437
  const isProtectedRelative = (relative) => {
1259
1438
  if (protectPrefixes.length === 0) {
1260
1439
  return false;
1261
1440
  }
1262
- const normalized = relative.replace(/[\\/]+/g, import_node_path9.default.sep);
1441
+ const normalized = relative.replace(/[\\/]+/g, import_node_path11.default.sep);
1263
1442
  return protectPrefixes.some(
1264
1443
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1265
1444
  );
1266
1445
  };
1267
- if (!options.force) {
1446
+ const isExcludedRelative = (relative) => {
1447
+ if (excludePrefixes.length === 0) {
1448
+ return false;
1449
+ }
1450
+ const normalized = relative.replace(/[\\/]+/g, import_node_path11.default.sep);
1451
+ return excludePrefixes.some(
1452
+ (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1453
+ );
1454
+ };
1455
+ const conflictPolicy = options.conflictPolicy ?? "error";
1456
+ if (!options.force && conflictPolicy === "error") {
1268
1457
  for (const file of files) {
1269
- const relative = import_node_path9.default.relative(sourceRoot, file);
1458
+ const relative = import_node_path11.default.relative(sourceRoot, file);
1459
+ if (isExcludedRelative(relative)) {
1460
+ continue;
1461
+ }
1270
1462
  if (isProtectedRelative(relative)) {
1271
1463
  continue;
1272
1464
  }
1273
- const dest = import_node_path9.default.join(destRoot, relative);
1465
+ const dest = import_node_path11.default.join(destRoot, relative);
1274
1466
  if (!await shouldWrite(dest, options.force)) {
1275
1467
  conflicts.push(dest);
1276
1468
  }
@@ -1280,16 +1472,19 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1280
1472
  }
1281
1473
  }
1282
1474
  for (const file of files) {
1283
- const relative = import_node_path9.default.relative(sourceRoot, file);
1284
- const dest = import_node_path9.default.join(destRoot, relative);
1475
+ const relative = import_node_path11.default.relative(sourceRoot, file);
1476
+ if (isExcludedRelative(relative)) {
1477
+ continue;
1478
+ }
1479
+ const dest = import_node_path11.default.join(destRoot, relative);
1285
1480
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1286
1481
  if (!await shouldWrite(dest, forceForThisFile)) {
1287
1482
  skipped.push(dest);
1288
1483
  continue;
1289
1484
  }
1290
1485
  if (!options.dryRun) {
1291
- await (0, import_promises9.mkdir)(import_node_path9.default.dirname(dest), { recursive: true });
1292
- await (0, import_promises9.copyFile)(file, dest);
1486
+ await (0, import_promises10.mkdir)(import_node_path11.default.dirname(dest), { recursive: true });
1487
+ await (0, import_promises10.copyFile)(file, dest);
1293
1488
  }
1294
1489
  copied.push(dest);
1295
1490
  }
@@ -1310,9 +1505,9 @@ async function collectTemplateFiles(root) {
1310
1505
  if (!await exists5(root)) {
1311
1506
  return entries;
1312
1507
  }
1313
- const items = await (0, import_promises9.readdir)(root, { withFileTypes: true });
1508
+ const items = await (0, import_promises10.readdir)(root, { withFileTypes: true });
1314
1509
  for (const item of items) {
1315
- const fullPath = import_node_path9.default.join(root, item.name);
1510
+ const fullPath = import_node_path11.default.join(root, item.name);
1316
1511
  if (item.isDirectory()) {
1317
1512
  const nested = await collectTemplateFiles(fullPath);
1318
1513
  entries.push(...nested);
@@ -1332,58 +1527,51 @@ async function shouldWrite(target, force) {
1332
1527
  }
1333
1528
  async function exists5(target) {
1334
1529
  try {
1335
- await (0, import_promises9.access)(target);
1530
+ await (0, import_promises10.access)(target);
1336
1531
  return true;
1337
1532
  } catch {
1338
1533
  return false;
1339
1534
  }
1340
1535
  }
1341
1536
 
1342
- // src/cli/lib/assets.ts
1343
- var import_node_fs = require("fs");
1344
- var import_node_path10 = __toESM(require("path"), 1);
1345
- var import_node_url2 = require("url");
1346
- function getInitAssetsDir() {
1347
- const base = __filename;
1348
- const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1349
- const baseDir = import_node_path10.default.dirname(basePath);
1350
- const candidates = [
1351
- import_node_path10.default.resolve(baseDir, "../../../assets/init"),
1352
- import_node_path10.default.resolve(baseDir, "../../assets/init")
1353
- ];
1354
- for (const candidate of candidates) {
1355
- if ((0, import_node_fs.existsSync)(candidate)) {
1356
- return candidate;
1357
- }
1358
- }
1359
- throw new Error(
1360
- [
1361
- "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1362
- "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1363
- ...candidates.map((candidate) => `- ${candidate}`)
1364
- ].join("\n")
1365
- );
1366
- }
1367
-
1368
1537
  // src/cli/commands/init.ts
1369
1538
  async function runInit(options) {
1370
1539
  const assetsRoot = getInitAssetsDir();
1371
- const rootAssets = import_node_path11.default.join(assetsRoot, "root");
1372
- const qfaiAssets = import_node_path11.default.join(assetsRoot, ".qfai");
1373
- const destRoot = import_node_path11.default.resolve(options.dir);
1374
- const destQfai = import_node_path11.default.join(destRoot, ".qfai");
1540
+ const rootAssets = import_node_path12.default.join(assetsRoot, "root");
1541
+ const qfaiAssets = import_node_path12.default.join(assetsRoot, ".qfai");
1542
+ const destRoot = import_node_path12.default.resolve(options.dir);
1543
+ const destQfai = import_node_path12.default.join(destRoot, ".qfai");
1544
+ if (options.force) {
1545
+ info(
1546
+ "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"
1547
+ );
1548
+ }
1375
1549
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1376
- force: options.force,
1377
- dryRun: options.dryRun
1550
+ force: false,
1551
+ dryRun: options.dryRun,
1552
+ conflictPolicy: "skip"
1378
1553
  });
1379
1554
  const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1380
- force: options.force,
1555
+ force: false,
1381
1556
  dryRun: options.dryRun,
1382
- protect: ["prompts.local"]
1557
+ conflictPolicy: "skip",
1558
+ protect: ["prompts.local"],
1559
+ exclude: ["prompts"]
1383
1560
  });
1561
+ const promptsResult = await copyTemplatePaths(
1562
+ qfaiAssets,
1563
+ destQfai,
1564
+ ["prompts"],
1565
+ {
1566
+ force: options.force,
1567
+ dryRun: options.dryRun,
1568
+ conflictPolicy: "skip",
1569
+ protect: ["prompts.local"]
1570
+ }
1571
+ );
1384
1572
  report(
1385
- [...rootResult.copied, ...qfaiResult.copied],
1386
- [...rootResult.skipped, ...qfaiResult.skipped],
1573
+ [...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
1574
+ [...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
1387
1575
  options.dryRun,
1388
1576
  "init"
1389
1577
  );
@@ -1399,8 +1587,8 @@ function report(copied, skipped, dryRun, label) {
1399
1587
  }
1400
1588
 
1401
1589
  // src/cli/commands/report.ts
1402
- var import_promises18 = require("fs/promises");
1403
- var import_node_path18 = __toESM(require("path"), 1);
1590
+ var import_promises19 = require("fs/promises");
1591
+ var import_node_path19 = __toESM(require("path"), 1);
1404
1592
 
1405
1593
  // src/core/normalize.ts
1406
1594
  function normalizeIssuePaths(root, issues) {
@@ -1440,12 +1628,12 @@ function normalizeValidationResult(root, result) {
1440
1628
  }
1441
1629
 
1442
1630
  // src/core/report.ts
1443
- var import_promises17 = require("fs/promises");
1444
- var import_node_path17 = __toESM(require("path"), 1);
1631
+ var import_promises18 = require("fs/promises");
1632
+ var import_node_path18 = __toESM(require("path"), 1);
1445
1633
 
1446
1634
  // src/core/contractIndex.ts
1447
- var import_promises10 = require("fs/promises");
1448
- var import_node_path12 = __toESM(require("path"), 1);
1635
+ var import_promises11 = require("fs/promises");
1636
+ var import_node_path13 = __toESM(require("path"), 1);
1449
1637
 
1450
1638
  // src/core/contractsDecl.ts
1451
1639
  var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
@@ -1467,9 +1655,9 @@ function stripContractDeclarationLines(text) {
1467
1655
  // src/core/contractIndex.ts
1468
1656
  async function buildContractIndex(root, config) {
1469
1657
  const contractsRoot = resolvePath(root, config, "contractsDir");
1470
- const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
1471
- const apiRoot = import_node_path12.default.join(contractsRoot, "api");
1472
- const dbRoot = import_node_path12.default.join(contractsRoot, "db");
1658
+ const uiRoot = import_node_path13.default.join(contractsRoot, "ui");
1659
+ const apiRoot = import_node_path13.default.join(contractsRoot, "api");
1660
+ const dbRoot = import_node_path13.default.join(contractsRoot, "db");
1473
1661
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1474
1662
  collectUiContractFiles(uiRoot),
1475
1663
  collectApiContractFiles(apiRoot),
@@ -1487,7 +1675,7 @@ async function buildContractIndex(root, config) {
1487
1675
  }
1488
1676
  async function indexContractFiles(files, index) {
1489
1677
  for (const file of files) {
1490
- const text = await (0, import_promises10.readFile)(file, "utf-8");
1678
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1491
1679
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1492
1680
  }
1493
1681
  }
@@ -1722,14 +1910,14 @@ function parseSpec(md, file) {
1722
1910
  }
1723
1911
 
1724
1912
  // src/core/validators/contracts.ts
1725
- var import_promises11 = require("fs/promises");
1726
- var import_node_path14 = __toESM(require("path"), 1);
1913
+ var import_promises12 = require("fs/promises");
1914
+ var import_node_path15 = __toESM(require("path"), 1);
1727
1915
 
1728
1916
  // src/core/contracts.ts
1729
- var import_node_path13 = __toESM(require("path"), 1);
1917
+ var import_node_path14 = __toESM(require("path"), 1);
1730
1918
  var import_yaml2 = require("yaml");
1731
1919
  function parseStructuredContract(file, text) {
1732
- const ext = import_node_path13.default.extname(file).toLowerCase();
1920
+ const ext = import_node_path14.default.extname(file).toLowerCase();
1733
1921
  if (ext === ".json") {
1734
1922
  return JSON.parse(text);
1735
1923
  }
@@ -1749,9 +1937,9 @@ var SQL_DANGEROUS_PATTERNS = [
1749
1937
  async function validateContracts(root, config) {
1750
1938
  const issues = [];
1751
1939
  const contractsRoot = resolvePath(root, config, "contractsDir");
1752
- issues.push(...await validateUiContracts(import_node_path14.default.join(contractsRoot, "ui")));
1753
- issues.push(...await validateApiContracts(import_node_path14.default.join(contractsRoot, "api")));
1754
- issues.push(...await validateDbContracts(import_node_path14.default.join(contractsRoot, "db")));
1940
+ issues.push(...await validateUiContracts(import_node_path15.default.join(contractsRoot, "ui")));
1941
+ issues.push(...await validateApiContracts(import_node_path15.default.join(contractsRoot, "api")));
1942
+ issues.push(...await validateDbContracts(import_node_path15.default.join(contractsRoot, "db")));
1755
1943
  const contractIndex = await buildContractIndex(root, config);
1756
1944
  issues.push(...validateDuplicateContractIds(contractIndex));
1757
1945
  return issues;
@@ -1771,7 +1959,7 @@ async function validateUiContracts(uiRoot) {
1771
1959
  }
1772
1960
  const issues = [];
1773
1961
  for (const file of files) {
1774
- const text = await (0, import_promises11.readFile)(file, "utf-8");
1962
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1775
1963
  const invalidIds = extractInvalidIds(text, [
1776
1964
  "SPEC",
1777
1965
  "BR",
@@ -1826,7 +2014,7 @@ async function validateApiContracts(apiRoot) {
1826
2014
  }
1827
2015
  const issues = [];
1828
2016
  for (const file of files) {
1829
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2017
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1830
2018
  const invalidIds = extractInvalidIds(text, [
1831
2019
  "SPEC",
1832
2020
  "BR",
@@ -1894,7 +2082,7 @@ async function validateDbContracts(dbRoot) {
1894
2082
  }
1895
2083
  const issues = [];
1896
2084
  for (const file of files) {
1897
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2085
+ const text = await (0, import_promises12.readFile)(file, "utf-8");
1898
2086
  const invalidIds = extractInvalidIds(text, [
1899
2087
  "SPEC",
1900
2088
  "BR",
@@ -2014,12 +2202,16 @@ function formatError4(error2) {
2014
2202
  }
2015
2203
  return String(error2);
2016
2204
  }
2017
- function issue(code, message, severity, file, rule, refs) {
2205
+ function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2018
2206
  const issue7 = {
2019
2207
  code,
2020
2208
  severity,
2209
+ category,
2021
2210
  message
2022
2211
  };
2212
+ if (suggested_action) {
2213
+ issue7.suggested_action = suggested_action;
2214
+ }
2023
2215
  if (file) {
2024
2216
  issue7.file = file;
2025
2217
  }
@@ -2033,8 +2225,8 @@ function issue(code, message, severity, file, rule, refs) {
2033
2225
  }
2034
2226
 
2035
2227
  // src/core/validators/delta.ts
2036
- var import_promises12 = require("fs/promises");
2037
- var import_node_path15 = __toESM(require("path"), 1);
2228
+ var import_promises13 = require("fs/promises");
2229
+ var import_node_path16 = __toESM(require("path"), 1);
2038
2230
  var SECTION_RE = /^##\s+変更区分/m;
2039
2231
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2040
2232
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2048,10 +2240,10 @@ async function validateDeltas(root, config) {
2048
2240
  }
2049
2241
  const issues = [];
2050
2242
  for (const pack of packs) {
2051
- const deltaPath = import_node_path15.default.join(pack, "delta.md");
2243
+ const deltaPath = import_node_path16.default.join(pack, "delta.md");
2052
2244
  let text;
2053
2245
  try {
2054
- text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
2246
+ text = await (0, import_promises13.readFile)(deltaPath, "utf-8");
2055
2247
  } catch (error2) {
2056
2248
  if (isMissingFileError2(error2)) {
2057
2249
  issues.push(
@@ -2104,12 +2296,16 @@ function isMissingFileError2(error2) {
2104
2296
  }
2105
2297
  return error2.code === "ENOENT";
2106
2298
  }
2107
- function issue2(code, message, severity, file, rule, refs) {
2299
+ function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
2108
2300
  const issue7 = {
2109
2301
  code,
2110
2302
  severity,
2303
+ category,
2111
2304
  message
2112
2305
  };
2306
+ if (suggested_action) {
2307
+ issue7.suggested_action = suggested_action;
2308
+ }
2113
2309
  if (file) {
2114
2310
  issue7.file = file;
2115
2311
  }
@@ -2123,8 +2319,8 @@ function issue2(code, message, severity, file, rule, refs) {
2123
2319
  }
2124
2320
 
2125
2321
  // src/core/validators/ids.ts
2126
- var import_promises13 = require("fs/promises");
2127
- var import_node_path16 = __toESM(require("path"), 1);
2322
+ var import_promises14 = require("fs/promises");
2323
+ var import_node_path17 = __toESM(require("path"), 1);
2128
2324
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2129
2325
  async function validateDefinedIds(root, config) {
2130
2326
  const issues = [];
@@ -2159,7 +2355,7 @@ async function validateDefinedIds(root, config) {
2159
2355
  }
2160
2356
  async function collectSpecDefinitionIds(files, out) {
2161
2357
  for (const file of files) {
2162
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2358
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2163
2359
  const parsed = parseSpec(text, file);
2164
2360
  if (parsed.specId) {
2165
2361
  recordId(out, parsed.specId, file);
@@ -2169,7 +2365,7 @@ async function collectSpecDefinitionIds(files, out) {
2169
2365
  }
2170
2366
  async function collectScenarioDefinitionIds(files, out) {
2171
2367
  for (const file of files) {
2172
- const text = await (0, import_promises13.readFile)(file, "utf-8");
2368
+ const text = await (0, import_promises14.readFile)(file, "utf-8");
2173
2369
  const { document, errors } = parseScenarioDocument(text, file);
2174
2370
  if (!document || errors.length > 0) {
2175
2371
  continue;
@@ -2190,16 +2386,20 @@ function recordId(out, id, file) {
2190
2386
  }
2191
2387
  function formatFileList(files, root) {
2192
2388
  return files.map((file) => {
2193
- const relative = import_node_path16.default.relative(root, file);
2389
+ const relative = import_node_path17.default.relative(root, file);
2194
2390
  return relative.length > 0 ? relative : file;
2195
2391
  }).join(", ");
2196
2392
  }
2197
- function issue3(code, message, severity, file, rule, refs) {
2393
+ function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2198
2394
  const issue7 = {
2199
2395
  code,
2200
2396
  severity,
2397
+ category,
2201
2398
  message
2202
2399
  };
2400
+ if (suggested_action) {
2401
+ issue7.suggested_action = suggested_action;
2402
+ }
2203
2403
  if (file) {
2204
2404
  issue7.file = file;
2205
2405
  }
@@ -2212,8 +2412,39 @@ function issue3(code, message, severity, file, rule, refs) {
2212
2412
  return issue7;
2213
2413
  }
2214
2414
 
2415
+ // src/core/validators/promptsIntegrity.ts
2416
+ async function validatePromptsIntegrity(root) {
2417
+ const diff = await diffProjectPromptsAgainstInitAssets(root);
2418
+ if (diff.status !== "modified") {
2419
+ return [];
2420
+ }
2421
+ const total = diff.missing.length + diff.extra.length + diff.changed.length;
2422
+ const hints = [
2423
+ diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
2424
+ diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
2425
+ diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
2426
+ ].filter(Boolean).join(" / ");
2427
+ const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
2428
+ const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
2429
+ return [
2430
+ {
2431
+ code: "QFAI-PROMPTS-001",
2432
+ severity: "error",
2433
+ category: "change",
2434
+ message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
2435
+ suggested_action: [
2436
+ "prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
2437
+ "\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
2438
+ "- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
2439
+ "- \u6A19\u6E96\u72B6\u614B\u3078\u623B\u3059\u5834\u5408: 'qfai init --force' \u3092\u5B9F\u884C\uFF08prompts \u306E\u307F\u4E0A\u66F8\u304D\u3001prompts.local \u306F\u4FDD\u8B77\uFF09"
2440
+ ].join("\n"),
2441
+ rule: "prompts.integrity"
2442
+ }
2443
+ ];
2444
+ }
2445
+
2215
2446
  // src/core/validators/scenario.ts
2216
- var import_promises14 = require("fs/promises");
2447
+ var import_promises15 = require("fs/promises");
2217
2448
  var GIVEN_PATTERN = /\bGiven\b/;
2218
2449
  var WHEN_PATTERN = /\bWhen\b/;
2219
2450
  var THEN_PATTERN = /\bThen\b/;
@@ -2239,7 +2470,7 @@ async function validateScenarios(root, config) {
2239
2470
  for (const entry of entries) {
2240
2471
  let text;
2241
2472
  try {
2242
- text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
2473
+ text = await (0, import_promises15.readFile)(entry.scenarioPath, "utf-8");
2243
2474
  } catch (error2) {
2244
2475
  if (isMissingFileError3(error2)) {
2245
2476
  issues.push(
@@ -2384,12 +2615,16 @@ function validateScenarioContent(text, file) {
2384
2615
  }
2385
2616
  return issues;
2386
2617
  }
2387
- function issue4(code, message, severity, file, rule, refs) {
2618
+ function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2388
2619
  const issue7 = {
2389
2620
  code,
2390
2621
  severity,
2622
+ category,
2391
2623
  message
2392
2624
  };
2625
+ if (suggested_action) {
2626
+ issue7.suggested_action = suggested_action;
2627
+ }
2393
2628
  if (file) {
2394
2629
  issue7.file = file;
2395
2630
  }
@@ -2409,7 +2644,7 @@ function isMissingFileError3(error2) {
2409
2644
  }
2410
2645
 
2411
2646
  // src/core/validators/spec.ts
2412
- var import_promises15 = require("fs/promises");
2647
+ var import_promises16 = require("fs/promises");
2413
2648
  async function validateSpecs(root, config) {
2414
2649
  const specsRoot = resolvePath(root, config, "specsDir");
2415
2650
  const entries = await collectSpecEntries(specsRoot);
@@ -2430,7 +2665,7 @@ async function validateSpecs(root, config) {
2430
2665
  for (const entry of entries) {
2431
2666
  let text;
2432
2667
  try {
2433
- text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
2668
+ text = await (0, import_promises16.readFile)(entry.specPath, "utf-8");
2434
2669
  } catch (error2) {
2435
2670
  if (isMissingFileError4(error2)) {
2436
2671
  issues.push(
@@ -2554,12 +2789,16 @@ function validateSpecContent(text, file, requiredSections) {
2554
2789
  }
2555
2790
  return issues;
2556
2791
  }
2557
- function issue5(code, message, severity, file, rule, refs) {
2792
+ function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2558
2793
  const issue7 = {
2559
2794
  code,
2560
2795
  severity,
2796
+ category,
2561
2797
  message
2562
2798
  };
2799
+ if (suggested_action) {
2800
+ issue7.suggested_action = suggested_action;
2801
+ }
2563
2802
  if (file) {
2564
2803
  issue7.file = file;
2565
2804
  }
@@ -2579,7 +2818,7 @@ function isMissingFileError4(error2) {
2579
2818
  }
2580
2819
 
2581
2820
  // src/core/validators/traceability.ts
2582
- var import_promises16 = require("fs/promises");
2821
+ var import_promises17 = require("fs/promises");
2583
2822
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2584
2823
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2585
2824
  async function validateTraceability(root, config) {
@@ -2599,7 +2838,7 @@ async function validateTraceability(root, config) {
2599
2838
  const contractIndex = await buildContractIndex(root, config);
2600
2839
  const contractIds = contractIndex.ids;
2601
2840
  for (const file of specFiles) {
2602
- const text = await (0, import_promises16.readFile)(file, "utf-8");
2841
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2603
2842
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2604
2843
  const parsed = parseSpec(text, file);
2605
2844
  if (parsed.specId) {
@@ -2672,7 +2911,7 @@ async function validateTraceability(root, config) {
2672
2911
  }
2673
2912
  }
2674
2913
  for (const file of scenarioFiles) {
2675
- const text = await (0, import_promises16.readFile)(file, "utf-8");
2914
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2676
2915
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2677
2916
  const scenarioContractRefs = parseContractRefs(text, {
2678
2917
  allowCommentPrefix: true
@@ -2994,7 +3233,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2994
3233
  const pattern = buildIdPattern(Array.from(upstreamIds));
2995
3234
  let found = false;
2996
3235
  for (const file of targetFiles) {
2997
- const text = await (0, import_promises16.readFile)(file, "utf-8");
3236
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
2998
3237
  if (pattern.test(text)) {
2999
3238
  found = true;
3000
3239
  break;
@@ -3017,12 +3256,16 @@ function buildIdPattern(ids) {
3017
3256
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3018
3257
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
3019
3258
  }
3020
- function issue6(code, message, severity, file, rule, refs) {
3259
+ function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
3021
3260
  const issue7 = {
3022
3261
  code,
3023
3262
  severity,
3263
+ category,
3024
3264
  message
3025
3265
  };
3266
+ if (suggested_action) {
3267
+ issue7.suggested_action = suggested_action;
3268
+ }
3026
3269
  if (file) {
3027
3270
  issue7.file = file;
3028
3271
  }
@@ -3041,6 +3284,7 @@ async function validateProject(root, configResult) {
3041
3284
  const { config, issues: configIssues } = resolved;
3042
3285
  const issues = [
3043
3286
  ...configIssues,
3287
+ ...await validatePromptsIntegrity(root),
3044
3288
  ...await validateSpecs(root, config),
3045
3289
  ...await validateDeltas(root, config),
3046
3290
  ...await validateScenarios(root, config),
@@ -3081,15 +3325,15 @@ function countIssues(issues) {
3081
3325
  // src/core/report.ts
3082
3326
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3083
3327
  async function createReportData(root, validation, configResult) {
3084
- const resolvedRoot = import_node_path17.default.resolve(root);
3328
+ const resolvedRoot = import_node_path18.default.resolve(root);
3085
3329
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3086
3330
  const config = resolved.config;
3087
3331
  const configPath = resolved.configPath;
3088
3332
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3089
3333
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3090
- const apiRoot = import_node_path17.default.join(contractsRoot, "api");
3091
- const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
3092
- const dbRoot = import_node_path17.default.join(contractsRoot, "db");
3334
+ const apiRoot = import_node_path18.default.join(contractsRoot, "api");
3335
+ const uiRoot = import_node_path18.default.join(contractsRoot, "ui");
3336
+ const dbRoot = import_node_path18.default.join(contractsRoot, "db");
3093
3337
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3094
3338
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3095
3339
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3203,7 +3447,39 @@ function formatReportMarkdown(data) {
3203
3447
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
3204
3448
  lines.push(`- \u7248: ${data.version}`);
3205
3449
  lines.push("");
3206
- lines.push("## Summary");
3450
+ const severityOrder = {
3451
+ error: 0,
3452
+ warning: 1,
3453
+ info: 2
3454
+ };
3455
+ const categoryOrder = {
3456
+ compatibility: 0,
3457
+ change: 1
3458
+ };
3459
+ const issuesByCategory = {
3460
+ compatibility: [],
3461
+ change: []
3462
+ };
3463
+ for (const issue7 of data.issues) {
3464
+ const cat = issue7.category;
3465
+ if (cat === "change") {
3466
+ issuesByCategory.change.push(issue7);
3467
+ } else {
3468
+ issuesByCategory.compatibility.push(issue7);
3469
+ }
3470
+ }
3471
+ const countIssuesBySeverity = (issues) => issues.reduce(
3472
+ (acc, i) => {
3473
+ acc[i.severity] += 1;
3474
+ return acc;
3475
+ },
3476
+ { info: 0, warning: 0, error: 0 }
3477
+ );
3478
+ const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
3479
+ const changeCounts = countIssuesBySeverity(issuesByCategory.change);
3480
+ lines.push("## Dashboard");
3481
+ lines.push("");
3482
+ lines.push("### Summary");
3207
3483
  lines.push("");
3208
3484
  lines.push(`- specs: ${data.summary.specs}`);
3209
3485
  lines.push(`- scenarios: ${data.summary.scenarios}`);
@@ -3211,7 +3487,13 @@ function formatReportMarkdown(data) {
3211
3487
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3212
3488
  );
3213
3489
  lines.push(
3214
- `- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3490
+ `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3491
+ );
3492
+ lines.push(
3493
+ `- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
3494
+ );
3495
+ lines.push(
3496
+ `- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
3215
3497
  );
3216
3498
  lines.push(
3217
3499
  `- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
@@ -3220,49 +3502,65 @@ function formatReportMarkdown(data) {
3220
3502
  `- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
3221
3503
  );
3222
3504
  lines.push("");
3223
- lines.push("## Findings");
3224
- lines.push("");
3225
- lines.push("### Issues (by code)");
3505
+ lines.push("### Next Actions");
3226
3506
  lines.push("");
3227
- const severityOrder = {
3228
- error: 0,
3229
- warning: 1,
3230
- info: 2
3231
- };
3232
- const issueKeyToCount = /* @__PURE__ */ new Map();
3233
- for (const issue7 of data.issues) {
3234
- const key = `${issue7.severity}|${issue7.code}`;
3235
- const current = issueKeyToCount.get(key);
3236
- if (current) {
3237
- current.count += 1;
3238
- continue;
3239
- }
3240
- issueKeyToCount.set(key, {
3241
- severity: issue7.severity,
3242
- code: issue7.code,
3243
- count: 1
3244
- });
3245
- }
3246
- const issueSummaryRows = Array.from(issueKeyToCount.values()).sort((a, b) => {
3247
- const sa = severityOrder[a.severity] ?? 999;
3248
- const sb = severityOrder[b.severity] ?? 999;
3249
- if (sa !== sb) return sa - sb;
3250
- return a.code.localeCompare(b.code);
3251
- }).map((x) => [x.severity, x.code, String(x.count)]);
3252
- if (issueSummaryRows.length === 0) {
3253
- lines.push("- (none)");
3507
+ if (data.summary.counts.error > 0) {
3508
+ lines.push(
3509
+ "- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A `qfai validate --fail-on error` \u3092\u901A\u308B\u307E\u3067\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3510
+ );
3511
+ lines.push(
3512
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3513
+ );
3514
+ } else if (data.summary.counts.warning > 0) {
3515
+ lines.push(
3516
+ "- warning \u306E\u6271\u3044\u306F\u30C1\u30FC\u30E0\u5224\u65AD\u3067\u3059\u3002`--fail-on warning` \u904B\u7528\u306A\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3517
+ );
3518
+ lines.push(
3519
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3520
+ );
3254
3521
  } else {
3522
+ lines.push("- issue \u306F\u3042\u308A\u307E\u305B\u3093\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
3255
3523
  lines.push(
3256
- ...formatMarkdownTable(["Severity", "Code", "Count"], issueSummaryRows)
3524
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
3257
3525
  );
3258
3526
  }
3259
3527
  lines.push("");
3260
- lines.push("### Issues (list)");
3528
+ lines.push("### Index");
3261
3529
  lines.push("");
3262
- if (data.issues.length === 0) {
3263
- lines.push("- (none)");
3264
- } else {
3265
- const sortedIssues = [...data.issues].sort((a, b) => {
3530
+ lines.push("- [Compatibility Issues](#compatibility-issues)");
3531
+ lines.push("- [Change Issues](#change-issues)");
3532
+ lines.push("- [IDs](#ids)");
3533
+ lines.push("- [Traceability](#traceability)");
3534
+ lines.push("");
3535
+ const formatIssueSummaryTable = (issues) => {
3536
+ const issueKeyToCount = /* @__PURE__ */ new Map();
3537
+ for (const issue7 of issues) {
3538
+ const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
3539
+ const current = issueKeyToCount.get(key);
3540
+ if (current) {
3541
+ current.count += 1;
3542
+ continue;
3543
+ }
3544
+ issueKeyToCount.set(key, {
3545
+ category: issue7.category,
3546
+ severity: issue7.severity,
3547
+ code: issue7.code,
3548
+ count: 1
3549
+ });
3550
+ }
3551
+ const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
3552
+ const ca = categoryOrder[a.category] ?? 999;
3553
+ const cb = categoryOrder[b.category] ?? 999;
3554
+ if (ca !== cb) return ca - cb;
3555
+ const sa = severityOrder[a.severity] ?? 999;
3556
+ const sb = severityOrder[b.severity] ?? 999;
3557
+ if (sa !== sb) return sa - sb;
3558
+ return a.code.localeCompare(b.code);
3559
+ }).map((x) => [x.severity, x.code, String(x.count)]);
3560
+ return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
3561
+ };
3562
+ const formatIssueCards = (issues) => {
3563
+ const sorted = [...issues].sort((a, b) => {
3266
3564
  const sa = severityOrder[a.severity] ?? 999;
3267
3565
  const sb = severityOrder[b.severity] ?? 999;
3268
3566
  if (sa !== sb) return sa - sb;
@@ -3276,16 +3574,54 @@ function formatReportMarkdown(data) {
3276
3574
  const lineB = b.loc?.line ?? 0;
3277
3575
  return lineA - lineB;
3278
3576
  });
3279
- for (const item of sortedIssues) {
3280
- const location = item.file ? ` (${item.file})` : "";
3281
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
3282
- lines.push(
3283
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
3577
+ if (sorted.length === 0) {
3578
+ return ["- (none)"];
3579
+ }
3580
+ const out = [];
3581
+ for (const item of sorted) {
3582
+ out.push(
3583
+ `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
3284
3584
  );
3585
+ if (item.file) {
3586
+ const loc = item.loc?.line ? `:${item.loc.line}` : "";
3587
+ out.push(`- file: ${item.file}${loc}`);
3588
+ }
3589
+ if (item.rule) {
3590
+ out.push(`- rule: ${item.rule}`);
3591
+ }
3592
+ if (item.refs && item.refs.length > 0) {
3593
+ out.push(`- refs: ${item.refs.join(", ")}`);
3594
+ }
3595
+ if (item.suggested_action) {
3596
+ out.push("- suggested_action:");
3597
+ const actionLines = String(item.suggested_action).split("\n");
3598
+ for (const line of actionLines) {
3599
+ out.push(` ${line}`);
3600
+ }
3601
+ }
3602
+ out.push("");
3285
3603
  }
3286
- }
3604
+ return out;
3605
+ };
3606
+ lines.push("## Compatibility Issues");
3607
+ lines.push("");
3608
+ lines.push("### Summary");
3609
+ lines.push("");
3610
+ lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
3611
+ lines.push("");
3612
+ lines.push("### Issues");
3613
+ lines.push("");
3614
+ lines.push(...formatIssueCards(issuesByCategory.compatibility));
3615
+ lines.push("## Change Issues");
3287
3616
  lines.push("");
3288
- lines.push("### IDs");
3617
+ lines.push("### Summary");
3618
+ lines.push("");
3619
+ lines.push(...formatIssueSummaryTable(issuesByCategory.change));
3620
+ lines.push("");
3621
+ lines.push("### Issues");
3622
+ lines.push("");
3623
+ lines.push(...formatIssueCards(issuesByCategory.change));
3624
+ lines.push("## IDs");
3289
3625
  lines.push("");
3290
3626
  lines.push(formatIdLine("SPEC", data.ids.spec));
3291
3627
  lines.push(formatIdLine("BR", data.ids.br));
@@ -3294,7 +3630,7 @@ function formatReportMarkdown(data) {
3294
3630
  lines.push(formatIdLine("API", data.ids.api));
3295
3631
  lines.push(formatIdLine("DB", data.ids.db));
3296
3632
  lines.push("");
3297
- lines.push("### Traceability");
3633
+ lines.push("## Traceability");
3298
3634
  lines.push("");
3299
3635
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
3300
3636
  lines.push(
@@ -3468,7 +3804,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3468
3804
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3469
3805
  }
3470
3806
  for (const file of specFiles) {
3471
- const text = await (0, import_promises17.readFile)(file, "utf-8");
3807
+ const text = await (0, import_promises18.readFile)(file, "utf-8");
3472
3808
  const parsed = parseSpec(text, file);
3473
3809
  const specKey = parsed.specId;
3474
3810
  if (!specKey) {
@@ -3509,7 +3845,7 @@ async function collectIds(files) {
3509
3845
  DB: /* @__PURE__ */ new Set()
3510
3846
  };
3511
3847
  for (const file of files) {
3512
- const text = await (0, import_promises17.readFile)(file, "utf-8");
3848
+ const text = await (0, import_promises18.readFile)(file, "utf-8");
3513
3849
  for (const prefix of ID_PREFIXES2) {
3514
3850
  const ids = extractIds(text, prefix);
3515
3851
  ids.forEach((id) => result[prefix].add(id));
@@ -3527,7 +3863,7 @@ async function collectIds(files) {
3527
3863
  async function collectUpstreamIds(files) {
3528
3864
  const ids = /* @__PURE__ */ new Set();
3529
3865
  for (const file of files) {
3530
- const text = await (0, import_promises17.readFile)(file, "utf-8");
3866
+ const text = await (0, import_promises18.readFile)(file, "utf-8");
3531
3867
  extractAllIds(text).forEach((id) => ids.add(id));
3532
3868
  }
3533
3869
  return ids;
@@ -3548,7 +3884,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3548
3884
  }
3549
3885
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3550
3886
  for (const file of targetFiles) {
3551
- const text = await (0, import_promises17.readFile)(file, "utf-8");
3887
+ const text = await (0, import_promises18.readFile)(file, "utf-8");
3552
3888
  if (pattern.test(text)) {
3553
3889
  return true;
3554
3890
  }
@@ -3640,7 +3976,7 @@ function buildHotspots(issues) {
3640
3976
 
3641
3977
  // src/cli/commands/report.ts
3642
3978
  async function runReport(options) {
3643
- const root = import_node_path18.default.resolve(options.root);
3979
+ const root = import_node_path19.default.resolve(options.root);
3644
3980
  const configResult = await loadConfig(root);
3645
3981
  let validation;
3646
3982
  if (options.runValidate) {
@@ -3657,7 +3993,7 @@ async function runReport(options) {
3657
3993
  validation = normalized;
3658
3994
  } else {
3659
3995
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3660
- const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
3996
+ const inputPath = import_node_path19.default.isAbsolute(input) ? input : import_node_path19.default.resolve(root, input);
3661
3997
  try {
3662
3998
  validation = await readValidationResult(inputPath);
3663
3999
  } catch (err) {
@@ -3683,11 +4019,11 @@ async function runReport(options) {
3683
4019
  const data = await createReportData(root, validation, configResult);
3684
4020
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3685
4021
  const outRoot = resolvePath(root, configResult.config, "outDir");
3686
- const defaultOut = options.format === "json" ? import_node_path18.default.join(outRoot, "report.json") : import_node_path18.default.join(outRoot, "report.md");
4022
+ const defaultOut = options.format === "json" ? import_node_path19.default.join(outRoot, "report.json") : import_node_path19.default.join(outRoot, "report.md");
3687
4023
  const out = options.outPath ?? defaultOut;
3688
- const outPath = import_node_path18.default.isAbsolute(out) ? out : import_node_path18.default.resolve(root, out);
3689
- await (0, import_promises18.mkdir)(import_node_path18.default.dirname(outPath), { recursive: true });
3690
- await (0, import_promises18.writeFile)(outPath, `${output}
4024
+ const outPath = import_node_path19.default.isAbsolute(out) ? out : import_node_path19.default.resolve(root, out);
4025
+ await (0, import_promises19.mkdir)(import_node_path19.default.dirname(outPath), { recursive: true });
4026
+ await (0, import_promises19.writeFile)(outPath, `${output}
3691
4027
  `, "utf-8");
3692
4028
  info(
3693
4029
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3695,7 +4031,7 @@ async function runReport(options) {
3695
4031
  info(`wrote report: ${outPath}`);
3696
4032
  }
3697
4033
  async function readValidationResult(inputPath) {
3698
- const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
4034
+ const raw = await (0, import_promises19.readFile)(inputPath, "utf-8");
3699
4035
  const parsed = JSON.parse(raw);
3700
4036
  if (!isValidationResult(parsed)) {
3701
4037
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3751,15 +4087,15 @@ function isMissingFileError5(error2) {
3751
4087
  return record2.code === "ENOENT";
3752
4088
  }
3753
4089
  async function writeValidationResult(root, outputPath, result) {
3754
- const abs = import_node_path18.default.isAbsolute(outputPath) ? outputPath : import_node_path18.default.resolve(root, outputPath);
3755
- await (0, import_promises18.mkdir)(import_node_path18.default.dirname(abs), { recursive: true });
3756
- await (0, import_promises18.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4090
+ const abs = import_node_path19.default.isAbsolute(outputPath) ? outputPath : import_node_path19.default.resolve(root, outputPath);
4091
+ await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
4092
+ await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3757
4093
  `, "utf-8");
3758
4094
  }
3759
4095
 
3760
4096
  // src/cli/commands/validate.ts
3761
- var import_promises19 = require("fs/promises");
3762
- var import_node_path19 = __toESM(require("path"), 1);
4097
+ var import_promises20 = require("fs/promises");
4098
+ var import_node_path20 = __toESM(require("path"), 1);
3763
4099
 
3764
4100
  // src/cli/lib/failOn.ts
3765
4101
  function shouldFail(result, failOn) {
@@ -3774,7 +4110,7 @@ function shouldFail(result, failOn) {
3774
4110
 
3775
4111
  // src/cli/commands/validate.ts
3776
4112
  async function runValidate(options) {
3777
- const root = import_node_path19.default.resolve(options.root);
4113
+ const root = import_node_path20.default.resolve(options.root);
3778
4114
  const configResult = await loadConfig(root);
3779
4115
  const result = await validateProject(root, configResult);
3780
4116
  const normalized = normalizeValidationResult(root, result);
@@ -3898,12 +4234,12 @@ function issueKey(issue7) {
3898
4234
  }
3899
4235
  async function emitJson(result, root, jsonPath) {
3900
4236
  const abs = resolveJsonPath(root, jsonPath);
3901
- await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
3902
- await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
4237
+ await (0, import_promises20.mkdir)(import_node_path20.default.dirname(abs), { recursive: true });
4238
+ await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3903
4239
  `, "utf-8");
3904
4240
  }
3905
4241
  function resolveJsonPath(root, jsonPath) {
3906
- return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
4242
+ return import_node_path20.default.isAbsolute(jsonPath) ? jsonPath : import_node_path20.default.resolve(root, jsonPath);
3907
4243
  }
3908
4244
  var GITHUB_ANNOTATION_LIMIT = 100;
3909
4245
 
@@ -4101,7 +4437,7 @@ Commands:
4101
4437
  Options:
4102
4438
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
4103
4439
  --dir <path> init \u306E\u51FA\u529B\u5148
4104
- --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
4440
+ --force init: .qfai/prompts \u306E\u307F\u4E0A\u66F8\u304D\uFF08\u305D\u308C\u4EE5\u5916\u306F\u65E2\u5B58\u304C\u3042\u308C\u3070\u30B9\u30AD\u30C3\u30D7\uFF09
4105
4441
  --yes init: \u4E88\u7D04\u30D5\u30E9\u30B0\uFF08\u73FE\u72B6\u306F\u975E\u5BFE\u8A71\u306E\u305F\u3081\u6319\u52D5\u5DEE\u306A\u3057\u3002\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u306B\u81EA\u52D5Yes\uFF09
4106
4442
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
4107
4443
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F