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.
package/dist/index.d.cts CHANGED
@@ -9,6 +9,8 @@ type TestFileScan = {
9
9
  globs: string[];
10
10
  excludeGlobs: string[];
11
11
  matchedFileCount: number;
12
+ truncated: boolean;
13
+ limit: number;
12
14
  };
13
15
 
14
16
  type IssueSeverity = "info" | "warning" | "error";
@@ -157,7 +159,10 @@ type ReportData = {
157
159
  issues: Issue[];
158
160
  };
159
161
  declare function createReportData(root: string, validation?: ValidationResult, configResult?: ConfigLoadResult): Promise<ReportData>;
160
- declare function formatReportMarkdown(data: ReportData): string;
162
+ type ReportMarkdownOptions = {
163
+ baseUrl?: string;
164
+ };
165
+ declare function formatReportMarkdown(data: ReportData, options?: ReportMarkdownOptions): string;
161
166
  declare function formatReportJson(data: ReportData): string;
162
167
 
163
168
  declare function validateProject(root: string, configResult?: ConfigLoadResult): Promise<ValidationResult>;
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ type TestFileScan = {
9
9
  globs: string[];
10
10
  excludeGlobs: string[];
11
11
  matchedFileCount: number;
12
+ truncated: boolean;
13
+ limit: number;
12
14
  };
13
15
 
14
16
  type IssueSeverity = "info" | "warning" | "error";
@@ -157,7 +159,10 @@ type ReportData = {
157
159
  issues: Issue[];
158
160
  };
159
161
  declare function createReportData(root: string, validation?: ValidationResult, configResult?: ConfigLoadResult): Promise<ReportData>;
160
- declare function formatReportMarkdown(data: ReportData): string;
162
+ type ReportMarkdownOptions = {
163
+ baseUrl?: string;
164
+ };
165
+ declare function formatReportMarkdown(data: ReportData, options?: ReportMarkdownOptions): string;
161
166
  declare function formatReportJson(data: ReportData): string;
162
167
 
163
168
  declare function validateProject(root: string, configResult?: ConfigLoadResult): Promise<ValidationResult>;
package/dist/index.mjs CHANGED
@@ -474,6 +474,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
474
474
  "tmp",
475
475
  ".mcp-tools"
476
476
  ]);
477
+ var DEFAULT_GLOB_FILE_LIMIT = 2e4;
477
478
  async function collectFiles(root, options = {}) {
478
479
  const entries = [];
479
480
  if (!await exists2(root)) {
@@ -488,16 +489,29 @@ async function collectFiles(root, options = {}) {
488
489
  return entries;
489
490
  }
490
491
  async function collectFilesByGlobs(root, options) {
492
+ const limit = normalizeLimit(options.limit);
491
493
  if (options.globs.length === 0) {
492
- return [];
494
+ return { files: [], truncated: false, matchedFileCount: 0, limit };
493
495
  }
494
- return fg(options.globs, {
496
+ const stream = fg.stream(options.globs, {
495
497
  cwd: root,
496
498
  ignore: options.ignore ?? [],
497
499
  onlyFiles: true,
498
500
  absolute: true,
499
501
  unique: true
500
502
  });
503
+ const files = [];
504
+ let truncated = false;
505
+ for await (const entry of stream) {
506
+ if (files.length >= limit) {
507
+ truncated = true;
508
+ destroyStream(stream);
509
+ break;
510
+ }
511
+ files.push(String(entry));
512
+ }
513
+ const matchedFileCount = files.length;
514
+ return { files, truncated, matchedFileCount, limit };
501
515
  }
502
516
  async function walk(base, current, ignoreDirs, extensions, out) {
503
517
  const items = await readdir(current, { withFileTypes: true });
@@ -529,6 +543,25 @@ async function exists2(target) {
529
543
  return false;
530
544
  }
531
545
  }
546
+ function normalizeLimit(value) {
547
+ if (typeof value !== "number" || Number.isNaN(value)) {
548
+ return DEFAULT_GLOB_FILE_LIMIT;
549
+ }
550
+ const flooredValue = Math.floor(value);
551
+ if (flooredValue <= 0) {
552
+ return DEFAULT_GLOB_FILE_LIMIT;
553
+ }
554
+ return flooredValue;
555
+ }
556
+ function destroyStream(stream) {
557
+ if (!stream || typeof stream !== "object") {
558
+ return;
559
+ }
560
+ const record2 = stream;
561
+ if (typeof record2.destroy === "function") {
562
+ record2.destroy();
563
+ }
564
+ }
532
565
 
533
566
  // src/core/specLayout.ts
534
567
  import { readdir as readdir2 } from "fs/promises";
@@ -540,7 +573,7 @@ async function collectSpecEntries(specsRoot) {
540
573
  dir,
541
574
  specPath: path3.join(dir, "spec.md"),
542
575
  deltaPath: path3.join(dir, "delta.md"),
543
- scenarioPath: path3.join(dir, "scenario.md")
576
+ scenarioPath: path3.join(dir, "scenario.feature")
544
577
  }));
545
578
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
546
579
  }
@@ -1094,15 +1127,18 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1094
1127
  scan: {
1095
1128
  globs: normalizedGlobs,
1096
1129
  excludeGlobs: mergedExcludeGlobs,
1097
- matchedFileCount: 0
1130
+ matchedFileCount: 0,
1131
+ truncated: false,
1132
+ limit: DEFAULT_GLOB_FILE_LIMIT
1098
1133
  }
1099
1134
  };
1100
1135
  }
1101
- let files = [];
1136
+ let scanResult;
1102
1137
  try {
1103
- files = await collectFilesByGlobs(root, {
1138
+ scanResult = await collectFilesByGlobs(root, {
1104
1139
  globs: normalizedGlobs,
1105
- ignore: mergedExcludeGlobs
1140
+ ignore: mergedExcludeGlobs,
1141
+ limit: DEFAULT_GLOB_FILE_LIMIT
1106
1142
  });
1107
1143
  } catch (error) {
1108
1144
  return {
@@ -1110,13 +1146,15 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1110
1146
  scan: {
1111
1147
  globs: normalizedGlobs,
1112
1148
  excludeGlobs: mergedExcludeGlobs,
1113
- matchedFileCount: 0
1149
+ matchedFileCount: 0,
1150
+ truncated: false,
1151
+ limit: DEFAULT_GLOB_FILE_LIMIT
1114
1152
  },
1115
1153
  error: formatError3(error)
1116
1154
  };
1117
1155
  }
1118
1156
  const normalizedFiles = Array.from(
1119
- new Set(files.map((file) => path6.normalize(file)))
1157
+ new Set(scanResult.files.map((file) => path6.normalize(file)))
1120
1158
  );
1121
1159
  for (const file of normalizedFiles) {
1122
1160
  const text = await readFile3(file, "utf-8");
@@ -1135,7 +1173,9 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1135
1173
  scan: {
1136
1174
  globs: normalizedGlobs,
1137
1175
  excludeGlobs: mergedExcludeGlobs,
1138
- matchedFileCount: normalizedFiles.length
1176
+ matchedFileCount: scanResult.matchedFileCount,
1177
+ truncated: scanResult.truncated,
1178
+ limit: scanResult.limit
1139
1179
  }
1140
1180
  };
1141
1181
  }
@@ -1180,8 +1220,8 @@ import { readFile as readFile4 } from "fs/promises";
1180
1220
  import path7 from "path";
1181
1221
  import { fileURLToPath } from "url";
1182
1222
  async function resolveToolVersion() {
1183
- if ("1.0.0".length > 0) {
1184
- return "1.0.0";
1223
+ if ("1.0.2".length > 0) {
1224
+ return "1.0.2";
1185
1225
  }
1186
1226
  try {
1187
1227
  const packagePath = resolvePackageJsonPath();
@@ -1869,12 +1909,11 @@ async function validateScenarios(root, config) {
1869
1909
  const specsRoot = resolvePath(root, config, "specsDir");
1870
1910
  const entries = await collectSpecEntries(specsRoot);
1871
1911
  if (entries.length === 0) {
1872
- const expected = "spec-0001/scenario.md";
1873
- const legacy = "spec-001/scenario.md";
1912
+ const expected = "spec-0001/scenario.feature";
1874
1913
  return [
1875
1914
  issue4(
1876
1915
  "QFAI-SC-000",
1877
- `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)`,
1916
+ `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}`,
1878
1917
  "info",
1879
1918
  specsRoot,
1880
1919
  "scenario.files"
@@ -1891,7 +1930,7 @@ async function validateScenarios(root, config) {
1891
1930
  issues.push(
1892
1931
  issue4(
1893
1932
  "QFAI-SC-001",
1894
- "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1933
+ "scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1895
1934
  "error",
1896
1935
  entry.scenarioPath,
1897
1936
  "scenario.exists"
@@ -2853,13 +2892,14 @@ async function createReportData(root, validation, configResult) {
2853
2892
  issues: normalizedValidation.issues
2854
2893
  };
2855
2894
  }
2856
- function formatReportMarkdown(data) {
2895
+ function formatReportMarkdown(data, options = {}) {
2857
2896
  const lines = [];
2897
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
2858
2898
  lines.push("# QFAI Report");
2859
2899
  lines.push("");
2860
2900
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2861
- lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2862
- lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2901
+ lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
2902
+ lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
2863
2903
  lines.push(`- \u7248: ${data.version}`);
2864
2904
  lines.push("");
2865
2905
  const severityOrder = {
@@ -2998,8 +3038,7 @@ function formatReportMarkdown(data) {
2998
3038
  `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
2999
3039
  );
3000
3040
  if (item.file) {
3001
- const loc = item.loc?.line ? `:${item.loc.line}` : "";
3002
- out.push(`- file: ${item.file}${loc}`);
3041
+ out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
3003
3042
  }
3004
3043
  if (item.rule) {
3005
3044
  out.push(`- rule: ${item.rule}`);
@@ -3125,6 +3164,11 @@ function formatReportMarkdown(data) {
3125
3164
  lines.push(
3126
3165
  `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
3127
3166
  );
3167
+ if (data.traceability.testFiles.truncated) {
3168
+ lines.push(
3169
+ `- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
3170
+ );
3171
+ }
3128
3172
  if (data.traceability.sc.missingIds.length === 0) {
3129
3173
  lines.push("- missingIds: (none)");
3130
3174
  } else {
@@ -3134,7 +3178,8 @@ function formatReportMarkdown(data) {
3134
3178
  if (files.length === 0) {
3135
3179
  return id;
3136
3180
  }
3137
- return `${id} (${files.join(", ")})`;
3181
+ const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
3182
+ return `${id} (${formattedFiles.join(", ")})`;
3138
3183
  });
3139
3184
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
3140
3185
  }
@@ -3151,7 +3196,8 @@ function formatReportMarkdown(data) {
3151
3196
  if (refs.length === 0) {
3152
3197
  lines.push(`- ${scId}: (none)`);
3153
3198
  } else {
3154
- lines.push(`- ${scId}: ${refs.join(", ")}`);
3199
+ const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
3200
+ lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
3155
3201
  }
3156
3202
  }
3157
3203
  }
@@ -3166,8 +3212,9 @@ function formatReportMarkdown(data) {
3166
3212
  } else {
3167
3213
  for (const item of specScIssues) {
3168
3214
  const location = item.file ?? "(unknown)";
3215
+ const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
3169
3216
  const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
3170
- lines.push(`- ${location}: ${refs}`);
3217
+ lines.push(`- ${formattedLocation}: ${refs}`);
3171
3218
  }
3172
3219
  }
3173
3220
  lines.push("");
@@ -3179,7 +3226,7 @@ function formatReportMarkdown(data) {
3179
3226
  } else {
3180
3227
  for (const spot of hotspots) {
3181
3228
  lines.push(
3182
- `- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3229
+ `- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3183
3230
  );
3184
3231
  }
3185
3232
  }
@@ -3336,6 +3383,41 @@ function formatMarkdownTable(headers, rows) {
3336
3383
  const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3337
3384
  return [formatRow(headers), separator, ...rows.map(formatRow)];
3338
3385
  }
3386
+ function normalizeBaseUrl(value) {
3387
+ if (!value) {
3388
+ return void 0;
3389
+ }
3390
+ const trimmed = value.trim();
3391
+ if (!trimmed) {
3392
+ return void 0;
3393
+ }
3394
+ return trimmed.replace(/\/+$/, "");
3395
+ }
3396
+ function formatPathLink(value, baseUrl) {
3397
+ if (!baseUrl) {
3398
+ return value;
3399
+ }
3400
+ if (value === ".") {
3401
+ return `[${value}](${baseUrl})`;
3402
+ }
3403
+ const encoded = encodePathForUrl(value);
3404
+ if (!encoded) {
3405
+ return value;
3406
+ }
3407
+ return `[${value}](${baseUrl}/${encoded})`;
3408
+ }
3409
+ function formatPathWithLine(value, loc, baseUrl) {
3410
+ const link = formatPathLink(value, baseUrl);
3411
+ const line = loc?.line ? `:${loc.line}` : "";
3412
+ return `${link}${line}`;
3413
+ }
3414
+ function encodePathForUrl(value) {
3415
+ const normalized = value.replace(/\\/g, "/");
3416
+ if (normalized === ".") {
3417
+ return "";
3418
+ }
3419
+ return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
3420
+ }
3339
3421
  function toSortedArray2(values) {
3340
3422
  return Array.from(values).sort((a, b) => a.localeCompare(b));
3341
3423
  }