qfai 0.5.2 → 0.6.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.
@@ -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;
@@ -1260,116 +788,900 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1260
788
  }
1261
789
  };
1262
790
  }
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
- };
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.2".length > 0) {
874
+ return "0.6.2";
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 outDirAbs = resolvePath(root, config, "outDir");
1006
+ const rel = path7.relative(outDirAbs, validateJsonAbs);
1007
+ const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
1008
+ addCheck(checks, {
1009
+ id: "output.pathAlignment",
1010
+ severity: inside ? "ok" : "warning",
1011
+ title: "Output path alignment",
1012
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1013
+ details: {
1014
+ outDir: toRelativePath(root, outDirAbs),
1015
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1016
+ }
1017
+ });
1018
+ if (options.rootExplicit) {
1019
+ addCheck(checks, await buildOutDirCollisionCheck(root));
1020
+ }
1021
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1022
+ const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1023
+ const exclude = normalizeGlobs2([
1024
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1025
+ ...config.validation.traceability.testFileExcludeGlobs
1026
+ ]);
1027
+ try {
1028
+ const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1029
+ const matchedCount = matched.length;
1030
+ const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1031
+ addCheck(checks, {
1032
+ id: "traceability.testGlobs",
1033
+ severity,
1034
+ title: "Test file globs",
1035
+ message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
1036
+ details: {
1037
+ globs,
1038
+ excludeGlobs: exclude,
1039
+ scenarioFiles: scenarioFiles.length,
1040
+ scMustHaveTest: config.validation.traceability.scMustHaveTest
1041
+ }
1042
+ });
1043
+ } catch (error2) {
1044
+ addCheck(checks, {
1045
+ id: "traceability.testGlobs",
1046
+ severity: "error",
1047
+ title: "Test file globs",
1048
+ message: "Glob scan failed (invalid pattern or filesystem error)",
1049
+ details: { globs, excludeGlobs: exclude, error: String(error2) }
1050
+ });
1051
+ }
1052
+ return {
1053
+ tool: "qfai",
1054
+ version,
1055
+ generatedAt,
1056
+ root: toRelativePath(process.cwd(), root),
1057
+ config: {
1058
+ startDir: toRelativePath(process.cwd(), startDir),
1059
+ found: search.found,
1060
+ configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
1061
+ },
1062
+ summary: summarize(checks),
1063
+ checks
1064
+ };
1065
+ }
1066
+ var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
1067
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1068
+ "**/.pnpm/**",
1069
+ "**/tmp/**",
1070
+ "**/.mcp-tools/**"
1071
+ ];
1072
+ async function buildOutDirCollisionCheck(root) {
1073
+ try {
1074
+ const result = await detectOutDirCollisions(root);
1075
+ const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
1076
+ const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
1077
+ const collisions = result.collisions.map((item) => ({
1078
+ outDir: toRelativePath(result.monorepoRoot, item.outDir),
1079
+ roots: item.roots.map(
1080
+ (collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
1081
+ ).sort((a, b) => a.localeCompare(b))
1082
+ })).sort((a, b) => a.outDir.localeCompare(b.outDir));
1083
+ const severity = collisions.length > 0 ? "warning" : "ok";
1084
+ const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1085
+ return {
1086
+ id: "output.outDirCollision",
1087
+ severity,
1088
+ title: "OutDir collision",
1089
+ message,
1090
+ details: {
1091
+ monorepoRoot: relativeRoot,
1092
+ configRoots,
1093
+ collisions
1094
+ }
1095
+ };
1096
+ } catch (error2) {
1097
+ return {
1098
+ id: "output.outDirCollision",
1099
+ severity: "error",
1100
+ title: "OutDir collision",
1101
+ message: "OutDir collision scan failed",
1102
+ details: { error: String(error2) }
1103
+ };
1104
+ }
1105
+ }
1106
+ async function detectOutDirCollisions(root) {
1107
+ const monorepoRoot = await findMonorepoRoot(root);
1108
+ const configPaths = await collectFilesByGlobs(monorepoRoot, {
1109
+ globs: ["**/qfai.config.yaml"],
1110
+ ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1111
+ });
1112
+ const configRoots = Array.from(
1113
+ new Set(configPaths.map((configPath) => path7.dirname(configPath)))
1114
+ ).sort((a, b) => a.localeCompare(b));
1115
+ const outDirToRoots = /* @__PURE__ */ new Map();
1116
+ for (const configRoot of configRoots) {
1117
+ const { config } = await loadConfig(configRoot);
1118
+ const outDir = path7.normalize(resolvePath(configRoot, config, "outDir"));
1119
+ const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1120
+ roots.add(configRoot);
1121
+ outDirToRoots.set(outDir, roots);
1122
+ }
1123
+ const collisions = [];
1124
+ for (const [outDir, roots] of outDirToRoots.entries()) {
1125
+ if (roots.size > 1) {
1126
+ collisions.push({
1127
+ outDir,
1128
+ roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
1129
+ });
1130
+ }
1131
+ }
1132
+ return { monorepoRoot, configRoots, collisions };
1133
+ }
1134
+ async function findMonorepoRoot(startDir) {
1135
+ let current = path7.resolve(startDir);
1136
+ while (true) {
1137
+ const gitPath = path7.join(current, ".git");
1138
+ const workspacePath = path7.join(current, "pnpm-workspace.yaml");
1139
+ if (await exists4(gitPath) || await exists4(workspacePath)) {
1140
+ return current;
1141
+ }
1142
+ const parent = path7.dirname(current);
1143
+ if (parent === current) {
1144
+ break;
1145
+ }
1146
+ current = parent;
1147
+ }
1148
+ return path7.resolve(startDir);
1149
+ }
1150
+
1151
+ // src/cli/lib/logger.ts
1152
+ function info(message) {
1153
+ process.stdout.write(`${message}
1154
+ `);
1155
+ }
1156
+ function warn(message) {
1157
+ process.stdout.write(`${message}
1158
+ `);
1159
+ }
1160
+ function error(message) {
1161
+ process.stderr.write(`${message}
1162
+ `);
1163
+ }
1164
+
1165
+ // src/cli/commands/doctor.ts
1166
+ function formatDoctorText(data) {
1167
+ const lines = [];
1168
+ lines.push(
1169
+ `qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
1170
+ );
1171
+ for (const check of data.checks) {
1172
+ lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
1173
+ }
1174
+ lines.push(
1175
+ `summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
1176
+ );
1177
+ return lines.join("\n");
1178
+ }
1179
+ function formatDoctorJson(data) {
1180
+ return JSON.stringify(data, null, 2);
1181
+ }
1182
+ async function runDoctor(options) {
1183
+ const data = await createDoctorData({
1184
+ startDir: options.root,
1185
+ rootExplicit: options.rootExplicit
1186
+ });
1187
+ const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1188
+ const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1189
+ if (options.outPath) {
1190
+ const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
1191
+ await mkdir(path8.dirname(outAbs), { recursive: true });
1192
+ await writeFile(outAbs, `${output}
1193
+ `, "utf-8");
1194
+ info(`doctor: wrote ${outAbs}`);
1195
+ return exitCode;
1196
+ }
1197
+ info(output);
1198
+ return exitCode;
1199
+ }
1200
+ function shouldFailDoctor(summary, failOn) {
1201
+ if (!failOn) {
1202
+ return false;
1203
+ }
1204
+ if (failOn === "error") {
1205
+ return summary.error > 0;
1206
+ }
1207
+ return summary.warning + summary.error > 0;
1208
+ }
1209
+
1210
+ // src/cli/commands/init.ts
1211
+ import path11 from "path";
1212
+
1213
+ // src/cli/lib/fs.ts
1214
+ import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1215
+ import path9 from "path";
1216
+ async function copyTemplateTree(sourceRoot, destRoot, options) {
1217
+ const files = await collectTemplateFiles(sourceRoot);
1218
+ return copyFiles(files, sourceRoot, destRoot, options);
1219
+ }
1220
+ async function copyFiles(files, sourceRoot, destRoot, options) {
1221
+ const copied = [];
1222
+ const skipped = [];
1223
+ const conflicts = [];
1224
+ if (!options.force) {
1225
+ for (const file of files) {
1226
+ const relative = path9.relative(sourceRoot, file);
1227
+ const dest = path9.join(destRoot, relative);
1228
+ if (!await shouldWrite(dest, options.force)) {
1229
+ conflicts.push(dest);
1230
+ }
1231
+ }
1232
+ if (conflicts.length > 0) {
1233
+ throw new Error(formatConflictMessage(conflicts));
1234
+ }
1235
+ }
1236
+ for (const file of files) {
1237
+ const relative = path9.relative(sourceRoot, file);
1238
+ const dest = path9.join(destRoot, relative);
1239
+ if (!await shouldWrite(dest, options.force)) {
1240
+ skipped.push(dest);
1241
+ continue;
1242
+ }
1243
+ if (!options.dryRun) {
1244
+ await mkdir2(path9.dirname(dest), { recursive: true });
1245
+ await copyFile(file, dest);
1246
+ }
1247
+ copied.push(dest);
1248
+ }
1249
+ return { copied, skipped };
1250
+ }
1251
+ function formatConflictMessage(conflicts) {
1252
+ return [
1253
+ "\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",
1254
+ "",
1255
+ "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
1256
+ ...conflicts.map((conflict) => `- ${conflict}`),
1257
+ "",
1258
+ "\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"
1259
+ ].join("\n");
1260
+ }
1261
+ async function collectTemplateFiles(root) {
1262
+ const entries = [];
1263
+ if (!await exists5(root)) {
1264
+ return entries;
1265
+ }
1266
+ const items = await readdir3(root, { withFileTypes: true });
1267
+ for (const item of items) {
1268
+ const fullPath = path9.join(root, item.name);
1269
+ if (item.isDirectory()) {
1270
+ const nested = await collectTemplateFiles(fullPath);
1271
+ entries.push(...nested);
1272
+ continue;
1273
+ }
1274
+ if (item.isFile()) {
1275
+ entries.push(fullPath);
1276
+ }
1277
+ }
1278
+ return entries;
1279
+ }
1280
+ async function shouldWrite(target, force) {
1281
+ if (force) {
1282
+ return true;
1283
+ }
1284
+ return !await exists5(target);
1285
+ }
1286
+ async function exists5(target) {
1287
+ try {
1288
+ await access5(target);
1289
+ return true;
1290
+ } catch {
1291
+ return false;
1292
+ }
1293
+ }
1294
+
1295
+ // src/cli/lib/assets.ts
1296
+ import { existsSync } from "fs";
1297
+ import path10 from "path";
1298
+ import { fileURLToPath as fileURLToPath2 } from "url";
1299
+ function getInitAssetsDir() {
1300
+ const base = import.meta.url;
1301
+ const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1302
+ const baseDir = path10.dirname(basePath);
1303
+ const candidates = [
1304
+ path10.resolve(baseDir, "../../../assets/init"),
1305
+ path10.resolve(baseDir, "../../assets/init")
1306
+ ];
1307
+ for (const candidate of candidates) {
1308
+ if (existsSync(candidate)) {
1309
+ return candidate;
1310
+ }
1311
+ }
1312
+ throw new Error(
1313
+ [
1314
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1315
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1316
+ ...candidates.map((candidate) => `- ${candidate}`)
1317
+ ].join("\n")
1318
+ );
1319
+ }
1320
+
1321
+ // src/cli/commands/init.ts
1322
+ async function runInit(options) {
1323
+ const assetsRoot = getInitAssetsDir();
1324
+ const rootAssets = path11.join(assetsRoot, "root");
1325
+ const qfaiAssets = path11.join(assetsRoot, ".qfai");
1326
+ const destRoot = path11.resolve(options.dir);
1327
+ const destQfai = path11.join(destRoot, ".qfai");
1328
+ const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1329
+ force: options.force,
1330
+ dryRun: options.dryRun
1331
+ });
1332
+ const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1333
+ force: options.force,
1334
+ dryRun: options.dryRun
1335
+ });
1336
+ report(
1337
+ [...rootResult.copied, ...qfaiResult.copied],
1338
+ [...rootResult.skipped, ...qfaiResult.skipped],
1339
+ options.dryRun,
1340
+ "init"
1341
+ );
1342
+ }
1343
+ function report(copied, skipped, dryRun, label) {
1344
+ info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
1345
+ if (copied.length > 0) {
1346
+ info(` created: ${copied.length}`);
1347
+ }
1348
+ if (skipped.length > 0) {
1349
+ info(` skipped: ${skipped.length}`);
1350
+ }
1351
+ }
1352
+
1353
+ // src/cli/commands/report.ts
1354
+ import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
1355
+ import path18 from "path";
1356
+
1357
+ // src/core/normalize.ts
1358
+ function normalizeIssuePaths(root, issues) {
1359
+ return issues.map((issue7) => {
1360
+ if (!issue7.file) {
1361
+ return issue7;
1362
+ }
1363
+ const normalized = toRelativePath(root, issue7.file);
1364
+ if (normalized === issue7.file) {
1365
+ return issue7;
1366
+ }
1367
+ return {
1368
+ ...issue7,
1369
+ file: normalized
1370
+ };
1371
+ });
1372
+ }
1373
+ function normalizeScCoverage(root, sc) {
1374
+ const refs = {};
1375
+ for (const [scId, files] of Object.entries(sc.refs)) {
1376
+ refs[scId] = files.map((file) => toRelativePath(root, file));
1377
+ }
1378
+ return {
1379
+ ...sc,
1380
+ refs
1381
+ };
1382
+ }
1383
+ function normalizeValidationResult(root, result) {
1384
+ return {
1385
+ ...result,
1386
+ issues: normalizeIssuePaths(root, result.issues),
1387
+ traceability: {
1388
+ ...result.traceability,
1389
+ sc: normalizeScCoverage(root, result.traceability.sc)
1390
+ }
1391
+ };
1392
+ }
1393
+
1394
+ // src/core/report.ts
1395
+ import { readFile as readFile11 } from "fs/promises";
1396
+ import path17 from "path";
1397
+
1398
+ // src/core/contractIndex.ts
1399
+ import { readFile as readFile4 } from "fs/promises";
1400
+ import path12 from "path";
1401
+
1402
+ // src/core/contractsDecl.ts
1403
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1404
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1405
+ function extractDeclaredContractIds(text) {
1406
+ const ids = [];
1407
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
1408
+ const id = match[1];
1409
+ if (id) {
1410
+ ids.push(id);
1411
+ }
1412
+ }
1413
+ return ids;
1414
+ }
1415
+ function stripContractDeclarationLines(text) {
1416
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
1417
+ }
1418
+
1419
+ // src/core/contractIndex.ts
1420
+ async function buildContractIndex(root, config) {
1421
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1422
+ const uiRoot = path12.join(contractsRoot, "ui");
1423
+ const apiRoot = path12.join(contractsRoot, "api");
1424
+ const dbRoot = path12.join(contractsRoot, "db");
1425
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1426
+ collectUiContractFiles(uiRoot),
1427
+ collectApiContractFiles(apiRoot),
1428
+ collectDbContractFiles(dbRoot)
1429
+ ]);
1430
+ const index = {
1431
+ ids: /* @__PURE__ */ new Set(),
1432
+ idToFiles: /* @__PURE__ */ new Map(),
1433
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1434
+ };
1435
+ await indexContractFiles(uiFiles, index);
1436
+ await indexContractFiles(apiFiles, index);
1437
+ await indexContractFiles(dbFiles, index);
1438
+ return index;
1439
+ }
1440
+ async function indexContractFiles(files, index) {
1441
+ for (const file of files) {
1442
+ const text = await readFile4(file, "utf-8");
1443
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1444
+ }
1445
+ }
1446
+ function record(index, id, file) {
1447
+ index.ids.add(id);
1448
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1449
+ current.add(file);
1450
+ index.idToFiles.set(id, current);
1451
+ }
1452
+
1453
+ // src/core/ids.ts
1454
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1455
+ var STRICT_ID_PATTERNS = {
1456
+ SPEC: /\bSPEC-\d{4}\b/g,
1457
+ BR: /\bBR-\d{4}\b/g,
1458
+ SC: /\bSC-\d{4}\b/g,
1459
+ UI: /\bUI-\d{4}\b/g,
1460
+ API: /\bAPI-\d{4}\b/g,
1461
+ DB: /\bDB-\d{4}\b/g,
1462
+ ADR: /\bADR-\d{4}\b/g
1463
+ };
1464
+ var LOOSE_ID_PATTERNS = {
1465
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
1466
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
1467
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
1468
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1469
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1470
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1471
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1472
+ };
1473
+ function extractIds(text, prefix) {
1474
+ const pattern = STRICT_ID_PATTERNS[prefix];
1475
+ const matches = text.match(pattern);
1476
+ return unique2(matches ?? []);
1477
+ }
1478
+ function extractAllIds(text) {
1479
+ const all = [];
1480
+ ID_PREFIXES.forEach((prefix) => {
1481
+ all.push(...extractIds(text, prefix));
1482
+ });
1483
+ return unique2(all);
1484
+ }
1485
+ function extractInvalidIds(text, prefixes) {
1486
+ const invalid = [];
1487
+ for (const prefix of prefixes) {
1488
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
1489
+ for (const candidate of candidates) {
1490
+ if (!isValidId(candidate, prefix)) {
1491
+ invalid.push(candidate);
1492
+ }
1493
+ }
1494
+ }
1495
+ return unique2(invalid);
1496
+ }
1497
+ function unique2(values) {
1498
+ return Array.from(new Set(values));
1499
+ }
1500
+ function isValidId(value, prefix) {
1501
+ const pattern = STRICT_ID_PATTERNS[prefix];
1502
+ const strict = new RegExp(pattern.source);
1503
+ return strict.test(value);
1504
+ }
1505
+
1506
+ // src/core/parse/contractRefs.ts
1507
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1508
+ function parseContractRefs(text, options = {}) {
1509
+ const linePattern = buildLinePattern(options);
1510
+ const lines = [];
1511
+ for (const match of text.matchAll(linePattern)) {
1512
+ lines.push((match[1] ?? "").trim());
1279
1513
  }
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) {
1514
+ const ids = [];
1515
+ const invalidTokens = [];
1516
+ let hasNone = false;
1517
+ for (const line of lines) {
1518
+ if (line.length === 0) {
1519
+ invalidTokens.push("(empty)");
1287
1520
  continue;
1288
1521
  }
1289
- for (const scId of scIds) {
1290
- const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1291
- current.add(file);
1292
- refs.set(scId, current);
1522
+ const tokens = line.split(",").map((token) => token.trim());
1523
+ for (const token of tokens) {
1524
+ if (token.length === 0) {
1525
+ invalidTokens.push("(empty)");
1526
+ continue;
1527
+ }
1528
+ if (token === "none") {
1529
+ hasNone = true;
1530
+ continue;
1531
+ }
1532
+ if (CONTRACT_REF_ID_RE.test(token)) {
1533
+ ids.push(token);
1534
+ continue;
1535
+ }
1536
+ invalidTokens.push(token);
1293
1537
  }
1294
1538
  }
1295
1539
  return {
1296
- refs,
1297
- scan: {
1298
- globs: normalizedGlobs,
1299
- excludeGlobs: mergedExcludeGlobs,
1300
- matchedFileCount: normalizedFiles.length
1301
- }
1540
+ lines,
1541
+ ids: unique3(ids),
1542
+ invalidTokens: unique3(invalidTokens),
1543
+ hasNone
1302
1544
  };
1303
1545
  }
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
- };
1546
+ function buildLinePattern(options) {
1547
+ const prefix = options.allowCommentPrefix ? "#" : "";
1548
+ return new RegExp(
1549
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
1550
+ "gm"
1551
+ );
1326
1552
  }
1327
- function toSortedArray(values) {
1328
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1553
+ function unique3(values) {
1554
+ return Array.from(new Set(values));
1329
1555
  }
1330
- function normalizeGlobs(globs) {
1331
- return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1556
+
1557
+ // src/core/parse/markdown.ts
1558
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1559
+ function parseHeadings(md) {
1560
+ const lines = md.split(/\r?\n/);
1561
+ const headings = [];
1562
+ for (let i = 0; i < lines.length; i++) {
1563
+ const line = lines[i] ?? "";
1564
+ const match = line.match(HEADING_RE);
1565
+ if (!match) continue;
1566
+ const levelToken = match[1];
1567
+ const title = match[2];
1568
+ if (!levelToken || !title) continue;
1569
+ headings.push({
1570
+ level: levelToken.length,
1571
+ title: title.trim(),
1572
+ line: i + 1
1573
+ });
1574
+ }
1575
+ return headings;
1332
1576
  }
1333
- function formatError3(error2) {
1334
- if (error2 instanceof Error) {
1335
- return error2.message;
1577
+ function extractH2Sections(md) {
1578
+ const lines = md.split(/\r?\n/);
1579
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1580
+ const sections = /* @__PURE__ */ new Map();
1581
+ for (let i = 0; i < headings.length; i++) {
1582
+ const current = headings[i];
1583
+ if (!current) continue;
1584
+ const next = headings[i + 1];
1585
+ const startLine = current.line + 1;
1586
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1587
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1588
+ sections.set(current.title.trim(), {
1589
+ title: current.title.trim(),
1590
+ startLine,
1591
+ endLine,
1592
+ body
1593
+ });
1336
1594
  }
1337
- return String(error2);
1595
+ return sections;
1338
1596
  }
1339
1597
 
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";
1598
+ // src/core/parse/spec.ts
1599
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1600
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1601
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1602
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1603
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1604
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1605
+ function parseSpec(md, file) {
1606
+ const headings = parseHeadings(md);
1607
+ const h1 = headings.find((heading) => heading.level === 1);
1608
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1609
+ const sections = extractH2Sections(md);
1610
+ const sectionNames = new Set(Array.from(sections.keys()));
1611
+ const brSection = sections.get(BR_SECTION_TITLE);
1612
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1613
+ const startLine = brSection?.startLine ?? 1;
1614
+ const brs = [];
1615
+ const brsWithoutPriority = [];
1616
+ const brsWithInvalidPriority = [];
1617
+ for (let i = 0; i < brLines.length; i++) {
1618
+ const lineText = brLines[i] ?? "";
1619
+ const lineNumber = startLine + i;
1620
+ const validMatch = lineText.match(BR_LINE_RE);
1621
+ if (validMatch) {
1622
+ const id = validMatch[1];
1623
+ const priority = validMatch[2];
1624
+ const text = validMatch[3];
1625
+ if (!id || !priority || !text) continue;
1626
+ brs.push({
1627
+ id,
1628
+ priority,
1629
+ text: text.trim(),
1630
+ line: lineNumber
1631
+ });
1632
+ continue;
1633
+ }
1634
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1635
+ if (anyPriorityMatch) {
1636
+ const id = anyPriorityMatch[1];
1637
+ const priority = anyPriorityMatch[2];
1638
+ const text = anyPriorityMatch[3];
1639
+ if (!id || !priority || !text) continue;
1640
+ if (!VALID_PRIORITIES.has(priority)) {
1641
+ brsWithInvalidPriority.push({
1642
+ id,
1643
+ priority,
1644
+ text: text.trim(),
1645
+ line: lineNumber
1646
+ });
1647
+ }
1648
+ continue;
1649
+ }
1650
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1651
+ if (noPriorityMatch) {
1652
+ const id = noPriorityMatch[1];
1653
+ const text = noPriorityMatch[2];
1654
+ if (!id || !text) continue;
1655
+ brsWithoutPriority.push({
1656
+ id,
1657
+ text: text.trim(),
1658
+ line: lineNumber
1659
+ });
1660
+ }
1347
1661
  }
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";
1662
+ const parsed = {
1663
+ file,
1664
+ sections: sectionNames,
1665
+ brs,
1666
+ brsWithoutPriority,
1667
+ brsWithInvalidPriority,
1668
+ contractRefs: parseContractRefs(md)
1669
+ };
1670
+ if (specId) {
1671
+ parsed.specId = specId;
1356
1672
  }
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");
1673
+ return parsed;
1362
1674
  }
1363
1675
 
1364
1676
  // src/core/validators/contracts.ts
1365
1677
  import { readFile as readFile5 } from "fs/promises";
1366
- import path12 from "path";
1678
+ import path14 from "path";
1367
1679
 
1368
1680
  // src/core/contracts.ts
1369
- import path11 from "path";
1681
+ import path13 from "path";
1370
1682
  import { parse as parseYaml2 } from "yaml";
1371
1683
  function parseStructuredContract(file, text) {
1372
- const ext = path11.extname(file).toLowerCase();
1684
+ const ext = path13.extname(file).toLowerCase();
1373
1685
  if (ext === ".json") {
1374
1686
  return JSON.parse(text);
1375
1687
  }
@@ -1389,9 +1701,9 @@ var SQL_DANGEROUS_PATTERNS = [
1389
1701
  async function validateContracts(root, config) {
1390
1702
  const issues = [];
1391
1703
  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")));
1704
+ issues.push(...await validateUiContracts(path14.join(contractsRoot, "ui")));
1705
+ issues.push(...await validateApiContracts(path14.join(contractsRoot, "api")));
1706
+ issues.push(...await validateDbContracts(path14.join(contractsRoot, "db")));
1395
1707
  const contractIndex = await buildContractIndex(root, config);
1396
1708
  issues.push(...validateDuplicateContractIds(contractIndex));
1397
1709
  return issues;
@@ -1674,7 +1986,7 @@ function issue(code, message, severity, file, rule, refs) {
1674
1986
 
1675
1987
  // src/core/validators/delta.ts
1676
1988
  import { readFile as readFile6 } from "fs/promises";
1677
- import path13 from "path";
1989
+ import path15 from "path";
1678
1990
  var SECTION_RE = /^##\s+変更区分/m;
1679
1991
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1680
1992
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1688,7 +2000,7 @@ async function validateDeltas(root, config) {
1688
2000
  }
1689
2001
  const issues = [];
1690
2002
  for (const pack of packs) {
1691
- const deltaPath = path13.join(pack, "delta.md");
2003
+ const deltaPath = path15.join(pack, "delta.md");
1692
2004
  let text;
1693
2005
  try {
1694
2006
  text = await readFile6(deltaPath, "utf-8");
@@ -1764,7 +2076,7 @@ function issue2(code, message, severity, file, rule, refs) {
1764
2076
 
1765
2077
  // src/core/validators/ids.ts
1766
2078
  import { readFile as readFile7 } from "fs/promises";
1767
- import path14 from "path";
2079
+ import path16 from "path";
1768
2080
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1769
2081
  async function validateDefinedIds(root, config) {
1770
2082
  const issues = [];
@@ -1830,7 +2142,7 @@ function recordId(out, id, file) {
1830
2142
  }
1831
2143
  function formatFileList(files, root) {
1832
2144
  return files.map((file) => {
1833
- const relative = path14.relative(root, file);
2145
+ const relative = path16.relative(root, file);
1834
2146
  return relative.length > 0 ? relative : file;
1835
2147
  }).join(", ");
1836
2148
  }
@@ -2721,15 +3033,15 @@ function countIssues(issues) {
2721
3033
  // src/core/report.ts
2722
3034
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2723
3035
  async function createReportData(root, validation, configResult) {
2724
- const resolvedRoot = path15.resolve(root);
3036
+ const resolvedRoot = path17.resolve(root);
2725
3037
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2726
3038
  const config = resolved.config;
2727
3039
  const configPath = resolved.configPath;
2728
3040
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2729
3041
  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");
3042
+ const apiRoot = path17.join(contractsRoot, "api");
3043
+ const uiRoot = path17.join(contractsRoot, "ui");
3044
+ const dbRoot = path17.join(contractsRoot, "db");
2733
3045
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2734
3046
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2735
3047
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3216,7 +3528,7 @@ function buildHotspots(issues) {
3216
3528
 
3217
3529
  // src/cli/commands/report.ts
3218
3530
  async function runReport(options) {
3219
- const root = path16.resolve(options.root);
3531
+ const root = path18.resolve(options.root);
3220
3532
  const configResult = await loadConfig(root);
3221
3533
  let validation;
3222
3534
  if (options.runValidate) {
@@ -3233,7 +3545,7 @@ async function runReport(options) {
3233
3545
  validation = normalized;
3234
3546
  } else {
3235
3547
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3236
- const inputPath = path16.isAbsolute(input) ? input : path16.resolve(root, input);
3548
+ const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
3237
3549
  try {
3238
3550
  validation = await readValidationResult(inputPath);
3239
3551
  } catch (err) {
@@ -3259,11 +3571,11 @@ async function runReport(options) {
3259
3571
  const data = await createReportData(root, validation, configResult);
3260
3572
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3261
3573
  const outRoot = resolvePath(root, configResult.config, "outDir");
3262
- const defaultOut = options.format === "json" ? path16.join(outRoot, "report.json") : path16.join(outRoot, "report.md");
3574
+ const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
3263
3575
  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}
3576
+ const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
3577
+ await mkdir3(path18.dirname(outPath), { recursive: true });
3578
+ await writeFile2(outPath, `${output}
3267
3579
  `, "utf-8");
3268
3580
  info(
3269
3581
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3327,15 +3639,15 @@ function isMissingFileError5(error2) {
3327
3639
  return record2.code === "ENOENT";
3328
3640
  }
3329
3641
  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)}
3642
+ const abs = path18.isAbsolute(outputPath) ? outputPath : path18.resolve(root, outputPath);
3643
+ await mkdir3(path18.dirname(abs), { recursive: true });
3644
+ await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3333
3645
  `, "utf-8");
3334
3646
  }
3335
3647
 
3336
3648
  // src/cli/commands/validate.ts
3337
- import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3338
- import path17 from "path";
3649
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
3650
+ import path19 from "path";
3339
3651
 
3340
3652
  // src/cli/lib/failOn.ts
3341
3653
  function shouldFail(result, failOn) {
@@ -3350,7 +3662,7 @@ function shouldFail(result, failOn) {
3350
3662
 
3351
3663
  // src/cli/commands/validate.ts
3352
3664
  async function runValidate(options) {
3353
- const root = path17.resolve(options.root);
3665
+ const root = path19.resolve(options.root);
3354
3666
  const configResult = await loadConfig(root);
3355
3667
  const result = await validateProject(root, configResult);
3356
3668
  const normalized = normalizeValidationResult(root, result);
@@ -3467,12 +3779,12 @@ function issueKey(issue7) {
3467
3779
  }
3468
3780
  async function emitJson(result, root, jsonPath) {
3469
3781
  const abs = resolveJsonPath(root, jsonPath);
3470
- await mkdir3(path17.dirname(abs), { recursive: true });
3471
- await writeFile2(abs, `${JSON.stringify(result, null, 2)}
3782
+ await mkdir4(path19.dirname(abs), { recursive: true });
3783
+ await writeFile3(abs, `${JSON.stringify(result, null, 2)}
3472
3784
  `, "utf-8");
3473
3785
  }
3474
3786
  function resolveJsonPath(root, jsonPath) {
3475
- return path17.isAbsolute(jsonPath) ? jsonPath : path17.resolve(root, jsonPath);
3787
+ return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
3476
3788
  }
3477
3789
  var GITHUB_ANNOTATION_LIMIT = 100;
3478
3790
 
@@ -3487,6 +3799,7 @@ function parseArgs(argv, cwd) {
3487
3799
  dryRun: false,
3488
3800
  reportFormat: "md",
3489
3801
  reportRunValidate: false,
3802
+ doctorFormat: "text",
3490
3803
  validateFormat: "text",
3491
3804
  strict: false,
3492
3805
  help: false
@@ -3539,7 +3852,11 @@ function parseArgs(argv, cwd) {
3539
3852
  {
3540
3853
  const next = args[i + 1];
3541
3854
  if (next) {
3542
- options.reportOut = next;
3855
+ if (command === "doctor") {
3856
+ options.doctorOut = next;
3857
+ } else {
3858
+ options.reportOut = next;
3859
+ }
3543
3860
  }
3544
3861
  }
3545
3862
  i += 1;
@@ -3582,6 +3899,12 @@ function applyFormatOption(command, value, options) {
3582
3899
  }
3583
3900
  return;
3584
3901
  }
3902
+ if (command === "doctor") {
3903
+ if (value === "text" || value === "json") {
3904
+ options.doctorFormat = value;
3905
+ }
3906
+ return;
3907
+ }
3585
3908
  if (value === "md" || value === "json") {
3586
3909
  options.reportFormat = value;
3587
3910
  }
@@ -3629,6 +3952,18 @@ async function run(argv, cwd) {
3629
3952
  });
3630
3953
  }
3631
3954
  return;
3955
+ case "doctor":
3956
+ {
3957
+ const exitCode = await runDoctor({
3958
+ root: options.root,
3959
+ rootExplicit: options.rootExplicit,
3960
+ format: options.doctorFormat,
3961
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
3962
+ ...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
3963
+ });
3964
+ process.exitCode = exitCode;
3965
+ }
3966
+ return;
3632
3967
  default:
3633
3968
  error(`Unknown command: ${command}`);
3634
3969
  info(usage());
@@ -3642,6 +3977,7 @@ Commands:
3642
3977
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
3643
3978
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3644
3979
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3980
+ doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
3645
3981
 
3646
3982
  Options:
3647
3983
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3651,9 +3987,11 @@ Options:
3651
3987
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3652
3988
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3653
3989
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
3990
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3654
3991
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3655
3992
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3656
- --out <path> report: \u51FA\u529B\u5148
3993
+ --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
3994
+ --out <path> report/doctor: \u51FA\u529B\u5148
3657
3995
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3658
3996
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3659
3997
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A