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.
- package/README.md +8 -7
- package/dist/cli/index.cjs +166 -32
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +166 -32
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +103 -20
- 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 +103 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pnpm の場合(推奨):
|
|
|
39
39
|
pnpm add -D qfai
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
**必要環境**: Node.js >= 18
|
|
42
|
+
**必要環境**: Node.js >= 18.0.0(Supported) / Tested: Node.js 18, 20 / Recommended: Node.js 20 LTS 以上
|
|
43
43
|
|
|
44
44
|
## パッケージ
|
|
45
45
|
|
|
@@ -62,12 +62,12 @@ npx qfai report
|
|
|
62
62
|
- `npx qfai doctor` による設定/探索/パス/glob/validate.json の事前診断
|
|
63
63
|
- `npx qfai report` によるレポート出力
|
|
64
64
|
|
|
65
|
-
補足:
|
|
65
|
+
補足: v1.x は日本語テンプレ中心で提供します。将来は英語を正本、日本語を別ドキュメントに切り替える方針です。
|
|
66
66
|
|
|
67
67
|
## 使い方(CLI)
|
|
68
68
|
|
|
69
69
|
`validate` は `--fail-on` / `--strict` によって CI ゲート化できます。`validate` は常に `.qfai/out/validate.json`(`output.validateJsonPath`)へ JSON を出力します。`--format` は画面表示(text/github)のみを制御します。`--format github` はアノテーションの上限と重複排除を行い、先頭にサマリを出します(全量は `validate.json` か `--format text` を参照)。
|
|
70
|
-
`report` は `.qfai/out/validate.json` を既定入力とし、`--in` で上書きできます(優先順位: CLI > config)。`--run-validate` を指定すると validate を実行してから report を生成します。出力先は `--out` で変更できます(`--format json` の場合は `.qfai/out/report.json`)。
|
|
70
|
+
`report` は `.qfai/out/validate.json` を既定入力とし、`--in` で上書きできます(優先順位: CLI > config)。`--run-validate` を指定すると validate を実行してから report を生成します。出力先は `--out` で変更できます(`--format json` の場合は `.qfai/out/report.json`)。`--base-url <url>` を指定すると、report.md 内の相対パスをリンク化します(例: `npx qfai report --base-url https://example.com/repo`)。
|
|
71
71
|
`doctor` は validate/report の前段で設定/探索/パス/glob/validate.json を診断します。`--format text|json`、`--out` をサポートし、診断のみ(修復はしません)。`--fail-on warning|error` を指定すると該当 severity 以上で exit 1(未指定は常に exit 0)になります。
|
|
72
72
|
|
|
73
73
|
### Prompts Overlay(v0.7 以降の方針)
|
|
@@ -78,7 +78,8 @@ QFAI が提供するプロンプト資産は次の 2 つに分離します。
|
|
|
78
78
|
- `.qfai/prompts.local/**`: 利用者カスタム資産(QFAI はここを上書きしない)
|
|
79
79
|
|
|
80
80
|
同じ相対パスのファイルがある場合は `.qfai/prompts.local` を優先して参照する運用とします。
|
|
81
|
-
|
|
81
|
+
|
|
82
|
+
`report.json` / `doctor.json` は内部表現で互換非保証です。外部連携は `report.md` など Markdown 出力を推奨します。破壊的変更は SemVer で管理しますが、JSON schema を固定する約束はしません。短い例:
|
|
82
83
|
|
|
83
84
|
```json
|
|
84
85
|
{
|
|
@@ -100,7 +101,7 @@ qfai doctor: root=. config=qfai.config.yaml (found)
|
|
|
100
101
|
summary: ok=10 info=1 warning=2 error=0
|
|
101
102
|
```
|
|
102
103
|
|
|
103
|
-
doctor の JSON
|
|
104
|
+
doctor の JSON 例:
|
|
104
105
|
|
|
105
106
|
```json
|
|
106
107
|
{
|
|
@@ -123,7 +124,7 @@ doctor の JSON も非契約(内部形式。将来予告なく変更あり)
|
|
|
123
124
|
|
|
124
125
|
## analyze(意味矛盾のレビュー補助)
|
|
125
126
|
|
|
126
|
-
`validate` は deterministic な構造矛盾(参照/フォーマット/トレーサビリティ)を検査し、CI Hard Gate にできます。一方で、**意味矛盾(解釈/前提/用語/例外/受入条件の齟齬)**は deterministic に検出できないため、
|
|
127
|
+
`validate` は deterministic な構造矛盾(参照/フォーマット/トレーサビリティ)を検査し、CI Hard Gate にできます。一方で、**意味矛盾(解釈/前提/用語/例外/受入条件の齟齬)**は deterministic に検出できないため、v1.0 では **手動プロンプト**として導線を提供します。
|
|
127
128
|
|
|
128
129
|
重要:
|
|
129
130
|
|
|
@@ -301,4 +302,4 @@ pnpm test:assets
|
|
|
301
302
|
|
|
302
303
|
## ライセンス
|
|
303
304
|
|
|
304
|
-
[MIT](
|
|
305
|
+
[MIT](./LICENSE)
|
package/dist/cli/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
39
39
|
"tmp",
|
|
40
40
|
".mcp-tools"
|
|
41
41
|
]);
|
|
42
|
+
var DEFAULT_GLOB_FILE_LIMIT = 2e4;
|
|
42
43
|
async function collectFiles(root, options = {}) {
|
|
43
44
|
const entries = [];
|
|
44
45
|
if (!await exists(root)) {
|
|
@@ -53,16 +54,29 @@ async function collectFiles(root, options = {}) {
|
|
|
53
54
|
return entries;
|
|
54
55
|
}
|
|
55
56
|
async function collectFilesByGlobs(root, options) {
|
|
57
|
+
const limit = normalizeLimit(options.limit);
|
|
56
58
|
if (options.globs.length === 0) {
|
|
57
|
-
return [];
|
|
59
|
+
return { files: [], truncated: false, matchedFileCount: 0, limit };
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
const stream = import_fast_glob.default.stream(options.globs, {
|
|
60
62
|
cwd: root,
|
|
61
63
|
ignore: options.ignore ?? [],
|
|
62
64
|
onlyFiles: true,
|
|
63
65
|
absolute: true,
|
|
64
66
|
unique: true
|
|
65
67
|
});
|
|
68
|
+
const files = [];
|
|
69
|
+
let truncated = false;
|
|
70
|
+
for await (const entry of stream) {
|
|
71
|
+
if (files.length >= limit) {
|
|
72
|
+
truncated = true;
|
|
73
|
+
destroyStream(stream);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
files.push(String(entry));
|
|
77
|
+
}
|
|
78
|
+
const matchedFileCount = files.length;
|
|
79
|
+
return { files, truncated, matchedFileCount, limit };
|
|
66
80
|
}
|
|
67
81
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
68
82
|
const items = await (0, import_promises.readdir)(current, { withFileTypes: true });
|
|
@@ -94,6 +108,25 @@ async function exists(target) {
|
|
|
94
108
|
return false;
|
|
95
109
|
}
|
|
96
110
|
}
|
|
111
|
+
function normalizeLimit(value) {
|
|
112
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
113
|
+
return DEFAULT_GLOB_FILE_LIMIT;
|
|
114
|
+
}
|
|
115
|
+
const flooredValue = Math.floor(value);
|
|
116
|
+
if (flooredValue <= 0) {
|
|
117
|
+
return DEFAULT_GLOB_FILE_LIMIT;
|
|
118
|
+
}
|
|
119
|
+
return flooredValue;
|
|
120
|
+
}
|
|
121
|
+
function destroyStream(stream) {
|
|
122
|
+
if (!stream || typeof stream !== "object") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const record2 = stream;
|
|
126
|
+
if (typeof record2.destroy === "function") {
|
|
127
|
+
record2.destroy();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
97
130
|
|
|
98
131
|
// src/cli/commands/analyze.ts
|
|
99
132
|
async function runAnalyze(options) {
|
|
@@ -909,15 +942,18 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
909
942
|
scan: {
|
|
910
943
|
globs: normalizedGlobs,
|
|
911
944
|
excludeGlobs: mergedExcludeGlobs,
|
|
912
|
-
matchedFileCount: 0
|
|
945
|
+
matchedFileCount: 0,
|
|
946
|
+
truncated: false,
|
|
947
|
+
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
913
948
|
}
|
|
914
949
|
};
|
|
915
950
|
}
|
|
916
|
-
let
|
|
951
|
+
let scanResult;
|
|
917
952
|
try {
|
|
918
|
-
|
|
953
|
+
scanResult = await collectFilesByGlobs(root, {
|
|
919
954
|
globs: normalizedGlobs,
|
|
920
|
-
ignore: mergedExcludeGlobs
|
|
955
|
+
ignore: mergedExcludeGlobs,
|
|
956
|
+
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
921
957
|
});
|
|
922
958
|
} catch (error2) {
|
|
923
959
|
return {
|
|
@@ -925,13 +961,15 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
925
961
|
scan: {
|
|
926
962
|
globs: normalizedGlobs,
|
|
927
963
|
excludeGlobs: mergedExcludeGlobs,
|
|
928
|
-
matchedFileCount: 0
|
|
964
|
+
matchedFileCount: 0,
|
|
965
|
+
truncated: false,
|
|
966
|
+
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
929
967
|
},
|
|
930
968
|
error: formatError3(error2)
|
|
931
969
|
};
|
|
932
970
|
}
|
|
933
971
|
const normalizedFiles = Array.from(
|
|
934
|
-
new Set(files.map((file) => import_node_path6.default.normalize(file)))
|
|
972
|
+
new Set(scanResult.files.map((file) => import_node_path6.default.normalize(file)))
|
|
935
973
|
);
|
|
936
974
|
for (const file of normalizedFiles) {
|
|
937
975
|
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
@@ -950,7 +988,9 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
950
988
|
scan: {
|
|
951
989
|
globs: normalizedGlobs,
|
|
952
990
|
excludeGlobs: mergedExcludeGlobs,
|
|
953
|
-
matchedFileCount:
|
|
991
|
+
matchedFileCount: scanResult.matchedFileCount,
|
|
992
|
+
truncated: scanResult.truncated,
|
|
993
|
+
limit: scanResult.limit
|
|
954
994
|
}
|
|
955
995
|
};
|
|
956
996
|
}
|
|
@@ -1120,8 +1160,8 @@ var import_promises8 = require("fs/promises");
|
|
|
1120
1160
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
1121
1161
|
var import_node_url2 = require("url");
|
|
1122
1162
|
async function resolveToolVersion() {
|
|
1123
|
-
if ("0.
|
|
1124
|
-
return "0.
|
|
1163
|
+
if ("1.0.1".length > 0) {
|
|
1164
|
+
return "1.0.1";
|
|
1125
1165
|
}
|
|
1126
1166
|
try {
|
|
1127
1167
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1332,19 +1372,27 @@ async function createDoctorData(options) {
|
|
|
1332
1372
|
...config.validation.traceability.testFileExcludeGlobs
|
|
1333
1373
|
]);
|
|
1334
1374
|
try {
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1375
|
+
const scanResult = globs.length === 0 ? {
|
|
1376
|
+
files: [],
|
|
1377
|
+
truncated: false,
|
|
1378
|
+
matchedFileCount: 0,
|
|
1379
|
+
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
1380
|
+
} : await collectFilesByGlobs(root, { globs, ignore: exclude });
|
|
1381
|
+
const matchedCount = scanResult.matchedFileCount;
|
|
1382
|
+
const truncated = scanResult.truncated;
|
|
1383
|
+
const severity = globs.length === 0 ? "warning" : truncated ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
|
|
1338
1384
|
addCheck(checks, {
|
|
1339
1385
|
id: "traceability.testGlobs",
|
|
1340
1386
|
severity,
|
|
1341
1387
|
title: "Test file globs",
|
|
1342
|
-
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `
|
|
1388
|
+
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : truncated ? `fileCount=${matchedCount} (truncated, limit=${scanResult.limit})` : `fileCount=${matchedCount}`,
|
|
1343
1389
|
details: {
|
|
1344
1390
|
globs,
|
|
1345
1391
|
excludeGlobs: exclude,
|
|
1346
1392
|
scenarioFiles: scenarioFiles.length,
|
|
1347
|
-
scMustHaveTest: config.validation.traceability.scMustHaveTest
|
|
1393
|
+
scMustHaveTest: config.validation.traceability.scMustHaveTest,
|
|
1394
|
+
truncated,
|
|
1395
|
+
limit: scanResult.limit
|
|
1348
1396
|
}
|
|
1349
1397
|
});
|
|
1350
1398
|
} catch (error2) {
|
|
@@ -1353,7 +1401,12 @@ async function createDoctorData(options) {
|
|
|
1353
1401
|
severity: "error",
|
|
1354
1402
|
title: "Test file globs",
|
|
1355
1403
|
message: "Glob scan failed (invalid pattern or filesystem error)",
|
|
1356
|
-
details: {
|
|
1404
|
+
details: {
|
|
1405
|
+
globs,
|
|
1406
|
+
excludeGlobs: exclude,
|
|
1407
|
+
limit: DEFAULT_GLOB_FILE_LIMIT,
|
|
1408
|
+
error: String(error2)
|
|
1409
|
+
}
|
|
1357
1410
|
});
|
|
1358
1411
|
}
|
|
1359
1412
|
return {
|
|
@@ -1387,8 +1440,10 @@ async function buildOutDirCollisionCheck(root) {
|
|
|
1387
1440
|
(collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
|
|
1388
1441
|
).sort((a, b) => a.localeCompare(b))
|
|
1389
1442
|
})).sort((a, b) => a.outDir.localeCompare(b.outDir));
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1443
|
+
const truncated = result.scan.truncated;
|
|
1444
|
+
const severity = collisions.length > 0 || truncated ? "warning" : "ok";
|
|
1445
|
+
const messageBase = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
|
|
1446
|
+
const message = truncated ? `${messageBase}; scan truncated (collected=${result.scan.matchedFileCount}, limit=${result.scan.limit})` : messageBase;
|
|
1392
1447
|
return {
|
|
1393
1448
|
id: "output.outDirCollision",
|
|
1394
1449
|
severity,
|
|
@@ -1397,7 +1452,8 @@ async function buildOutDirCollisionCheck(root) {
|
|
|
1397
1452
|
details: {
|
|
1398
1453
|
monorepoRoot: relativeRoot,
|
|
1399
1454
|
configRoots,
|
|
1400
|
-
collisions
|
|
1455
|
+
collisions,
|
|
1456
|
+
scan: result.scan
|
|
1401
1457
|
}
|
|
1402
1458
|
};
|
|
1403
1459
|
} catch (error2) {
|
|
@@ -1412,10 +1468,11 @@ async function buildOutDirCollisionCheck(root) {
|
|
|
1412
1468
|
}
|
|
1413
1469
|
async function detectOutDirCollisions(root) {
|
|
1414
1470
|
const monorepoRoot = await findMonorepoRoot(root);
|
|
1415
|
-
const
|
|
1471
|
+
const configScan = await collectFilesByGlobs(monorepoRoot, {
|
|
1416
1472
|
globs: ["**/qfai.config.yaml"],
|
|
1417
1473
|
ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
|
|
1418
1474
|
});
|
|
1475
|
+
const configPaths = configScan.files;
|
|
1419
1476
|
const configRoots = Array.from(
|
|
1420
1477
|
new Set(configPaths.map((configPath) => import_node_path10.default.dirname(configPath)))
|
|
1421
1478
|
).sort((a, b) => a.localeCompare(b));
|
|
@@ -1436,7 +1493,16 @@ async function detectOutDirCollisions(root) {
|
|
|
1436
1493
|
});
|
|
1437
1494
|
}
|
|
1438
1495
|
}
|
|
1439
|
-
return {
|
|
1496
|
+
return {
|
|
1497
|
+
monorepoRoot,
|
|
1498
|
+
configRoots,
|
|
1499
|
+
collisions,
|
|
1500
|
+
scan: {
|
|
1501
|
+
truncated: configScan.truncated,
|
|
1502
|
+
matchedFileCount: configScan.matchedFileCount,
|
|
1503
|
+
limit: configScan.limit
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1440
1506
|
}
|
|
1441
1507
|
async function findMonorepoRoot(startDir) {
|
|
1442
1508
|
let current = import_node_path10.default.resolve(startDir);
|
|
@@ -3543,13 +3609,14 @@ async function createReportData(root, validation, configResult) {
|
|
|
3543
3609
|
issues: normalizedValidation.issues
|
|
3544
3610
|
};
|
|
3545
3611
|
}
|
|
3546
|
-
function formatReportMarkdown(data) {
|
|
3612
|
+
function formatReportMarkdown(data, options = {}) {
|
|
3547
3613
|
const lines = [];
|
|
3614
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
3548
3615
|
lines.push("# QFAI Report");
|
|
3549
3616
|
lines.push("");
|
|
3550
3617
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
3551
|
-
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
3552
|
-
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
3618
|
+
lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
|
|
3619
|
+
lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
|
|
3553
3620
|
lines.push(`- \u7248: ${data.version}`);
|
|
3554
3621
|
lines.push("");
|
|
3555
3622
|
const severityOrder = {
|
|
@@ -3688,8 +3755,7 @@ function formatReportMarkdown(data) {
|
|
|
3688
3755
|
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
3689
3756
|
);
|
|
3690
3757
|
if (item.file) {
|
|
3691
|
-
|
|
3692
|
-
out.push(`- file: ${item.file}${loc}`);
|
|
3758
|
+
out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
|
|
3693
3759
|
}
|
|
3694
3760
|
if (item.rule) {
|
|
3695
3761
|
out.push(`- rule: ${item.rule}`);
|
|
@@ -3815,6 +3881,11 @@ function formatReportMarkdown(data) {
|
|
|
3815
3881
|
lines.push(
|
|
3816
3882
|
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
3817
3883
|
);
|
|
3884
|
+
if (data.traceability.testFiles.truncated) {
|
|
3885
|
+
lines.push(
|
|
3886
|
+
`- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3818
3889
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
3819
3890
|
lines.push("- missingIds: (none)");
|
|
3820
3891
|
} else {
|
|
@@ -3824,7 +3895,8 @@ function formatReportMarkdown(data) {
|
|
|
3824
3895
|
if (files.length === 0) {
|
|
3825
3896
|
return id;
|
|
3826
3897
|
}
|
|
3827
|
-
|
|
3898
|
+
const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
|
|
3899
|
+
return `${id} (${formattedFiles.join(", ")})`;
|
|
3828
3900
|
});
|
|
3829
3901
|
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
3830
3902
|
}
|
|
@@ -3841,7 +3913,8 @@ function formatReportMarkdown(data) {
|
|
|
3841
3913
|
if (refs.length === 0) {
|
|
3842
3914
|
lines.push(`- ${scId}: (none)`);
|
|
3843
3915
|
} else {
|
|
3844
|
-
|
|
3916
|
+
const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
|
|
3917
|
+
lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
|
|
3845
3918
|
}
|
|
3846
3919
|
}
|
|
3847
3920
|
}
|
|
@@ -3856,8 +3929,9 @@ function formatReportMarkdown(data) {
|
|
|
3856
3929
|
} else {
|
|
3857
3930
|
for (const item of specScIssues) {
|
|
3858
3931
|
const location = item.file ?? "(unknown)";
|
|
3932
|
+
const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
|
|
3859
3933
|
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
3860
|
-
lines.push(`- ${
|
|
3934
|
+
lines.push(`- ${formattedLocation}: ${refs}`);
|
|
3861
3935
|
}
|
|
3862
3936
|
}
|
|
3863
3937
|
lines.push("");
|
|
@@ -3869,7 +3943,7 @@ function formatReportMarkdown(data) {
|
|
|
3869
3943
|
} else {
|
|
3870
3944
|
for (const spot of hotspots) {
|
|
3871
3945
|
lines.push(
|
|
3872
|
-
`- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
|
|
3946
|
+
`- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
|
|
3873
3947
|
);
|
|
3874
3948
|
}
|
|
3875
3949
|
}
|
|
@@ -4026,6 +4100,41 @@ function formatMarkdownTable(headers, rows) {
|
|
|
4026
4100
|
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
4027
4101
|
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
4028
4102
|
}
|
|
4103
|
+
function normalizeBaseUrl(value) {
|
|
4104
|
+
if (!value) {
|
|
4105
|
+
return void 0;
|
|
4106
|
+
}
|
|
4107
|
+
const trimmed = value.trim();
|
|
4108
|
+
if (!trimmed) {
|
|
4109
|
+
return void 0;
|
|
4110
|
+
}
|
|
4111
|
+
return trimmed.replace(/\/+$/, "");
|
|
4112
|
+
}
|
|
4113
|
+
function formatPathLink(value, baseUrl) {
|
|
4114
|
+
if (!baseUrl) {
|
|
4115
|
+
return value;
|
|
4116
|
+
}
|
|
4117
|
+
if (value === ".") {
|
|
4118
|
+
return `[${value}](${baseUrl})`;
|
|
4119
|
+
}
|
|
4120
|
+
const encoded = encodePathForUrl(value);
|
|
4121
|
+
if (!encoded) {
|
|
4122
|
+
return value;
|
|
4123
|
+
}
|
|
4124
|
+
return `[${value}](${baseUrl}/${encoded})`;
|
|
4125
|
+
}
|
|
4126
|
+
function formatPathWithLine(value, loc, baseUrl) {
|
|
4127
|
+
const link = formatPathLink(value, baseUrl);
|
|
4128
|
+
const line = loc?.line ? `:${loc.line}` : "";
|
|
4129
|
+
return `${link}${line}`;
|
|
4130
|
+
}
|
|
4131
|
+
function encodePathForUrl(value) {
|
|
4132
|
+
const normalized = value.replace(/\\/g, "/");
|
|
4133
|
+
if (normalized === ".") {
|
|
4134
|
+
return "";
|
|
4135
|
+
}
|
|
4136
|
+
return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
4137
|
+
}
|
|
4029
4138
|
function toSortedArray2(values) {
|
|
4030
4139
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
4031
4140
|
}
|
|
@@ -4079,6 +4188,16 @@ function buildHotspots(issues) {
|
|
|
4079
4188
|
);
|
|
4080
4189
|
}
|
|
4081
4190
|
|
|
4191
|
+
// src/cli/lib/warnings.ts
|
|
4192
|
+
function warnIfTruncated(scan, context) {
|
|
4193
|
+
if (!scan.truncated) {
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
warn(
|
|
4197
|
+
`[warn] ${context}: file scan truncated: collected ${scan.matchedFileCount} files (limit ${scan.limit})`
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4082
4201
|
// src/cli/commands/report.ts
|
|
4083
4202
|
async function runReport(options) {
|
|
4084
4203
|
const root = import_node_path20.default.resolve(options.root);
|
|
@@ -4122,7 +4241,8 @@ async function runReport(options) {
|
|
|
4122
4241
|
}
|
|
4123
4242
|
}
|
|
4124
4243
|
const data = await createReportData(root, validation, configResult);
|
|
4125
|
-
|
|
4244
|
+
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4245
|
+
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4126
4246
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4127
4247
|
const defaultOut = options.format === "json" ? import_node_path20.default.join(outRoot, "report.json") : import_node_path20.default.join(outRoot, "report.md");
|
|
4128
4248
|
const out = options.outPath ?? defaultOut;
|
|
@@ -4219,6 +4339,7 @@ async function runValidate(options) {
|
|
|
4219
4339
|
const configResult = await loadConfig(root);
|
|
4220
4340
|
const result = await validateProject(root, configResult);
|
|
4221
4341
|
const normalized = normalizeValidationResult(root, result);
|
|
4342
|
+
warnIfTruncated(normalized.traceability.testFiles, "validate");
|
|
4222
4343
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
4223
4344
|
const willFail = shouldFail(normalized, failOn);
|
|
4224
4345
|
const format = options.format ?? "text";
|
|
@@ -4477,6 +4598,17 @@ function parseArgs(argv, cwd) {
|
|
|
4477
4598
|
case "--run-validate":
|
|
4478
4599
|
options.reportRunValidate = true;
|
|
4479
4600
|
break;
|
|
4601
|
+
case "--base-url": {
|
|
4602
|
+
const next = readOptionValue(args, i);
|
|
4603
|
+
if (next === null) {
|
|
4604
|
+
invalid = true;
|
|
4605
|
+
options.help = true;
|
|
4606
|
+
break;
|
|
4607
|
+
}
|
|
4608
|
+
options.reportBaseUrl = next;
|
|
4609
|
+
i += 1;
|
|
4610
|
+
break;
|
|
4611
|
+
}
|
|
4480
4612
|
case "--help":
|
|
4481
4613
|
case "-h":
|
|
4482
4614
|
options.help = true;
|
|
@@ -4573,6 +4705,7 @@ async function run(argv, cwd) {
|
|
|
4573
4705
|
format: options.reportFormat,
|
|
4574
4706
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
4575
4707
|
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
4708
|
+
...options.reportBaseUrl !== void 0 ? { baseUrl: options.reportBaseUrl } : {},
|
|
4576
4709
|
...options.reportRunValidate ? { runValidate: true } : {}
|
|
4577
4710
|
});
|
|
4578
4711
|
}
|
|
@@ -4622,6 +4755,7 @@ Options:
|
|
|
4622
4755
|
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
4623
4756
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
4624
4757
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
4758
|
+
--base-url <url> report: \u30D1\u30B9\u3092\u30EA\u30F3\u30AF\u5316\u3059\u308B\u57FA\u6E96URL
|
|
4625
4759
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
4626
4760
|
`;
|
|
4627
4761
|
}
|