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.
@@ -23,170 +23,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
- // src/cli/commands/init.ts
27
- var import_node_path3 = __toESM(require("path"), 1);
28
-
29
- // src/cli/lib/fs.ts
30
- var import_promises = require("fs/promises");
31
- var import_node_path = __toESM(require("path"), 1);
32
- async function copyTemplateTree(sourceRoot, destRoot, options) {
33
- const files = await collectTemplateFiles(sourceRoot);
34
- return copyFiles(files, sourceRoot, destRoot, options);
35
- }
36
- async function copyFiles(files, sourceRoot, destRoot, options) {
37
- const copied = [];
38
- const skipped = [];
39
- const conflicts = [];
40
- if (!options.force) {
41
- for (const file of files) {
42
- const relative = import_node_path.default.relative(sourceRoot, file);
43
- const dest = import_node_path.default.join(destRoot, relative);
44
- if (!await shouldWrite(dest, options.force)) {
45
- conflicts.push(dest);
46
- }
47
- }
48
- if (conflicts.length > 0) {
49
- throw new Error(formatConflictMessage(conflicts));
50
- }
51
- }
52
- for (const file of files) {
53
- const relative = import_node_path.default.relative(sourceRoot, file);
54
- const dest = import_node_path.default.join(destRoot, relative);
55
- if (!await shouldWrite(dest, options.force)) {
56
- skipped.push(dest);
57
- continue;
58
- }
59
- if (!options.dryRun) {
60
- await (0, import_promises.mkdir)(import_node_path.default.dirname(dest), { recursive: true });
61
- await (0, import_promises.copyFile)(file, dest);
62
- }
63
- copied.push(dest);
64
- }
65
- return { copied, skipped };
66
- }
67
- function formatConflictMessage(conflicts) {
68
- return [
69
- "\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",
70
- "",
71
- "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
72
- ...conflicts.map((conflict) => `- ${conflict}`),
73
- "",
74
- "\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"
75
- ].join("\n");
76
- }
77
- async function collectTemplateFiles(root) {
78
- const entries = [];
79
- if (!await exists(root)) {
80
- return entries;
81
- }
82
- const items = await (0, import_promises.readdir)(root, { withFileTypes: true });
83
- for (const item of items) {
84
- const fullPath = import_node_path.default.join(root, item.name);
85
- if (item.isDirectory()) {
86
- const nested = await collectTemplateFiles(fullPath);
87
- entries.push(...nested);
88
- continue;
89
- }
90
- if (item.isFile()) {
91
- entries.push(fullPath);
92
- }
93
- }
94
- return entries;
95
- }
96
- async function shouldWrite(target, force) {
97
- if (force) {
98
- return true;
99
- }
100
- return !await exists(target);
101
- }
102
- async function exists(target) {
103
- try {
104
- await (0, import_promises.access)(target);
105
- return true;
106
- } catch {
107
- return false;
108
- }
109
- }
110
-
111
- // src/cli/lib/assets.ts
112
- var import_node_fs = require("fs");
113
- var import_node_path2 = __toESM(require("path"), 1);
114
- var import_node_url = require("url");
115
- function getInitAssetsDir() {
116
- const base = __filename;
117
- const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
118
- const baseDir = import_node_path2.default.dirname(basePath);
119
- const candidates = [
120
- import_node_path2.default.resolve(baseDir, "../../../assets/init"),
121
- import_node_path2.default.resolve(baseDir, "../../assets/init")
122
- ];
123
- for (const candidate of candidates) {
124
- if ((0, import_node_fs.existsSync)(candidate)) {
125
- return candidate;
126
- }
127
- }
128
- throw new Error(
129
- [
130
- "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
131
- "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
132
- ...candidates.map((candidate) => `- ${candidate}`)
133
- ].join("\n")
134
- );
135
- }
136
-
137
- // src/cli/lib/logger.ts
138
- function info(message) {
139
- process.stdout.write(`${message}
140
- `);
141
- }
142
- function warn(message) {
143
- process.stdout.write(`${message}
144
- `);
145
- }
146
- function error(message) {
147
- process.stderr.write(`${message}
148
- `);
149
- }
150
-
151
- // src/cli/commands/init.ts
152
- async function runInit(options) {
153
- const assetsRoot = getInitAssetsDir();
154
- const rootAssets = import_node_path3.default.join(assetsRoot, "root");
155
- const qfaiAssets = import_node_path3.default.join(assetsRoot, ".qfai");
156
- const destRoot = import_node_path3.default.resolve(options.dir);
157
- const destQfai = import_node_path3.default.join(destRoot, ".qfai");
158
- const rootResult = await copyTemplateTree(rootAssets, destRoot, {
159
- force: options.force,
160
- dryRun: options.dryRun
161
- });
162
- const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
163
- force: options.force,
164
- dryRun: options.dryRun
165
- });
166
- report(
167
- [...rootResult.copied, ...qfaiResult.copied],
168
- [...rootResult.skipped, ...qfaiResult.skipped],
169
- options.dryRun,
170
- "init"
171
- );
172
- }
173
- function report(copied, skipped, dryRun, label) {
174
- info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
175
- if (copied.length > 0) {
176
- info(` created: ${copied.length}`);
177
- }
178
- if (skipped.length > 0) {
179
- info(` skipped: ${skipped.length}`);
180
- }
181
- }
26
+ // src/cli/commands/doctor.ts
27
+ var import_promises8 = require("fs/promises");
28
+ var import_node_path8 = __toESM(require("path"), 1);
182
29
 
183
- // src/cli/commands/report.ts
184
- var import_promises16 = require("fs/promises");
185
- var import_node_path16 = __toESM(require("path"), 1);
30
+ // src/core/doctor.ts
31
+ var import_promises7 = require("fs/promises");
32
+ var import_node_path7 = __toESM(require("path"), 1);
186
33
 
187
34
  // src/core/config.ts
188
- var import_promises2 = require("fs/promises");
189
- var import_node_path4 = __toESM(require("path"), 1);
35
+ var import_promises = require("fs/promises");
36
+ var import_node_path = __toESM(require("path"), 1);
190
37
  var import_yaml = require("yaml");
191
38
  var defaultConfig = {
192
39
  paths: {
@@ -226,17 +73,17 @@ var defaultConfig = {
226
73
  }
227
74
  };
228
75
  function getConfigPath(root) {
229
- return import_node_path4.default.join(root, "qfai.config.yaml");
76
+ return import_node_path.default.join(root, "qfai.config.yaml");
230
77
  }
231
78
  async function findConfigRoot(startDir) {
232
- const resolvedStart = import_node_path4.default.resolve(startDir);
79
+ const resolvedStart = import_node_path.default.resolve(startDir);
233
80
  let current = resolvedStart;
234
81
  while (true) {
235
82
  const configPath = getConfigPath(current);
236
- if (await exists2(configPath)) {
83
+ if (await exists(configPath)) {
237
84
  return { root: current, configPath, found: true };
238
85
  }
239
- const parent = import_node_path4.default.dirname(current);
86
+ const parent = import_node_path.default.dirname(current);
240
87
  if (parent === current) {
241
88
  break;
242
89
  }
@@ -253,7 +100,7 @@ async function loadConfig(root) {
253
100
  const issues = [];
254
101
  let parsed;
255
102
  try {
256
- const raw = await (0, import_promises2.readFile)(configPath, "utf-8");
103
+ const raw = await (0, import_promises.readFile)(configPath, "utf-8");
257
104
  parsed = (0, import_yaml.parse)(raw);
258
105
  } catch (error2) {
259
106
  if (isMissingFile(error2)) {
@@ -266,7 +113,7 @@ async function loadConfig(root) {
266
113
  return { config: normalized, issues, configPath };
267
114
  }
268
115
  function resolvePath(root, config, key) {
269
- return import_node_path4.default.resolve(root, config.paths[key]);
116
+ return import_node_path.default.resolve(root, config.paths[key]);
270
117
  }
271
118
  function normalizeConfig(raw, configPath, issues) {
272
119
  if (!isRecord(raw)) {
@@ -565,9 +412,9 @@ function isMissingFile(error2) {
565
412
  }
566
413
  return false;
567
414
  }
568
- async function exists2(target) {
415
+ async function exists(target) {
569
416
  try {
570
- await (0, import_promises2.access)(target);
417
+ await (0, import_promises.access)(target);
571
418
  return true;
572
419
  } catch {
573
420
  return false;
@@ -583,76 +430,12 @@ function isRecord(value) {
583
430
  return value !== null && typeof value === "object" && !Array.isArray(value);
584
431
  }
585
432
 
586
- // src/core/paths.ts
587
- var import_node_path5 = __toESM(require("path"), 1);
588
- function toRelativePath(root, target) {
589
- if (!target) {
590
- return target;
591
- }
592
- if (!import_node_path5.default.isAbsolute(target)) {
593
- return toPosixPath(target);
594
- }
595
- const relative = import_node_path5.default.relative(root, target);
596
- if (!relative) {
597
- return ".";
598
- }
599
- return toPosixPath(relative);
600
- }
601
- function toPosixPath(value) {
602
- return value.replace(/\\/g, "/");
603
- }
604
-
605
- // src/core/normalize.ts
606
- function normalizeIssuePaths(root, issues) {
607
- return issues.map((issue7) => {
608
- if (!issue7.file) {
609
- return issue7;
610
- }
611
- const normalized = toRelativePath(root, issue7.file);
612
- if (normalized === issue7.file) {
613
- return issue7;
614
- }
615
- return {
616
- ...issue7,
617
- file: normalized
618
- };
619
- });
620
- }
621
- function normalizeScCoverage(root, sc) {
622
- const refs = {};
623
- for (const [scId, files] of Object.entries(sc.refs)) {
624
- refs[scId] = files.map((file) => toRelativePath(root, file));
625
- }
626
- return {
627
- ...sc,
628
- refs
629
- };
630
- }
631
- function normalizeValidationResult(root, result) {
632
- return {
633
- ...result,
634
- issues: normalizeIssuePaths(root, result.issues),
635
- traceability: {
636
- ...result.traceability,
637
- sc: normalizeScCoverage(root, result.traceability.sc)
638
- }
639
- };
640
- }
641
-
642
- // src/core/report.ts
643
- var import_promises15 = require("fs/promises");
644
- var import_node_path15 = __toESM(require("path"), 1);
645
-
646
- // src/core/contractIndex.ts
647
- var import_promises6 = require("fs/promises");
648
- var import_node_path8 = __toESM(require("path"), 1);
649
-
650
433
  // src/core/discovery.ts
651
- var import_promises5 = require("fs/promises");
434
+ var import_promises4 = require("fs/promises");
652
435
 
653
436
  // src/core/fs.ts
654
- var import_promises3 = require("fs/promises");
655
- var import_node_path6 = __toESM(require("path"), 1);
437
+ var import_promises2 = require("fs/promises");
438
+ var import_node_path2 = __toESM(require("path"), 1);
656
439
  var import_fast_glob = __toESM(require("fast-glob"), 1);
657
440
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
658
441
  "node_modules",
@@ -664,7 +447,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
664
447
  ]);
665
448
  async function collectFiles(root, options = {}) {
666
449
  const entries = [];
667
- if (!await exists3(root)) {
450
+ if (!await exists2(root)) {
668
451
  return entries;
669
452
  }
670
453
  const ignoreDirs = /* @__PURE__ */ new Set([
@@ -688,9 +471,9 @@ async function collectFilesByGlobs(root, options) {
688
471
  });
689
472
  }
690
473
  async function walk(base, current, ignoreDirs, extensions, out) {
691
- const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
474
+ const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
692
475
  for (const item of items) {
693
- const fullPath = import_node_path6.default.join(current, item.name);
476
+ const fullPath = import_node_path2.default.join(current, item.name);
694
477
  if (item.isDirectory()) {
695
478
  if (ignoreDirs.has(item.name)) {
696
479
  continue;
@@ -700,7 +483,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
700
483
  }
701
484
  if (item.isFile()) {
702
485
  if (extensions.length > 0) {
703
- const ext = import_node_path6.default.extname(item.name).toLowerCase();
486
+ const ext = import_node_path2.default.extname(item.name).toLowerCase();
704
487
  if (!extensions.includes(ext)) {
705
488
  continue;
706
489
  }
@@ -709,9 +492,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
709
492
  }
710
493
  }
711
494
  }
712
- async function exists3(target) {
495
+ async function exists2(target) {
713
496
  try {
714
- await (0, import_promises3.access)(target);
497
+ await (0, import_promises2.access)(target);
715
498
  return true;
716
499
  } catch {
717
500
  return false;
@@ -719,23 +502,23 @@ async function exists3(target) {
719
502
  }
720
503
 
721
504
  // src/core/specLayout.ts
722
- var import_promises4 = require("fs/promises");
723
- var import_node_path7 = __toESM(require("path"), 1);
505
+ var import_promises3 = require("fs/promises");
506
+ var import_node_path3 = __toESM(require("path"), 1);
724
507
  var SPEC_DIR_RE = /^spec-\d{4}$/;
725
508
  async function collectSpecEntries(specsRoot) {
726
509
  const dirs = await listSpecDirs(specsRoot);
727
510
  const entries = dirs.map((dir) => ({
728
511
  dir,
729
- specPath: import_node_path7.default.join(dir, "spec.md"),
730
- deltaPath: import_node_path7.default.join(dir, "delta.md"),
731
- scenarioPath: import_node_path7.default.join(dir, "scenario.md")
512
+ specPath: import_node_path3.default.join(dir, "spec.md"),
513
+ deltaPath: import_node_path3.default.join(dir, "delta.md"),
514
+ scenarioPath: import_node_path3.default.join(dir, "scenario.md")
732
515
  }));
733
516
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
734
517
  }
735
518
  async function listSpecDirs(specsRoot) {
736
519
  try {
737
- const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
738
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path7.default.join(specsRoot, name));
520
+ const items = await (0, import_promises3.readdir)(specsRoot, { withFileTypes: true });
521
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, name));
739
522
  } catch (error2) {
740
523
  if (isMissingFileError(error2)) {
741
524
  return [];
@@ -783,298 +566,43 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
783
566
  async function filterExisting(files) {
784
567
  const existing = [];
785
568
  for (const file of files) {
786
- if (await exists4(file)) {
569
+ if (await exists3(file)) {
787
570
  existing.push(file);
788
571
  }
789
572
  }
790
573
  return existing;
791
574
  }
792
- async function exists4(target) {
575
+ async function exists3(target) {
793
576
  try {
794
- await (0, import_promises5.access)(target);
577
+ await (0, import_promises4.access)(target);
795
578
  return true;
796
579
  } catch {
797
580
  return false;
798
581
  }
799
582
  }
800
583
 
801
- // src/core/contractsDecl.ts
802
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
803
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
804
- function extractDeclaredContractIds(text) {
805
- const ids = [];
806
- for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
807
- const id = match[1];
808
- if (id) {
809
- ids.push(id);
810
- }
584
+ // src/core/paths.ts
585
+ var import_node_path4 = __toESM(require("path"), 1);
586
+ function toRelativePath(root, target) {
587
+ if (!target) {
588
+ return target;
811
589
  }
812
- return ids;
813
- }
814
- function stripContractDeclarationLines(text) {
815
- return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
590
+ if (!import_node_path4.default.isAbsolute(target)) {
591
+ return toPosixPath(target);
592
+ }
593
+ const relative = import_node_path4.default.relative(root, target);
594
+ if (!relative) {
595
+ return ".";
596
+ }
597
+ return toPosixPath(relative);
816
598
  }
817
-
818
- // src/core/contractIndex.ts
819
- async function buildContractIndex(root, config) {
820
- const contractsRoot = resolvePath(root, config, "contractsDir");
821
- const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
822
- const apiRoot = import_node_path8.default.join(contractsRoot, "api");
823
- const dbRoot = import_node_path8.default.join(contractsRoot, "db");
824
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
825
- collectUiContractFiles(uiRoot),
826
- collectApiContractFiles(apiRoot),
827
- collectDbContractFiles(dbRoot)
828
- ]);
829
- const index = {
830
- ids: /* @__PURE__ */ new Set(),
831
- idToFiles: /* @__PURE__ */ new Map(),
832
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
833
- };
834
- await indexContractFiles(uiFiles, index);
835
- await indexContractFiles(apiFiles, index);
836
- await indexContractFiles(dbFiles, index);
837
- return index;
838
- }
839
- async function indexContractFiles(files, index) {
840
- for (const file of files) {
841
- const text = await (0, import_promises6.readFile)(file, "utf-8");
842
- extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
843
- }
844
- }
845
- function record(index, id, file) {
846
- index.ids.add(id);
847
- const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
848
- current.add(file);
849
- index.idToFiles.set(id, current);
850
- }
851
-
852
- // src/core/ids.ts
853
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
854
- var STRICT_ID_PATTERNS = {
855
- SPEC: /\bSPEC-\d{4}\b/g,
856
- BR: /\bBR-\d{4}\b/g,
857
- SC: /\bSC-\d{4}\b/g,
858
- UI: /\bUI-\d{4}\b/g,
859
- API: /\bAPI-\d{4}\b/g,
860
- DB: /\bDB-\d{4}\b/g,
861
- ADR: /\bADR-\d{4}\b/g
862
- };
863
- var LOOSE_ID_PATTERNS = {
864
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
865
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
866
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
867
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
868
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
869
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
870
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
871
- };
872
- function extractIds(text, prefix) {
873
- const pattern = STRICT_ID_PATTERNS[prefix];
874
- const matches = text.match(pattern);
875
- return unique(matches ?? []);
876
- }
877
- function extractAllIds(text) {
878
- const all = [];
879
- ID_PREFIXES.forEach((prefix) => {
880
- all.push(...extractIds(text, prefix));
881
- });
882
- return unique(all);
883
- }
884
- function extractInvalidIds(text, prefixes) {
885
- const invalid = [];
886
- for (const prefix of prefixes) {
887
- const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
888
- for (const candidate of candidates) {
889
- if (!isValidId(candidate, prefix)) {
890
- invalid.push(candidate);
891
- }
892
- }
893
- }
894
- return unique(invalid);
895
- }
896
- function unique(values) {
897
- return Array.from(new Set(values));
898
- }
899
- function isValidId(value, prefix) {
900
- const pattern = STRICT_ID_PATTERNS[prefix];
901
- const strict = new RegExp(pattern.source);
902
- return strict.test(value);
903
- }
904
-
905
- // src/core/parse/contractRefs.ts
906
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
907
- function parseContractRefs(text, options = {}) {
908
- const linePattern = buildLinePattern(options);
909
- const lines = [];
910
- for (const match of text.matchAll(linePattern)) {
911
- lines.push((match[1] ?? "").trim());
912
- }
913
- const ids = [];
914
- const invalidTokens = [];
915
- let hasNone = false;
916
- for (const line of lines) {
917
- if (line.length === 0) {
918
- invalidTokens.push("(empty)");
919
- continue;
920
- }
921
- const tokens = line.split(",").map((token) => token.trim());
922
- for (const token of tokens) {
923
- if (token.length === 0) {
924
- invalidTokens.push("(empty)");
925
- continue;
926
- }
927
- if (token === "none") {
928
- hasNone = true;
929
- continue;
930
- }
931
- if (CONTRACT_REF_ID_RE.test(token)) {
932
- ids.push(token);
933
- continue;
934
- }
935
- invalidTokens.push(token);
936
- }
937
- }
938
- return {
939
- lines,
940
- ids: unique2(ids),
941
- invalidTokens: unique2(invalidTokens),
942
- hasNone
943
- };
944
- }
945
- function buildLinePattern(options) {
946
- const prefix = options.allowCommentPrefix ? "#" : "";
947
- return new RegExp(
948
- `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
949
- "gm"
950
- );
951
- }
952
- function unique2(values) {
953
- return Array.from(new Set(values));
954
- }
955
-
956
- // src/core/parse/markdown.ts
957
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
958
- function parseHeadings(md) {
959
- const lines = md.split(/\r?\n/);
960
- const headings = [];
961
- for (let i = 0; i < lines.length; i++) {
962
- const line = lines[i] ?? "";
963
- const match = line.match(HEADING_RE);
964
- if (!match) continue;
965
- const levelToken = match[1];
966
- const title = match[2];
967
- if (!levelToken || !title) continue;
968
- headings.push({
969
- level: levelToken.length,
970
- title: title.trim(),
971
- line: i + 1
972
- });
973
- }
974
- return headings;
975
- }
976
- function extractH2Sections(md) {
977
- const lines = md.split(/\r?\n/);
978
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
979
- const sections = /* @__PURE__ */ new Map();
980
- for (let i = 0; i < headings.length; i++) {
981
- const current = headings[i];
982
- if (!current) continue;
983
- const next = headings[i + 1];
984
- const startLine = current.line + 1;
985
- const endLine = (next?.line ?? lines.length + 1) - 1;
986
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
987
- sections.set(current.title.trim(), {
988
- title: current.title.trim(),
989
- startLine,
990
- endLine,
991
- body
992
- });
993
- }
994
- return sections;
995
- }
996
-
997
- // src/core/parse/spec.ts
998
- var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
999
- var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1000
- var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1001
- var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1002
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1003
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1004
- function parseSpec(md, file) {
1005
- const headings = parseHeadings(md);
1006
- const h1 = headings.find((heading) => heading.level === 1);
1007
- const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1008
- const sections = extractH2Sections(md);
1009
- const sectionNames = new Set(Array.from(sections.keys()));
1010
- const brSection = sections.get(BR_SECTION_TITLE);
1011
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1012
- const startLine = brSection?.startLine ?? 1;
1013
- const brs = [];
1014
- const brsWithoutPriority = [];
1015
- const brsWithInvalidPriority = [];
1016
- for (let i = 0; i < brLines.length; i++) {
1017
- const lineText = brLines[i] ?? "";
1018
- const lineNumber = startLine + i;
1019
- const validMatch = lineText.match(BR_LINE_RE);
1020
- if (validMatch) {
1021
- const id = validMatch[1];
1022
- const priority = validMatch[2];
1023
- const text = validMatch[3];
1024
- if (!id || !priority || !text) continue;
1025
- brs.push({
1026
- id,
1027
- priority,
1028
- text: text.trim(),
1029
- line: lineNumber
1030
- });
1031
- continue;
1032
- }
1033
- const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1034
- if (anyPriorityMatch) {
1035
- const id = anyPriorityMatch[1];
1036
- const priority = anyPriorityMatch[2];
1037
- const text = anyPriorityMatch[3];
1038
- if (!id || !priority || !text) continue;
1039
- if (!VALID_PRIORITIES.has(priority)) {
1040
- brsWithInvalidPriority.push({
1041
- id,
1042
- priority,
1043
- text: text.trim(),
1044
- line: lineNumber
1045
- });
1046
- }
1047
- continue;
1048
- }
1049
- const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1050
- if (noPriorityMatch) {
1051
- const id = noPriorityMatch[1];
1052
- const text = noPriorityMatch[2];
1053
- if (!id || !text) continue;
1054
- brsWithoutPriority.push({
1055
- id,
1056
- text: text.trim(),
1057
- line: lineNumber
1058
- });
1059
- }
1060
- }
1061
- const parsed = {
1062
- file,
1063
- sections: sectionNames,
1064
- brs,
1065
- brsWithoutPriority,
1066
- brsWithInvalidPriority,
1067
- contractRefs: parseContractRefs(md)
1068
- };
1069
- if (specId) {
1070
- parsed.specId = specId;
1071
- }
1072
- return parsed;
599
+ function toPosixPath(value) {
600
+ return value.replace(/\\/g, "/");
1073
601
  }
1074
602
 
1075
603
  // src/core/traceability.ts
1076
- var import_promises7 = require("fs/promises");
1077
- var import_node_path9 = __toESM(require("path"), 1);
604
+ var import_promises5 = require("fs/promises");
605
+ var import_node_path5 = __toESM(require("path"), 1);
1078
606
 
1079
607
  // src/core/gherkin/parse.ts
1080
608
  var import_gherkin = require("@cucumber/gherkin");
@@ -1130,13 +658,13 @@ function parseScenarioDocument(text, uri) {
1130
658
  };
1131
659
  }
1132
660
  function buildScenarioAtoms(document, contractIds = []) {
1133
- const uniqueContractIds = unique3(contractIds).sort(
661
+ const uniqueContractIds = unique(contractIds).sort(
1134
662
  (a, b) => a.localeCompare(b)
1135
663
  );
1136
664
  return document.scenarios.map((scenario) => {
1137
665
  const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
1138
666
  const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
1139
- const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
667
+ const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
1140
668
  const atom = {
1141
669
  uri: document.uri,
1142
670
  featureName: document.featureName ?? "",
@@ -1196,7 +724,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
1196
724
  function collectTagNames(tags) {
1197
725
  return tags.map((tag) => tag.name.replace(/^@/, ""));
1198
726
  }
1199
- function unique3(values) {
727
+ function unique(values) {
1200
728
  return Array.from(new Set(values));
1201
729
  }
1202
730
 
@@ -1226,7 +754,7 @@ function extractAnnotatedScIds(text) {
1226
754
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
1227
755
  const scIds = /* @__PURE__ */ new Set();
1228
756
  for (const file of scenarioFiles) {
1229
- const text = await (0, import_promises7.readFile)(file, "utf-8");
757
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
1230
758
  const { document, errors } = parseScenarioDocument(text, file);
1231
759
  if (!document || errors.length > 0) {
1232
760
  continue;
@@ -1244,7 +772,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
1244
772
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1245
773
  const sources = /* @__PURE__ */ new Map();
1246
774
  for (const file of scenarioFiles) {
1247
- const text = await (0, import_promises7.readFile)(file, "utf-8");
775
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
1248
776
  const { document, errors } = parseScenarioDocument(text, file);
1249
777
  if (!document || errors.length > 0) {
1250
778
  continue;
@@ -1296,99 +824,883 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1296
824
  error: formatError3(error2)
1297
825
  };
1298
826
  }
1299
- const normalizedFiles = Array.from(
1300
- new Set(files.map((file) => import_node_path9.default.normalize(file)))
1301
- );
1302
- for (const file of normalizedFiles) {
1303
- const text = await (0, import_promises7.readFile)(file, "utf-8");
1304
- const scIds = extractAnnotatedScIds(text);
1305
- if (scIds.length === 0) {
827
+ const normalizedFiles = Array.from(
828
+ new Set(files.map((file) => import_node_path5.default.normalize(file)))
829
+ );
830
+ for (const file of normalizedFiles) {
831
+ const text = await (0, import_promises5.readFile)(file, "utf-8");
832
+ const scIds = extractAnnotatedScIds(text);
833
+ if (scIds.length === 0) {
834
+ continue;
835
+ }
836
+ for (const scId of scIds) {
837
+ const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
838
+ current.add(file);
839
+ refs.set(scId, current);
840
+ }
841
+ }
842
+ return {
843
+ refs,
844
+ scan: {
845
+ globs: normalizedGlobs,
846
+ excludeGlobs: mergedExcludeGlobs,
847
+ matchedFileCount: normalizedFiles.length
848
+ }
849
+ };
850
+ }
851
+ function buildScCoverage(scIds, refs) {
852
+ const sortedScIds = toSortedArray(scIds);
853
+ const refsRecord = {};
854
+ const missingIds = [];
855
+ let covered = 0;
856
+ for (const scId of sortedScIds) {
857
+ const files = refs.get(scId);
858
+ const sortedFiles = files ? toSortedArray(files) : [];
859
+ refsRecord[scId] = sortedFiles;
860
+ if (sortedFiles.length === 0) {
861
+ missingIds.push(scId);
862
+ } else {
863
+ covered += 1;
864
+ }
865
+ }
866
+ return {
867
+ total: sortedScIds.length,
868
+ covered,
869
+ missing: missingIds.length,
870
+ missingIds,
871
+ refs: refsRecord
872
+ };
873
+ }
874
+ function toSortedArray(values) {
875
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
876
+ }
877
+ function normalizeGlobs(globs) {
878
+ return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
879
+ }
880
+ function formatError3(error2) {
881
+ if (error2 instanceof Error) {
882
+ return error2.message;
883
+ }
884
+ return String(error2);
885
+ }
886
+
887
+ // src/core/version.ts
888
+ var import_promises6 = require("fs/promises");
889
+ var import_node_path6 = __toESM(require("path"), 1);
890
+ var import_node_url = require("url");
891
+ async function resolveToolVersion() {
892
+ if ("0.6.2".length > 0) {
893
+ return "0.6.2";
894
+ }
895
+ try {
896
+ const packagePath = resolvePackageJsonPath();
897
+ const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
898
+ const parsed = JSON.parse(raw);
899
+ const version = typeof parsed.version === "string" ? parsed.version : "";
900
+ return version.length > 0 ? version : "unknown";
901
+ } catch {
902
+ return "unknown";
903
+ }
904
+ }
905
+ function resolvePackageJsonPath() {
906
+ const base = __filename;
907
+ const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
908
+ return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
909
+ }
910
+
911
+ // src/core/doctor.ts
912
+ async function exists4(target) {
913
+ try {
914
+ await (0, import_promises7.access)(target);
915
+ return true;
916
+ } catch {
917
+ return false;
918
+ }
919
+ }
920
+ function addCheck(checks, check) {
921
+ checks.push(check);
922
+ }
923
+ function summarize(checks) {
924
+ const summary = { ok: 0, warning: 0, error: 0 };
925
+ for (const check of checks) {
926
+ summary[check.severity] += 1;
927
+ }
928
+ return summary;
929
+ }
930
+ function normalizeGlobs2(values) {
931
+ return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
932
+ }
933
+ async function createDoctorData(options) {
934
+ const startDir = import_node_path7.default.resolve(options.startDir);
935
+ const checks = [];
936
+ const configPath = getConfigPath(startDir);
937
+ const search = options.rootExplicit ? {
938
+ root: startDir,
939
+ configPath,
940
+ found: await exists4(configPath)
941
+ } : await findConfigRoot(startDir);
942
+ const root = search.root;
943
+ const version = await resolveToolVersion();
944
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
945
+ addCheck(checks, {
946
+ id: "config.search",
947
+ severity: search.found ? "ok" : "warning",
948
+ title: "Config search",
949
+ message: search.found ? "qfai.config.yaml found" : "qfai.config.yaml not found (default config will be used)",
950
+ details: { configPath: toRelativePath(root, search.configPath) }
951
+ });
952
+ const {
953
+ config,
954
+ issues,
955
+ configPath: resolvedConfigPath
956
+ } = await loadConfig(root);
957
+ if (issues.length === 0) {
958
+ addCheck(checks, {
959
+ id: "config.load",
960
+ severity: "ok",
961
+ title: "Config load",
962
+ message: "Loaded and normalized with 0 issues",
963
+ details: { configPath: toRelativePath(root, resolvedConfigPath) }
964
+ });
965
+ } else {
966
+ addCheck(checks, {
967
+ id: "config.load",
968
+ severity: "warning",
969
+ title: "Config load",
970
+ message: `Loaded with ${issues.length} issue(s) (normalized with defaults when needed)`,
971
+ details: {
972
+ configPath: toRelativePath(root, resolvedConfigPath),
973
+ issues
974
+ }
975
+ });
976
+ }
977
+ const pathKeys = [
978
+ "specsDir",
979
+ "contractsDir",
980
+ "outDir",
981
+ "srcDir",
982
+ "testsDir",
983
+ "rulesDir",
984
+ "promptsDir"
985
+ ];
986
+ for (const key of pathKeys) {
987
+ const resolved = resolvePath(root, config, key);
988
+ const ok = await exists4(resolved);
989
+ addCheck(checks, {
990
+ id: `paths.${key}`,
991
+ severity: ok ? "ok" : "warning",
992
+ title: `Path exists: ${key}`,
993
+ message: ok ? `${key} exists` : `${key} is missing (did you run 'qfai init'?)`,
994
+ details: { path: toRelativePath(root, resolved) }
995
+ });
996
+ }
997
+ const specsRoot = resolvePath(root, config, "specsDir");
998
+ const entries = await collectSpecEntries(specsRoot);
999
+ let missingFiles = 0;
1000
+ for (const entry of entries) {
1001
+ const requiredFiles = [entry.specPath, entry.deltaPath, entry.scenarioPath];
1002
+ for (const filePath of requiredFiles) {
1003
+ if (!await exists4(filePath)) {
1004
+ missingFiles += 1;
1005
+ }
1006
+ }
1007
+ }
1008
+ addCheck(checks, {
1009
+ id: "spec.layout",
1010
+ severity: missingFiles === 0 ? "ok" : "warning",
1011
+ title: "Spec pack shape",
1012
+ message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1013
+ details: { specPacks: entries.length, missingFiles }
1014
+ });
1015
+ const validateJsonAbs = import_node_path7.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path7.default.resolve(root, config.output.validateJsonPath);
1016
+ const validateJsonExists = await exists4(validateJsonAbs);
1017
+ addCheck(checks, {
1018
+ id: "output.validateJson",
1019
+ severity: validateJsonExists ? "ok" : "warning",
1020
+ title: "validate.json",
1021
+ message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
1022
+ details: { path: toRelativePath(root, validateJsonAbs) }
1023
+ });
1024
+ const outDirAbs = resolvePath(root, config, "outDir");
1025
+ const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
1026
+ const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
1027
+ addCheck(checks, {
1028
+ id: "output.pathAlignment",
1029
+ severity: inside ? "ok" : "warning",
1030
+ title: "Output path alignment",
1031
+ message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
1032
+ details: {
1033
+ outDir: toRelativePath(root, outDirAbs),
1034
+ validateJsonPath: toRelativePath(root, validateJsonAbs)
1035
+ }
1036
+ });
1037
+ if (options.rootExplicit) {
1038
+ addCheck(checks, await buildOutDirCollisionCheck(root));
1039
+ }
1040
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
1041
+ const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
1042
+ const exclude = normalizeGlobs2([
1043
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1044
+ ...config.validation.traceability.testFileExcludeGlobs
1045
+ ]);
1046
+ try {
1047
+ const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
1048
+ const matchedCount = matched.length;
1049
+ const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
1050
+ addCheck(checks, {
1051
+ id: "traceability.testGlobs",
1052
+ severity,
1053
+ title: "Test file globs",
1054
+ message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
1055
+ details: {
1056
+ globs,
1057
+ excludeGlobs: exclude,
1058
+ scenarioFiles: scenarioFiles.length,
1059
+ scMustHaveTest: config.validation.traceability.scMustHaveTest
1060
+ }
1061
+ });
1062
+ } catch (error2) {
1063
+ addCheck(checks, {
1064
+ id: "traceability.testGlobs",
1065
+ severity: "error",
1066
+ title: "Test file globs",
1067
+ message: "Glob scan failed (invalid pattern or filesystem error)",
1068
+ details: { globs, excludeGlobs: exclude, error: String(error2) }
1069
+ });
1070
+ }
1071
+ return {
1072
+ tool: "qfai",
1073
+ version,
1074
+ generatedAt,
1075
+ root: toRelativePath(process.cwd(), root),
1076
+ config: {
1077
+ startDir: toRelativePath(process.cwd(), startDir),
1078
+ found: search.found,
1079
+ configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
1080
+ },
1081
+ summary: summarize(checks),
1082
+ checks
1083
+ };
1084
+ }
1085
+ var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
1086
+ ...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
1087
+ "**/.pnpm/**",
1088
+ "**/tmp/**",
1089
+ "**/.mcp-tools/**"
1090
+ ];
1091
+ async function buildOutDirCollisionCheck(root) {
1092
+ try {
1093
+ const result = await detectOutDirCollisions(root);
1094
+ const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
1095
+ const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
1096
+ const collisions = result.collisions.map((item) => ({
1097
+ outDir: toRelativePath(result.monorepoRoot, item.outDir),
1098
+ roots: item.roots.map(
1099
+ (collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
1100
+ ).sort((a, b) => a.localeCompare(b))
1101
+ })).sort((a, b) => a.outDir.localeCompare(b.outDir));
1102
+ const severity = collisions.length > 0 ? "warning" : "ok";
1103
+ const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
1104
+ return {
1105
+ id: "output.outDirCollision",
1106
+ severity,
1107
+ title: "OutDir collision",
1108
+ message,
1109
+ details: {
1110
+ monorepoRoot: relativeRoot,
1111
+ configRoots,
1112
+ collisions
1113
+ }
1114
+ };
1115
+ } catch (error2) {
1116
+ return {
1117
+ id: "output.outDirCollision",
1118
+ severity: "error",
1119
+ title: "OutDir collision",
1120
+ message: "OutDir collision scan failed",
1121
+ details: { error: String(error2) }
1122
+ };
1123
+ }
1124
+ }
1125
+ async function detectOutDirCollisions(root) {
1126
+ const monorepoRoot = await findMonorepoRoot(root);
1127
+ const configPaths = await collectFilesByGlobs(monorepoRoot, {
1128
+ globs: ["**/qfai.config.yaml"],
1129
+ ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1130
+ });
1131
+ const configRoots = Array.from(
1132
+ new Set(configPaths.map((configPath) => import_node_path7.default.dirname(configPath)))
1133
+ ).sort((a, b) => a.localeCompare(b));
1134
+ const outDirToRoots = /* @__PURE__ */ new Map();
1135
+ for (const configRoot of configRoots) {
1136
+ const { config } = await loadConfig(configRoot);
1137
+ const outDir = import_node_path7.default.normalize(resolvePath(configRoot, config, "outDir"));
1138
+ const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1139
+ roots.add(configRoot);
1140
+ outDirToRoots.set(outDir, roots);
1141
+ }
1142
+ const collisions = [];
1143
+ for (const [outDir, roots] of outDirToRoots.entries()) {
1144
+ if (roots.size > 1) {
1145
+ collisions.push({
1146
+ outDir,
1147
+ roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
1148
+ });
1149
+ }
1150
+ }
1151
+ return { monorepoRoot, configRoots, collisions };
1152
+ }
1153
+ async function findMonorepoRoot(startDir) {
1154
+ let current = import_node_path7.default.resolve(startDir);
1155
+ while (true) {
1156
+ const gitPath = import_node_path7.default.join(current, ".git");
1157
+ const workspacePath = import_node_path7.default.join(current, "pnpm-workspace.yaml");
1158
+ if (await exists4(gitPath) || await exists4(workspacePath)) {
1159
+ return current;
1160
+ }
1161
+ const parent = import_node_path7.default.dirname(current);
1162
+ if (parent === current) {
1163
+ break;
1164
+ }
1165
+ current = parent;
1166
+ }
1167
+ return import_node_path7.default.resolve(startDir);
1168
+ }
1169
+
1170
+ // src/cli/lib/logger.ts
1171
+ function info(message) {
1172
+ process.stdout.write(`${message}
1173
+ `);
1174
+ }
1175
+ function warn(message) {
1176
+ process.stdout.write(`${message}
1177
+ `);
1178
+ }
1179
+ function error(message) {
1180
+ process.stderr.write(`${message}
1181
+ `);
1182
+ }
1183
+
1184
+ // src/cli/commands/doctor.ts
1185
+ function formatDoctorText(data) {
1186
+ const lines = [];
1187
+ lines.push(
1188
+ `qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
1189
+ );
1190
+ for (const check of data.checks) {
1191
+ lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
1192
+ }
1193
+ lines.push(
1194
+ `summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
1195
+ );
1196
+ return lines.join("\n");
1197
+ }
1198
+ function formatDoctorJson(data) {
1199
+ return JSON.stringify(data, null, 2);
1200
+ }
1201
+ async function runDoctor(options) {
1202
+ const data = await createDoctorData({
1203
+ startDir: options.root,
1204
+ rootExplicit: options.rootExplicit
1205
+ });
1206
+ const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1207
+ const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1208
+ if (options.outPath) {
1209
+ const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
1210
+ await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
1211
+ await (0, import_promises8.writeFile)(outAbs, `${output}
1212
+ `, "utf-8");
1213
+ info(`doctor: wrote ${outAbs}`);
1214
+ return exitCode;
1215
+ }
1216
+ info(output);
1217
+ return exitCode;
1218
+ }
1219
+ function shouldFailDoctor(summary, failOn) {
1220
+ if (!failOn) {
1221
+ return false;
1222
+ }
1223
+ if (failOn === "error") {
1224
+ return summary.error > 0;
1225
+ }
1226
+ return summary.warning + summary.error > 0;
1227
+ }
1228
+
1229
+ // src/cli/commands/init.ts
1230
+ var import_node_path11 = __toESM(require("path"), 1);
1231
+
1232
+ // src/cli/lib/fs.ts
1233
+ var import_promises9 = require("fs/promises");
1234
+ var import_node_path9 = __toESM(require("path"), 1);
1235
+ async function copyTemplateTree(sourceRoot, destRoot, options) {
1236
+ const files = await collectTemplateFiles(sourceRoot);
1237
+ return copyFiles(files, sourceRoot, destRoot, options);
1238
+ }
1239
+ async function copyFiles(files, sourceRoot, destRoot, options) {
1240
+ const copied = [];
1241
+ const skipped = [];
1242
+ const conflicts = [];
1243
+ if (!options.force) {
1244
+ for (const file of files) {
1245
+ const relative = import_node_path9.default.relative(sourceRoot, file);
1246
+ const dest = import_node_path9.default.join(destRoot, relative);
1247
+ if (!await shouldWrite(dest, options.force)) {
1248
+ conflicts.push(dest);
1249
+ }
1250
+ }
1251
+ if (conflicts.length > 0) {
1252
+ throw new Error(formatConflictMessage(conflicts));
1253
+ }
1254
+ }
1255
+ for (const file of files) {
1256
+ const relative = import_node_path9.default.relative(sourceRoot, file);
1257
+ const dest = import_node_path9.default.join(destRoot, relative);
1258
+ if (!await shouldWrite(dest, options.force)) {
1259
+ skipped.push(dest);
1260
+ continue;
1261
+ }
1262
+ if (!options.dryRun) {
1263
+ await (0, import_promises9.mkdir)(import_node_path9.default.dirname(dest), { recursive: true });
1264
+ await (0, import_promises9.copyFile)(file, dest);
1265
+ }
1266
+ copied.push(dest);
1267
+ }
1268
+ return { copied, skipped };
1269
+ }
1270
+ function formatConflictMessage(conflicts) {
1271
+ return [
1272
+ "\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",
1273
+ "",
1274
+ "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
1275
+ ...conflicts.map((conflict) => `- ${conflict}`),
1276
+ "",
1277
+ "\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"
1278
+ ].join("\n");
1279
+ }
1280
+ async function collectTemplateFiles(root) {
1281
+ const entries = [];
1282
+ if (!await exists5(root)) {
1283
+ return entries;
1284
+ }
1285
+ const items = await (0, import_promises9.readdir)(root, { withFileTypes: true });
1286
+ for (const item of items) {
1287
+ const fullPath = import_node_path9.default.join(root, item.name);
1288
+ if (item.isDirectory()) {
1289
+ const nested = await collectTemplateFiles(fullPath);
1290
+ entries.push(...nested);
1291
+ continue;
1292
+ }
1293
+ if (item.isFile()) {
1294
+ entries.push(fullPath);
1295
+ }
1296
+ }
1297
+ return entries;
1298
+ }
1299
+ async function shouldWrite(target, force) {
1300
+ if (force) {
1301
+ return true;
1302
+ }
1303
+ return !await exists5(target);
1304
+ }
1305
+ async function exists5(target) {
1306
+ try {
1307
+ await (0, import_promises9.access)(target);
1308
+ return true;
1309
+ } catch {
1310
+ return false;
1311
+ }
1312
+ }
1313
+
1314
+ // src/cli/lib/assets.ts
1315
+ var import_node_fs = require("fs");
1316
+ var import_node_path10 = __toESM(require("path"), 1);
1317
+ var import_node_url2 = require("url");
1318
+ function getInitAssetsDir() {
1319
+ const base = __filename;
1320
+ const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1321
+ const baseDir = import_node_path10.default.dirname(basePath);
1322
+ const candidates = [
1323
+ import_node_path10.default.resolve(baseDir, "../../../assets/init"),
1324
+ import_node_path10.default.resolve(baseDir, "../../assets/init")
1325
+ ];
1326
+ for (const candidate of candidates) {
1327
+ if ((0, import_node_fs.existsSync)(candidate)) {
1328
+ return candidate;
1329
+ }
1330
+ }
1331
+ throw new Error(
1332
+ [
1333
+ "init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
1334
+ "\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
1335
+ ...candidates.map((candidate) => `- ${candidate}`)
1336
+ ].join("\n")
1337
+ );
1338
+ }
1339
+
1340
+ // src/cli/commands/init.ts
1341
+ async function runInit(options) {
1342
+ const assetsRoot = getInitAssetsDir();
1343
+ const rootAssets = import_node_path11.default.join(assetsRoot, "root");
1344
+ const qfaiAssets = import_node_path11.default.join(assetsRoot, ".qfai");
1345
+ const destRoot = import_node_path11.default.resolve(options.dir);
1346
+ const destQfai = import_node_path11.default.join(destRoot, ".qfai");
1347
+ const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1348
+ force: options.force,
1349
+ dryRun: options.dryRun
1350
+ });
1351
+ const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
1352
+ force: options.force,
1353
+ dryRun: options.dryRun
1354
+ });
1355
+ report(
1356
+ [...rootResult.copied, ...qfaiResult.copied],
1357
+ [...rootResult.skipped, ...qfaiResult.skipped],
1358
+ options.dryRun,
1359
+ "init"
1360
+ );
1361
+ }
1362
+ function report(copied, skipped, dryRun, label) {
1363
+ info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
1364
+ if (copied.length > 0) {
1365
+ info(` created: ${copied.length}`);
1366
+ }
1367
+ if (skipped.length > 0) {
1368
+ info(` skipped: ${skipped.length}`);
1369
+ }
1370
+ }
1371
+
1372
+ // src/cli/commands/report.ts
1373
+ var import_promises18 = require("fs/promises");
1374
+ var import_node_path18 = __toESM(require("path"), 1);
1375
+
1376
+ // src/core/normalize.ts
1377
+ function normalizeIssuePaths(root, issues) {
1378
+ return issues.map((issue7) => {
1379
+ if (!issue7.file) {
1380
+ return issue7;
1381
+ }
1382
+ const normalized = toRelativePath(root, issue7.file);
1383
+ if (normalized === issue7.file) {
1384
+ return issue7;
1385
+ }
1386
+ return {
1387
+ ...issue7,
1388
+ file: normalized
1389
+ };
1390
+ });
1391
+ }
1392
+ function normalizeScCoverage(root, sc) {
1393
+ const refs = {};
1394
+ for (const [scId, files] of Object.entries(sc.refs)) {
1395
+ refs[scId] = files.map((file) => toRelativePath(root, file));
1396
+ }
1397
+ return {
1398
+ ...sc,
1399
+ refs
1400
+ };
1401
+ }
1402
+ function normalizeValidationResult(root, result) {
1403
+ return {
1404
+ ...result,
1405
+ issues: normalizeIssuePaths(root, result.issues),
1406
+ traceability: {
1407
+ ...result.traceability,
1408
+ sc: normalizeScCoverage(root, result.traceability.sc)
1409
+ }
1410
+ };
1411
+ }
1412
+
1413
+ // src/core/report.ts
1414
+ var import_promises17 = require("fs/promises");
1415
+ var import_node_path17 = __toESM(require("path"), 1);
1416
+
1417
+ // src/core/contractIndex.ts
1418
+ var import_promises10 = require("fs/promises");
1419
+ var import_node_path12 = __toESM(require("path"), 1);
1420
+
1421
+ // src/core/contractsDecl.ts
1422
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
1423
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
1424
+ function extractDeclaredContractIds(text) {
1425
+ const ids = [];
1426
+ for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
1427
+ const id = match[1];
1428
+ if (id) {
1429
+ ids.push(id);
1430
+ }
1431
+ }
1432
+ return ids;
1433
+ }
1434
+ function stripContractDeclarationLines(text) {
1435
+ return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
1436
+ }
1437
+
1438
+ // src/core/contractIndex.ts
1439
+ async function buildContractIndex(root, config) {
1440
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1441
+ const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
1442
+ const apiRoot = import_node_path12.default.join(contractsRoot, "api");
1443
+ const dbRoot = import_node_path12.default.join(contractsRoot, "db");
1444
+ const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1445
+ collectUiContractFiles(uiRoot),
1446
+ collectApiContractFiles(apiRoot),
1447
+ collectDbContractFiles(dbRoot)
1448
+ ]);
1449
+ const index = {
1450
+ ids: /* @__PURE__ */ new Set(),
1451
+ idToFiles: /* @__PURE__ */ new Map(),
1452
+ files: { ui: uiFiles, api: apiFiles, db: dbFiles }
1453
+ };
1454
+ await indexContractFiles(uiFiles, index);
1455
+ await indexContractFiles(apiFiles, index);
1456
+ await indexContractFiles(dbFiles, index);
1457
+ return index;
1458
+ }
1459
+ async function indexContractFiles(files, index) {
1460
+ for (const file of files) {
1461
+ const text = await (0, import_promises10.readFile)(file, "utf-8");
1462
+ extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1463
+ }
1464
+ }
1465
+ function record(index, id, file) {
1466
+ index.ids.add(id);
1467
+ const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
1468
+ current.add(file);
1469
+ index.idToFiles.set(id, current);
1470
+ }
1471
+
1472
+ // src/core/ids.ts
1473
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
1474
+ var STRICT_ID_PATTERNS = {
1475
+ SPEC: /\bSPEC-\d{4}\b/g,
1476
+ BR: /\bBR-\d{4}\b/g,
1477
+ SC: /\bSC-\d{4}\b/g,
1478
+ UI: /\bUI-\d{4}\b/g,
1479
+ API: /\bAPI-\d{4}\b/g,
1480
+ DB: /\bDB-\d{4}\b/g,
1481
+ ADR: /\bADR-\d{4}\b/g
1482
+ };
1483
+ var LOOSE_ID_PATTERNS = {
1484
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
1485
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
1486
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
1487
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
1488
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
1489
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
1490
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
1491
+ };
1492
+ function extractIds(text, prefix) {
1493
+ const pattern = STRICT_ID_PATTERNS[prefix];
1494
+ const matches = text.match(pattern);
1495
+ return unique2(matches ?? []);
1496
+ }
1497
+ function extractAllIds(text) {
1498
+ const all = [];
1499
+ ID_PREFIXES.forEach((prefix) => {
1500
+ all.push(...extractIds(text, prefix));
1501
+ });
1502
+ return unique2(all);
1503
+ }
1504
+ function extractInvalidIds(text, prefixes) {
1505
+ const invalid = [];
1506
+ for (const prefix of prefixes) {
1507
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
1508
+ for (const candidate of candidates) {
1509
+ if (!isValidId(candidate, prefix)) {
1510
+ invalid.push(candidate);
1511
+ }
1512
+ }
1513
+ }
1514
+ return unique2(invalid);
1515
+ }
1516
+ function unique2(values) {
1517
+ return Array.from(new Set(values));
1518
+ }
1519
+ function isValidId(value, prefix) {
1520
+ const pattern = STRICT_ID_PATTERNS[prefix];
1521
+ const strict = new RegExp(pattern.source);
1522
+ return strict.test(value);
1523
+ }
1524
+
1525
+ // src/core/parse/contractRefs.ts
1526
+ var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
1527
+ function parseContractRefs(text, options = {}) {
1528
+ const linePattern = buildLinePattern(options);
1529
+ const lines = [];
1530
+ for (const match of text.matchAll(linePattern)) {
1531
+ lines.push((match[1] ?? "").trim());
1532
+ }
1533
+ const ids = [];
1534
+ const invalidTokens = [];
1535
+ let hasNone = false;
1536
+ for (const line of lines) {
1537
+ if (line.length === 0) {
1538
+ invalidTokens.push("(empty)");
1306
1539
  continue;
1307
1540
  }
1308
- for (const scId of scIds) {
1309
- const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
1310
- current.add(file);
1311
- refs.set(scId, current);
1541
+ const tokens = line.split(",").map((token) => token.trim());
1542
+ for (const token of tokens) {
1543
+ if (token.length === 0) {
1544
+ invalidTokens.push("(empty)");
1545
+ continue;
1546
+ }
1547
+ if (token === "none") {
1548
+ hasNone = true;
1549
+ continue;
1550
+ }
1551
+ if (CONTRACT_REF_ID_RE.test(token)) {
1552
+ ids.push(token);
1553
+ continue;
1554
+ }
1555
+ invalidTokens.push(token);
1312
1556
  }
1313
1557
  }
1314
1558
  return {
1315
- refs,
1316
- scan: {
1317
- globs: normalizedGlobs,
1318
- excludeGlobs: mergedExcludeGlobs,
1319
- matchedFileCount: normalizedFiles.length
1320
- }
1559
+ lines,
1560
+ ids: unique3(ids),
1561
+ invalidTokens: unique3(invalidTokens),
1562
+ hasNone
1321
1563
  };
1322
1564
  }
1323
- function buildScCoverage(scIds, refs) {
1324
- const sortedScIds = toSortedArray(scIds);
1325
- const refsRecord = {};
1326
- const missingIds = [];
1327
- let covered = 0;
1328
- for (const scId of sortedScIds) {
1329
- const files = refs.get(scId);
1330
- const sortedFiles = files ? toSortedArray(files) : [];
1331
- refsRecord[scId] = sortedFiles;
1332
- if (sortedFiles.length === 0) {
1333
- missingIds.push(scId);
1334
- } else {
1335
- covered += 1;
1336
- }
1337
- }
1338
- return {
1339
- total: sortedScIds.length,
1340
- covered,
1341
- missing: missingIds.length,
1342
- missingIds,
1343
- refs: refsRecord
1344
- };
1565
+ function buildLinePattern(options) {
1566
+ const prefix = options.allowCommentPrefix ? "#" : "";
1567
+ return new RegExp(
1568
+ `^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
1569
+ "gm"
1570
+ );
1345
1571
  }
1346
- function toSortedArray(values) {
1347
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
1572
+ function unique3(values) {
1573
+ return Array.from(new Set(values));
1348
1574
  }
1349
- function normalizeGlobs(globs) {
1350
- return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1575
+
1576
+ // src/core/parse/markdown.ts
1577
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1578
+ function parseHeadings(md) {
1579
+ const lines = md.split(/\r?\n/);
1580
+ const headings = [];
1581
+ for (let i = 0; i < lines.length; i++) {
1582
+ const line = lines[i] ?? "";
1583
+ const match = line.match(HEADING_RE);
1584
+ if (!match) continue;
1585
+ const levelToken = match[1];
1586
+ const title = match[2];
1587
+ if (!levelToken || !title) continue;
1588
+ headings.push({
1589
+ level: levelToken.length,
1590
+ title: title.trim(),
1591
+ line: i + 1
1592
+ });
1593
+ }
1594
+ return headings;
1351
1595
  }
1352
- function formatError3(error2) {
1353
- if (error2 instanceof Error) {
1354
- return error2.message;
1596
+ function extractH2Sections(md) {
1597
+ const lines = md.split(/\r?\n/);
1598
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1599
+ const sections = /* @__PURE__ */ new Map();
1600
+ for (let i = 0; i < headings.length; i++) {
1601
+ const current = headings[i];
1602
+ if (!current) continue;
1603
+ const next = headings[i + 1];
1604
+ const startLine = current.line + 1;
1605
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1606
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1607
+ sections.set(current.title.trim(), {
1608
+ title: current.title.trim(),
1609
+ startLine,
1610
+ endLine,
1611
+ body
1612
+ });
1355
1613
  }
1356
- return String(error2);
1614
+ return sections;
1357
1615
  }
1358
1616
 
1359
- // src/core/version.ts
1360
- var import_promises8 = require("fs/promises");
1361
- var import_node_path10 = __toESM(require("path"), 1);
1362
- var import_node_url2 = require("url");
1363
- async function resolveToolVersion() {
1364
- if ("0.5.2".length > 0) {
1365
- return "0.5.2";
1617
+ // src/core/parse/spec.ts
1618
+ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1619
+ var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1620
+ var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1621
+ var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1622
+ var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1623
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1624
+ function parseSpec(md, file) {
1625
+ const headings = parseHeadings(md);
1626
+ const h1 = headings.find((heading) => heading.level === 1);
1627
+ const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1628
+ const sections = extractH2Sections(md);
1629
+ const sectionNames = new Set(Array.from(sections.keys()));
1630
+ const brSection = sections.get(BR_SECTION_TITLE);
1631
+ const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1632
+ const startLine = brSection?.startLine ?? 1;
1633
+ const brs = [];
1634
+ const brsWithoutPriority = [];
1635
+ const brsWithInvalidPriority = [];
1636
+ for (let i = 0; i < brLines.length; i++) {
1637
+ const lineText = brLines[i] ?? "";
1638
+ const lineNumber = startLine + i;
1639
+ const validMatch = lineText.match(BR_LINE_RE);
1640
+ if (validMatch) {
1641
+ const id = validMatch[1];
1642
+ const priority = validMatch[2];
1643
+ const text = validMatch[3];
1644
+ if (!id || !priority || !text) continue;
1645
+ brs.push({
1646
+ id,
1647
+ priority,
1648
+ text: text.trim(),
1649
+ line: lineNumber
1650
+ });
1651
+ continue;
1652
+ }
1653
+ const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
1654
+ if (anyPriorityMatch) {
1655
+ const id = anyPriorityMatch[1];
1656
+ const priority = anyPriorityMatch[2];
1657
+ const text = anyPriorityMatch[3];
1658
+ if (!id || !priority || !text) continue;
1659
+ if (!VALID_PRIORITIES.has(priority)) {
1660
+ brsWithInvalidPriority.push({
1661
+ id,
1662
+ priority,
1663
+ text: text.trim(),
1664
+ line: lineNumber
1665
+ });
1666
+ }
1667
+ continue;
1668
+ }
1669
+ const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
1670
+ if (noPriorityMatch) {
1671
+ const id = noPriorityMatch[1];
1672
+ const text = noPriorityMatch[2];
1673
+ if (!id || !text) continue;
1674
+ brsWithoutPriority.push({
1675
+ id,
1676
+ text: text.trim(),
1677
+ line: lineNumber
1678
+ });
1679
+ }
1366
1680
  }
1367
- try {
1368
- const packagePath = resolvePackageJsonPath();
1369
- const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
1370
- const parsed = JSON.parse(raw);
1371
- const version = typeof parsed.version === "string" ? parsed.version : "";
1372
- return version.length > 0 ? version : "unknown";
1373
- } catch {
1374
- return "unknown";
1681
+ const parsed = {
1682
+ file,
1683
+ sections: sectionNames,
1684
+ brs,
1685
+ brsWithoutPriority,
1686
+ brsWithInvalidPriority,
1687
+ contractRefs: parseContractRefs(md)
1688
+ };
1689
+ if (specId) {
1690
+ parsed.specId = specId;
1375
1691
  }
1376
- }
1377
- function resolvePackageJsonPath() {
1378
- const base = __filename;
1379
- const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1380
- return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
1692
+ return parsed;
1381
1693
  }
1382
1694
 
1383
1695
  // src/core/validators/contracts.ts
1384
- var import_promises9 = require("fs/promises");
1385
- var import_node_path12 = __toESM(require("path"), 1);
1696
+ var import_promises11 = require("fs/promises");
1697
+ var import_node_path14 = __toESM(require("path"), 1);
1386
1698
 
1387
1699
  // src/core/contracts.ts
1388
- var import_node_path11 = __toESM(require("path"), 1);
1700
+ var import_node_path13 = __toESM(require("path"), 1);
1389
1701
  var import_yaml2 = require("yaml");
1390
1702
  function parseStructuredContract(file, text) {
1391
- const ext = import_node_path11.default.extname(file).toLowerCase();
1703
+ const ext = import_node_path13.default.extname(file).toLowerCase();
1392
1704
  if (ext === ".json") {
1393
1705
  return JSON.parse(text);
1394
1706
  }
@@ -1408,9 +1720,9 @@ var SQL_DANGEROUS_PATTERNS = [
1408
1720
  async function validateContracts(root, config) {
1409
1721
  const issues = [];
1410
1722
  const contractsRoot = resolvePath(root, config, "contractsDir");
1411
- issues.push(...await validateUiContracts(import_node_path12.default.join(contractsRoot, "ui")));
1412
- issues.push(...await validateApiContracts(import_node_path12.default.join(contractsRoot, "api")));
1413
- issues.push(...await validateDbContracts(import_node_path12.default.join(contractsRoot, "db")));
1723
+ issues.push(...await validateUiContracts(import_node_path14.default.join(contractsRoot, "ui")));
1724
+ issues.push(...await validateApiContracts(import_node_path14.default.join(contractsRoot, "api")));
1725
+ issues.push(...await validateDbContracts(import_node_path14.default.join(contractsRoot, "db")));
1414
1726
  const contractIndex = await buildContractIndex(root, config);
1415
1727
  issues.push(...validateDuplicateContractIds(contractIndex));
1416
1728
  return issues;
@@ -1430,7 +1742,7 @@ async function validateUiContracts(uiRoot) {
1430
1742
  }
1431
1743
  const issues = [];
1432
1744
  for (const file of files) {
1433
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1745
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1434
1746
  const invalidIds = extractInvalidIds(text, [
1435
1747
  "SPEC",
1436
1748
  "BR",
@@ -1485,7 +1797,7 @@ async function validateApiContracts(apiRoot) {
1485
1797
  }
1486
1798
  const issues = [];
1487
1799
  for (const file of files) {
1488
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1800
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1489
1801
  const invalidIds = extractInvalidIds(text, [
1490
1802
  "SPEC",
1491
1803
  "BR",
@@ -1553,7 +1865,7 @@ async function validateDbContracts(dbRoot) {
1553
1865
  }
1554
1866
  const issues = [];
1555
1867
  for (const file of files) {
1556
- const text = await (0, import_promises9.readFile)(file, "utf-8");
1868
+ const text = await (0, import_promises11.readFile)(file, "utf-8");
1557
1869
  const invalidIds = extractInvalidIds(text, [
1558
1870
  "SPEC",
1559
1871
  "BR",
@@ -1692,8 +2004,8 @@ function issue(code, message, severity, file, rule, refs) {
1692
2004
  }
1693
2005
 
1694
2006
  // src/core/validators/delta.ts
1695
- var import_promises10 = require("fs/promises");
1696
- var import_node_path13 = __toESM(require("path"), 1);
2007
+ var import_promises12 = require("fs/promises");
2008
+ var import_node_path15 = __toESM(require("path"), 1);
1697
2009
  var SECTION_RE = /^##\s+変更区分/m;
1698
2010
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1699
2011
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1707,10 +2019,10 @@ async function validateDeltas(root, config) {
1707
2019
  }
1708
2020
  const issues = [];
1709
2021
  for (const pack of packs) {
1710
- const deltaPath = import_node_path13.default.join(pack, "delta.md");
2022
+ const deltaPath = import_node_path15.default.join(pack, "delta.md");
1711
2023
  let text;
1712
2024
  try {
1713
- text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
2025
+ text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
1714
2026
  } catch (error2) {
1715
2027
  if (isMissingFileError2(error2)) {
1716
2028
  issues.push(
@@ -1782,8 +2094,8 @@ function issue2(code, message, severity, file, rule, refs) {
1782
2094
  }
1783
2095
 
1784
2096
  // src/core/validators/ids.ts
1785
- var import_promises11 = require("fs/promises");
1786
- var import_node_path14 = __toESM(require("path"), 1);
2097
+ var import_promises13 = require("fs/promises");
2098
+ var import_node_path16 = __toESM(require("path"), 1);
1787
2099
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1788
2100
  async function validateDefinedIds(root, config) {
1789
2101
  const issues = [];
@@ -1818,7 +2130,7 @@ async function validateDefinedIds(root, config) {
1818
2130
  }
1819
2131
  async function collectSpecDefinitionIds(files, out) {
1820
2132
  for (const file of files) {
1821
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2133
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1822
2134
  const parsed = parseSpec(text, file);
1823
2135
  if (parsed.specId) {
1824
2136
  recordId(out, parsed.specId, file);
@@ -1828,7 +2140,7 @@ async function collectSpecDefinitionIds(files, out) {
1828
2140
  }
1829
2141
  async function collectScenarioDefinitionIds(files, out) {
1830
2142
  for (const file of files) {
1831
- const text = await (0, import_promises11.readFile)(file, "utf-8");
2143
+ const text = await (0, import_promises13.readFile)(file, "utf-8");
1832
2144
  const { document, errors } = parseScenarioDocument(text, file);
1833
2145
  if (!document || errors.length > 0) {
1834
2146
  continue;
@@ -1849,7 +2161,7 @@ function recordId(out, id, file) {
1849
2161
  }
1850
2162
  function formatFileList(files, root) {
1851
2163
  return files.map((file) => {
1852
- const relative = import_node_path14.default.relative(root, file);
2164
+ const relative = import_node_path16.default.relative(root, file);
1853
2165
  return relative.length > 0 ? relative : file;
1854
2166
  }).join(", ");
1855
2167
  }
@@ -1872,7 +2184,7 @@ function issue3(code, message, severity, file, rule, refs) {
1872
2184
  }
1873
2185
 
1874
2186
  // src/core/validators/scenario.ts
1875
- var import_promises12 = require("fs/promises");
2187
+ var import_promises14 = require("fs/promises");
1876
2188
  var GIVEN_PATTERN = /\bGiven\b/;
1877
2189
  var WHEN_PATTERN = /\bWhen\b/;
1878
2190
  var THEN_PATTERN = /\bThen\b/;
@@ -1898,7 +2210,7 @@ async function validateScenarios(root, config) {
1898
2210
  for (const entry of entries) {
1899
2211
  let text;
1900
2212
  try {
1901
- text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
2213
+ text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
1902
2214
  } catch (error2) {
1903
2215
  if (isMissingFileError3(error2)) {
1904
2216
  issues.push(
@@ -2068,7 +2380,7 @@ function isMissingFileError3(error2) {
2068
2380
  }
2069
2381
 
2070
2382
  // src/core/validators/spec.ts
2071
- var import_promises13 = require("fs/promises");
2383
+ var import_promises15 = require("fs/promises");
2072
2384
  async function validateSpecs(root, config) {
2073
2385
  const specsRoot = resolvePath(root, config, "specsDir");
2074
2386
  const entries = await collectSpecEntries(specsRoot);
@@ -2089,7 +2401,7 @@ async function validateSpecs(root, config) {
2089
2401
  for (const entry of entries) {
2090
2402
  let text;
2091
2403
  try {
2092
- text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
2404
+ text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
2093
2405
  } catch (error2) {
2094
2406
  if (isMissingFileError4(error2)) {
2095
2407
  issues.push(
@@ -2238,7 +2550,7 @@ function isMissingFileError4(error2) {
2238
2550
  }
2239
2551
 
2240
2552
  // src/core/validators/traceability.ts
2241
- var import_promises14 = require("fs/promises");
2553
+ var import_promises16 = require("fs/promises");
2242
2554
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2243
2555
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2244
2556
  async function validateTraceability(root, config) {
@@ -2258,7 +2570,7 @@ async function validateTraceability(root, config) {
2258
2570
  const contractIndex = await buildContractIndex(root, config);
2259
2571
  const contractIds = contractIndex.ids;
2260
2572
  for (const file of specFiles) {
2261
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2573
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2262
2574
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2263
2575
  const parsed = parseSpec(text, file);
2264
2576
  if (parsed.specId) {
@@ -2331,7 +2643,7 @@ async function validateTraceability(root, config) {
2331
2643
  }
2332
2644
  }
2333
2645
  for (const file of scenarioFiles) {
2334
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2646
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2335
2647
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2336
2648
  const scenarioContractRefs = parseContractRefs(text, {
2337
2649
  allowCommentPrefix: true
@@ -2653,7 +2965,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
2653
2965
  const pattern = buildIdPattern(Array.from(upstreamIds));
2654
2966
  let found = false;
2655
2967
  for (const file of targetFiles) {
2656
- const text = await (0, import_promises14.readFile)(file, "utf-8");
2968
+ const text = await (0, import_promises16.readFile)(file, "utf-8");
2657
2969
  if (pattern.test(text)) {
2658
2970
  found = true;
2659
2971
  break;
@@ -2740,15 +3052,15 @@ function countIssues(issues) {
2740
3052
  // src/core/report.ts
2741
3053
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
2742
3054
  async function createReportData(root, validation, configResult) {
2743
- const resolvedRoot = import_node_path15.default.resolve(root);
3055
+ const resolvedRoot = import_node_path17.default.resolve(root);
2744
3056
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2745
3057
  const config = resolved.config;
2746
3058
  const configPath = resolved.configPath;
2747
3059
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2748
3060
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2749
- const apiRoot = import_node_path15.default.join(contractsRoot, "api");
2750
- const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
2751
- const dbRoot = import_node_path15.default.join(contractsRoot, "db");
3061
+ const apiRoot = import_node_path17.default.join(contractsRoot, "api");
3062
+ const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
3063
+ const dbRoot = import_node_path17.default.join(contractsRoot, "db");
2752
3064
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2753
3065
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2754
3066
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3063,7 +3375,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3063
3375
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3064
3376
  }
3065
3377
  for (const file of specFiles) {
3066
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3378
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3067
3379
  const parsed = parseSpec(text, file);
3068
3380
  const specKey = parsed.specId;
3069
3381
  if (!specKey) {
@@ -3104,7 +3416,7 @@ async function collectIds(files) {
3104
3416
  DB: /* @__PURE__ */ new Set()
3105
3417
  };
3106
3418
  for (const file of files) {
3107
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3419
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3108
3420
  for (const prefix of ID_PREFIXES2) {
3109
3421
  const ids = extractIds(text, prefix);
3110
3422
  ids.forEach((id) => result[prefix].add(id));
@@ -3122,7 +3434,7 @@ async function collectIds(files) {
3122
3434
  async function collectUpstreamIds(files) {
3123
3435
  const ids = /* @__PURE__ */ new Set();
3124
3436
  for (const file of files) {
3125
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3437
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3126
3438
  extractAllIds(text).forEach((id) => ids.add(id));
3127
3439
  }
3128
3440
  return ids;
@@ -3143,7 +3455,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3143
3455
  }
3144
3456
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3145
3457
  for (const file of targetFiles) {
3146
- const text = await (0, import_promises15.readFile)(file, "utf-8");
3458
+ const text = await (0, import_promises17.readFile)(file, "utf-8");
3147
3459
  if (pattern.test(text)) {
3148
3460
  return true;
3149
3461
  }
@@ -3235,7 +3547,7 @@ function buildHotspots(issues) {
3235
3547
 
3236
3548
  // src/cli/commands/report.ts
3237
3549
  async function runReport(options) {
3238
- const root = import_node_path16.default.resolve(options.root);
3550
+ const root = import_node_path18.default.resolve(options.root);
3239
3551
  const configResult = await loadConfig(root);
3240
3552
  let validation;
3241
3553
  if (options.runValidate) {
@@ -3252,7 +3564,7 @@ async function runReport(options) {
3252
3564
  validation = normalized;
3253
3565
  } else {
3254
3566
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3255
- const inputPath = import_node_path16.default.isAbsolute(input) ? input : import_node_path16.default.resolve(root, input);
3567
+ const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
3256
3568
  try {
3257
3569
  validation = await readValidationResult(inputPath);
3258
3570
  } catch (err) {
@@ -3278,11 +3590,11 @@ async function runReport(options) {
3278
3590
  const data = await createReportData(root, validation, configResult);
3279
3591
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3280
3592
  const outRoot = resolvePath(root, configResult.config, "outDir");
3281
- const defaultOut = options.format === "json" ? import_node_path16.default.join(outRoot, "report.json") : import_node_path16.default.join(outRoot, "report.md");
3593
+ const defaultOut = options.format === "json" ? import_node_path18.default.join(outRoot, "report.json") : import_node_path18.default.join(outRoot, "report.md");
3282
3594
  const out = options.outPath ?? defaultOut;
3283
- const outPath = import_node_path16.default.isAbsolute(out) ? out : import_node_path16.default.resolve(root, out);
3284
- await (0, import_promises16.mkdir)(import_node_path16.default.dirname(outPath), { recursive: true });
3285
- await (0, import_promises16.writeFile)(outPath, `${output}
3595
+ const outPath = import_node_path18.default.isAbsolute(out) ? out : import_node_path18.default.resolve(root, out);
3596
+ await (0, import_promises18.mkdir)(import_node_path18.default.dirname(outPath), { recursive: true });
3597
+ await (0, import_promises18.writeFile)(outPath, `${output}
3286
3598
  `, "utf-8");
3287
3599
  info(
3288
3600
  `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
@@ -3290,7 +3602,7 @@ async function runReport(options) {
3290
3602
  info(`wrote report: ${outPath}`);
3291
3603
  }
3292
3604
  async function readValidationResult(inputPath) {
3293
- const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
3605
+ const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
3294
3606
  const parsed = JSON.parse(raw);
3295
3607
  if (!isValidationResult(parsed)) {
3296
3608
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -3346,15 +3658,15 @@ function isMissingFileError5(error2) {
3346
3658
  return record2.code === "ENOENT";
3347
3659
  }
3348
3660
  async function writeValidationResult(root, outputPath, result) {
3349
- const abs = import_node_path16.default.isAbsolute(outputPath) ? outputPath : import_node_path16.default.resolve(root, outputPath);
3350
- await (0, import_promises16.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
3351
- await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3661
+ const abs = import_node_path18.default.isAbsolute(outputPath) ? outputPath : import_node_path18.default.resolve(root, outputPath);
3662
+ await (0, import_promises18.mkdir)(import_node_path18.default.dirname(abs), { recursive: true });
3663
+ await (0, import_promises18.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3352
3664
  `, "utf-8");
3353
3665
  }
3354
3666
 
3355
3667
  // src/cli/commands/validate.ts
3356
- var import_promises17 = require("fs/promises");
3357
- var import_node_path17 = __toESM(require("path"), 1);
3668
+ var import_promises19 = require("fs/promises");
3669
+ var import_node_path19 = __toESM(require("path"), 1);
3358
3670
 
3359
3671
  // src/cli/lib/failOn.ts
3360
3672
  function shouldFail(result, failOn) {
@@ -3369,7 +3681,7 @@ function shouldFail(result, failOn) {
3369
3681
 
3370
3682
  // src/cli/commands/validate.ts
3371
3683
  async function runValidate(options) {
3372
- const root = import_node_path17.default.resolve(options.root);
3684
+ const root = import_node_path19.default.resolve(options.root);
3373
3685
  const configResult = await loadConfig(root);
3374
3686
  const result = await validateProject(root, configResult);
3375
3687
  const normalized = normalizeValidationResult(root, result);
@@ -3486,12 +3798,12 @@ function issueKey(issue7) {
3486
3798
  }
3487
3799
  async function emitJson(result, root, jsonPath) {
3488
3800
  const abs = resolveJsonPath(root, jsonPath);
3489
- await (0, import_promises17.mkdir)(import_node_path17.default.dirname(abs), { recursive: true });
3490
- await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3801
+ await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
3802
+ await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
3491
3803
  `, "utf-8");
3492
3804
  }
3493
3805
  function resolveJsonPath(root, jsonPath) {
3494
- return import_node_path17.default.isAbsolute(jsonPath) ? jsonPath : import_node_path17.default.resolve(root, jsonPath);
3806
+ return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
3495
3807
  }
3496
3808
  var GITHUB_ANNOTATION_LIMIT = 100;
3497
3809
 
@@ -3506,6 +3818,7 @@ function parseArgs(argv, cwd) {
3506
3818
  dryRun: false,
3507
3819
  reportFormat: "md",
3508
3820
  reportRunValidate: false,
3821
+ doctorFormat: "text",
3509
3822
  validateFormat: "text",
3510
3823
  strict: false,
3511
3824
  help: false
@@ -3558,7 +3871,11 @@ function parseArgs(argv, cwd) {
3558
3871
  {
3559
3872
  const next = args[i + 1];
3560
3873
  if (next) {
3561
- options.reportOut = next;
3874
+ if (command === "doctor") {
3875
+ options.doctorOut = next;
3876
+ } else {
3877
+ options.reportOut = next;
3878
+ }
3562
3879
  }
3563
3880
  }
3564
3881
  i += 1;
@@ -3601,6 +3918,12 @@ function applyFormatOption(command, value, options) {
3601
3918
  }
3602
3919
  return;
3603
3920
  }
3921
+ if (command === "doctor") {
3922
+ if (value === "text" || value === "json") {
3923
+ options.doctorFormat = value;
3924
+ }
3925
+ return;
3926
+ }
3604
3927
  if (value === "md" || value === "json") {
3605
3928
  options.reportFormat = value;
3606
3929
  }
@@ -3648,6 +3971,18 @@ async function run(argv, cwd) {
3648
3971
  });
3649
3972
  }
3650
3973
  return;
3974
+ case "doctor":
3975
+ {
3976
+ const exitCode = await runDoctor({
3977
+ root: options.root,
3978
+ rootExplicit: options.rootExplicit,
3979
+ format: options.doctorFormat,
3980
+ ...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
3981
+ ...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
3982
+ });
3983
+ process.exitCode = exitCode;
3984
+ }
3985
+ return;
3651
3986
  default:
3652
3987
  error(`Unknown command: ${command}`);
3653
3988
  info(usage());
@@ -3661,6 +3996,7 @@ Commands:
3661
3996
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
3662
3997
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
3663
3998
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
3999
+ doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
3664
4000
 
3665
4001
  Options:
3666
4002
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -3670,9 +4006,11 @@ Options:
3670
4006
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
3671
4007
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
3672
4008
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
4009
+ --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
3673
4010
  --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
3674
4011
  --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
3675
- --out <path> report: \u51FA\u529B\u5148
4012
+ --fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
4013
+ --out <path> report/doctor: \u51FA\u529B\u5148
3676
4014
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
3677
4015
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
3678
4016
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A