qfai 0.6.0 → 0.6.2

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.
package/README.md CHANGED
@@ -28,9 +28,9 @@ npx qfai report
28
28
 
29
29
  `validate` は `--fail-on` / `--strict` によって CI ゲート化できます。`validate` は常に `.qfai/out/validate.json`(`output.validateJsonPath`)へ JSON を出力し、`--format` は表示形式(text/github)のみを制御します。
30
30
  `report` は `.qfai/out/validate.json` を読み、既定で `.qfai/out/report.md` を出力します(`--format json` の場合は `.qfai/out/report.json`)。出力先は `--out` で変更できます。入力パスは固定です。
31
- `doctor` は validate/report の前段で設定/探索/パス/glob/validate.json を診断します。`--format text|json`、`--out` をサポートします。
31
+ `doctor` は validate/report の前段で設定/探索/パス/glob/validate.json を診断します。`--format text|json`、`--out` をサポートします。`--fail-on warning|error` を指定すると該当 severity 以上で exit 1(未指定は常に exit 0)になります。
32
32
  `init --yes` は予約フラグです(現行の init は非対話のため挙動差はありません)。既存ファイルがある場合は `--force` が必要です。
33
- `report.json` は experimental(互換保証なし)として扱います。`reportFormatVersion` を含み、破壊的変更時のみ増分します。
33
+ `report.json` / doctor JSON は experimental(互換保証なし)として扱います。フィールドは例であり固定ではありません。
34
34
 
35
35
  設定はリポジトリ直下の `qfai.config.yaml` で行います。
36
36
  命名規約は `docs/rules/naming.md` を参照してください。
@@ -889,8 +889,8 @@ var import_promises6 = require("fs/promises");
889
889
  var import_node_path6 = __toESM(require("path"), 1);
890
890
  var import_node_url = require("url");
891
891
  async function resolveToolVersion() {
892
- if ("0.6.0".length > 0) {
893
- return "0.6.0";
892
+ if ("0.6.2".length > 0) {
893
+ return "0.6.2";
894
894
  }
895
895
  try {
896
896
  const packagePath = resolvePackageJsonPath();
@@ -1021,6 +1021,22 @@ async function createDoctorData(options) {
1021
1021
  message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
1022
1022
  details: { path: toRelativePath(root, validateJsonAbs) }
1023
1023
  });
1024
+ const outDirAbs = resolvePath(root, config, "outDir");
1025
+ const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
1026
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
1027
+ addCheck(checks, {
1028
+ id: "output.pathAlignment",
1029
+ severity: inside ? "ok" : "warning",
1030
+ title: "Output path alignment",
1031
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1032
+ details: {
1033
+ outDir: toRelativePath(root, outDirAbs),
1034
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1035
+ }
1036
+ });
1037
+ if (options.rootExplicit) {
1038
+ addCheck(checks, await buildOutDirCollisionCheck(root));
1039
+ }
1024
1040
  const scenarioFiles = await collectScenarioFiles(specsRoot);
1025
1041
  const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1026
1042
  const exclude = normalizeGlobs2([
@@ -1052,23 +1068,9 @@ async function createDoctorData(options) {
1052
1068
  details: { globs, excludeGlobs: exclude, error: String(error2) }
1053
1069
  });
1054
1070
  }
1055
- const outDirAbs = resolvePath(root, config, "outDir");
1056
- const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
1057
- const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
1058
- addCheck(checks, {
1059
- id: "output.pathAlignment",
1060
- severity: inside ? "ok" : "warning",
1061
- title: "Output path alignment",
1062
- message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1063
- details: {
1064
- outDir: toRelativePath(root, outDirAbs),
1065
- validateJsonPath: toRelativePath(root, validateJsonAbs)
1066
- }
1067
- });
1068
1071
  return {
1069
1072
  tool: "qfai",
1070
1073
  version,
1071
- doctorFormatVersion: 1,
1072
1074
  generatedAt,
1073
1075
  root: toRelativePath(process.cwd(), root),
1074
1076
  config: {
@@ -1080,6 +1082,90 @@ async function createDoctorData(options) {
1080
1082
  checks
1081
1083
  };
1082
1084
  }
1085
+ var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
1086
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1087
+ "**/.pnpm/**",
1088
+ "**/tmp/**",
1089
+ "**/.mcp-tools/**"
1090
+ ];
1091
+ async function buildOutDirCollisionCheck(root) {
1092
+ try {
1093
+ const result = await detectOutDirCollisions(root);
1094
+ const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
1095
+ const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
1096
+ const collisions = result.collisions.map((item) => ({
1097
+ outDir: toRelativePath(result.monorepoRoot, item.outDir),
1098
+ roots: item.roots.map(
1099
+ (collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
1100
+ ).sort((a, b) => a.localeCompare(b))
1101
+ })).sort((a, b) => a.outDir.localeCompare(b.outDir));
1102
+ const severity = collisions.length > 0 ? "warning" : "ok";
1103
+ const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1104
+ return {
1105
+ id: "output.outDirCollision",
1106
+ severity,
1107
+ title: "OutDir collision",
1108
+ message,
1109
+ details: {
1110
+ monorepoRoot: relativeRoot,
1111
+ configRoots,
1112
+ collisions
1113
+ }
1114
+ };
1115
+ } catch (error2) {
1116
+ return {
1117
+ id: "output.outDirCollision",
1118
+ severity: "error",
1119
+ title: "OutDir collision",
1120
+ message: "OutDir collision scan failed",
1121
+ details: { error: String(error2) }
1122
+ };
1123
+ }
1124
+ }
1125
+ async function detectOutDirCollisions(root) {
1126
+ const monorepoRoot = await findMonorepoRoot(root);
1127
+ const configPaths = await collectFilesByGlobs(monorepoRoot, {
1128
+ globs: ["**/qfai.config.yaml"],
1129
+ ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1130
+ });
1131
+ const configRoots = Array.from(
1132
+ new Set(configPaths.map((configPath) => import_node_path7.default.dirname(configPath)))
1133
+ ).sort((a, b) => a.localeCompare(b));
1134
+ const outDirToRoots = /* @__PURE__ */ new Map();
1135
+ for (const configRoot of configRoots) {
1136
+ const { config } = await loadConfig(configRoot);
1137
+ const outDir = import_node_path7.default.normalize(resolvePath(configRoot, config, "outDir"));
1138
+ const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1139
+ roots.add(configRoot);
1140
+ outDirToRoots.set(outDir, roots);
1141
+ }
1142
+ const collisions = [];
1143
+ for (const [outDir, roots] of outDirToRoots.entries()) {
1144
+ if (roots.size > 1) {
1145
+ collisions.push({
1146
+ outDir,
1147
+ roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
1148
+ });
1149
+ }
1150
+ }
1151
+ return { monorepoRoot, configRoots, collisions };
1152
+ }
1153
+ async function findMonorepoRoot(startDir) {
1154
+ let current = import_node_path7.default.resolve(startDir);
1155
+ while (true) {
1156
+ const gitPath = import_node_path7.default.join(current, ".git");
1157
+ const workspacePath = import_node_path7.default.join(current, "pnpm-workspace.yaml");
1158
+ if (await exists4(gitPath) || await exists4(workspacePath)) {
1159
+ return current;
1160
+ }
1161
+ const parent = import_node_path7.default.dirname(current);
1162
+ if (parent === current) {
1163
+ break;
1164
+ }
1165
+ current = parent;
1166
+ }
1167
+ return import_node_path7.default.resolve(startDir);
1168
+ }
1083
1169
 
1084
1170
  // src/cli/lib/logger.ts
1085
1171
  function info(message) {
@@ -1118,15 +1204,26 @@ async function runDoctor(options) {
1118
1204
  rootExplicit: options.rootExplicit
1119
1205
  });
1120
1206
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1207
+ const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1121
1208
  if (options.outPath) {
1122
1209
  const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
1123
1210
  await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
1124
1211
  await (0, import_promises8.writeFile)(outAbs, `${output}
1125
1212
  `, "utf-8");
1126
1213
  info(`doctor: wrote ${outAbs}`);
1127
- return;
1214
+ return exitCode;
1128
1215
  }
1129
1216
  info(output);
1217
+ return exitCode;
1218
+ }
1219
+ function shouldFailDoctor(summary, failOn) {
1220
+ if (!failOn) {
1221
+ return false;
1222
+ }
1223
+ if (failOn === "error") {
1224
+ return summary.error > 0;
1225
+ }
1226
+ return summary.warning + summary.error > 0;
1130
1227
  }
1131
1228
 
1132
1229
  // src/cli/commands/init.ts
@@ -3021,13 +3118,11 @@ async function createReportData(root, validation, configResult) {
3021
3118
  normalizeScSources(resolvedRoot, scSources)
3022
3119
  );
3023
3120
  const version = await resolveToolVersion();
3024
- const reportFormatVersion = 1;
3025
3121
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
3026
3122
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
3027
3123
  return {
3028
3124
  tool: "qfai",
3029
3125
  version,
3030
- reportFormatVersion,
3031
3126
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3032
3127
  root: displayRoot,
3033
3128
  configPath: displayConfigPath,
@@ -3877,12 +3972,16 @@ async function run(argv, cwd) {
3877
3972
  }
3878
3973
  return;
3879
3974
  case "doctor":
3880
- await runDoctor({
3881
- root: options.root,
3882
- rootExplicit: options.rootExplicit,
3883
- format: options.doctorFormat,
3884
- ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
3885
- });
3975
+ {
3976
+ const exitCode = await runDoctor({
3977
+ root: options.root,
3978
+ rootExplicit: options.rootExplicit,
3979
+ format: options.doctorFormat,
3980
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
3981
+ ...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
3982
+ });
3983
+ process.exitCode = exitCode;
3984
+ }
3886
3985
  return;
3887
3986
  default:
3888
3987
  error(`Unknown command: ${command}`);
@@ -3910,6 +4009,7 @@ Options:
3910
4009
  --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3911
4010
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3912
4011
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
4012
+ --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
3913
4013
  --out <path> report/doctor: \u51FA\u529B\u5148
3914
4014
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3915
4015
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210