qfai 0.8.0 → 0.8.1

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.8.1".length > 0) {
1000
+ return "0.8.1";
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,39 @@ 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");
1356
1525
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1357
- force: options.force,
1358
- dryRun: options.dryRun
1526
+ force: false,
1527
+ dryRun: options.dryRun,
1528
+ conflictPolicy: "skip"
1359
1529
  });
1360
1530
  const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1361
- force: options.force,
1531
+ force: false,
1362
1532
  dryRun: options.dryRun,
1363
- protect: ["prompts.local"]
1533
+ conflictPolicy: "skip",
1534
+ protect: ["prompts.local"],
1535
+ exclude: ["prompts"]
1364
1536
  });
1537
+ const promptsResult = await copyTemplatePaths(
1538
+ qfaiAssets,
1539
+ destQfai,
1540
+ ["prompts"],
1541
+ {
1542
+ force: options.force,
1543
+ dryRun: options.dryRun,
1544
+ conflictPolicy: "skip",
1545
+ protect: ["prompts.local"]
1546
+ }
1547
+ );
1365
1548
  report(
1366
- [...rootResult.copied, ...qfaiResult.copied],
1367
- [...rootResult.skipped, ...qfaiResult.skipped],
1549
+ [...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
1550
+ [...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
1368
1551
  options.dryRun,
1369
1552
  "init"
1370
1553
  );
@@ -1380,8 +1563,8 @@ function report(copied, skipped, dryRun, label) {
1380
1563
  }
1381
1564
 
1382
1565
  // src/cli/commands/report.ts
1383
- import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
1384
- import path18 from "path";
1566
+ import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
1567
+ import path19 from "path";
1385
1568
 
1386
1569
  // src/core/normalize.ts
1387
1570
  function normalizeIssuePaths(root, issues) {
@@ -1421,12 +1604,12 @@ function normalizeValidationResult(root, result) {
1421
1604
  }
1422
1605
 
1423
1606
  // src/core/report.ts
1424
- import { readFile as readFile11 } from "fs/promises";
1425
- import path17 from "path";
1607
+ import { readFile as readFile12 } from "fs/promises";
1608
+ import path18 from "path";
1426
1609
 
1427
1610
  // src/core/contractIndex.ts
1428
- import { readFile as readFile4 } from "fs/promises";
1429
- import path12 from "path";
1611
+ import { readFile as readFile5 } from "fs/promises";
1612
+ import path13 from "path";
1430
1613
 
1431
1614
  // src/core/contractsDecl.ts
1432
1615
  var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
@@ -1448,9 +1631,9 @@ function stripContractDeclarationLines(text) {
1448
1631
  // src/core/contractIndex.ts
1449
1632
  async function buildContractIndex(root, config) {
1450
1633
  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");
1634
+ const uiRoot = path13.join(contractsRoot, "ui");
1635
+ const apiRoot = path13.join(contractsRoot, "api");
1636
+ const dbRoot = path13.join(contractsRoot, "db");
1454
1637
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1455
1638
  collectUiContractFiles(uiRoot),
1456
1639
  collectApiContractFiles(apiRoot),
@@ -1468,7 +1651,7 @@ async function buildContractIndex(root, config) {
1468
1651
  }
1469
1652
  async function indexContractFiles(files, index) {
1470
1653
  for (const file of files) {
1471
- const text = await readFile4(file, "utf-8");
1654
+ const text = await readFile5(file, "utf-8");
1472
1655
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1473
1656
  }
1474
1657
  }
@@ -1703,14 +1886,14 @@ function parseSpec(md, file) {
1703
1886
  }
1704
1887
 
1705
1888
  // src/core/validators/contracts.ts
1706
- import { readFile as readFile5 } from "fs/promises";
1707
- import path14 from "path";
1889
+ import { readFile as readFile6 } from "fs/promises";
1890
+ import path15 from "path";
1708
1891
 
1709
1892
  // src/core/contracts.ts
1710
- import path13 from "path";
1893
+ import path14 from "path";
1711
1894
  import { parse as parseYaml2 } from "yaml";
1712
1895
  function parseStructuredContract(file, text) {
1713
- const ext = path13.extname(file).toLowerCase();
1896
+ const ext = path14.extname(file).toLowerCase();
1714
1897
  if (ext === ".json") {
1715
1898
  return JSON.parse(text);
1716
1899
  }
@@ -1730,9 +1913,9 @@ var SQL_DANGEROUS_PATTERNS = [
1730
1913
  async function validateContracts(root, config) {
1731
1914
  const issues = [];
1732
1915
  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")));
1916
+ issues.push(...await validateUiContracts(path15.join(contractsRoot, "ui")));
1917
+ issues.push(...await validateApiContracts(path15.join(contractsRoot, "api")));
1918
+ issues.push(...await validateDbContracts(path15.join(contractsRoot, "db")));
1736
1919
  const contractIndex = await buildContractIndex(root, config);
1737
1920
  issues.push(...validateDuplicateContractIds(contractIndex));
1738
1921
  return issues;
@@ -1752,7 +1935,7 @@ async function validateUiContracts(uiRoot) {
1752
1935
  }
1753
1936
  const issues = [];
1754
1937
  for (const file of files) {
1755
- const text = await readFile5(file, "utf-8");
1938
+ const text = await readFile6(file, "utf-8");
1756
1939
  const invalidIds = extractInvalidIds(text, [
1757
1940
  "SPEC",
1758
1941
  "BR",
@@ -1807,7 +1990,7 @@ async function validateApiContracts(apiRoot) {
1807
1990
  }
1808
1991
  const issues = [];
1809
1992
  for (const file of files) {
1810
- const text = await readFile5(file, "utf-8");
1993
+ const text = await readFile6(file, "utf-8");
1811
1994
  const invalidIds = extractInvalidIds(text, [
1812
1995
  "SPEC",
1813
1996
  "BR",
@@ -1875,7 +2058,7 @@ async function validateDbContracts(dbRoot) {
1875
2058
  }
1876
2059
  const issues = [];
1877
2060
  for (const file of files) {
1878
- const text = await readFile5(file, "utf-8");
2061
+ const text = await readFile6(file, "utf-8");
1879
2062
  const invalidIds = extractInvalidIds(text, [
1880
2063
  "SPEC",
1881
2064
  "BR",
@@ -1995,12 +2178,16 @@ function formatError4(error2) {
1995
2178
  }
1996
2179
  return String(error2);
1997
2180
  }
1998
- function issue(code, message, severity, file, rule, refs) {
2181
+ function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
1999
2182
  const issue7 = {
2000
2183
  code,
2001
2184
  severity,
2185
+ category,
2002
2186
  message
2003
2187
  };
2188
+ if (suggested_action) {
2189
+ issue7.suggested_action = suggested_action;
2190
+ }
2004
2191
  if (file) {
2005
2192
  issue7.file = file;
2006
2193
  }
@@ -2014,8 +2201,8 @@ function issue(code, message, severity, file, rule, refs) {
2014
2201
  }
2015
2202
 
2016
2203
  // src/core/validators/delta.ts
2017
- import { readFile as readFile6 } from "fs/promises";
2018
- import path15 from "path";
2204
+ import { readFile as readFile7 } from "fs/promises";
2205
+ import path16 from "path";
2019
2206
  var SECTION_RE = /^##\s+変更区分/m;
2020
2207
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2021
2208
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2029,10 +2216,10 @@ async function validateDeltas(root, config) {
2029
2216
  }
2030
2217
  const issues = [];
2031
2218
  for (const pack of packs) {
2032
- const deltaPath = path15.join(pack, "delta.md");
2219
+ const deltaPath = path16.join(pack, "delta.md");
2033
2220
  let text;
2034
2221
  try {
2035
- text = await readFile6(deltaPath, "utf-8");
2222
+ text = await readFile7(deltaPath, "utf-8");
2036
2223
  } catch (error2) {
2037
2224
  if (isMissingFileError2(error2)) {
2038
2225
  issues.push(
@@ -2085,12 +2272,16 @@ function isMissingFileError2(error2) {
2085
2272
  }
2086
2273
  return error2.code === "ENOENT";
2087
2274
  }
2088
- function issue2(code, message, severity, file, rule, refs) {
2275
+ function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
2089
2276
  const issue7 = {
2090
2277
  code,
2091
2278
  severity,
2279
+ category,
2092
2280
  message
2093
2281
  };
2282
+ if (suggested_action) {
2283
+ issue7.suggested_action = suggested_action;
2284
+ }
2094
2285
  if (file) {
2095
2286
  issue7.file = file;
2096
2287
  }
@@ -2104,8 +2295,8 @@ function issue2(code, message, severity, file, rule, refs) {
2104
2295
  }
2105
2296
 
2106
2297
  // src/core/validators/ids.ts
2107
- import { readFile as readFile7 } from "fs/promises";
2108
- import path16 from "path";
2298
+ import { readFile as readFile8 } from "fs/promises";
2299
+ import path17 from "path";
2109
2300
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2110
2301
  async function validateDefinedIds(root, config) {
2111
2302
  const issues = [];
@@ -2140,7 +2331,7 @@ async function validateDefinedIds(root, config) {
2140
2331
  }
2141
2332
  async function collectSpecDefinitionIds(files, out) {
2142
2333
  for (const file of files) {
2143
- const text = await readFile7(file, "utf-8");
2334
+ const text = await readFile8(file, "utf-8");
2144
2335
  const parsed = parseSpec(text, file);
2145
2336
  if (parsed.specId) {
2146
2337
  recordId(out, parsed.specId, file);
@@ -2150,7 +2341,7 @@ async function collectSpecDefinitionIds(files, out) {
2150
2341
  }
2151
2342
  async function collectScenarioDefinitionIds(files, out) {
2152
2343
  for (const file of files) {
2153
- const text = await readFile7(file, "utf-8");
2344
+ const text = await readFile8(file, "utf-8");
2154
2345
  const { document, errors } = parseScenarioDocument(text, file);
2155
2346
  if (!document || errors.length > 0) {
2156
2347
  continue;
@@ -2171,16 +2362,20 @@ function recordId(out, id, file) {
2171
2362
  }
2172
2363
  function formatFileList(files, root) {
2173
2364
  return files.map((file) => {
2174
- const relative = path16.relative(root, file);
2365
+ const relative = path17.relative(root, file);
2175
2366
  return relative.length > 0 ? relative : file;
2176
2367
  }).join(", ");
2177
2368
  }
2178
- function issue3(code, message, severity, file, rule, refs) {
2369
+ function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2179
2370
  const issue7 = {
2180
2371
  code,
2181
2372
  severity,
2373
+ category,
2182
2374
  message
2183
2375
  };
2376
+ if (suggested_action) {
2377
+ issue7.suggested_action = suggested_action;
2378
+ }
2184
2379
  if (file) {
2185
2380
  issue7.file = file;
2186
2381
  }
@@ -2193,8 +2388,39 @@ function issue3(code, message, severity, file, rule, refs) {
2193
2388
  return issue7;
2194
2389
  }
2195
2390
 
2391
+ // src/core/validators/promptsIntegrity.ts
2392
+ async function validatePromptsIntegrity(root) {
2393
+ const diff = await diffProjectPromptsAgainstInitAssets(root);
2394
+ if (diff.status !== "modified") {
2395
+ return [];
2396
+ }
2397
+ const total = diff.missing.length + diff.extra.length + diff.changed.length;
2398
+ const hints = [
2399
+ diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
2400
+ diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
2401
+ diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
2402
+ ].filter(Boolean).join(" / ");
2403
+ const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
2404
+ const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
2405
+ return [
2406
+ {
2407
+ code: "QFAI-PROMPTS-001",
2408
+ severity: "error",
2409
+ category: "change",
2410
+ message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
2411
+ suggested_action: [
2412
+ "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",
2413
+ "\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
2414
+ "- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
2415
+ "- \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"
2416
+ ].join("\n"),
2417
+ rule: "prompts.integrity"
2418
+ }
2419
+ ];
2420
+ }
2421
+
2196
2422
  // src/core/validators/scenario.ts
2197
- import { readFile as readFile8 } from "fs/promises";
2423
+ import { readFile as readFile9 } from "fs/promises";
2198
2424
  var GIVEN_PATTERN = /\bGiven\b/;
2199
2425
  var WHEN_PATTERN = /\bWhen\b/;
2200
2426
  var THEN_PATTERN = /\bThen\b/;
@@ -2220,7 +2446,7 @@ async function validateScenarios(root, config) {
2220
2446
  for (const entry of entries) {
2221
2447
  let text;
2222
2448
  try {
2223
- text = await readFile8(entry.scenarioPath, "utf-8");
2449
+ text = await readFile9(entry.scenarioPath, "utf-8");
2224
2450
  } catch (error2) {
2225
2451
  if (isMissingFileError3(error2)) {
2226
2452
  issues.push(
@@ -2365,12 +2591,16 @@ function validateScenarioContent(text, file) {
2365
2591
  }
2366
2592
  return issues;
2367
2593
  }
2368
- function issue4(code, message, severity, file, rule, refs) {
2594
+ function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2369
2595
  const issue7 = {
2370
2596
  code,
2371
2597
  severity,
2598
+ category,
2372
2599
  message
2373
2600
  };
2601
+ if (suggested_action) {
2602
+ issue7.suggested_action = suggested_action;
2603
+ }
2374
2604
  if (file) {
2375
2605
  issue7.file = file;
2376
2606
  }
@@ -2390,7 +2620,7 @@ function isMissingFileError3(error2) {
2390
2620
  }
2391
2621
 
2392
2622
  // src/core/validators/spec.ts
2393
- import { readFile as readFile9 } from "fs/promises";
2623
+ import { readFile as readFile10 } from "fs/promises";
2394
2624
  async function validateSpecs(root, config) {
2395
2625
  const specsRoot = resolvePath(root, config, "specsDir");
2396
2626
  const entries = await collectSpecEntries(specsRoot);
@@ -2411,7 +2641,7 @@ async function validateSpecs(root, config) {
2411
2641
  for (const entry of entries) {
2412
2642
  let text;
2413
2643
  try {
2414
- text = await readFile9(entry.specPath, "utf-8");
2644
+ text = await readFile10(entry.specPath, "utf-8");
2415
2645
  } catch (error2) {
2416
2646
  if (isMissingFileError4(error2)) {
2417
2647
  issues.push(
@@ -2535,12 +2765,16 @@ function validateSpecContent(text, file, requiredSections) {
2535
2765
  }
2536
2766
  return issues;
2537
2767
  }
2538
- function issue5(code, message, severity, file, rule, refs) {
2768
+ function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
2539
2769
  const issue7 = {
2540
2770
  code,
2541
2771
  severity,
2772
+ category,
2542
2773
  message
2543
2774
  };
2775
+ if (suggested_action) {
2776
+ issue7.suggested_action = suggested_action;
2777
+ }
2544
2778
  if (file) {
2545
2779
  issue7.file = file;
2546
2780
  }
@@ -2560,7 +2794,7 @@ function isMissingFileError4(error2) {
2560
2794
  }
2561
2795
 
2562
2796
  // src/core/validators/traceability.ts
2563
- import { readFile as readFile10 } from "fs/promises";
2797
+ import { readFile as readFile11 } from "fs/promises";
2564
2798
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2565
2799
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2566
2800
  async function validateTraceability(root, config) {
@@ -2580,7 +2814,7 @@ async function validateTraceability(root, config) {
2580
2814
  const contractIndex = await buildContractIndex(root, config);
2581
2815
  const contractIds = contractIndex.ids;
2582
2816
  for (const file of specFiles) {
2583
- const text = await readFile10(file, "utf-8");
2817
+ const text = await readFile11(file, "utf-8");
2584
2818
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2585
2819
  const parsed = parseSpec(text, file);
2586
2820
  if (parsed.specId) {
@@ -2653,7 +2887,7 @@ async function validateTraceability(root, config) {
2653
2887
  }
2654
2888
  }
2655
2889
  for (const file of scenarioFiles) {
2656
- const text = await readFile10(file, "utf-8");
2890
+ const text = await readFile11(file, "utf-8");
2657
2891
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2658
2892
  const scenarioContractRefs = parseContractRefs(text, {
2659
2893
  allowCommentPrefix: true
@@ -2975,7 +3209,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2975
3209
  const pattern = buildIdPattern(Array.from(upstreamIds));
2976
3210
  let found = false;
2977
3211
  for (const file of targetFiles) {
2978
- const text = await readFile10(file, "utf-8");
3212
+ const text = await readFile11(file, "utf-8");
2979
3213
  if (pattern.test(text)) {
2980
3214
  found = true;
2981
3215
  break;
@@ -2998,12 +3232,16 @@ function buildIdPattern(ids) {
2998
3232
  const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2999
3233
  return new RegExp(`\\b(${escaped.join("|")})\\b`);
3000
3234
  }
3001
- function issue6(code, message, severity, file, rule, refs) {
3235
+ function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
3002
3236
  const issue7 = {
3003
3237
  code,
3004
3238
  severity,
3239
+ category,
3005
3240
  message
3006
3241
  };
3242
+ if (suggested_action) {
3243
+ issue7.suggested_action = suggested_action;
3244
+ }
3007
3245
  if (file) {
3008
3246
  issue7.file = file;
3009
3247
  }
@@ -3022,6 +3260,7 @@ async function validateProject(root, configResult) {
3022
3260
  const { config, issues: configIssues } = resolved;
3023
3261
  const issues = [
3024
3262
  ...configIssues,
3263
+ ...await validatePromptsIntegrity(root),
3025
3264
  ...await validateSpecs(root, config),
3026
3265
  ...await validateDeltas(root, config),
3027
3266
  ...await validateScenarios(root, config),
@@ -3062,15 +3301,15 @@ function countIssues(issues) {
3062
3301
  // src/core/report.ts
3063
3302
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3064
3303
  async function createReportData(root, validation, configResult) {
3065
- const resolvedRoot = path17.resolve(root);
3304
+ const resolvedRoot = path18.resolve(root);
3066
3305
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3067
3306
  const config = resolved.config;
3068
3307
  const configPath = resolved.configPath;
3069
3308
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3070
3309
  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");
3310
+ const apiRoot = path18.join(contractsRoot, "api");
3311
+ const uiRoot = path18.join(contractsRoot, "ui");
3312
+ const dbRoot = path18.join(contractsRoot, "db");
3074
3313
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3075
3314
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3076
3315
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3184,7 +3423,39 @@ function formatReportMarkdown(data) {
3184
3423
  lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
3185
3424
  lines.push(`- \u7248: ${data.version}`);
3186
3425
  lines.push("");
3187
- lines.push("## Summary");
3426
+ const severityOrder = {
3427
+ error: 0,
3428
+ warning: 1,
3429
+ info: 2
3430
+ };
3431
+ const categoryOrder = {
3432
+ compatibility: 0,
3433
+ change: 1
3434
+ };
3435
+ const issuesByCategory = {
3436
+ compatibility: [],
3437
+ change: []
3438
+ };
3439
+ for (const issue7 of data.issues) {
3440
+ const cat = issue7.category;
3441
+ if (cat === "change") {
3442
+ issuesByCategory.change.push(issue7);
3443
+ } else {
3444
+ issuesByCategory.compatibility.push(issue7);
3445
+ }
3446
+ }
3447
+ const countIssuesBySeverity = (issues) => issues.reduce(
3448
+ (acc, i) => {
3449
+ acc[i.severity] += 1;
3450
+ return acc;
3451
+ },
3452
+ { info: 0, warning: 0, error: 0 }
3453
+ );
3454
+ const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
3455
+ const changeCounts = countIssuesBySeverity(issuesByCategory.change);
3456
+ lines.push("## Dashboard");
3457
+ lines.push("");
3458
+ lines.push("### Summary");
3188
3459
  lines.push("");
3189
3460
  lines.push(`- specs: ${data.summary.specs}`);
3190
3461
  lines.push(`- scenarios: ${data.summary.scenarios}`);
@@ -3192,7 +3463,13 @@ function formatReportMarkdown(data) {
3192
3463
  `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3193
3464
  );
3194
3465
  lines.push(
3195
- `- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3466
+ `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
3467
+ );
3468
+ lines.push(
3469
+ `- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
3470
+ );
3471
+ lines.push(
3472
+ `- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
3196
3473
  );
3197
3474
  lines.push(
3198
3475
  `- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
@@ -3201,49 +3478,65 @@ function formatReportMarkdown(data) {
3201
3478
  `- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
3202
3479
  );
3203
3480
  lines.push("");
3204
- lines.push("## Findings");
3481
+ lines.push("### Next Actions");
3205
3482
  lines.push("");
3206
- lines.push("### Issues (by code)");
3207
- 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)");
3483
+ if (data.summary.counts.error > 0) {
3484
+ lines.push(
3485
+ "- 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"
3486
+ );
3487
+ lines.push(
3488
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3489
+ );
3490
+ } else if (data.summary.counts.warning > 0) {
3491
+ lines.push(
3492
+ "- 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"
3493
+ );
3494
+ lines.push(
3495
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
3496
+ );
3235
3497
  } else {
3498
+ 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
3499
  lines.push(
3237
- ...formatMarkdownTable(["Severity", "Code", "Count"], issueSummaryRows)
3500
+ "- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
3238
3501
  );
3239
3502
  }
3240
3503
  lines.push("");
3241
- lines.push("### Issues (list)");
3504
+ lines.push("### Index");
3242
3505
  lines.push("");
3243
- if (data.issues.length === 0) {
3244
- lines.push("- (none)");
3245
- } else {
3246
- const sortedIssues = [...data.issues].sort((a, b) => {
3506
+ lines.push("- [Compatibility Issues](#compatibility-issues)");
3507
+ lines.push("- [Change Issues](#change-issues)");
3508
+ lines.push("- [IDs](#ids)");
3509
+ lines.push("- [Traceability](#traceability)");
3510
+ lines.push("");
3511
+ const formatIssueSummaryTable = (issues) => {
3512
+ const issueKeyToCount = /* @__PURE__ */ new Map();
3513
+ for (const issue7 of issues) {
3514
+ const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
3515
+ const current = issueKeyToCount.get(key);
3516
+ if (current) {
3517
+ current.count += 1;
3518
+ continue;
3519
+ }
3520
+ issueKeyToCount.set(key, {
3521
+ category: issue7.category,
3522
+ severity: issue7.severity,
3523
+ code: issue7.code,
3524
+ count: 1
3525
+ });
3526
+ }
3527
+ const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
3528
+ const ca = categoryOrder[a.category] ?? 999;
3529
+ const cb = categoryOrder[b.category] ?? 999;
3530
+ if (ca !== cb) return ca - cb;
3531
+ const sa = severityOrder[a.severity] ?? 999;
3532
+ const sb = severityOrder[b.severity] ?? 999;
3533
+ if (sa !== sb) return sa - sb;
3534
+ return a.code.localeCompare(b.code);
3535
+ }).map((x) => [x.severity, x.code, String(x.count)]);
3536
+ return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
3537
+ };
3538
+ const formatIssueCards = (issues) => {
3539
+ const sorted = [...issues].sort((a, b) => {
3247
3540
  const sa = severityOrder[a.severity] ?? 999;
3248
3541
  const sb = severityOrder[b.severity] ?? 999;
3249
3542
  if (sa !== sb) return sa - sb;
@@ -3257,16 +3550,54 @@ function formatReportMarkdown(data) {
3257
3550
  const lineB = b.loc?.line ?? 0;
3258
3551
  return lineA - lineB;
3259
3552
  });
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}`
3553
+ if (sorted.length === 0) {
3554
+ return ["- (none)"];
3555
+ }
3556
+ const out = [];
3557
+ for (const item of sorted) {
3558
+ out.push(
3559
+ `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
3265
3560
  );
3561
+ if (item.file) {
3562
+ const loc = item.loc?.line ? `:${item.loc.line}` : "";
3563
+ out.push(`- file: ${item.file}${loc}`);
3564
+ }
3565
+ if (item.rule) {
3566
+ out.push(`- rule: ${item.rule}`);
3567
+ }
3568
+ if (item.refs && item.refs.length > 0) {
3569
+ out.push(`- refs: ${item.refs.join(", ")}`);
3570
+ }
3571
+ if (item.suggested_action) {
3572
+ out.push("- suggested_action:");
3573
+ const actionLines = String(item.suggested_action).split("\n");
3574
+ for (const line of actionLines) {
3575
+ out.push(` ${line}`);
3576
+ }
3577
+ }
3578
+ out.push("");
3266
3579
  }
3267
- }
3580
+ return out;
3581
+ };
3582
+ lines.push("## Compatibility Issues");
3268
3583
  lines.push("");
3269
- lines.push("### IDs");
3584
+ lines.push("### Summary");
3585
+ lines.push("");
3586
+ lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
3587
+ lines.push("");
3588
+ lines.push("### Issues");
3589
+ lines.push("");
3590
+ lines.push(...formatIssueCards(issuesByCategory.compatibility));
3591
+ lines.push("## Change Issues");
3592
+ lines.push("");
3593
+ lines.push("### Summary");
3594
+ lines.push("");
3595
+ lines.push(...formatIssueSummaryTable(issuesByCategory.change));
3596
+ lines.push("");
3597
+ lines.push("### Issues");
3598
+ lines.push("");
3599
+ lines.push(...formatIssueCards(issuesByCategory.change));
3600
+ lines.push("## IDs");
3270
3601
  lines.push("");
3271
3602
  lines.push(formatIdLine("SPEC", data.ids.spec));
3272
3603
  lines.push(formatIdLine("BR", data.ids.br));
@@ -3275,7 +3606,7 @@ function formatReportMarkdown(data) {
3275
3606
  lines.push(formatIdLine("API", data.ids.api));
3276
3607
  lines.push(formatIdLine("DB", data.ids.db));
3277
3608
  lines.push("");
3278
- lines.push("### Traceability");
3609
+ lines.push("## Traceability");
3279
3610
  lines.push("");
3280
3611
  lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
3281
3612
  lines.push(
@@ -3449,7 +3780,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3449
3780
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3450
3781
  }
3451
3782
  for (const file of specFiles) {
3452
- const text = await readFile11(file, "utf-8");
3783
+ const text = await readFile12(file, "utf-8");
3453
3784
  const parsed = parseSpec(text, file);
3454
3785
  const specKey = parsed.specId;
3455
3786
  if (!specKey) {
@@ -3490,7 +3821,7 @@ async function collectIds(files) {
3490
3821
  DB: /* @__PURE__ */ new Set()
3491
3822
  };
3492
3823
  for (const file of files) {
3493
- const text = await readFile11(file, "utf-8");
3824
+ const text = await readFile12(file, "utf-8");
3494
3825
  for (const prefix of ID_PREFIXES2) {
3495
3826
  const ids = extractIds(text, prefix);
3496
3827
  ids.forEach((id) => result[prefix].add(id));
@@ -3508,7 +3839,7 @@ async function collectIds(files) {
3508
3839
  async function collectUpstreamIds(files) {
3509
3840
  const ids = /* @__PURE__ */ new Set();
3510
3841
  for (const file of files) {
3511
- const text = await readFile11(file, "utf-8");
3842
+ const text = await readFile12(file, "utf-8");
3512
3843
  extractAllIds(text).forEach((id) => ids.add(id));
3513
3844
  }
3514
3845
  return ids;
@@ -3529,7 +3860,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3529
3860
  }
3530
3861
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3531
3862
  for (const file of targetFiles) {
3532
- const text = await readFile11(file, "utf-8");
3863
+ const text = await readFile12(file, "utf-8");
3533
3864
  if (pattern.test(text)) {
3534
3865
  return true;
3535
3866
  }
@@ -3621,7 +3952,7 @@ function buildHotspots(issues) {
3621
3952
 
3622
3953
  // src/cli/commands/report.ts
3623
3954
  async function runReport(options) {
3624
- const root = path18.resolve(options.root);
3955
+ const root = path19.resolve(options.root);
3625
3956
  const configResult = await loadConfig(root);
3626
3957
  let validation;
3627
3958
  if (options.runValidate) {
@@ -3638,7 +3969,7 @@ async function runReport(options) {
3638
3969
  validation = normalized;
3639
3970
  } else {
3640
3971
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3641
- const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
3972
+ const inputPath = path19.isAbsolute(input) ? input : path19.resolve(root, input);
3642
3973
  try {
3643
3974
  validation = await readValidationResult(inputPath);
3644
3975
  } catch (err) {
@@ -3664,10 +3995,10 @@ async function runReport(options) {
3664
3995
  const data = await createReportData(root, validation, configResult);
3665
3996
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3666
3997
  const outRoot = resolvePath(root, configResult.config, "outDir");
3667
- const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
3998
+ const defaultOut = options.format === "json" ? path19.join(outRoot, "report.json") : path19.join(outRoot, "report.md");
3668
3999
  const out = options.outPath ?? defaultOut;
3669
- const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
3670
- await mkdir3(path18.dirname(outPath), { recursive: true });
4000
+ const outPath = path19.isAbsolute(out) ? out : path19.resolve(root, out);
4001
+ await mkdir3(path19.dirname(outPath), { recursive: true });
3671
4002
  await writeFile2(outPath, `${output}
3672
4003
  `, "utf-8");
3673
4004
  info(
@@ -3676,7 +4007,7 @@ async function runReport(options) {
3676
4007
  info(`wrote report: ${outPath}`);
3677
4008
  }
3678
4009
  async function readValidationResult(inputPath) {
3679
- const raw = await readFile12(inputPath, "utf-8");
4010
+ const raw = await readFile13(inputPath, "utf-8");
3680
4011
  const parsed = JSON.parse(raw);
3681
4012
  if (!isValidationResult(parsed)) {
3682
4013
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3732,15 +4063,15 @@ function isMissingFileError5(error2) {
3732
4063
  return record2.code === "ENOENT";
3733
4064
  }
3734
4065
  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 });
4066
+ const abs = path19.isAbsolute(outputPath) ? outputPath : path19.resolve(root, outputPath);
4067
+ await mkdir3(path19.dirname(abs), { recursive: true });
3737
4068
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3738
4069
  `, "utf-8");
3739
4070
  }
3740
4071
 
3741
4072
  // src/cli/commands/validate.ts
3742
4073
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3743
- import path19 from "path";
4074
+ import path20 from "path";
3744
4075
 
3745
4076
  // src/cli/lib/failOn.ts
3746
4077
  function shouldFail(result, failOn) {
@@ -3755,7 +4086,7 @@ function shouldFail(result, failOn) {
3755
4086
 
3756
4087
  // src/cli/commands/validate.ts
3757
4088
  async function runValidate(options) {
3758
- const root = path19.resolve(options.root);
4089
+ const root = path20.resolve(options.root);
3759
4090
  const configResult = await loadConfig(root);
3760
4091
  const result = await validateProject(root, configResult);
3761
4092
  const normalized = normalizeValidationResult(root, result);
@@ -3879,12 +4210,12 @@ function issueKey(issue7) {
3879
4210
  }
3880
4211
  async function emitJson(result, root, jsonPath) {
3881
4212
  const abs = resolveJsonPath(root, jsonPath);
3882
- await mkdir4(path19.dirname(abs), { recursive: true });
4213
+ await mkdir4(path20.dirname(abs), { recursive: true });
3883
4214
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
3884
4215
  `, "utf-8");
3885
4216
  }
3886
4217
  function resolveJsonPath(root, jsonPath) {
3887
- return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
4218
+ return path20.isAbsolute(jsonPath) ? jsonPath : path20.resolve(root, jsonPath);
3888
4219
  }
3889
4220
  var GITHUB_ANNOTATION_LIMIT = 100;
3890
4221