qfai 0.6.0 → 0.6.3

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.
@@ -870,8 +870,8 @@ import { readFile as readFile3 } from "fs/promises";
870
870
  import path6 from "path";
871
871
  import { fileURLToPath } from "url";
872
872
  async function resolveToolVersion() {
873
- if ("0.6.0".length > 0) {
874
- return "0.6.0";
873
+ if ("0.6.3".length > 0) {
874
+ return "0.6.3";
875
875
  }
876
876
  try {
877
877
  const packagePath = resolvePackageJsonPath();
@@ -1002,6 +1002,22 @@ async function createDoctorData(options) {
1002
1002
  message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
1003
1003
  details: { path: toRelativePath(root, validateJsonAbs) }
1004
1004
  });
1005
+ const outDirAbs = resolvePath(root, config, "outDir");
1006
+ const rel = path7.relative(outDirAbs, validateJsonAbs);
1007
+ const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1008
+ addCheck(checks, {
1009
+ id: "output.pathAlignment",
1010
+ severity: inside ? "ok" : "warning",
1011
+ title: "Output path alignment",
1012
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1013
+ details: {
1014
+ outDir: toRelativePath(root, outDirAbs),
1015
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1016
+ }
1017
+ });
1018
+ if (options.rootExplicit) {
1019
+ addCheck(checks, await buildOutDirCollisionCheck(root));
1020
+ }
1005
1021
  const scenarioFiles = await collectScenarioFiles(specsRoot);
1006
1022
  const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1007
1023
  const exclude = normalizeGlobs2([
@@ -1033,23 +1049,9 @@ async function createDoctorData(options) {
1033
1049
  details: { globs, excludeGlobs: exclude, error: String(error2) }
1034
1050
  });
1035
1051
  }
1036
- const outDirAbs = resolvePath(root, config, "outDir");
1037
- const rel = path7.relative(outDirAbs, validateJsonAbs);
1038
- const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1039
- addCheck(checks, {
1040
- id: "output.pathAlignment",
1041
- severity: inside ? "ok" : "warning",
1042
- title: "Output path alignment",
1043
- message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1044
- details: {
1045
- outDir: toRelativePath(root, outDirAbs),
1046
- validateJsonPath: toRelativePath(root, validateJsonAbs)
1047
- }
1048
- });
1049
1052
  return {
1050
1053
  tool: "qfai",
1051
1054
  version,
1052
- doctorFormatVersion: 1,
1053
1055
  generatedAt,
1054
1056
  root: toRelativePath(process.cwd(), root),
1055
1057
  config: {
@@ -1061,6 +1063,90 @@ async function createDoctorData(options) {
1061
1063
  checks
1062
1064
  };
1063
1065
  }
1066
+ var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
1067
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1068
+ "**/.pnpm/**",
1069
+ "**/tmp/**",
1070
+ "**/.mcp-tools/**"
1071
+ ];
1072
+ async function buildOutDirCollisionCheck(root) {
1073
+ try {
1074
+ const result = await detectOutDirCollisions(root);
1075
+ const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
1076
+ const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
1077
+ const collisions = result.collisions.map((item) => ({
1078
+ outDir: toRelativePath(result.monorepoRoot, item.outDir),
1079
+ roots: item.roots.map(
1080
+ (collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
1081
+ ).sort((a, b) => a.localeCompare(b))
1082
+ })).sort((a, b) => a.outDir.localeCompare(b.outDir));
1083
+ const severity = collisions.length > 0 ? "warning" : "ok";
1084
+ const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1085
+ return {
1086
+ id: "output.outDirCollision",
1087
+ severity,
1088
+ title: "OutDir collision",
1089
+ message,
1090
+ details: {
1091
+ monorepoRoot: relativeRoot,
1092
+ configRoots,
1093
+ collisions
1094
+ }
1095
+ };
1096
+ } catch (error2) {
1097
+ return {
1098
+ id: "output.outDirCollision",
1099
+ severity: "error",
1100
+ title: "OutDir collision",
1101
+ message: "OutDir collision scan failed",
1102
+ details: { error: String(error2) }
1103
+ };
1104
+ }
1105
+ }
1106
+ async function detectOutDirCollisions(root) {
1107
+ const monorepoRoot = await findMonorepoRoot(root);
1108
+ const configPaths = await collectFilesByGlobs(monorepoRoot, {
1109
+ globs: ["**/qfai.config.yaml"],
1110
+ ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1111
+ });
1112
+ const configRoots = Array.from(
1113
+ new Set(configPaths.map((configPath) => path7.dirname(configPath)))
1114
+ ).sort((a, b) => a.localeCompare(b));
1115
+ const outDirToRoots = /* @__PURE__ */ new Map();
1116
+ for (const configRoot of configRoots) {
1117
+ const { config } = await loadConfig(configRoot);
1118
+ const outDir = path7.normalize(resolvePath(configRoot, config, "outDir"));
1119
+ const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1120
+ roots.add(configRoot);
1121
+ outDirToRoots.set(outDir, roots);
1122
+ }
1123
+ const collisions = [];
1124
+ for (const [outDir, roots] of outDirToRoots.entries()) {
1125
+ if (roots.size > 1) {
1126
+ collisions.push({
1127
+ outDir,
1128
+ roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
1129
+ });
1130
+ }
1131
+ }
1132
+ return { monorepoRoot, configRoots, collisions };
1133
+ }
1134
+ async function findMonorepoRoot(startDir) {
1135
+ let current = path7.resolve(startDir);
1136
+ while (true) {
1137
+ const gitPath = path7.join(current, ".git");
1138
+ const workspacePath = path7.join(current, "pnpm-workspace.yaml");
1139
+ if (await exists4(gitPath) || await exists4(workspacePath)) {
1140
+ return current;
1141
+ }
1142
+ const parent = path7.dirname(current);
1143
+ if (parent === current) {
1144
+ break;
1145
+ }
1146
+ current = parent;
1147
+ }
1148
+ return path7.resolve(startDir);
1149
+ }
1064
1150
 
1065
1151
  // src/cli/lib/logger.ts
1066
1152
  function info(message) {
@@ -1099,15 +1185,26 @@ async function runDoctor(options) {
1099
1185
  rootExplicit: options.rootExplicit
1100
1186
  });
1101
1187
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1188
+ const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1102
1189
  if (options.outPath) {
1103
1190
  const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
1104
1191
  await mkdir(path8.dirname(outAbs), { recursive: true });
1105
1192
  await writeFile(outAbs, `${output}
1106
1193
  `, "utf-8");
1107
1194
  info(`doctor: wrote ${outAbs}`);
1108
- return;
1195
+ return exitCode;
1109
1196
  }
1110
1197
  info(output);
1198
+ return exitCode;
1199
+ }
1200
+ function shouldFailDoctor(summary, failOn) {
1201
+ if (!failOn) {
1202
+ return false;
1203
+ }
1204
+ if (failOn === "error") {
1205
+ return summary.error > 0;
1206
+ }
1207
+ return summary.warning + summary.error > 0;
1111
1208
  }
1112
1209
 
1113
1210
  // src/cli/commands/init.ts
@@ -3002,13 +3099,11 @@ async function createReportData(root, validation, configResult) {
3002
3099
  normalizeScSources(resolvedRoot, scSources)
3003
3100
  );
3004
3101
  const version = await resolveToolVersion();
3005
- const reportFormatVersion = 1;
3006
3102
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
3007
3103
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
3008
3104
  return {
3009
3105
  tool: "qfai",
3010
3106
  version,
3011
- reportFormatVersion,
3012
3107
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3013
3108
  root: displayRoot,
3014
3109
  configPath: displayConfigPath,
@@ -3858,12 +3953,16 @@ async function run(argv, cwd) {
3858
3953
  }
3859
3954
  return;
3860
3955
  case "doctor":
3861
- await runDoctor({
3862
- root: options.root,
3863
- rootExplicit: options.rootExplicit,
3864
- format: options.doctorFormat,
3865
- ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
3866
- });
3956
+ {
3957
+ const exitCode = await runDoctor({
3958
+ root: options.root,
3959
+ rootExplicit: options.rootExplicit,
3960
+ format: options.doctorFormat,
3961
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
3962
+ ...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
3963
+ });
3964
+ process.exitCode = exitCode;
3965
+ }
3867
3966
  return;
3868
3967
  default:
3869
3968
  error(`Unknown command: ${command}`);
@@ -3891,6 +3990,7 @@ Options:
3891
3990
  --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3892
3991
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3893
3992
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3993
+ --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
3894
3994
  --out <path> report/doctor: \u51FA\u529B\u5148
3895
3995
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3896
3996
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210