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.
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli/commands/doctor.ts
4
4
  import { mkdir, writeFile } from "fs/promises";
5
- import path8 from "path";
5
+ import path10 from "path";
6
6
 
7
7
  // src/core/doctor.ts
8
8
  import { access as access4 } from "fs/promises";
9
- import path7 from "path";
9
+ import path9 from "path";
10
10
 
11
11
  // src/core/config.ts
12
12
  import { access, readFile } from "fs/promises";
@@ -378,6 +378,7 @@ function configIssue(file, message) {
378
378
  return {
379
379
  code: "QFAI_CONFIG_INVALID",
380
380
  severity: "error",
381
+ category: "compatibility",
381
382
  message,
382
383
  file,
383
384
  rule: "config.invalid"
@@ -865,17 +866,142 @@ function formatError3(error2) {
865
866
  return String(error2);
866
867
  }
867
868
 
868
- // src/core/version.ts
869
+ // src/core/promptsIntegrity.ts
869
870
  import { readFile as readFile3 } from "fs/promises";
871
+ import path7 from "path";
872
+
873
+ // src/shared/assets.ts
874
+ import { existsSync } from "fs";
870
875
  import path6 from "path";
871
876
  import { fileURLToPath } from "url";
877
+ function getInitAssetsDir() {
878
+ const base = import.meta.url;
879
+ const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
880
+ const baseDir = path6.dirname(basePath);
881
+ const candidates = [
882
+ path6.resolve(baseDir, "../../../assets/init"),
883
+ path6.resolve(baseDir, "../../assets/init")
884
+ ];
885
+ for (const candidate of candidates) {
886
+ if (existsSync(candidate)) {
887
+ return candidate;
888
+ }
889
+ }
890
+ throw new Error(
891
+ [
892
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
893
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
894
+ ...candidates.map((candidate) => `- ${candidate}`)
895
+ ].join("\n")
896
+ );
897
+ }
898
+
899
+ // src/core/promptsIntegrity.ts
900
+ async function diffProjectPromptsAgainstInitAssets(root) {
901
+ const promptsDir = path7.resolve(root, ".qfai", "prompts");
902
+ let templateDir;
903
+ try {
904
+ templateDir = path7.join(getInitAssetsDir(), ".qfai", "prompts");
905
+ } catch {
906
+ return {
907
+ status: "skipped_missing_assets",
908
+ promptsDir,
909
+ templateDir: "",
910
+ missing: [],
911
+ extra: [],
912
+ changed: []
913
+ };
914
+ }
915
+ const projectFiles = await collectFiles(promptsDir);
916
+ if (projectFiles.length === 0) {
917
+ return {
918
+ status: "skipped_missing_prompts",
919
+ promptsDir,
920
+ templateDir,
921
+ missing: [],
922
+ extra: [],
923
+ changed: []
924
+ };
925
+ }
926
+ const templateFiles = await collectFiles(templateDir);
927
+ const templateByRel = /* @__PURE__ */ new Map();
928
+ for (const abs of templateFiles) {
929
+ templateByRel.set(toRel(templateDir, abs), abs);
930
+ }
931
+ const projectByRel = /* @__PURE__ */ new Map();
932
+ for (const abs of projectFiles) {
933
+ projectByRel.set(toRel(promptsDir, abs), abs);
934
+ }
935
+ const missing = [];
936
+ const extra = [];
937
+ const changed = [];
938
+ for (const rel of templateByRel.keys()) {
939
+ if (!projectByRel.has(rel)) {
940
+ missing.push(rel);
941
+ }
942
+ }
943
+ for (const rel of projectByRel.keys()) {
944
+ if (!templateByRel.has(rel)) {
945
+ extra.push(rel);
946
+ }
947
+ }
948
+ const common = intersectKeys(templateByRel, projectByRel);
949
+ for (const rel of common) {
950
+ const templateAbs = templateByRel.get(rel);
951
+ const projectAbs = projectByRel.get(rel);
952
+ if (!templateAbs || !projectAbs) {
953
+ continue;
954
+ }
955
+ try {
956
+ const [a, b] = await Promise.all([
957
+ readFile3(templateAbs, "utf-8"),
958
+ readFile3(projectAbs, "utf-8")
959
+ ]);
960
+ if (normalizeNewlines(a) !== normalizeNewlines(b)) {
961
+ changed.push(rel);
962
+ }
963
+ } catch {
964
+ changed.push(rel);
965
+ }
966
+ }
967
+ const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
968
+ return {
969
+ status,
970
+ promptsDir,
971
+ templateDir,
972
+ missing: missing.sort(),
973
+ extra: extra.sort(),
974
+ changed: changed.sort()
975
+ };
976
+ }
977
+ function normalizeNewlines(text) {
978
+ return text.replace(/\r\n/g, "\n");
979
+ }
980
+ function toRel(base, abs) {
981
+ const rel = path7.relative(base, abs);
982
+ return rel.replace(/[\\/]+/g, "/");
983
+ }
984
+ function intersectKeys(a, b) {
985
+ const out = [];
986
+ for (const key of a.keys()) {
987
+ if (b.has(key)) {
988
+ out.push(key);
989
+ }
990
+ }
991
+ return out;
992
+ }
993
+
994
+ // src/core/version.ts
995
+ import { readFile as readFile4 } from "fs/promises";
996
+ import path8 from "path";
997
+ import { fileURLToPath as fileURLToPath2 } from "url";
872
998
  async function resolveToolVersion() {
873
- if ("0.8.0".length > 0) {
874
- return "0.8.0";
999
+ if ("0.9.0".length > 0) {
1000
+ return "0.9.0";
875
1001
  }
876
1002
  try {
877
1003
  const packagePath = resolvePackageJsonPath();
878
- const raw = await readFile3(packagePath, "utf-8");
1004
+ const raw = await readFile4(packagePath, "utf-8");
879
1005
  const parsed = JSON.parse(raw);
880
1006
  const version = typeof parsed.version === "string" ? parsed.version : "";
881
1007
  return version.length > 0 ? version : "unknown";
@@ -885,8 +1011,8 @@ async function resolveToolVersion() {
885
1011
  }
886
1012
  function resolvePackageJsonPath() {
887
1013
  const base = import.meta.url;
888
- const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
889
- return path6.resolve(path6.dirname(basePath), "../../package.json");
1014
+ const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1015
+ return path8.resolve(path8.dirname(basePath), "../../package.json");
890
1016
  }
891
1017
 
892
1018
  // src/core/doctor.ts
@@ -912,7 +1038,7 @@ function normalizeGlobs2(values) {
912
1038
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
913
1039
  }
914
1040
  async function createDoctorData(options) {
915
- const startDir = path7.resolve(options.startDir);
1041
+ const startDir = path9.resolve(options.startDir);
916
1042
  const checks = [];
917
1043
  const configPath = getConfigPath(startDir);
918
1044
  const search = options.rootExplicit ? {
@@ -975,9 +1101,9 @@ async function createDoctorData(options) {
975
1101
  details: { path: toRelativePath(root, resolved) }
976
1102
  });
977
1103
  if (key === "promptsDir") {
978
- const promptsLocalDir = path7.join(
979
- path7.dirname(resolved),
980
- `${path7.basename(resolved)}.local`
1104
+ const promptsLocalDir = path9.join(
1105
+ path9.dirname(resolved),
1106
+ `${path9.basename(resolved)}.local`
981
1107
  );
982
1108
  const found = await exists4(promptsLocalDir);
983
1109
  addCheck(checks, {
@@ -987,6 +1113,49 @@ async function createDoctorData(options) {
987
1113
  message: found ? "prompts.local exists (overlay can be used)" : "prompts.local is optional (create it to override prompts)",
988
1114
  details: { path: toRelativePath(root, promptsLocalDir) }
989
1115
  });
1116
+ const diff = await diffProjectPromptsAgainstInitAssets(root);
1117
+ if (diff.status === "skipped_missing_prompts") {
1118
+ addCheck(checks, {
1119
+ id: "prompts.integrity",
1120
+ severity: "info",
1121
+ title: "Prompts integrity (.qfai/prompts)",
1122
+ 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",
1123
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1124
+ });
1125
+ } else if (diff.status === "skipped_missing_assets") {
1126
+ addCheck(checks, {
1127
+ id: "prompts.integrity",
1128
+ severity: "info",
1129
+ title: "Prompts integrity (.qfai/prompts)",
1130
+ 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",
1131
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1132
+ });
1133
+ } else if (diff.status === "ok") {
1134
+ addCheck(checks, {
1135
+ id: "prompts.integrity",
1136
+ severity: "ok",
1137
+ title: "Prompts integrity (.qfai/prompts)",
1138
+ message: "\u6A19\u6E96 assets \u3068\u4E00\u81F4\u3057\u3066\u3044\u307E\u3059",
1139
+ details: { promptsDir: toRelativePath(root, diff.promptsDir) }
1140
+ });
1141
+ } else {
1142
+ addCheck(checks, {
1143
+ id: "prompts.integrity",
1144
+ severity: "error",
1145
+ title: "Prompts integrity (.qfai/prompts)",
1146
+ 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",
1147
+ details: {
1148
+ promptsDir: toRelativePath(root, diff.promptsDir),
1149
+ missing: diff.missing,
1150
+ extra: diff.extra,
1151
+ changed: diff.changed,
1152
+ nextActions: [
1153
+ "\u5909\u66F4\u5185\u5BB9\u3092 .qfai/prompts.local/** \u306B\u79FB\u3059\uFF08\u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067\u914D\u7F6E\uFF09",
1154
+ "\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"
1155
+ ]
1156
+ }
1157
+ });
1158
+ }
990
1159
  }
991
1160
  }
992
1161
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -1007,7 +1176,7 @@ async function createDoctorData(options) {
1007
1176
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1008
1177
  details: { specPacks: entries.length, missingFiles }
1009
1178
  });
1010
- const validateJsonAbs = path7.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path7.resolve(root, config.output.validateJsonPath);
1179
+ const validateJsonAbs = path9.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path9.resolve(root, config.output.validateJsonPath);
1011
1180
  const validateJsonExists = await exists4(validateJsonAbs);
1012
1181
  addCheck(checks, {
1013
1182
  id: "output.validateJson",
@@ -1017,8 +1186,8 @@ async function createDoctorData(options) {
1017
1186
  details: { path: toRelativePath(root, validateJsonAbs) }
1018
1187
  });
1019
1188
  const outDirAbs = resolvePath(root, config, "outDir");
1020
- const rel = path7.relative(outDirAbs, validateJsonAbs);
1021
- const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1189
+ const rel = path9.relative(outDirAbs, validateJsonAbs);
1190
+ const inside = rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
1022
1191
  addCheck(checks, {
1023
1192
  id: "output.pathAlignment",
1024
1193
  severity: inside ? "ok" : "warning",
@@ -1124,12 +1293,12 @@ async function detectOutDirCollisions(root) {
1124
1293
  ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1125
1294
  });
1126
1295
  const configRoots = Array.from(
1127
- new Set(configPaths.map((configPath) => path7.dirname(configPath)))
1296
+ new Set(configPaths.map((configPath) => path9.dirname(configPath)))
1128
1297
  ).sort((a, b) => a.localeCompare(b));
1129
1298
  const outDirToRoots = /* @__PURE__ */ new Map();
1130
1299
  for (const configRoot of configRoots) {
1131
1300
  const { config } = await loadConfig(configRoot);
1132
- const outDir = path7.normalize(resolvePath(configRoot, config, "outDir"));
1301
+ const outDir = path9.normalize(resolvePath(configRoot, config, "outDir"));
1133
1302
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1134
1303
  roots.add(configRoot);
1135
1304
  outDirToRoots.set(outDir, roots);
@@ -1146,20 +1315,20 @@ async function detectOutDirCollisions(root) {
1146
1315
  return { monorepoRoot, configRoots, collisions };
1147
1316
  }
1148
1317
  async function findMonorepoRoot(startDir) {
1149
- let current = path7.resolve(startDir);
1318
+ let current = path9.resolve(startDir);
1150
1319
  while (true) {
1151
- const gitPath = path7.join(current, ".git");
1152
- const workspacePath = path7.join(current, "pnpm-workspace.yaml");
1320
+ const gitPath = path9.join(current, ".git");
1321
+ const workspacePath = path9.join(current, "pnpm-workspace.yaml");
1153
1322
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1154
1323
  return current;
1155
1324
  }
1156
- const parent = path7.dirname(current);
1325
+ const parent = path9.dirname(current);
1157
1326
  if (parent === current) {
1158
1327
  break;
1159
1328
  }
1160
1329
  current = parent;
1161
1330
  }
1162
- return path7.resolve(startDir);
1331
+ return path9.resolve(startDir);
1163
1332
  }
1164
1333
 
1165
1334
  // src/cli/lib/logger.ts
@@ -1201,8 +1370,8 @@ async function runDoctor(options) {
1201
1370
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1202
1371
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1203
1372
  if (options.outPath) {
1204
- const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
1205
- await mkdir(path8.dirname(outAbs), { recursive: true });
1373
+ const outAbs = path10.isAbsolute(options.outPath) ? options.outPath : path10.resolve(process.cwd(), options.outPath);
1374
+ await mkdir(path10.dirname(outAbs), { recursive: true });
1206
1375
  await writeFile(outAbs, `${output}
1207
1376
  `, "utf-8");
1208
1377
  info(`doctor: wrote ${outAbs}`);
@@ -1222,36 +1391,59 @@ function shouldFailDoctor(summary, failOn) {
1222
1391
  }
1223
1392
 
1224
1393
  // src/cli/commands/init.ts
1225
- import path11 from "path";
1394
+ import path12 from "path";
1226
1395
 
1227
1396
  // src/cli/lib/fs.ts
1228
1397
  import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1229
- import path9 from "path";
1398
+ import path11 from "path";
1230
1399
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1231
1400
  const files = await collectTemplateFiles(sourceRoot);
1232
1401
  return copyFiles(files, sourceRoot, destRoot, options);
1233
1402
  }
1403
+ async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1404
+ const allFiles = [];
1405
+ for (const relPath of relativePaths) {
1406
+ const fullPath = path11.join(sourceRoot, relPath);
1407
+ const files = await collectTemplateFiles(fullPath);
1408
+ allFiles.push(...files);
1409
+ }
1410
+ return copyFiles(allFiles, sourceRoot, destRoot, options);
1411
+ }
1234
1412
  async function copyFiles(files, sourceRoot, destRoot, options) {
1235
1413
  const copied = [];
1236
1414
  const skipped = [];
1237
1415
  const conflicts = [];
1238
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path9.sep);
1416
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path11.sep);
1417
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path11.sep);
1239
1418
  const isProtectedRelative = (relative) => {
1240
1419
  if (protectPrefixes.length === 0) {
1241
1420
  return false;
1242
1421
  }
1243
- const normalized = relative.replace(/[\\/]+/g, path9.sep);
1422
+ const normalized = relative.replace(/[\\/]+/g, path11.sep);
1244
1423
  return protectPrefixes.some(
1245
1424
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1246
1425
  );
1247
1426
  };
1248
- if (!options.force) {
1427
+ const isExcludedRelative = (relative) => {
1428
+ if (excludePrefixes.length === 0) {
1429
+ return false;
1430
+ }
1431
+ const normalized = relative.replace(/[\\/]+/g, path11.sep);
1432
+ return excludePrefixes.some(
1433
+ (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1434
+ );
1435
+ };
1436
+ const conflictPolicy = options.conflictPolicy ?? "error";
1437
+ if (!options.force && conflictPolicy === "error") {
1249
1438
  for (const file of files) {
1250
- const relative = path9.relative(sourceRoot, file);
1439
+ const relative = path11.relative(sourceRoot, file);
1440
+ if (isExcludedRelative(relative)) {
1441
+ continue;
1442
+ }
1251
1443
  if (isProtectedRelative(relative)) {
1252
1444
  continue;
1253
1445
  }
1254
- const dest = path9.join(destRoot, relative);
1446
+ const dest = path11.join(destRoot, relative);
1255
1447
  if (!await shouldWrite(dest, options.force)) {
1256
1448
  conflicts.push(dest);
1257
1449
  }
@@ -1261,15 +1453,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1261
1453
  }
1262
1454
  }
1263
1455
  for (const file of files) {
1264
- const relative = path9.relative(sourceRoot, file);
1265
- const dest = path9.join(destRoot, relative);
1456
+ const relative = path11.relative(sourceRoot, file);
1457
+ if (isExcludedRelative(relative)) {
1458
+ continue;
1459
+ }
1460
+ const dest = path11.join(destRoot, relative);
1266
1461
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1267
1462
  if (!await shouldWrite(dest, forceForThisFile)) {
1268
1463
  skipped.push(dest);
1269
1464
  continue;
1270
1465
  }
1271
1466
  if (!options.dryRun) {
1272
- await mkdir2(path9.dirname(dest), { recursive: true });
1467
+ await mkdir2(path11.dirname(dest), { recursive: true });
1273
1468
  await copyFile(file, dest);
1274
1469
  }
1275
1470
  copied.push(dest);
@@ -1293,7 +1488,7 @@ async function collectTemplateFiles(root) {
1293
1488
  }
1294
1489
  const items = await readdir3(root, { withFileTypes: true });
1295
1490
  for (const item of items) {
1296
- const fullPath = path9.join(root, item.name);
1491
+ const fullPath = path11.join(root, item.name);
1297
1492
  if (item.isDirectory()) {
1298
1493
  const nested = await collectTemplateFiles(fullPath);
1299
1494
  entries.push(...nested);
@@ -1320,51 +1515,44 @@ async function exists5(target) {
1320
1515
  }
1321
1516
  }
1322
1517
 
1323
- // src/cli/lib/assets.ts
1324
- import { existsSync } from "fs";
1325
- import path10 from "path";
1326
- import { fileURLToPath as fileURLToPath2 } from "url";
1327
- function getInitAssetsDir() {
1328
- const base = import.meta.url;
1329
- const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1330
- const baseDir = path10.dirname(basePath);
1331
- const candidates = [
1332
- path10.resolve(baseDir, "../../../assets/init"),
1333
- path10.resolve(baseDir, "../../assets/init")
1334
- ];
1335
- for (const candidate of candidates) {
1336
- if (existsSync(candidate)) {
1337
- return candidate;
1338
- }
1339
- }
1340
- throw new Error(
1341
- [
1342
- "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1343
- "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1344
- ...candidates.map((candidate) => `- ${candidate}`)
1345
- ].join("\n")
1346
- );
1347
- }
1348
-
1349
1518
  // src/cli/commands/init.ts
1350
1519
  async function runInit(options) {
1351
1520
  const assetsRoot = getInitAssetsDir();
1352
- const rootAssets = path11.join(assetsRoot, "root");
1353
- const qfaiAssets = path11.join(assetsRoot, ".qfai");
1354
- const destRoot = path11.resolve(options.dir);
1355
- const destQfai = path11.join(destRoot, ".qfai");
1521
+ const rootAssets = path12.join(assetsRoot, "root");
1522
+ const qfaiAssets = path12.join(assetsRoot, ".qfai");
1523
+ const destRoot = path12.resolve(options.dir);
1524
+ const destQfai = path12.join(destRoot, ".qfai");
1525
+ if (options.force) {
1526
+ info(
1527
+ "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"
1528
+ );
1529
+ }
1356
1530
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1357
- force: options.force,
1358
- dryRun: options.dryRun
1531
+ force: false,
1532
+ dryRun: options.dryRun,
1533
+ conflictPolicy: "skip"
1359
1534
  });
1360
1535
  const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1361
- force: options.force,
1536
+ force: false,
1362
1537
  dryRun: options.dryRun,
1363
- protect: ["prompts.local"]
1538
+ conflictPolicy: "skip",
1539
+ protect: ["prompts.local"],
1540
+ exclude: ["prompts"]
1364
1541
  });
1542
+ const promptsResult = await copyTemplatePaths(
1543
+ qfaiAssets,
1544
+ destQfai,
1545
+ ["prompts"],
1546
+ {
1547
+ force: options.force,
1548
+ dryRun: options.dryRun,
1549
+ conflictPolicy: "skip",
1550
+ protect: ["prompts.local"]
1551
+ }
1552
+ );
1365
1553
  report(
1366
- [...rootResult.copied, ...qfaiResult.copied],
1367
- [...rootResult.skipped, ...qfaiResult.skipped],
1554
+ [...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
1555
+ [...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
1368
1556
  options.dryRun,
1369
1557
  "init"
1370
1558
  );
@@ -1380,8 +1568,8 @@ function report(copied, skipped, dryRun, label) {
1380
1568
  }
1381
1569
 
1382
1570
  // src/cli/commands/report.ts
1383
- import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
1384
- import path18 from "path";
1571
+ import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
1572
+ import path19 from "path";
1385
1573
 
1386
1574
  // src/core/normalize.ts
1387
1575
  function normalizeIssuePaths(root, issues) {
@@ -1421,12 +1609,12 @@ function normalizeValidationResult(root, result) {
1421
1609
  }
1422
1610
 
1423
1611
  // src/core/report.ts
1424
- import { readFile as readFile11 } from "fs/promises";
1425
- import path17 from "path";
1612
+ import { readFile as readFile12 } from "fs/promises";
1613
+ import path18 from "path";
1426
1614
 
1427
1615
  // src/core/contractIndex.ts
1428
- import { readFile as readFile4 } from "fs/promises";
1429
- import path12 from "path";
1616
+ import { readFile as readFile5 } from "fs/promises";
1617
+ import path13 from "path";
1430
1618
 
1431
1619
  // src/core/contractsDecl.ts
1432
1620
  var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
@@ -1448,9 +1636,9 @@ function stripContractDeclarationLines(text) {
1448
1636
  // src/core/contractIndex.ts
1449
1637
  async function buildContractIndex(root, config) {
1450
1638
  const contractsRoot = resolvePath(root, config, "contractsDir");
1451
- const uiRoot = path12.join(contractsRoot, "ui");
1452
- const apiRoot = path12.join(contractsRoot, "api");
1453
- const dbRoot = path12.join(contractsRoot, "db");
1639
+ const uiRoot = path13.join(contractsRoot, "ui");
1640
+ const apiRoot = path13.join(contractsRoot, "api");
1641
+ const dbRoot = path13.join(contractsRoot, "db");
1454
1642
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1455
1643
  collectUiContractFiles(uiRoot),
1456
1644
  collectApiContractFiles(apiRoot),
@@ -1468,7 +1656,7 @@ async function buildContractIndex(root, config) {
1468
1656
  }
1469
1657
  async function indexContractFiles(files, index) {
1470
1658
  for (const file of files) {
1471
- const text = await readFile4(file, "utf-8");
1659
+ const text = await readFile5(file, "utf-8");
1472
1660
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1473
1661
  }
1474
1662
  }
@@ -1703,14 +1891,14 @@ function parseSpec(md, file) {
1703
1891
  }
1704
1892
 
1705
1893
  // src/core/validators/contracts.ts
1706
- import { readFile as readFile5 } from "fs/promises";
1707
- import path14 from "path";
1894
+ import { readFile as readFile6 } from "fs/promises";
1895
+ import path15 from "path";
1708
1896
 
1709
1897
  // src/core/contracts.ts
1710
- import path13 from "path";
1898
+ import path14 from "path";
1711
1899
  import { parse as parseYaml2 } from "yaml";
1712
1900
  function parseStructuredContract(file, text) {
1713
- const ext = path13.extname(file).toLowerCase();
1901
+ const ext = path14.extname(file).toLowerCase();
1714
1902
  if (ext === ".json") {
1715
1903
  return JSON.parse(text);
1716
1904
  }
@@ -1730,9 +1918,9 @@ var SQL_DANGEROUS_PATTERNS = [
1730
1918
  async function validateContracts(root, config) {
1731
1919
  const issues = [];
1732
1920
  const contractsRoot = resolvePath(root, config, "contractsDir");
1733
- issues.push(...await validateUiContracts(path14.join(contractsRoot, "ui")));
1734
- issues.push(...await validateApiContracts(path14.join(contractsRoot, "api")));
1735
- issues.push(...await validateDbContracts(path14.join(contractsRoot, "db")));
1921
+ issues.push(...await validateUiContracts(path15.join(contractsRoot, "ui")));
1922
+ issues.push(...await validateApiContracts(path15.join(contractsRoot, "api")));
1923
+ issues.push(...await validateDbContracts(path15.join(contractsRoot, "db")));
1736
1924
  const contractIndex = await buildContractIndex(root, config);
1737
1925
  issues.push(...validateDuplicateContractIds(contractIndex));
1738
1926
  return issues;
@@ -1752,7 +1940,7 @@ async function validateUiContracts(uiRoot) {
1752
1940
  }
1753
1941
  const issues = [];
1754
1942
  for (const file of files) {
1755
- const text = await readFile5(file, "utf-8");
1943
+ const text = await readFile6(file, "utf-8");
1756
1944
  const invalidIds = extractInvalidIds(text, [
1757
1945
  "SPEC",
1758
1946
  "BR",
@@ -1807,7 +1995,7 @@ async function validateApiContracts(apiRoot) {
1807
1995
  }
1808
1996
  const issues = [];
1809
1997
  for (const file of files) {
1810
- const text = await readFile5(file, "utf-8");
1998
+ const text = await readFile6(file, "utf-8");
1811
1999
  const invalidIds = extractInvalidIds(text, [
1812
2000
  "SPEC",
1813
2001
  "BR",
@@ -1875,7 +2063,7 @@ async function validateDbContracts(dbRoot) {
1875
2063
  }
1876
2064
  const issues = [];
1877
2065
  for (const file of files) {
1878
- const text = await readFile5(file, "utf-8");
2066
+ const text = await readFile6(file, "utf-8");
1879
2067
  const invalidIds = extractInvalidIds(text, [
1880
2068
  "SPEC",
1881
2069
  "BR",
@@ -1995,12 +2183,16 @@ function formatError4(error2) {
1995
2183
  }
1996
2184
  return String(error2);
1997
2185
  }
1998
- function issue(code, message, severity, file, rule, refs) {
2186
+ function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
1999
2187
  const issue7 = {
2000
2188
  code,
2001
2189
  severity,
2190
+ category,
2002
2191
  message
2003
2192
  };
2193
+ if (suggested_action) {
2194
+ issue7.suggested_action = suggested_action;
2195
+ }
2004
2196
  if (file) {
2005
2197
  issue7.file = file;
2006
2198
  }
@@ -2014,8 +2206,8 @@ function issue(code, message, severity, file, rule, refs) {
2014
2206
  }
2015
2207
 
2016
2208
  // src/core/validators/delta.ts
2017
- import { readFile as readFile6 } from "fs/promises";
2018
- import path15 from "path";
2209
+ import { readFile as readFile7 } from "fs/promises";
2210
+ import path16 from "path";
2019
2211
  var SECTION_RE = /^##\s+変更区分/m;
2020
2212
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2021
2213
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2029,10 +2221,10 @@ async function validateDeltas(root, config) {
2029
2221
  }
2030
2222
  const issues = [];
2031
2223
  for (const pack of packs) {
2032
- const deltaPath = path15.join(pack, "delta.md");
2224
+ const deltaPath = path16.join(pack, "delta.md");
2033
2225
  let text;
2034
2226
  try {
2035
- text = await readFile6(deltaPath, "utf-8");
2227
+ text = await readFile7(deltaPath, "utf-8");
2036
2228
  } catch (error2) {
2037
2229
  if (isMissingFileError2(error2)) {
2038
2230
  issues.push(
@@ -2085,12 +2277,16 @@ function isMissingFileError2(error2) {
2085
2277
  }
2086
2278
  return error2.code === "ENOENT";
2087
2279
  }
2088
- function issue2(code, message, severity, file, rule, refs) {
2280
+ function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
2089
2281
  const issue7 = {
2090
2282
  code,
2091
2283
  severity,
2284
+ category,
2092
2285
  message
2093
2286
  };
2287
+ if (suggested_action) {
2288
+ issue7.suggested_action = suggested_action;
2289
+ }
2094
2290
  if (file) {
2095
2291
  issue7.file = file;
2096
2292
  }
@@ -2104,8 +2300,8 @@ function issue2(code, message, severity, file, rule, refs) {
2104
2300
  }
2105
2301
 
2106
2302
  // src/core/validators/ids.ts
2107
- import { readFile as readFile7 } from "fs/promises";
2108
- import path16 from "path";
2303
+ import { readFile as readFile8 } from "fs/promises";
2304
+ import path17 from "path";
2109
2305
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2110
2306
  async function validateDefinedIds(root, config) {
2111
2307
  const issues = [];
@@ -2140,7 +2336,7 @@ async function validateDefinedIds(root, config) {
2140
2336
  }
2141
2337
  async function collectSpecDefinitionIds(files, out) {
2142
2338
  for (const file of files) {
2143
- const text = await readFile7(file, "utf-8");
2339
+ const text = await readFile8(file, "utf-8");
2144
2340
  const parsed = parseSpec(text, file);
2145
2341
  if (parsed.specId) {
2146
2342
  recordId(out, parsed.specId, file);
@@ -2150,7 +2346,7 @@ async function collectSpecDefinitionIds(files, out) {
2150
2346
  }
2151
2347
  async function collectScenarioDefinitionIds(files, out) {
2152
2348
  for (const file of files) {
2153
- const text = await readFile7(file, "utf-8");
2349
+ const text = await readFile8(file, "utf-8");
2154
2350
  const { document, errors } = parseScenarioDocument(text, file);
2155
2351
  if (!document || errors.length > 0) {
2156
2352
  continue;
@@ -2171,16 +2367,20 @@ function recordId(out, id, file) {
2171
2367
  }
2172
2368
  function formatFileList(files, root) {
2173
2369
  return files.map((file) => {
2174
- const relative = path16.relative(root, file);
2370
+ const relative = path17.relative(root, file);
2175
2371
  return relative.length > 0 ? relative : file;
2176
2372
  }).join(", ");
2177
2373
  }
2178
- function issue3(code, message, severity, file, rule, refs) {
2374
+ function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2179
2375
  const issue7 = {
2180
2376
  code,
2181
2377
  severity,
2378
+ category,
2182
2379
  message
2183
2380
  };
2381
+ if (suggested_action) {
2382
+ issue7.suggested_action = suggested_action;
2383
+ }
2184
2384
  if (file) {
2185
2385
  issue7.file = file;
2186
2386
  }
@@ -2193,8 +2393,39 @@ function issue3(code, message, severity, file, rule, refs) {
2193
2393
  return issue7;
2194
2394
  }
2195
2395
 
2396
+ // src/core/validators/promptsIntegrity.ts
2397
+ async function validatePromptsIntegrity(root) {
2398
+ const diff = await diffProjectPromptsAgainstInitAssets(root);
2399
+ if (diff.status !== "modified") {
2400
+ return [];
2401
+ }
2402
+ const total = diff.missing.length + diff.extra.length + diff.changed.length;
2403
+ const hints = [
2404
+ diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
2405
+ diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
2406
+ diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
2407
+ ].filter(Boolean).join(" / ");
2408
+ const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
2409
+ const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
2410
+ return [
2411
+ {
2412
+ code: "QFAI-PROMPTS-001",
2413
+ severity: "error",
2414
+ category: "change",
2415
+ message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
2416
+ suggested_action: [
2417
+ "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",
2418
+ "\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
2419
+ "- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
2420
+ "- \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"
2421
+ ].join("\n"),
2422
+ rule: "prompts.integrity"
2423
+ }
2424
+ ];
2425
+ }
2426
+
2196
2427
  // src/core/validators/scenario.ts
2197
- import { readFile as readFile8 } from "fs/promises";
2428
+ import { readFile as readFile9 } from "fs/promises";
2198
2429
  var GIVEN_PATTERN = /\bGiven\b/;
2199
2430
  var WHEN_PATTERN = /\bWhen\b/;
2200
2431
  var THEN_PATTERN = /\bThen\b/;
@@ -2220,7 +2451,7 @@ async function validateScenarios(root, config) {
2220
2451
  for (const entry of entries) {
2221
2452
  let text;
2222
2453
  try {
2223
- text = await readFile8(entry.scenarioPath, "utf-8");
2454
+ text = await readFile9(entry.scenarioPath, "utf-8");
2224
2455
  } catch (error2) {
2225
2456
  if (isMissingFileError3(error2)) {
2226
2457
  issues.push(
@@ -2365,12 +2596,16 @@ function validateScenarioContent(text, file) {
2365
2596
  }
2366
2597
  return issues;
2367
2598
  }
2368
- function issue4(code, message, severity, file, rule, refs) {
2599
+ function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2369
2600
  const issue7 = {
2370
2601
  code,
2371
2602
  severity,
2603
+ category,
2372
2604
  message
2373
2605
  };
2606
+ if (suggested_action) {
2607
+ issue7.suggested_action = suggested_action;
2608
+ }
2374
2609
  if (file) {
2375
2610
  issue7.file = file;
2376
2611
  }
@@ -2390,7 +2625,7 @@ function isMissingFileError3(error2) {
2390
2625
  }
2391
2626
 
2392
2627
  // src/core/validators/spec.ts
2393
- import { readFile as readFile9 } from "fs/promises";
2628
+ import { readFile as readFile10 } from "fs/promises";
2394
2629
  async function validateSpecs(root, config) {
2395
2630
  const specsRoot = resolvePath(root, config, "specsDir");
2396
2631
  const entries = await collectSpecEntries(specsRoot);
@@ -2411,7 +2646,7 @@ async function validateSpecs(root, config) {
2411
2646
  for (const entry of entries) {
2412
2647
  let text;
2413
2648
  try {
2414
- text = await readFile9(entry.specPath, "utf-8");
2649
+ text = await readFile10(entry.specPath, "utf-8");
2415
2650
  } catch (error2) {
2416
2651
  if (isMissingFileError4(error2)) {
2417
2652
  issues.push(
@@ -2535,12 +2770,16 @@ function validateSpecContent(text, file, requiredSections) {
2535
2770
  }
2536
2771
  return issues;
2537
2772
  }
2538
- function issue5(code, message, severity, file, rule, refs) {
2773
+ function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2539
2774
  const issue7 = {
2540
2775
  code,
2541
2776
  severity,
2777
+ category,
2542
2778
  message
2543
2779
  };
2780
+ if (suggested_action) {
2781
+ issue7.suggested_action = suggested_action;
2782
+ }
2544
2783
  if (file) {
2545
2784
  issue7.file = file;
2546
2785
  }
@@ -2560,7 +2799,7 @@ function isMissingFileError4(error2) {
2560
2799
  }
2561
2800
 
2562
2801
  // src/core/validators/traceability.ts
2563
- import { readFile as readFile10 } from "fs/promises";
2802
+ import { readFile as readFile11 } from "fs/promises";
2564
2803
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2565
2804
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2566
2805
  async function validateTraceability(root, config) {
@@ -2580,7 +2819,7 @@ async function validateTraceability(root, config) {
2580
2819
  const contractIndex = await buildContractIndex(root, config);
2581
2820
  const contractIds = contractIndex.ids;
2582
2821
  for (const file of specFiles) {
2583
- const text = await readFile10(file, "utf-8");
2822
+ const text = await readFile11(file, "utf-8");
2584
2823
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2585
2824
  const parsed = parseSpec(text, file);
2586
2825
  if (parsed.specId) {
@@ -2653,7 +2892,7 @@ async function validateTraceability(root, config) {
2653
2892
  }
2654
2893
  }
2655
2894
  for (const file of scenarioFiles) {
2656
- const text = await readFile10(file, "utf-8");
2895
+ const text = await readFile11(file, "utf-8");
2657
2896
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2658
2897
  const scenarioContractRefs = parseContractRefs(text, {
2659
2898
  allowCommentPrefix: true
@@ -2975,7 +3214,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2975
3214
  const pattern = buildIdPattern(Array.from(upstreamIds));
2976
3215
  let found = false;
2977
3216
  for (const file of targetFiles) {
2978
- const text = await readFile10(file, "utf-8");
3217
+ const text = await readFile11(file, "utf-8");
2979
3218
  if (pattern.test(text)) {
2980
3219
  found = true;
2981
3220
  break;
@@ -2998,12 +3237,16 @@ function buildIdPattern(ids) {
2998
3237
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2999
3238
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
3000
3239
  }
3001
- function issue6(code, message, severity, file, rule, refs) {
3240
+ function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
3002
3241
  const issue7 = {
3003
3242
  code,
3004
3243
  severity,
3244
+ category,
3005
3245
  message
3006
3246
  };
3247
+ if (suggested_action) {
3248
+ issue7.suggested_action = suggested_action;
3249
+ }
3007
3250
  if (file) {
3008
3251
  issue7.file = file;
3009
3252
  }
@@ -3022,6 +3265,7 @@ async function validateProject(root, configResult) {
3022
3265
  const { config, issues: configIssues } = resolved;
3023
3266
  const issues = [
3024
3267
  ...configIssues,
3268
+ ...await validatePromptsIntegrity(root),
3025
3269
  ...await validateSpecs(root, config),
3026
3270
  ...await validateDeltas(root, config),
3027
3271
  ...await validateScenarios(root, config),
@@ -3062,15 +3306,15 @@ function countIssues(issues) {
3062
3306
  // src/core/report.ts
3063
3307
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3064
3308
  async function createReportData(root, validation, configResult) {
3065
- const resolvedRoot = path17.resolve(root);
3309
+ const resolvedRoot = path18.resolve(root);
3066
3310
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3067
3311
  const config = resolved.config;
3068
3312
  const configPath = resolved.configPath;
3069
3313
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3070
3314
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3071
- const apiRoot = path17.join(contractsRoot, "api");
3072
- const uiRoot = path17.join(contractsRoot, "ui");
3073
- const dbRoot = path17.join(contractsRoot, "db");
3315
+ const apiRoot = path18.join(contractsRoot, "api");
3316
+ const uiRoot = path18.join(contractsRoot, "ui");
3317
+ const dbRoot = path18.join(contractsRoot, "db");
3074
3318
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3075
3319
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3076
3320
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3184,7 +3428,39 @@ function formatReportMarkdown(data) {
3184
3428
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
3185
3429
  lines.push(`- \u7248: ${data.version}`);
3186
3430
  lines.push("");
3187
- lines.push("## Summary");
3431
+ const severityOrder = {
3432
+ error: 0,
3433
+ warning: 1,
3434
+ info: 2
3435
+ };
3436
+ const categoryOrder = {
3437
+ compatibility: 0,
3438
+ change: 1
3439
+ };
3440
+ const issuesByCategory = {
3441
+ compatibility: [],
3442
+ change: []
3443
+ };
3444
+ for (const issue7 of data.issues) {
3445
+ const cat = issue7.category;
3446
+ if (cat === "change") {
3447
+ issuesByCategory.change.push(issue7);
3448
+ } else {
3449
+ issuesByCategory.compatibility.push(issue7);
3450
+ }
3451
+ }
3452
+ const countIssuesBySeverity = (issues) => issues.reduce(
3453
+ (acc, i) => {
3454
+ acc[i.severity] += 1;
3455
+ return acc;
3456
+ },
3457
+ { info: 0, warning: 0, error: 0 }
3458
+ );
3459
+ const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
3460
+ const changeCounts = countIssuesBySeverity(issuesByCategory.change);
3461
+ lines.push("## Dashboard");
3462
+ lines.push("");
3463
+ lines.push("### Summary");
3188
3464
  lines.push("");
3189
3465
  lines.push(`- specs: ${data.summary.specs}`);
3190
3466
  lines.push(`- scenarios: ${data.summary.scenarios}`);
@@ -3192,7 +3468,13 @@ function formatReportMarkdown(data) {
3192
3468
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3193
3469
  );
3194
3470
  lines.push(
3195
- `- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3471
+ `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3472
+ );
3473
+ lines.push(
3474
+ `- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
3475
+ );
3476
+ lines.push(
3477
+ `- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
3196
3478
  );
3197
3479
  lines.push(
3198
3480
  `- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
@@ -3201,49 +3483,65 @@ function formatReportMarkdown(data) {
3201
3483
  `- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
3202
3484
  );
3203
3485
  lines.push("");
3204
- lines.push("## Findings");
3205
- lines.push("");
3206
- lines.push("### Issues (by code)");
3486
+ lines.push("### Next Actions");
3207
3487
  lines.push("");
3208
- const severityOrder = {
3209
- error: 0,
3210
- warning: 1,
3211
- info: 2
3212
- };
3213
- const issueKeyToCount = /* @__PURE__ */ new Map();
3214
- for (const issue7 of data.issues) {
3215
- const key = `${issue7.severity}|${issue7.code}`;
3216
- const current = issueKeyToCount.get(key);
3217
- if (current) {
3218
- current.count += 1;
3219
- continue;
3220
- }
3221
- issueKeyToCount.set(key, {
3222
- severity: issue7.severity,
3223
- code: issue7.code,
3224
- count: 1
3225
- });
3226
- }
3227
- const issueSummaryRows = Array.from(issueKeyToCount.values()).sort((a, b) => {
3228
- const sa = severityOrder[a.severity] ?? 999;
3229
- const sb = severityOrder[b.severity] ?? 999;
3230
- if (sa !== sb) return sa - sb;
3231
- return a.code.localeCompare(b.code);
3232
- }).map((x) => [x.severity, x.code, String(x.count)]);
3233
- if (issueSummaryRows.length === 0) {
3234
- lines.push("- (none)");
3488
+ if (data.summary.counts.error > 0) {
3489
+ lines.push(
3490
+ "- 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"
3491
+ );
3492
+ lines.push(
3493
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3494
+ );
3495
+ } else if (data.summary.counts.warning > 0) {
3496
+ lines.push(
3497
+ "- 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"
3498
+ );
3499
+ lines.push(
3500
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3501
+ );
3235
3502
  } else {
3503
+ 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");
3236
3504
  lines.push(
3237
- ...formatMarkdownTable(["Severity", "Code", "Count"], issueSummaryRows)
3505
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
3238
3506
  );
3239
3507
  }
3240
3508
  lines.push("");
3241
- lines.push("### Issues (list)");
3509
+ lines.push("### Index");
3242
3510
  lines.push("");
3243
- if (data.issues.length === 0) {
3244
- lines.push("- (none)");
3245
- } else {
3246
- const sortedIssues = [...data.issues].sort((a, b) => {
3511
+ lines.push("- [Compatibility Issues](#compatibility-issues)");
3512
+ lines.push("- [Change Issues](#change-issues)");
3513
+ lines.push("- [IDs](#ids)");
3514
+ lines.push("- [Traceability](#traceability)");
3515
+ lines.push("");
3516
+ const formatIssueSummaryTable = (issues) => {
3517
+ const issueKeyToCount = /* @__PURE__ */ new Map();
3518
+ for (const issue7 of issues) {
3519
+ const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
3520
+ const current = issueKeyToCount.get(key);
3521
+ if (current) {
3522
+ current.count += 1;
3523
+ continue;
3524
+ }
3525
+ issueKeyToCount.set(key, {
3526
+ category: issue7.category,
3527
+ severity: issue7.severity,
3528
+ code: issue7.code,
3529
+ count: 1
3530
+ });
3531
+ }
3532
+ const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
3533
+ const ca = categoryOrder[a.category] ?? 999;
3534
+ const cb = categoryOrder[b.category] ?? 999;
3535
+ if (ca !== cb) return ca - cb;
3536
+ const sa = severityOrder[a.severity] ?? 999;
3537
+ const sb = severityOrder[b.severity] ?? 999;
3538
+ if (sa !== sb) return sa - sb;
3539
+ return a.code.localeCompare(b.code);
3540
+ }).map((x) => [x.severity, x.code, String(x.count)]);
3541
+ return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
3542
+ };
3543
+ const formatIssueCards = (issues) => {
3544
+ const sorted = [...issues].sort((a, b) => {
3247
3545
  const sa = severityOrder[a.severity] ?? 999;
3248
3546
  const sb = severityOrder[b.severity] ?? 999;
3249
3547
  if (sa !== sb) return sa - sb;
@@ -3257,16 +3555,54 @@ function formatReportMarkdown(data) {
3257
3555
  const lineB = b.loc?.line ?? 0;
3258
3556
  return lineA - lineB;
3259
3557
  });
3260
- for (const item of sortedIssues) {
3261
- const location = item.file ? ` (${item.file})` : "";
3262
- const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
3263
- lines.push(
3264
- `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
3558
+ if (sorted.length === 0) {
3559
+ return ["- (none)"];
3560
+ }
3561
+ const out = [];
3562
+ for (const item of sorted) {
3563
+ out.push(
3564
+ `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
3265
3565
  );
3566
+ if (item.file) {
3567
+ const loc = item.loc?.line ? `:${item.loc.line}` : "";
3568
+ out.push(`- file: ${item.file}${loc}`);
3569
+ }
3570
+ if (item.rule) {
3571
+ out.push(`- rule: ${item.rule}`);
3572
+ }
3573
+ if (item.refs && item.refs.length > 0) {
3574
+ out.push(`- refs: ${item.refs.join(", ")}`);
3575
+ }
3576
+ if (item.suggested_action) {
3577
+ out.push("- suggested_action:");
3578
+ const actionLines = String(item.suggested_action).split("\n");
3579
+ for (const line of actionLines) {
3580
+ out.push(` ${line}`);
3581
+ }
3582
+ }
3583
+ out.push("");
3266
3584
  }
3267
- }
3585
+ return out;
3586
+ };
3587
+ lines.push("## Compatibility Issues");
3268
3588
  lines.push("");
3269
- lines.push("### IDs");
3589
+ lines.push("### Summary");
3590
+ lines.push("");
3591
+ lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
3592
+ lines.push("");
3593
+ lines.push("### Issues");
3594
+ lines.push("");
3595
+ lines.push(...formatIssueCards(issuesByCategory.compatibility));
3596
+ lines.push("## Change Issues");
3597
+ lines.push("");
3598
+ lines.push("### Summary");
3599
+ lines.push("");
3600
+ lines.push(...formatIssueSummaryTable(issuesByCategory.change));
3601
+ lines.push("");
3602
+ lines.push("### Issues");
3603
+ lines.push("");
3604
+ lines.push(...formatIssueCards(issuesByCategory.change));
3605
+ lines.push("## IDs");
3270
3606
  lines.push("");
3271
3607
  lines.push(formatIdLine("SPEC", data.ids.spec));
3272
3608
  lines.push(formatIdLine("BR", data.ids.br));
@@ -3275,7 +3611,7 @@ function formatReportMarkdown(data) {
3275
3611
  lines.push(formatIdLine("API", data.ids.api));
3276
3612
  lines.push(formatIdLine("DB", data.ids.db));
3277
3613
  lines.push("");
3278
- lines.push("### Traceability");
3614
+ lines.push("## Traceability");
3279
3615
  lines.push("");
3280
3616
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
3281
3617
  lines.push(
@@ -3449,7 +3785,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3449
3785
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3450
3786
  }
3451
3787
  for (const file of specFiles) {
3452
- const text = await readFile11(file, "utf-8");
3788
+ const text = await readFile12(file, "utf-8");
3453
3789
  const parsed = parseSpec(text, file);
3454
3790
  const specKey = parsed.specId;
3455
3791
  if (!specKey) {
@@ -3490,7 +3826,7 @@ async function collectIds(files) {
3490
3826
  DB: /* @__PURE__ */ new Set()
3491
3827
  };
3492
3828
  for (const file of files) {
3493
- const text = await readFile11(file, "utf-8");
3829
+ const text = await readFile12(file, "utf-8");
3494
3830
  for (const prefix of ID_PREFIXES2) {
3495
3831
  const ids = extractIds(text, prefix);
3496
3832
  ids.forEach((id) => result[prefix].add(id));
@@ -3508,7 +3844,7 @@ async function collectIds(files) {
3508
3844
  async function collectUpstreamIds(files) {
3509
3845
  const ids = /* @__PURE__ */ new Set();
3510
3846
  for (const file of files) {
3511
- const text = await readFile11(file, "utf-8");
3847
+ const text = await readFile12(file, "utf-8");
3512
3848
  extractAllIds(text).forEach((id) => ids.add(id));
3513
3849
  }
3514
3850
  return ids;
@@ -3529,7 +3865,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3529
3865
  }
3530
3866
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3531
3867
  for (const file of targetFiles) {
3532
- const text = await readFile11(file, "utf-8");
3868
+ const text = await readFile12(file, "utf-8");
3533
3869
  if (pattern.test(text)) {
3534
3870
  return true;
3535
3871
  }
@@ -3621,7 +3957,7 @@ function buildHotspots(issues) {
3621
3957
 
3622
3958
  // src/cli/commands/report.ts
3623
3959
  async function runReport(options) {
3624
- const root = path18.resolve(options.root);
3960
+ const root = path19.resolve(options.root);
3625
3961
  const configResult = await loadConfig(root);
3626
3962
  let validation;
3627
3963
  if (options.runValidate) {
@@ -3638,7 +3974,7 @@ async function runReport(options) {
3638
3974
  validation = normalized;
3639
3975
  } else {
3640
3976
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3641
- const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
3977
+ const inputPath = path19.isAbsolute(input) ? input : path19.resolve(root, input);
3642
3978
  try {
3643
3979
  validation = await readValidationResult(inputPath);
3644
3980
  } catch (err) {
@@ -3664,10 +4000,10 @@ async function runReport(options) {
3664
4000
  const data = await createReportData(root, validation, configResult);
3665
4001
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3666
4002
  const outRoot = resolvePath(root, configResult.config, "outDir");
3667
- const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
4003
+ const defaultOut = options.format === "json" ? path19.join(outRoot, "report.json") : path19.join(outRoot, "report.md");
3668
4004
  const out = options.outPath ?? defaultOut;
3669
- const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
3670
- await mkdir3(path18.dirname(outPath), { recursive: true });
4005
+ const outPath = path19.isAbsolute(out) ? out : path19.resolve(root, out);
4006
+ await mkdir3(path19.dirname(outPath), { recursive: true });
3671
4007
  await writeFile2(outPath, `${output}
3672
4008
  `, "utf-8");
3673
4009
  info(
@@ -3676,7 +4012,7 @@ async function runReport(options) {
3676
4012
  info(`wrote report: ${outPath}`);
3677
4013
  }
3678
4014
  async function readValidationResult(inputPath) {
3679
- const raw = await readFile12(inputPath, "utf-8");
4015
+ const raw = await readFile13(inputPath, "utf-8");
3680
4016
  const parsed = JSON.parse(raw);
3681
4017
  if (!isValidationResult(parsed)) {
3682
4018
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3732,15 +4068,15 @@ function isMissingFileError5(error2) {
3732
4068
  return record2.code === "ENOENT";
3733
4069
  }
3734
4070
  async function writeValidationResult(root, outputPath, result) {
3735
- const abs = path18.isAbsolute(outputPath) ? outputPath : path18.resolve(root, outputPath);
3736
- await mkdir3(path18.dirname(abs), { recursive: true });
4071
+ const abs = path19.isAbsolute(outputPath) ? outputPath : path19.resolve(root, outputPath);
4072
+ await mkdir3(path19.dirname(abs), { recursive: true });
3737
4073
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3738
4074
  `, "utf-8");
3739
4075
  }
3740
4076
 
3741
4077
  // src/cli/commands/validate.ts
3742
4078
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3743
- import path19 from "path";
4079
+ import path20 from "path";
3744
4080
 
3745
4081
  // src/cli/lib/failOn.ts
3746
4082
  function shouldFail(result, failOn) {
@@ -3755,7 +4091,7 @@ function shouldFail(result, failOn) {
3755
4091
 
3756
4092
  // src/cli/commands/validate.ts
3757
4093
  async function runValidate(options) {
3758
- const root = path19.resolve(options.root);
4094
+ const root = path20.resolve(options.root);
3759
4095
  const configResult = await loadConfig(root);
3760
4096
  const result = await validateProject(root, configResult);
3761
4097
  const normalized = normalizeValidationResult(root, result);
@@ -3879,12 +4215,12 @@ function issueKey(issue7) {
3879
4215
  }
3880
4216
  async function emitJson(result, root, jsonPath) {
3881
4217
  const abs = resolveJsonPath(root, jsonPath);
3882
- await mkdir4(path19.dirname(abs), { recursive: true });
4218
+ await mkdir4(path20.dirname(abs), { recursive: true });
3883
4219
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
3884
4220
  `, "utf-8");
3885
4221
  }
3886
4222
  function resolveJsonPath(root, jsonPath) {
3887
- return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
4223
+ return path20.isAbsolute(jsonPath) ? jsonPath : path20.resolve(root, jsonPath);
3888
4224
  }
3889
4225
  var GITHUB_ANNOTATION_LIMIT = 100;
3890
4226
 
@@ -4082,7 +4418,7 @@ Commands:
4082
4418
  Options:
4083
4419
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
4084
4420
  --dir <path> init \u306E\u51FA\u529B\u5148
4085
- --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
4421
+ --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
4086
4422
  --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
4087
4423
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
4088
4424
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F