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/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
|
|
|
@@ -67,7 +67,7 @@ npx qfai report
|
|
|
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 以降の方針)
|
|
@@ -79,7 +79,7 @@ QFAI が提供するプロンプト資産は次の 2 つに分離します。
|
|
|
79
79
|
|
|
80
80
|
同じ相対パスのファイルがある場合は `.qfai/prompts.local` を優先して参照する運用とします。
|
|
81
81
|
|
|
82
|
-
`report.json`
|
|
82
|
+
`report.json` / `doctor.json` は内部表現で互換非保証です。外部連携は `report.md` など Markdown 出力を推奨します。破壊的変更は原則 SemVer で管理しますが、プロジェクト方針により例外的に minor/patch で破壊的変更を行う場合があります(CHANGELOG に明記)。JSON schema を固定する約束はしません。短い例:
|
|
83
83
|
|
|
84
84
|
```json
|
|
85
85
|
{
|
|
@@ -101,7 +101,7 @@ qfai doctor: root=. config=qfai.config.yaml (found)
|
|
|
101
101
|
summary: ok=10 info=1 warning=2 error=0
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
doctor の JSON
|
|
104
|
+
doctor の JSON 例:
|
|
105
105
|
|
|
106
106
|
```json
|
|
107
107
|
{
|
|
@@ -239,7 +239,7 @@ qfai.config.yaml
|
|
|
239
239
|
spec-0001/
|
|
240
240
|
spec.md
|
|
241
241
|
delta.md
|
|
242
|
-
scenario.
|
|
242
|
+
scenario.feature
|
|
243
243
|
rules/
|
|
244
244
|
conventions.md
|
|
245
245
|
pnpm.md
|
|
@@ -17,11 +17,11 @@ npx qfai report
|
|
|
17
17
|
|
|
18
18
|
- `validation.traceability.testFileGlobs` に一致するテストコードで `QFAI:SC-xxxx` を参照する(コメント可)
|
|
19
19
|
- Spec→Contract は `spec.md` の `QFAI-CONTRACT-REF` 行で宣言する
|
|
20
|
-
- Scenario→Contract は `scenario.
|
|
20
|
+
- Scenario→Contract は `scenario.feature` の `# QFAI-CONTRACT-REF` で宣言する(none 可)
|
|
21
21
|
|
|
22
22
|
## ディレクトリ概要
|
|
23
23
|
|
|
24
|
-
- `specs/` : Spec Pack(spec.md / delta.md / scenario.
|
|
24
|
+
- `specs/` : Spec Pack(spec.md / delta.md / scenario.feature)
|
|
25
25
|
- `contracts/` : UI / API / DB 契約を置く場所
|
|
26
26
|
- `require/` : 既存要件の集約(validate 対象外)
|
|
27
27
|
- `rules/` : 規約・運用ルール
|
|
@@ -63,7 +63,7 @@ CREATE TABLE sample_table (
|
|
|
63
63
|
## 依存関係
|
|
64
64
|
|
|
65
65
|
- Spec → Contracts(spec.md に `QFAI-CONTRACT-REF` を必ず1行以上宣言、0件は `none`。この行が SSOT)
|
|
66
|
-
- Scenario → Contracts(scenario.
|
|
66
|
+
- Scenario → Contracts(scenario.feature に `# QFAI-CONTRACT-REF` を必ず1行以上宣言、0件は `none`)
|
|
67
67
|
|
|
68
68
|
## 良い例 / 悪い例
|
|
69
69
|
|
|
@@ -5,7 +5,7 @@ Spec群を読み、Specに明示されている情報のみを根拠として、
|
|
|
5
5
|
|
|
6
6
|
## Inputs
|
|
7
7
|
|
|
8
|
-
- 対象: `.qfai/specs/spec-*/spec.md` 形式のファイル(Spec Pack の `spec.md`。必要に応じて `scenario.
|
|
8
|
+
- 対象: `.qfai/specs/spec-*/spec.md` 形式のファイル(Spec Pack の `spec.md`。必要に応じて `scenario.feature` / `delta.md` も補助参照)
|
|
9
9
|
|
|
10
10
|
## Output (Option)
|
|
11
11
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## 必須入力
|
|
11
11
|
|
|
12
|
-
- `.qfai/specs/**/spec.md` / `scenario.
|
|
12
|
+
- `.qfai/specs/**/spec.md` / `scenario.feature` / `delta.md`
|
|
13
13
|
- `.qfai/out/validate.json`(あれば)
|
|
14
14
|
- `.qfai/out/report.md`(あれば)
|
|
15
15
|
- テストコード(`validation.traceability.testFileGlobs` に一致する範囲)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
- `.qfai/specs/spec-0001/spec.md`
|
|
13
13
|
- `.qfai/specs/spec-0001/delta.md`
|
|
14
|
-
- `.qfai/specs/spec-0001/scenario.
|
|
14
|
+
- `.qfai/specs/spec-0001/scenario.feature`
|
|
15
15
|
- 必要に応じて `.qfai/contracts/{ui,api,db}` 配下
|
|
16
16
|
|
|
17
17
|
> Spec は `spec-0001` から開始し、必要なら連番で増やすこと。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- `spec.md` の必須セクションは `qfai.config.yaml` の設定に従う。
|
|
24
24
|
- BR は `## 業務ルール` にのみ定義し、`- [BR-0001][P1] ...` 形式で書く。
|
|
25
25
|
- `spec.md` に `QFAI-CONTRACT-REF:` を必ず記載する(不要なら `none`)。
|
|
26
|
-
- `scenario.
|
|
26
|
+
- `scenario.feature` は Gherkin で書き、Feature に `@SPEC-xxxx` を付与する。
|
|
27
27
|
- 各 Scenario は `@SC-xxxx` を **ちょうど1つ**、`@BR-xxxx` を **1つ以上**持つこと。
|
|
28
28
|
- 契約ファイルには `QFAI-CONTRACT-ID: <ID>` を宣言する。
|
|
29
29
|
- 契約 ID(UI/API/DB)を Scenario で参照する場合はタグまたは本文に明示する。
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `spec-0001/spec.md`(必須)
|
|
8
8
|
- `spec-0001/delta.md`(必須)
|
|
9
|
-
- `spec-0001/scenario.
|
|
9
|
+
- `spec-0001/scenario.feature`(必須・Gherkin)
|
|
10
10
|
|
|
11
11
|
> `spec-0001` は **4桁連番**。Spec ID も **4桁(SPEC-0001)** です。
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ QFAI-CONTRACT-REF: UI-0001, API-0001, DB-0001
|
|
|
42
42
|
- 互換維持 / 仕様変更の **どちらか1つ**に必ずチェックする
|
|
43
43
|
- 根拠と影響範囲を明記する
|
|
44
44
|
|
|
45
|
-
## Scenario(scenario.
|
|
45
|
+
## Scenario(scenario.feature)最小要件
|
|
46
46
|
|
|
47
47
|
- **Gherkin 記法**(Given/When/Then)
|
|
48
48
|
- **1ファイル = 1 Scenario**(Scenario Outline 含む)
|
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) {
|
|
@@ -617,7 +650,7 @@ async function collectSpecEntries(specsRoot) {
|
|
|
617
650
|
dir,
|
|
618
651
|
specPath: import_node_path4.default.join(dir, "spec.md"),
|
|
619
652
|
deltaPath: import_node_path4.default.join(dir, "delta.md"),
|
|
620
|
-
scenarioPath: import_node_path4.default.join(dir, "scenario.
|
|
653
|
+
scenarioPath: import_node_path4.default.join(dir, "scenario.feature")
|
|
621
654
|
}));
|
|
622
655
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
623
656
|
}
|
|
@@ -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 ("1.0.
|
|
1124
|
-
return "1.0.
|
|
1163
|
+
if ("1.0.2".length > 0) {
|
|
1164
|
+
return "1.0.2";
|
|
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);
|
|
@@ -2559,12 +2625,11 @@ async function validateScenarios(root, config) {
|
|
|
2559
2625
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2560
2626
|
const entries = await collectSpecEntries(specsRoot);
|
|
2561
2627
|
if (entries.length === 0) {
|
|
2562
|
-
const expected = "spec-0001/scenario.
|
|
2563
|
-
const legacy = "spec-001/scenario.md";
|
|
2628
|
+
const expected = "spec-0001/scenario.feature";
|
|
2564
2629
|
return [
|
|
2565
2630
|
issue4(
|
|
2566
2631
|
"QFAI-SC-000",
|
|
2567
|
-
`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}
|
|
2632
|
+
`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}`,
|
|
2568
2633
|
"info",
|
|
2569
2634
|
specsRoot,
|
|
2570
2635
|
"scenario.files"
|
|
@@ -2581,7 +2646,7 @@ async function validateScenarios(root, config) {
|
|
|
2581
2646
|
issues.push(
|
|
2582
2647
|
issue4(
|
|
2583
2648
|
"QFAI-SC-001",
|
|
2584
|
-
"scenario.
|
|
2649
|
+
"scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2585
2650
|
"error",
|
|
2586
2651
|
entry.scenarioPath,
|
|
2587
2652
|
"scenario.exists"
|
|
@@ -3543,13 +3608,14 @@ async function createReportData(root, validation, configResult) {
|
|
|
3543
3608
|
issues: normalizedValidation.issues
|
|
3544
3609
|
};
|
|
3545
3610
|
}
|
|
3546
|
-
function formatReportMarkdown(data) {
|
|
3611
|
+
function formatReportMarkdown(data, options = {}) {
|
|
3547
3612
|
const lines = [];
|
|
3613
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
3548
3614
|
lines.push("# QFAI Report");
|
|
3549
3615
|
lines.push("");
|
|
3550
3616
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
3551
|
-
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
3552
|
-
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
3617
|
+
lines.push(`- \u30EB\u30FC\u30C8: ${formatPathLink(data.root, baseUrl)}`);
|
|
3618
|
+
lines.push(`- \u8A2D\u5B9A: ${formatPathLink(data.configPath, baseUrl)}`);
|
|
3553
3619
|
lines.push(`- \u7248: ${data.version}`);
|
|
3554
3620
|
lines.push("");
|
|
3555
3621
|
const severityOrder = {
|
|
@@ -3688,8 +3754,7 @@ function formatReportMarkdown(data) {
|
|
|
3688
3754
|
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
3689
3755
|
);
|
|
3690
3756
|
if (item.file) {
|
|
3691
|
-
|
|
3692
|
-
out.push(`- file: ${item.file}${loc}`);
|
|
3757
|
+
out.push(`- file: ${formatPathWithLine(item.file, item.loc, baseUrl)}`);
|
|
3693
3758
|
}
|
|
3694
3759
|
if (item.rule) {
|
|
3695
3760
|
out.push(`- rule: ${item.rule}`);
|
|
@@ -3815,6 +3880,11 @@ function formatReportMarkdown(data) {
|
|
|
3815
3880
|
lines.push(
|
|
3816
3881
|
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
3817
3882
|
);
|
|
3883
|
+
if (data.traceability.testFiles.truncated) {
|
|
3884
|
+
lines.push(
|
|
3885
|
+
`- testFileTruncated: true (limit=${data.traceability.testFiles.limit})`
|
|
3886
|
+
);
|
|
3887
|
+
}
|
|
3818
3888
|
if (data.traceability.sc.missingIds.length === 0) {
|
|
3819
3889
|
lines.push("- missingIds: (none)");
|
|
3820
3890
|
} else {
|
|
@@ -3824,7 +3894,8 @@ function formatReportMarkdown(data) {
|
|
|
3824
3894
|
if (files.length === 0) {
|
|
3825
3895
|
return id;
|
|
3826
3896
|
}
|
|
3827
|
-
|
|
3897
|
+
const formattedFiles = files.map((file) => formatPathLink(file, baseUrl));
|
|
3898
|
+
return `${id} (${formattedFiles.join(", ")})`;
|
|
3828
3899
|
});
|
|
3829
3900
|
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
3830
3901
|
}
|
|
@@ -3841,7 +3912,8 @@ function formatReportMarkdown(data) {
|
|
|
3841
3912
|
if (refs.length === 0) {
|
|
3842
3913
|
lines.push(`- ${scId}: (none)`);
|
|
3843
3914
|
} else {
|
|
3844
|
-
|
|
3915
|
+
const formattedRefs = refs.map((ref) => formatPathLink(ref, baseUrl));
|
|
3916
|
+
lines.push(`- ${scId}: ${formattedRefs.join(", ")}`);
|
|
3845
3917
|
}
|
|
3846
3918
|
}
|
|
3847
3919
|
}
|
|
@@ -3856,8 +3928,9 @@ function formatReportMarkdown(data) {
|
|
|
3856
3928
|
} else {
|
|
3857
3929
|
for (const item of specScIssues) {
|
|
3858
3930
|
const location = item.file ?? "(unknown)";
|
|
3931
|
+
const formattedLocation = location === "(unknown)" ? location : formatPathLink(location, baseUrl);
|
|
3859
3932
|
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
3860
|
-
lines.push(`- ${
|
|
3933
|
+
lines.push(`- ${formattedLocation}: ${refs}`);
|
|
3861
3934
|
}
|
|
3862
3935
|
}
|
|
3863
3936
|
lines.push("");
|
|
@@ -3869,7 +3942,7 @@ function formatReportMarkdown(data) {
|
|
|
3869
3942
|
} else {
|
|
3870
3943
|
for (const spot of hotspots) {
|
|
3871
3944
|
lines.push(
|
|
3872
|
-
`- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
|
|
3945
|
+
`- ${formatPathLink(spot.file, baseUrl)}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
|
|
3873
3946
|
);
|
|
3874
3947
|
}
|
|
3875
3948
|
}
|
|
@@ -4026,6 +4099,41 @@ function formatMarkdownTable(headers, rows) {
|
|
|
4026
4099
|
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
4027
4100
|
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
4028
4101
|
}
|
|
4102
|
+
function normalizeBaseUrl(value) {
|
|
4103
|
+
if (!value) {
|
|
4104
|
+
return void 0;
|
|
4105
|
+
}
|
|
4106
|
+
const trimmed = value.trim();
|
|
4107
|
+
if (!trimmed) {
|
|
4108
|
+
return void 0;
|
|
4109
|
+
}
|
|
4110
|
+
return trimmed.replace(/\/+$/, "");
|
|
4111
|
+
}
|
|
4112
|
+
function formatPathLink(value, baseUrl) {
|
|
4113
|
+
if (!baseUrl) {
|
|
4114
|
+
return value;
|
|
4115
|
+
}
|
|
4116
|
+
if (value === ".") {
|
|
4117
|
+
return `[${value}](${baseUrl})`;
|
|
4118
|
+
}
|
|
4119
|
+
const encoded = encodePathForUrl(value);
|
|
4120
|
+
if (!encoded) {
|
|
4121
|
+
return value;
|
|
4122
|
+
}
|
|
4123
|
+
return `[${value}](${baseUrl}/${encoded})`;
|
|
4124
|
+
}
|
|
4125
|
+
function formatPathWithLine(value, loc, baseUrl) {
|
|
4126
|
+
const link = formatPathLink(value, baseUrl);
|
|
4127
|
+
const line = loc?.line ? `:${loc.line}` : "";
|
|
4128
|
+
return `${link}${line}`;
|
|
4129
|
+
}
|
|
4130
|
+
function encodePathForUrl(value) {
|
|
4131
|
+
const normalized = value.replace(/\\/g, "/");
|
|
4132
|
+
if (normalized === ".") {
|
|
4133
|
+
return "";
|
|
4134
|
+
}
|
|
4135
|
+
return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
4136
|
+
}
|
|
4029
4137
|
function toSortedArray2(values) {
|
|
4030
4138
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
4031
4139
|
}
|
|
@@ -4079,6 +4187,16 @@ function buildHotspots(issues) {
|
|
|
4079
4187
|
);
|
|
4080
4188
|
}
|
|
4081
4189
|
|
|
4190
|
+
// src/cli/lib/warnings.ts
|
|
4191
|
+
function warnIfTruncated(scan, context) {
|
|
4192
|
+
if (!scan.truncated) {
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
warn(
|
|
4196
|
+
`[warn] ${context}: file scan truncated: collected ${scan.matchedFileCount} files (limit ${scan.limit})`
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4082
4200
|
// src/cli/commands/report.ts
|
|
4083
4201
|
async function runReport(options) {
|
|
4084
4202
|
const root = import_node_path20.default.resolve(options.root);
|
|
@@ -4122,7 +4240,8 @@ async function runReport(options) {
|
|
|
4122
4240
|
}
|
|
4123
4241
|
}
|
|
4124
4242
|
const data = await createReportData(root, validation, configResult);
|
|
4125
|
-
|
|
4243
|
+
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4244
|
+
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4126
4245
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4127
4246
|
const defaultOut = options.format === "json" ? import_node_path20.default.join(outRoot, "report.json") : import_node_path20.default.join(outRoot, "report.md");
|
|
4128
4247
|
const out = options.outPath ?? defaultOut;
|
|
@@ -4219,6 +4338,7 @@ async function runValidate(options) {
|
|
|
4219
4338
|
const configResult = await loadConfig(root);
|
|
4220
4339
|
const result = await validateProject(root, configResult);
|
|
4221
4340
|
const normalized = normalizeValidationResult(root, result);
|
|
4341
|
+
warnIfTruncated(normalized.traceability.testFiles, "validate");
|
|
4222
4342
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
4223
4343
|
const willFail = shouldFail(normalized, failOn);
|
|
4224
4344
|
const format = options.format ?? "text";
|
|
@@ -4477,6 +4597,17 @@ function parseArgs(argv, cwd) {
|
|
|
4477
4597
|
case "--run-validate":
|
|
4478
4598
|
options.reportRunValidate = true;
|
|
4479
4599
|
break;
|
|
4600
|
+
case "--base-url": {
|
|
4601
|
+
const next = readOptionValue(args, i);
|
|
4602
|
+
if (next === null) {
|
|
4603
|
+
invalid = true;
|
|
4604
|
+
options.help = true;
|
|
4605
|
+
break;
|
|
4606
|
+
}
|
|
4607
|
+
options.reportBaseUrl = next;
|
|
4608
|
+
i += 1;
|
|
4609
|
+
break;
|
|
4610
|
+
}
|
|
4480
4611
|
case "--help":
|
|
4481
4612
|
case "-h":
|
|
4482
4613
|
options.help = true;
|
|
@@ -4573,6 +4704,7 @@ async function run(argv, cwd) {
|
|
|
4573
4704
|
format: options.reportFormat,
|
|
4574
4705
|
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
4575
4706
|
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
4707
|
+
...options.reportBaseUrl !== void 0 ? { baseUrl: options.reportBaseUrl } : {},
|
|
4576
4708
|
...options.reportRunValidate ? { runValidate: true } : {}
|
|
4577
4709
|
});
|
|
4578
4710
|
}
|
|
@@ -4622,6 +4754,7 @@ Options:
|
|
|
4622
4754
|
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
4623
4755
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
4624
4756
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
4757
|
+
--base-url <url> report: \u30D1\u30B9\u3092\u30EA\u30F3\u30AF\u5316\u3059\u308B\u57FA\u6E96URL
|
|
4625
4758
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
4626
4759
|
`;
|
|
4627
4760
|
}
|