qfai 1.0.0 → 1.0.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.
@@ -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) {
@@ -594,7 +627,7 @@ async function collectSpecEntries(specsRoot) {
594
627
  dir,
595
628
  specPath: path4.join(dir, "spec.md"),
596
629
  deltaPath: path4.join(dir, "delta.md"),
597
- scenarioPath: path4.join(dir, "scenario.md")
630
+ scenarioPath: path4.join(dir, "scenario.feature")
598
631
  }));
599
632
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
600
633
  }
@@ -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 ("1.0.0".length > 0) {
1105
- return "1.0.0";
1144
+ if ("1.0.2".length > 0) {
1145
+ return "1.0.2";
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);
@@ -2540,12 +2606,11 @@ async function validateScenarios(root, config) {
2540
2606
  const specsRoot = resolvePath(root, config, "specsDir");
2541
2607
  const entries = await collectSpecEntries(specsRoot);
2542
2608
  if (entries.length === 0) {
2543
- const expected = "spec-0001/scenario.md";
2544
- const legacy = "spec-001/scenario.md";
2609
+ const expected = "spec-0001/scenario.feature";
2545
2610
  return [
2546
2611
  issue4(
2547
2612
  "QFAI-SC-000",
2548
- `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
2613
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
2549
2614
  "info",
2550
2615
  specsRoot,
2551
2616
  "scenario.files"
@@ -2562,7 +2627,7 @@ async function validateScenarios(root, config) {
2562
2627
  issues.push(
2563
2628
  issue4(
2564
2629
  "QFAI-SC-001",
2565
- "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2630
+ "scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2566
2631
  "error",
2567
2632
  entry.scenarioPath,
2568
2633
  "scenario.exists"
@@ -3524,13 +3589,14 @@ async function createReportData(root, validation, configResult) {
3524
3589
  issues: normalizedValidation.issues
3525
3590
  };
3526
3591
  }
3527
- function formatReportMarkdown(data) {
3592
+ function formatReportMarkdown(data, options = {}) {
3528
3593
  const lines = [];
3594
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
3529
3595
  lines.push("# QFAI Report");
3530
3596
  lines.push("");
3531
3597
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
3532
- lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
3533
- lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
3598
+ lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
3599
+ lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
3534
3600
  lines.push(`- \u7248: ${data.version}`);
3535
3601
  lines.push("");
3536
3602
  const severityOrder = {
@@ -3669,8 +3735,7 @@ function formatReportMarkdown(data) {
3669
3735
  `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
3670
3736
  );
3671
3737
  if (item.file) {
3672
- const loc = item.loc?.line ? `:${item.loc.line}` : "";
3673
- out.push(`- file: ${item.file}${loc}`);
3738
+ out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
3674
3739
  }
3675
3740
  if (item.rule) {
3676
3741
  out.push(`- rule: ${item.rule}`);
@@ -3796,6 +3861,11 @@ function formatReportMarkdown(data) {
3796
3861
  lines.push(
3797
3862
  `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
3798
3863
  );
3864
+ if (data.traceability.testFiles.truncated) {
3865
+ lines.push(
3866
+ `- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
3867
+ );
3868
+ }
3799
3869
  if (data.traceability.sc.missingIds.length === 0) {
3800
3870
  lines.push("- missingIds: (none)");
3801
3871
  } else {
@@ -3805,7 +3875,8 @@ function formatReportMarkdown(data) {
3805
3875
  if (files.length === 0) {
3806
3876
  return id;
3807
3877
  }
3808
- return `${id} (${files.join(", ")})`;
3878
+ const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
3879
+ return `${id} (${formattedFiles.join(", ")})`;
3809
3880
  });
3810
3881
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
3811
3882
  }
@@ -3822,7 +3893,8 @@ function formatReportMarkdown(data) {
3822
3893
  if (refs.length === 0) {
3823
3894
  lines.push(`- ${scId}: (none)`);
3824
3895
  } else {
3825
- lines.push(`- ${scId}: ${refs.join(", ")}`);
3896
+ const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
3897
+ lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
3826
3898
  }
3827
3899
  }
3828
3900
  }
@@ -3837,8 +3909,9 @@ function formatReportMarkdown(data) {
3837
3909
  } else {
3838
3910
  for (const item of specScIssues) {
3839
3911
  const location = item.file ?? "(unknown)";
3912
+ const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
3840
3913
  const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
3841
- lines.push(`- ${location}: ${refs}`);
3914
+ lines.push(`- ${formattedLocation}: ${refs}`);
3842
3915
  }
3843
3916
  }
3844
3917
  lines.push("");
@@ -3850,7 +3923,7 @@ function formatReportMarkdown(data) {
3850
3923
  } else {
3851
3924
  for (const spot of hotspots) {
3852
3925
  lines.push(
3853
- `- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3926
+ `- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3854
3927
  );
3855
3928
  }
3856
3929
  }
@@ -4007,6 +4080,41 @@ function formatMarkdownTable(headers, rows) {
4007
4080
  const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
4008
4081
  return [formatRow(headers), separator, ...rows.map(formatRow)];
4009
4082
  }
4083
+ function normalizeBaseUrl(value) {
4084
+ if (!value) {
4085
+ return void 0;
4086
+ }
4087
+ const trimmed = value.trim();
4088
+ if (!trimmed) {
4089
+ return void 0;
4090
+ }
4091
+ return trimmed.replace(/\/+$/, "");
4092
+ }
4093
+ function formatPathLink(value, baseUrl) {
4094
+ if (!baseUrl) {
4095
+ return value;
4096
+ }
4097
+ if (value === ".") {
4098
+ return `[${value}](${baseUrl})`;
4099
+ }
4100
+ const encoded = encodePathForUrl(value);
4101
+ if (!encoded) {
4102
+ return value;
4103
+ }
4104
+ return `[${value}](${baseUrl}/${encoded})`;
4105
+ }
4106
+ function formatPathWithLine(value, loc, baseUrl) {
4107
+ const link = formatPathLink(value, baseUrl);
4108
+ const line = loc?.line ? `:${loc.line}` : "";
4109
+ return `${link}${line}`;
4110
+ }
4111
+ function encodePathForUrl(value) {
4112
+ const normalized = value.replace(/\\/g, "/");
4113
+ if (normalized === ".") {
4114
+ return "";
4115
+ }
4116
+ return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
4117
+ }
4010
4118
  function toSortedArray2(values) {
4011
4119
  return Array.from(values).sort((a, b) => a.localeCompare(b));
4012
4120
  }
@@ -4060,6 +4168,16 @@ function buildHotspots(issues) {
4060
4168
  );
4061
4169
  }
4062
4170
 
4171
+ // src/cli/lib/warnings.ts
4172
+ function warnIfTruncated(scan, context) {
4173
+ if (!scan.truncated) {
4174
+ return;
4175
+ }
4176
+ warn(
4177
+ `[warn] ${context}: file scan truncated: collected ${scan.matchedFileCount} files (limit ${scan.limit})`
4178
+ );
4179
+ }
4180
+
4063
4181
  // src/cli/commands/report.ts
4064
4182
  async function runReport(options) {
4065
4183
  const root = path20.resolve(options.root);
@@ -4103,7 +4221,8 @@ async function runReport(options) {
4103
4221
  }
4104
4222
  }
4105
4223
  const data = await createReportData(root, validation, configResult);
4106
- const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
4224
+ warnIfTruncated(data.traceability.testFiles, "report");
4225
+ const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4107
4226
  const outRoot = resolvePath(root, configResult.config, "outDir");
4108
4227
  const defaultOut = options.format === "json" ? path20.join(outRoot, "report.json") : path20.join(outRoot, "report.md");
4109
4228
  const out = options.outPath ?? defaultOut;
@@ -4200,6 +4319,7 @@ async function runValidate(options) {
4200
4319
  const configResult = await loadConfig(root);
4201
4320
  const result = await validateProject(root, configResult);
4202
4321
  const normalized = normalizeValidationResult(root, result);
4322
+ warnIfTruncated(normalized.traceability.testFiles, "validate");
4203
4323
  const failOn = resolveFailOn(options, configResult.config.validation.failOn);
4204
4324
  const willFail = shouldFail(normalized, failOn);
4205
4325
  const format = options.format ?? "text";
@@ -4458,6 +4578,17 @@ function parseArgs(argv, cwd) {
4458
4578
  case "--run-validate":
4459
4579
  options.reportRunValidate = true;
4460
4580
  break;
4581
+ case "--base-url": {
4582
+ const next = readOptionValue(args, i);
4583
+ if (next === null) {
4584
+ invalid = true;
4585
+ options.help = true;
4586
+ break;
4587
+ }
4588
+ options.reportBaseUrl = next;
4589
+ i += 1;
4590
+ break;
4591
+ }
4461
4592
  case "--help":
4462
4593
  case "-h":
4463
4594
  options.help = true;
@@ -4554,6 +4685,7 @@ async function run(argv, cwd) {
4554
4685
  format: options.reportFormat,
4555
4686
  ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
4556
4687
  ...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
4688
+ ...options.reportBaseUrl !== void 0 ? { baseUrl: options.reportBaseUrl } : {},
4557
4689
  ...options.reportRunValidate ? { runValidate: true } : {}
4558
4690
  });
4559
4691
  }
@@ -4603,6 +4735,7 @@ Options:
4603
4735
  --out <path> report/doctor: \u51FA\u529B\u5148
4604
4736
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
4605
4737
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
4738
+ --base-url <url> report: \u30D1\u30B9\u3092\u30EA\u30F3\u30AF\u5316\u3059\u308B\u57FA\u6E96URL
4606
4739
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
4607
4740
  `;
4608
4741
  }