qfai 0.9.2 → 1.0.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.
@@ -16,6 +16,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
16
16
  "tmp",
17
17
  ".mcp-tools"
18
18
  ]);
19
+ var DEFAULT_GLOB_FILE_LIMIT = 2e4;
19
20
  async function collectFiles(root, options = {}) {
20
21
  const entries = [];
21
22
  if (!await exists(root)) {
@@ -30,16 +31,29 @@ async function collectFiles(root, options = {}) {
30
31
  return entries;
31
32
  }
32
33
  async function collectFilesByGlobs(root, options) {
34
+ const limit = normalizeLimit(options.limit);
33
35
  if (options.globs.length === 0) {
34
- return [];
36
+ return { files: [], truncated: false, matchedFileCount: 0, limit };
35
37
  }
36
- return fg(options.globs, {
38
+ const stream = fg.stream(options.globs, {
37
39
  cwd: root,
38
40
  ignore: options.ignore ?? [],
39
41
  onlyFiles: true,
40
42
  absolute: true,
41
43
  unique: true
42
44
  });
45
+ const files = [];
46
+ let truncated = false;
47
+ for await (const entry of stream) {
48
+ if (files.length >= limit) {
49
+ truncated = true;
50
+ destroyStream(stream);
51
+ break;
52
+ }
53
+ files.push(String(entry));
54
+ }
55
+ const matchedFileCount = files.length;
56
+ return { files, truncated, matchedFileCount, limit };
43
57
  }
44
58
  async function walk(base, current, ignoreDirs, extensions, out) {
45
59
  const items = await readdir(current, { withFileTypes: true });
@@ -71,6 +85,25 @@ async function exists(target) {
71
85
  return false;
72
86
  }
73
87
  }
88
+ function normalizeLimit(value) {
89
+ if (typeof value !== "number" || Number.isNaN(value)) {
90
+ return DEFAULT_GLOB_FILE_LIMIT;
91
+ }
92
+ const flooredValue = Math.floor(value);
93
+ if (flooredValue <= 0) {
94
+ return DEFAULT_GLOB_FILE_LIMIT;
95
+ }
96
+ return flooredValue;
97
+ }
98
+ function destroyStream(stream) {
99
+ if (!stream || typeof stream !== "object") {
100
+ return;
101
+ }
102
+ const record2 = stream;
103
+ if (typeof record2.destroy === "function") {
104
+ record2.destroy();
105
+ }
106
+ }
74
107
 
75
108
  // src/cli/commands/analyze.ts
76
109
  async function runAnalyze(options) {
@@ -890,15 +923,18 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
890
923
  scan: {
891
924
  globs: normalizedGlobs,
892
925
  excludeGlobs: mergedExcludeGlobs,
893
- matchedFileCount: 0
926
+ matchedFileCount: 0,
927
+ truncated: false,
928
+ limit: DEFAULT_GLOB_FILE_LIMIT
894
929
  }
895
930
  };
896
931
  }
897
- let files = [];
932
+ let scanResult;
898
933
  try {
899
- files = await collectFilesByGlobs(root, {
934
+ scanResult = await collectFilesByGlobs(root, {
900
935
  globs: normalizedGlobs,
901
- ignore: mergedExcludeGlobs
936
+ ignore: mergedExcludeGlobs,
937
+ limit: DEFAULT_GLOB_FILE_LIMIT
902
938
  });
903
939
  } catch (error2) {
904
940
  return {
@@ -906,13 +942,15 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
906
942
  scan: {
907
943
  globs: normalizedGlobs,
908
944
  excludeGlobs: mergedExcludeGlobs,
909
- matchedFileCount: 0
945
+ matchedFileCount: 0,
946
+ truncated: false,
947
+ limit: DEFAULT_GLOB_FILE_LIMIT
910
948
  },
911
949
  error: formatError3(error2)
912
950
  };
913
951
  }
914
952
  const normalizedFiles = Array.from(
915
- new Set(files.map((file) => path6.normalize(file)))
953
+ new Set(scanResult.files.map((file) => path6.normalize(file)))
916
954
  );
917
955
  for (const file of normalizedFiles) {
918
956
  const text = await readFile3(file, "utf-8");
@@ -931,7 +969,9 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
931
969
  scan: {
932
970
  globs: normalizedGlobs,
933
971
  excludeGlobs: mergedExcludeGlobs,
934
- matchedFileCount: normalizedFiles.length
972
+ matchedFileCount: scanResult.matchedFileCount,
973
+ truncated: scanResult.truncated,
974
+ limit: scanResult.limit
935
975
  }
936
976
  };
937
977
  }
@@ -1101,8 +1141,8 @@ import { readFile as readFile5 } from "fs/promises";
1101
1141
  import path9 from "path";
1102
1142
  import { fileURLToPath as fileURLToPath2 } from "url";
1103
1143
  async function resolveToolVersion() {
1104
- if ("0.9.2".length > 0) {
1105
- return "0.9.2";
1144
+ if ("1.0.1".length > 0) {
1145
+ return "1.0.1";
1106
1146
  }
1107
1147
  try {
1108
1148
  const packagePath = resolvePackageJsonPath();
@@ -1313,19 +1353,27 @@ async function createDoctorData(options) {
1313
1353
  ...config.validation.traceability.testFileExcludeGlobs
1314
1354
  ]);
1315
1355
  try {
1316
- const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1317
- const matchedCount = matched.length;
1318
- const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1356
+ const scanResult = globs.length === 0 ? {
1357
+ files: [],
1358
+ truncated: false,
1359
+ matchedFileCount: 0,
1360
+ limit: DEFAULT_GLOB_FILE_LIMIT
1361
+ } : await collectFilesByGlobs(root, { globs, ignore: exclude });
1362
+ const matchedCount = scanResult.matchedFileCount;
1363
+ const truncated = scanResult.truncated;
1364
+ const severity = globs.length === 0 ? "warning" : truncated ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1319
1365
  addCheck(checks, {
1320
1366
  id: "traceability.testGlobs",
1321
1367
  severity,
1322
1368
  title: "Test file globs",
1323
- message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
1369
+ message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : truncated ? `fileCount=${matchedCount} (truncated, limit=${scanResult.limit})` : `fileCount=${matchedCount}`,
1324
1370
  details: {
1325
1371
  globs,
1326
1372
  excludeGlobs: exclude,
1327
1373
  scenarioFiles: scenarioFiles.length,
1328
- scMustHaveTest: config.validation.traceability.scMustHaveTest
1374
+ scMustHaveTest: config.validation.traceability.scMustHaveTest,
1375
+ truncated,
1376
+ limit: scanResult.limit
1329
1377
  }
1330
1378
  });
1331
1379
  } catch (error2) {
@@ -1334,7 +1382,12 @@ async function createDoctorData(options) {
1334
1382
  severity: "error",
1335
1383
  title: "Test file globs",
1336
1384
  message: "Glob scan failed (invalid pattern or filesystem error)",
1337
- details: { globs, excludeGlobs: exclude, error: String(error2) }
1385
+ details: {
1386
+ globs,
1387
+ excludeGlobs: exclude,
1388
+ limit: DEFAULT_GLOB_FILE_LIMIT,
1389
+ error: String(error2)
1390
+ }
1338
1391
  });
1339
1392
  }
1340
1393
  return {
@@ -1368,8 +1421,10 @@ async function buildOutDirCollisionCheck(root) {
1368
1421
  (collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
1369
1422
  ).sort((a, b) => a.localeCompare(b))
1370
1423
  })).sort((a, b) => a.outDir.localeCompare(b.outDir));
1371
- const severity = collisions.length > 0 ? "warning" : "ok";
1372
- const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1424
+ const truncated = result.scan.truncated;
1425
+ const severity = collisions.length > 0 || truncated ? "warning" : "ok";
1426
+ const messageBase = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1427
+ const message = truncated ? `${messageBase}; scan truncated (collected=${result.scan.matchedFileCount}, limit=${result.scan.limit})` : messageBase;
1373
1428
  return {
1374
1429
  id: "output.outDirCollision",
1375
1430
  severity,
@@ -1378,7 +1433,8 @@ async function buildOutDirCollisionCheck(root) {
1378
1433
  details: {
1379
1434
  monorepoRoot: relativeRoot,
1380
1435
  configRoots,
1381
- collisions
1436
+ collisions,
1437
+ scan: result.scan
1382
1438
  }
1383
1439
  };
1384
1440
  } catch (error2) {
@@ -1393,10 +1449,11 @@ async function buildOutDirCollisionCheck(root) {
1393
1449
  }
1394
1450
  async function detectOutDirCollisions(root) {
1395
1451
  const monorepoRoot = await findMonorepoRoot(root);
1396
- const configPaths = await collectFilesByGlobs(monorepoRoot, {
1452
+ const configScan = await collectFilesByGlobs(monorepoRoot, {
1397
1453
  globs: ["**/qfai.config.yaml"],
1398
1454
  ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1399
1455
  });
1456
+ const configPaths = configScan.files;
1400
1457
  const configRoots = Array.from(
1401
1458
  new Set(configPaths.map((configPath) => path10.dirname(configPath)))
1402
1459
  ).sort((a, b) => a.localeCompare(b));
@@ -1417,7 +1474,16 @@ async function detectOutDirCollisions(root) {
1417
1474
  });
1418
1475
  }
1419
1476
  }
1420
- return { monorepoRoot, configRoots, collisions };
1477
+ return {
1478
+ monorepoRoot,
1479
+ configRoots,
1480
+ collisions,
1481
+ scan: {
1482
+ truncated: configScan.truncated,
1483
+ matchedFileCount: configScan.matchedFileCount,
1484
+ limit: configScan.limit
1485
+ }
1486
+ };
1421
1487
  }
1422
1488
  async function findMonorepoRoot(startDir) {
1423
1489
  let current = path10.resolve(startDir);
@@ -3524,13 +3590,14 @@ async function createReportData(root, validation, configResult) {
3524
3590
  issues: normalizedValidation.issues
3525
3591
  };
3526
3592
  }
3527
- function formatReportMarkdown(data) {
3593
+ function formatReportMarkdown(data, options = {}) {
3528
3594
  const lines = [];
3595
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
3529
3596
  lines.push("# QFAI Report");
3530
3597
  lines.push("");
3531
3598
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
3532
- lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
3533
- lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
3599
+ lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
3600
+ lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
3534
3601
  lines.push(`- \u7248: ${data.version}`);
3535
3602
  lines.push("");
3536
3603
  const severityOrder = {
@@ -3669,8 +3736,7 @@ function formatReportMarkdown(data) {
3669
3736
  `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
3670
3737
  );
3671
3738
  if (item.file) {
3672
- const loc = item.loc?.line ? `:${item.loc.line}` : "";
3673
- out.push(`- file: ${item.file}${loc}`);
3739
+ out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
3674
3740
  }
3675
3741
  if (item.rule) {
3676
3742
  out.push(`- rule: ${item.rule}`);
@@ -3796,6 +3862,11 @@ function formatReportMarkdown(data) {
3796
3862
  lines.push(
3797
3863
  `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
3798
3864
  );
3865
+ if (data.traceability.testFiles.truncated) {
3866
+ lines.push(
3867
+ `- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
3868
+ );
3869
+ }
3799
3870
  if (data.traceability.sc.missingIds.length === 0) {
3800
3871
  lines.push("- missingIds: (none)");
3801
3872
  } else {
@@ -3805,7 +3876,8 @@ function formatReportMarkdown(data) {
3805
3876
  if (files.length === 0) {
3806
3877
  return id;
3807
3878
  }
3808
- return `${id} (${files.join(", ")})`;
3879
+ const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
3880
+ return `${id} (${formattedFiles.join(", ")})`;
3809
3881
  });
3810
3882
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
3811
3883
  }
@@ -3822,7 +3894,8 @@ function formatReportMarkdown(data) {
3822
3894
  if (refs.length === 0) {
3823
3895
  lines.push(`- ${scId}: (none)`);
3824
3896
  } else {
3825
- lines.push(`- ${scId}: ${refs.join(", ")}`);
3897
+ const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
3898
+ lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
3826
3899
  }
3827
3900
  }
3828
3901
  }
@@ -3837,8 +3910,9 @@ function formatReportMarkdown(data) {
3837
3910
  } else {
3838
3911
  for (const item of specScIssues) {
3839
3912
  const location = item.file ?? "(unknown)";
3913
+ const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
3840
3914
  const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
3841
- lines.push(`- ${location}: ${refs}`);
3915
+ lines.push(`- ${formattedLocation}: ${refs}`);
3842
3916
  }
3843
3917
  }
3844
3918
  lines.push("");
@@ -3850,7 +3924,7 @@ function formatReportMarkdown(data) {
3850
3924
  } else {
3851
3925
  for (const spot of hotspots) {
3852
3926
  lines.push(
3853
- `- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3927
+ `- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3854
3928
  );
3855
3929
  }
3856
3930
  }
@@ -4007,6 +4081,41 @@ function formatMarkdownTable(headers, rows) {
4007
4081
  const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
4008
4082
  return [formatRow(headers), separator, ...rows.map(formatRow)];
4009
4083
  }
4084
+ function normalizeBaseUrl(value) {
4085
+ if (!value) {
4086
+ return void 0;
4087
+ }
4088
+ const trimmed = value.trim();
4089
+ if (!trimmed) {
4090
+ return void 0;
4091
+ }
4092
+ return trimmed.replace(/\/+$/, "");
4093
+ }
4094
+ function formatPathLink(value, baseUrl) {
4095
+ if (!baseUrl) {
4096
+ return value;
4097
+ }
4098
+ if (value === ".") {
4099
+ return `[${value}](${baseUrl})`;
4100
+ }
4101
+ const encoded = encodePathForUrl(value);
4102
+ if (!encoded) {
4103
+ return value;
4104
+ }
4105
+ return `[${value}](${baseUrl}/${encoded})`;
4106
+ }
4107
+ function formatPathWithLine(value, loc, baseUrl) {
4108
+ const link = formatPathLink(value, baseUrl);
4109
+ const line = loc?.line ? `:${loc.line}` : "";
4110
+ return `${link}${line}`;
4111
+ }
4112
+ function encodePathForUrl(value) {
4113
+ const normalized = value.replace(/\\/g, "/");
4114
+ if (normalized === ".") {
4115
+ return "";
4116
+ }
4117
+ return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
4118
+ }
4010
4119
  function toSortedArray2(values) {
4011
4120
  return Array.from(values).sort((a, b) => a.localeCompare(b));
4012
4121
  }
@@ -4060,6 +4169,16 @@ function buildHotspots(issues) {
4060
4169
  );
4061
4170
  }
4062
4171
 
4172
+ // src/cli/lib/warnings.ts
4173
+ function warnIfTruncated(scan, context) {
4174
+ if (!scan.truncated) {
4175
+ return;
4176
+ }
4177
+ warn(
4178
+ `[warn] ${context}: file scan truncated: collected ${scan.matchedFileCount} files (limit ${scan.limit})`
4179
+ );
4180
+ }
4181
+
4063
4182
  // src/cli/commands/report.ts
4064
4183
  async function runReport(options) {
4065
4184
  const root = path20.resolve(options.root);
@@ -4103,7 +4222,8 @@ async function runReport(options) {
4103
4222
  }
4104
4223
  }
4105
4224
  const data = await createReportData(root, validation, configResult);
4106
- const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
4225
+ warnIfTruncated(data.traceability.testFiles, "report");
4226
+ const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4107
4227
  const outRoot = resolvePath(root, configResult.config, "outDir");
4108
4228
  const defaultOut = options.format === "json" ? path20.join(outRoot, "report.json") : path20.join(outRoot, "report.md");
4109
4229
  const out = options.outPath ?? defaultOut;
@@ -4200,6 +4320,7 @@ async function runValidate(options) {
4200
4320
  const configResult = await loadConfig(root);
4201
4321
  const result = await validateProject(root, configResult);
4202
4322
  const normalized = normalizeValidationResult(root, result);
4323
+ warnIfTruncated(normalized.traceability.testFiles, "validate");
4203
4324
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
4204
4325
  const willFail = shouldFail(normalized, failOn);
4205
4326
  const format = options.format ?? "text";
@@ -4458,6 +4579,17 @@ function parseArgs(argv, cwd) {
4458
4579
  case "--run-validate":
4459
4580
  options.reportRunValidate = true;
4460
4581
  break;
4582
+ case "--base-url": {
4583
+ const next = readOptionValue(args, i);
4584
+ if (next === null) {
4585
+ invalid = true;
4586
+ options.help = true;
4587
+ break;
4588
+ }
4589
+ options.reportBaseUrl = next;
4590
+ i += 1;
4591
+ break;
4592
+ }
4461
4593
  case "--help":
4462
4594
  case "-h":
4463
4595
  options.help = true;
@@ -4554,6 +4686,7 @@ async function run(argv, cwd) {
4554
4686
  format: options.reportFormat,
4555
4687
  ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
4556
4688
  ...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
4689
+ ...options.reportBaseUrl !== void 0 ? { baseUrl: options.reportBaseUrl } : {},
4557
4690
  ...options.reportRunValidate ? { runValidate: true } : {}
4558
4691
  });
4559
4692
  }
@@ -4603,6 +4736,7 @@ Options:
4603
4736
  --out <path> report/doctor: \u51FA\u529B\u5148
4604
4737
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
4605
4738
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
4739
+ --base-url <url> report: \u30D1\u30B9\u3092\u30EA\u30F3\u30AF\u5316\u3059\u308B\u57FA\u6E96URL
4606
4740
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
4607
4741
  `;
4608
4742
  }