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 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` は非契約(experimental / internal)として扱います。外部 consumer は依存しないでください。フィールドは例であり固定ではありません。短い例:
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.md
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.md` の `# QFAI-CONTRACT-REF` で宣言する(none 可)
20
+ - Scenario→Contract は `scenario.feature` の `# QFAI-CONTRACT-REF` で宣言する(none 可)
21
21
 
22
22
  ## ディレクトリ概要
23
23
 
24
- - `specs/` : Spec Pack(spec.md / delta.md / scenario.md
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.md に `# QFAI-CONTRACT-REF` を必ず1行以上宣言、0件は `none`)
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.md` / `delta.md` も補助参照)
8
+ - 対象: `.qfai/specs/spec-*/spec.md` 形式のファイル(Spec Pack の `spec.md`。必要に応じて `scenario.feature` / `delta.md` も補助参照)
9
9
 
10
10
  ## Output (Option)
11
11
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Inputs
6
6
 
7
- - 対象: `.qfai/specs/spec-*/spec.md` 形式のファイル(Spec Pack の `spec.md`。必要に応じて `scenario.md` / `delta.md` も補助参照)
7
+ - 対象: `.qfai/specs/spec-*/spec.md` 形式のファイル(Spec Pack の `spec.md`。必要に応じて `scenario.feature` / `delta.md` も補助参照)
8
8
 
9
9
  ## Output
10
10
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## 必須入力
11
11
 
12
- - `.qfai/specs/**/spec.md` / `scenario.md` / `delta.md`
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.md`
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.md` は Gherkin で書き、Feature に `@SPEC-xxxx` を付与する。
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 で参照する場合はタグまたは本文に明示する。
@@ -12,7 +12,7 @@
12
12
  ## 入力(貼り付けたもの)
13
13
 
14
14
  - Spec: <spec.md / delta.md の該当範囲>
15
- - Scenario: <scenario.md の該当範囲>
15
+ - Scenario: <scenario.feature の該当範囲>
16
16
  - Contract(任意): <該当契約>
17
17
  - validate/report 要約: <report.md または validate.json の要約>
18
18
  - 差分: <PR diff / 変更ファイル一覧>
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `spec-0001/spec.md`(必須)
8
8
  - `spec-0001/delta.md`(必須)
9
- - `spec-0001/scenario.md`(必須・Gherkin)
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.md)最小要件
45
+ ## Scenario(scenario.feature)最小要件
46
46
 
47
47
  - **Gherkin 記法**(Given/When/Then)
48
48
  - **1ファイル = 1 Scenario**(Scenario Outline 含む)
@@ -16,7 +16,7 @@
16
16
  ## 影響範囲(Impact)
17
17
 
18
18
  - 仕様(spec.md):
19
- - シナリオ(scenario.md):
19
+ - シナリオ(scenario.feature):
20
20
  - 契約(contracts):
21
21
  - 実装(src):
22
22
  - テスト(tests):
@@ -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
- return (0, import_fast_glob.default)(options.globs, {
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.md")
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 files = [];
951
+ let scanResult;
917
952
  try {
918
- files = await collectFilesByGlobs(root, {
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: normalizedFiles.length
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.0".length > 0) {
1124
- return "1.0.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 matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1336
- const matchedCount = matched.length;
1337
- const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
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)" : `matchedFileCount=${matchedCount}`,
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: { globs, excludeGlobs: exclude, error: String(error2) }
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 severity = collisions.length > 0 ? "warning" : "ok";
1391
- const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
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 configPaths = await collectFilesByGlobs(monorepoRoot, {
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 { monorepoRoot, configRoots, collisions };
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.md";
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} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
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.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
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
- const loc = item.loc?.line ? `:${item.loc.line}` : "";
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
- return `${id} (${files.join(", ")})`;
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
- lines.push(`- ${scId}: ${refs.join(", ")}`);
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(`- ${location}: ${refs}`);
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
- const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
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
  }