qfai 1.0.0 → 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.
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";
@@ -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.1".length > 0) {
1224
+ return "1.0.1";
1185
1225
  }
1186
1226
  try {
1187
1227
  const packagePath = resolvePackageJsonPath();
@@ -2853,13 +2893,14 @@ async function createReportData(root, validation, configResult) {
2853
2893
  issues: normalizedValidation.issues
2854
2894
  };
2855
2895
  }
2856
- function formatReportMarkdown(data) {
2896
+ function formatReportMarkdown(data, options = {}) {
2857
2897
  const lines = [];
2898
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
2858
2899
  lines.push("# QFAI Report");
2859
2900
  lines.push("");
2860
2901
  lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
2861
- lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
2862
- lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
2902
+ lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
2903
+ lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
2863
2904
  lines.push(`- \u7248: ${data.version}`);
2864
2905
  lines.push("");
2865
2906
  const severityOrder = {
@@ -2998,8 +3039,7 @@ function formatReportMarkdown(data) {
2998
3039
  `#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
2999
3040
  );
3000
3041
  if (item.file) {
3001
- const loc = item.loc?.line ? `:${item.loc.line}` : "";
3002
- out.push(`- file: ${item.file}${loc}`);
3042
+ out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
3003
3043
  }
3004
3044
  if (item.rule) {
3005
3045
  out.push(`- rule: ${item.rule}`);
@@ -3125,6 +3165,11 @@ function formatReportMarkdown(data) {
3125
3165
  lines.push(
3126
3166
  `- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
3127
3167
  );
3168
+ if (data.traceability.testFiles.truncated) {
3169
+ lines.push(
3170
+ `- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
3171
+ );
3172
+ }
3128
3173
  if (data.traceability.sc.missingIds.length === 0) {
3129
3174
  lines.push("- missingIds: (none)");
3130
3175
  } else {
@@ -3134,7 +3179,8 @@ function formatReportMarkdown(data) {
3134
3179
  if (files.length === 0) {
3135
3180
  return id;
3136
3181
  }
3137
- return `${id} (${files.join(", ")})`;
3182
+ const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
3183
+ return `${id} (${formattedFiles.join(", ")})`;
3138
3184
  });
3139
3185
  lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
3140
3186
  }
@@ -3151,7 +3197,8 @@ function formatReportMarkdown(data) {
3151
3197
  if (refs.length === 0) {
3152
3198
  lines.push(`- ${scId}: (none)`);
3153
3199
  } else {
3154
- lines.push(`- ${scId}: ${refs.join(", ")}`);
3200
+ const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
3201
+ lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
3155
3202
  }
3156
3203
  }
3157
3204
  }
@@ -3166,8 +3213,9 @@ function formatReportMarkdown(data) {
3166
3213
  } else {
3167
3214
  for (const item of specScIssues) {
3168
3215
  const location = item.file ?? "(unknown)";
3216
+ const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
3169
3217
  const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
3170
- lines.push(`- ${location}: ${refs}`);
3218
+ lines.push(`- ${formattedLocation}: ${refs}`);
3171
3219
  }
3172
3220
  }
3173
3221
  lines.push("");
@@ -3179,7 +3227,7 @@ function formatReportMarkdown(data) {
3179
3227
  } else {
3180
3228
  for (const spot of hotspots) {
3181
3229
  lines.push(
3182
- `- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3230
+ `- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
3183
3231
  );
3184
3232
  }
3185
3233
  }
@@ -3336,6 +3384,41 @@ function formatMarkdownTable(headers, rows) {
3336
3384
  const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
3337
3385
  return [formatRow(headers), separator, ...rows.map(formatRow)];
3338
3386
  }
3387
+ function normalizeBaseUrl(value) {
3388
+ if (!value) {
3389
+ return void 0;
3390
+ }
3391
+ const trimmed = value.trim();
3392
+ if (!trimmed) {
3393
+ return void 0;
3394
+ }
3395
+ return trimmed.replace(/\/+$/, "");
3396
+ }
3397
+ function formatPathLink(value, baseUrl) {
3398
+ if (!baseUrl) {
3399
+ return value;
3400
+ }
3401
+ if (value === ".") {
3402
+ return `[${value}](${baseUrl})`;
3403
+ }
3404
+ const encoded = encodePathForUrl(value);
3405
+ if (!encoded) {
3406
+ return value;
3407
+ }
3408
+ return `[${value}](${baseUrl}/${encoded})`;
3409
+ }
3410
+ function formatPathWithLine(value, loc, baseUrl) {
3411
+ const link = formatPathLink(value, baseUrl);
3412
+ const line = loc?.line ? `:${loc.line}` : "";
3413
+ return `${link}${line}`;
3414
+ }
3415
+ function encodePathForUrl(value) {
3416
+ const normalized = value.replace(/\\/g, "/");
3417
+ if (normalized === ".") {
3418
+ return "";
3419
+ }
3420
+ return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
3421
+ }
3339
3422
  function toSortedArray2(values) {
3340
3423
  return Array.from(values).sort((a, b) => a.localeCompare(b));
3341
3424
  }