qfai 0.5.2 → 0.6.0

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.
@@ -1,169 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli/commands/init.ts
4
- import path3 from "path";
5
-
6
- // src/cli/lib/fs.ts
7
- import { access, copyFile, mkdir, readdir } from "fs/promises";
8
- import path from "path";
9
- async function copyTemplateTree(sourceRoot, destRoot, options) {
10
- const files = await collectTemplateFiles(sourceRoot);
11
- return copyFiles(files, sourceRoot, destRoot, options);
12
- }
13
- async function copyFiles(files, sourceRoot, destRoot, options) {
14
- const copied = [];
15
- const skipped = [];
16
- const conflicts = [];
17
- if (!options.force) {
18
- for (const file of files) {
19
- const relative = path.relative(sourceRoot, file);
20
- const dest = path.join(destRoot, relative);
21
- if (!await shouldWrite(dest, options.force)) {
22
- conflicts.push(dest);
23
- }
24
- }
25
- if (conflicts.length > 0) {
26
- throw new Error(formatConflictMessage(conflicts));
27
- }
28
- }
29
- for (const file of files) {
30
- const relative = path.relative(sourceRoot, file);
31
- const dest = path.join(destRoot, relative);
32
- if (!await shouldWrite(dest, options.force)) {
33
- skipped.push(dest);
34
- continue;
35
- }
36
- if (!options.dryRun) {
37
- await mkdir(path.dirname(dest), { recursive: true });
38
- await copyFile(file, dest);
39
- }
40
- copied.push(dest);
41
- }
42
- return { copied, skipped };
43
- }
44
- function formatConflictMessage(conflicts) {
45
- return [
46
- "\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3068\u885D\u7A81\u3057\u307E\u3057\u305F\u3002\u5B89\u5168\u306E\u305F\u3081\u505C\u6B62\u3057\u307E\u3059\u3002",
47
- "",
48
- "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
49
- ...conflicts.map((conflict) => `- ${conflict}`),
50
- "",
51
- "\u4E0A\u66F8\u304D\u3057\u3066\u7D9A\u884C\u3059\u308B\u5834\u5408\u306F --force \u3092\u4ED8\u3051\u3066\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
52
- ].join("\n");
53
- }
54
- async function collectTemplateFiles(root) {
55
- const entries = [];
56
- if (!await exists(root)) {
57
- return entries;
58
- }
59
- const items = await readdir(root, { withFileTypes: true });
60
- for (const item of items) {
61
- const fullPath = path.join(root, item.name);
62
- if (item.isDirectory()) {
63
- const nested = await collectTemplateFiles(fullPath);
64
- entries.push(...nested);
65
- continue;
66
- }
67
- if (item.isFile()) {
68
- entries.push(fullPath);
69
- }
70
- }
71
- return entries;
72
- }
73
- async function shouldWrite(target, force) {
74
- if (force) {
75
- return true;
76
- }
77
- return !await exists(target);
78
- }
79
- async function exists(target) {
80
- try {
81
- await access(target);
82
- return true;
83
- } catch {
84
- return false;
85
- }
86
- }
87
-
88
- // src/cli/lib/assets.ts
89
- import { existsSync } from "fs";
90
- import path2 from "path";
91
- import { fileURLToPath } from "url";
92
- function getInitAssetsDir() {
93
- const base = import.meta.url;
94
- const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
95
- const baseDir = path2.dirname(basePath);
96
- const candidates = [
97
- path2.resolve(baseDir, "../../../assets/init"),
98
- path2.resolve(baseDir, "../../assets/init")
99
- ];
100
- for (const candidate of candidates) {
101
- if (existsSync(candidate)) {
102
- return candidate;
103
- }
104
- }
105
- throw new Error(
106
- [
107
- "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
108
- "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
109
- ...candidates.map((candidate) => `- ${candidate}`)
110
- ].join("\n")
111
- );
112
- }
113
-
114
- // src/cli/lib/logger.ts
115
- function info(message) {
116
- process.stdout.write(`${message}
117
- `);
118
- }
119
- function warn(message) {
120
- process.stdout.write(`${message}
121
- `);
122
- }
123
- function error(message) {
124
- process.stderr.write(`${message}
125
- `);
126
- }
127
-
128
- // src/cli/commands/init.ts
129
- async function runInit(options) {
130
- const assetsRoot = getInitAssetsDir();
131
- const rootAssets = path3.join(assetsRoot, "root");
132
- const qfaiAssets = path3.join(assetsRoot, ".qfai");
133
- const destRoot = path3.resolve(options.dir);
134
- const destQfai = path3.join(destRoot, ".qfai");
135
- const rootResult = await copyTemplateTree(rootAssets, destRoot, {
136
- force: options.force,
137
- dryRun: options.dryRun
138
- });
139
- const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
140
- force: options.force,
141
- dryRun: options.dryRun
142
- });
143
- report(
144
- [...rootResult.copied, ...qfaiResult.copied],
145
- [...rootResult.skipped, ...qfaiResult.skipped],
146
- options.dryRun,
147
- "init"
148
- );
149
- }
150
- function report(copied, skipped, dryRun, label) {
151
- info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
152
- if (copied.length > 0) {
153
- info(` created: ${copied.length}`);
154
- }
155
- if (skipped.length > 0) {
156
- info(` skipped: ${skipped.length}`);
157
- }
158
- }
3
+ // src/cli/commands/doctor.ts
4
+ import { mkdir, writeFile } from "fs/promises";
5
+ import path8 from "path";
159
6
 
160
- // src/cli/commands/report.ts
161
- import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
162
- import path16 from "path";
7
+ // src/core/doctor.ts
8
+ import { access as access4 } from "fs/promises";
9
+ import path7 from "path";
163
10
 
164
11
  // src/core/config.ts
165
- import { access as access2, readFile } from "fs/promises";
166
- import path4 from "path";
12
+ import { access, readFile } from "fs/promises";
13
+ import path from "path";
167
14
  import { parse as parseYaml } from "yaml";
168
15
  var defaultConfig = {
169
16
  paths: {
@@ -203,17 +50,17 @@ var defaultConfig = {
203
50
  }
204
51
  };
205
52
  function getConfigPath(root) {
206
- return path4.join(root, "qfai.config.yaml");
53
+ return path.join(root, "qfai.config.yaml");
207
54
  }
208
55
  async function findConfigRoot(startDir) {
209
- const resolvedStart = path4.resolve(startDir);
56
+ const resolvedStart = path.resolve(startDir);
210
57
  let current = resolvedStart;
211
58
  while (true) {
212
59
  const configPath = getConfigPath(current);
213
- if (await exists2(configPath)) {
60
+ if (await exists(configPath)) {
214
61
  return { root: current, configPath, found: true };
215
62
  }
216
- const parent = path4.dirname(current);
63
+ const parent = path.dirname(current);
217
64
  if (parent === current) {
218
65
  break;
219
66
  }
@@ -243,7 +90,7 @@ async function loadConfig(root) {
243
90
  return { config: normalized, issues, configPath };
244
91
  }
245
92
  function resolvePath(root, config, key) {
246
- return path4.resolve(root, config.paths[key]);
93
+ return path.resolve(root, config.paths[key]);
247
94
  }
248
95
  function normalizeConfig(raw, configPath, issues) {
249
96
  if (!isRecord(raw)) {
@@ -542,9 +389,9 @@ function isMissingFile(error2) {
542
389
  }
543
390
  return false;
544
391
  }
545
- async function exists2(target) {
392
+ async function exists(target) {
546
393
  try {
547
- await access2(target);
394
+ await access(target);
548
395
  return true;
549
396
  } catch {
550
397
  return false;
@@ -560,76 +407,12 @@ function isRecord(value) {
560
407
  return value !== null && typeof value === "object" && !Array.isArray(value);
561
408
  }
562
409
 
563
- // src/core/paths.ts
564
- import path5 from "path";
565
- function toRelativePath(root, target) {
566
- if (!target) {
567
- return target;
568
- }
569
- if (!path5.isAbsolute(target)) {
570
- return toPosixPath(target);
571
- }
572
- const relative = path5.relative(root, target);
573
- if (!relative) {
574
- return ".";
575
- }
576
- return toPosixPath(relative);
577
- }
578
- function toPosixPath(value) {
579
- return value.replace(/\\/g, "/");
580
- }
581
-
582
- // src/core/normalize.ts
583
- function normalizeIssuePaths(root, issues) {
584
- return issues.map((issue7) => {
585
- if (!issue7.file) {
586
- return issue7;
587
- }
588
- const normalized = toRelativePath(root, issue7.file);
589
- if (normalized === issue7.file) {
590
- return issue7;
591
- }
592
- return {
593
- ...issue7,
594
- file: normalized
595
- };
596
- });
597
- }
598
- function normalizeScCoverage(root, sc) {
599
- const refs = {};
600
- for (const [scId, files] of Object.entries(sc.refs)) {
601
- refs[scId] = files.map((file) => toRelativePath(root, file));
602
- }
603
- return {
604
- ...sc,
605
- refs
606
- };
607
- }
608
- function normalizeValidationResult(root, result) {
609
- return {
610
- ...result,
611
- issues: normalizeIssuePaths(root, result.issues),
612
- traceability: {
613
- ...result.traceability,
614
- sc: normalizeScCoverage(root, result.traceability.sc)
615
- }
616
- };
617
- }
618
-
619
- // src/core/report.ts
620
- import { readFile as readFile11 } from "fs/promises";
621
- import path15 from "path";
622
-
623
- // src/core/contractIndex.ts
624
- import { readFile as readFile2 } from "fs/promises";
625
- import path8 from "path";
626
-
627
410
  // src/core/discovery.ts
628
- import { access as access4 } from "fs/promises";
411
+ import { access as access3 } from "fs/promises";
629
412
 
630
413
  // src/core/fs.ts
631
- import { access as access3, readdir as readdir2 } from "fs/promises";
632
- import path6 from "path";
414
+ import { access as access2, readdir } from "fs/promises";
415
+ import path2 from "path";
633
416
  import fg from "fast-glob";
634
417
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
635
418
  "node_modules",
@@ -641,7 +424,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
641
424
  ]);
642
425
  async function collectFiles(root, options = {}) {
643
426
  const entries = [];
644
- if (!await exists3(root)) {
427
+ if (!await exists2(root)) {
645
428
  return entries;
646
429
  }
647
430
  const ignoreDirs = /* @__PURE__ */ new Set([
@@ -665,9 +448,9 @@ async function collectFilesByGlobs(root, options) {
665
448
  });
666
449
  }
667
450
  async function walk(base, current, ignoreDirs, extensions, out) {
668
- const items = await readdir2(current, { withFileTypes: true });
451
+ const items = await readdir(current, { withFileTypes: true });
669
452
  for (const item of items) {
670
- const fullPath = path6.join(current, item.name);
453
+ const fullPath = path2.join(current, item.name);
671
454
  if (item.isDirectory()) {
672
455
  if (ignoreDirs.has(item.name)) {
673
456
  continue;
@@ -677,7 +460,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
677
460
  }
678
461
  if (item.isFile()) {
679
462
  if (extensions.length > 0) {
680
- const ext = path6.extname(item.name).toLowerCase();
463
+ const ext = path2.extname(item.name).toLowerCase();
681
464
  if (!extensions.includes(ext)) {
682
465
  continue;
683
466
  }
@@ -686,9 +469,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
686
469
  }
687
470
  }
688
471
  }
689
- async function exists3(target) {
472
+ async function exists2(target) {
690
473
  try {
691
- await access3(target);
474
+ await access2(target);
692
475
  return true;
693
476
  } catch {
694
477
  return false;
@@ -696,23 +479,23 @@ async function exists3(target) {
696
479
  }
697
480
 
698
481
  // src/core/specLayout.ts
699
- import { readdir as readdir3 } from "fs/promises";
700
- import path7 from "path";
482
+ import { readdir as readdir2 } from "fs/promises";
483
+ import path3 from "path";
701
484
  var SPEC_DIR_RE = /^spec-\d{4}$/;
702
485
  async function collectSpecEntries(specsRoot) {
703
486
  const dirs = await listSpecDirs(specsRoot);
704
487
  const entries = dirs.map((dir) => ({
705
488
  dir,
706
- specPath: path7.join(dir, "spec.md"),
707
- deltaPath: path7.join(dir, "delta.md"),
708
- scenarioPath: path7.join(dir, "scenario.md")
489
+ specPath: path3.join(dir, "spec.md"),
490
+ deltaPath: path3.join(dir, "delta.md"),
491
+ scenarioPath: path3.join(dir, "scenario.md")
709
492
  }));
710
493
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
711
494
  }
712
495
  async function listSpecDirs(specsRoot) {
713
496
  try {
714
- const items = await readdir3(specsRoot, { withFileTypes: true });
715
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path7.join(specsRoot, name));
497
+ const items = await readdir2(specsRoot, { withFileTypes: true });
498
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
716
499
  } catch (error2) {
717
500
  if (isMissingFileError(error2)) {
718
501
  return [];
@@ -760,298 +543,43 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
760
543
  async function filterExisting(files) {
761
544
  const existing = [];
762
545
  for (const file of files) {
763
- if (await exists4(file)) {
546
+ if (await exists3(file)) {
764
547
  existing.push(file);
765
548
  }
766
549
  }
767
550
  return existing;
768
551
  }
769
- async function exists4(target) {
552
+ async function exists3(target) {
770
553
  try {
771
- await access4(target);
554
+ await access3(target);
772
555
  return true;
773
556
  } catch {
774
557
  return false;
775
558
  }
776
559
  }
777
560
 
778
- // src/core/contractsDecl.ts
779
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
780
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
781
- function extractDeclaredContractIds(text) {
782
- const ids = [];
783
- for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
784
- const id = match[1];
785
- if (id) {
786
- ids.push(id);
787
- }
561
+ // src/core/paths.ts
562
+ import path4 from "path";
563
+ function toRelativePath(root, target) {
564
+ if (!target) {
565
+ return target;
788
566
  }
789
- return ids;
567
+ if (!path4.isAbsolute(target)) {
568
+ return toPosixPath(target);
569
+ }
570
+ const relative = path4.relative(root, target);
571
+ if (!relative) {
572
+ return ".";
573
+ }
574
+ return toPosixPath(relative);
790
575
  }
791
- function stripContractDeclarationLines(text) {
792
- return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
793
- }
794
-
795
- // src/core/contractIndex.ts
796
- async function buildContractIndex(root, config) {
797
- const contractsRoot = resolvePath(root, config, "contractsDir");
798
- const uiRoot = path8.join(contractsRoot, "ui");
799
- const apiRoot = path8.join(contractsRoot, "api");
800
- const dbRoot = path8.join(contractsRoot, "db");
801
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
802
- collectUiContractFiles(uiRoot),
803
- collectApiContractFiles(apiRoot),
804
- collectDbContractFiles(dbRoot)
805
- ]);
806
- const index = {
807
- ids: /* @__PURE__ */ new Set(),
808
- idToFiles: /* @__PURE__ */ new Map(),
809
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
810
- };
811
- await indexContractFiles(uiFiles, index);
812
- await indexContractFiles(apiFiles, index);
813
- await indexContractFiles(dbFiles, index);
814
- return index;
815
- }
816
- async function indexContractFiles(files, index) {
817
- for (const file of files) {
818
- const text = await readFile2(file, "utf-8");
819
- extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
820
- }
821
- }
822
- function record(index, id, file) {
823
- index.ids.add(id);
824
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
825
- current.add(file);
826
- index.idToFiles.set(id, current);
827
- }
828
-
829
- // src/core/ids.ts
830
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
831
- var STRICT_ID_PATTERNS = {
832
- SPEC: /\bSPEC-\d{4}\b/g,
833
- BR: /\bBR-\d{4}\b/g,
834
- SC: /\bSC-\d{4}\b/g,
835
- UI: /\bUI-\d{4}\b/g,
836
- API: /\bAPI-\d{4}\b/g,
837
- DB: /\bDB-\d{4}\b/g,
838
- ADR: /\bADR-\d{4}\b/g
839
- };
840
- var LOOSE_ID_PATTERNS = {
841
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
842
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
843
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
844
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
845
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
846
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
847
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
848
- };
849
- function extractIds(text, prefix) {
850
- const pattern = STRICT_ID_PATTERNS[prefix];
851
- const matches = text.match(pattern);
852
- return unique(matches ?? []);
853
- }
854
- function extractAllIds(text) {
855
- const all = [];
856
- ID_PREFIXES.forEach((prefix) => {
857
- all.push(...extractIds(text, prefix));
858
- });
859
- return unique(all);
860
- }
861
- function extractInvalidIds(text, prefixes) {
862
- const invalid = [];
863
- for (const prefix of prefixes) {
864
- const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
865
- for (const candidate of candidates) {
866
- if (!isValidId(candidate, prefix)) {
867
- invalid.push(candidate);
868
- }
869
- }
870
- }
871
- return unique(invalid);
872
- }
873
- function unique(values) {
874
- return Array.from(new Set(values));
875
- }
876
- function isValidId(value, prefix) {
877
- const pattern = STRICT_ID_PATTERNS[prefix];
878
- const strict = new RegExp(pattern.source);
879
- return strict.test(value);
880
- }
881
-
882
- // src/core/parse/contractRefs.ts
883
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
884
- function parseContractRefs(text, options = {}) {
885
- const linePattern = buildLinePattern(options);
886
- const lines = [];
887
- for (const match of text.matchAll(linePattern)) {
888
- lines.push((match[1] ?? "").trim());
889
- }
890
- const ids = [];
891
- const invalidTokens = [];
892
- let hasNone = false;
893
- for (const line of lines) {
894
- if (line.length === 0) {
895
- invalidTokens.push("(empty)");
896
- continue;
897
- }
898
- const tokens = line.split(",").map((token) => token.trim());
899
- for (const token of tokens) {
900
- if (token.length === 0) {
901
- invalidTokens.push("(empty)");
902
- continue;
903
- }
904
- if (token === "none") {
905
- hasNone = true;
906
- continue;
907
- }
908
- if (CONTRACT_REF_ID_RE.test(token)) {
909
- ids.push(token);
910
- continue;
911
- }
912
- invalidTokens.push(token);
913
- }
914
- }
915
- return {
916
- lines,
917
- ids: unique2(ids),
918
- invalidTokens: unique2(invalidTokens),
919
- hasNone
920
- };
921
- }
922
- function buildLinePattern(options) {
923
- const prefix = options.allowCommentPrefix ? "#" : "";
924
- return new RegExp(
925
- `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
926
- "gm"
927
- );
928
- }
929
- function unique2(values) {
930
- return Array.from(new Set(values));
931
- }
932
-
933
- // src/core/parse/markdown.ts
934
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
935
- function parseHeadings(md) {
936
- const lines = md.split(/\r?\n/);
937
- const headings = [];
938
- for (let i = 0; i < lines.length; i++) {
939
- const line = lines[i] ?? "";
940
- const match = line.match(HEADING_RE);
941
- if (!match) continue;
942
- const levelToken = match[1];
943
- const title = match[2];
944
- if (!levelToken || !title) continue;
945
- headings.push({
946
- level: levelToken.length,
947
- title: title.trim(),
948
- line: i + 1
949
- });
950
- }
951
- return headings;
952
- }
953
- function extractH2Sections(md) {
954
- const lines = md.split(/\r?\n/);
955
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
956
- const sections = /* @__PURE__ */ new Map();
957
- for (let i = 0; i < headings.length; i++) {
958
- const current = headings[i];
959
- if (!current) continue;
960
- const next = headings[i + 1];
961
- const startLine = current.line + 1;
962
- const endLine = (next?.line ?? lines.length + 1) - 1;
963
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
964
- sections.set(current.title.trim(), {
965
- title: current.title.trim(),
966
- startLine,
967
- endLine,
968
- body
969
- });
970
- }
971
- return sections;
972
- }
973
-
974
- // src/core/parse/spec.ts
975
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
976
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
977
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
978
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
979
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
980
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
981
- function parseSpec(md, file) {
982
- const headings = parseHeadings(md);
983
- const h1 = headings.find((heading) => heading.level === 1);
984
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
985
- const sections = extractH2Sections(md);
986
- const sectionNames = new Set(Array.from(sections.keys()));
987
- const brSection = sections.get(BR_SECTION_TITLE);
988
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
989
- const startLine = brSection?.startLine ?? 1;
990
- const brs = [];
991
- const brsWithoutPriority = [];
992
- const brsWithInvalidPriority = [];
993
- for (let i = 0; i < brLines.length; i++) {
994
- const lineText = brLines[i] ?? "";
995
- const lineNumber = startLine + i;
996
- const validMatch = lineText.match(BR_LINE_RE);
997
- if (validMatch) {
998
- const id = validMatch[1];
999
- const priority = validMatch[2];
1000
- const text = validMatch[3];
1001
- if (!id || !priority || !text) continue;
1002
- brs.push({
1003
- id,
1004
- priority,
1005
- text: text.trim(),
1006
- line: lineNumber
1007
- });
1008
- continue;
1009
- }
1010
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1011
- if (anyPriorityMatch) {
1012
- const id = anyPriorityMatch[1];
1013
- const priority = anyPriorityMatch[2];
1014
- const text = anyPriorityMatch[3];
1015
- if (!id || !priority || !text) continue;
1016
- if (!VALID_PRIORITIES.has(priority)) {
1017
- brsWithInvalidPriority.push({
1018
- id,
1019
- priority,
1020
- text: text.trim(),
1021
- line: lineNumber
1022
- });
1023
- }
1024
- continue;
1025
- }
1026
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1027
- if (noPriorityMatch) {
1028
- const id = noPriorityMatch[1];
1029
- const text = noPriorityMatch[2];
1030
- if (!id || !text) continue;
1031
- brsWithoutPriority.push({
1032
- id,
1033
- text: text.trim(),
1034
- line: lineNumber
1035
- });
1036
- }
1037
- }
1038
- const parsed = {
1039
- file,
1040
- sections: sectionNames,
1041
- brs,
1042
- brsWithoutPriority,
1043
- brsWithInvalidPriority,
1044
- contractRefs: parseContractRefs(md)
1045
- };
1046
- if (specId) {
1047
- parsed.specId = specId;
1048
- }
1049
- return parsed;
576
+ function toPosixPath(value) {
577
+ return value.replace(/\\/g, "/");
1050
578
  }
1051
579
 
1052
580
  // src/core/traceability.ts
1053
- import { readFile as readFile3 } from "fs/promises";
1054
- import path9 from "path";
581
+ import { readFile as readFile2 } from "fs/promises";
582
+ import path5 from "path";
1055
583
 
1056
584
  // src/core/gherkin/parse.ts
1057
585
  import {
@@ -1111,13 +639,13 @@ function parseScenarioDocument(text, uri) {
1111
639
  };
1112
640
  }
1113
641
  function buildScenarioAtoms(document, contractIds = []) {
1114
- const uniqueContractIds = unique3(contractIds).sort(
642
+ const uniqueContractIds = unique(contractIds).sort(
1115
643
  (a, b) => a.localeCompare(b)
1116
644
  );
1117
645
  return document.scenarios.map((scenario) => {
1118
646
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1119
647
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1120
- const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
648
+ const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1121
649
  const atom = {
1122
650
  uri: document.uri,
1123
651
  featureName: document.featureName ?? "",
@@ -1177,7 +705,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
1177
705
  function collectTagNames(tags) {
1178
706
  return tags.map((tag) => tag.name.replace(/^@/, ""));
1179
707
  }
1180
- function unique3(values) {
708
+ function unique(values) {
1181
709
  return Array.from(new Set(values));
1182
710
  }
1183
711
 
@@ -1207,7 +735,7 @@ function extractAnnotatedScIds(text) {
1207
735
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
1208
736
  const scIds = /* @__PURE__ */ new Set();
1209
737
  for (const file of scenarioFiles) {
1210
- const text = await readFile3(file, "utf-8");
738
+ const text = await readFile2(file, "utf-8");
1211
739
  const { document, errors } = parseScenarioDocument(text, file);
1212
740
  if (!document || errors.length > 0) {
1213
741
  continue;
@@ -1225,7 +753,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
1225
753
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1226
754
  const sources = /* @__PURE__ */ new Map();
1227
755
  for (const file of scenarioFiles) {
1228
- const text = await readFile3(file, "utf-8");
756
+ const text = await readFile2(file, "utf-8");
1229
757
  const { document, errors } = parseScenarioDocument(text, file);
1230
758
  if (!document || errors.length > 0) {
1231
759
  continue;
@@ -1258,118 +786,805 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1258
786
  excludeGlobs: mergedExcludeGlobs,
1259
787
  matchedFileCount: 0
1260
788
  }
1261
- };
789
+ };
790
+ }
791
+ let files = [];
792
+ try {
793
+ files = await collectFilesByGlobs(root, {
794
+ globs: normalizedGlobs,
795
+ ignore: mergedExcludeGlobs
796
+ });
797
+ } catch (error2) {
798
+ return {
799
+ refs,
800
+ scan: {
801
+ globs: normalizedGlobs,
802
+ excludeGlobs: mergedExcludeGlobs,
803
+ matchedFileCount: 0
804
+ },
805
+ error: formatError3(error2)
806
+ };
807
+ }
808
+ const normalizedFiles = Array.from(
809
+ new Set(files.map((file) => path5.normalize(file)))
810
+ );
811
+ for (const file of normalizedFiles) {
812
+ const text = await readFile2(file, "utf-8");
813
+ const scIds = extractAnnotatedScIds(text);
814
+ if (scIds.length === 0) {
815
+ continue;
816
+ }
817
+ for (const scId of scIds) {
818
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
819
+ current.add(file);
820
+ refs.set(scId, current);
821
+ }
822
+ }
823
+ return {
824
+ refs,
825
+ scan: {
826
+ globs: normalizedGlobs,
827
+ excludeGlobs: mergedExcludeGlobs,
828
+ matchedFileCount: normalizedFiles.length
829
+ }
830
+ };
831
+ }
832
+ function buildScCoverage(scIds, refs) {
833
+ const sortedScIds = toSortedArray(scIds);
834
+ const refsRecord = {};
835
+ const missingIds = [];
836
+ let covered = 0;
837
+ for (const scId of sortedScIds) {
838
+ const files = refs.get(scId);
839
+ const sortedFiles = files ? toSortedArray(files) : [];
840
+ refsRecord[scId] = sortedFiles;
841
+ if (sortedFiles.length === 0) {
842
+ missingIds.push(scId);
843
+ } else {
844
+ covered += 1;
845
+ }
846
+ }
847
+ return {
848
+ total: sortedScIds.length,
849
+ covered,
850
+ missing: missingIds.length,
851
+ missingIds,
852
+ refs: refsRecord
853
+ };
854
+ }
855
+ function toSortedArray(values) {
856
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
857
+ }
858
+ function normalizeGlobs(globs) {
859
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
860
+ }
861
+ function formatError3(error2) {
862
+ if (error2 instanceof Error) {
863
+ return error2.message;
864
+ }
865
+ return String(error2);
866
+ }
867
+
868
+ // src/core/version.ts
869
+ import { readFile as readFile3 } from "fs/promises";
870
+ import path6 from "path";
871
+ import { fileURLToPath } from "url";
872
+ async function resolveToolVersion() {
873
+ if ("0.6.0".length > 0) {
874
+ return "0.6.0";
875
+ }
876
+ try {
877
+ const packagePath = resolvePackageJsonPath();
878
+ const raw = await readFile3(packagePath, "utf-8");
879
+ const parsed = JSON.parse(raw);
880
+ const version = typeof parsed.version === "string" ? parsed.version : "";
881
+ return version.length > 0 ? version : "unknown";
882
+ } catch {
883
+ return "unknown";
884
+ }
885
+ }
886
+ function resolvePackageJsonPath() {
887
+ const base = import.meta.url;
888
+ const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
889
+ return path6.resolve(path6.dirname(basePath), "../../package.json");
890
+ }
891
+
892
+ // src/core/doctor.ts
893
+ async function exists4(target) {
894
+ try {
895
+ await access4(target);
896
+ return true;
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+ function addCheck(checks, check) {
902
+ checks.push(check);
903
+ }
904
+ function summarize(checks) {
905
+ const summary = { ok: 0, warning: 0, error: 0 };
906
+ for (const check of checks) {
907
+ summary[check.severity] += 1;
908
+ }
909
+ return summary;
910
+ }
911
+ function normalizeGlobs2(values) {
912
+ return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
913
+ }
914
+ async function createDoctorData(options) {
915
+ const startDir = path7.resolve(options.startDir);
916
+ const checks = [];
917
+ const configPath = getConfigPath(startDir);
918
+ const search = options.rootExplicit ? {
919
+ root: startDir,
920
+ configPath,
921
+ found: await exists4(configPath)
922
+ } : await findConfigRoot(startDir);
923
+ const root = search.root;
924
+ const version = await resolveToolVersion();
925
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
926
+ addCheck(checks, {
927
+ id: "config.search",
928
+ severity: search.found ? "ok" : "warning",
929
+ title: "Config search",
930
+ message: search.found ? "qfai.config.yaml found" : "qfai.config.yaml not found (default config will be used)",
931
+ details: { configPath: toRelativePath(root, search.configPath) }
932
+ });
933
+ const {
934
+ config,
935
+ issues,
936
+ configPath: resolvedConfigPath
937
+ } = await loadConfig(root);
938
+ if (issues.length === 0) {
939
+ addCheck(checks, {
940
+ id: "config.load",
941
+ severity: "ok",
942
+ title: "Config load",
943
+ message: "Loaded and normalized with 0 issues",
944
+ details: { configPath: toRelativePath(root, resolvedConfigPath) }
945
+ });
946
+ } else {
947
+ addCheck(checks, {
948
+ id: "config.load",
949
+ severity: "warning",
950
+ title: "Config load",
951
+ message: `Loaded with ${issues.length} issue(s) (normalized with defaults when needed)`,
952
+ details: {
953
+ configPath: toRelativePath(root, resolvedConfigPath),
954
+ issues
955
+ }
956
+ });
957
+ }
958
+ const pathKeys = [
959
+ "specsDir",
960
+ "contractsDir",
961
+ "outDir",
962
+ "srcDir",
963
+ "testsDir",
964
+ "rulesDir",
965
+ "promptsDir"
966
+ ];
967
+ for (const key of pathKeys) {
968
+ const resolved = resolvePath(root, config, key);
969
+ const ok = await exists4(resolved);
970
+ addCheck(checks, {
971
+ id: `paths.${key}`,
972
+ severity: ok ? "ok" : "warning",
973
+ title: `Path exists: ${key}`,
974
+ message: ok ? `${key} exists` : `${key} is missing (did you run 'qfai init'?)`,
975
+ details: { path: toRelativePath(root, resolved) }
976
+ });
977
+ }
978
+ const specsRoot = resolvePath(root, config, "specsDir");
979
+ const entries = await collectSpecEntries(specsRoot);
980
+ let missingFiles = 0;
981
+ for (const entry of entries) {
982
+ const requiredFiles = [entry.specPath, entry.deltaPath, entry.scenarioPath];
983
+ for (const filePath of requiredFiles) {
984
+ if (!await exists4(filePath)) {
985
+ missingFiles += 1;
986
+ }
987
+ }
988
+ }
989
+ addCheck(checks, {
990
+ id: "spec.layout",
991
+ severity: missingFiles === 0 ? "ok" : "warning",
992
+ title: "Spec pack shape",
993
+ message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
994
+ details: { specPacks: entries.length, missingFiles }
995
+ });
996
+ const validateJsonAbs = path7.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path7.resolve(root, config.output.validateJsonPath);
997
+ const validateJsonExists = await exists4(validateJsonAbs);
998
+ addCheck(checks, {
999
+ id: "output.validateJson",
1000
+ severity: validateJsonExists ? "ok" : "warning",
1001
+ title: "validate.json",
1002
+ message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
1003
+ details: { path: toRelativePath(root, validateJsonAbs) }
1004
+ });
1005
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1006
+ const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1007
+ const exclude = normalizeGlobs2([
1008
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1009
+ ...config.validation.traceability.testFileExcludeGlobs
1010
+ ]);
1011
+ try {
1012
+ const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1013
+ const matchedCount = matched.length;
1014
+ const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1015
+ addCheck(checks, {
1016
+ id: "traceability.testGlobs",
1017
+ severity,
1018
+ title: "Test file globs",
1019
+ message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
1020
+ details: {
1021
+ globs,
1022
+ excludeGlobs: exclude,
1023
+ scenarioFiles: scenarioFiles.length,
1024
+ scMustHaveTest: config.validation.traceability.scMustHaveTest
1025
+ }
1026
+ });
1027
+ } catch (error2) {
1028
+ addCheck(checks, {
1029
+ id: "traceability.testGlobs",
1030
+ severity: "error",
1031
+ title: "Test file globs",
1032
+ message: "Glob scan failed (invalid pattern or filesystem error)",
1033
+ details: { globs, excludeGlobs: exclude, error: String(error2) }
1034
+ });
1035
+ }
1036
+ const outDirAbs = resolvePath(root, config, "outDir");
1037
+ const rel = path7.relative(outDirAbs, validateJsonAbs);
1038
+ const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1039
+ addCheck(checks, {
1040
+ id: "output.pathAlignment",
1041
+ severity: inside ? "ok" : "warning",
1042
+ title: "Output path alignment",
1043
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1044
+ details: {
1045
+ outDir: toRelativePath(root, outDirAbs),
1046
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1047
+ }
1048
+ });
1049
+ return {
1050
+ tool: "qfai",
1051
+ version,
1052
+ doctorFormatVersion: 1,
1053
+ generatedAt,
1054
+ root: toRelativePath(process.cwd(), root),
1055
+ config: {
1056
+ startDir: toRelativePath(process.cwd(), startDir),
1057
+ found: search.found,
1058
+ configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
1059
+ },
1060
+ summary: summarize(checks),
1061
+ checks
1062
+ };
1063
+ }
1064
+
1065
+ // src/cli/lib/logger.ts
1066
+ function info(message) {
1067
+ process.stdout.write(`${message}
1068
+ `);
1069
+ }
1070
+ function warn(message) {
1071
+ process.stdout.write(`${message}
1072
+ `);
1073
+ }
1074
+ function error(message) {
1075
+ process.stderr.write(`${message}
1076
+ `);
1077
+ }
1078
+
1079
+ // src/cli/commands/doctor.ts
1080
+ function formatDoctorText(data) {
1081
+ const lines = [];
1082
+ lines.push(
1083
+ `qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
1084
+ );
1085
+ for (const check of data.checks) {
1086
+ lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
1087
+ }
1088
+ lines.push(
1089
+ `summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
1090
+ );
1091
+ return lines.join("\n");
1092
+ }
1093
+ function formatDoctorJson(data) {
1094
+ return JSON.stringify(data, null, 2);
1095
+ }
1096
+ async function runDoctor(options) {
1097
+ const data = await createDoctorData({
1098
+ startDir: options.root,
1099
+ rootExplicit: options.rootExplicit
1100
+ });
1101
+ const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1102
+ if (options.outPath) {
1103
+ const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
1104
+ await mkdir(path8.dirname(outAbs), { recursive: true });
1105
+ await writeFile(outAbs, `${output}
1106
+ `, "utf-8");
1107
+ info(`doctor: wrote ${outAbs}`);
1108
+ return;
1109
+ }
1110
+ info(output);
1111
+ }
1112
+
1113
+ // src/cli/commands/init.ts
1114
+ import path11 from "path";
1115
+
1116
+ // src/cli/lib/fs.ts
1117
+ import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1118
+ import path9 from "path";
1119
+ async function copyTemplateTree(sourceRoot, destRoot, options) {
1120
+ const files = await collectTemplateFiles(sourceRoot);
1121
+ return copyFiles(files, sourceRoot, destRoot, options);
1122
+ }
1123
+ async function copyFiles(files, sourceRoot, destRoot, options) {
1124
+ const copied = [];
1125
+ const skipped = [];
1126
+ const conflicts = [];
1127
+ if (!options.force) {
1128
+ for (const file of files) {
1129
+ const relative = path9.relative(sourceRoot, file);
1130
+ const dest = path9.join(destRoot, relative);
1131
+ if (!await shouldWrite(dest, options.force)) {
1132
+ conflicts.push(dest);
1133
+ }
1134
+ }
1135
+ if (conflicts.length > 0) {
1136
+ throw new Error(formatConflictMessage(conflicts));
1137
+ }
1138
+ }
1139
+ for (const file of files) {
1140
+ const relative = path9.relative(sourceRoot, file);
1141
+ const dest = path9.join(destRoot, relative);
1142
+ if (!await shouldWrite(dest, options.force)) {
1143
+ skipped.push(dest);
1144
+ continue;
1145
+ }
1146
+ if (!options.dryRun) {
1147
+ await mkdir2(path9.dirname(dest), { recursive: true });
1148
+ await copyFile(file, dest);
1149
+ }
1150
+ copied.push(dest);
1151
+ }
1152
+ return { copied, skipped };
1153
+ }
1154
+ function formatConflictMessage(conflicts) {
1155
+ return [
1156
+ "\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3068\u885D\u7A81\u3057\u307E\u3057\u305F\u3002\u5B89\u5168\u306E\u305F\u3081\u505C\u6B62\u3057\u307E\u3059\u3002",
1157
+ "",
1158
+ "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
1159
+ ...conflicts.map((conflict) => `- ${conflict}`),
1160
+ "",
1161
+ "\u4E0A\u66F8\u304D\u3057\u3066\u7D9A\u884C\u3059\u308B\u5834\u5408\u306F --force \u3092\u4ED8\u3051\u3066\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
1162
+ ].join("\n");
1163
+ }
1164
+ async function collectTemplateFiles(root) {
1165
+ const entries = [];
1166
+ if (!await exists5(root)) {
1167
+ return entries;
1168
+ }
1169
+ const items = await readdir3(root, { withFileTypes: true });
1170
+ for (const item of items) {
1171
+ const fullPath = path9.join(root, item.name);
1172
+ if (item.isDirectory()) {
1173
+ const nested = await collectTemplateFiles(fullPath);
1174
+ entries.push(...nested);
1175
+ continue;
1176
+ }
1177
+ if (item.isFile()) {
1178
+ entries.push(fullPath);
1179
+ }
1180
+ }
1181
+ return entries;
1182
+ }
1183
+ async function shouldWrite(target, force) {
1184
+ if (force) {
1185
+ return true;
1186
+ }
1187
+ return !await exists5(target);
1188
+ }
1189
+ async function exists5(target) {
1190
+ try {
1191
+ await access5(target);
1192
+ return true;
1193
+ } catch {
1194
+ return false;
1195
+ }
1196
+ }
1197
+
1198
+ // src/cli/lib/assets.ts
1199
+ import { existsSync } from "fs";
1200
+ import path10 from "path";
1201
+ import { fileURLToPath as fileURLToPath2 } from "url";
1202
+ function getInitAssetsDir() {
1203
+ const base = import.meta.url;
1204
+ const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1205
+ const baseDir = path10.dirname(basePath);
1206
+ const candidates = [
1207
+ path10.resolve(baseDir, "../../../assets/init"),
1208
+ path10.resolve(baseDir, "../../assets/init")
1209
+ ];
1210
+ for (const candidate of candidates) {
1211
+ if (existsSync(candidate)) {
1212
+ return candidate;
1213
+ }
1214
+ }
1215
+ throw new Error(
1216
+ [
1217
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1218
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1219
+ ...candidates.map((candidate) => `- ${candidate}`)
1220
+ ].join("\n")
1221
+ );
1222
+ }
1223
+
1224
+ // src/cli/commands/init.ts
1225
+ async function runInit(options) {
1226
+ const assetsRoot = getInitAssetsDir();
1227
+ const rootAssets = path11.join(assetsRoot, "root");
1228
+ const qfaiAssets = path11.join(assetsRoot, ".qfai");
1229
+ const destRoot = path11.resolve(options.dir);
1230
+ const destQfai = path11.join(destRoot, ".qfai");
1231
+ const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1232
+ force: options.force,
1233
+ dryRun: options.dryRun
1234
+ });
1235
+ const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1236
+ force: options.force,
1237
+ dryRun: options.dryRun
1238
+ });
1239
+ report(
1240
+ [...rootResult.copied, ...qfaiResult.copied],
1241
+ [...rootResult.skipped, ...qfaiResult.skipped],
1242
+ options.dryRun,
1243
+ "init"
1244
+ );
1245
+ }
1246
+ function report(copied, skipped, dryRun, label) {
1247
+ info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
1248
+ if (copied.length > 0) {
1249
+ info(` created: ${copied.length}`);
1250
+ }
1251
+ if (skipped.length > 0) {
1252
+ info(` skipped: ${skipped.length}`);
1253
+ }
1254
+ }
1255
+
1256
+ // src/cli/commands/report.ts
1257
+ import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
1258
+ import path18 from "path";
1259
+
1260
+ // src/core/normalize.ts
1261
+ function normalizeIssuePaths(root, issues) {
1262
+ return issues.map((issue7) => {
1263
+ if (!issue7.file) {
1264
+ return issue7;
1265
+ }
1266
+ const normalized = toRelativePath(root, issue7.file);
1267
+ if (normalized === issue7.file) {
1268
+ return issue7;
1269
+ }
1270
+ return {
1271
+ ...issue7,
1272
+ file: normalized
1273
+ };
1274
+ });
1275
+ }
1276
+ function normalizeScCoverage(root, sc) {
1277
+ const refs = {};
1278
+ for (const [scId, files] of Object.entries(sc.refs)) {
1279
+ refs[scId] = files.map((file) => toRelativePath(root, file));
1280
+ }
1281
+ return {
1282
+ ...sc,
1283
+ refs
1284
+ };
1285
+ }
1286
+ function normalizeValidationResult(root, result) {
1287
+ return {
1288
+ ...result,
1289
+ issues: normalizeIssuePaths(root, result.issues),
1290
+ traceability: {
1291
+ ...result.traceability,
1292
+ sc: normalizeScCoverage(root, result.traceability.sc)
1293
+ }
1294
+ };
1295
+ }
1296
+
1297
+ // src/core/report.ts
1298
+ import { readFile as readFile11 } from "fs/promises";
1299
+ import path17 from "path";
1300
+
1301
+ // src/core/contractIndex.ts
1302
+ import { readFile as readFile4 } from "fs/promises";
1303
+ import path12 from "path";
1304
+
1305
+ // src/core/contractsDecl.ts
1306
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1307
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1308
+ function extractDeclaredContractIds(text) {
1309
+ const ids = [];
1310
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
1311
+ const id = match[1];
1312
+ if (id) {
1313
+ ids.push(id);
1314
+ }
1315
+ }
1316
+ return ids;
1317
+ }
1318
+ function stripContractDeclarationLines(text) {
1319
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
1320
+ }
1321
+
1322
+ // src/core/contractIndex.ts
1323
+ async function buildContractIndex(root, config) {
1324
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1325
+ const uiRoot = path12.join(contractsRoot, "ui");
1326
+ const apiRoot = path12.join(contractsRoot, "api");
1327
+ const dbRoot = path12.join(contractsRoot, "db");
1328
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1329
+ collectUiContractFiles(uiRoot),
1330
+ collectApiContractFiles(apiRoot),
1331
+ collectDbContractFiles(dbRoot)
1332
+ ]);
1333
+ const index = {
1334
+ ids: /* @__PURE__ */ new Set(),
1335
+ idToFiles: /* @__PURE__ */ new Map(),
1336
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1337
+ };
1338
+ await indexContractFiles(uiFiles, index);
1339
+ await indexContractFiles(apiFiles, index);
1340
+ await indexContractFiles(dbFiles, index);
1341
+ return index;
1342
+ }
1343
+ async function indexContractFiles(files, index) {
1344
+ for (const file of files) {
1345
+ const text = await readFile4(file, "utf-8");
1346
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1347
+ }
1348
+ }
1349
+ function record(index, id, file) {
1350
+ index.ids.add(id);
1351
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1352
+ current.add(file);
1353
+ index.idToFiles.set(id, current);
1354
+ }
1355
+
1356
+ // src/core/ids.ts
1357
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1358
+ var STRICT_ID_PATTERNS = {
1359
+ SPEC: /\bSPEC-\d{4}\b/g,
1360
+ BR: /\bBR-\d{4}\b/g,
1361
+ SC: /\bSC-\d{4}\b/g,
1362
+ UI: /\bUI-\d{4}\b/g,
1363
+ API: /\bAPI-\d{4}\b/g,
1364
+ DB: /\bDB-\d{4}\b/g,
1365
+ ADR: /\bADR-\d{4}\b/g
1366
+ };
1367
+ var LOOSE_ID_PATTERNS = {
1368
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
1369
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
1370
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
1371
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1372
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1373
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1374
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1375
+ };
1376
+ function extractIds(text, prefix) {
1377
+ const pattern = STRICT_ID_PATTERNS[prefix];
1378
+ const matches = text.match(pattern);
1379
+ return unique2(matches ?? []);
1380
+ }
1381
+ function extractAllIds(text) {
1382
+ const all = [];
1383
+ ID_PREFIXES.forEach((prefix) => {
1384
+ all.push(...extractIds(text, prefix));
1385
+ });
1386
+ return unique2(all);
1387
+ }
1388
+ function extractInvalidIds(text, prefixes) {
1389
+ const invalid = [];
1390
+ for (const prefix of prefixes) {
1391
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
1392
+ for (const candidate of candidates) {
1393
+ if (!isValidId(candidate, prefix)) {
1394
+ invalid.push(candidate);
1395
+ }
1396
+ }
1262
1397
  }
1263
- let files = [];
1264
- try {
1265
- files = await collectFilesByGlobs(root, {
1266
- globs: normalizedGlobs,
1267
- ignore: mergedExcludeGlobs
1268
- });
1269
- } catch (error2) {
1270
- return {
1271
- refs,
1272
- scan: {
1273
- globs: normalizedGlobs,
1274
- excludeGlobs: mergedExcludeGlobs,
1275
- matchedFileCount: 0
1276
- },
1277
- error: formatError3(error2)
1278
- };
1398
+ return unique2(invalid);
1399
+ }
1400
+ function unique2(values) {
1401
+ return Array.from(new Set(values));
1402
+ }
1403
+ function isValidId(value, prefix) {
1404
+ const pattern = STRICT_ID_PATTERNS[prefix];
1405
+ const strict = new RegExp(pattern.source);
1406
+ return strict.test(value);
1407
+ }
1408
+
1409
+ // src/core/parse/contractRefs.ts
1410
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1411
+ function parseContractRefs(text, options = {}) {
1412
+ const linePattern = buildLinePattern(options);
1413
+ const lines = [];
1414
+ for (const match of text.matchAll(linePattern)) {
1415
+ lines.push((match[1] ?? "").trim());
1279
1416
  }
1280
- const normalizedFiles = Array.from(
1281
- new Set(files.map((file) => path9.normalize(file)))
1282
- );
1283
- for (const file of normalizedFiles) {
1284
- const text = await readFile3(file, "utf-8");
1285
- const scIds = extractAnnotatedScIds(text);
1286
- if (scIds.length === 0) {
1417
+ const ids = [];
1418
+ const invalidTokens = [];
1419
+ let hasNone = false;
1420
+ for (const line of lines) {
1421
+ if (line.length === 0) {
1422
+ invalidTokens.push("(empty)");
1287
1423
  continue;
1288
1424
  }
1289
- for (const scId of scIds) {
1290
- const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1291
- current.add(file);
1292
- refs.set(scId, current);
1425
+ const tokens = line.split(",").map((token) => token.trim());
1426
+ for (const token of tokens) {
1427
+ if (token.length === 0) {
1428
+ invalidTokens.push("(empty)");
1429
+ continue;
1430
+ }
1431
+ if (token === "none") {
1432
+ hasNone = true;
1433
+ continue;
1434
+ }
1435
+ if (CONTRACT_REF_ID_RE.test(token)) {
1436
+ ids.push(token);
1437
+ continue;
1438
+ }
1439
+ invalidTokens.push(token);
1293
1440
  }
1294
1441
  }
1295
1442
  return {
1296
- refs,
1297
- scan: {
1298
- globs: normalizedGlobs,
1299
- excludeGlobs: mergedExcludeGlobs,
1300
- matchedFileCount: normalizedFiles.length
1301
- }
1443
+ lines,
1444
+ ids: unique3(ids),
1445
+ invalidTokens: unique3(invalidTokens),
1446
+ hasNone
1302
1447
  };
1303
1448
  }
1304
- function buildScCoverage(scIds, refs) {
1305
- const sortedScIds = toSortedArray(scIds);
1306
- const refsRecord = {};
1307
- const missingIds = [];
1308
- let covered = 0;
1309
- for (const scId of sortedScIds) {
1310
- const files = refs.get(scId);
1311
- const sortedFiles = files ? toSortedArray(files) : [];
1312
- refsRecord[scId] = sortedFiles;
1313
- if (sortedFiles.length === 0) {
1314
- missingIds.push(scId);
1315
- } else {
1316
- covered += 1;
1317
- }
1318
- }
1319
- return {
1320
- total: sortedScIds.length,
1321
- covered,
1322
- missing: missingIds.length,
1323
- missingIds,
1324
- refs: refsRecord
1325
- };
1449
+ function buildLinePattern(options) {
1450
+ const prefix = options.allowCommentPrefix ? "#" : "";
1451
+ return new RegExp(
1452
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
1453
+ "gm"
1454
+ );
1326
1455
  }
1327
- function toSortedArray(values) {
1328
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1456
+ function unique3(values) {
1457
+ return Array.from(new Set(values));
1329
1458
  }
1330
- function normalizeGlobs(globs) {
1331
- return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1459
+
1460
+ // src/core/parse/markdown.ts
1461
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1462
+ function parseHeadings(md) {
1463
+ const lines = md.split(/\r?\n/);
1464
+ const headings = [];
1465
+ for (let i = 0; i < lines.length; i++) {
1466
+ const line = lines[i] ?? "";
1467
+ const match = line.match(HEADING_RE);
1468
+ if (!match) continue;
1469
+ const levelToken = match[1];
1470
+ const title = match[2];
1471
+ if (!levelToken || !title) continue;
1472
+ headings.push({
1473
+ level: levelToken.length,
1474
+ title: title.trim(),
1475
+ line: i + 1
1476
+ });
1477
+ }
1478
+ return headings;
1332
1479
  }
1333
- function formatError3(error2) {
1334
- if (error2 instanceof Error) {
1335
- return error2.message;
1480
+ function extractH2Sections(md) {
1481
+ const lines = md.split(/\r?\n/);
1482
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1483
+ const sections = /* @__PURE__ */ new Map();
1484
+ for (let i = 0; i < headings.length; i++) {
1485
+ const current = headings[i];
1486
+ if (!current) continue;
1487
+ const next = headings[i + 1];
1488
+ const startLine = current.line + 1;
1489
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1490
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1491
+ sections.set(current.title.trim(), {
1492
+ title: current.title.trim(),
1493
+ startLine,
1494
+ endLine,
1495
+ body
1496
+ });
1336
1497
  }
1337
- return String(error2);
1498
+ return sections;
1338
1499
  }
1339
1500
 
1340
- // src/core/version.ts
1341
- import { readFile as readFile4 } from "fs/promises";
1342
- import path10 from "path";
1343
- import { fileURLToPath as fileURLToPath2 } from "url";
1344
- async function resolveToolVersion() {
1345
- if ("0.5.2".length > 0) {
1346
- return "0.5.2";
1501
+ // src/core/parse/spec.ts
1502
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1503
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1504
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1505
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1506
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1507
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1508
+ function parseSpec(md, file) {
1509
+ const headings = parseHeadings(md);
1510
+ const h1 = headings.find((heading) => heading.level === 1);
1511
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1512
+ const sections = extractH2Sections(md);
1513
+ const sectionNames = new Set(Array.from(sections.keys()));
1514
+ const brSection = sections.get(BR_SECTION_TITLE);
1515
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1516
+ const startLine = brSection?.startLine ?? 1;
1517
+ const brs = [];
1518
+ const brsWithoutPriority = [];
1519
+ const brsWithInvalidPriority = [];
1520
+ for (let i = 0; i < brLines.length; i++) {
1521
+ const lineText = brLines[i] ?? "";
1522
+ const lineNumber = startLine + i;
1523
+ const validMatch = lineText.match(BR_LINE_RE);
1524
+ if (validMatch) {
1525
+ const id = validMatch[1];
1526
+ const priority = validMatch[2];
1527
+ const text = validMatch[3];
1528
+ if (!id || !priority || !text) continue;
1529
+ brs.push({
1530
+ id,
1531
+ priority,
1532
+ text: text.trim(),
1533
+ line: lineNumber
1534
+ });
1535
+ continue;
1536
+ }
1537
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1538
+ if (anyPriorityMatch) {
1539
+ const id = anyPriorityMatch[1];
1540
+ const priority = anyPriorityMatch[2];
1541
+ const text = anyPriorityMatch[3];
1542
+ if (!id || !priority || !text) continue;
1543
+ if (!VALID_PRIORITIES.has(priority)) {
1544
+ brsWithInvalidPriority.push({
1545
+ id,
1546
+ priority,
1547
+ text: text.trim(),
1548
+ line: lineNumber
1549
+ });
1550
+ }
1551
+ continue;
1552
+ }
1553
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1554
+ if (noPriorityMatch) {
1555
+ const id = noPriorityMatch[1];
1556
+ const text = noPriorityMatch[2];
1557
+ if (!id || !text) continue;
1558
+ brsWithoutPriority.push({
1559
+ id,
1560
+ text: text.trim(),
1561
+ line: lineNumber
1562
+ });
1563
+ }
1347
1564
  }
1348
- try {
1349
- const packagePath = resolvePackageJsonPath();
1350
- const raw = await readFile4(packagePath, "utf-8");
1351
- const parsed = JSON.parse(raw);
1352
- const version = typeof parsed.version === "string" ? parsed.version : "";
1353
- return version.length > 0 ? version : "unknown";
1354
- } catch {
1355
- return "unknown";
1565
+ const parsed = {
1566
+ file,
1567
+ sections: sectionNames,
1568
+ brs,
1569
+ brsWithoutPriority,
1570
+ brsWithInvalidPriority,
1571
+ contractRefs: parseContractRefs(md)
1572
+ };
1573
+ if (specId) {
1574
+ parsed.specId = specId;
1356
1575
  }
1357
- }
1358
- function resolvePackageJsonPath() {
1359
- const base = import.meta.url;
1360
- const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1361
- return path10.resolve(path10.dirname(basePath), "../../package.json");
1576
+ return parsed;
1362
1577
  }
1363
1578
 
1364
1579
  // src/core/validators/contracts.ts
1365
1580
  import { readFile as readFile5 } from "fs/promises";
1366
- import path12 from "path";
1581
+ import path14 from "path";
1367
1582
 
1368
1583
  // src/core/contracts.ts
1369
- import path11 from "path";
1584
+ import path13 from "path";
1370
1585
  import { parse as parseYaml2 } from "yaml";
1371
1586
  function parseStructuredContract(file, text) {
1372
- const ext = path11.extname(file).toLowerCase();
1587
+ const ext = path13.extname(file).toLowerCase();
1373
1588
  if (ext === ".json") {
1374
1589
  return JSON.parse(text);
1375
1590
  }
@@ -1389,9 +1604,9 @@ var SQL_DANGEROUS_PATTERNS = [
1389
1604
  async function validateContracts(root, config) {
1390
1605
  const issues = [];
1391
1606
  const contractsRoot = resolvePath(root, config, "contractsDir");
1392
- issues.push(...await validateUiContracts(path12.join(contractsRoot, "ui")));
1393
- issues.push(...await validateApiContracts(path12.join(contractsRoot, "api")));
1394
- issues.push(...await validateDbContracts(path12.join(contractsRoot, "db")));
1607
+ issues.push(...await validateUiContracts(path14.join(contractsRoot, "ui")));
1608
+ issues.push(...await validateApiContracts(path14.join(contractsRoot, "api")));
1609
+ issues.push(...await validateDbContracts(path14.join(contractsRoot, "db")));
1395
1610
  const contractIndex = await buildContractIndex(root, config);
1396
1611
  issues.push(...validateDuplicateContractIds(contractIndex));
1397
1612
  return issues;
@@ -1674,7 +1889,7 @@ function issue(code, message, severity, file, rule, refs) {
1674
1889
 
1675
1890
  // src/core/validators/delta.ts
1676
1891
  import { readFile as readFile6 } from "fs/promises";
1677
- import path13 from "path";
1892
+ import path15 from "path";
1678
1893
  var SECTION_RE = /^##\s+変更区分/m;
1679
1894
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1680
1895
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1688,7 +1903,7 @@ async function validateDeltas(root, config) {
1688
1903
  }
1689
1904
  const issues = [];
1690
1905
  for (const pack of packs) {
1691
- const deltaPath = path13.join(pack, "delta.md");
1906
+ const deltaPath = path15.join(pack, "delta.md");
1692
1907
  let text;
1693
1908
  try {
1694
1909
  text = await readFile6(deltaPath, "utf-8");
@@ -1764,7 +1979,7 @@ function issue2(code, message, severity, file, rule, refs) {
1764
1979
 
1765
1980
  // src/core/validators/ids.ts
1766
1981
  import { readFile as readFile7 } from "fs/promises";
1767
- import path14 from "path";
1982
+ import path16 from "path";
1768
1983
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1769
1984
  async function validateDefinedIds(root, config) {
1770
1985
  const issues = [];
@@ -1830,7 +2045,7 @@ function recordId(out, id, file) {
1830
2045
  }
1831
2046
  function formatFileList(files, root) {
1832
2047
  return files.map((file) => {
1833
- const relative = path14.relative(root, file);
2048
+ const relative = path16.relative(root, file);
1834
2049
  return relative.length > 0 ? relative : file;
1835
2050
  }).join(", ");
1836
2051
  }
@@ -2721,15 +2936,15 @@ function countIssues(issues) {
2721
2936
  // src/core/report.ts
2722
2937
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2723
2938
  async function createReportData(root, validation, configResult) {
2724
- const resolvedRoot = path15.resolve(root);
2939
+ const resolvedRoot = path17.resolve(root);
2725
2940
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2726
2941
  const config = resolved.config;
2727
2942
  const configPath = resolved.configPath;
2728
2943
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2729
2944
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2730
- const apiRoot = path15.join(contractsRoot, "api");
2731
- const uiRoot = path15.join(contractsRoot, "ui");
2732
- const dbRoot = path15.join(contractsRoot, "db");
2945
+ const apiRoot = path17.join(contractsRoot, "api");
2946
+ const uiRoot = path17.join(contractsRoot, "ui");
2947
+ const dbRoot = path17.join(contractsRoot, "db");
2733
2948
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2734
2949
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2735
2950
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2787,11 +3002,13 @@ async function createReportData(root, validation, configResult) {
2787
3002
  normalizeScSources(resolvedRoot, scSources)
2788
3003
  );
2789
3004
  const version = await resolveToolVersion();
3005
+ const reportFormatVersion = 1;
2790
3006
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
2791
3007
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
2792
3008
  return {
2793
3009
  tool: "qfai",
2794
3010
  version,
3011
+ reportFormatVersion,
2795
3012
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2796
3013
  root: displayRoot,
2797
3014
  configPath: displayConfigPath,
@@ -3216,7 +3433,7 @@ function buildHotspots(issues) {
3216
3433
 
3217
3434
  // src/cli/commands/report.ts
3218
3435
  async function runReport(options) {
3219
- const root = path16.resolve(options.root);
3436
+ const root = path18.resolve(options.root);
3220
3437
  const configResult = await loadConfig(root);
3221
3438
  let validation;
3222
3439
  if (options.runValidate) {
@@ -3233,7 +3450,7 @@ async function runReport(options) {
3233
3450
  validation = normalized;
3234
3451
  } else {
3235
3452
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3236
- const inputPath = path16.isAbsolute(input) ? input : path16.resolve(root, input);
3453
+ const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
3237
3454
  try {
3238
3455
  validation = await readValidationResult(inputPath);
3239
3456
  } catch (err) {
@@ -3259,11 +3476,11 @@ async function runReport(options) {
3259
3476
  const data = await createReportData(root, validation, configResult);
3260
3477
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3261
3478
  const outRoot = resolvePath(root, configResult.config, "outDir");
3262
- const defaultOut = options.format === "json" ? path16.join(outRoot, "report.json") : path16.join(outRoot, "report.md");
3479
+ const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
3263
3480
  const out = options.outPath ?? defaultOut;
3264
- const outPath = path16.isAbsolute(out) ? out : path16.resolve(root, out);
3265
- await mkdir2(path16.dirname(outPath), { recursive: true });
3266
- await writeFile(outPath, `${output}
3481
+ const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
3482
+ await mkdir3(path18.dirname(outPath), { recursive: true });
3483
+ await writeFile2(outPath, `${output}
3267
3484
  `, "utf-8");
3268
3485
  info(
3269
3486
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3327,15 +3544,15 @@ function isMissingFileError5(error2) {
3327
3544
  return record2.code === "ENOENT";
3328
3545
  }
3329
3546
  async function writeValidationResult(root, outputPath, result) {
3330
- const abs = path16.isAbsolute(outputPath) ? outputPath : path16.resolve(root, outputPath);
3331
- await mkdir2(path16.dirname(abs), { recursive: true });
3332
- await writeFile(abs, `${JSON.stringify(result, null, 2)}
3547
+ const abs = path18.isAbsolute(outputPath) ? outputPath : path18.resolve(root, outputPath);
3548
+ await mkdir3(path18.dirname(abs), { recursive: true });
3549
+ await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3333
3550
  `, "utf-8");
3334
3551
  }
3335
3552
 
3336
3553
  // src/cli/commands/validate.ts
3337
- import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3338
- import path17 from "path";
3554
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3555
+ import path19 from "path";
3339
3556
 
3340
3557
  // src/cli/lib/failOn.ts
3341
3558
  function shouldFail(result, failOn) {
@@ -3350,7 +3567,7 @@ function shouldFail(result, failOn) {
3350
3567
 
3351
3568
  // src/cli/commands/validate.ts
3352
3569
  async function runValidate(options) {
3353
- const root = path17.resolve(options.root);
3570
+ const root = path19.resolve(options.root);
3354
3571
  const configResult = await loadConfig(root);
3355
3572
  const result = await validateProject(root, configResult);
3356
3573
  const normalized = normalizeValidationResult(root, result);
@@ -3467,12 +3684,12 @@ function issueKey(issue7) {
3467
3684
  }
3468
3685
  async function emitJson(result, root, jsonPath) {
3469
3686
  const abs = resolveJsonPath(root, jsonPath);
3470
- await mkdir3(path17.dirname(abs), { recursive: true });
3471
- await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3687
+ await mkdir4(path19.dirname(abs), { recursive: true });
3688
+ await writeFile3(abs, `${JSON.stringify(result, null, 2)}
3472
3689
  `, "utf-8");
3473
3690
  }
3474
3691
  function resolveJsonPath(root, jsonPath) {
3475
- return path17.isAbsolute(jsonPath) ? jsonPath : path17.resolve(root, jsonPath);
3692
+ return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
3476
3693
  }
3477
3694
  var GITHUB_ANNOTATION_LIMIT = 100;
3478
3695
 
@@ -3487,6 +3704,7 @@ function parseArgs(argv, cwd) {
3487
3704
  dryRun: false,
3488
3705
  reportFormat: "md",
3489
3706
  reportRunValidate: false,
3707
+ doctorFormat: "text",
3490
3708
  validateFormat: "text",
3491
3709
  strict: false,
3492
3710
  help: false
@@ -3539,7 +3757,11 @@ function parseArgs(argv, cwd) {
3539
3757
  {
3540
3758
  const next = args[i + 1];
3541
3759
  if (next) {
3542
- options.reportOut = next;
3760
+ if (command === "doctor") {
3761
+ options.doctorOut = next;
3762
+ } else {
3763
+ options.reportOut = next;
3764
+ }
3543
3765
  }
3544
3766
  }
3545
3767
  i += 1;
@@ -3582,6 +3804,12 @@ function applyFormatOption(command, value, options) {
3582
3804
  }
3583
3805
  return;
3584
3806
  }
3807
+ if (command === "doctor") {
3808
+ if (value === "text" || value === "json") {
3809
+ options.doctorFormat = value;
3810
+ }
3811
+ return;
3812
+ }
3585
3813
  if (value === "md" || value === "json") {
3586
3814
  options.reportFormat = value;
3587
3815
  }
@@ -3629,6 +3857,14 @@ async function run(argv, cwd) {
3629
3857
  });
3630
3858
  }
3631
3859
  return;
3860
+ case "doctor":
3861
+ await runDoctor({
3862
+ root: options.root,
3863
+ rootExplicit: options.rootExplicit,
3864
+ format: options.doctorFormat,
3865
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
3866
+ });
3867
+ return;
3632
3868
  default:
3633
3869
  error(`Unknown command: ${command}`);
3634
3870
  info(usage());
@@ -3642,6 +3878,7 @@ Commands:
3642
3878
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
3643
3879
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3644
3880
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3881
+ doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
3645
3882
 
3646
3883
  Options:
3647
3884
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3651,9 +3888,10 @@ Options:
3651
3888
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3652
3889
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3653
3890
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
3891
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3654
3892
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3655
3893
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3656
- --out <path> report: \u51FA\u529B\u5148
3894
+ --out <path> report/doctor: \u51FA\u529B\u5148
3657
3895
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3658
3896
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3659
3897
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A