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/README.md +5 -5
- package/assets/init/.qfai/README.md +2 -2
- package/assets/init/.qfai/contracts/README.md +1 -1
- package/assets/init/.qfai/prompts/makeBusinessFlow.md +1 -1
- package/assets/init/.qfai/prompts/makeOverview.md +1 -1
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +1 -1
- package/assets/init/.qfai/prompts/require-to-spec.md +2 -2
- package/assets/init/.qfai/samples/analyze/analysis.md +1 -1
- package/assets/init/.qfai/specs/README.md +2 -2
- package/assets/init/.qfai/specs/spec-0001/delta.md +1 -1
- package/dist/cli/index.cjs +170 -37
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +170 -37
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +107 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.mjs +107 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- /package/assets/init/.qfai/specs/spec-0001/{scenario.md → scenario.feature} +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1136
|
+
let scanResult;
|
|
1102
1137
|
try {
|
|
1103
|
-
|
|
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:
|
|
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.
|
|
1184
|
-
return "1.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.
|
|
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}
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`- ${
|
|
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
|
}
|